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 +3 -0
- package/build/executor.d.ts +1 -0
- package/build/executor.js +14 -4
- package/build/index.js +2 -1
- package/build/loader-hook.d.ts +2 -0
- package/build/loader-hook.js +33 -0
- package/build/register-hook.js +1 -14
- package/build/types.d.ts +1 -0
- package/build/utils.js +2 -15
- package/build/worker.js +13 -1
- package/package.json +2 -2
- package/src/__tests__/fixtures/enum-bench.ts +17 -0
- package/src/__tests__/fixtures/param-property-bench.ts +14 -0
- package/src/__tests__/loader-hook.ts +33 -0
- package/src/cli.ts +3 -0
- package/src/executor.ts +15 -4
- package/src/index.ts +2 -0
- package/src/loader-hook.ts +33 -0
- package/src/register-hook.ts +1 -13
- package/src/types.ts +1 -0
- package/src/utils.ts +1 -13
- package/src/worker.ts +13 -0
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
|
};
|
package/build/executor.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,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
|
+
}
|
package/build/register-hook.js
CHANGED
|
@@ -1,15 +1,2 @@
|
|
|
1
1
|
import { register } from 'node:module';
|
|
2
|
-
|
|
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
package/build/utils.js
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import { parseSync } from '@swc/core';
|
|
2
|
-
|
|
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 =
|
|
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
|
|
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": ">=
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/register-hook.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
1
|
import { register } from 'node:module';
|
|
2
2
|
|
|
3
|
-
|
|
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
package/src/utils.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { parseSync } from '@swc/core';
|
|
2
2
|
|
|
3
|
-
|
|
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;
|