@zmdb/compiler 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +40 -0
- package/dist/codegen/index.d.ts +50 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +364 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/scan.d.ts +105 -0
- package/dist/codegen/scan.d.ts.map +1 -0
- package/dist/codegen/scan.js +450 -0
- package/dist/codegen/scan.js.map +1 -0
- package/dist/codegen/witness.d.ts +76 -0
- package/dist/codegen/witness.d.ts.map +1 -0
- package/dist/codegen/witness.js +690 -0
- package/dist/codegen/witness.js.map +1 -0
- package/dist/config/contract.d.ts +41 -0
- package/dist/config/contract.d.ts.map +1 -0
- package/dist/config/contract.js +5 -0
- package/dist/config/contract.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +277 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.zmdb.generated.d.ts +12 -0
- package/dist/config/index.zmdb.generated.js +28 -0
- package/dist/config/index.zmdb.witness.d.ts +3 -0
- package/dist/config/index.zmdb.witness.d.ts.map +1 -0
- package/dist/config/index.zmdb.witness.js +20 -0
- package/dist/config/index.zmdb.witness.js.map +1 -0
- package/dist/configured-plugin.d.ts +11 -0
- package/dist/configured-plugin.d.ts.map +1 -0
- package/dist/configured-plugin.js +27 -0
- package/dist/configured-plugin.js.map +1 -0
- package/dist/emit/index.d.ts +103 -0
- package/dist/emit/index.d.ts.map +1 -0
- package/dist/emit/index.js +1356 -0
- package/dist/emit/index.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/lint/ast.d.ts +17 -0
- package/dist/lint/ast.d.ts.map +1 -0
- package/dist/lint/ast.js +77 -0
- package/dist/lint/ast.js.map +1 -0
- package/dist/lint/host-types.d.ts +9 -0
- package/dist/lint/host-types.d.ts.map +1 -0
- package/dist/lint/host-types.js +2 -0
- package/dist/lint/host-types.js.map +1 -0
- package/dist/lint/index.d.ts +23 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +53 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts +3 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js +71 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js.map +1 -0
- package/dist/lint/rules/no-empty-patch.d.ts +3 -0
- package/dist/lint/rules/no-empty-patch.d.ts.map +1 -0
- package/dist/lint/rules/no-empty-patch.js +26 -0
- package/dist/lint/rules/no-empty-patch.js.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts +3 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.js +35 -0
- package/dist/lint/rules/no-interpolated-sql.js.map +1 -0
- package/dist/lint/rules/no-unbounded-find.d.ts +3 -0
- package/dist/lint/rules/no-unbounded-find.d.ts.map +1 -0
- package/dist/lint/rules/no-unbounded-find.js +26 -0
- package/dist/lint/rules/no-unbounded-find.js.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts +3 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.js +35 -0
- package/dist/lint/rules/no-unknown-json-column.js.map +1 -0
- package/dist/lint/rules/require-sql-on-number.d.ts +3 -0
- package/dist/lint/rules/require-sql-on-number.d.ts.map +1 -0
- package/dist/lint/rules/require-sql-on-number.js +34 -0
- package/dist/lint/rules/require-sql-on-number.js.map +1 -0
- package/dist/lint/types.d.ts +12 -0
- package/dist/lint/types.d.ts.map +1 -0
- package/dist/lint/types.js +2 -0
- package/dist/lint/types.js.map +1 -0
- package/dist/metro/metro.d.ts +22 -0
- package/dist/metro/metro.d.ts.map +1 -0
- package/dist/metro/metro.js +242 -0
- package/dist/metro/metro.js.map +1 -0
- package/dist/protobuf/decode.d.ts +20 -0
- package/dist/protobuf/decode.d.ts.map +1 -0
- package/dist/protobuf/decode.js +289 -0
- package/dist/protobuf/decode.js.map +1 -0
- package/dist/protobuf/descriptor.d.ts +16 -0
- package/dist/protobuf/descriptor.d.ts.map +1 -0
- package/dist/protobuf/descriptor.js +303 -0
- package/dist/protobuf/descriptor.js.map +1 -0
- package/dist/protobuf/encode.d.ts +20 -0
- package/dist/protobuf/encode.d.ts.map +1 -0
- package/dist/protobuf/encode.js +203 -0
- package/dist/protobuf/encode.js.map +1 -0
- package/dist/protobuf/grpc-ir.d.ts +16 -0
- package/dist/protobuf/grpc-ir.d.ts.map +1 -0
- package/dist/protobuf/grpc-ir.js +2 -0
- package/dist/protobuf/grpc-ir.js.map +1 -0
- package/dist/protobuf/plan.d.ts +42 -0
- package/dist/protobuf/plan.d.ts.map +1 -0
- package/dist/protobuf/plan.js +158 -0
- package/dist/protobuf/plan.js.map +1 -0
- package/dist/reflect/callsites.d.ts +40 -0
- package/dist/reflect/callsites.d.ts.map +1 -0
- package/dist/reflect/callsites.js +153 -0
- package/dist/reflect/callsites.js.map +1 -0
- package/dist/reflect/index.d.ts +106 -0
- package/dist/reflect/index.d.ts.map +1 -0
- package/dist/reflect/index.js +1421 -0
- package/dist/reflect/index.js.map +1 -0
- package/dist/reflect/session.d.ts +118 -0
- package/dist/reflect/session.d.ts.map +1 -0
- package/dist/reflect/session.js +228 -0
- package/dist/reflect/session.js.map +1 -0
- package/dist/testing/index.d.ts +69 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +210 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transform/index.d.ts +74 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +572 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/unplugin/index.d.ts +53 -0
- package/dist/unplugin/index.d.ts.map +1 -0
- package/dist/unplugin/index.js +130 -0
- package/dist/unplugin/index.js.map +1 -0
- package/dist/unplugin/inline-bench.d.ts +29 -0
- package/dist/unplugin/inline-bench.d.ts.map +1 -0
- package/dist/unplugin/inline-bench.js +82 -0
- package/dist/unplugin/inline-bench.js.map +1 -0
- package/package.json +110 -0
- package/src/codegen/index.ts +451 -0
- package/src/codegen/scan.ts +574 -0
- package/src/codegen/witness.ts +819 -0
- package/src/config/contract.ts +48 -0
- package/src/config/index.ts +384 -0
- package/src/config/index.zmdb.generated.d.ts +12 -0
- package/src/config/index.zmdb.generated.js +28 -0
- package/src/config/index.zmdb.witness.ts +22 -0
- package/src/configured-plugin.ts +35 -0
- package/src/emit/index.ts +1496 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +336 -0
- package/src/lint/ast.ts +96 -0
- package/src/lint/host-types.ts +9 -0
- package/src/lint/index.ts +80 -0
- package/src/lint/rules/no-distributed-nullable-tags.ts +76 -0
- package/src/lint/rules/no-empty-patch.ts +26 -0
- package/src/lint/rules/no-interpolated-sql.ts +45 -0
- package/src/lint/rules/no-unbounded-find.ts +26 -0
- package/src/lint/rules/no-unknown-json-column.ts +36 -0
- package/src/lint/rules/require-sql-on-number.ts +34 -0
- package/src/lint/types.ts +11 -0
- package/src/metro/metro.ts +313 -0
- package/src/protobuf/decode.ts +375 -0
- package/src/protobuf/descriptor.ts +353 -0
- package/src/protobuf/encode.ts +277 -0
- package/src/protobuf/grpc-ir.ts +17 -0
- package/src/protobuf/plan.ts +244 -0
- package/src/reflect/callsites.ts +198 -0
- package/src/reflect/index.ts +1723 -0
- package/src/reflect/session.ts +276 -0
- package/src/testing/index.ts +298 -0
- package/src/transform/index.ts +690 -0
- package/src/unplugin/index.ts +182 -0
- package/src/unplugin/inline-bench.ts +92 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// The bundler hook. One compiler session per build, one transform per module.
|
|
2
|
+
//
|
|
3
|
+
// `enforce: 'pre'` is load-bearing rather than decorative. The transform rewrites by
|
|
4
|
+
// text offset against the positions the TypeScript compiler parsed, so it has to see
|
|
5
|
+
// the module before anything else edits it. Running late means `sourceFile.text !== code`
|
|
6
|
+
// and the whole file degrades to the tag-only path — silently faster to build and
|
|
7
|
+
// silently slower to run, which is the worst way for this to fail.
|
|
8
|
+
//
|
|
9
|
+
// The session is opened on the first module and held for the build (REQ-TF-11). Loading
|
|
10
|
+
// the project is the expensive half; a checker call is a cheap round-trip. A session per
|
|
11
|
+
// file would make the AOT path cost more than the runtime walker it replaces.
|
|
12
|
+
|
|
13
|
+
import { resolveNaming, type NamingStrategyConfig } from '@zmdb/schema/naming';
|
|
14
|
+
|
|
15
|
+
import type { EmitOptions } from '../emit/index.js';
|
|
16
|
+
import { ReflectSession } from '../reflect/session.js';
|
|
17
|
+
import { transformCode, transformFile, type TransformDiagnostic } from '../transform/index.js';
|
|
18
|
+
|
|
19
|
+
/** Inline `validate(tags.X(…), …)` with no compiler. Type arguments need `transformFile`. */
|
|
20
|
+
export function transformTypeChecks(code: string): string {
|
|
21
|
+
return transformCode(code);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UnpluginLike {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/**
|
|
27
|
+
* Rollup/Vite ordering. `'pre'` because the offsets this plugin rewrites at are only
|
|
28
|
+
* valid for the text the compiler parsed.
|
|
29
|
+
*/
|
|
30
|
+
readonly enforce?: 'pre' | 'post';
|
|
31
|
+
transform(code: string, id: string): { code: string } | null;
|
|
32
|
+
/**
|
|
33
|
+
* Watch mode: re-check the changed file without reloading the project.
|
|
34
|
+
*
|
|
35
|
+
* The `change` argument is Rollup's, and it matters rather than being decorative: a
|
|
36
|
+
* `changed` notification for a file the program has never seen does nothing at all, so a
|
|
37
|
+
* newly added module has to be reported as `create` or it stays invisible.
|
|
38
|
+
*/
|
|
39
|
+
watchChange?(id: string, change?: WatchChange): void;
|
|
40
|
+
buildEnd?(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Rollup's second argument to `watchChange`. */
|
|
44
|
+
export interface WatchChange {
|
|
45
|
+
readonly event: 'create' | 'update' | 'delete';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ZmdbAotOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Absolute path to the `tsconfig.json` that defines the program. Without one — and
|
|
51
|
+
* without `session` — the plugin cannot ask what a type is, so it inlines only the
|
|
52
|
+
* `validate(tags.X(…), …)` form and leaves every `f<T>(…)` call alone.
|
|
53
|
+
*/
|
|
54
|
+
readonly project?: string;
|
|
55
|
+
readonly cwd?: string;
|
|
56
|
+
/** An already-open session. The caller keeps ownership, including closing it. */
|
|
57
|
+
readonly session?: ReflectSession;
|
|
58
|
+
readonly emit?: EmitOptions;
|
|
59
|
+
/** A named or custom build-time strategy, resolved once for this plugin instance. */
|
|
60
|
+
readonly naming?: NamingStrategyConfig;
|
|
61
|
+
/**
|
|
62
|
+
* Called for each refused call site. The default throws, because a type the emitter
|
|
63
|
+
* could not model means the call falls back to a runtime walker that has no
|
|
64
|
+
* descriptor to walk — a build that "succeeded" and a program that throws on first
|
|
65
|
+
* use (plan D4).
|
|
66
|
+
*/
|
|
67
|
+
readonly onDiagnostic?: (diagnostic: TransformDiagnostic) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SOURCE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs)$/;
|
|
71
|
+
|
|
72
|
+
export function zmdbAot(options: ZmdbAotOptions = {}): UnpluginLike {
|
|
73
|
+
let session: ReflectSession | undefined = options.session;
|
|
74
|
+
const reflect = { naming: resolveNaming(options.naming) } as const;
|
|
75
|
+
/** Whether this plugin opened the session, and therefore has to close it. */
|
|
76
|
+
let owned = false;
|
|
77
|
+
/** Files already refreshed after a text mismatch, so the retry happens at most once. */
|
|
78
|
+
const retried = new Set<string>();
|
|
79
|
+
|
|
80
|
+
const ensureSession = (): ReflectSession | undefined => {
|
|
81
|
+
if (session) return session;
|
|
82
|
+
if (options.project === undefined) return undefined;
|
|
83
|
+
session = ReflectSession.open({ project: options.project, ...(options.cwd ? { cwd: options.cwd } : {}) });
|
|
84
|
+
owned = true;
|
|
85
|
+
return session;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
name: 'zmdb-aot',
|
|
90
|
+
enforce: 'pre',
|
|
91
|
+
|
|
92
|
+
transform(code: string, id: string): { code: string } | null {
|
|
93
|
+
// Never dependencies and never declaration files: a package ships already-built
|
|
94
|
+
// JavaScript, and rewriting it would be rewriting someone else's compiled output.
|
|
95
|
+
if (id.includes('node_modules')) return null;
|
|
96
|
+
if (id.endsWith('.d.ts') || !SOURCE.test(id)) return null;
|
|
97
|
+
|
|
98
|
+
const open = ensureSession();
|
|
99
|
+
if (!open) {
|
|
100
|
+
const out = transformCode(code);
|
|
101
|
+
return out === code ? null : { code: out };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Workspace package exports point at source files, so a bundler can hand us a
|
|
105
|
+
// dependency whose path does not contain `node_modules`. The compiler still marks
|
|
106
|
+
// that file as an external library. Treat it exactly like an installed dependency:
|
|
107
|
+
// framework code ships its own build and must not be recompiled as consumer code.
|
|
108
|
+
const sourceFile = open.sourceFile(id);
|
|
109
|
+
if (sourceFile !== undefined && open.program.isSourceFileFromExternalLibrary(sourceFile)) return null;
|
|
110
|
+
|
|
111
|
+
let result = transformFile(id, code, {
|
|
112
|
+
session: open,
|
|
113
|
+
reflect,
|
|
114
|
+
...(options.emit ? { emit: options.emit } : {}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// A stale program is the likely cause of a text mismatch, and it is cheap to rule
|
|
118
|
+
// out: re-check this one file and try again. Once per file — if the text still
|
|
119
|
+
// disagrees, something else edited the module and no refresh will fix that.
|
|
120
|
+
if (isStale(result.diagnostics) && !retried.has(id)) {
|
|
121
|
+
retried.add(id);
|
|
122
|
+
// A module the program has never seen has to be announced as created; telling it a
|
|
123
|
+
// file it does not know about "changed" is a no-op.
|
|
124
|
+
if (open.sourceFile(id)) open.refresh([id]);
|
|
125
|
+
else open.created([id]);
|
|
126
|
+
result = transformFile(id, code, {
|
|
127
|
+
session: open,
|
|
128
|
+
reflect,
|
|
129
|
+
...(options.emit ? { emit: options.emit } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const refusals = result.diagnostics.filter(diagnostic => diagnostic.position !== undefined);
|
|
134
|
+
if (refusals.length > 0) {
|
|
135
|
+
if (options.onDiagnostic) for (const refusal of refusals) options.onDiagnostic(refusal);
|
|
136
|
+
else throw new Error(formatRefusals(refusals));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result.changed ? { code: result.code } : null;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
watchChange(id: string, change?: WatchChange): void {
|
|
143
|
+
retried.delete(id);
|
|
144
|
+
if (!session) return;
|
|
145
|
+
if (change?.event === 'create') session.created([id]);
|
|
146
|
+
else if (change?.event === 'delete') session.deleted([id]);
|
|
147
|
+
else session.refresh([id]);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
buildEnd(): void {
|
|
151
|
+
if (owned) session?.close();
|
|
152
|
+
session = undefined;
|
|
153
|
+
owned = false;
|
|
154
|
+
retried.clear();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether the complaint was about the program's view of the file rather than the code.
|
|
161
|
+
*
|
|
162
|
+
* Both cases are recoverable and both are ordinary: the bundler is handing over a module
|
|
163
|
+
* the compiler has not read yet (a new file) or has read at an older revision (an edit).
|
|
164
|
+
* A refusal about a call site is not in here, because no amount of re-reading fixes it.
|
|
165
|
+
*/
|
|
166
|
+
function isStale(diagnostics: readonly TransformDiagnostic[]): boolean {
|
|
167
|
+
return diagnostics.some(
|
|
168
|
+
diagnostic =>
|
|
169
|
+
diagnostic.position === undefined &&
|
|
170
|
+
(diagnostic.reason.includes('not the text the compiler parsed') ||
|
|
171
|
+
diagnostic.reason.includes('not part of the TypeScript project')),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatRefusals(refusals: readonly TransformDiagnostic[]): string {
|
|
176
|
+
const lines = refusals.map(refusal => {
|
|
177
|
+
const where = refusal.path === '' ? '' : ` at \`${refusal.path}\``;
|
|
178
|
+
const printed = refusal.source === undefined ? '' : ` (${refusal.source})`;
|
|
179
|
+
return ` ${refusal.fileName}:${refusal.position ?? 0} — \`${refusal.callee ?? '?'}<T>\`${where}: ${refusal.reason}${printed}`;
|
|
180
|
+
});
|
|
181
|
+
return `@zmdb/compiler cannot compile ${refusals.length} call site(s):\n${lines.join('\n')}`;
|
|
182
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The timing helper the AOT plugin benchmarks share.
|
|
2
|
+
//
|
|
3
|
+
// Both #82 (the build produces a working inlined validator) and #83 (the acceptance gate)
|
|
4
|
+
// time the emitted check against the runtime walker. Each used to carry its own copy of the
|
|
5
|
+
// loop, so a change to how "ops/sec" is measured would have had to be made twice for the
|
|
6
|
+
// two numbers to stay comparable.
|
|
7
|
+
//
|
|
8
|
+
// Building the validator no longer lives here. It used to, back when the transform was a
|
|
9
|
+
// text parser that needed nothing but a string; now it needs a compiler, and the project
|
|
10
|
+
// harness in `emit/__testing__/project.ts` is what owns that.
|
|
11
|
+
//
|
|
12
|
+
// A note on why the loops below look the way they do, since it is the whole reason these
|
|
13
|
+
// numbers mean anything. The measured function must be called for its *result*, and that
|
|
14
|
+
// result has to reach something observable. An earlier version timed `() => void check(input)`
|
|
15
|
+
// and discarded the boolean: for the inlined AOT check — a pure function of a hoisted constant
|
|
16
|
+
// — V8 eventually proves the call has no effect and deletes the loop body. That does not read
|
|
17
|
+
// as an error, it reads as 1.4 *billion* ops/sec, about 90x the honest figure, and it only
|
|
18
|
+
// appears once the function is fully optimized. So the fixed 200k-iteration single shot was
|
|
19
|
+
// getting a roughly truthful number by not running long enough to be optimized properly, and
|
|
20
|
+
// any attempt to measure it more carefully made it worse.
|
|
21
|
+
//
|
|
22
|
+
// Hence: `fn` returns a value, every return value is folded into a counter, and the counter is
|
|
23
|
+
// read afterwards. The input is also rotated, so the checked object is not a constant the
|
|
24
|
+
// optimizer can fold the property loads out of.
|
|
25
|
+
|
|
26
|
+
import { threadCpuUsage } from 'node:process';
|
|
27
|
+
|
|
28
|
+
declare const performance: { now(): number };
|
|
29
|
+
|
|
30
|
+
function measuredOpsPerSecond(
|
|
31
|
+
fn: (index: number) => unknown,
|
|
32
|
+
n: number,
|
|
33
|
+
now: () => number,
|
|
34
|
+
ticksPerSecond: number,
|
|
35
|
+
): number {
|
|
36
|
+
let sink = 0;
|
|
37
|
+
for (let i = 0; i < 10_000; i++) if (fn(i)) sink++;
|
|
38
|
+
const start = now();
|
|
39
|
+
for (let i = 0; i < n; i++) if (fn(i)) sink++;
|
|
40
|
+
const elapsed = now() - start;
|
|
41
|
+
// Reading `sink` is what keeps the calls that produced it alive. Without a use, the whole
|
|
42
|
+
// loop is dead code and a sufficiently good optimizer is entitled to say so.
|
|
43
|
+
if (sink < 0) throw new Error('unreachable, and here to make the measured work observable');
|
|
44
|
+
return Math.round((n / elapsed) * ticksPerSecond);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ops/sec for `fn` over `n` iterations, after a 10k-iteration warm-up.
|
|
49
|
+
*
|
|
50
|
+
* `fn` receives a rotating index and must return something derived from the work it did. Both
|
|
51
|
+
* halves matter — see the file header.
|
|
52
|
+
*/
|
|
53
|
+
export function opsPerSecond(fn: (index: number) => unknown, n: number): number {
|
|
54
|
+
return measuredOpsPerSecond(fn, n, () => performance.now(), 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ops per current-thread CPU second for an absolute performance floor.
|
|
59
|
+
*
|
|
60
|
+
* Unlike wall time, this clock does not advance while another test worker has descheduled the
|
|
61
|
+
* current one. The measured work remains synchronous JavaScript on this thread, so CPU time
|
|
62
|
+
* preserves the cost distinction while removing suite contention from the result.
|
|
63
|
+
*/
|
|
64
|
+
export function threadCpuOpsPerSecond(fn: (index: number) => unknown, n: number): number {
|
|
65
|
+
return measuredOpsPerSecond(
|
|
66
|
+
fn,
|
|
67
|
+
n,
|
|
68
|
+
() => {
|
|
69
|
+
const usage = threadCpuUsage();
|
|
70
|
+
return usage.user + usage.system;
|
|
71
|
+
},
|
|
72
|
+
1_000_000,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The best of `trials` runs of `opsPerSecond(fn, n)`.
|
|
78
|
+
*
|
|
79
|
+
* For a *ratio* between two functions, one measurement each is not enough. A single timed loop
|
|
80
|
+
* here is tens of milliseconds long, and when vitest is running the whole suite across every
|
|
81
|
+
* core, one descheduling of that length is the difference between a 6x reading and a 4.6x one
|
|
82
|
+
* — which is exactly how the acceptance gate came to fail under load and pass in isolation.
|
|
83
|
+
*
|
|
84
|
+
* Best-of rather than a mean, because contention is one-sided: it can only ever make a
|
|
85
|
+
* function look slower than it is, never faster. The fastest of several runs is the closest
|
|
86
|
+
* available estimate of what the code itself does, which is what the gate is a claim about.
|
|
87
|
+
*/
|
|
88
|
+
export function peakOpsPerSecond(fn: (index: number) => unknown, n: number, trials: number): number {
|
|
89
|
+
let best = 0;
|
|
90
|
+
for (let trial = 0; trial < trials; trial++) best = Math.max(best, opsPerSecond(fn, n));
|
|
91
|
+
return best;
|
|
92
|
+
}
|