overtake 2.0.2 → 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,
@@ -115,15 +124,16 @@ export const createExecutor = (options) => {
115
124
  }
116
125
  }
117
126
  const stats = count > 0 ? computeStats(durations) : undefined;
118
- const report = reportTypes
127
+ const entries = reportTypes
119
128
  .map((type) => [type, createReport(durations, type, stats)])
120
129
  .concat([
121
130
  ['count', count],
122
131
  ['heapUsedKB', heapUsedKB],
123
132
  ['dceWarning', dceWarning],
124
- ['error', workerError],
125
133
  ]);
126
- return Object.fromEntries(report);
134
+ if (workerError)
135
+ entries.push(['error', workerError]);
136
+ return Object.fromEntries(entries);
127
137
  };
128
138
  return {
129
139
  pushAsync,
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,
@@ -0,0 +1,2 @@
1
+ export declare function resolve(specifier: string, context: unknown, nextResolve: (...args: unknown[]) => unknown): Promise<unknown>;
2
+ export declare function load(url: string, context: unknown, nextLoad: (...args: unknown[]) => unknown): Promise<unknown>;
@@ -0,0 +1,33 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { transformSync } from '@swc/core';
4
+ export async function resolve(specifier, context, nextResolve) {
5
+ try {
6
+ return await nextResolve(specifier, context);
7
+ }
8
+ catch (e) {
9
+ if (specifier.endsWith('.js'))
10
+ try {
11
+ return await nextResolve(specifier.slice(0, -3) + '.ts', context);
12
+ }
13
+ catch { }
14
+ throw e;
15
+ }
16
+ }
17
+ export async function load(url, context, nextLoad) {
18
+ if (!url.endsWith('.ts') && !url.endsWith('.mts')) {
19
+ return nextLoad(url, context);
20
+ }
21
+ const filePath = fileURLToPath(url);
22
+ const rawSource = await readFile(filePath, 'utf-8');
23
+ const { code } = transformSync(rawSource, {
24
+ filename: filePath,
25
+ jsc: {
26
+ parser: { syntax: 'typescript' },
27
+ target: 'esnext',
28
+ },
29
+ module: { type: 'es6' },
30
+ sourceMaps: false,
31
+ });
32
+ return { format: 'module', source: code, shortCircuit: true };
33
+ }
@@ -1,15 +1,2 @@
1
1
  import { register } from 'node:module';
2
- async function resolve(s, c, n) {
3
- try {
4
- return await n(s, c);
5
- }
6
- catch (e) {
7
- if (s.endsWith('.js'))
8
- try {
9
- return await n(s.slice(0, -3) + '.ts', c);
10
- }
11
- catch { }
12
- throw e;
13
- }
14
- }
15
- register('data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`));
2
+ register(new URL('../build/loader-hook.js', import.meta.url).href);
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/utils.js CHANGED
@@ -1,18 +1,5 @@
1
1
  import { parseSync } from '@swc/core';
2
- async function resolve(s, c, n) {
3
- try {
4
- return await n(s, c);
5
- }
6
- catch (e) {
7
- if (s.endsWith('.js'))
8
- try {
9
- return await n(s.slice(0, -3) + '.ts', c);
10
- }
11
- catch { }
12
- throw e;
13
- }
14
- }
15
- export const resolveHookUrl = 'data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`);
2
+ export const resolveHookUrl = new URL('./loader-hook.js', import.meta.url).href;
16
3
  export const isqrt = (n) => {
17
4
  if (n < 0n)
18
5
  throw new RangeError('Square root of negative');
@@ -44,7 +31,7 @@ export const max = (a, b) => {
44
31
  export function div(a, b, decimals = 2) {
45
32
  if (b === 0n)
46
33
  throw new RangeError('Division by zero');
47
- const neg = (a < 0n) !== (b < 0n);
34
+ const neg = a < 0n !== b < 0n;
48
35
  const absA = a < 0n ? -a : a;
49
36
  const absB = b < 0n ? -b : b;
50
37
  const scale = 10n ** BigInt(decimals);
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.2",
3
+ "version": "2.1.0",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "type": "module",
6
6
  "types": "build/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "overtake": "bin/overtake.js"
13
13
  },
14
14
  "engines": {
15
- "node": ">=24"
15
+ "node": ">=22"
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
@@ -0,0 +1,17 @@
1
+ import 'overtake';
2
+
3
+ const suite = benchmark('ops', () => null);
4
+
5
+ const target = suite.target('enum', () => {
6
+ enum Direction {
7
+ Up,
8
+ Down,
9
+ Left,
10
+ Right,
11
+ }
12
+ return { Direction };
13
+ });
14
+
15
+ target.measure('access', ({ Direction }) => {
16
+ return Direction.Up + Direction.Right;
17
+ });
@@ -0,0 +1,14 @@
1
+ import 'overtake';
2
+
3
+ const suite = benchmark('ops', () => null);
4
+
5
+ const target = suite.target('param-property', () => {
6
+ class Container {
7
+ constructor(public value: number) {}
8
+ }
9
+ return { Container };
10
+ });
11
+
12
+ target.measure('create', ({ Container }) => {
13
+ return new Container(42);
14
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const exec = promisify(execFile);
8
+ const overtakeBin = fileURLToPath(new URL('../../bin/overtake.js', import.meta.url));
9
+ const nodeFlags = ['--experimental-vm-modules', '--no-warnings', '--expose-gc'];
10
+
11
+ const runBench = async (fixture: string) => {
12
+ const { stdout } = await exec(process.execPath, [...nodeFlags, overtakeBin, '-f', 'json', '--max-cycles', '50', '--min-cycles', '10', '--warmup-cycles', '5', fixture], {
13
+ timeout: 30_000,
14
+ });
15
+
16
+ const result = JSON.parse(stdout);
17
+ const key = Object.keys(result)[0];
18
+ assert.ok(key, 'should have at least one benchmark result');
19
+ const feeds = result[key];
20
+ const feed = Object.keys(feeds)[0];
21
+ assert.ok(feeds[feed].ops, 'should have ops metric');
22
+ assert.ok(!feeds[feed].error, 'should not have an error');
23
+ };
24
+
25
+ describe('loader-hook', () => {
26
+ it('loads benchmark files using parameter properties', async () => {
27
+ await runBench(fileURLToPath(new URL('fixtures/param-property-bench.ts', import.meta.url)));
28
+ });
29
+
30
+ it('loads benchmark files using enums', async () => {
31
+ await runBench(fileURLToPath(new URL('fixtures/enum-bench.ts', import.meta.url)));
32
+ });
33
+ });
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,
@@ -153,15 +164,15 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
153
164
  }
154
165
 
155
166
  const stats = count > 0 ? computeStats(durations) : undefined;
156
- const report = reportTypes
167
+ const entries: [string, unknown][] = reportTypes
157
168
  .map<[string, unknown]>((type) => [type, createReport(durations, type, stats)] as [ReportType, Report])
158
169
  .concat([
159
170
  ['count', count],
160
171
  ['heapUsedKB', heapUsedKB],
161
172
  ['dceWarning', dceWarning],
162
- ['error', workerError],
163
173
  ]);
164
- return Object.fromEntries(report);
174
+ if (workerError) entries.push(['error', workerError]);
175
+ return Object.fromEntries(entries);
165
176
  };
166
177
 
167
178
  return {
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,
@@ -0,0 +1,33 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { transformSync } from '@swc/core';
4
+
5
+ export async function resolve(specifier: string, context: unknown, nextResolve: (...args: unknown[]) => unknown) {
6
+ try {
7
+ return await nextResolve(specifier, context);
8
+ } catch (e) {
9
+ if (specifier.endsWith('.js'))
10
+ try {
11
+ return await nextResolve(specifier.slice(0, -3) + '.ts', context);
12
+ } catch {}
13
+ throw e;
14
+ }
15
+ }
16
+
17
+ export async function load(url: string, context: unknown, nextLoad: (...args: unknown[]) => unknown) {
18
+ if (!url.endsWith('.ts') && !url.endsWith('.mts')) {
19
+ return nextLoad(url, context);
20
+ }
21
+ const filePath = fileURLToPath(url);
22
+ const rawSource = await readFile(filePath, 'utf-8');
23
+ const { code } = transformSync(rawSource, {
24
+ filename: filePath,
25
+ jsc: {
26
+ parser: { syntax: 'typescript' },
27
+ target: 'esnext',
28
+ },
29
+ module: { type: 'es6' },
30
+ sourceMaps: false,
31
+ });
32
+ return { format: 'module', source: code, shortCircuit: true };
33
+ }
@@ -1,15 +1,3 @@
1
1
  import { register } from 'node:module';
2
2
 
3
- async function resolve(s: string, c: unknown, n: (...args: unknown[]) => unknown) {
4
- try {
5
- return await n(s, c);
6
- } catch (e) {
7
- if (s.endsWith('.js'))
8
- try {
9
- return await n(s.slice(0, -3) + '.ts', c);
10
- } catch {}
11
- throw e;
12
- }
13
- }
14
-
15
- register('data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`));
3
+ register(new URL('../build/loader-hook.js', import.meta.url).href);
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/utils.ts CHANGED
@@ -1,18 +1,6 @@
1
1
  import { parseSync } from '@swc/core';
2
2
 
3
- async function resolve(s: string, c: unknown, n: (...args: unknown[]) => unknown) {
4
- try {
5
- return await n(s, c);
6
- } catch (e) {
7
- if (s.endsWith('.js'))
8
- try {
9
- return await n(s.slice(0, -3) + '.ts', c);
10
- } catch {}
11
- throw e;
12
- }
13
- }
14
-
15
- export const resolveHookUrl = 'data:text/javascript,' + encodeURIComponent(`export ${resolve.toString()}`);
3
+ export const resolveHookUrl = new URL('./loader-hook.js', import.meta.url).href;
16
4
 
17
5
  export const isqrt = (n: bigint): bigint => {
18
6
  if (n < 0n) throw new RangeError('Square root of negative');
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;