diff --git a/public/js/compare/filmstrip.js b/public/js/compare/filmstrip.js index 2a3a886..56daeaa 100644 --- a/public/js/compare/filmstrip.js +++ b/public/js/compare/filmstrip.js @@ -10,9 +10,18 @@ // `/data/filmstrip//ms_.jpg`. We detect this // by looking at `_meta.screenshot` (always present when sitespeed // was run with --video / --visualMetrics) and derive the base from -// it. Frame timestamps come from `_visualMetrics`: a frame at 0, -// every 100 ms between FirstVisualChange and LastVisualChange, and -// a final frame at LastVisualChange exact. +// it. Sitespeed.io's filmstrip plugin writes files on a fixed +// 100 ms cadence anchored to FirstVisualChange and LastVisualChange +// — NOT one per VisualProgress change point. Specifically it +// writes: +// - ms 0 (the pre-load frame) +// - FirstVisualChange (only when not on a 100 ms boundary) +// - every 100 ms multiple strictly between FVC and LVC +// - LastVisualChange (only when not on a 100 ms boundary) +// VisualProgress is sampled at video-frame cadence (~30 fps), +// so most change points fall between filmstrip frames and have +// no file on disk. Deriving the strip from VP change points +// therefore produced a steady stream of 404s. // // 2. Older WPT HARs that embed a `filmstrip` array on the page — // handled the legacy way via pageXray.meta.filmstrip when that's @@ -31,50 +40,57 @@ function getFilmstripForPage(har, pageIndex) { // sitespeed.io path — derive the filmstrip URL base from the // screenshot URL (e.g. .../data/screenshots/1/afterPageCompleteCheck.jpg - // → .../data/filmstrip/1/ms_NNNNNN.jpg) and read the actual frame - // timestamps from _visualMetrics.VisualProgress. Sitespeed.io emits - // exactly one filmstrip JPG per visual-progress *change point*, named - // after that ms — so the set of distinct-percentage timestamps in - // VisualProgress is the authoritative list of frames on disk. + // → .../data/filmstrip/1/ms_NNNNNN.jpg) and construct the frame + // timestamps to match what the filmstrip plugin actually wrote. const meta = page._meta || {}; const vm = page._visualMetrics; - if (meta.screenshot && vm && vm.VisualProgress) { + if (meta.screenshot && vm && + typeof vm.FirstVisualChange === 'number' && + typeof vm.LastVisualChange === 'number') { const m = meta.screenshot.match(/^(.+\/data)\/screenshots\/(\d+)\//); if (m) { const dataBase = m[1]; const runId = m[2]; - const vp = vm.VisualProgress; + const fvc = vm.FirstVisualChange; + const lvc = vm.LastVisualChange; + const vp = vm.VisualProgress || {}; - const sorted = Object.keys(vp) - .map(function (k) { return Number(k); }) - .sort(function (a, b) { return a - b; }); + const times = [0]; + if (fvc > 0 && fvc % 100 !== 0) times.push(fvc); + // First 100 ms boundary strictly after FVC, then step to the + // last boundary strictly before LVC. The `< lvc` guard is what + // matches sitespeed.io's behaviour when LVC sits exactly on a + // boundary (the file is the LVC one, not an extra boundary + // frame). + const firstBoundary = Math.floor(fvc / 100) * 100 + 100; + for (let t = firstBoundary; t < lvc; t += 100) { + times.push(t); + } + if (lvc > 0 && (lvc % 100 !== 0 || lvc > (times[times.length - 1] || 0))) { + if (lvc !== times[times.length - 1]) times.push(lvc); + } - const times = []; - let prevPct = null; - for (let i = 0; i < sorted.length; i++) { - const ms = sorted[i]; - if (vp[ms] !== prevPct) { - times.push(ms); - prevPct = vp[ms]; + // VisualProgress is sampled finer than the filmstrip; for each + // filmstrip timestamp pick the most recent VP entry at or + // before it as the "rendered %" tag. Drives the divergence + // colouring in the column view. + const vpKeys = Object.keys(vp).map(Number).sort(function (a, b) { return a - b; }); + function progressAt(ms) { + let best = null; + for (let i = 0; i < vpKeys.length; i++) { + if (vpKeys[i] <= ms) best = vp[vpKeys[i]]; + else break; } + return best; } - // VisualProgress should always start with a 0ms entry; if it - // didn't for some reason, anchor the strip so the first frame - // is the pre-load state. - if (!times.length || times[0] !== 0) times.unshift(0); - return times.map(function (ms) { return { ms: ms, time: (ms / 1000).toFixed(2), img: dataBase + '/filmstrip/' + runId + '/ms_' + String(ms).padStart(6, '0') + '.jpg', - // Visual-progress percent at this change point. Carries - // through padFrames so each padded cell knows how rendered - // the page was at that moment — used to flag divergence - // between the two HARs. - progress: vp[ms] + progress: progressAt(ms) }; }); }