overtake 2.0.3 → 2.1.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.
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.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,
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.0",
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,
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;