overtake 2.0.3 → 2.1.1

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/build/cli.js CHANGED
@@ -23,6 +23,7 @@ const { values: opts, positionals: patterns } = parseArgs({
23
23
  'max-cycles': { type: 'string' },
24
24
  'min-cycles': { type: 'string' },
25
25
  'no-gc-observer': { type: 'boolean' },
26
+ 'pin-cores': { type: 'boolean' },
26
27
  progress: { type: 'boolean' },
27
28
  'save-baseline': { type: 'string' },
28
29
  'compare-baseline': { type: 'string' },
@@ -49,6 +50,7 @@ Options:
49
50
  --max-cycles <n> maximum measurement cycles per feed
50
51
  --min-cycles <n> minimum measurement cycles per feed
51
52
  --no-gc-observer disable GC overlap detection
53
+ --pin-cores pin each worker to a dedicated CPU core (Linux)
52
54
  --progress show progress bar
53
55
  --save-baseline <file> save results to baseline file
54
56
  --compare-baseline <file> compare results against baseline file
@@ -69,6 +71,7 @@ const executeOptions = {
69
71
  maxCycles: opts['max-cycles'] ? parseInt(opts['max-cycles']) : undefined,
70
72
  minCycles: opts['min-cycles'] ? parseInt(opts['min-cycles']) : undefined,
71
73
  gcObserver: !opts['no-gc-observer'],
74
+ pinCores: opts['pin-cores'] ?? false,
72
75
  progress: opts.progress ?? false,
73
76
  format,
74
77
  };
@@ -9,6 +9,7 @@ export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report>
9
9
  export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOptions, ReportOptions<R> {
10
10
  workers?: number;
11
11
  maxCycles?: number;
12
+ pinCores?: boolean;
12
13
  onProgress?: ProgressCallback;
13
14
  progressInterval?: number;
14
15
  }
package/build/executor.js CHANGED
@@ -1,14 +1,21 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
  import { once } from 'node:events';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { cpus } from 'node:os';
4
5
  import { createReport, computeStats, Report } from './reporter.js';
5
6
  import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
6
7
  import { Control, CONTROL_SLOTS, COMPLETE_VALUE, } from './types.js';
7
8
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
8
9
  export const createExecutor = (options) => {
9
- const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, onProgress, progressInterval = 100 } = options;
10
+ const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, pinCores = false, onProgress, progressInterval = 100 } = options;
10
11
  const benchmarkUrl = options[BENCHMARK_URL];
11
12
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
13
+ let coreList = null;
14
+ if (pinCores) {
15
+ const count = cpus().length;
16
+ coreList = count > 1 ? Array.from({ length: count - 1 }, (_, i) => i + 1) : [0];
17
+ }
18
+ let nextCoreIdx = 0;
12
19
  const pending = [];
13
20
  const activeWorkers = new Set();
14
21
  let running = 0;
@@ -50,6 +57,7 @@ export const createExecutor = (options) => {
50
57
  assertNoClosure(postCode, 'post');
51
58
  const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
52
59
  const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
60
+ const cpuPin = coreList !== null ? coreList[nextCoreIdx++ % coreList.length] : undefined;
53
61
  const workerFile = new URL('./worker.js', import.meta.url);
54
62
  const workerData = {
55
63
  benchmarkUrl: resolvedBenchmarkUrl,
@@ -59,6 +67,7 @@ export const createExecutor = (options) => {
59
67
  runCode,
60
68
  postCode,
61
69
  data,
70
+ cpuPin,
62
71
  warmupCycles,
63
72
  minCycles,
64
73
  absThreshold,
package/build/index.d.ts CHANGED
@@ -71,7 +71,7 @@ export declare const printHistogramReports: <R extends ReportTypeList>(reports:
71
71
  export interface BaselineData {
72
72
  version: number;
73
73
  timestamp: string;
74
- results: Record<string, Record<string, number>>;
74
+ results: Record<string, Record<string, number | boolean>>;
75
75
  }
76
76
  export declare const reportsToBaseline: <R extends ReportTypeList>(reports: TargetReport<R>[]) => BaselineData;
77
77
  export declare const printComparisonReports: <R extends ReportTypeList>(reports: TargetReport<R>[], baseline: BaselineData, threshold?: number) => void;
package/build/index.js CHANGED
@@ -99,7 +99,7 @@ export class Benchmark {
99
99
  return new Target(target);
100
100
  }
101
101
  async execute(options = {}) {
102
- const { workers = DEFAULT_WORKERS, warmupCycles = 20, maxCycles = DEFAULT_CYCLES, minCycles = 50, absThreshold = 1_000, relThreshold = 0.02, gcObserver = true, reportTypes = DEFAULT_REPORT_TYPES, progress = false, progressInterval = 100, } = options;
102
+ const { workers = DEFAULT_WORKERS, warmupCycles = 20, maxCycles = DEFAULT_CYCLES, minCycles = 50, absThreshold = 1_000, relThreshold = 0.02, gcObserver = true, reportTypes = DEFAULT_REPORT_TYPES, progress = false, progressInterval = 100, pinCores = false, } = options;
103
103
  if (this.#executed) {
104
104
  throw new Error("Benchmark is executed and can't be reused");
105
105
  }
@@ -134,6 +134,7 @@ export class Benchmark {
134
134
  relThreshold,
135
135
  gcObserver,
136
136
  reportTypes,
137
+ pinCores,
137
138
  onProgress,
138
139
  progressInterval,
139
140
  [BENCHMARK_URL]: benchmarkUrl,
@@ -350,6 +351,19 @@ export const printComparisonReports = (reports, baseline, threshold = 5) => {
350
351
  continue;
351
352
  const current = value.valueOf();
352
353
  const baselineValue = baselineData?.[metric];
354
+ if (typeof current === 'boolean') {
355
+ if (baselineValue === undefined) {
356
+ console.log(` * ${metric}: ${current} (new)`);
357
+ }
358
+ else if (current === Boolean(baselineValue)) {
359
+ console.log(` ${metric}: ${current}`);
360
+ }
361
+ else {
362
+ const indicator = current ? '\x1b[31m!\x1b[0m' : '\x1b[32m+\x1b[0m';
363
+ console.log(` ${indicator} ${metric}: ${current} (was ${baselineValue})`);
364
+ }
365
+ continue;
366
+ }
353
367
  if (baselineValue !== undefined && baselineValue !== 0) {
354
368
  const change = ((current - baselineValue) / baselineValue) * 100;
355
369
  const isOps = metric === 'ops';
package/build/types.d.ts CHANGED
@@ -46,6 +46,7 @@ export interface WorkerOptions extends Required<BenchmarkOptions> {
46
46
  runCode: string;
47
47
  postCode?: string;
48
48
  data?: unknown;
49
+ cpuPin?: number;
49
50
  durationsSAB: SharedArrayBuffer;
50
51
  controlSAB: SharedArrayBuffer;
51
52
  }
package/build/worker.js CHANGED
@@ -2,12 +2,24 @@ import { workerData } from 'node:worker_threads';
2
2
  import { SourceTextModule, SyntheticModule } from 'node:vm';
3
3
  import { createRequire, register } from 'node:module';
4
4
  import { isAbsolute } from 'node:path';
5
+ import { readFileSync } from 'node:fs';
6
+ import { execFileSync } from 'node:child_process';
5
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
8
  import { benchmark } from './runner.js';
7
9
  import {} from './types.js';
8
10
  import { resolveHookUrl } from './utils.js';
9
11
  register(resolveHookUrl);
10
- const { benchmarkUrl, setupCode, teardownCode, preCode, runCode, postCode, data, warmupCycles, minCycles, absThreshold, relThreshold, gcObserver = true, durationsSAB, controlSAB, } = workerData;
12
+ const { benchmarkUrl, setupCode, teardownCode, preCode, runCode, postCode, data, cpuPin, warmupCycles, minCycles, absThreshold, relThreshold, gcObserver = true, durationsSAB, controlSAB, } = workerData;
13
+ if (cpuPin !== undefined && process.platform === 'linux') {
14
+ try {
15
+ const status = readFileSync('/proc/thread-self/status', 'utf8');
16
+ const tid = status.match(/^Pid:\t(\d+)/m)?.[1];
17
+ if (tid) {
18
+ execFileSync('taskset', ['-cp', String(cpuPin), tid], { stdio: 'ignore' });
19
+ }
20
+ }
21
+ catch { }
22
+ }
11
23
  const serialize = (code) => (code ? code : 'undefined');
12
24
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
13
25
  const benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "type": "module",
6
6
  "types": "build/index.d.ts",
package/src/cli.ts CHANGED
@@ -39,6 +39,7 @@ const { values: opts, positionals: patterns } = parseArgs({
39
39
  'max-cycles': { type: 'string' },
40
40
  'min-cycles': { type: 'string' },
41
41
  'no-gc-observer': { type: 'boolean' },
42
+ 'pin-cores': { type: 'boolean' },
42
43
  progress: { type: 'boolean' },
43
44
  'save-baseline': { type: 'string' },
44
45
  'compare-baseline': { type: 'string' },
@@ -67,6 +68,7 @@ Options:
67
68
  --max-cycles <n> maximum measurement cycles per feed
68
69
  --min-cycles <n> minimum measurement cycles per feed
69
70
  --no-gc-observer disable GC overlap detection
71
+ --pin-cores pin each worker to a dedicated CPU core (Linux)
70
72
  --progress show progress bar
71
73
  --save-baseline <file> save results to baseline file
72
74
  --compare-baseline <file> compare results against baseline file
@@ -89,6 +91,7 @@ const executeOptions = {
89
91
  maxCycles: opts['max-cycles'] ? parseInt(opts['max-cycles']) : undefined,
90
92
  minCycles: opts['min-cycles'] ? parseInt(opts['min-cycles']) : undefined,
91
93
  gcObserver: !opts['no-gc-observer'],
94
+ pinCores: opts['pin-cores'] ?? false,
92
95
  progress: opts.progress ?? false,
93
96
  format,
94
97
  };
package/src/executor.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
  import { once } from 'node:events';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { cpus } from 'node:os';
4
5
  import { createReport, computeStats, Report } from './reporter.js';
5
6
  import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
6
7
  import {
@@ -26,6 +27,7 @@ export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report>
26
27
  export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOptions, ReportOptions<R> {
27
28
  workers?: number;
28
29
  maxCycles?: number;
30
+ pinCores?: boolean;
29
31
  onProgress?: ProgressCallback;
30
32
  progressInterval?: number;
31
33
  }
@@ -38,10 +40,17 @@ export interface Executor<TContext, TInput> {
38
40
  }
39
41
 
40
42
  export const createExecutor = <TContext, TInput, R extends ReportTypeList>(options: Required<ExecutorOptions<R>>): Executor<TContext, TInput> => {
41
- const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, onProgress, progressInterval = 100 } = options;
43
+ const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes, pinCores = false, onProgress, progressInterval = 100 } = options;
42
44
  const benchmarkUrl = (options as Record<symbol, unknown>)[BENCHMARK_URL];
43
45
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
44
46
 
47
+ let coreList: number[] | null = null;
48
+ if (pinCores) {
49
+ const count = cpus().length;
50
+ coreList = count > 1 ? Array.from({ length: count - 1 }, (_, i) => i + 1) : [0];
51
+ }
52
+ let nextCoreIdx = 0;
53
+
45
54
  const pending: { task: ExecutorRunOptions<TContext, TInput>; resolve: (v: unknown) => void; reject: (e: unknown) => void }[] = [];
46
55
  const activeWorkers = new Set<Worker>();
47
56
  let running = 0;
@@ -84,6 +93,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
84
93
  const controlSAB = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * CONTROL_SLOTS);
85
94
  const durationsSAB = new SharedArrayBuffer(BigUint64Array.BYTES_PER_ELEMENT * maxCycles);
86
95
 
96
+ const cpuPin = coreList !== null ? coreList[nextCoreIdx++ % coreList.length] : undefined;
87
97
  const workerFile = new URL('./worker.js', import.meta.url);
88
98
  const workerData: WorkerOptions = {
89
99
  benchmarkUrl: resolvedBenchmarkUrl,
@@ -93,6 +103,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
93
103
  runCode,
94
104
  postCode,
95
105
  data,
106
+ cpuPin,
96
107
 
97
108
  warmupCycles,
98
109
  minCycles,
package/src/index.ts CHANGED
@@ -162,6 +162,7 @@ export class Benchmark<TInput> {
162
162
  reportTypes = DEFAULT_REPORT_TYPES as unknown as R,
163
163
  progress = false,
164
164
  progressInterval = 100,
165
+ pinCores = false,
165
166
  } = options;
166
167
  if (this.#executed) {
167
168
  throw new Error("Benchmark is executed and can't be reused");
@@ -201,6 +202,7 @@ export class Benchmark<TInput> {
201
202
  relThreshold,
202
203
  gcObserver,
203
204
  reportTypes,
205
+ pinCores,
204
206
  onProgress,
205
207
  progressInterval,
206
208
  [BENCHMARK_URL]: benchmarkUrl,
@@ -392,11 +394,11 @@ export const printHistogramReports = <R extends ReportTypeList>(reports: TargetR
392
394
  export interface BaselineData {
393
395
  version: number;
394
396
  timestamp: string;
395
- results: Record<string, Record<string, number>>;
397
+ results: Record<string, Record<string, number | boolean>>;
396
398
  }
397
399
 
398
400
  export const reportsToBaseline = <R extends ReportTypeList>(reports: TargetReport<R>[]): BaselineData => {
399
- const results: Record<string, Record<string, number>> = {};
401
+ const results: Record<string, Record<string, number | boolean>> = {};
400
402
  for (const report of reports) {
401
403
  for (const { measure, feeds } of report.measures) {
402
404
  for (const { feed, data } of feeds) {
@@ -439,8 +441,20 @@ export const printComparisonReports = <R extends ReportTypeList>(reports: Target
439
441
  const current = (value as { valueOf(): number }).valueOf();
440
442
  const baselineValue = baselineData?.[metric];
441
443
 
444
+ if (typeof current === 'boolean') {
445
+ if (baselineValue === undefined) {
446
+ console.log(` * ${metric}: ${current} (new)`);
447
+ } else if (current === Boolean(baselineValue)) {
448
+ console.log(` ${metric}: ${current}`);
449
+ } else {
450
+ const indicator = current ? '\x1b[31m!\x1b[0m' : '\x1b[32m+\x1b[0m';
451
+ console.log(` ${indicator} ${metric}: ${current} (was ${baselineValue})`);
452
+ }
453
+ continue;
454
+ }
455
+
442
456
  if (baselineValue !== undefined && baselineValue !== 0) {
443
- const change = ((current - baselineValue) / baselineValue) * 100;
457
+ const change = ((current - (baselineValue as number)) / (baselineValue as number)) * 100;
444
458
  const isOps = metric === 'ops';
445
459
  const improved = isOps ? change > threshold : change < -threshold;
446
460
  const regressed = isOps ? change < -threshold : change > threshold;
package/src/types.ts CHANGED
@@ -89,6 +89,7 @@ export interface WorkerOptions extends Required<BenchmarkOptions> {
89
89
  runCode: string;
90
90
  postCode?: string;
91
91
  data?: unknown;
92
+ cpuPin?: number;
92
93
 
93
94
  durationsSAB: SharedArrayBuffer;
94
95
  controlSAB: SharedArrayBuffer;
package/src/worker.ts CHANGED
@@ -2,6 +2,8 @@ import { workerData } from 'node:worker_threads';
2
2
  import { SourceTextModule, SyntheticModule } from 'node:vm';
3
3
  import { createRequire, register } from 'node:module';
4
4
  import { isAbsolute } from 'node:path';
5
+ import { readFileSync } from 'node:fs';
6
+ import { execFileSync } from 'node:child_process';
5
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
8
  import { benchmark } from './runner.js';
7
9
  import { type WorkerOptions } from './types.js';
@@ -17,6 +19,7 @@ const {
17
19
  runCode,
18
20
  postCode,
19
21
  data,
22
+ cpuPin,
20
23
 
21
24
  warmupCycles,
22
25
  minCycles,
@@ -28,6 +31,16 @@ const {
28
31
  controlSAB,
29
32
  }: WorkerOptions = workerData;
30
33
 
34
+ if (cpuPin !== undefined && process.platform === 'linux') {
35
+ try {
36
+ const status = readFileSync('/proc/thread-self/status', 'utf8');
37
+ const tid = status.match(/^Pid:\t(\d+)/m)?.[1];
38
+ if (tid) {
39
+ execFileSync('taskset', ['-cp', String(cpuPin), tid], { stdio: 'ignore' });
40
+ }
41
+ } catch {}
42
+ }
43
+
31
44
  const serialize = (code?: string) => (code ? code : 'undefined');
32
45
 
33
46
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;