Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ reanalysis.
benchmark for arithmetic shapes. `benchmark-nested-fallbacks.js` does the
same for nested `var()` fallbacks. Both accept `--baseline`, `--blocks`,
`--max-attempts`, `--seed`, and `--output`, and write schema-v2 artifacts under
`reports/benchmarks/`.
`reports/benchmarks/`. The default arithmetic grid uses four logarithmically
spaced sizes with uniform doubling steps (`2,000` to `16,000`) and a tuned
batch schedule so a controlled run completes under 5 minutes while preserving
the paired fresh-process blocks, balanced process order, stratified interval,
doubling-growth gate, and family-adjusted precision gate; request more
`--blocks` to trade time for power.
- Parser benchmark exit codes are `0` pass, `1` regression, `2` inconclusive,
`3` benchmark/correctness/infrastructure failure, and `64` invalid usage or
artifact. Twenty blocks are the minimum operational floor, not a guarantee
Expand Down
50 changes: 33 additions & 17 deletions scripts/lib/parser-benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
DECISION_INTERVAL_METHOD,
DRIFT_THRESHOLD,
GROWTH_THRESHOLD,
MAX_WARMUPS,
MEASURED_BATCHES,
linearRegression,
logRatio,
materializeBaseline,
Expand All @@ -27,8 +25,17 @@ import {
validateSchemaV2Artifact,
} from './benchmark.js';

const SIZES = [500, 1_000, 2_000, 4_000, 8_000, 16_000];
// Four logarithmically spaced sizes with uniform doubling steps (2x)
// keep the scaling claims (slope and doubling growth) sound and well-powered
// while holding the default 20-block run to under five minutes.
const SIZES = [2_000, 4_000, 8_000, 16_000];
const DEPTHS = [16, 32, 64, 128, 256, 512];
// 16ms batch targets provide ample separation above the timer resolution floor
// while keeping worker durations concise.
const PARSER_TARGET_BATCH_MS = 16;
const PARSER_WARMUP_MINIMUM = 4;
const PARSER_WARMUP_MAXIMUM = 8;
const PARSER_MEASURED_BATCHES = 5;
const SCRIPT_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const WORKER = join(SCRIPT_ROOT, 'parser-benchmark-worker.js');

Expand Down Expand Up @@ -160,10 +167,8 @@ function analyzeParser(
endpoints.push(endpoint);
}

const largest = endpoints.filter(
(endpoint) =>
endpoint.key.endsWith(':16000') || endpoint.key.endsWith(':512')
);
const largestKeys = largestSizeKeys(artifact.workloadKeys);
const largest = endpoints.filter((endpoint) => largestKeys.has(endpoint.key));
const slopes = analyzeSlopes(artifact, blocks, config);
const growthData = analyzeGrowth(artifact, blocks);
const familyRows = blocks.map((_, index) => [
Expand Down Expand Up @@ -599,17 +604,18 @@ function analyzeGrowth(artifact, blocks) {
for (const [group, items] of groups) {
items.sort((a, b) => a.size - b.size);
for (let i = 1; i < items.length; i++) {
const doublings = Math.log2(items[i].size / items[i - 1].size);
const base = [];
const cand = [];
for (const block of blocks) {
base.push(
const baseRatio =
median(findWorkload(block, 'baseline', items[i].key).measured) /
median(findWorkload(block, 'baseline', items[i - 1].key).measured)
);
cand.push(
median(findWorkload(block, 'baseline', items[i - 1].key).measured);
const candRatio =
median(findWorkload(block, 'candidate', items[i].key).measured) /
median(findWorkload(block, 'candidate', items[i - 1].key).measured)
);
median(findWorkload(block, 'candidate', items[i - 1].key).measured);
base.push(doublings === 1 ? baseRatio : baseRatio ** (1 / doublings));
cand.push(doublings === 1 ? candRatio : candRatio ** (1 / doublings));
}
logs.push(cand.map(Math.log));
results.push({
Expand Down Expand Up @@ -665,6 +671,16 @@ function parseKey(key) {
if (parts.length === 3) return { key, shape: parts[0], mode: parts[1], size };
return { key, shape: 'nested-fallbacks', mode: parts[0], size };
}
function largestSizeKeys(workloadKeys) {
const maximum = new Map();
for (const key of workloadKeys) {
const { shape, mode, size } = parseKey(key);
const group = `${shape}:${mode}`;
maximum.set(group, Math.max(maximum.get(group) ?? 0, size));
}
return new Set([...maximum].map(([group, size]) => `${group}:${size}`));
}

function findWorkload(block, revision, key) {
return block.revisions
.find((item) => item.revision === revision)
Expand Down Expand Up @@ -700,10 +716,10 @@ export function runParserBenchmark({
requestedBlocks: blocks,
minimumBlocks: MIN_VALID_BLOCKS,
maxAttempts,
targetBatchMs: 25,
warmupMinimum: 5,
warmupMaximum: MAX_WARMUPS,
measuredBatchCount: MEASURED_BATCHES,
targetBatchMs: PARSER_TARGET_BATCH_MS,
warmupMinimum: PARSER_WARMUP_MINIMUM,
warmupMaximum: PARSER_WARMUP_MAXIMUM,
measuredBatchCount: PARSER_MEASURED_BATCHES,
driftThreshold: DRIFT_THRESHOLD,
bootstrapResamples: 100_000,
confidence: 0.95,
Expand Down
9 changes: 6 additions & 3 deletions scripts/parser-benchmark-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,19 @@ function control() {
};
const run = makeRun(workload);
const repetitions = 50;
for (let i = 0; i < 5; i++) timedBatch(run, repetitions);
// The first batches after process start are slower (JIT and frequency
// ramp). Warm to a steady state before sampling so before/after ratios
// measure drift during the run rather than process startup.
for (let i = 0; i < 10; i++) timedBatch(run, repetitions);
const samples = [];
const elapsedMs = [];
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 5; i++) {
const batch = timedBatch(run, repetitions);
samples.push(batch.perRun);
elapsedMs.push(batch.elapsed);
}
samples.sort((a, b) => a - b);
return { medianMs: samples[1], samplesMs: elapsedMs };
return { medianMs: samples[2], samplesMs: elapsedMs };
}

const before = control();
Expand Down
25 changes: 0 additions & 25 deletions src/lib/calculation-type.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ function negate(node) {
}
if (node.type === 'Sum') {
// A grouped sum may contain opaque terms whose meaning depends on the
// surrounding context. Keep the group intact so `-(a + b)` cannot turn
// surrounding context. Keep the group intact so `-1 * (a + b)` cannot turn
// into `-a - b` while it is still unresolved.
if (node.grouped) {
return mkSum([{ sign: -1, node }]);
Expand Down
18 changes: 4 additions & 14 deletions src/lib/parser.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
// Pratt parser over native @csstools/css-tokenizer tokens.
import { TokenType as CssType } from '@csstools/css-tokenizer';
import { baseOf } from './convertUnits.js';
import {
call,
dim,
ident,
mkProduct,
mkSum,
negate,
num,
opaqueCall,
} from './node.js';
import { call, dim, ident, mkProduct, mkSum, num, opaqueCall } from './node.js';
import { isCalculationFunction, isSupportedMathFunction } from './functions.js';
import { assertDepth } from './limits.js';
import { CSS_NUMBER_PREFIX } from './regex.js';
Expand Down Expand Up @@ -293,11 +284,10 @@ function parsePrefix(input, cursor, token, depth) {
? { ...expression, grouped: true }
: expression;
}
case '-':
return negate(parseExpr(input, cursor, 7, depth + 1));
case '+':
return parseExpr(input, cursor, 7, depth + 1);
}
// No unary `+`/`-` production exists in the <calc-value> grammar; a
// sign is only valid inside a number/dimension token or as a binary
// operator. `-(...)` therefore fails to parse and is preserved.
}
throw new Error(`Unexpected token "${token.raw}" at position ${token.pos}`);
}
Expand Down
Loading
Loading