overtake 0.0.1-rc2 → 0.0.3

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
@@ -55,7 +55,7 @@ benchmark('mongodb vs postgres', () => {
55
55
  return { postgres, mongo };
56
56
  });
57
57
 
58
- measure('mongodb inserts', ({ mongo }/* context */, next) => {
58
+ measure('mongodb inserts', ({ mongo } /* context */, next) => {
59
59
  // prepare a collection
60
60
  const database = mongo.db('overtake');
61
61
  const test = database.collection('test');
@@ -63,21 +63,19 @@ benchmark('mongodb vs postgres', () => {
63
63
  return (data) => test.insertOne(data).then(next);
64
64
  });
65
65
 
66
- measure('postgres inserts', ({ postgres }/* context */, next) => {
66
+ measure('postgres inserts', ({ postgres } /* context */, next) => {
67
67
  // prepare a query
68
68
  const query = 'INSERT INTO overtake(value) VALUES($1) RETURNING *';
69
69
 
70
70
  return (data) => postgres.query(query, [data.value]).then(next);
71
71
  });
72
72
 
73
- teardown(({ mongo, postgres }) => {
74
- await postgres.end()
75
- await mongo.end()
73
+ teardown(async ({ mongo, postgres }) => {
74
+ await postgres.end();
75
+ await mongo.end();
76
76
  });
77
77
 
78
- perform('simple test', 100000, [
79
- { value: 'test' },
80
- ]);
78
+ perform('simple test', 100000, [[{ value: 'test' }]]);
81
79
  });
82
80
  ```
83
81
 
@@ -87,6 +85,12 @@ Make sure you have installed used modules and run
87
85
  yarn overtake
88
86
  ```
89
87
 
88
+ or
89
+
90
+ ```bash
91
+ npx overtake
92
+ ```
93
+
90
94
  Please take a look at [benchmarks](__benchmarks__) to see more examples
91
95
 
92
96
  ## License
package/cli.js CHANGED
@@ -1,19 +1,77 @@
1
1
  #!/usr/bin/env node
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 = 100, 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);
63
+
64
+ //
65
+
66
+ //
67
+ // (async () => {
68
+ // const files = await globAsync(pattern);
69
+ // const scripts = [];
70
+ // for (const file of files) {
71
+ // const filename = Path.resolve(file);
72
+ // const script = await load(filename);
73
+ // scripts.push(script);
74
+ // }
75
+ //
76
+ // await run(scripts, defaultReporter);
77
+ // })().catch((e) => console.error(e));
package/index.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ declare global {
2
+ type CanBePromise<T> = Promise<T> | T;
3
+
4
+ interface Report {
5
+ type: string;
6
+ success: boolean;
7
+ count: number;
8
+ min: number;
9
+ max: number;
10
+ sum: number;
11
+ avg: number;
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;
21
+ p90: number;
22
+ p95: number;
23
+ p99: number;
24
+ setup: number;
25
+ init: number;
26
+ cycles: number;
27
+ teardown: number;
28
+ total: number;
29
+ }
30
+
31
+ type MeasureInitResult = CanBePromise<() => void>;
32
+
33
+ function measure(title: string, init: () => MeasureInitResult): void;
34
+ function measure(title: string, init: (next: () => void) => MeasureInitResult): void;
35
+ function measure<C>(title: string, init: (context: C, next: () => void) => MeasureInitResult): void;
36
+ function measure<C, A>(title: string, init: (context: C, args: A, next: () => void) => MeasureInitResult): void;
37
+
38
+ function perform<A>(title: string, counter: number, args: A): void;
39
+
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[]>;
56
+ }
package/index.js CHANGED
@@ -1,185 +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;
36
-
37
- measures = [];
7
+ export const NOOP = () => {};
38
8
 
39
- performs = [];
9
+ export const setup = (fn) => {
10
+ suiteContext.getContext().setup = fn;
11
+ };
40
12
 
41
- teardown = NOOP;
13
+ export const teardown = (fn) => {
14
+ suiteContext.getContext().teardown = fn;
15
+ };
42
16
 
43
- #title = '';
17
+ export const measure = (title, fn) => {
18
+ suiteContext.getContext().measures.push({ title, init: fn });
19
+ };
44
20
 
45
- #init = NOOP;
21
+ export const perform = (title, count, args) => {
22
+ suiteContext.getContext().performs.push({ title, count, args });
23
+ };
46
24
 
47
- #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
+ };
48
40
 
49
- constructor(overtake, title, init = NOOP) {
50
- this.#overtake = overtake;
51
- this.title = title;
52
- this.#init = init;
53
- }
41
+ export const createScript = async (filename, fn) => {
42
+ const suites = [];
43
+ const script = { filename, suites };
44
+ await overtakeContext.contextualize(script, fn);
54
45
 
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
- }
65
- }
46
+ return script;
47
+ };
66
48
 
67
- export class Script {
68
- onLoad = new Event();
49
+ export const load = async (filename) => {
50
+ return createScript(filename, () => import(filename));
51
+ };
69
52
 
70
- filename = '';
53
+ const map = {
54
+ script: '⭐ Script ',
55
+ suite: '⇶ Suite ',
56
+ perform: '➤ Perform ',
57
+ measure: '✓ Measure',
58
+ };
71
59
 
72
- 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
+ };
73
65
 
74
- constructor(filename) {
75
- this.filename = filename;
76
- }
66
+ const ACCURACY = 6;
67
+ export function formatFloat(value, digits = ACCURACY) {
68
+ return parseFloat(value.toFixed(digits));
77
69
  }
78
70
 
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);
71
+ export const run = async (scripts, reporter) => {
72
+ for (const script of scripts) {
73
+ await reporter('script', script.filename, async (scriptTest) => {
152
74
  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);
75
+ await scriptTest.test('suite', suite.title, async (suiteTest) => {
76
+ await suiteContext.contextualize(suite, suite.init);
157
77
  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);
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
+ });
171
108
  }
172
- this.onMeasureComplete(perform);
173
- }
174
- this.onSuiteComplete(perform);
109
+ });
175
110
  }
176
- this.onScriptComplete(perform);
177
- }
178
- this.onComplete(this);
111
+ });
179
112
  }
180
- }
113
+ };
181
114
 
182
- export async function runWorker({ args, count, ...options }, onProgress) {
115
+ export async function runWorker({ args, count, ...options }, onProgress = null) {
183
116
  const setupCode = options.setup.toString();
184
117
  const teardownCode = options.teardown.toString();
185
118
  const initCode = options.init.toString();
@@ -194,7 +127,7 @@ export async function runWorker({ args, count, ...options }, onProgress) {
194
127
  const worker = new WorkerThreads.Worker(new URL('runner.js', import.meta.url), { argv: [params] });
195
128
  return new Promise((resolve) => {
196
129
  worker.on('message', (data) => {
197
- if (data.type === 'progress') {
130
+ if (onProgress && data.type === 'progress') {
198
131
  onProgress(data);
199
132
  } else if (data.type === 'report') {
200
133
  resolve(data);
@@ -213,8 +146,8 @@ export async function start(input) {
213
146
  const setup = Function(`return ${setupCode};`)();
214
147
  const teardown = Function(`return ${teardownCode};`)();
215
148
  const init = Function(`return ${initCode};`)();
149
+ const initArgsSize = init.length;
216
150
  const send = WorkerThreads.parentPort ? (data) => WorkerThreads.parentPort.postMessage(data) : (data) => console.log(data);
217
-
218
151
  let i = count;
219
152
  let done = FALSE_START;
220
153
 
@@ -226,11 +159,14 @@ export async function start(input) {
226
159
  const context = await setup();
227
160
  const setupMark = performance.now();
228
161
 
229
- const initArgs = [() => done()];
230
- if (init.length > 2) {
162
+ const initArgs = [];
163
+ if (initArgsSize !== 0) {
164
+ initArgs.push(() => done());
165
+ }
166
+ if (initArgsSize > 2) {
231
167
  initArgs.unshift(args);
232
168
  }
233
- if (init.length > 1) {
169
+ if (initArgsSize > 1) {
234
170
  initArgs.unshift(context);
235
171
  }
236
172
 
@@ -261,6 +197,9 @@ export async function start(input) {
261
197
 
262
198
  const startTickTime = performance.now();
263
199
  action(...args[argIdx], idx);
200
+ if (!initArgsSize) {
201
+ done();
202
+ }
264
203
  };
265
204
  const cyclesMark = performance.now();
266
205
 
@@ -294,14 +233,7 @@ export async function start(input) {
294
233
  });
295
234
  buckets.sort((a, b) => a[1] - b[1]);
296
235
 
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];
236
+ const percentile = (p) => timings[Math.trunc((p * timings.length) / 100)];
305
237
  const mode = buckets[buckets.length - 1][0];
306
238
 
307
239
  send({
@@ -312,11 +244,19 @@ export async function start(input) {
312
244
  max,
313
245
  sum,
314
246
  avg,
315
- med,
316
247
  mode,
317
- p90,
318
- p95,
319
- 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),
320
260
  setup: setupMark - startMark,
321
261
  init: initDoneMark - initMark,
322
262
  cycles: teardownMark - cyclesMark,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "0.0.1-rc2",
3
+ "version": "0.0.3",
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": "node --no-warnings ./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
- }