overtake 1.1.0 → 1.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["import { workerData } from 'node:worker_threads';\nimport { SourceTextModule, SyntheticModule, createContext } from 'node:vm';\nimport { createRequire } from 'node:module';\nimport { fileURLToPath } from 'node:url';\nimport { benchmark } from './runner.js';\nimport { WorkerOptions } from './types.js';\n\nconst {\n baseUrl,\n setupCode,\n teardownCode,\n preCode,\n runCode,\n postCode,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver = true,\n\n durationsSAB,\n controlSAB,\n}: WorkerOptions = workerData;\n\nconst serialize = (code?: string) => (code ? code : '() => {}');\n\nconst source = `\nexport const setup = ${serialize(setupCode)};\nexport const teardown = ${serialize(teardownCode)};\nexport const pre = ${serialize(preCode)};\nexport const run = ${serialize(runCode)};\nexport const post = ${serialize(postCode)};\n `;\n\nconst context = createContext({ console, Buffer });\nconst imports = new Map<string, SyntheticModule>();\nconst mod = new SourceTextModule(source, {\n identifier: baseUrl,\n context,\n initializeImportMeta(meta) {\n meta.url = baseUrl;\n },\n importModuleDynamically(specifier, referencingModule) {\n const base = referencingModule.identifier ?? baseUrl;\n const resolveFrom = createRequire(fileURLToPath(base));\n return import(resolveFrom.resolve(specifier));\n },\n});\n\nawait mod.link(async (specifier, referencingModule) => {\n const base = referencingModule.identifier ?? baseUrl;\n const resolveFrom = createRequire(fileURLToPath(base));\n const target = resolveFrom.resolve(specifier);\n const cached = imports.get(target);\n if (cached) return cached;\n\n const importedModule = await import(target);\n const exportNames = Object.keys(importedModule);\n const imported = new SyntheticModule(\n exportNames,\n () => {\n exportNames.forEach((key) => imported.setExport(key, importedModule[key]));\n },\n { identifier: target, context: referencingModule.context },\n );\n imports.set(target, imported);\n return imported;\n});\n\nawait mod.evaluate();\nconst { setup, teardown, pre, run, post } = mod.namespace as any;\n\nif (!run) {\n throw new Error('Benchmark run function is required');\n}\n\nprocess.exitCode = await benchmark({\n baseUrl,\n setup,\n teardown,\n pre,\n run,\n post,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver,\n\n durationsSAB,\n controlSAB,\n});\n"],"names":["workerData","SourceTextModule","SyntheticModule","createContext","createRequire","fileURLToPath","benchmark","baseUrl","setupCode","teardownCode","preCode","runCode","postCode","data","warmupCycles","minCycles","absThreshold","relThreshold","gcObserver","durationsSAB","controlSAB","serialize","code","source","context","console","Buffer","imports","Map","mod","identifier","initializeImportMeta","meta","url","importModuleDynamically","specifier","referencingModule","base","resolveFrom","resolve","link","target","cached","get","importedModule","exportNames","Object","keys","imported","forEach","key","setExport","set","evaluate","setup","teardown","pre","run","post","namespace","Error","process","exitCode"],"mappings":"AAAA,SAASA,UAAU,QAAQ,sBAAsB;AACjD,SAASC,gBAAgB,EAAEC,eAAe,EAAEC,aAAa,QAAQ,UAAU;AAC3E,SAASC,aAAa,QAAQ,cAAc;AAC5C,SAASC,aAAa,QAAQ,WAAW;AACzC,SAASC,SAAS,QAAQ,cAAc;AAGxC,MAAM,EACJC,OAAO,EACPC,SAAS,EACTC,YAAY,EACZC,OAAO,EACPC,OAAO,EACPC,QAAQ,EACRC,IAAI,EAEJC,YAAY,EACZC,SAAS,EACTC,YAAY,EACZC,YAAY,EACZC,aAAa,IAAI,EAEjBC,YAAY,EACZC,UAAU,EACX,GAAkBpB;AAEnB,MAAMqB,YAAY,CAACC,OAAmBA,OAAOA,OAAO;AAEpD,MAAMC,SAAS,CAAC;qBACK,EAAEF,UAAUb,WAAW;wBACpB,EAAEa,UAAUZ,cAAc;mBAC/B,EAAEY,UAAUX,SAAS;mBACrB,EAAEW,UAAUV,SAAS;oBACpB,EAAEU,UAAUT,UAAU;EACxC,CAAC;AAEH,MAAMY,UAAUrB,cAAc;IAAEsB;IAASC;AAAO;AAChD,MAAMC,UAAU,IAAIC;AACpB,MAAMC,MAAM,IAAI5B,iBAAiBsB,QAAQ;IACvCO,YAAYvB;IACZiB;IACAO,sBAAqBC,IAAI;QACvBA,KAAKC,GAAG,GAAG1B;IACb;IACA2B,yBAAwBC,SAAS,EAAEC,iBAAiB;QAClD,MAAMC,OAAOD,kBAAkBN,UAAU,IAAIvB;QAC7C,MAAM+B,cAAclC,cAAcC,cAAcgC;QAChD,OAAO,MAAM,CAACC,YAAYC,OAAO,CAACJ;IACpC;AACF;AAEA,MAAMN,IAAIW,IAAI,CAAC,OAAOL,WAAWC;IAC/B,MAAMC,OAAOD,kBAAkBN,UAAU,IAAIvB;IAC7C,MAAM+B,cAAclC,cAAcC,cAAcgC;IAChD,MAAMI,SAASH,YAAYC,OAAO,CAACJ;IACnC,MAAMO,SAASf,QAAQgB,GAAG,CAACF;IAC3B,IAAIC,QAAQ,OAAOA;IAEnB,MAAME,iBAAiB,MAAM,MAAM,CAACH;IACpC,MAAMI,cAAcC,OAAOC,IAAI,CAACH;IAChC,MAAMI,WAAW,IAAI9C,gBACnB2C,aACA;QACEA,YAAYI,OAAO,CAAC,CAACC,MAAQF,SAASG,SAAS,CAACD,KAAKN,cAAc,CAACM,IAAI;IAC1E,GACA;QAAEpB,YAAYW;QAAQjB,SAASY,kBAAkBZ,OAAO;IAAC;IAE3DG,QAAQyB,GAAG,CAACX,QAAQO;IACpB,OAAOA;AACT;AAEA,MAAMnB,IAAIwB,QAAQ;AAClB,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAEC,GAAG,EAAEC,GAAG,EAAEC,IAAI,EAAE,GAAG7B,IAAI8B,SAAS;AAEzD,IAAI,CAACF,KAAK;IACR,MAAM,IAAIG,MAAM;AAClB;AAEAC,QAAQC,QAAQ,GAAG,MAAMxD,UAAU;IACjCC;IACA+C;IACAC;IACAC;IACAC;IACAC;IACA7C;IAEAC;IACAC;IACAC;IACAC;IACAC;IAEAC;IACAC;AACF"}
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["import { workerData } from 'node:worker_threads';\nimport { SourceTextModule, SyntheticModule, createContext } from 'node:vm';\nimport { createRequire } from 'node:module';\nimport { isAbsolute } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { benchmark } from './runner.js';\nimport { WorkerOptions } from './types.js';\n\nconst {\n benchmarkUrl,\n setupCode,\n teardownCode,\n preCode,\n runCode,\n postCode,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver = true,\n\n durationsSAB,\n controlSAB,\n}: WorkerOptions = workerData;\n\nconst serialize = (code?: string) => (code ? code : '() => {}');\n\nconst resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;\nconst benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;\nconst requireFrom = createRequire(fileURLToPath(new URL('benchmark.js', benchmarkDirUrl)));\n\nconst resolveSpecifier = (specifier: string) => {\n if (specifier.startsWith('file:')) {\n return specifier;\n }\n if (specifier.startsWith('./') || specifier.startsWith('../')) {\n return new URL(specifier, benchmarkDirUrl).href;\n }\n if (isAbsolute(specifier)) {\n return pathToFileURL(specifier).href;\n }\n return requireFrom.resolve(specifier);\n};\n\nconst source = `\nexport const setup = ${serialize(setupCode)};\nexport const teardown = ${serialize(teardownCode)};\nexport const pre = ${serialize(preCode)};\nexport const run = ${serialize(runCode)};\nexport const post = ${serialize(postCode)};\n `;\n\nconst context = createContext({ console, Buffer });\nconst imports = new Map<string, SyntheticModule>();\n\nconst createSyntheticModule = (moduleExports: unknown, exportNames: string[], identifier: string) => {\n const mod = new SyntheticModule(\n exportNames,\n () => {\n for (const name of exportNames) {\n if (name === 'default') {\n mod.setExport(name, moduleExports);\n continue;\n }\n mod.setExport(name, (moduleExports as Record<string, unknown>)[name]);\n }\n },\n { identifier, context },\n );\n return mod;\n};\n\nconst isCjsModule = (target: string) => target.endsWith('.cjs') || target.endsWith('.cts');\n\nconst toRequireTarget = (target: string) => (target.startsWith('file:') ? fileURLToPath(target) : target);\n\nconst loadModule = async (target: string) => {\n const cached = imports.get(target);\n if (cached) return cached;\n\n if (isCjsModule(target)) {\n const required = requireFrom(toRequireTarget(target));\n const exportNames = required && (typeof required === 'object' || typeof required === 'function') ? Object.keys(required) : [];\n if (!exportNames.includes('default')) {\n exportNames.push('default');\n }\n const mod = createSyntheticModule(required, exportNames, target);\n imports.set(target, mod);\n return mod;\n }\n\n const importedModule = await import(target);\n const exportNames = Object.keys(importedModule);\n const mod = createSyntheticModule(importedModule, exportNames, target);\n imports.set(target, mod);\n return mod;\n};\n\nconst loadDynamicModule = async (target: string) => {\n const mod = await loadModule(target);\n if (mod.status !== 'evaluated') {\n await mod.evaluate();\n }\n return mod;\n};\nconst mod = new SourceTextModule(source, {\n identifier: resolvedBenchmarkUrl,\n context,\n initializeImportMeta(meta) {\n meta.url = resolvedBenchmarkUrl;\n },\n importModuleDynamically(specifier) {\n const resolved = resolveSpecifier(specifier);\n return loadDynamicModule(resolved);\n },\n});\n\nawait mod.link(async (specifier) => loadModule(resolveSpecifier(specifier)));\n\nawait mod.evaluate();\nconst { setup, teardown, pre, run, post } = mod.namespace as any;\n\nif (!run) {\n throw new Error('Benchmark run function is required');\n}\n\nprocess.exitCode = await benchmark({\n setup,\n teardown,\n pre,\n run,\n post,\n data,\n\n warmupCycles,\n minCycles,\n absThreshold,\n relThreshold,\n gcObserver,\n\n durationsSAB,\n controlSAB,\n});\n"],"names":["workerData","SourceTextModule","SyntheticModule","createContext","createRequire","isAbsolute","fileURLToPath","pathToFileURL","benchmark","benchmarkUrl","setupCode","teardownCode","preCode","runCode","postCode","data","warmupCycles","minCycles","absThreshold","relThreshold","gcObserver","durationsSAB","controlSAB","serialize","code","resolvedBenchmarkUrl","process","cwd","href","benchmarkDirUrl","URL","requireFrom","resolveSpecifier","specifier","startsWith","resolve","source","context","console","Buffer","imports","Map","createSyntheticModule","moduleExports","exportNames","identifier","mod","name","setExport","isCjsModule","target","endsWith","toRequireTarget","loadModule","cached","get","required","Object","keys","includes","push","set","importedModule","loadDynamicModule","status","evaluate","initializeImportMeta","meta","url","importModuleDynamically","resolved","link","setup","teardown","pre","run","post","namespace","Error","exitCode"],"mappings":"AAAA,SAASA,UAAU,QAAQ,sBAAsB;AACjD,SAASC,gBAAgB,EAAEC,eAAe,EAAEC,aAAa,QAAQ,UAAU;AAC3E,SAASC,aAAa,QAAQ,cAAc;AAC5C,SAASC,UAAU,QAAQ,YAAY;AACvC,SAASC,aAAa,EAAEC,aAAa,QAAQ,WAAW;AACxD,SAASC,SAAS,QAAQ,cAAc;AAGxC,MAAM,EACJC,YAAY,EACZC,SAAS,EACTC,YAAY,EACZC,OAAO,EACPC,OAAO,EACPC,QAAQ,EACRC,IAAI,EAEJC,YAAY,EACZC,SAAS,EACTC,YAAY,EACZC,YAAY,EACZC,aAAa,IAAI,EAEjBC,YAAY,EACZC,UAAU,EACX,GAAkBtB;AAEnB,MAAMuB,YAAY,CAACC,OAAmBA,OAAOA,OAAO;AAEpD,MAAMC,uBAAuB,OAAOhB,iBAAiB,WAAWA,eAAeF,cAAcmB,QAAQC,GAAG,IAAIC,IAAI;AAChH,MAAMC,kBAAkB,IAAIC,IAAI,KAAKL,sBAAsBG,IAAI;AAC/D,MAAMG,cAAc3B,cAAcE,cAAc,IAAIwB,IAAI,gBAAgBD;AAExE,MAAMG,mBAAmB,CAACC;IACxB,IAAIA,UAAUC,UAAU,CAAC,UAAU;QACjC,OAAOD;IACT;IACA,IAAIA,UAAUC,UAAU,CAAC,SAASD,UAAUC,UAAU,CAAC,QAAQ;QAC7D,OAAO,IAAIJ,IAAIG,WAAWJ,iBAAiBD,IAAI;IACjD;IACA,IAAIvB,WAAW4B,YAAY;QACzB,OAAO1B,cAAc0B,WAAWL,IAAI;IACtC;IACA,OAAOG,YAAYI,OAAO,CAACF;AAC7B;AAEA,MAAMG,SAAS,CAAC;qBACK,EAAEb,UAAUb,WAAW;wBACpB,EAAEa,UAAUZ,cAAc;mBAC/B,EAAEY,UAAUX,SAAS;mBACrB,EAAEW,UAAUV,SAAS;oBACpB,EAAEU,UAAUT,UAAU;EACxC,CAAC;AAEH,MAAMuB,UAAUlC,cAAc;IAAEmC;IAASC;AAAO;AAChD,MAAMC,UAAU,IAAIC;AAEpB,MAAMC,wBAAwB,CAACC,eAAwBC,aAAuBC;IAC5E,MAAMC,MAAM,IAAI5C,gBACd0C,aACA;QACE,KAAK,MAAMG,QAAQH,YAAa;YAC9B,IAAIG,SAAS,WAAW;gBACtBD,IAAIE,SAAS,CAACD,MAAMJ;gBACpB;YACF;YACAG,IAAIE,SAAS,CAACD,MAAM,AAACJ,aAAyC,CAACI,KAAK;QACtE;IACF,GACA;QAAEF;QAAYR;IAAQ;IAExB,OAAOS;AACT;AAEA,MAAMG,cAAc,CAACC,SAAmBA,OAAOC,QAAQ,CAAC,WAAWD,OAAOC,QAAQ,CAAC;AAEnF,MAAMC,kBAAkB,CAACF,SAAoBA,OAAOhB,UAAU,CAAC,WAAW5B,cAAc4C,UAAUA;AAElG,MAAMG,aAAa,OAAOH;IACxB,MAAMI,SAASd,QAAQe,GAAG,CAACL;IAC3B,IAAII,QAAQ,OAAOA;IAEnB,IAAIL,YAAYC,SAAS;QACvB,MAAMM,WAAWzB,YAAYqB,gBAAgBF;QAC7C,MAAMN,cAAcY,YAAa,CAAA,OAAOA,aAAa,YAAY,OAAOA,aAAa,UAAS,IAAKC,OAAOC,IAAI,CAACF,YAAY,EAAE;QAC7H,IAAI,CAACZ,YAAYe,QAAQ,CAAC,YAAY;YACpCf,YAAYgB,IAAI,CAAC;QACnB;QACA,MAAMd,MAAMJ,sBAAsBc,UAAUZ,aAAaM;QACzDV,QAAQqB,GAAG,CAACX,QAAQJ;QACpB,OAAOA;IACT;IAEA,MAAMgB,iBAAiB,MAAM,MAAM,CAACZ;IACpC,MAAMN,cAAca,OAAOC,IAAI,CAACI;IAChC,MAAMhB,MAAMJ,sBAAsBoB,gBAAgBlB,aAAaM;IAC/DV,QAAQqB,GAAG,CAACX,QAAQJ;IACpB,OAAOA;AACT;AAEA,MAAMiB,oBAAoB,OAAOb;IAC/B,MAAMJ,MAAM,MAAMO,WAAWH;IAC7B,IAAIJ,IAAIkB,MAAM,KAAK,aAAa;QAC9B,MAAMlB,IAAImB,QAAQ;IACpB;IACA,OAAOnB;AACT;AACA,MAAMA,MAAM,IAAI7C,iBAAiBmC,QAAQ;IACvCS,YAAYpB;IACZY;IACA6B,sBAAqBC,IAAI;QACvBA,KAAKC,GAAG,GAAG3C;IACb;IACA4C,yBAAwBpC,SAAS;QAC/B,MAAMqC,WAAWtC,iBAAiBC;QAClC,OAAO8B,kBAAkBO;IAC3B;AACF;AAEA,MAAMxB,IAAIyB,IAAI,CAAC,OAAOtC,YAAcoB,WAAWrB,iBAAiBC;AAEhE,MAAMa,IAAImB,QAAQ;AAClB,MAAM,EAAEO,KAAK,EAAEC,QAAQ,EAAEC,GAAG,EAAEC,GAAG,EAAEC,IAAI,EAAE,GAAG9B,IAAI+B,SAAS;AAEzD,IAAI,CAACF,KAAK;IACR,MAAM,IAAIG,MAAM;AAClB;AAEApD,QAAQqD,QAAQ,GAAG,MAAMvE,UAAU;IACjCgE;IACAC;IACAC;IACAC;IACAC;IACA7D;IAEAC;IACAC;IACAC;IACAC;IACAC;IAEAC;IACAC;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "type": "module",
6
6
  "types": "build/index.d.ts",
@@ -45,17 +45,17 @@
45
45
  "@swc/jest": "^0.2.39",
46
46
  "@types/async": "^3.2.25",
47
47
  "@types/jest": "^30.0.0",
48
- "@types/node": "^24.10.1",
48
+ "@types/node": "^25.0.3",
49
49
  "husky": "^9.1.7",
50
- "inop": "^0.8.0",
50
+ "inop": "^0.8.10",
51
51
  "jest": "^30.2.0",
52
- "overtake": "^1.0.5",
53
- "prettier": "^3.6.2",
52
+ "overtake": "^1.1.1",
53
+ "prettier": "^3.7.4",
54
54
  "pretty-quick": "^4.2.2",
55
55
  "typescript": "^5.9.3"
56
56
  },
57
57
  "dependencies": {
58
- "@swc/core": "^1.15.2",
58
+ "@swc/core": "^1.15.8",
59
59
  "async": "^3.2.6",
60
60
  "commander": "^14.0.2",
61
61
  "glob": "^13.0.0"
package/src/cli.ts CHANGED
@@ -10,6 +10,7 @@ import { REPORT_TYPES } from './types.js';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
  const { name, description, version } = require('../package.json');
13
+ const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
13
14
 
14
15
  const commander = new Command();
15
16
 
@@ -21,8 +22,8 @@ commander
21
22
  .addOption(new Option('-r, --report-types [reportTypes...]', 'statistic types to include in the report').choices(REPORT_TYPES).default(DEFAULT_REPORT_TYPES))
22
23
  .addOption(new Option('-w, --workers [workers]', 'number of concurent workers').default(DEFAULT_WORKERS).argParser(parseInt))
23
24
  .addOption(new Option('-f, --format [format]', 'output format').default('simple').choices(['simple', 'json', 'pjson', 'table']))
24
- .addOption(new Option('--abs-threshold [absThreshold]', 'absolute error threshold in nanoseconds').argParser(parseInt))
25
- .addOption(new Option('--rel-threshold [relThreshold]', 'relative error threshold (fraction between 0 and 1)').argParser(parseInt))
25
+ .addOption(new Option('--abs-threshold [absThreshold]', 'absolute error threshold in nanoseconds').argParser(parseFloat))
26
+ .addOption(new Option('--rel-threshold [relThreshold]', 'relative error threshold (fraction between 0 and 1)').argParser(parseFloat))
26
27
  .addOption(new Option('--warmup-cycles [warmupCycles]', 'number of warmup cycles before measuring').argParser(parseInt))
27
28
  .addOption(new Option('--max-cycles [maxCycles]', 'maximum measurement cycles per feed').argParser(parseInt))
28
29
  .addOption(new Option('--min-cycles [minCycles]', 'minimum measurement cycles per feed').argParser(parseInt))
@@ -97,8 +98,8 @@ commander
97
98
  if (instance) {
98
99
  const reports = await instance.execute({
99
100
  ...executeOptions,
100
- baseUrl: identifier,
101
- });
101
+ [BENCHMARK_URL]: identifier,
102
+ } as typeof executeOptions);
102
103
  switch (executeOptions.format) {
103
104
  case 'json':
104
105
  {
package/src/executor.ts CHANGED
@@ -9,23 +9,18 @@ import { RunOptions, ReportOptions, WorkerOptions, BenchmarkOptions, Control, Re
9
9
  export type ExecutorReport<R extends ReportTypeList> = Record<R[number], Report> & { count: number };
10
10
 
11
11
  export interface ExecutorOptions<R extends ReportTypeList> extends BenchmarkOptions, ReportOptions<R> {
12
- baseUrl?: string;
13
12
  workers?: number;
14
13
  maxCycles?: number;
15
14
  }
16
15
 
17
- export const createExecutor = <TContext, TInput, R extends ReportTypeList>({
18
- baseUrl = pathToFileURL(process.cwd()).href,
19
- workers,
20
- warmupCycles,
21
- maxCycles,
22
- minCycles,
23
- absThreshold,
24
- relThreshold,
25
- gcObserver = true,
26
- reportTypes,
27
- }: Required<ExecutorOptions<R>>) => {
28
- const executor = queue<RunOptions<TContext, TInput>>(async ({ baseUrl: runBaseUrl = baseUrl, setup, teardown, pre, run, post, data }) => {
16
+ const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
17
+
18
+ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(options: Required<ExecutorOptions<R>>) => {
19
+ const { workers, warmupCycles, maxCycles, minCycles, absThreshold, relThreshold, gcObserver = true, reportTypes } = options;
20
+ const benchmarkUrl = (options as Record<symbol, unknown>)[BENCHMARK_URL];
21
+ const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
22
+
23
+ const executor = queue<RunOptions<TContext, TInput>>(async ({ setup, teardown, pre, run, post, data }) => {
29
24
  const setupCode = setup?.toString();
30
25
  const teardownCode = teardown?.toString();
31
26
  const preCode = pre?.toString();
@@ -37,7 +32,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>({
37
32
 
38
33
  const workerFile = new URL('./worker.js', import.meta.url);
39
34
  const workerData: WorkerOptions = {
40
- baseUrl: runBaseUrl,
35
+ benchmarkUrl: resolvedBenchmarkUrl,
41
36
  setupCode,
42
37
  teardownCode,
43
38
  preCode,
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { cpus } from 'node:os';
2
- import { pathToFileURL } from 'node:url';
3
2
  import { createExecutor, ExecutorOptions, ExecutorReport } from './executor.js';
4
3
  import { MaybePromise, StepFn, SetupFn, TeardownFn, FeedFn, ReportType, ReportTypeList, DEFAULT_CYCLES } from './types.js';
5
4
 
@@ -10,6 +9,7 @@ declare global {
10
9
  export const DEFAULT_WORKERS = cpus().length;
11
10
 
12
11
  export const AsyncFunction = (async () => {}).constructor;
12
+ const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
13
13
 
14
14
  export interface TargetReport<R extends ReportTypeList> {
15
15
  target: string;
@@ -135,29 +135,24 @@ export class Benchmark<TInput> {
135
135
  return new Target<TContext, TInput>(target);
136
136
  }
137
137
 
138
- async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>({
139
- workers = DEFAULT_WORKERS,
140
- warmupCycles = 20,
141
- maxCycles = DEFAULT_CYCLES,
142
- minCycles = 50,
143
- absThreshold = 1_000,
144
- relThreshold = 0.02,
145
- gcObserver = true,
146
- reportTypes = DEFAULT_REPORT_TYPES as unknown as R,
147
- baseUrl,
148
- }: ExecutorOptions<R>): Promise<TargetReport<R>[]> {
138
+ async execute<R extends readonly ReportType[] = typeof DEFAULT_REPORT_TYPES>(options: ExecutorOptions<R>): Promise<TargetReport<R>[]> {
139
+ const {
140
+ workers = DEFAULT_WORKERS,
141
+ warmupCycles = 20,
142
+ maxCycles = DEFAULT_CYCLES,
143
+ minCycles = 50,
144
+ absThreshold = 1_000,
145
+ relThreshold = 0.02,
146
+ gcObserver = true,
147
+ reportTypes = DEFAULT_REPORT_TYPES as unknown as R,
148
+ } = options;
149
149
  if (this.#executed) {
150
150
  throw new Error("Benchmark is executed and can't be reused");
151
151
  }
152
152
  this.#executed = true;
153
-
154
- const resolvedBaseUrl = baseUrl ?? pathToFileURL(process.cwd()).href;
155
- if (!baseUrl) {
156
- console.warn("Overtake: baseUrl not provided; defaulting to process.cwd(). Pass the benchmark's import.meta.url so relative imports resolve correctly.");
157
- }
153
+ const benchmarkUrl = (options as unknown as Record<symbol, unknown>)[BENCHMARK_URL];
158
154
 
159
155
  const executor = createExecutor<unknown, TInput, R>({
160
- baseUrl: resolvedBaseUrl,
161
156
  workers,
162
157
  warmupCycles,
163
158
  maxCycles,
@@ -166,7 +161,8 @@ export class Benchmark<TInput> {
166
161
  relThreshold,
167
162
  gcObserver,
168
163
  reportTypes,
169
- });
164
+ [BENCHMARK_URL]: benchmarkUrl,
165
+ } as Required<ExecutorOptions<R>>);
170
166
 
171
167
  const reports: TargetReport<R>[] = [];
172
168
  for (const target of this.#targets) {
@@ -177,7 +173,6 @@ export class Benchmark<TInput> {
177
173
  const data = await feed.fn?.();
178
174
  executor
179
175
  .push<ExecutorReport<R>>({
180
- baseUrl: resolvedBaseUrl,
181
176
  setup: target.setup,
182
177
  teardown: target.teardown,
183
178
  pre: measure.pre,
package/src/runner.ts CHANGED
@@ -23,6 +23,10 @@ const runAsync = (run: Function) => {
23
23
  };
24
24
  };
25
25
 
26
+ const isThenable = (value: unknown): value is PromiseLike<unknown> => {
27
+ return value !== null && (typeof value === 'object' || typeof value === 'function') && typeof (value as PromiseLike<unknown>).then === 'function';
28
+ };
29
+
26
30
  const TARGET_SAMPLE_NS = 1_000_000n; // aim for ~1ms per measured sample
27
31
  const MAX_BATCH = 1_048_576;
28
32
  const PROGRESS_STRIDE = 16;
@@ -198,7 +202,7 @@ export const benchmark = async <TContext, TInput>({
198
202
 
199
203
  const context = (await setup?.()) as TContext;
200
204
  const maxCycles = durations.length;
201
- const gcWatcher = new GCWatcher();
205
+ const gcWatcher = gcObserver ? new GCWatcher() : null;
202
206
  const gcTracker = gcObserver ? createGCTracker() : null;
203
207
 
204
208
  try {
@@ -206,7 +210,7 @@ export const benchmark = async <TContext, TInput>({
206
210
  await pre?.(context, data!);
207
211
  const probeStart = hr();
208
212
  const probeResult = runRaw(context, data!);
209
- const isAsync = probeResult instanceof Promise;
213
+ const isAsync = isThenable(probeResult);
210
214
  if (isAsync) {
211
215
  await probeResult;
212
216
  }
@@ -282,7 +286,7 @@ export const benchmark = async <TContext, TInput>({
282
286
  while (true) {
283
287
  if (i >= maxCycles) break;
284
288
 
285
- const gcMarker = gcWatcher.start();
289
+ const gcMarker = gcWatcher?.start();
286
290
  const sampleStart = performance.now();
287
291
  let sampleDuration = 0n;
288
292
  for (let b = 0; b < batchSize; b++) {
@@ -298,7 +302,7 @@ export const benchmark = async <TContext, TInput>({
298
302
  sampleDuration /= BigInt(batchSize);
299
303
 
300
304
  const sampleEnd = performance.now();
301
- const gcNoise = gcWatcher.seen(gcMarker) || (gcTracker?.overlaps(sampleStart, sampleEnd) ?? false);
305
+ const gcNoise = (gcMarker ? gcWatcher!.seen(gcMarker) : false) || (gcTracker?.overlaps(sampleStart, sampleEnd) ?? false);
302
306
  if (gcNoise) {
303
307
  continue;
304
308
  }
package/src/types.ts CHANGED
@@ -34,11 +34,9 @@ export interface BenchmarkOptions {
34
34
  absThreshold?: number; // ns
35
35
  relThreshold?: number; // %
36
36
  gcObserver?: boolean;
37
- baseUrl?: string;
38
37
  }
39
38
 
40
39
  export interface RunOptions<TContext, TInput> {
41
- baseUrl?: string;
42
40
  setup?: SetupFn<TContext>;
43
41
  teardown?: TeardownFn<TContext>;
44
42
  pre?: StepFn<TContext, TInput>;
@@ -48,7 +46,7 @@ export interface RunOptions<TContext, TInput> {
48
46
  }
49
47
 
50
48
  export interface WorkerOptions extends Required<BenchmarkOptions> {
51
- baseUrl: string;
49
+ benchmarkUrl?: string;
52
50
  setupCode?: string;
53
51
  teardownCode?: string;
54
52
  preCode?: string;
package/src/worker.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { workerData } from 'node:worker_threads';
2
2
  import { SourceTextModule, SyntheticModule, createContext } from 'node:vm';
3
3
  import { createRequire } from 'node:module';
4
- import { fileURLToPath } from 'node:url';
4
+ import { isAbsolute } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { benchmark } from './runner.js';
6
7
  import { WorkerOptions } from './types.js';
7
8
 
8
9
  const {
9
- baseUrl,
10
+ benchmarkUrl,
10
11
  setupCode,
11
12
  teardownCode,
12
13
  preCode,
@@ -26,6 +27,23 @@ const {
26
27
 
27
28
  const serialize = (code?: string) => (code ? code : '() => {}');
28
29
 
30
+ const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
31
+ const benchmarkDirUrl = new URL('.', resolvedBenchmarkUrl).href;
32
+ const requireFrom = createRequire(fileURLToPath(new URL('benchmark.js', benchmarkDirUrl)));
33
+
34
+ const resolveSpecifier = (specifier: string) => {
35
+ if (specifier.startsWith('file:')) {
36
+ return specifier;
37
+ }
38
+ if (specifier.startsWith('./') || specifier.startsWith('../')) {
39
+ return new URL(specifier, benchmarkDirUrl).href;
40
+ }
41
+ if (isAbsolute(specifier)) {
42
+ return pathToFileURL(specifier).href;
43
+ }
44
+ return requireFrom.resolve(specifier);
45
+ };
46
+
29
47
  const source = `
30
48
  export const setup = ${serialize(setupCode)};
31
49
  export const teardown = ${serialize(teardownCode)};
@@ -36,39 +54,71 @@ export const post = ${serialize(postCode)};
36
54
 
37
55
  const context = createContext({ console, Buffer });
38
56
  const imports = new Map<string, SyntheticModule>();
39
- const mod = new SourceTextModule(source, {
40
- identifier: baseUrl,
41
- context,
42
- initializeImportMeta(meta) {
43
- meta.url = baseUrl;
44
- },
45
- importModuleDynamically(specifier, referencingModule) {
46
- const base = referencingModule.identifier ?? baseUrl;
47
- const resolveFrom = createRequire(fileURLToPath(base));
48
- return import(resolveFrom.resolve(specifier));
49
- },
50
- });
51
57
 
52
- await mod.link(async (specifier, referencingModule) => {
53
- const base = referencingModule.identifier ?? baseUrl;
54
- const resolveFrom = createRequire(fileURLToPath(base));
55
- const target = resolveFrom.resolve(specifier);
58
+ const createSyntheticModule = (moduleExports: unknown, exportNames: string[], identifier: string) => {
59
+ const mod = new SyntheticModule(
60
+ exportNames,
61
+ () => {
62
+ for (const name of exportNames) {
63
+ if (name === 'default') {
64
+ mod.setExport(name, moduleExports);
65
+ continue;
66
+ }
67
+ mod.setExport(name, (moduleExports as Record<string, unknown>)[name]);
68
+ }
69
+ },
70
+ { identifier, context },
71
+ );
72
+ return mod;
73
+ };
74
+
75
+ const isCjsModule = (target: string) => target.endsWith('.cjs') || target.endsWith('.cts');
76
+
77
+ const toRequireTarget = (target: string) => (target.startsWith('file:') ? fileURLToPath(target) : target);
78
+
79
+ const loadModule = async (target: string) => {
56
80
  const cached = imports.get(target);
57
81
  if (cached) return cached;
58
82
 
83
+ if (isCjsModule(target)) {
84
+ const required = requireFrom(toRequireTarget(target));
85
+ const exportNames = required && (typeof required === 'object' || typeof required === 'function') ? Object.keys(required) : [];
86
+ if (!exportNames.includes('default')) {
87
+ exportNames.push('default');
88
+ }
89
+ const mod = createSyntheticModule(required, exportNames, target);
90
+ imports.set(target, mod);
91
+ return mod;
92
+ }
93
+
59
94
  const importedModule = await import(target);
60
95
  const exportNames = Object.keys(importedModule);
61
- const imported = new SyntheticModule(
62
- exportNames,
63
- () => {
64
- exportNames.forEach((key) => imported.setExport(key, importedModule[key]));
65
- },
66
- { identifier: target, context: referencingModule.context },
67
- );
68
- imports.set(target, imported);
69
- return imported;
96
+ const mod = createSyntheticModule(importedModule, exportNames, target);
97
+ imports.set(target, mod);
98
+ return mod;
99
+ };
100
+
101
+ const loadDynamicModule = async (target: string) => {
102
+ const mod = await loadModule(target);
103
+ if (mod.status !== 'evaluated') {
104
+ await mod.evaluate();
105
+ }
106
+ return mod;
107
+ };
108
+ const mod = new SourceTextModule(source, {
109
+ identifier: resolvedBenchmarkUrl,
110
+ context,
111
+ initializeImportMeta(meta) {
112
+ meta.url = resolvedBenchmarkUrl;
113
+ },
114
+ importModuleDynamically(specifier) {
115
+ const resolved = resolveSpecifier(specifier);
116
+ return loadDynamicModule(resolved);
117
+ },
70
118
  });
71
119
 
120
+ await mod.link(async (specifier) => loadModule(resolveSpecifier(specifier)));
121
+
72
122
  await mod.evaluate();
73
123
  const { setup, teardown, pre, run, post } = mod.namespace as any;
74
124
 
@@ -77,7 +127,6 @@ if (!run) {
77
127
  }
78
128
 
79
129
  process.exitCode = await benchmark({
80
- baseUrl,
81
130
  setup,
82
131
  teardown,
83
132
  pre,
package/build/queue.cjs DELETED
@@ -1,48 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- Object.defineProperty(exports, "createQueue", {
6
- enumerable: true,
7
- get: function() {
8
- return createQueue;
9
- }
10
- });
11
- const createQueue = (worker, concurency = 1)=>{
12
- const queue = new Set();
13
- const processing = new Map();
14
- const iterator = queue[Symbol.iterator]();
15
- let next;
16
- let counter = 0;
17
- queueMicrotask(async ()=>{
18
- while(true){
19
- if (concurency > 0 && processing.size === concurency) {
20
- await Promise.race(processing.values());
21
- }
22
- if (queue.size === 0) {
23
- const { promise, resolve } = Promise.withResolvers();
24
- next = resolve;
25
- await promise;
26
- }
27
- const result = iterator.next();
28
- if (result.done) {
29
- break;
30
- }
31
- const id = counter++;
32
- const task = Promise.resolve(worker(result.value)).catch(()=>{}).finally(()=>{
33
- processing.delete(id);
34
- });
35
- processing.set(id, task);
36
- }
37
- });
38
- return {
39
- push: async (input)=>{
40
- queue.add(input);
41
- if (queue.size === 0) {
42
- next?.();
43
- }
44
- }
45
- };
46
- };
47
-
48
- //# sourceMappingURL=queue.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queue.ts"],"sourcesContent":["export const createQueue = <T>(worker: (task: T) => Promise<void>, concurency: number = 1) => {\n const queue = new Set<T>();\n const processing = new Map<number, Promise<void>>();\n const iterator = queue[Symbol.iterator]();\n\n let next: () => void;\n let counter = 0;\n\n queueMicrotask(async () => {\n while (true) {\n if (concurency > 0 && processing.size === concurency) {\n await Promise.race(processing.values());\n }\n if (queue.size === 0) {\n const { promise, resolve } = Promise.withResolvers<void>();\n next = resolve;\n await promise;\n }\n const result = iterator.next();\n if (result.done) {\n break;\n }\n const id = counter++;\n const task = Promise.resolve(worker(result.value))\n .catch(() => {})\n .finally(() => {\n processing.delete(id);\n });\n processing.set(id, task);\n }\n });\n\n return {\n push: async (input: T) => {\n queue.add(input);\n\n if (queue.size === 0) {\n next?.();\n }\n },\n };\n};\n"],"names":["createQueue","worker","concurency","queue","Set","processing","Map","iterator","Symbol","next","counter","queueMicrotask","size","Promise","race","values","promise","resolve","withResolvers","result","done","id","task","value","catch","finally","delete","set","push","input","add"],"mappings":";;;;+BAAaA;;;eAAAA;;;AAAN,MAAMA,cAAc,CAAIC,QAAoCC,aAAqB,CAAC;IACvF,MAAMC,QAAQ,IAAIC;IAClB,MAAMC,aAAa,IAAIC;IACvB,MAAMC,WAAWJ,KAAK,CAACK,OAAOD,QAAQ,CAAC;IAEvC,IAAIE;IACJ,IAAIC,UAAU;IAEdC,eAAe;QACb,MAAO,KAAM;YACX,IAAIT,aAAa,KAAKG,WAAWO,IAAI,KAAKV,YAAY;gBACpD,MAAMW,QAAQC,IAAI,CAACT,WAAWU,MAAM;YACtC;YACA,IAAIZ,MAAMS,IAAI,KAAK,GAAG;gBACpB,MAAM,EAAEI,OAAO,EAAEC,OAAO,EAAE,GAAGJ,QAAQK,aAAa;gBAClDT,OAAOQ;gBACP,MAAMD;YACR;YACA,MAAMG,SAASZ,SAASE,IAAI;YAC5B,IAAIU,OAAOC,IAAI,EAAE;gBACf;YACF;YACA,MAAMC,KAAKX;YACX,MAAMY,OAAOT,QAAQI,OAAO,CAAChB,OAAOkB,OAAOI,KAAK,GAC7CC,KAAK,CAAC,KAAO,GACbC,OAAO,CAAC;gBACPpB,WAAWqB,MAAM,CAACL;YACpB;YACFhB,WAAWsB,GAAG,CAACN,IAAIC;QACrB;IACF;IAEA,OAAO;QACLM,MAAM,OAAOC;YACX1B,MAAM2B,GAAG,CAACD;YAEV,IAAI1B,MAAMS,IAAI,KAAK,GAAG;gBACpBH;YACF;QACF;IACF;AACF"}
package/build/queue.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export declare const createQueue: <T>(worker: (task: T) => Promise<void>, concurency?: number) => {
2
- push: (input: T) => Promise<void>;
3
- };
package/build/queue.js DELETED
@@ -1,38 +0,0 @@
1
- export const createQueue = (worker, concurency = 1)=>{
2
- const queue = new Set();
3
- const processing = new Map();
4
- const iterator = queue[Symbol.iterator]();
5
- let next;
6
- let counter = 0;
7
- queueMicrotask(async ()=>{
8
- while(true){
9
- if (concurency > 0 && processing.size === concurency) {
10
- await Promise.race(processing.values());
11
- }
12
- if (queue.size === 0) {
13
- const { promise, resolve } = Promise.withResolvers();
14
- next = resolve;
15
- await promise;
16
- }
17
- const result = iterator.next();
18
- if (result.done) {
19
- break;
20
- }
21
- const id = counter++;
22
- const task = Promise.resolve(worker(result.value)).catch(()=>{}).finally(()=>{
23
- processing.delete(id);
24
- });
25
- processing.set(id, task);
26
- }
27
- });
28
- return {
29
- push: async (input)=>{
30
- queue.add(input);
31
- if (queue.size === 0) {
32
- next?.();
33
- }
34
- }
35
- };
36
- };
37
-
38
- //# sourceMappingURL=queue.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/queue.ts"],"sourcesContent":["export const createQueue = <T>(worker: (task: T) => Promise<void>, concurency: number = 1) => {\n const queue = new Set<T>();\n const processing = new Map<number, Promise<void>>();\n const iterator = queue[Symbol.iterator]();\n\n let next: () => void;\n let counter = 0;\n\n queueMicrotask(async () => {\n while (true) {\n if (concurency > 0 && processing.size === concurency) {\n await Promise.race(processing.values());\n }\n if (queue.size === 0) {\n const { promise, resolve } = Promise.withResolvers<void>();\n next = resolve;\n await promise;\n }\n const result = iterator.next();\n if (result.done) {\n break;\n }\n const id = counter++;\n const task = Promise.resolve(worker(result.value))\n .catch(() => {})\n .finally(() => {\n processing.delete(id);\n });\n processing.set(id, task);\n }\n });\n\n return {\n push: async (input: T) => {\n queue.add(input);\n\n if (queue.size === 0) {\n next?.();\n }\n },\n };\n};\n"],"names":["createQueue","worker","concurency","queue","Set","processing","Map","iterator","Symbol","next","counter","queueMicrotask","size","Promise","race","values","promise","resolve","withResolvers","result","done","id","task","value","catch","finally","delete","set","push","input","add"],"mappings":"AAAA,OAAO,MAAMA,cAAc,CAAIC,QAAoCC,aAAqB,CAAC;IACvF,MAAMC,QAAQ,IAAIC;IAClB,MAAMC,aAAa,IAAIC;IACvB,MAAMC,WAAWJ,KAAK,CAACK,OAAOD,QAAQ,CAAC;IAEvC,IAAIE;IACJ,IAAIC,UAAU;IAEdC,eAAe;QACb,MAAO,KAAM;YACX,IAAIT,aAAa,KAAKG,WAAWO,IAAI,KAAKV,YAAY;gBACpD,MAAMW,QAAQC,IAAI,CAACT,WAAWU,MAAM;YACtC;YACA,IAAIZ,MAAMS,IAAI,KAAK,GAAG;gBACpB,MAAM,EAAEI,OAAO,EAAEC,OAAO,EAAE,GAAGJ,QAAQK,aAAa;gBAClDT,OAAOQ;gBACP,MAAMD;YACR;YACA,MAAMG,SAASZ,SAASE,IAAI;YAC5B,IAAIU,OAAOC,IAAI,EAAE;gBACf;YACF;YACA,MAAMC,KAAKX;YACX,MAAMY,OAAOT,QAAQI,OAAO,CAAChB,OAAOkB,OAAOI,KAAK,GAC7CC,KAAK,CAAC,KAAO,GACbC,OAAO,CAAC;gBACPpB,WAAWqB,MAAM,CAACL;YACpB;YACFhB,WAAWsB,GAAG,CAACN,IAAIC;QACrB;IACF;IAEA,OAAO;QACLM,MAAM,OAAOC;YACX1B,MAAM2B,GAAG,CAACD;YAEV,IAAI1B,MAAMS,IAAI,KAAK,GAAG;gBACpBH;YACF;QACF;IACF;AACF,EAAE"}
package/src/queue.ts DELETED
@@ -1,42 +0,0 @@
1
- export const createQueue = <T>(worker: (task: T) => Promise<void>, concurency: number = 1) => {
2
- const queue = new Set<T>();
3
- const processing = new Map<number, Promise<void>>();
4
- const iterator = queue[Symbol.iterator]();
5
-
6
- let next: () => void;
7
- let counter = 0;
8
-
9
- queueMicrotask(async () => {
10
- while (true) {
11
- if (concurency > 0 && processing.size === concurency) {
12
- await Promise.race(processing.values());
13
- }
14
- if (queue.size === 0) {
15
- const { promise, resolve } = Promise.withResolvers<void>();
16
- next = resolve;
17
- await promise;
18
- }
19
- const result = iterator.next();
20
- if (result.done) {
21
- break;
22
- }
23
- const id = counter++;
24
- const task = Promise.resolve(worker(result.value))
25
- .catch(() => {})
26
- .finally(() => {
27
- processing.delete(id);
28
- });
29
- processing.set(id, task);
30
- }
31
- });
32
-
33
- return {
34
- push: async (input: T) => {
35
- queue.add(input);
36
-
37
- if (queue.size === 0) {
38
- next?.();
39
- }
40
- },
41
- };
42
- };