overtake 0.0.1-rc3 → 0.0.4

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.
Files changed (5) hide show
  1. package/cli.js +59 -16
  2. package/index.d.ts +24 -50
  3. package/index.js +117 -175
  4. package/package.json +4 -3
  5. package/reporter.js +0 -90
package/cli.js CHANGED
@@ -1,19 +1,62 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --no-warnings
2
2
 
3
+ import { Command } from 'commander';
3
4
  import { promisify } from 'util';
5
+ import Path from 'path';
4
6
  import glob from 'glob';
5
- import { Overtake } from './index.js';
6
- import { defaultReporter } from './reporter.js';
7
-
8
- const globAsync = promisify(glob);
9
- const pattern = process.argv[2] || '**/__benchmarks__/**/*.js';
10
- const overtake = new Overtake({});
11
-
12
- (async () => {
13
- const files = await globAsync(pattern);
14
- await overtake.load(files);
15
- if (overtake.reporters.length === 0) {
16
- reporter(defaultReporter);
17
- }
18
- await overtake.run();
19
- })().catch((e) => console.error(e));
7
+ import { load, createScript, benchmark, setup, teardown, measure, perform, run, defaultReporter } from './index.js';
8
+ import packageJson from './package.json' assert { type: 'json' };
9
+
10
+ const commands = new Command();
11
+
12
+ commands.name('overtake').description(packageJson.description).version(packageJson.version, '-v, --version');
13
+
14
+ commands
15
+ .argument('[files...]', 'file paths or path patterns to search benchmark scripts')
16
+ .option('-i, --inline [inline]', 'inline code to benchmark', (value, previous) => previous.concat([value]), [])
17
+ .option('-c, --count [count]', 'perform count for inline code', (v) => parseInt(v))
18
+ .action(async (patterns, { count = 1, inline }) => {
19
+ Object.assign(globalThis, { benchmark, setup, teardown, measure, perform });
20
+
21
+ const globAsync = promisify(glob);
22
+ const foundFiles = await Promise.all(patterns.map((pattern) => globAsync(pattern)));
23
+ const files = [
24
+ ...new Set(
25
+ []
26
+ .concat(...foundFiles)
27
+ .map((filename) => Path.resolve(filename))
28
+ .filter(Boolean)
29
+ ),
30
+ ];
31
+
32
+ const scripts = [];
33
+ if (inline.length) {
34
+ const inlineScript = await createScript('', () => {
35
+ benchmark('', () => {
36
+ inline.forEach((code) => {
37
+ measure(code, `() => () => { ${code} }`);
38
+ });
39
+ perform('', count);
40
+ });
41
+ });
42
+ scripts.push(inlineScript);
43
+ }
44
+
45
+ for (const file of files) {
46
+ const filename = Path.resolve(file);
47
+ const script = await load(filename);
48
+ scripts.push(script);
49
+ }
50
+
51
+ await run(scripts, defaultReporter);
52
+ });
53
+
54
+ commands.on('--help', () => {
55
+ console.log('');
56
+ console.log('Examples:');
57
+ console.log(' $ overtake **/__benchmarks__/*.js');
58
+ console.log(' $ overtake -i "class A{}" -i "function A(){}" -i "const A = () => {}" -c 1000000');
59
+ console.log(' $ overtake -v');
60
+ });
61
+
62
+ commands.parse(process.argv);
package/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { Event } from 'evnty';
2
-
3
1
  declare global {
4
2
  type CanBePromise<T> = Promise<T> | T;
5
3
 
@@ -11,8 +9,15 @@ declare global {
11
9
  max: number;
12
10
  sum: number;
13
11
  avg: number;
14
- med: number;
15
12
  mode: number;
13
+ p1: number;
14
+ p5: number;
15
+ p20: number;
16
+ p33: number;
17
+ p50: number;
18
+ med: number;
19
+ p66: number;
20
+ p80: number;
16
21
  p90: number;
17
22
  p95: number;
18
23
  p99: number;
@@ -23,52 +28,6 @@ declare global {
23
28
  total: number;
24
29
  }
25
30
 
26
- interface Overtake {
27
- onLoad: Event<any>;
28
-
29
- onRun: Event<any>;
30
-
31
- onComplete: Event<any>;
32
-
33
- onScriptRegister: Event<any>;
34
-
35
- onScriptStart: Event<any>;
36
-
37
- onScriptComplete: Event<any>;
38
-
39
- onSuiteRegister: Event<any>;
40
-
41
- onSuiteStart: Event<any>;
42
-
43
- onSuiteComplete: Event<any>;
44
-
45
- onSetupRegister: Event<any>;
46
-
47
- onTeardownRegister: Event<any>;
48
-
49
- onMeasureRegister: Event<any>;
50
-
51
- onMeasureStart: Event<any>;
52
-
53
- onMeasureComplete: Event<any>;
54
-
55
- onPerformRegister: Event<any>;
56
-
57
- onPerformStart: Event<any>;
58
-
59
- onPerformProgress: Event<any>;
60
-
61
- onPerformComplete: Event<any>;
62
-
63
- onReport: Event<Report>;
64
- }
65
-
66
- function benchmark(title: string, init: () => void): void;
67
-
68
- function setup<C>(init: () => CanBePromise<C>): any;
69
-
70
- function teardown<C>(teardown: (context: C) => CanBePromise<void>): void;
71
-
72
31
  type MeasureInitResult = CanBePromise<() => void>;
73
32
 
74
33
  function measure(title: string, init: () => MeasureInitResult): void;
@@ -78,5 +37,20 @@ declare global {
78
37
 
79
38
  function perform<A>(title: string, counter: number, args: A): void;
80
39
 
81
- function reporter(reporter: (overtake: Overtake) => void): void;
40
+ function setup<C>(init: () => CanBePromise<C>): void;
41
+
42
+ function teardown<C>(teardown: (context: C) => CanBePromise<void>): void;
43
+
44
+ interface Suite {
45
+ title: string;
46
+ setup: typeof setup;
47
+ teardown: typeof teardown;
48
+ measures: typeof measure[];
49
+ performs: typeof perform[];
50
+ init: () => void;
51
+ }
52
+
53
+ function benchmark(title: string, init: () => void): void;
54
+
55
+ function script(filename): Promise<Suite[]>;
82
56
  }
package/index.js CHANGED
@@ -1,183 +1,118 @@
1
- import Path from 'path';
1
+ import { createContext } from 'conode';
2
2
  import WorkerThreads from 'worker_threads';
3
- import { Event } from 'evnty';
4
3
 
5
- export const NOOP = () => {};
6
-
7
- export class Perform {
8
- title = '';
9
-
10
- count = 0;
11
-
12
- args;
13
-
14
- constructor(overtake, title, count, args) {
15
- this.title = title;
16
- this.count = count;
17
- this.args = args;
18
- }
19
- }
20
-
21
- export class Measure {
22
- title = '';
4
+ const overtakeContext = createContext();
5
+ const suiteContext = createContext();
23
6
 
24
- init = NOOP;
25
-
26
- constructor(overtake, title, init = NOOP) {
27
- this.title = title;
28
- this.init = init;
29
- }
30
- }
31
-
32
- export class Suite {
33
- title = '';
34
-
35
- setup = NOOP;
7
+ export const NOOP = () => {};
36
8
 
37
- measures = [];
9
+ export const setup = (fn) => {
10
+ suiteContext.getContext().setup = fn;
11
+ };
38
12
 
39
- performs = [];
13
+ export const teardown = (fn) => {
14
+ suiteContext.getContext().teardown = fn;
15
+ };
40
16
 
41
- teardown = NOOP;
17
+ export const measure = (title, fn) => {
18
+ suiteContext.getContext().measures.push({ title, init: fn });
19
+ };
42
20
 
43
- #init = NOOP;
21
+ export const perform = (title, count, args) => {
22
+ suiteContext.getContext().performs.push({ title, count, args });
23
+ };
44
24
 
45
- #overtake = null;
25
+ export const benchmark = (title, fn) => {
26
+ const setup = NOOP;
27
+ const teardown = NOOP;
28
+ const measures = [];
29
+ const performs = [];
30
+
31
+ overtakeContext.getContext().suites.push({
32
+ title,
33
+ setup,
34
+ teardown,
35
+ measures,
36
+ performs,
37
+ init: fn,
38
+ });
39
+ };
46
40
 
47
- constructor(overtake, title, init = NOOP) {
48
- this.#overtake = overtake;
49
- this.title = title;
50
- this.#init = init;
51
- }
41
+ export const createScript = async (filename, fn) => {
42
+ const suites = [];
43
+ const script = { filename, suites };
44
+ await overtakeContext.contextualize(script, fn);
52
45
 
53
- async init() {
54
- const unsubscribes = [
55
- this.#overtake.onSetupRegister.on((setup) => (this.setup = setup)),
56
- this.#overtake.onMeasureRegister.on((measure) => this.measures.push(measure)),
57
- this.#overtake.onPerformRegister.on((perform) => this.performs.push(perform)),
58
- this.#overtake.onTeardownRegister.on((teardown) => (this.teardown = teardown)),
59
- ];
60
- await this.#init();
61
- unsubscribes.forEach((unsubscribe) => unsubscribe());
62
- }
63
- }
46
+ return script;
47
+ };
64
48
 
65
- export class Script {
66
- onLoad = new Event();
49
+ export const load = async (filename) => {
50
+ return createScript(filename, () => import(filename));
51
+ };
67
52
 
68
- filename = '';
53
+ const map = {
54
+ script: '⭐ Script ',
55
+ suite: '⇶ Suite ',
56
+ perform: '➤ Perform ',
57
+ measure: '✓ Measure',
58
+ };
69
59
 
70
- suites = [];
60
+ export const defaultReporter = async (type, title, test) => {
61
+ console.group(`${map[type]} ${title}`);
62
+ await test({ test: defaultReporter, output: (report) => console.table(report) });
63
+ console.groupEnd();
64
+ };
71
65
 
72
- constructor(filename) {
73
- this.filename = filename;
74
- }
66
+ const ACCURACY = 6;
67
+ export function formatFloat(value, digits = ACCURACY) {
68
+ return parseFloat(value.toFixed(digits));
75
69
  }
76
70
 
77
- export class Overtake {
78
- onLoad = new Event();
79
-
80
- onRun = new Event();
81
-
82
- onComplete = new Event();
83
-
84
- onScriptRegister = new Event();
85
-
86
- onScriptStart = new Event();
87
-
88
- onScriptComplete = new Event();
89
-
90
- onSuiteRegister = new Event();
91
-
92
- onSuiteStart = new Event();
93
-
94
- onSuiteComplete = new Event();
95
-
96
- onSetupRegister = new Event();
97
-
98
- onTeardownRegister = new Event();
99
-
100
- onMeasureRegister = new Event();
101
-
102
- onMeasureStart = new Event();
103
-
104
- onMeasureComplete = new Event();
105
-
106
- onPerformRegister = new Event();
107
-
108
- onPerformStart = new Event();
109
-
110
- onPerformProgress = new Event();
111
-
112
- onPerformComplete = new Event();
113
-
114
- onReport = new Event();
115
-
116
- scripts = [];
117
-
118
- reporters = [];
119
-
120
- constructor(options = {}) {
121
- Object.assign(globalThis, {
122
- benchmark: (title, init) => this.onSuiteRegister(new Suite(this, title, init)),
123
- setup: (init) => this.onSetupRegister(init),
124
- teardown: (init) => this.onTeardownRegister(init),
125
- measure: (title, init) => this.onMeasureRegister(new Measure(this, title, init)),
126
- perform: (title, count, args) => this.onPerformRegister(new Perform(this, title, count, args)),
127
- reporter: (reporter) => this.reporters.push(reporter(this)),
128
- });
129
- }
130
-
131
- async load(files) {
132
- this.onLoad(this);
133
- for (const file of files) {
134
- const filename = Path.resolve(file);
135
- const script = new Script(filename);
136
- const unsubscribe = this.onSuiteRegister.on((suite) => {
137
- script.suites.push(suite);
138
- });
139
- await import(filename);
140
- unsubscribe();
141
- this.scripts.push(script);
142
- this.onScriptRegister(script);
143
- }
144
- }
145
-
146
- async run() {
147
- this.onRun();
148
- for (const script of this.scripts) {
149
- this.onScriptStart(script);
71
+ export const run = async (scripts, reporter) => {
72
+ for (const script of scripts) {
73
+ await reporter('script', script.filename, async (scriptTest) => {
150
74
  for (const suite of script.suites) {
151
- await suite.init().catch((e) => console.error(e));
152
- this.onSuiteStart(suite);
153
- for (const measure of suite.measures) {
154
- this.onMeasureStart(measure);
75
+ await scriptTest.test('suite', suite.title, async (suiteTest) => {
76
+ await suiteContext.contextualize(suite, suite.init);
155
77
  for (const perform of suite.performs) {
156
- this.onPerformStart(perform);
157
- const result = await runWorker(
158
- {
159
- setup: suite.setup,
160
- teardown: suite.teardown,
161
- init: measure.init,
162
- count: perform.count,
163
- args: perform.args,
164
- },
165
- this.onPerformProgress
166
- );
167
- this.onPerformComplete(perform);
168
- this.onReport(result);
78
+ await suiteTest.test('perform', perform.title, async (performTest) => {
79
+ for (const measure of suite.measures) {
80
+ await performTest.test('measure', measure.title, async (measureTest) => {
81
+ const result = await runWorker({
82
+ setup: suite.setup,
83
+ teardown: suite.teardown,
84
+ init: measure.init,
85
+ count: perform.count,
86
+ args: perform.args,
87
+ });
88
+ if (result.success) {
89
+ measureTest.output({
90
+ [formatFloat(result.mode)]: {
91
+ total: formatFloat(result.total),
92
+ med: formatFloat(result.med),
93
+ p95: formatFloat(result.p95),
94
+ p99: formatFloat(result.p99),
95
+ count: result.count,
96
+ },
97
+ });
98
+ } else {
99
+ measureTest.output({
100
+ error: {
101
+ reason: result.error,
102
+ },
103
+ });
104
+ }
105
+ });
106
+ }
107
+ });
169
108
  }
170
- this.onMeasureComplete(perform);
171
- }
172
- this.onSuiteComplete(perform);
109
+ });
173
110
  }
174
- this.onScriptComplete(perform);
175
- }
176
- this.onComplete(this);
111
+ });
177
112
  }
178
- }
113
+ };
179
114
 
180
- export async function runWorker({ args, count, ...options }, onProgress) {
115
+ export async function runWorker({ args, count, ...options }, onProgress = null) {
181
116
  const setupCode = options.setup.toString();
182
117
  const teardownCode = options.teardown.toString();
183
118
  const initCode = options.init.toString();
@@ -192,7 +127,7 @@ export async function runWorker({ args, count, ...options }, onProgress) {
192
127
  const worker = new WorkerThreads.Worker(new URL('runner.js', import.meta.url), { argv: [params] });
193
128
  return new Promise((resolve) => {
194
129
  worker.on('message', (data) => {
195
- if (data.type === 'progress') {
130
+ if (onProgress && data.type === 'progress') {
196
131
  onProgress(data);
197
132
  } else if (data.type === 'report') {
198
133
  resolve(data);
@@ -211,8 +146,8 @@ export async function start(input) {
211
146
  const setup = Function(`return ${setupCode};`)();
212
147
  const teardown = Function(`return ${teardownCode};`)();
213
148
  const init = Function(`return ${initCode};`)();
149
+ const initArgsSize = init.length;
214
150
  const send = WorkerThreads.parentPort ? (data) => WorkerThreads.parentPort.postMessage(data) : (data) => console.log(data);
215
-
216
151
  let i = count;
217
152
  let done = FALSE_START;
218
153
 
@@ -224,11 +159,14 @@ export async function start(input) {
224
159
  const context = await setup();
225
160
  const setupMark = performance.now();
226
161
 
227
- const initArgs = [() => done()];
228
- if (init.length > 2) {
162
+ const initArgs = [];
163
+ if (initArgsSize !== 0) {
164
+ initArgs.push(() => done());
165
+ }
166
+ if (initArgsSize > 2) {
229
167
  initArgs.unshift(args);
230
168
  }
231
- if (init.length > 1) {
169
+ if (initArgsSize > 1) {
232
170
  initArgs.unshift(context);
233
171
  }
234
172
 
@@ -259,6 +197,9 @@ export async function start(input) {
259
197
 
260
198
  const startTickTime = performance.now();
261
199
  action(...args[argIdx], idx);
200
+ if (!initArgsSize) {
201
+ done();
202
+ }
262
203
  };
263
204
  const cyclesMark = performance.now();
264
205
 
@@ -292,14 +233,7 @@ export async function start(input) {
292
233
  });
293
234
  buckets.sort((a, b) => a[1] - b[1]);
294
235
 
295
- const medIdx = Math.trunc((50 * timings.length) / 100);
296
- const med = timings[medIdx];
297
- const p90Idx = Math.trunc((90 * timings.length) / 100);
298
- const p90 = timings[p90Idx];
299
- const p95Idx = Math.trunc((95 * timings.length) / 100);
300
- const p95 = timings[p95Idx];
301
- const p99Idx = Math.trunc((99 * timings.length) / 100);
302
- const p99 = timings[p99Idx];
236
+ const percentile = (p) => timings[Math.trunc((p * timings.length) / 100)];
303
237
  const mode = buckets[buckets.length - 1][0];
304
238
 
305
239
  send({
@@ -310,11 +244,19 @@ export async function start(input) {
310
244
  max,
311
245
  sum,
312
246
  avg,
313
- med,
314
247
  mode,
315
- p90,
316
- p95,
317
- p99,
248
+ p1: percentile(1),
249
+ p5: percentile(5),
250
+ p10: percentile(10),
251
+ p20: percentile(20),
252
+ p33: percentile(33),
253
+ p50: percentile(50),
254
+ med: percentile(50),
255
+ p66: percentile(66),
256
+ p80: percentile(80),
257
+ p90: percentile(90),
258
+ p95: percentile(95),
259
+ p99: percentile(99),
318
260
  setup: setupMark - startMark,
319
261
  init: initDoneMark - initMark,
320
262
  cycles: teardownMark - cyclesMark,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "0.0.1-rc3",
3
+ "version": "0.0.4",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,7 +9,7 @@
9
9
  "overtake": "cli.js"
10
10
  },
11
11
  "scripts": {
12
- "start": "node ./cli.js",
12
+ "start": "./cli.js",
13
13
  "test": "yarn node --experimental-vm-modules $(yarn bin jest) --detectOpenHandles",
14
14
  "lint": "eslint .",
15
15
  "prepare": "husky install"
@@ -39,7 +39,8 @@
39
39
  "pretty-quick": "^3.1.3"
40
40
  },
41
41
  "dependencies": {
42
- "evnty": "^0.7.4",
42
+ "commander": "^9.2.0",
43
+ "conode": "^0.1.0",
43
44
  "glob": "^8.0.1"
44
45
  }
45
46
  }
package/reporter.js DELETED
@@ -1,90 +0,0 @@
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
- }