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
Binary file modified archify.zip
Binary file not shown.
19 changes: 17 additions & 2 deletions archify/renderers/waterfall/render-waterfall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ for (const [index, span] of raw.entries()) {
const hasDuration = Number.isFinite(span.duration);
const timing = (message, fixes) => fail('waterfall/invalid-timing', `Span "${span.id}" ${message}`, { path: `/spans/${index}`, nodeId: span.id },
{ start: span.start, end: span.end ?? null, duration: span.duration ?? null, status: span.status ?? 'ok' }, fixes);
if (hasDuration && !Number.isFinite(span.start + span.duration)) {
timing('has a derived end outside the finite numeric range.', ['Rescale the recorded timing and unit to a representable range.']);
}
if (hasEnd && span.end < span.start) timing(`ends (${span.end}) before it starts (${span.start}).`, ['Correct start or end from the recorded timing.']);
if (hasEnd && hasDuration && Math.abs(span.end - span.start - span.duration) > 1e-9) {
timing(`states end ${span.end} and duration ${span.duration}, which disagree with start ${span.start}.`, ['Keep only one of end or duration.']);
Expand Down Expand Up @@ -126,7 +129,7 @@ const rows = [];
}(spans.filter((span) => span.parent === undefined), 0));

function number(value) {
const rounded = Math.round(value * 100) / 100;
const rounded = Math.abs(value) > Number.MAX_VALUE / 100 ? value : Math.round(value * 100) / 100;
return rounded.toLocaleString('en-US', { maximumFractionDigits: 2 });
}
const formatDuration = (value) => `${number(value)} ${unitLabel}`;
Expand Down Expand Up @@ -157,6 +160,11 @@ const labelW = Math.ceil(Math.max(140, ...rows.map(nameWidth)));
const axisX0 = layout.margin + labelW + layout.columnGap;
const axisX1 = axisX0 + layout.axisWidth;
const scale = wall > 0 ? layout.axisWidth / wall : 0;
if (!Number.isFinite(scale)) {
fail('waterfall/invalid-timing', 'The recorded range cannot form a finite axis scale.',
{ path: '/spans' }, { start: t0, end: t1, wall }, ['Rescale the recorded timing and unit to a representable range.']);
throwDiagnosticProblems('Waterfall validation failed', problems, { diagnostics: details });
}
const xOf = (t) => Math.round((axisX0 + (t - t0) * scale) * 100) / 100;
const rowsTop = layout.top + layout.axisH;

Expand All @@ -171,7 +179,14 @@ const tickStep = (() => {
}
})();
const ticks = [];
for (let t = Math.ceil(t0 / tickStep) * tickStep; t <= t1 + 1e-9; t += tickStep) ticks.push({ t, x: xOf(t) });
const firstTick = Math.ceil(t0 / tickStep) * tickStep;
const tickCount = Math.max(0, Math.floor((t1 - firstTick) / tickStep + 1e-9) + 1);
for (let index = 0; index < tickCount; index += 1) {
const t = firstTick + index * tickStep;
// Large timestamps can round adjacent ticks to the same double. Index-based
// iteration remains bounded even when adding tickStep cannot advance t.
if (ticks.at(-1)?.t !== t) ticks.push({ t, x: xOf(t) });
}

const LABEL_GAP = 7;
const parentIds = new Set(rows.map((row) => row.parent).filter((id) => id !== undefined));
Expand Down
44 changes: 43 additions & 1 deletion test/waterfall-rendering.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function run(diagram, extra = []) {
const input = path.join(directory, 'candidate.waterfall.json');
const output = path.join(directory, 'candidate.html');
fs.writeFileSync(input, JSON.stringify(diagram));
return { ...spawnSync(process.execPath, [renderer, input, output, ...extra], { cwd: directory, encoding: 'utf8' }), output };
return { ...spawnSync(process.execPath, [renderer, input, output, ...extra], { cwd: directory, encoding: 'utf8', timeout: 10000 }), input, output };
}
function layoutOf(diagram) {
const result = run(diagram, ['--layout-json']);
Expand Down Expand Up @@ -58,6 +58,48 @@ test('waterfall: bar geometry is exactly the recorded timing on one axis', () =>
assert.deepEqual(report.rows.map((row) => row.id), ['request', 'auth', 'inventory', 'discount', 'payment', 'save']);
});

test('waterfall: large timestamps terminate when tick increments are below floating-point resolution', () => {
const diagram = clone(small);
diagram.spans = [{ id: 'request', name: 'Request', start: 1e16, end: 1e16 + 2 }];
const result = run(diagram, ['--layout-json']);
assert.equal(result.status, 0, result.stderr || result.error?.message);
const report = JSON.parse(result.stdout);
assert.equal(report.wall, 2);
assert.equal(report.rows[0].label, '2 ms');
assert.ok(Number.isFinite(report.rows[0].x0));
assert.ok(Number.isFinite(report.rows[0].x1));
assert.ok(report.rows[0].x1 > report.rows[0].x0);
});

test('waterfall: unrepresentable derived timing is rejected without writing invalid geometry', () => {
for (const span of [
{ id: 'request', name: 'Request', start: 1e308, duration: 1e308 },
{ id: 'request', name: 'Request', start: 0, duration: Number.MIN_VALUE },
]) {
const diagram = clone(small);
diagram.spans = [span];
const result = run(diagram);
assert.equal(result.status, 1, result.stderr || result.error?.message);
assert.match(result.stderr, /finite/);
assert.equal(fs.existsSync(result.output), false);
const cli = spawnSync(process.execPath, [path.join(skillRoot, 'bin/archify.mjs'), 'validate', 'waterfall', result.input, '--json'], { encoding: 'utf8', timeout: 10000 });
assert.equal(cli.status, 1, cli.stderr || cli.stdout);
const receipt = JSON.parse(cli.stdout);
const diagnostic = receipt.diagnostics.find(({ code }) => code === 'waterfall/invalid-timing');
assert.ok(diagnostic, cli.stdout);
assert.ok(diagnostic.supportedFixes.length > 0, cli.stdout);
}
});

test('waterfall: finite very large timing labels do not overflow while formatting', () => {
const diagram = clone(small);
diagram.spans = [{ id: 'request', name: 'Request', start: 1e307, end: 1.1e307 }];
const result = run(diagram);
assert.equal(result.status, 0, result.stderr || result.error?.message);
const html = fs.readFileSync(result.output, 'utf8');
assert.doesNotMatch(html, /(?:NaN|Infinity|∞)/);
});

test('waterfall: percentages name their denominator and are never summed into the parent', () => {
const html = fs.readFileSync(run(small).output, 'utf8');
assert.match(html, /data-node-id="payment"[^>]*aria-label="[^"]*400–1,050 ms · 650 ms · 54% of 1,200 ms wall-clock/);
Expand Down
Loading