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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/console/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@harperfast/prerender-console",
"version": "0.5.0",
"version": "0.6.0",
"type": "module",
"description": "Standalone Harper component serving the prerender management console UI, proxying to a prerender deployment's /prerender_admin API",
"license": "Apache-2.0",
Expand Down
32 changes: 31 additions & 1 deletion packages/console/src/admin/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ export function fmtRatio(v) {
return `${v.toFixed(2)}×`;
}

/**
* A ratio, or null when either side is missing — never a number that only looks like an answer.
*
* `null / 48h` is **0**, not NaN, so a missing measurement divided by a present yardstick formats
* as a confident "0.00×" — the most flattering possible reading of "we have no data". It is the
* same `Number(null) === 0` trap the plugin's own `numberOf()` exists for, arriving through
* division instead of coercion. Every ÷-cadence figure on the Traffic view goes through here so
* the guard cannot be forgotten at one call site out of four.
*/
export const ratioOf = (value, yardstick) =>
Number.isFinite(value) && Number.isFinite(yardstick) && yardstick > 0 ? value / yardstick : null;

export function fmtCount(v) {
if (v === null || v === undefined || !Number.isFinite(v)) return '—';
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
Expand Down Expand Up @@ -150,7 +162,25 @@ export function stackBy(combos, dim, bucketCount) {
const sum = (a, b) => a + b;

/**
* Count-weighted merge of a distribution stat across combos (approximate by construction).
* Count-weighted merge of a distribution stat across combos.
*
* WHICH STATISTIC TO ASK FOR, because the three this payload carries answer different questions
* and the console gets one of them wrong per panel if nobody says this out loud:
*
* `mean` — the only one that merges EXACTLY. A count-weighted mean of means is the true mean
* of the pooled population, across combos, buckets and nodes alike. It is also the
* statistic that governs throughput: renders/hour is concurrency ÷ MEAN render time,
* never ÷ p95. Use it for capacity and for anything that has to add up.
* `median` — the typical experience, and the one to lead with when the question is "what does a
* crawler normally get". Robust to a tail that a mean would swallow.
* `p95` — the tail, and ONLY the tail. It is the right alarm for a pathology that hides
* behind a healthy middle (a cohort of cache hits at 13.6s while the median stayed
* at 2.3ms), and the wrong headline for anything else — including any distribution
* with a natural ceiling, where a perfectly healthy population already sits near it.
*
* A merged median or p95 is a count-weighted average of per-row percentiles, which is NOT the
* percentile of the pooled population — close in practice, wrong in principle, and always written
* "≈". Only the mean escapes that.
*
* `scaleOf` divides each combo's value by its OWN yardstick before the merge, which is what makes
* a mixed population comparable: a 2h-cadence route and a 24h-cadence route both express their
Expand Down
8 changes: 5 additions & 3 deletions packages/console/src/admin/views/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ function traffic(ctx) {
const total = sumCount(serves);
const originServes = sumCount(serves.filter((s) => s.path === 'origin'));
const cacheServes = sumCount(serves.filter((s) => s.path === 'cache'));
const ageP95 = weighted(pick(data, 'page_age'), 'p95');
// The median, matching Traffic: an evenly refreshed corpus puts its p95 within a whisker of the
// interval by construction, so a p95 tile here would read as "behind" on a healthy fleet.
const ageMedian = weighted(pick(data, 'page_age'), 'median');
const interval = data.intervals?.defaultRenderInterval;

const { keys, stacks } = stackBy(serves, 'method', data.bucketCount);
Expand All @@ -222,8 +224,8 @@ function traffic(ctx) {
warn: total > 0 && originServes > total / 2,
}),
stat('Cache-served', pct(cacheServes, total)),
stat('Page age p95', fmtMs(ageP95), 'cache serves only ≈', {
warn: Number.isFinite(ageP95) && Number.isFinite(interval) && ageP95 > interval,
stat('Page age', fmtMs(ageMedian), 'median, cache serves only ≈', {
warn: Number.isFinite(ageMedian) && Number.isFinite(interval) && ageMedian > interval,
}),
]),
total > 0 ? stackedBars(data, keys, stacks, (k) => colorFor(CACHE_STATUS_COLORS, k)) : null,
Expand Down
39 changes: 35 additions & 4 deletions packages/console/src/admin/views/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ function supply(ctx) {
const outcomes = pick(data, 'render', (s) => s.path === 'outcome');
const times = pick(data, 'render', (s) => s.path === 'time_ms');
const claims = pick(data, 'queue_health', (s) => s.path === 'claim_scan_ms');
// `candidate` is a render whose result was actually stored — the work that produces a cached
// page, as opposed to a settle-skipping bail on a page we were never going to keep.
const candidateTimes = pick(data, 'render', (s) => s.path === 'time_ms' && s.type === 'candidate');

const total = sumCount(outcomes);
const failedLike = sumCount(outcomes.filter((s) => s.method === 'failed' || s.method === 'auth-failure'));
Expand All @@ -104,8 +107,34 @@ function supply(ctx) {
// One-in-ten failing is past tail noise for any healthy corpus.
warn: total > 0 && failedLike > total / 10,
}),
stat('Render p95', fmtMs(weighted(times, 'p95')), 'browser-reported duration ≈'),
stat('Claim scan p95', fmtMs(weighted(claims, 'p95')), 'the leading indicator — watch the trend ≈'),
// THE MEAN, because this tile is the capacity number and capacity is governed by the mean:
// renders/hour is concurrency ÷ MEAN render time (a queue's throughput follows the average
// service time, not its tail). Sizing off the p95 understates the fleet by whatever the tail
// is worth — measured here, 16.0s against a mean of 11.0s, a third of the fleet's capacity
// argued away.
//
// AND THE POOLED MEAN IS NOW TWO MODES. Since browser v1.18.0,
// `navigation.skipSettleWhenNonIndexable` returns a page that already disowns itself at
// DOMContentLoaded without settling — ~1.7s against ~10.9s — so the fleet runs cheap bails
// beside full renders. Pooled, that mean falls as the BAIL RATE rises, which is a real
// throughput gain and not a faster settle; read alone it looks like the renderer got quicker.
// The candidacy slot already separates them (a bail posts a non-indexable verdict), so the
// subtitle carries the mean of the renders that actually produced a stored page. Capacity
// still uses the pooled mean: a bail occupies a worker slot like anything else.
stat(
'Render time',
fmtMs(weighted(times, 'mean')),
`mean, all renders — capacity is concurrency ÷ this${
candidateTimes.length && candidateTimes.length !== times.length
? ` · stored ${fmtMs(weighted(candidateTimes, 'mean'))}`
: ''
} · p95 ${fmtMs(weighted(times, 'p95'))}`
),
stat(
'Claim scan p95',
fmtMs(weighted(claims, 'p95')),
`the leading indicator — watch the trend ≈ · median ${fmtMs(weighted(claims, 'median'))}`
),
]);

// Outcomes stacked over time: the shape of "renders are failing" as it develops.
Expand All @@ -132,8 +161,10 @@ function supply(ctx) {
? lineChart(data, timeSeries)
: emptyNote('render time / claim_scan_ms', data),
el('p', { cls: 'muted chart-note' }, [
'Render time is fleet capacity (renders/hour = concurrency ÷ time). The claim scan degrades ',
'BEFORE any backlog shows — measured 17× once — so its trend matters more than its level.',
'These are TAILS, not the capacity figure: renders/hour is concurrency ÷ the MEAN render time, ',
'which is the tile above — a p95 line answers "are some renders pathological", never "how many ',
'can the fleet do". The claim scan degrades BEFORE any backlog shows — measured 17× once — so ',
'its trend matters more than its level.',
]),
],
});
Expand Down
Loading