overtake 0.1.2 → 1.0.0-rc.0

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 (70) hide show
  1. package/README.md +72 -79
  2. package/bin/overtake.js +2 -0
  3. package/build/__tests__/runner.d.ts +1 -0
  4. package/build/benchmark.cjs +237 -0
  5. package/build/benchmark.cjs.map +1 -0
  6. package/build/benchmark.d.ts +64 -0
  7. package/build/benchmark.js +189 -0
  8. package/build/benchmark.js.map +1 -0
  9. package/build/cli.cjs +149 -0
  10. package/build/cli.cjs.map +1 -0
  11. package/build/cli.d.ts +1 -0
  12. package/build/cli.js +104 -0
  13. package/build/cli.js.map +1 -0
  14. package/build/executor.cjs +68 -0
  15. package/build/executor.cjs.map +1 -0
  16. package/build/executor.d.ts +10 -0
  17. package/build/executor.js +58 -0
  18. package/build/executor.js.map +1 -0
  19. package/build/index.cjs +20 -0
  20. package/build/index.cjs.map +1 -0
  21. package/build/index.d.ts +5 -0
  22. package/build/index.js +3 -0
  23. package/build/index.js.map +1 -0
  24. package/build/queue.cjs +48 -0
  25. package/build/queue.cjs.map +1 -0
  26. package/build/queue.d.ts +3 -0
  27. package/build/queue.js +38 -0
  28. package/build/queue.js.map +1 -0
  29. package/build/reporter.cjs +175 -0
  30. package/build/reporter.cjs.map +1 -0
  31. package/build/reporter.d.ts +11 -0
  32. package/build/reporter.js +157 -0
  33. package/build/reporter.js.map +1 -0
  34. package/build/runner.cjs +92 -0
  35. package/build/runner.cjs.map +1 -0
  36. package/build/runner.d.ts +2 -0
  37. package/build/runner.js +82 -0
  38. package/build/runner.js.map +1 -0
  39. package/build/types.cjs +48 -0
  40. package/build/types.cjs.map +1 -0
  41. package/build/types.d.ts +59 -0
  42. package/build/types.js +21 -0
  43. package/build/types.js.map +1 -0
  44. package/build/utils.cjs +100 -0
  45. package/build/utils.cjs.map +1 -0
  46. package/build/utils.d.ts +20 -0
  47. package/build/utils.js +67 -0
  48. package/build/utils.js.map +1 -0
  49. package/build/worker.cjs +29 -0
  50. package/build/worker.cjs.map +1 -0
  51. package/build/worker.d.ts +1 -0
  52. package/build/worker.js +25 -0
  53. package/build/worker.js.map +1 -0
  54. package/package.json +13 -11
  55. package/src/__tests__/runner.ts +34 -0
  56. package/src/benchmark.ts +231 -0
  57. package/src/cli.ts +114 -0
  58. package/src/executor.ts +73 -0
  59. package/src/index.ts +6 -0
  60. package/src/queue.ts +42 -0
  61. package/src/reporter.ts +139 -0
  62. package/src/runner.ts +111 -0
  63. package/src/types.ts +72 -0
  64. package/src/utils.ts +65 -0
  65. package/src/worker.ts +46 -0
  66. package/tsconfig.json +17 -0
  67. package/cli.js +0 -70
  68. package/index.d.ts +0 -56
  69. package/index.js +0 -303
  70. package/runner.js +0 -3
package/src/runner.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { Options, Control } from './types.js';
2
+
3
+ const COMPLETE_VALUE = 100_00;
4
+
5
+ const runSync = (run: Function) => {
6
+ return (...args: unknown[]) => {
7
+ const start = process.hrtime.bigint();
8
+ run(...args);
9
+ return process.hrtime.bigint() - start;
10
+ };
11
+ };
12
+
13
+ const runAsync = (run: Function) => {
14
+ return async (...args: unknown[]) => {
15
+ const start = process.hrtime.bigint();
16
+ await run(...args);
17
+ return process.hrtime.bigint() - start;
18
+ };
19
+ };
20
+
21
+ export const benchmark = async <TContext, TInput>({
22
+ setup,
23
+ teardown,
24
+ pre,
25
+ run: runRaw,
26
+ post,
27
+ data,
28
+
29
+ warmupCycles,
30
+ minCycles,
31
+ absThreshold,
32
+ relThreshold,
33
+
34
+ durationsSAB,
35
+ controlSAB,
36
+ }: Required<Options<TContext, TInput>>) => {
37
+ const durations = new BigUint64Array(durationsSAB);
38
+ const control = new Int32Array(controlSAB);
39
+
40
+ control[Control.INDEX] = 0;
41
+ control[Control.PROGRESS] = 0;
42
+ control[Control.COMPLETE] = 255;
43
+
44
+ const context = (await setup?.()) as TContext;
45
+ const maxCycles = durations.length;
46
+
47
+ try {
48
+ await pre?.(context, data!);
49
+ const result = runRaw(context, data!);
50
+ await post?.(context, data!);
51
+ const run = result instanceof Promise ? runAsync(runRaw) : runSync(runRaw);
52
+ const start = Date.now();
53
+ while (Date.now() - start < 1_000) {
54
+ Math.sqrt(Math.random());
55
+ }
56
+ for (let i = 0; i < warmupCycles; i++) {
57
+ await pre?.(context, data!);
58
+ await run(context, data);
59
+ await post?.(context, data!);
60
+ }
61
+
62
+ let i = 0;
63
+ let mean = 0n;
64
+ let m2 = 0n;
65
+
66
+ while (true) {
67
+ if (i >= maxCycles) break;
68
+
69
+ await pre?.(context, data!);
70
+ const duration = await run(context, data);
71
+ await post?.(context, data!);
72
+
73
+ durations[i++] = duration;
74
+ const delta = duration - mean;
75
+ mean += delta / BigInt(i);
76
+ m2 += delta * (duration - mean);
77
+
78
+ const progress = Math.max(i / maxCycles) * COMPLETE_VALUE;
79
+ control[Control.PROGRESS] = progress;
80
+
81
+ if (i >= minCycles) {
82
+ const variance = Number(m2) / (i - 1);
83
+ const stddev = Math.sqrt(variance);
84
+ if (stddev <= Number(absThreshold)) {
85
+ break;
86
+ }
87
+
88
+ const meanNum = Number(mean);
89
+ const cov = stddev / (meanNum || 1);
90
+ if (cov <= relThreshold) {
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ control[Control.INDEX] = i;
97
+ control[Control.COMPLETE] = 0;
98
+ } catch (e) {
99
+ console.error(e && typeof e === 'object' && 'stack' in e ? e.stack : e);
100
+ control[Control.COMPLETE] = 1;
101
+ } finally {
102
+ try {
103
+ await teardown?.(context);
104
+ } catch (e) {
105
+ control[Control.COMPLETE] = 2;
106
+ console.error(e && typeof e === 'object' && 'stack' in e ? e.stack : e);
107
+ }
108
+ }
109
+
110
+ return control[Control.COMPLETE];
111
+ };
package/src/types.ts ADDED
@@ -0,0 +1,72 @@
1
+ export type MaybePromise<T> = Promise<T> | PromiseLike<T> | T;
2
+
3
+ export interface SetupFn<TContext> {
4
+ (): MaybePromise<TContext>;
5
+ }
6
+
7
+ export interface TeardownFn<TContext> {
8
+ (ctx: TContext): MaybePromise<void>;
9
+ }
10
+
11
+ export interface StepFn<TContext, TInput> {
12
+ (ctx: TContext, input: TInput): MaybePromise<void>;
13
+ }
14
+
15
+ export interface FeedFn<TInput> {
16
+ (): MaybePromise<TInput>;
17
+ }
18
+
19
+ type _Sequence<To extends number, R extends unknown[]> = R['length'] extends To ? R[number] : _Sequence<To, [R['length'], ...R]>;
20
+ export type Sequence<To extends number> = number extends To ? number : _Sequence<To, []>;
21
+ export type Between<From extends number, To extends number> = Exclude<Sequence<To>, Sequence<From>>;
22
+
23
+ export type ReportType = 'ops' | 'min' | 'max' | 'mean' | 'median' | 'mode' | `p${Between<1, 100>}`;
24
+ export type ReportTypeList = readonly ReportType[];
25
+ export const REPORT_TYPES: ReportTypeList = Array.from({ length: 99 }, (_, idx) => `p${idx + 1}` as ReportType).concat(['ops', 'mean', 'min', 'max', 'median', 'mode']);
26
+
27
+ export interface ReportOptions<R extends ReportTypeList> {
28
+ reportTypes: R;
29
+ }
30
+
31
+ export interface BenchmarkOptions {
32
+ warmupCycles?: number;
33
+ minCycles?: number;
34
+ absThreshold?: number; // ns
35
+ relThreshold?: number; // %
36
+ }
37
+
38
+ export interface RunOptions<TContext, TInput> {
39
+ setup?: SetupFn<TContext>;
40
+ teardown?: TeardownFn<TContext>;
41
+ pre?: StepFn<TContext, TInput>;
42
+ run: StepFn<TContext, TInput>;
43
+ post?: StepFn<TContext, TInput>;
44
+ data?: TInput;
45
+ }
46
+
47
+ export interface WorkerOptions extends Required<BenchmarkOptions> {
48
+ setupCode?: string;
49
+ teardownCode?: string;
50
+ preCode?: string;
51
+ runCode: string;
52
+ postCode?: string;
53
+ data?: unknown;
54
+
55
+ durationsSAB: SharedArrayBuffer;
56
+ controlSAB: SharedArrayBuffer;
57
+ }
58
+
59
+ export interface Options<TContext, TInput> extends RunOptions<TContext, TInput>, BenchmarkOptions {
60
+ durationsSAB: SharedArrayBuffer;
61
+ controlSAB: SharedArrayBuffer;
62
+ }
63
+
64
+ export enum Control {
65
+ INDEX,
66
+ PROGRESS,
67
+ COMPLETE,
68
+ }
69
+
70
+ export const CONTROL_SLOTS = Object.values(Control).length / 2;
71
+ export const DEFAULT_CYCLES = 1_000;
72
+ export const Z95 = 1.96;
package/src/utils.ts ADDED
@@ -0,0 +1,65 @@
1
+ export const abs = (value: bigint) => {
2
+ if (value < 0n) {
3
+ return -value;
4
+ }
5
+ return value;
6
+ };
7
+ export const cmp = (a: bigint | number, b: bigint | number): number => {
8
+ if (a > b) {
9
+ return 1;
10
+ }
11
+ if (a < b) {
12
+ return -1;
13
+ }
14
+ return 0;
15
+ };
16
+
17
+ export const max = (a: bigint, b: bigint) => {
18
+ if (a > b) {
19
+ return a;
20
+ }
21
+ return b;
22
+ };
23
+
24
+ export const divMod = (a: bigint, b: bigint) => {
25
+ return { quotient: a / b, remainder: a % b };
26
+ };
27
+
28
+ export function div(a: bigint, b: bigint, decimals: number = 2): string {
29
+ if (b === 0n) throw new RangeError('Division by zero');
30
+ const scale = 10n ** BigInt(decimals);
31
+ const scaled = (a * scale) / b;
32
+ const intPart = scaled / scale;
33
+ const fracPart = scaled % scale;
34
+ return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
35
+ }
36
+
37
+ export function divs(a: bigint, b: bigint, scale: bigint): bigint {
38
+ if (b === 0n) throw new RangeError('Division by zero');
39
+ return (a * scale) / b;
40
+ }
41
+
42
+ export class ScaledBigInt {
43
+ constructor(
44
+ public value: bigint,
45
+ public scale: bigint,
46
+ ) {}
47
+ add(value: bigint) {
48
+ this.value += value * this.scale;
49
+ }
50
+ sub(value: bigint) {
51
+ this.value -= value * this.scale;
52
+ }
53
+ div(value: bigint) {
54
+ this.value /= value;
55
+ }
56
+ mul(value: bigint) {
57
+ this.value *= value;
58
+ }
59
+ unscale() {
60
+ return this.value / this.value;
61
+ }
62
+ number() {
63
+ return Number(div(this.value, this.scale));
64
+ }
65
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { workerData } from 'node:worker_threads';
2
+ import { benchmark } from './runner.js';
3
+ import { SetupFn, TeardownFn, StepFn, WorkerOptions } from './types.js';
4
+
5
+ const {
6
+ setupCode,
7
+ teardownCode,
8
+ preCode,
9
+ runCode,
10
+ postCode,
11
+ data,
12
+
13
+ warmupCycles,
14
+ minCycles,
15
+ absThreshold,
16
+ relThreshold,
17
+
18
+ durationsSAB,
19
+ controlSAB,
20
+ }: WorkerOptions = workerData;
21
+
22
+ const setup: SetupFn<unknown> = setupCode && Function(`return ${setupCode};`)();
23
+ const teardown: TeardownFn<unknown> = teardownCode && Function(`return ${teardownCode};`)();
24
+
25
+ const pre: StepFn<unknown, unknown> = preCode && Function(`return ${preCode};`)();
26
+ const run: StepFn<unknown, unknown> = runCode && Function(`return ${runCode};`)();
27
+ const post: StepFn<unknown, unknown> = postCode && Function(`return ${postCode};`)();
28
+
29
+ const exitCode = await benchmark({
30
+ setup,
31
+ teardown,
32
+ pre,
33
+ run,
34
+ post,
35
+ data,
36
+
37
+ warmupCycles,
38
+ minCycles,
39
+ absThreshold,
40
+ relThreshold,
41
+
42
+ durationsSAB,
43
+ controlSAB,
44
+ });
45
+
46
+ process.exit(exitCode);
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "ESNext",
5
+ "module": "NodeNext",
6
+ "declaration": true,
7
+ "esModuleInterop": true,
8
+ "resolveJsonModule": true,
9
+ "allowJs": true,
10
+ "moduleResolution": "NodeNext",
11
+ "lib": ["ESNext"],
12
+ "rootDir": "src",
13
+ "outDir": "build"
14
+ },
15
+ "include": ["src/**/*.ts"],
16
+ "exclude": ["node_modules", "**/*.tmp.ts"]
17
+ }
package/cli.js DELETED
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env -S node --no-warnings
2
-
3
- import { Command, Option } from 'commander';
4
- import Path from 'path';
5
- import { glob } from 'glob';
6
- import { load, createScript, benchmark, setup, teardown, measure, perform, run, defaultReporter, allowedFields } from './index.js';
7
- import packageJson from './package.json' with { type: 'json' };
8
-
9
- const commands = new Command();
10
-
11
- commands.name('overtake').description(packageJson.description).version(packageJson.version, '-v, --version');
12
-
13
- commands
14
- .argument('[files...]', 'File paths or path patterns to search benchmark scripts')
15
- .option('-i, --inline [inline]', 'Inline benchmark.', (value, previous) => previous.concat([value]), [])
16
- .option('-c, --count [count]', 'Perform count for inline benchmark.', (v) => parseInt(v))
17
- .addOption(
18
- new Option('-f, --fields [fields]', `Comma separated list of fields to report. Allowed values are: ${allowedFields}.`)
19
- .default(['med', 'p95', 'p99', 'sum:total', 'count'])
20
- .argParser((fields) =>
21
- fields.split(',').filter((field) => {
22
- if (!allowedFields.includes(field)) {
23
- console.error(`Invalid field name: ${field}. Allowed values are: ${allowedFields.join(', ')}.`);
24
- process.exit(1);
25
- }
26
- return true;
27
- }),
28
- ),
29
- )
30
- .action(async (patterns, { count = 1, inline, fields }) => {
31
- Object.assign(globalThis, { benchmark, setup, teardown, measure, perform });
32
-
33
- const foundFiles = await glob(patterns);
34
- if (!foundFiles.length) {
35
- console.error(`No files found with patterns ${patterns.join(', ')}`);
36
- process.exit(1);
37
- }
38
- const files = [...new Set(foundFiles.map((filename) => Path.resolve(filename)).filter(Boolean))];
39
-
40
- const scripts = [];
41
- if (inline.length) {
42
- const inlineScript = await createScript('', () => {
43
- benchmark('', () => {
44
- inline.forEach((code) => {
45
- measure(code, `() => () => { ${code} }`);
46
- });
47
- perform('', count);
48
- });
49
- });
50
- scripts.push(inlineScript);
51
- }
52
-
53
- for (const file of files) {
54
- const filename = Path.resolve(file);
55
- const script = await load(filename);
56
- scripts.push(script);
57
- }
58
-
59
- await run(scripts, defaultReporter, fields);
60
- });
61
-
62
- commands.on('--help', () => {
63
- console.log('');
64
- console.log('Examples:');
65
- console.log(' $ overtake **/__benchmarks__/*.js');
66
- console.log(' $ overtake -i "class A{}" -i "function A(){}" -i "const A = () => {}" -c 1000000');
67
- console.log(' $ overtake -v');
68
- });
69
-
70
- commands.parse(process.argv);
package/index.d.ts DELETED
@@ -1,56 +0,0 @@
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
- }