overtake 0.0.1-rc1 → 0.0.1-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Overtake
2
2
 
3
- NodeJS performance benchmark
3
+ Performance benchmark for NodeJS
4
4
 
5
5
  [![Build Status][github-image]][github-url]
6
6
  [![NPM version][npm-image]][npm-url]
@@ -81,6 +81,8 @@ benchmark('mongodb vs postgres', () => {
81
81
  });
82
82
  ```
83
83
 
84
+ Make sure you have installed used modules and run
85
+
84
86
  ```bash
85
87
  yarn overtake
86
88
  ```
package/cli.js CHANGED
@@ -1,28 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import Path from 'path';
2
+
3
+ import { promisify } from 'util';
3
4
  import glob from 'glob';
4
- import { benchmark, setup, teardown, measure, perform, suites, runner } from './index.js';
5
+ import { Overtake } from './index.js';
6
+ import { defaultReporter } from './reporter.js';
5
7
 
8
+ const globAsync = promisify(glob);
6
9
  const pattern = process.argv[2] || '**/__benchmarks__/**/*.js';
10
+ const overtake = new Overtake({});
7
11
 
8
- Object.assign(globalThis, {
9
- benchmark,
10
- setup,
11
- teardown,
12
- measure,
13
- perform,
14
- });
15
-
16
- glob(pattern, async (err, files) => {
17
- try {
18
- for (const file of files) {
19
- const filepath = Path.resolve(file);
20
- await import(filepath);
21
- }
22
- } catch (e) {
23
- console.error(e.stack);
24
- }
25
- for (const suite of suites) {
26
- await runner(suite);
12
+ (async () => {
13
+ const files = await globAsync(pattern);
14
+ await overtake.load(files);
15
+ if (overtake.reporters.length === 0) {
16
+ reporter(defaultReporter);
27
17
  }
28
- });
18
+ await overtake.run();
19
+ })().catch((e) => console.error(e));
package/index.js CHANGED
@@ -1,67 +1,185 @@
1
+ import Path from 'path';
1
2
  import WorkerThreads from 'worker_threads';
3
+ import { Event } from 'evnty';
2
4
 
3
- export const suites = [];
5
+ export const NOOP = () => {};
4
6
 
5
- const NOOP = () => {};
7
+ export class Perform {
8
+ title = '';
6
9
 
7
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', '⠿'];
8
- const SPINNER_INTERVAL = 80;
10
+ count = 0;
9
11
 
10
- const ACCURACY = 6;
12
+ args;
11
13
 
12
- const renderSpinner = (index) => {
13
- process.stdout.moveCursor(0, -1);
14
- process.stdout.write(SPINNER[index]);
15
- process.stdout.moveCursor(-3, 1);
16
- };
17
-
18
- export function benchmark(title, init) {
19
- const suite = {
20
- title,
21
- current: true,
22
- measures: [],
23
- performs: [],
24
- setup: NOOP,
25
- teardown: NOOP,
26
- };
27
- suites.push(suite);
28
- init();
29
- suite.current = false;
14
+ constructor(overtake, title, count, args) {
15
+ this.title = title;
16
+ this.count = count;
17
+ this.args = args;
18
+ }
30
19
  }
31
20
 
32
- export function getSuite() {
33
- const suite = suites[suites.length - 1];
34
- if (!suite.current) {
35
- throw new Error('should be inside benchmark');
21
+ export class Measure {
22
+ title = '';
23
+
24
+ init = NOOP;
25
+
26
+ constructor(overtake, title, init = NOOP) {
27
+ this.title = title;
28
+ this.init = init;
36
29
  }
37
- return suite;
38
30
  }
39
31
 
40
- export function setup(init) {
41
- const suite = getSuite();
42
- suite.setup = init;
43
- }
32
+ export class Suite {
33
+ title = '';
44
34
 
45
- export function teardown(init) {
46
- const suite = getSuite();
47
- suite.teardown = init;
48
- }
35
+ setup = NOOP;
36
+
37
+ measures = [];
38
+
39
+ performs = [];
40
+
41
+ teardown = NOOP;
49
42
 
50
- export function measure(title, init) {
51
- const suite = getSuite();
52
- suite.measures.push({ title, init });
43
+ #title = '';
44
+
45
+ #init = NOOP;
46
+
47
+ #overtake = null;
48
+
49
+ constructor(overtake, title, init = NOOP) {
50
+ this.#overtake = overtake;
51
+ this.title = title;
52
+ this.#init = init;
53
+ }
54
+
55
+ async init() {
56
+ const unsubscribes = [
57
+ this.#overtake.onSetupRegister.on((setup) => (this.setup = setup)),
58
+ this.#overtake.onMeasureRegister.on((measure) => this.measures.push(measure)),
59
+ this.#overtake.onPerformRegister.on((perform) => this.performs.push(perform)),
60
+ this.#overtake.onTeardownRegister.on((teardown) => (this.teardown = teardown)),
61
+ ];
62
+ await this.#init();
63
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
64
+ }
53
65
  }
54
66
 
55
- export function perform(title, count, args) {
56
- const suite = getSuite();
57
- suite.performs.push({ title, count, args });
67
+ export class Script {
68
+ onLoad = new Event();
69
+
70
+ filename = '';
71
+
72
+ suites = [];
73
+
74
+ constructor(filename) {
75
+ this.filename = filename;
76
+ }
58
77
  }
59
78
 
60
- export function formatFloat(value, digits = ACCURACY) {
61
- return parseFloat(value.toFixed(digits));
79
+ export class Overtake {
80
+ onLoad = new Event();
81
+
82
+ onRun = new Event();
83
+
84
+ onComplete = new Event();
85
+
86
+ onScriptRegister = new Event();
87
+
88
+ onScriptStart = new Event();
89
+
90
+ onScriptComplete = new Event();
91
+
92
+ onSuiteRegister = new Event();
93
+
94
+ onSuiteStart = new Event();
95
+
96
+ onSuiteComplete = new Event();
97
+
98
+ onSetupRegister = new Event();
99
+
100
+ onTeardownRegister = new Event();
101
+
102
+ onMeasureRegister = new Event();
103
+
104
+ onMeasureStart = new Event();
105
+
106
+ onMeasureComplete = new Event();
107
+
108
+ onPerformRegister = new Event();
109
+
110
+ onPerformStart = new Event();
111
+
112
+ onPerformProgress = new Event();
113
+
114
+ onPerformComplete = new Event();
115
+
116
+ onReport = new Event();
117
+
118
+ scripts = [];
119
+
120
+ reporters = [];
121
+
122
+ constructor(options = {}) {
123
+ Object.assign(globalThis, {
124
+ benchmark: (title, init) => this.onSuiteRegister(new Suite(this, title, init)),
125
+ setup: (init) => this.onSetupRegister(init),
126
+ teardown: (init) => this.onTeardownRegister(init),
127
+ measure: (title, init) => this.onMeasureRegister(new Measure(this, title, init)),
128
+ perform: (title, count, args) => this.onPerformRegister(new Perform(this, title, count, args)),
129
+ reporter: (reporter) => this.reporters.push(reporter(this)),
130
+ });
131
+ }
132
+
133
+ async load(files) {
134
+ this.onLoad(this);
135
+ for (const file of files) {
136
+ const filename = Path.resolve(file);
137
+ const script = new Script(filename);
138
+ const unsubscribe = this.onSuiteRegister.on((suite) => {
139
+ script.suites.push(suite);
140
+ });
141
+ await import(filename);
142
+ unsubscribe();
143
+ this.scripts.push(script);
144
+ this.onScriptRegister(script);
145
+ }
146
+ }
147
+
148
+ async run() {
149
+ this.onRun();
150
+ for (const script of this.scripts) {
151
+ this.onScriptStart(script);
152
+ for (const suite of script.suites) {
153
+ await suite.init().catch((e) => console.error(e));
154
+ this.onSuiteStart(suite);
155
+ for (const measure of suite.measures) {
156
+ this.onMeasureStart(measure);
157
+ for (const perform of suite.performs) {
158
+ this.onPerformStart(perform);
159
+ const result = await runWorker(
160
+ {
161
+ setup: suite.setup,
162
+ teardown: suite.teardown,
163
+ init: measure.init,
164
+ count: perform.count,
165
+ args: perform.args,
166
+ },
167
+ this.onPerformProgress
168
+ );
169
+ this.onPerformComplete(perform);
170
+ this.onReport(result);
171
+ }
172
+ this.onMeasureComplete(perform);
173
+ }
174
+ this.onSuiteComplete(perform);
175
+ }
176
+ this.onScriptComplete(perform);
177
+ }
178
+ this.onComplete(this);
179
+ }
62
180
  }
63
181
 
64
- export async function runWorker({ args, count, ...options }) {
182
+ export async function runWorker({ args, count, ...options }, onProgress) {
65
183
  const setupCode = options.setup.toString();
66
184
  const teardownCode = options.teardown.toString();
67
185
  const initCode = options.init.toString();
@@ -72,81 +190,42 @@ export async function runWorker({ args, count, ...options }) {
72
190
  count,
73
191
  args,
74
192
  });
75
- let i = 0;
76
- const spinnerSize = SPINNER.length - 1;
77
-
78
- const timerId = setInterval(() => renderSpinner(i++ % spinnerSize), SPINNER_INTERVAL);
79
193
 
80
194
  const worker = new WorkerThreads.Worker(new URL('runner.js', import.meta.url), { argv: [params] });
81
195
  return new Promise((resolve) => {
82
- worker.on('message', resolve);
196
+ worker.on('message', (data) => {
197
+ if (data.type === 'progress') {
198
+ onProgress(data);
199
+ } else if (data.type === 'report') {
200
+ resolve(data);
201
+ }
202
+ });
83
203
  worker.on('error', (error) => resolve({ success: false, error: error.message }));
84
- }).finally((result) => {
85
- clearInterval(timerId);
86
- renderSpinner(spinnerSize);
87
-
88
- return result;
89
204
  });
90
205
  }
91
206
 
92
- export async function runner(suite) {
93
- console.group(`\nStart ${suite.title} benchmark`);
94
-
95
- for (let measureIdx = 0; measureIdx < suite.measures.length; measureIdx++) {
96
- const currentMeasure = suite.measures[measureIdx];
97
- const reports = {};
98
- console.group(`\n Measuring performance of ${currentMeasure.title}`);
99
- for (let performIdx = 0; performIdx < suite.performs.length; performIdx++) {
100
- const currentPerform = suite.performs[performIdx];
101
- const report = await runWorker({
102
- setup: suite.setup,
103
- teardown: suite.teardown,
104
- init: currentMeasure.init,
105
- count: currentPerform.count,
106
- args: currentPerform.args,
107
- });
108
- reports[currentPerform.title] = { title: perform.title, report };
109
- if (report.success) {
110
- reports[currentPerform.title] = {
111
- Count: currentPerform.count,
112
- 'Setup, ms': formatFloat(report.setup),
113
- 'Work, ms': formatFloat(report.work),
114
- 'Avg, ms': formatFloat(report.avg),
115
- 'Mode, ms': report.mode,
116
- };
117
- } else {
118
- reports[`${currentPerform.title} ${currentMeasure.title}`] = {
119
- Count: currentPerform.count,
120
- 'Setup, ms': '?',
121
- 'Work, ms': '?',
122
- 'Avg, ms': '?',
123
- 'Mode, ms': '?',
124
- Error: report.error,
125
- };
126
- }
127
- }
128
- console.groupEnd();
129
- console.table(reports);
130
- }
131
- console.groupEnd();
132
- }
133
-
134
207
  const FALSE_START = () => {
135
208
  throw new Error('False start');
136
209
  };
137
210
 
138
211
  export async function start(input) {
139
- const { setupCode, teardownCode, initCode, count, args = [[]] } = JSON.parse(input);
212
+ const { setupCode, teardownCode, initCode, count, reportInterval = 500, args = [[]] } = JSON.parse(input);
140
213
  const setup = Function(`return ${setupCode};`)();
141
214
  const teardown = Function(`return ${teardownCode};`)();
142
215
  const init = Function(`return ${initCode};`)();
216
+ const send = WorkerThreads.parentPort ? (data) => WorkerThreads.parentPort.postMessage(data) : (data) => console.log(data);
143
217
 
144
218
  let i = count;
145
219
  let done = FALSE_START;
146
220
 
147
221
  const timings = [];
148
222
  const argSize = args.length;
223
+
224
+ send({ type: 'progress', stage: 'setup' });
225
+ const startMark = performance.now();
149
226
  const context = await setup();
227
+ const setupMark = performance.now();
228
+
150
229
  const initArgs = [() => done()];
151
230
  if (init.length > 2) {
152
231
  initArgs.unshift(args);
@@ -154,70 +233,97 @@ export async function start(input) {
154
233
  if (init.length > 1) {
155
234
  initArgs.unshift(context);
156
235
  }
157
- const startMark = performance.now();
236
+
237
+ send({ type: 'progress', stage: 'init' });
238
+ const initMark = performance.now();
158
239
  const action = await init(...initArgs);
159
- const workMark = performance.now();
240
+ const initDoneMark = performance.now();
160
241
 
161
242
  try {
243
+ let lastCheck = performance.now();
162
244
  const loop = (resolve, reject) => {
163
- const argIdx = i % argSize;
245
+ const idx = count - i;
246
+ const argIdx = idx % argSize;
164
247
  const timerId = setTimeout(reject, 10000, new Error('Timeout'));
165
248
 
166
249
  done = () => {
167
- // eslint-disable-next-line
168
- const elapsed = performance.now() - startTickTime;
250
+ const doneTick = performance.now();
251
+ const elapsed = doneTick - startTickTime;
169
252
  clearTimeout(timerId);
170
253
  done = FALSE_START;
171
254
  timings.push(elapsed);
172
-
255
+ if (doneTick - lastCheck > reportInterval) {
256
+ lastCheck = doneTick;
257
+ send({ type: 'progress', stage: 'cycles', progress: idx / count });
258
+ }
173
259
  resolve();
174
260
  };
261
+
175
262
  const startTickTime = performance.now();
176
- action(...args[argIdx], count - i);
263
+ action(...args[argIdx], idx);
177
264
  };
265
+ const cyclesMark = performance.now();
178
266
 
267
+ send({ type: 'progress', stage: 'cycles', progress: 0 });
179
268
  while (i--) {
180
269
  await new Promise(loop);
181
270
  }
271
+ send({ type: 'progress', stage: 'teardown' });
272
+ const teardownMark = performance.now();
273
+ await teardown(context);
182
274
  const completeMark = performance.now();
275
+ send({ type: 'progress', stage: 'complete', progress: (count - i) / count });
183
276
 
184
- await teardown(context);
277
+ timings.sort((a, b) => a - b);
185
278
 
186
- timings.sort();
187
-
188
- const {
189
- mode: { time: mode },
190
- } = timings
191
- .map((time) => parseFloat(time.toFixed(ACCURACY)))
192
- .reduce((result, time) => {
193
- const value = (result[time] || 0) + 1;
194
- const mode = !result.mode || result.mode.value < value ? { time, value } : result.mode;
195
- return { ...result, [time]: value, mode };
196
- }, {});
197
-
198
- const min = timings.reduce((a, b) => Math.min(a, b), Infinity);
199
- const max = timings.reduce((a, b) => Math.max(a, b), 0);
200
- const avg = timings.reduce((a, b) => a + b, 0) / count;
201
- const p90idx = parseInt(timings.length * 0.9, 10);
202
- const p95idx = parseInt(timings.length * 0.95, 10);
203
- const p99idx = parseInt(timings.length * 0.99, 10);
204
- const p90 = timings[p90idx];
205
- const p95 = timings[p95idx];
206
- const p99 = timings[p99idx];
207
-
208
- WorkerThreads.parentPort.postMessage({
279
+ const min = timings[0];
280
+ const max = timings[timings.length - 1];
281
+ const range = max - min || Number.MIN_VALUE;
282
+ const sum = timings.reduce((a, b) => a + b, 0);
283
+ const avg = sum / timings.length;
284
+
285
+ const step = range / 99 || Number.MIN_VALUE;
286
+ const buckets = Array(100)
287
+ .fill(0)
288
+ .map((_, idx) => [min + idx * step, 0]);
289
+
290
+ // Calc mode O(n)
291
+ timings.forEach((timing, idx) => {
292
+ const index = Math.round((timing - min) / step);
293
+ buckets[index][1] += 1;
294
+ });
295
+ buckets.sort((a, b) => a[1] - b[1]);
296
+
297
+ const medIdx = Math.trunc((50 * timings.length) / 100);
298
+ const med = timings[medIdx];
299
+ const p90Idx = Math.trunc((90 * timings.length) / 100);
300
+ const p90 = timings[p90Idx];
301
+ const p95Idx = Math.trunc((95 * timings.length) / 100);
302
+ const p95 = timings[p95Idx];
303
+ const p99Idx = Math.trunc((99 * timings.length) / 100);
304
+ const p99 = timings[p99Idx];
305
+ const mode = buckets[buckets.length - 1][0];
306
+
307
+ send({
308
+ type: 'report',
309
+ success: true,
310
+ count: timings.length,
209
311
  min,
210
312
  max,
313
+ sum,
211
314
  avg,
315
+ med,
316
+ mode,
212
317
  p90,
213
318
  p95,
214
319
  p99,
215
- mode,
216
- setup: workMark - startMark,
217
- work: completeMark - workMark,
218
- success: true,
320
+ setup: setupMark - startMark,
321
+ init: initDoneMark - initMark,
322
+ cycles: teardownMark - cyclesMark,
323
+ teardown: completeMark - teardownMark,
324
+ total: completeMark - setupMark,
219
325
  });
220
326
  } catch (error) {
221
- WorkerThreads.parentPort.postMessage({ success: false, error: error.stack });
327
+ send({ type: 'report', success: false, error: error.stack });
222
328
  }
223
329
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "0.0.1-rc1",
3
+ "version": "0.0.1-rc2",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,8 +9,8 @@
9
9
  "overtake": "cli.js"
10
10
  },
11
11
  "scripts": {
12
- "measure": "node ./__runner__/class-vs-function.js",
13
- "test": "yarn node --experimental-vm-modules $(yarn bin jest)",
12
+ "start": "node ./cli.js",
13
+ "test": "yarn node --experimental-vm-modules $(yarn bin jest) --detectOpenHandles",
14
14
  "lint": "eslint .",
15
15
  "prepare": "husky install"
16
16
  },
@@ -33,16 +33,13 @@
33
33
  "devDependencies": {
34
34
  "@jest/globals": "^27.5.1",
35
35
  "@types/jest": "^27.4.1",
36
- "eslint": "^8.14.0",
37
- "eslint-config-airbnb-base": "^15.0.0",
38
- "eslint-config-prettier": "^8.5.0",
39
- "eslint-plugin-import": "^2.26.0",
40
36
  "husky": "^7.0.4",
41
37
  "jest": "^27.5.1",
42
38
  "prettier": "^2.6.2",
43
39
  "pretty-quick": "^3.1.3"
44
40
  },
45
41
  "dependencies": {
42
+ "evnty": "^0.7.4",
46
43
  "glob": "^8.0.1"
47
44
  }
48
45
  }
package/reporter.js ADDED
@@ -0,0 +1,90 @@
1
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', '⠿'];
2
+ const SPINNER_INTERVAL = 100;
3
+ const SPINNER_PADDING = 5;
4
+
5
+ const ACCURACY = 6;
6
+
7
+ /** TODO
8
+ * [PROGRESS] script/file.js
9
+ * Test suite title
10
+ * ➤ Measure
11
+ * ✓ Perform
12
+ * ✕ Perform
13
+ * */
14
+
15
+ const renderString = (message, x, direction) => {
16
+ const size = message.length;
17
+ process.stdout.moveCursor(0, -1);
18
+ process.stdout.cursorTo(x);
19
+ process.stdout.clearLine(direction);
20
+ if (direction === -1) {
21
+ process.stdout.cursorTo(0);
22
+ }
23
+ process.stdout.write(message);
24
+ process.stdout.moveCursor(-process.stdout.rows, 1);
25
+
26
+ return size;
27
+ };
28
+
29
+ const renderSpinner = (index) => renderString(SPINNER[index], 0, -1);
30
+
31
+ const paddings = [];
32
+
33
+ const renderMessage = (column, message) => {
34
+ const padding = SPINNER_PADDING + column * 10;
35
+ paddings[column + 1] = renderString(message, padding, 1);
36
+ };
37
+
38
+ export function formatFloat(value, digits = ACCURACY) {
39
+ return parseFloat(value.toFixed(digits));
40
+ }
41
+
42
+ export function defaultReporter(overtake) {
43
+ const spinnerSize = SPINNER.length - 1;
44
+ let i = 0;
45
+ let timerId = null;
46
+
47
+ overtake.onLoad.on(() => {});
48
+ overtake.onRun.on(() => {
49
+ console.log();
50
+ });
51
+ overtake.onComplete.on(() => {});
52
+ overtake.onScriptRegister.on(() => {});
53
+ overtake.onScriptStart.on(() => {});
54
+ overtake.onScriptComplete.on(() => {});
55
+ overtake.onSuiteRegister.on(() => {});
56
+ overtake.onSuiteStart.on(() => {});
57
+ overtake.onSuiteComplete.on(() => {});
58
+ overtake.onSetupRegister.on(() => {});
59
+ overtake.onTeardownRegister.on(() => {});
60
+ overtake.onMeasureRegister.on(() => {});
61
+ overtake.onMeasureStart.on((measure) => {
62
+ console.log(measure.title);
63
+ });
64
+ overtake.onMeasureComplete.on(() => {});
65
+ overtake.onPerformRegister.on(() => {});
66
+ overtake.onPerformStart.on((perform) => {
67
+ console.log(perform.title);
68
+ console.log();
69
+ i = 0;
70
+ timerId = setInterval(() => renderSpinner(i++ % spinnerSize), SPINNER_INTERVAL);
71
+ });
72
+ overtake.onPerformComplete.on(() => {
73
+ clearInterval(timerId);
74
+ renderSpinner(spinnerSize);
75
+ });
76
+ overtake.onPerformProgress.on(({ stage, progress }) => {
77
+ renderMessage(0, stage);
78
+ if (typeof progress !== 'undefined') {
79
+ renderMessage(1, `${(progress * 100).toFixed(0)}%`.padStart(4, ' '));
80
+ }
81
+ });
82
+ overtake.onReport.on((report) => {
83
+ if (report.success) {
84
+ renderMessage(2, `total:${report.total.toFixed(0)}ms mode:${report.mode.toFixed(ACCURACY)}ms`);
85
+ } else {
86
+ renderMessage(1, report.error);
87
+ }
88
+ console.log();
89
+ });
90
+ }
package/index.d.ts DELETED
@@ -1,15 +0,0 @@
1
- declare global {
2
- declare function benchmark(title: string, init: () => void): void;
3
-
4
- declare function setup<C>(init: () => C): any;
5
-
6
- declare function teardown<C>(teardown: (context: C) => void): void;
7
-
8
- declare function measure(title: string, init: (next: () => void) => void): void;
9
- declare function measure<C>(title: string, init: (context: C, next: () => void) => void): void;
10
- declare function measure<C, A>(title: string, init: (context: C, args: A, next: () => void) => void): void;
11
-
12
- declare function perform<A>(title: string, counter: number, args: A): void;
13
- }
14
-
15
- export { benchmark, setup, teardown, measure, perform };