overtake 0.0.1-rc1 → 0.0.2

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/README.md +15 -9
  2. package/cli.js +15 -20
  3. package/index.d.ts +50 -9
  4. package/index.js +171 -137
  5. package/package.json +4 -7
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]
@@ -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,28 +63,34 @@ 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
 
82
+ Make sure you have installed used modules and run
83
+
84
84
  ```bash
85
85
  yarn overtake
86
86
  ```
87
87
 
88
+ or
89
+
90
+ ```bash
91
+ npx overtake
92
+ ```
93
+
88
94
  Please take a look at [benchmarks](__benchmarks__) to see more examples
89
95
 
90
96
  ## License
package/cli.js CHANGED
@@ -1,28 +1,23 @@
1
1
  #!/usr/bin/env node
2
+
3
+ import { promisify } from 'util';
2
4
  import Path from 'path';
3
5
  import glob from 'glob';
4
- import { benchmark, setup, teardown, measure, perform, suites, runner } from './index.js';
6
+ import { load, benchmark, setup, teardown, measure, perform, run, defaultReporter } from './index.js';
5
7
 
8
+ const globAsync = promisify(glob);
6
9
  const pattern = process.argv[2] || '**/__benchmarks__/**/*.js';
7
10
 
8
- Object.assign(globalThis, {
9
- benchmark,
10
- setup,
11
- teardown,
12
- measure,
13
- perform,
14
- });
11
+ Object.assign(globalThis, { benchmark, setup, teardown, measure, perform });
15
12
 
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);
13
+ (async () => {
14
+ const files = await globAsync(pattern);
15
+ const scripts = [];
16
+ for (const file of files) {
17
+ const filename = Path.resolve(file);
18
+ const script = await load(filename);
19
+ scripts.push(script);
27
20
  }
28
- });
21
+
22
+ await run(scripts, defaultReporter);
23
+ })().catch((e) => console.error(e));
package/index.d.ts CHANGED
@@ -1,15 +1,56 @@
1
1
  declare global {
2
- declare function benchmark(title: string, init: () => void): void;
2
+ type CanBePromise<T> = Promise<T> | T;
3
3
 
4
- declare function setup<C>(init: () => C): any;
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
+ }
5
30
 
6
- declare function teardown<C>(teardown: (context: C) => void): void;
31
+ type MeasureInitResult = CanBePromise<() => void>;
7
32
 
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;
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;
11
37
 
12
- declare function perform<A>(title: string, counter: number, args: A): void;
13
- }
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;
14
43
 
15
- export { benchmark, setup, teardown, measure, perform };
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,67 +1,112 @@
1
+ import { createContext } from 'conode';
1
2
  import WorkerThreads from 'worker_threads';
2
3
 
3
- export const suites = [];
4
+ const overtakeContext = createContext();
5
+ const suiteContext = createContext();
4
6
 
5
- const NOOP = () => {};
7
+ export const NOOP = () => {};
6
8
 
7
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', '⠿'];
8
- const SPINNER_INTERVAL = 80;
9
+ export const setup = (fn) => {
10
+ suiteContext.getContext().setup = fn;
11
+ };
9
12
 
10
- const ACCURACY = 6;
13
+ export const teardown = (fn) => {
14
+ suiteContext.getContext().teardown = fn;
15
+ };
11
16
 
12
- const renderSpinner = (index) => {
13
- process.stdout.moveCursor(0, -1);
14
- process.stdout.write(SPINNER[index]);
15
- process.stdout.moveCursor(-3, 1);
17
+ export const measure = (title, fn) => {
18
+ suiteContext.getContext().measures.push({ title, init: fn });
16
19
  };
17
20
 
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;
30
- }
21
+ export const perform = (title, count, args) => {
22
+ suiteContext.getContext().performs.push({ title, count, args });
23
+ };
31
24
 
32
- export function getSuite() {
33
- const suite = suites[suites.length - 1];
34
- if (!suite.current) {
35
- throw new Error('should be inside benchmark');
36
- }
37
- return suite;
38
- }
25
+ export const benchmark = (title, fn) => {
26
+ const setup = NOOP;
27
+ const teardown = NOOP;
28
+ const measures = [];
29
+ const performs = [];
39
30
 
40
- export function setup(init) {
41
- const suite = getSuite();
42
- suite.setup = init;
43
- }
31
+ overtakeContext.getContext().suites.push({
32
+ title,
33
+ setup,
34
+ teardown,
35
+ measures,
36
+ performs,
37
+ init: fn,
38
+ });
39
+ };
44
40
 
45
- export function teardown(init) {
46
- const suite = getSuite();
47
- suite.teardown = init;
48
- }
41
+ export const load = async (filename) => {
42
+ const suites = [];
43
+ const script = { filename, suites };
44
+ await overtakeContext.contextualize(script, () => import(filename));
49
45
 
50
- export function measure(title, init) {
51
- const suite = getSuite();
52
- suite.measures.push({ title, init });
53
- }
46
+ return script;
47
+ };
48
+ const map = {
49
+ script: '⭐ Script ',
50
+ suite: '⇶ Suite ',
51
+ perform: '➤ Perform ',
52
+ measure: '✓ Measure',
53
+ };
54
54
 
55
- export function perform(title, count, args) {
56
- const suite = getSuite();
57
- suite.performs.push({ title, count, args });
58
- }
55
+ export const defaultReporter = async (type, title, test) => {
56
+ console.group(`${map[type]} ${title}`);
57
+ await test({ test: defaultReporter, output: (report) => console.table(report) });
58
+ console.groupEnd();
59
+ };
59
60
 
61
+ const ACCURACY = 6;
60
62
  export function formatFloat(value, digits = ACCURACY) {
61
63
  return parseFloat(value.toFixed(digits));
62
64
  }
63
65
 
64
- export async function runWorker({ args, count, ...options }) {
66
+ export const run = async (scripts, reporter) => {
67
+ for (const script of scripts) {
68
+ await reporter('script', script.filename, async (scriptTest) => {
69
+ for (const suite of script.suites) {
70
+ await scriptTest.test('suite', suite.title, async (suiteTest) => {
71
+ await suiteContext.contextualize(suite, suite.init);
72
+ for (const perform of suite.performs) {
73
+ await suiteTest.test('perform', perform.title, async (performTest) => {
74
+ for (const measure of suite.measures) {
75
+ await performTest.test('measure', measure.title, async (measureTest) => {
76
+ const result = await runWorker({
77
+ setup: suite.setup,
78
+ teardown: suite.teardown,
79
+ init: measure.init,
80
+ count: perform.count,
81
+ args: perform.args,
82
+ });
83
+ if (result.success) {
84
+ measureTest.output({
85
+ [formatFloat(result.mode)]: {
86
+ total: formatFloat(result.total),
87
+ med: formatFloat(result.med),
88
+ p95: formatFloat(result.p95),
89
+ p99: formatFloat(result.p99),
90
+ },
91
+ });
92
+ } else {
93
+ measureTest.output({
94
+ error: {
95
+ reason: result.error,
96
+ },
97
+ });
98
+ }
99
+ });
100
+ }
101
+ });
102
+ }
103
+ });
104
+ }
105
+ });
106
+ }
107
+ };
108
+
109
+ export async function runWorker({ args, count, ...options }, onProgress = null) {
65
110
  const setupCode = options.setup.toString();
66
111
  const teardownCode = options.teardown.toString();
67
112
  const initCode = options.init.toString();
@@ -72,81 +117,42 @@ export async function runWorker({ args, count, ...options }) {
72
117
  count,
73
118
  args,
74
119
  });
75
- let i = 0;
76
- const spinnerSize = SPINNER.length - 1;
77
-
78
- const timerId = setInterval(() => renderSpinner(i++ % spinnerSize), SPINNER_INTERVAL);
79
120
 
80
121
  const worker = new WorkerThreads.Worker(new URL('runner.js', import.meta.url), { argv: [params] });
81
122
  return new Promise((resolve) => {
82
- worker.on('message', resolve);
123
+ worker.on('message', (data) => {
124
+ if (onProgress && data.type === 'progress') {
125
+ onProgress(data);
126
+ } else if (data.type === 'report') {
127
+ resolve(data);
128
+ }
129
+ });
83
130
  worker.on('error', (error) => resolve({ success: false, error: error.message }));
84
- }).finally((result) => {
85
- clearInterval(timerId);
86
- renderSpinner(spinnerSize);
87
-
88
- return result;
89
131
  });
90
132
  }
91
133
 
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
134
  const FALSE_START = () => {
135
135
  throw new Error('False start');
136
136
  };
137
137
 
138
138
  export async function start(input) {
139
- const { setupCode, teardownCode, initCode, count, args = [[]] } = JSON.parse(input);
139
+ const { setupCode, teardownCode, initCode, count, reportInterval = 500, args = [[]] } = JSON.parse(input);
140
140
  const setup = Function(`return ${setupCode};`)();
141
141
  const teardown = Function(`return ${teardownCode};`)();
142
142
  const init = Function(`return ${initCode};`)();
143
+ const send = WorkerThreads.parentPort ? (data) => WorkerThreads.parentPort.postMessage(data) : (data) => console.log(data);
143
144
 
144
145
  let i = count;
145
146
  let done = FALSE_START;
146
147
 
147
148
  const timings = [];
148
149
  const argSize = args.length;
150
+
151
+ send({ type: 'progress', stage: 'setup' });
152
+ const startMark = performance.now();
149
153
  const context = await setup();
154
+ const setupMark = performance.now();
155
+
150
156
  const initArgs = [() => done()];
151
157
  if (init.length > 2) {
152
158
  initArgs.unshift(args);
@@ -154,70 +160,98 @@ export async function start(input) {
154
160
  if (init.length > 1) {
155
161
  initArgs.unshift(context);
156
162
  }
157
- const startMark = performance.now();
163
+
164
+ send({ type: 'progress', stage: 'init' });
165
+ const initMark = performance.now();
158
166
  const action = await init(...initArgs);
159
- const workMark = performance.now();
167
+ const initDoneMark = performance.now();
160
168
 
161
169
  try {
170
+ let lastCheck = performance.now();
162
171
  const loop = (resolve, reject) => {
163
- const argIdx = i % argSize;
172
+ const idx = count - i;
173
+ const argIdx = idx % argSize;
164
174
  const timerId = setTimeout(reject, 10000, new Error('Timeout'));
165
175
 
166
176
  done = () => {
167
- // eslint-disable-next-line
168
- const elapsed = performance.now() - startTickTime;
177
+ const doneTick = performance.now();
178
+ const elapsed = doneTick - startTickTime;
169
179
  clearTimeout(timerId);
170
180
  done = FALSE_START;
171
181
  timings.push(elapsed);
172
-
182
+ if (doneTick - lastCheck > reportInterval) {
183
+ lastCheck = doneTick;
184
+ send({ type: 'progress', stage: 'cycles', progress: idx / count });
185
+ }
173
186
  resolve();
174
187
  };
188
+
175
189
  const startTickTime = performance.now();
176
- action(...args[argIdx], count - i);
190
+ action(...args[argIdx], idx);
177
191
  };
192
+ const cyclesMark = performance.now();
178
193
 
194
+ send({ type: 'progress', stage: 'cycles', progress: 0 });
179
195
  while (i--) {
180
196
  await new Promise(loop);
181
197
  }
198
+ send({ type: 'progress', stage: 'teardown' });
199
+ const teardownMark = performance.now();
200
+ await teardown(context);
182
201
  const completeMark = performance.now();
202
+ send({ type: 'progress', stage: 'complete', progress: (count - i) / count });
183
203
 
184
- await teardown(context);
204
+ timings.sort((a, b) => a - b);
205
+
206
+ const min = timings[0];
207
+ const max = timings[timings.length - 1];
208
+ const range = max - min || Number.MIN_VALUE;
209
+ const sum = timings.reduce((a, b) => a + b, 0);
210
+ const avg = sum / timings.length;
185
211
 
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({
212
+ const step = range / 99 || Number.MIN_VALUE;
213
+ const buckets = Array(100)
214
+ .fill(0)
215
+ .map((_, idx) => [min + idx * step, 0]);
216
+
217
+ // Calc mode O(n)
218
+ timings.forEach((timing, idx) => {
219
+ const index = Math.round((timing - min) / step);
220
+ buckets[index][1] += 1;
221
+ });
222
+ buckets.sort((a, b) => a[1] - b[1]);
223
+
224
+ const percentile = (p) => timings[Math.trunc((p * timings.length) / 100)];
225
+ const mode = buckets[buckets.length - 1][0];
226
+
227
+ send({
228
+ type: 'report',
229
+ success: true,
230
+ count: timings.length,
209
231
  min,
210
232
  max,
233
+ sum,
211
234
  avg,
212
- p90,
213
- p95,
214
- p99,
215
235
  mode,
216
- setup: workMark - startMark,
217
- work: completeMark - workMark,
218
- success: true,
236
+ p1: percentile(1),
237
+ p5: percentile(5),
238
+ p10: percentile(10),
239
+ p20: percentile(20),
240
+ p33: percentile(33),
241
+ p50: percentile(50),
242
+ med: percentile(50),
243
+ p66: percentile(66),
244
+ p80: percentile(80),
245
+ p90: percentile(90),
246
+ p95: percentile(95),
247
+ p99: percentile(99),
248
+ setup: setupMark - startMark,
249
+ init: initDoneMark - initMark,
250
+ cycles: teardownMark - cyclesMark,
251
+ teardown: completeMark - teardownMark,
252
+ total: completeMark - setupMark,
219
253
  });
220
254
  } catch (error) {
221
- WorkerThreads.parentPort.postMessage({ success: false, error: error.stack });
255
+ send({ type: 'report', success: false, error: error.stack });
222
256
  }
223
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "0.0.1-rc1",
3
+ "version": "0.0.2",
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
+ "conode": "^0.1.0",
46
43
  "glob": "^8.0.1"
47
44
  }
48
45
  }