Skip to content

Commit af8bd2b

Browse files
authored
Fix filmstrip 404s by matching sitespeed.io's actual filmstrip cadence (#166)
The previous filmstrip fix assumed sitespeed.io writes one JPG per VisualProgress change point and built URLs from those timestamps. That assumption is wrong: VisualProgress is sampled at video-frame cadence (every ~33 ms) while the filmstrip plugin writes files on a fixed 100 ms grid. Most change points fall between filmstrip frames and therefore 404 — including FirstVisualChange itself when it lands on a 100 ms boundary like 700 ms. Construct the timestamps directly from FVC/LVC using sitespeed.io's actual rule — ms 0, plus FVC when not on a 100 ms boundary, plus every 100 ms multiple strictly inside (FVC, LVC), plus LVC when not on a 100 ms boundary. Verified against the Sweden run on wikimedia.sitespeed.io: every constructed URL hits a real file. VisualProgress is still consulted to tag each frame with its rendered-% for the divergence-colour accent on the column view. Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com
1 parent 149ed0c commit af8bd2b

1 file changed

Lines changed: 46 additions & 30 deletions

File tree

public/js/compare/filmstrip.js

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,18 @@
1010
// `<base>/data/filmstrip/<run>/ms_<paddedMs>.jpg`. We detect this
1111
// by looking at `_meta.screenshot` (always present when sitespeed
1212
// was run with --video / --visualMetrics) and derive the base from
13-
// it. Frame timestamps come from `_visualMetrics`: a frame at 0,
14-
// every 100 ms between FirstVisualChange and LastVisualChange, and
15-
// a final frame at LastVisualChange exact.
13+
// it. Sitespeed.io's filmstrip plugin writes files on a fixed
14+
// 100 ms cadence anchored to FirstVisualChange and LastVisualChange
15+
// — NOT one per VisualProgress change point. Specifically it
16+
// writes:
17+
// - ms 0 (the pre-load frame)
18+
// - FirstVisualChange (only when not on a 100 ms boundary)
19+
// - every 100 ms multiple strictly between FVC and LVC
20+
// - LastVisualChange (only when not on a 100 ms boundary)
21+
// VisualProgress is sampled at video-frame cadence (~30 fps),
22+
// so most change points fall between filmstrip frames and have
23+
// no file on disk. Deriving the strip from VP change points
24+
// therefore produced a steady stream of 404s.
1625
//
1726
// 2. Older WPT HARs that embed a `filmstrip` array on the page —
1827
// handled the legacy way via pageXray.meta.filmstrip when that's
@@ -31,50 +40,57 @@ function getFilmstripForPage(har, pageIndex) {
3140

3241
// sitespeed.io path — derive the filmstrip URL base from the
3342
// screenshot URL (e.g. .../data/screenshots/1/afterPageCompleteCheck.jpg
34-
// → .../data/filmstrip/1/ms_NNNNNN.jpg) and read the actual frame
35-
// timestamps from _visualMetrics.VisualProgress. Sitespeed.io emits
36-
// exactly one filmstrip JPG per visual-progress *change point*, named
37-
// after that ms — so the set of distinct-percentage timestamps in
38-
// VisualProgress is the authoritative list of frames on disk.
43+
// → .../data/filmstrip/1/ms_NNNNNN.jpg) and construct the frame
44+
// timestamps to match what the filmstrip plugin actually wrote.
3945
const meta = page._meta || {};
4046
const vm = page._visualMetrics;
41-
if (meta.screenshot && vm && vm.VisualProgress) {
47+
if (meta.screenshot && vm &&
48+
typeof vm.FirstVisualChange === 'number' &&
49+
typeof vm.LastVisualChange === 'number') {
4250
const m = meta.screenshot.match(/^(.+\/data)\/screenshots\/(\d+)\//);
4351
if (m) {
4452
const dataBase = m[1];
4553
const runId = m[2];
46-
const vp = vm.VisualProgress;
54+
const fvc = vm.FirstVisualChange;
55+
const lvc = vm.LastVisualChange;
56+
const vp = vm.VisualProgress || {};
4757

48-
const sorted = Object.keys(vp)
49-
.map(function (k) { return Number(k); })
50-
.sort(function (a, b) { return a - b; });
58+
const times = [0];
59+
if (fvc > 0 && fvc % 100 !== 0) times.push(fvc);
60+
// First 100 ms boundary strictly after FVC, then step to the
61+
// last boundary strictly before LVC. The `< lvc` guard is what
62+
// matches sitespeed.io's behaviour when LVC sits exactly on a
63+
// boundary (the file is the LVC one, not an extra boundary
64+
// frame).
65+
const firstBoundary = Math.floor(fvc / 100) * 100 + 100;
66+
for (let t = firstBoundary; t < lvc; t += 100) {
67+
times.push(t);
68+
}
69+
if (lvc > 0 && (lvc % 100 !== 0 || lvc > (times[times.length - 1] || 0))) {
70+
if (lvc !== times[times.length - 1]) times.push(lvc);
71+
}
5172

52-
const times = [];
53-
let prevPct = null;
54-
for (let i = 0; i < sorted.length; i++) {
55-
const ms = sorted[i];
56-
if (vp[ms] !== prevPct) {
57-
times.push(ms);
58-
prevPct = vp[ms];
73+
// VisualProgress is sampled finer than the filmstrip; for each
74+
// filmstrip timestamp pick the most recent VP entry at or
75+
// before it as the "rendered %" tag. Drives the divergence
76+
// colouring in the column view.
77+
const vpKeys = Object.keys(vp).map(Number).sort(function (a, b) { return a - b; });
78+
function progressAt(ms) {
79+
let best = null;
80+
for (let i = 0; i < vpKeys.length; i++) {
81+
if (vpKeys[i] <= ms) best = vp[vpKeys[i]];
82+
else break;
5983
}
84+
return best;
6085
}
6186

62-
// VisualProgress should always start with a 0ms entry; if it
63-
// didn't for some reason, anchor the strip so the first frame
64-
// is the pre-load state.
65-
if (!times.length || times[0] !== 0) times.unshift(0);
66-
6787
return times.map(function (ms) {
6888
return {
6989
ms: ms,
7090
time: (ms / 1000).toFixed(2),
7191
img: dataBase + '/filmstrip/' + runId + '/ms_' +
7292
String(ms).padStart(6, '0') + '.jpg',
73-
// Visual-progress percent at this change point. Carries
74-
// through padFrames so each padded cell knows how rendered
75-
// the page was at that moment — used to flag divergence
76-
// between the two HARs.
77-
progress: vp[ms]
93+
progress: progressAt(ms)
7894
};
7995
});
8096
}

0 commit comments

Comments
 (0)