2016-08-03 18:00:07 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2016-08-03 18:00:07 -04:00
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
perf: switch angular to use StaticInjector instead of ReflectiveInjector
This change allows ReflectiveInjector to be tree shaken resulting
in not needed Reflect polyfil and smaller bundles.
Code savings for HelloWorld using Closure:
Reflective: bundle.js: 105,864(34,190 gzip)
Static: bundle.js: 154,889(33,555 gzip)
645( 2%)
BREAKING CHANGE:
`platformXXXX()` no longer accepts providers which depend on reflection.
Specifically the method signature when from `Provider[]` to
`StaticProvider[]`.
Example:
Before:
```
[
MyClass,
{provide: ClassA, useClass: SubClassA}
]
```
After:
```
[
{provide: MyClass, deps: [Dep1,...]},
{provide: ClassA, useClass: SubClassA, deps: [Dep1,...]}
]
```
NOTE: This only applies to platform creation and providers for the JIT
compiler. It does not apply to `@Compotent` or `@NgModule` provides
declarations.
Benchpress note: Previously Benchpress also supported reflective
provides, which now require static providers.
DEPRECATION:
- `ReflectiveInjector` is now deprecated as it will be remove. Use
`Injector.create` as a replacement.
closes #18496
2017-08-03 15:33:29 -04:00
|
|
|
import {ChromeDriverExtension, Injector, Options, WebDriverAdapter, WebDriverExtension} from '../../index';
|
2015-05-27 17:57:54 -04:00
|
|
|
import {TraceEventFactory} from '../trace_event_factory';
|
|
|
|
|
2017-12-16 17:42:55 -05:00
|
|
|
{
|
2015-05-27 17:57:54 -04:00
|
|
|
describe('chrome driver extension', () => {
|
2016-11-12 08:08:58 -05:00
|
|
|
const CHROME45_USER_AGENT =
|
2015-09-03 14:48:24 -04:00
|
|
|
'"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2499.0 Safari/537.36"';
|
|
|
|
|
2016-11-12 08:08:58 -05:00
|
|
|
let log: any[];
|
|
|
|
let extension: ChromeDriverExtension;
|
2015-05-27 17:57:54 -04:00
|
|
|
|
2016-11-12 08:08:58 -05:00
|
|
|
const blinkEvents = new TraceEventFactory('blink.console', 'pid0');
|
|
|
|
const v8Events = new TraceEventFactory('v8', 'pid0');
|
|
|
|
const v8EventsOtherProcess = new TraceEventFactory('v8', 'pid1');
|
|
|
|
const chromeTimelineEvents =
|
2015-05-27 17:57:54 -04:00
|
|
|
new TraceEventFactory('disabled-by-default-devtools.timeline', 'pid0');
|
2016-11-12 08:08:58 -05:00
|
|
|
const chrome45TimelineEvents = new TraceEventFactory('devtools.timeline', 'pid0');
|
|
|
|
const chromeTimelineV8Events = new TraceEventFactory('devtools.timeline,v8', 'pid0');
|
|
|
|
const chromeBlinkTimelineEvents = new TraceEventFactory('blink,devtools.timeline', 'pid0');
|
|
|
|
const chromeBlinkUserTimingEvents = new TraceEventFactory('blink.user_timing', 'pid0');
|
|
|
|
const benchmarkEvents = new TraceEventFactory('benchmark', 'pid0');
|
|
|
|
const normEvents = new TraceEventFactory('timeline', 'pid0');
|
2015-05-27 17:57:54 -04:00
|
|
|
|
2016-08-03 18:00:07 -04:00
|
|
|
function createExtension(
|
2020-04-13 19:40:21 -04:00
|
|
|
perfRecords: any[]|null = null, userAgent: string|null = null,
|
2016-08-03 18:00:07 -04:00
|
|
|
messageMethod = 'Tracing.dataCollected'): WebDriverExtension {
|
2016-09-30 12:26:53 -04:00
|
|
|
if (!perfRecords) {
|
2015-05-27 17:57:54 -04:00
|
|
|
perfRecords = [];
|
|
|
|
}
|
2017-03-02 12:37:01 -05:00
|
|
|
if (userAgent == null) {
|
2016-09-15 10:49:05 -04:00
|
|
|
userAgent = CHROME45_USER_AGENT;
|
2015-09-03 14:48:24 -04:00
|
|
|
}
|
2015-05-27 17:57:54 -04:00
|
|
|
log = [];
|
perf: switch angular to use StaticInjector instead of ReflectiveInjector
This change allows ReflectiveInjector to be tree shaken resulting
in not needed Reflect polyfil and smaller bundles.
Code savings for HelloWorld using Closure:
Reflective: bundle.js: 105,864(34,190 gzip)
Static: bundle.js: 154,889(33,555 gzip)
645( 2%)
BREAKING CHANGE:
`platformXXXX()` no longer accepts providers which depend on reflection.
Specifically the method signature when from `Provider[]` to
`StaticProvider[]`.
Example:
Before:
```
[
MyClass,
{provide: ClassA, useClass: SubClassA}
]
```
After:
```
[
{provide: MyClass, deps: [Dep1,...]},
{provide: ClassA, useClass: SubClassA, deps: [Dep1,...]}
]
```
NOTE: This only applies to platform creation and providers for the JIT
compiler. It does not apply to `@Compotent` or `@NgModule` provides
declarations.
Benchpress note: Previously Benchpress also supported reflective
provides, which now require static providers.
DEPRECATION:
- `ReflectiveInjector` is now deprecated as it will be remove. Use
`Injector.create` as a replacement.
closes #18496
2017-08-03 15:33:29 -04:00
|
|
|
extension = Injector
|
|
|
|
.create([
|
2016-08-03 18:00:07 -04:00
|
|
|
ChromeDriverExtension.PROVIDERS, {
|
|
|
|
provide: WebDriverAdapter,
|
|
|
|
useValue: new MockDriverAdapter(log, perfRecords, messageMethod)
|
|
|
|
},
|
2018-12-07 13:23:43 -05:00
|
|
|
{provide: Options.USER_AGENT, useValue: userAgent},
|
|
|
|
{provide: Options.RAW_PERFLOG_PATH, useValue: null}
|
2016-08-03 18:00:07 -04:00
|
|
|
])
|
|
|
|
.get(ChromeDriverExtension);
|
2015-05-27 17:57:54 -04:00
|
|
|
return extension;
|
|
|
|
}
|
|
|
|
|
2021-05-23 16:16:02 -04:00
|
|
|
it('should force gc via window.gc()', done => {
|
|
|
|
createExtension().gc().then((_) => {
|
|
|
|
expect(log).toEqual([['executeScript', 'window.gc()']]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-05-27 17:57:54 -04:00
|
|
|
|
2018-05-24 16:58:00 -04:00
|
|
|
it('should clear the perf logs and mark the timeline via performance.mark() on the first call',
|
2021-05-23 16:16:02 -04:00
|
|
|
done => {
|
2017-04-17 12:18:35 -04:00
|
|
|
createExtension().timeBegin('someName').then(() => {
|
2018-05-24 16:58:00 -04:00
|
|
|
expect(log).toEqual([
|
|
|
|
['logs', 'performance'], ['executeScript', `performance.mark('someName-bpstart');`]
|
|
|
|
]);
|
2021-05-23 16:16:02 -04:00
|
|
|
done();
|
2016-08-03 18:00:07 -04:00
|
|
|
});
|
2021-05-23 16:16:02 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should mark the timeline via performance.mark() on the second call', done => {
|
|
|
|
const ext = createExtension();
|
|
|
|
ext.timeBegin('someName')
|
|
|
|
.then((_) => {
|
|
|
|
log.splice(0, log.length);
|
|
|
|
ext.timeBegin('someName');
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
expect(log).toEqual([['executeScript', `performance.mark('someName-bpstart');`]]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-05-27 17:57:54 -04:00
|
|
|
|
2021-05-23 16:16:02 -04:00
|
|
|
it('should mark the timeline via performance.mark()', done => {
|
|
|
|
createExtension().timeEnd('someName', null).then((_) => {
|
|
|
|
expect(log).toEqual([['executeScript', `performance.mark('someName-bpend');`]]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should mark the timeline via performance.mark() with start and end of a test', done => {
|
|
|
|
createExtension().timeEnd('name1', 'name2').then((_) => {
|
|
|
|
expect(log).toEqual([
|
|
|
|
['executeScript', `performance.mark('name1-bpend');performance.mark('name2-bpstart');`]
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should normalize times to ms and forward ph and pid event properties', done => {
|
|
|
|
createExtension([chromeTimelineV8Events.complete('FunctionCall', 1100, 5500, null)])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.complete('script', 1.1, 5.5, null),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should normalize "tdur" to "dur"', done => {
|
|
|
|
const event: any = chromeTimelineV8Events.create('X', 'FunctionCall', 1100, null);
|
|
|
|
event['tdur'] = 5500;
|
|
|
|
createExtension([event]).readPerfLog().then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.complete('script', 1.1, 5.5, null),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report FunctionCall events as "script"', done => {
|
|
|
|
createExtension([chromeTimelineV8Events.start('FunctionCall', 0)])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.start('script', 0),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report EvaluateScript events as "script"', done => {
|
|
|
|
createExtension([chromeTimelineV8Events.start('EvaluateScript', 0)])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.start('script', 0),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report minor gc', done => {
|
|
|
|
createExtension([
|
|
|
|
chromeTimelineV8Events.start('MinorGC', 1000, {'usedHeapSizeBefore': 1000}),
|
|
|
|
chromeTimelineV8Events.end('MinorGC', 2000, {'usedHeapSizeAfter': 0}),
|
|
|
|
])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events.length).toEqual(2);
|
|
|
|
expect(events[0]).toEqual(
|
|
|
|
normEvents.start('gc', 1.0, {'usedHeapSize': 1000, 'majorGc': false}));
|
|
|
|
expect(events[1]).toEqual(
|
|
|
|
normEvents.end('gc', 2.0, {'usedHeapSize': 0, 'majorGc': false}));
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report major gc', done => {
|
|
|
|
createExtension(
|
|
|
|
[
|
|
|
|
chromeTimelineV8Events.start('MajorGC', 1000, {'usedHeapSizeBefore': 1000}),
|
|
|
|
chromeTimelineV8Events.end('MajorGC', 2000, {'usedHeapSizeAfter': 0}),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events.length).toEqual(2);
|
|
|
|
expect(events[0]).toEqual(
|
|
|
|
normEvents.start('gc', 1.0, {'usedHeapSize': 1000, 'majorGc': true}));
|
|
|
|
expect(events[1]).toEqual(
|
|
|
|
normEvents.end('gc', 2.0, {'usedHeapSize': 0, 'majorGc': true}));
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-09-03 14:48:24 -04:00
|
|
|
|
2016-09-15 10:49:05 -04:00
|
|
|
['Layout', 'UpdateLayerTree', 'Paint'].forEach((recordType) => {
|
2021-05-23 16:16:02 -04:00
|
|
|
it(`should report ${recordType} as "render"`, done => {
|
|
|
|
createExtension(
|
|
|
|
[
|
|
|
|
chrome45TimelineEvents.start(recordType, 1234),
|
|
|
|
chrome45TimelineEvents.end(recordType, 2345)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.start('render', 1.234),
|
|
|
|
normEvents.end('render', 2.345),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2016-09-15 10:49:05 -04:00
|
|
|
});
|
2015-05-27 17:57:54 -04:00
|
|
|
|
2021-05-23 16:16:02 -04:00
|
|
|
it(`should report UpdateLayoutTree as "render"`, done => {
|
|
|
|
createExtension(
|
|
|
|
[
|
|
|
|
chromeBlinkTimelineEvents.start('UpdateLayoutTree', 1234),
|
|
|
|
chromeBlinkTimelineEvents.end('UpdateLayoutTree', 2345)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.start('render', 1.234),
|
|
|
|
normEvents.end('render', 2.345),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should ignore FunctionCalls from webdriver', done => {
|
|
|
|
createExtension([chromeTimelineV8Events.start(
|
|
|
|
'FunctionCall', 0, {'data': {'scriptName': 'InjectedScript'}})])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should ignore FunctionCalls with empty scriptName', done => {
|
|
|
|
createExtension(
|
|
|
|
[chromeTimelineV8Events.start('FunctionCall', 0, {'data': {'scriptName': ''}})])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report navigationStart', done => {
|
|
|
|
createExtension([chromeBlinkUserTimingEvents.instant('navigationStart', 1234)])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([normEvents.instant('navigationStart', 1.234)]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report receivedData', done => {
|
|
|
|
createExtension(
|
|
|
|
[chrome45TimelineEvents.instant(
|
|
|
|
'ResourceReceivedData', 1234, {'data': {'encodedDataLength': 987}})],
|
|
|
|
)
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual(
|
|
|
|
[normEvents.instant('receivedData', 1.234, {'encodedDataLength': 987})]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report sendRequest', done => {
|
|
|
|
createExtension(
|
|
|
|
[chrome45TimelineEvents.instant(
|
|
|
|
'ResourceSendRequest', 1234,
|
|
|
|
{'data': {'url': 'http://here', 'requestMethod': 'GET'}})],
|
|
|
|
)
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([normEvents.instant(
|
|
|
|
'sendRequest', 1.234, {'url': 'http://here', 'method': 'GET'})]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-09-03 14:48:24 -04:00
|
|
|
|
|
|
|
describe('readPerfLog (common)', () => {
|
2021-05-23 16:16:02 -04:00
|
|
|
it('should execute a dummy script before reading them', done => {
|
|
|
|
// TODO(tbosch): This seems to be a bug in ChromeDriver:
|
|
|
|
// Sometimes it does not report the newest events of the performance log
|
|
|
|
// to the WebDriver client unless a script is executed...
|
|
|
|
createExtension([]).readPerfLog().then((_) => {
|
|
|
|
expect(log).toEqual([['executeScript', '1+1'], ['logs', 'performance']]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-09-03 14:48:24 -04:00
|
|
|
|
|
|
|
['Rasterize', 'CompositeLayers'].forEach((recordType) => {
|
2021-05-23 16:16:02 -04:00
|
|
|
it(`should report ${recordType} as "render"`, done => {
|
|
|
|
createExtension(
|
|
|
|
[
|
|
|
|
chromeTimelineEvents.start(recordType, 1234),
|
|
|
|
chromeTimelineEvents.end(recordType, 2345)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.start('render', 1.234),
|
|
|
|
normEvents.end('render', 2.345),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-09-03 14:48:24 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('frame metrics', () => {
|
2021-05-23 16:16:02 -04:00
|
|
|
it('should report ImplThreadRenderingStats as frame event', done => {
|
|
|
|
createExtension([benchmarkEvents.instant(
|
|
|
|
'BenchmarkInstrumentation::ImplThreadRenderingStats', 1100,
|
|
|
|
{'data': {'frame_count': 1}})])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([
|
|
|
|
normEvents.instant('frame', 1.1),
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not report ImplThreadRenderingStats with zero frames', done => {
|
|
|
|
createExtension([benchmarkEvents.instant(
|
|
|
|
'BenchmarkInstrumentation::ImplThreadRenderingStats', 1100,
|
|
|
|
{'data': {'frame_count': 0}})])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw when ImplThreadRenderingStats contains more than one frame', done => {
|
|
|
|
createExtension([benchmarkEvents.instant(
|
|
|
|
'BenchmarkInstrumentation::ImplThreadRenderingStats', 1100,
|
|
|
|
{'data': {'frame_count': 2}})])
|
|
|
|
.readPerfLog()
|
|
|
|
.catch((err): any => {
|
|
|
|
expect(() => {
|
|
|
|
throw err;
|
|
|
|
}).toThrowError('multi-frame render stats not supported');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-09-03 14:48:24 -04:00
|
|
|
});
|
|
|
|
|
2021-05-23 16:16:02 -04:00
|
|
|
it('should report begin timestamps', done => {
|
|
|
|
createExtension([blinkEvents.create('S', 'someName', 1000)])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([normEvents.markStart('someName', 1.0)]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should report end timestamps', done => {
|
|
|
|
createExtension([blinkEvents.create('F', 'someName', 1000)])
|
|
|
|
.readPerfLog()
|
|
|
|
.then((events) => {
|
|
|
|
expect(events).toEqual([normEvents.markEnd('someName', 1.0)]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw an error on buffer overflow', done => {
|
|
|
|
createExtension(
|
|
|
|
[
|
|
|
|
chromeTimelineEvents.start('FunctionCall', 1234),
|
|
|
|
],
|
|
|
|
CHROME45_USER_AGENT, 'Tracing.bufferUsage')
|
|
|
|
.readPerfLog()
|
|
|
|
.catch((err): any => {
|
|
|
|
expect(() => {
|
|
|
|
throw err;
|
|
|
|
}).toThrowError('The DevTools trace buffer filled during the test!');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2015-05-27 17:57:54 -04:00
|
|
|
|
|
|
|
it('should match chrome browsers', () => {
|
|
|
|
expect(createExtension().supports({'browserName': 'chrome'})).toBe(true);
|
|
|
|
|
|
|
|
expect(createExtension().supports({'browserName': 'Chrome'})).toBe(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
class MockDriverAdapter extends WebDriverAdapter {
|
2015-08-28 14:29:19 -04:00
|
|
|
constructor(private _log: any[], private _events: any[], private _messageMethod: string) {
|
2015-05-27 17:57:54 -04:00
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
2016-08-26 19:34:08 -04:00
|
|
|
executeScript(script: string) {
|
2015-06-17 14:17:21 -04:00
|
|
|
this._log.push(['executeScript', script]);
|
2016-08-02 18:53:34 -04:00
|
|
|
return Promise.resolve(null);
|
2015-05-27 17:57:54 -04:00
|
|
|
}
|
|
|
|
|
2017-03-24 12:56:50 -04:00
|
|
|
logs(type: string): Promise<any[]> {
|
2015-06-17 14:17:21 -04:00
|
|
|
this._log.push(['logs', type]);
|
2015-05-27 17:57:54 -04:00
|
|
|
if (type === 'performance') {
|
2016-10-04 18:57:37 -04:00
|
|
|
return Promise.resolve(this._events.map(
|
|
|
|
(event) => ({
|
2016-10-19 16:42:39 -04:00
|
|
|
'message': JSON.stringify(
|
|
|
|
{'message': {'method': this._messageMethod, 'params': event}}, null, 2)
|
2016-10-04 18:57:37 -04:00
|
|
|
})));
|
2015-05-27 17:57:54 -04:00
|
|
|
} else {
|
2020-04-13 19:40:21 -04:00
|
|
|
return null!;
|
2015-05-27 17:57:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|