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 +3 -0
- package/build/executor.d.ts +1 -0
- package/build/executor.js +10 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +15 -1
- package/build/types.d.ts +1 -0
- package/build/worker.js +13 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -0
- package/src/executor.ts +12 -1
- package/src/index.ts +17 -3
- package/src/types.ts +1 -0
- 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,
|
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
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
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
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;
|