forked from SunGard-Labs/sg-protractor-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
memory.ts
242 lines (209 loc) · 7.55 KB
/
memory.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { dirname, resolve } from 'path';
import { execSync } from 'child_process';
import { createWriteStream, ensureDirSync, writeFileSync, WriteStream } from 'fs-extra';
import { promise, ProtractorBrowser } from 'protractor';
import { Observable, Observer, Subscriber } from 'rxjs';
import { BaseUtil } from './base';
const quiche = require('quiche');
export interface MemoryInfo {
jsHeapSizeLimit: number;
totalJSHeapSize: number;
usedJSHeapSize: number;
}
export interface PerformanceTestOptions {
/**
* Whether the performance test suite should be a
* focused jasmine test suite. Default is false
*/
focused?: boolean;
/**
* Milliseconds to sleep after GC before iterations start.
* Default is 2500
*/
initialPostGcSleep?: number;
/**
* Number of iterations to be performed.
* Default is 100
*/
iterations?: number;
/**
* Milliseconds to sleep before final GC after all iterations happened.
* Default is 1500
*/
finalPostTestSleep?: number;
/**
* Milliseconds to sleep between last GC and final memory measurement
* Default is 4500
*/
finalPostGcSleep?: number;
writeLogFile?: boolean;
writeCsvFile?: boolean;
generateGraph?: boolean;
/**
* Called between initial GC and the start of the test
*/
preTestInit?(): void;
/**
* Called between the final GC and memory measurement
*/
postTestComplete?(): void;
}
export interface PerformanceReport {
iteration: number;
date: Date;
memory?: MemoryInfo;
gc?: boolean | undefined;
}
export class MemoryUtil extends BaseUtil {
private performanceTestDefaultOptions: PerformanceTestOptions = {
focused: false,
initialPostGcSleep: 2500,
iterations: 100,
finalPostTestSleep: 1500,
finalPostGcSleep: 4500,
writeLogFile: true,
writeCsvFile: true,
generateGraph: true,
preTestInit: () => {
},
postTestComplete: () => {
},
};
private static copyDefaults(src: any, target: any = {}) {
// Copy over default option values
Object.keys(src).forEach((key: string) => {
if (target[key] === undefined || target[key] === null) {
target[key] = src[key];
}
});
return target;
}
public static csvMapper = (value: PerformanceReport, index: number) => {
return `${value.iteration},${value.date.toString()},` +
(value.memory
? `${value.memory.usedJSHeapSize},${value.memory.totalJSHeapSize},${value.memory.jsHeapSizeLimit}`
: ',,') +
`,${!!value.gc}`;
}
public static fileWriter(filename: string): Subscriber<string> {
ensureDirSync(dirname(filename));
let stream: WriteStream = null;
return Subscriber.create((value: string) => {
if (stream === null) {
stream = createWriteStream(filename);
}
stream.write(value);
stream.write('\n');
},
(err) => {
stream.end();
throw err;
},
() => {
stream.end();
});
}
public static chartWriter(filename: string, title = 'Memory performance'): Subscriber<PerformanceReport[]> {
ensureDirSync(dirname(filename));
return Subscriber.create((value: PerformanceReport[]) => {
const chart: any = quiche('line');
chart.setTitle(title);
let data: { usedJSHeapSize: number[], totalJSHeapSize: number[] } = {
usedJSHeapSize: [],
totalJSHeapSize: [],
};
value.forEach((item) => {
if (item.memory) {
// Add the data, but shrink the numbers a little as the precise values are not that important,
// but it will reduce the image url length
data.usedJSHeapSize.push(Math.round(item.memory.usedJSHeapSize / 100000));
data.totalJSHeapSize.push(Math.round(item.memory.totalJSHeapSize / 100000));
}
});
chart.setWidth(440);
chart.setHeight(220);
chart.setLegendBottom();
chart.addData(data.usedJSHeapSize, 'Used JS Heap', '03a9f4', 1, 0, 0);
chart.addData(data.totalJSHeapSize, 'Total JS Heap', 'ff9800', 1, 6, 3);
// Utilize the array keys for the x axis
chart.addAxisLabels('x', Object.keys(data.usedJSHeapSize));
chart.setAutoScaling();
let chartUrl = chart.getUrl(true);
let command = 'curl ' + chartUrl;
console.log(command);
writeFileSync(resolve(filename), execSync(command, {encoding: 'binary'}), 'binary');
console.log('Graph created');
});
}
constructor(browser?: ProtractorBrowser) {
super(browser);
}
public isChrome(): promise.Promise<boolean> {
return this.browser.driver.executeScript('return navigator.userAgent.indexOf(\'Chrome/\') !== -1;');
}
public measure(): promise.Promise<MemoryInfo> {
return this.browser.driver.executeScript('return window.performance.memory;');
}
public garbageCollectAvailable(): promise.Promise<boolean> {
return this.browser.driver.executeScript('return typeof window.gc === "function";');
}
public garbageCollect(): promise.Promise<void> {
return this.browser.driver.executeScript('return window.gc();');
}
/**
* Perform a certain action multiple times and measure the memory consumption between the iterations.
* Triggers garbage collection before the first iteration and after the last one to identify possible
* memory leaks.
*
* Note 1: Ensure that the started browser supports garbage collection, use {@link MemoryUtil#garbageCollectAvailable()}
* Note 2: Subscribe to the returned observable in order to execute the tests!
*
* @param expectation descriptive text of the action being performed, will be used as testSuite name
* @param assertion function to be performed each iteration
* @param options
* @param timeout
* @returns {Observable<{date: Date, memory?: MemoryInfo, gc?: boolean}>}
*/
public performanceTest(expectation: string,
assertion: () => void,
options: PerformanceTestOptions = {},
timeout?: number): Observable<PerformanceReport> {
return new Observable((observer: Observer<PerformanceReport>) => {
const handleMeasureResult = (iteration: number) => {
return (memoryInfo: MemoryInfo) => {
observer.next({iteration, date: new Date(), memory: memoryInfo});
};
};
options = MemoryUtil.copyDefaults(this.performanceTestDefaultOptions, options);
(options.focused ? fdescribe : describe)(expectation, () => {
let iteration = 0;
beforeEach(() => {
iteration = ++iteration;
if (iteration === 1) {
// First iteration, collect garbage and sleep a while afterwards
this.garbageCollect().then(() => observer.next({iteration, date: new Date(), gc: true}));
this.browser.sleep(options.initialPostGcSleep);
this.measure().then(handleMeasureResult(iteration));
options.preTestInit();
}
}, 10000);
for (let i = 1; i <= options.iterations; ++i) {
it('Iteration ' + i, () => {
assertion();
this.measure().then(handleMeasureResult(iteration));
}, timeout);
}
afterEach(() => {
if (iteration === options.iterations) {
// Last iteration
this.browser.sleep(options.finalPostTestSleep);
this.garbageCollect().then(() => observer.next({iteration, date: new Date(), gc: true}));
options.postTestComplete();
this.browser.sleep(options.finalPostGcSleep);
this.measure().then(handleMeasureResult(iteration)).then(() => observer.complete());
}
}, 10000);
});
});
}
}