mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
test(napi/parser): investigate retained raw result costs
This commit is contained in:
1 parent
ce91724211
commit
77c641a10f
9 files changed
+3536
-31
No files matched your search
@@ -12,13 +12,15 @@ Object)` gives the result `Object.prototype` while installing the private brand.
|
||||
The constructor's prototype getters are only descriptor templates; the public
|
||||
result does not inherit from that prototype.
|
||||
|
||||
## Throughput results
|
||||
## Historical throughput results
|
||||
|
||||
Five serial paired fresh-process repetitions on 2026-11-03, Apple M4 Pro,
|
||||
Node 26.13.0 / V8 14.6.202.34-node.40, matching local release bindings built with
|
||||
`allocator,tokens`, production local JS printer. These are warmed corpus
|
||||
measurements of the eager raw entry point, not full-build or cold-start results.
|
||||
The public sync/async routing is covered by NAPI and contract tests.
|
||||
These runs discarded results during warmup, including the retained workload.
|
||||
The follow-up below corrects that mismatch without changing the runtime POC.
|
||||
|
||||
| Workload | Main median (range), seconds | POC median (range), seconds | Median change | Median max RSS MiB, main → POC |
|
||||
| ----------------------------------------- | ---------------------------- | --------------------------- | --------------- | ------------------------------ |
|
||||
@@ -39,6 +41,80 @@ private-wrapper samples are under `confirm/`; final samples use the actual edite
|
||||
`eager.js`. The interactive host's background apps/indexers were not disabled;
|
||||
final timing ranges and all individual samples are retained in `results.json`.
|
||||
|
||||
## Retained-result follow-up
|
||||
|
||||
Neither proposed replacement reliably removed the retained-AST regression. The
|
||||
runtime remains the shared-getter POC at `ce917242118dab18f6b7529bf0cc02e9e6812911`.
|
||||
[All follow-up samples, manifests, diagnostic summaries and exact trial patches](retention-results.json)
|
||||
are preserved, including unfavorable samples and screening outliers.
|
||||
|
||||
Two alternatives were confirmed through the actual public eager implementation,
|
||||
with five serial, rotated fresh-process repetitions per workload and matching
|
||||
retention during warmup. Each series compared main, the original POC, and one
|
||||
replacement using the same native/generated/printer artifacts:
|
||||
|
||||
| Retained workload, separate series | Main median | Original POC median | Replacement median | Replacement vs main |
|
||||
| ------------------------------------------- | ----------: | ------------------: | -----------------: | ------------------: |
|
||||
| Read-only data properties | 2.659s | 2.709s | 2.809s | 5.7% more time |
|
||||
| Literal getters with private-field stamping | 2.587s | 2.631s | 2.667s | 3.1% more time |
|
||||
|
||||
The data-property replacement used a fresh literal in public field order and
|
||||
`Object.defineProperties` to make its fields read-only. It changes accessors
|
||||
into configurable data properties. The literal-getter replacement stamped
|
||||
private fields onto a literal returned by a base constructor, removing both
|
||||
`Reflect.construct` and `Object.defineProperties`. Its getters were unique per
|
||||
result and referenced the receiver's private fields. The 13 focused tests and
|
||||
64 contract combinations plus async cases passed for both trials, with their
|
||||
descriptor/receiver differences made explicit. Neither replacement beat the
|
||||
original POC's retained median in its own confirmation series, so neither was
|
||||
kept. The literal lifetime probe reclaimed discarded payloads during minor GC,
|
||||
but retained private payloads after deleting accessors from live owners.
|
||||
|
||||
An isolated construction probe wrapped 32,267 preallocated tiny payloads. Five
|
||||
rotated repetitions had medians of 13.3ms for main, 25.4ms for the original POC,
|
||||
25.6ms for shared-getter stamping, 27.6ms for read-only data properties, and
|
||||
27.6ms for constructing an instance then resetting its prototype. A later
|
||||
single literal-getter sample took 18.2ms; it is not a paired median. These
|
||||
construction differences alone do not explain the historical 149ms retained
|
||||
gap, and removing reflection did not improve the complete retained workload.
|
||||
|
||||
Warmup lifetime materially affects the comparison. The worker now retains
|
||||
warmup results in `retain` mode, releases them before the premeasurement forced
|
||||
GC, and records the policy. A final control alternated policy order over five
|
||||
repetitions and reversed baseline/POC order on even repetitions:
|
||||
|
||||
| Warmup policy, original POC | Main median (range) | POC median (range) | Median change |
|
||||
| --------------------------- | -------------------: | -------------------: | --------------: |
|
||||
| Discard results | 3.166s (2.750–4.860) | 3.819s (3.553–4.547) | 20.6% more time |
|
||||
| Retain results | 2.842s (2.728–3.482) | 2.869s (2.768–2.917) | 1.0% more time |
|
||||
|
||||
The matching-policy pairs included three slower and two faster POC results.
|
||||
This corrects the benchmark's lifetime mismatch; it does not fix the runtime or
|
||||
establish retained-workload parity. The host had background contention, and
|
||||
absolute rates visibly shifted during the literal confirmation. The cause of
|
||||
that shift was not established. Do not pool absolute timings between series.
|
||||
The exploratory screen includes 17.5s main and 9.4s POC retention outliers;
|
||||
none were removed from the evidence.
|
||||
|
||||
Matched Node 26.13.0 V8 source supports two separate costs. The constructor
|
||||
[fast path checks the initial map's constructor](https://github.com/nodejs/node/blob/v26.13.0/deps/v8/src/builtins/builtins-constructor-gen.cc#L337),
|
||||
so a target/new-target mismatch can take the runtime allocation path.
|
||||
[`DefineProperties`](https://github.com/nodejs/node/blob/v26.13.0/deps/v8/src/objects/js-objects.cc#L1147)
|
||||
still enumerates and converts descriptors before applying them; caching the
|
||||
JavaScript descriptor objects does not eliminate that work. These source paths
|
||||
are not a measurement of the optimized machine code's exact cost.
|
||||
|
||||
V8's [allocation-site pretenuring decisions](https://github.com/nodejs/node/blob/v26.13.0/deps/v8/src/heap/pretenuring-handler.cc#L43)
|
||||
use survival feedback, so warmup lifetimes can change allocation behavior. In
|
||||
separate instrumented retained passes, discarded warmup produced 1781MB promoted
|
||||
for main versus 3869MB for the original POC; matching warmup produced 1941MB
|
||||
versus 1553MB. Exploratory CPU profiles attributed 35% of main's samples and 61%
|
||||
of the POC's samples to GC. These runs
|
||||
were contended and instrumented, so their elapsed times are not throughput
|
||||
claims. Disabling allocation-site pretenuring still left a retained gap
|
||||
(main 3.595s, POC 3.937s, inline data 3.786s in one traced control): adaptive
|
||||
pretenuring is not the sole cause. The remaining regression is unresolved.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The POC preserves ordinary reads, own key order (`program`, `module`, `comments`,
|
||||
@@ -163,6 +239,10 @@ pnpm -C packages/codegen build
|
||||
node napi/parser/bench/raw-result/prepare.mjs
|
||||
node napi/parser/bench/raw-result/contract.mjs
|
||||
node napi/parser/bench/raw-result/run.mjs "$CORPUS" baseline,poc raw,maps,retain 3 paired
|
||||
OXC_BENCH_WARMUP=discard node napi/parser/bench/raw-result/run.mjs "$CORPUS" baseline,poc retain 3 legacy-warmup
|
||||
node napi/parser/bench/raw-result/run.mjs "$CORPUS" baseline,prior,stamped,data,literal raw,maps,retain 3 retention-screen
|
||||
node napi/parser/bench/raw-result/contract.mjs literal
|
||||
node --expose-gc napi/parser/bench/raw-result/construction.mjs stamped /tmp/stamped-construction.json
|
||||
node napi/parser/bench/raw-result/run.mjs "$CORPUS" baseline,poc verify 1 correctness
|
||||
node napi/parser/bench/raw-result/diagnose.mjs "$CORPUS"
|
||||
node napi/parser/bench/raw-result/summarize.mjs paired
|
||||
@@ -172,6 +252,9 @@ node --expose-gc --allow-natives-syntax napi/parser/bench/raw-result/lifetime.mj
|
||||
|
||||
The runner launches serial fresh processes and rotates variant order. Sources
|
||||
are preloaded and hashed; two evenly spaced warmup passes precede forced GC.
|
||||
The default `OXC_BENCH_WARMUP=matching` retains warmup results only in `retain`
|
||||
mode, then releases them before forced GC. `OXC_BENCH_WARMUP=discard` reproduces
|
||||
the historical behavior. Warmup owners are not held through the measured pass.
|
||||
There is no forced GC inside the measured pass. Natural GC is included.
|
||||
`raw` consumes AST body/error counts; `maps` prints the complete AST with source
|
||||
maps; `retain` holds **every result** until after the pass and reads them again
|
||||
@@ -194,6 +277,13 @@ paired run saves a build manifest with native/generated-JS/printer hashes.
|
||||
`prepare.mjs` extracts main's wrapper from the pinned base, redirects all imports
|
||||
to this worktree, and asserts its patch boundaries. Set `OXC_BENCH_BASE` only
|
||||
when intentionally adapting the baseline to another revision.
|
||||
The `prior` variant extracts the original POC at the pinned `ce917` commit.
|
||||
`retention-results.json` embeds exact patches against that commit for the two
|
||||
public implementation trials. To reproduce those trials, apply one patch in an
|
||||
isolated checkout and compare `baseline,prior,poc`; the generated `data` variant
|
||||
uses a helper and is not identical to the inline public trial. The constructor
|
||||
probe retains wrappers around tiny preallocated payloads, includes natural GC,
|
||||
and does not estimate complete parsing throughput.
|
||||
|
||||
Required focused checks:
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ checkout and external reproduction were preserved. Implementation:
|
||||
[`eager.js`](../../src-js/raw-transfer/eager.js). Method, commands and caveats:
|
||||
[README](README.md). [All samples and checks](results.json).
|
||||
|
||||
Five paired fresh-process repetitions, Apple M4 Pro, Node 26.13.0:
|
||||
Historical five paired fresh-process repetitions, Apple M4 Pro, Node 26.13.0.
|
||||
These runs discarded results during warmup, including the retained workload:
|
||||
|
||||
| Workload | Main median | POC median | Change |
|
||||
| ---------------------------------- | ----------: | ---------: | --------------: |
|
||||
@@ -21,6 +22,28 @@ was similar at 4312 → 4269 MiB. Warmups, preloading and forced GC precede timi
|
||||
profiles and GC instrumentation are separate. Earlier heavily contended partial
|
||||
runs were excluded, and all final individual samples/ranges are preserved.
|
||||
|
||||
The follow-up investigated private-field stamping and read-only data properties.
|
||||
Neither removed the retained regression, so the runtime POC remains unchanged.
|
||||
In separate five-repetition confirmations through the public implementation,
|
||||
data properties took 2.809s versus main's 2.659s and the original POC's 2.709s;
|
||||
literal getters with private-field stamping took 2.667s versus 2.587s and 2.631s.
|
||||
A single literal-getter construction sample was faster, but neither trial
|
||||
established a complete retained-workload improvement. Both passed the 13 focused tests
|
||||
and 64 contract combinations plus async cases, with representation differences
|
||||
explicitly checked.
|
||||
|
||||
The benchmark now matches warmup retention to the measured workload and releases
|
||||
warmup owners before forced GC. Five counterbalanced repetitions of the original
|
||||
POC measured 3.166s main versus 3.819s POC with discarded warmup (+20.6%), and
|
||||
2.842s versus 2.869s with retained warmup (+1.0%; three slower, two faster pairs).
|
||||
This corrects methodology, not runtime behavior. Matched V8 source and GC traces
|
||||
support survival feedback affecting allocation-site pretenuring; a traced control
|
||||
with that feature disabled still left a gap. The retained regression remains
|
||||
unresolved. Background contention and visible rate changes limit comparisons;
|
||||
absolute timings from different series must not be pooled. All completed samples,
|
||||
including screening outliers, manifests, exact trial patches and diagnostic
|
||||
summaries are in [the follow-up evidence](retention-results.json).
|
||||
|
||||
The evidence supports **old-space accessor pairs retaining getter closures and
|
||||
their captured ASTs across minor GC**. In three fresh controlled repetitions,
|
||||
all 128 wrappers died after two minor GCs, but all closure-backed payloads
|
||||
@@ -49,6 +72,10 @@ imports match prior evidence. Generated output remains 217,840,905 bytes,
|
||||
SHA-256 `c56c4b008d33671dc659dc7f48e677d98d96cdb789173e36acc8934e56818cc5`.
|
||||
All 178,470 NAPI tests passed, as did focused Node 26.13 tests, parser conformance
|
||||
with unchanged snapshots, generation, formatting and focused lint.
|
||||
Those full-suite results remain applicable because the follow-up restored the
|
||||
exact original runtime, native binding, generated deserializers and printer.
|
||||
The follow-up reran focused tests and contract checks and exercised the updated
|
||||
benchmark harness separately.
|
||||
|
||||
Baseline JSON/raw differences for metadata ranges and literal lone surrogates
|
||||
were recorded separately. Local parent tests pass; the existing package file
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// oxlint-disable no-console
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { root } from "./prepare.mjs";
|
||||
|
||||
const [variant, destination] = process.argv.slice(2);
|
||||
assert.ok(variant && destination);
|
||||
const { wrapResult } = await import(
|
||||
pathToFileURL(path.join(root, "target/raw-result", `${variant}.mjs`))
|
||||
);
|
||||
const inputs = Array.from({ length: 32267 }, () => ({
|
||||
program: { body: [] },
|
||||
module: {},
|
||||
comments: [],
|
||||
errors: [],
|
||||
}));
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
for (let i = 0; i < 257; i++) wrapResult(inputs[i], null);
|
||||
}
|
||||
globalThis.gc();
|
||||
const retained = [];
|
||||
const start = performance.now();
|
||||
for (const data of inputs) retained.push(wrapResult(data, null));
|
||||
const elapsedMilliseconds = performance.now() - start;
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < retained.length; i++) {
|
||||
assert.equal(retained[i].program, inputs[i].program);
|
||||
checksum += retained[i].program.body.length + retained[i].errors.length;
|
||||
}
|
||||
const report = {
|
||||
variant,
|
||||
node: process.version,
|
||||
v8: process.versions.v8,
|
||||
count: retained.length,
|
||||
elapsedMilliseconds,
|
||||
checksum,
|
||||
};
|
||||
fs.writeFileSync(destination, JSON.stringify(report, null, 2) + "\n");
|
||||
console.log(JSON.stringify(report));
|
||||
process.exit(0);
|
||||
@@ -7,6 +7,15 @@ import { parseSync, parse } from "../../src-js/index.js";
|
||||
import "./prepare.mjs";
|
||||
|
||||
const root = fileURLToPath(new URL("../../../../", import.meta.url));
|
||||
const [variant = "poc"] = process.argv.slice(2);
|
||||
assert.ok(["poc", "prior", "stamped", "data", "reset", "literal"].includes(variant));
|
||||
const dataProperties = variant === "data";
|
||||
const candidate =
|
||||
variant === "poc"
|
||||
? { parseSyncRaw: parseSync, parse }
|
||||
: await import(pathToFileURL(path.join(root, "target/raw-result", `${variant}.mjs`)));
|
||||
const actualSync = candidate.parseSyncRaw;
|
||||
const actualAsync = candidate.parse;
|
||||
const { parseSyncRaw: baselineSync, parse: baselineAsync } = await import(
|
||||
pathToFileURL(path.join(root, "target/raw-result/baseline.mjs"))
|
||||
);
|
||||
@@ -20,7 +29,13 @@ const fixtures = [
|
||||
["x.js", 'const x = "\ud800";'],
|
||||
["x.js", "let x; let x;"],
|
||||
];
|
||||
const report = { node: process.version, optionsChecked: 0, jsonDifferences: [], compatibility: {} };
|
||||
const report = {
|
||||
variant,
|
||||
node: process.version,
|
||||
optionsChecked: 0,
|
||||
jsonDifferences: [],
|
||||
compatibility: {},
|
||||
};
|
||||
for (const range of [false, true]) {
|
||||
for (const experimentalParent of [false, true]) {
|
||||
for (const experimentalTokens of [false, true]) {
|
||||
@@ -33,19 +48,31 @@ for (const range of [false, true]) {
|
||||
};
|
||||
for (const [filename, source] of fixtures) {
|
||||
const baseline = baselineSync(filename, source, options);
|
||||
const actual = parseSync(filename, source, options);
|
||||
const actual = actualSync(filename, source, options);
|
||||
assert.deepEqual(Reflect.ownKeys(actual), Reflect.ownKeys(baseline));
|
||||
assert.equal(Object.getPrototypeOf(actual), Object.getPrototypeOf(baseline));
|
||||
for (const key of Object.keys(baseline)) {
|
||||
assert.deepEqual(actual[key], baseline[key], `${filename}: ${key}`);
|
||||
const expectedDescriptor = Object.getOwnPropertyDescriptor(baseline, key);
|
||||
const actualDescriptor = Object.getOwnPropertyDescriptor(actual, key);
|
||||
assert.deepEqual(
|
||||
{ ...actualDescriptor, get: null },
|
||||
{ ...expectedDescriptor, get: null },
|
||||
);
|
||||
assert.equal(actualDescriptor.get.name, expectedDescriptor.get.name);
|
||||
assert.equal(actualDescriptor.get.length, expectedDescriptor.get.length);
|
||||
if (dataProperties) {
|
||||
assert.deepEqual(actualDescriptor, {
|
||||
value: actual[key],
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
} else {
|
||||
assert.deepEqual(
|
||||
{ ...actualDescriptor, get: null },
|
||||
{ ...expectedDescriptor, get: null },
|
||||
);
|
||||
assert.equal(actualDescriptor.get.name, expectedDescriptor.get.name);
|
||||
assert.equal(actualDescriptor.get.length, expectedDescriptor.get.length);
|
||||
}
|
||||
assert.throws(() => {
|
||||
actual[key] = null;
|
||||
}, TypeError);
|
||||
}
|
||||
report.optionsChecked++;
|
||||
if (!experimentalParent && !experimentalTokens) {
|
||||
@@ -71,36 +98,58 @@ await Promise.all(
|
||||
experimentalParent: true,
|
||||
};
|
||||
const expected = await baselineAsync(filename, source, options);
|
||||
const actual = await parse(filename, source, options);
|
||||
const actual = await actualAsync(filename, source, options);
|
||||
for (const key of Object.keys(expected)) assert.deepEqual(actual[key], expected[key]);
|
||||
}),
|
||||
);
|
||||
const options = { experimentalRawTransfer: true };
|
||||
const baseline = baselineSync("x.js", "let x;", options);
|
||||
const actual = parseSync("x.js", "let x;", options);
|
||||
const foreign = parseSync("other.js", "let other;", options);
|
||||
const actual = actualSync("x.js", "let x;", options);
|
||||
const foreign = actualSync("other.js", "let other;", options);
|
||||
for (const key of ["program", "module", "comments", "errors"]) {
|
||||
const getBefore = Object.getOwnPropertyDescriptor(baseline, key).get;
|
||||
const getAfter = Object.getOwnPropertyDescriptor(actual, key).get;
|
||||
assert.equal(getBefore.call({}), baseline[key]);
|
||||
assert.equal(new Proxy(baseline, {})[key], baseline[key]);
|
||||
assert.throws(() => getAfter.call({}), TypeError);
|
||||
assert.throws(() => new Proxy(actual, {})[key], TypeError);
|
||||
assert.equal(getAfter.call(foreign), foreign[key]);
|
||||
if (dataProperties) {
|
||||
assert.equal(getAfter, undefined);
|
||||
assert.equal(new Proxy(actual, {})[key], actual[key]);
|
||||
assert.equal(Object.create(actual)[key], actual[key]);
|
||||
} else {
|
||||
assert.throws(() => getAfter.call({}), TypeError);
|
||||
assert.throws(() => new Proxy(actual, {})[key], TypeError);
|
||||
assert.equal(getAfter.call(foreign), foreign[key]);
|
||||
}
|
||||
}
|
||||
report.compatibility = {
|
||||
ownDescriptors: "equal",
|
||||
ownKeyOrder: "equal",
|
||||
prototype: "Object.prototype",
|
||||
privateStorageVisibleToReflectOwnKeys: false,
|
||||
getterIdentity: "shared instead of unique per result",
|
||||
detachedForeignReceiver: "TypeError instead of original result field",
|
||||
proxyWithDefaultReceiver: "TypeError instead of original result field",
|
||||
getterOnAnotherResult: "returns receiver's field instead of original result field",
|
||||
inheritedReceiver: "TypeError instead of original result field",
|
||||
};
|
||||
report.compatibility = dataProperties
|
||||
? {
|
||||
ownDescriptors: "read-only configurable data properties instead of accessors",
|
||||
ownKeyOrder: "equal",
|
||||
prototype: "Object.prototype",
|
||||
proxyWithDefaultReceiver: "supported",
|
||||
inheritedReceiver: "supported",
|
||||
}
|
||||
: {
|
||||
ownDescriptors: "equal",
|
||||
ownKeyOrder: "equal",
|
||||
prototype: "Object.prototype",
|
||||
privateStorageVisibleToReflectOwnKeys: false,
|
||||
getterIdentity:
|
||||
Object.getOwnPropertyDescriptor(actual, "program").get
|
||||
=== Object.getOwnPropertyDescriptor(foreign, "program").get
|
||||
? "shared instead of unique per result"
|
||||
: "unique per result",
|
||||
detachedForeignReceiver: "TypeError instead of original result field",
|
||||
proxyWithDefaultReceiver: "TypeError instead of original result field",
|
||||
getterOnAnotherResult: "returns receiver's field instead of original result field",
|
||||
inheritedReceiver: "TypeError instead of original result field",
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(root, "target/raw-result/contract.json"),
|
||||
path.join(
|
||||
root,
|
||||
"target/raw-result",
|
||||
variant === "poc" ? "contract.json" : `contract-${variant}.json`,
|
||||
),
|
||||
JSON.stringify(report, null, 2) + "\n",
|
||||
);
|
||||
console.log(JSON.stringify(report));
|
||||
|
||||
@@ -8,6 +8,22 @@ const nextTurn = () => new Promise((resolve) => setImmediate(resolve));
|
||||
const descriptor = Object.getOwnPropertyDescriptor;
|
||||
// oxlint-disable-next-line typescript/no-implied-eval -- V8 intrinsic syntax requires a flagged runtime.
|
||||
const inYoungGeneration = new Function("value", "return %InYoungGeneration(value)");
|
||||
class ResultBase {
|
||||
constructor(result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
class LiteralResult extends ResultBase {
|
||||
#data;
|
||||
constructor(data) {
|
||||
super({
|
||||
get program() {
|
||||
return this.#data;
|
||||
},
|
||||
});
|
||||
this.#data = data;
|
||||
}
|
||||
}
|
||||
let privateDescriptors;
|
||||
class PrivateResult {
|
||||
#data;
|
||||
@@ -65,6 +81,7 @@ function wrap(data) {
|
||||
});
|
||||
}
|
||||
if (variant === "private") return Reflect.construct(PrivateResult, [data], Object);
|
||||
if (variant === "literal") return new LiteralResult(data);
|
||||
if (variant === "plain") return { program: data };
|
||||
throw new Error(variant);
|
||||
}
|
||||
@@ -112,7 +129,10 @@ globalThis.gc();
|
||||
const major = alive();
|
||||
console.log("LIFETIME_END");
|
||||
assert.equal(initialYoung, refs.length);
|
||||
assert.equal(major.payload, operation === "delete" && variant === "private" ? refs.length : 0);
|
||||
assert.equal(
|
||||
major.payload,
|
||||
operation === "delete" && ["private", "literal"].includes(variant) ? refs.length : 0,
|
||||
);
|
||||
assert.equal(major.wrapper, operation === "delete" ? refs.length : 0);
|
||||
let afterRelease;
|
||||
if (operation === "delete") {
|
||||
|
||||
@@ -44,6 +44,9 @@ const bodies = {
|
||||
define: " return createResult(data, tokens);",
|
||||
bound: " return createResult(data, tokens);",
|
||||
private: " return Reflect.construct(RawResult, [data, tokens], Object);",
|
||||
stamped: " return new RawResult(data, tokens);",
|
||||
literal: " return new RawResult(data, tokens);",
|
||||
data: " return createResult(data, tokens);",
|
||||
plain: " if (tokens !== null) data.tokens = tokens;\n return data;",
|
||||
};
|
||||
const helpers = {
|
||||
@@ -56,13 +59,107 @@ const helpers = {
|
||||
.join(
|
||||
",",
|
||||
)}, get tokens() { return this.#tokens; }, get errors() { return this.#data.errors; } });\ntokenDescriptors=all;\ndescriptors={program:all.program,module:all.module,comments:all.comments,errors:all.errors};\n}\n}`,
|
||||
stamped: `let descriptors, tokenDescriptors;
|
||||
class ResultBase {
|
||||
constructor(tokens) {
|
||||
return Object.defineProperties({}, tokens === null ? descriptors : tokenDescriptors);
|
||||
}
|
||||
}
|
||||
class RawResult extends ResultBase {
|
||||
#data;
|
||||
#tokens;
|
||||
constructor(data, tokens) {
|
||||
super(tokens);
|
||||
this.#data = data;
|
||||
this.#tokens = tokens;
|
||||
}
|
||||
get program() { return this.#data.program; }
|
||||
get module() { return this.#data.module; }
|
||||
get comments() { return this.#data.comments; }
|
||||
get tokens() { return this.#tokens; }
|
||||
get errors() { return this.#data.errors; }
|
||||
static {
|
||||
const all = Object.getOwnPropertyDescriptors(this.prototype);
|
||||
for (const key of ["program", "module", "comments", "tokens", "errors"]) all[key].enumerable = true;
|
||||
descriptors = { program: all.program, module: all.module, comments: all.comments, errors: all.errors };
|
||||
tokenDescriptors = { program: all.program, module: all.module, comments: all.comments, tokens: all.tokens, errors: all.errors };
|
||||
}
|
||||
}`,
|
||||
data: `const descriptors = { program: { writable: false }, module: { writable: false }, comments: { writable: false }, errors: { writable: false } };
|
||||
const tokenDescriptors = { ...descriptors, tokens: { writable: false } };
|
||||
function createResult(data, tokens) {
|
||||
const result = tokens === null
|
||||
? { program: data.program, module: data.module, comments: data.comments, errors: data.errors }
|
||||
: { program: data.program, module: data.module, comments: data.comments, tokens, errors: data.errors };
|
||||
return Object.defineProperties(result, tokens === null ? descriptors : tokenDescriptors);
|
||||
}`,
|
||||
literal: `class ResultBase {
|
||||
constructor(result) { return result; }
|
||||
}
|
||||
class RawResult extends ResultBase {
|
||||
#data;
|
||||
#tokens;
|
||||
constructor(data, tokens) {
|
||||
super(tokens === null ? {
|
||||
get program() { return this.#data.program; },
|
||||
get module() { return this.#data.module; },
|
||||
get comments() { return this.#data.comments; },
|
||||
get errors() { return this.#data.errors; },
|
||||
} : {
|
||||
get program() { return this.#data.program; },
|
||||
get module() { return this.#data.module; },
|
||||
get comments() { return this.#data.comments; },
|
||||
get tokens() { return this.#tokens; },
|
||||
get errors() { return this.#data.errors; },
|
||||
});
|
||||
this.#data = data;
|
||||
this.#tokens = tokens;
|
||||
}
|
||||
}`,
|
||||
};
|
||||
for (const [name, body] of Object.entries(bodies)) {
|
||||
const source =
|
||||
original.slice(0, start) + body + "\n}\n" + original.slice(end) + "\n" + (helpers[name] ?? "");
|
||||
original.slice(0, start)
|
||||
+ body
|
||||
+ "\n}\n"
|
||||
+ original.slice(end)
|
||||
+ "\n"
|
||||
+ (helpers[name] ?? "")
|
||||
+ `\nexport function wrapResult(data, tokens) {\n${body}\n}\n`;
|
||||
fs.writeFileSync(path.join(output, `${name}.mjs`), toLocal(source));
|
||||
}
|
||||
const prior = execFileSync(
|
||||
"git",
|
||||
["show", "ce917242118dab18f6b7529bf0cc02e9e6812911:napi/parser/src-js/raw-transfer/eager.js"],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(output, "prior.mjs"),
|
||||
toLocal(prior)
|
||||
+ "\nexport function wrapResult(data, tokens) { return Reflect.construct(RawResult, [data, tokens], Object); }\n",
|
||||
);
|
||||
const reset = prior.replace(
|
||||
"return Reflect.construct(RawResult, [data, tokens], Object);",
|
||||
"return Object.setPrototypeOf(new RawResult(data, tokens), Object.prototype);",
|
||||
);
|
||||
assert.notEqual(reset, prior);
|
||||
fs.writeFileSync(
|
||||
path.join(output, "reset.mjs"),
|
||||
toLocal(reset)
|
||||
+ "\nexport function wrapResult(data, tokens) { return Object.setPrototypeOf(new RawResult(data, tokens), Object.prototype); }\n",
|
||||
);
|
||||
const tracked = [
|
||||
"napi/parser/bench/raw-result/prepare.mjs",
|
||||
"napi/parser/bench/raw-result/run.mjs",
|
||||
"napi/parser/bench/raw-result/worker.mjs",
|
||||
"napi/parser/bench/raw-result/corpus.mjs",
|
||||
"napi/parser/bench/raw-result/construction.mjs",
|
||||
"napi/parser/bench/raw-result/contract.mjs",
|
||||
"napi/parser/bench/raw-result/lifetime.mjs",
|
||||
"napi/parser/src-js/index.js",
|
||||
"napi/parser/src-js/raw-transfer/eager.js",
|
||||
"napi/parser/src-js/raw-transfer/common.js",
|
||||
"napi/parser/src-js/generated/constants.js",
|
||||
@@ -99,6 +196,14 @@ fs.writeFileSync(
|
||||
arch: process.arch,
|
||||
cpu: os.cpus()[0].model,
|
||||
baselineSourceSha256: createHash("sha256").update(original).digest("hex"),
|
||||
variants: Object.fromEntries(
|
||||
[...Object.keys(bodies), "prior", "reset"].map((name) => [
|
||||
name,
|
||||
createHash("sha256")
|
||||
.update(fs.readFileSync(path.join(output, `${name}.mjs`)))
|
||||
.digest("hex"),
|
||||
]),
|
||||
),
|
||||
hashes,
|
||||
},
|
||||
null,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -53,6 +53,7 @@ for (let run = 0; run < Number(runsText); run++) {
|
||||
const previous = results.find((row) => row.mode === mode);
|
||||
if (previous) {
|
||||
assert.deepEqual(result.corpus, previous.corpus);
|
||||
assert.deepEqual(result.warmup, previous.warmup);
|
||||
assert.equal(result.checksum, previous.checksum);
|
||||
}
|
||||
results.push(result);
|
||||
|
||||
@@ -23,6 +23,8 @@ assert.ok(
|
||||
"worker.mjs CORPUS VARIANT MODE OUTPUT [LIMIT] [none|cpu|heap]",
|
||||
);
|
||||
assert.ok(["raw", "maps", "retain", "verify"].includes(mode));
|
||||
const warmupRetention = process.env.OXC_BENCH_WARMUP ?? "matching";
|
||||
assert.ok(["matching", "discard"].includes(warmupRetention));
|
||||
const root = fileURLToPath(new URL("../../../../", import.meta.url));
|
||||
const { parseSyncRaw } = await import(
|
||||
variant === "poc"
|
||||
@@ -111,10 +113,11 @@ function execute(file, measured = false) {
|
||||
if (mode !== "verify") {
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
for (let i = 0; i < files.length; i += Math.max(1, Math.floor(files.length / 256))) {
|
||||
execute(files[i]);
|
||||
execute(files[i], mode === "retain" && warmupRetention === "matching");
|
||||
}
|
||||
}
|
||||
}
|
||||
retained.length = 0;
|
||||
checksum = errors = 0;
|
||||
globalThis.gc();
|
||||
let session;
|
||||
@@ -166,6 +169,7 @@ const report = {
|
||||
v8: process.versions.v8,
|
||||
flags: process.execArgv,
|
||||
profile,
|
||||
warmup: { passes: mode === "verify" ? 0 : 2, retention: warmupRetention },
|
||||
corpus: metadata,
|
||||
elapsedMilliseconds,
|
||||
cpuMilliseconds: (cpuUsage.user + cpuUsage.system) / 1000,
|
||||
|
||||
Reference in new issue
Block a user