mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
test(perf): record which nodes move in a journey's layout shift
J2's renderer.layoutShiftScore went from 0.222 to 0.233 with #1814, and its evidence only held the recent-input / without-recent-input split, so the moved element could not be named from a summary. The probe now keeps the first 20 counted shifts (value, recent input, time since the journey start and the moved nodes, as J1's late shifts do), and J2 writes them to evidence.layoutShift.shifts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a006cb027
commit
655f54b362
6 files changed
+89
No files matched your search
@@ -798,6 +798,13 @@ test('drops performance entries from before the click and keeps recent-input shi
|
||||
{
|
||||
entryType: 'layout-shift',
|
||||
hadRecentInput: true,
|
||||
sources: [
|
||||
{
|
||||
currentRect: { height: 40, y: 152 },
|
||||
node: fixture.card,
|
||||
previousRect: { height: 40, y: 100 },
|
||||
},
|
||||
],
|
||||
startTime: now(),
|
||||
value: 0.25,
|
||||
},
|
||||
@@ -823,6 +830,20 @@ test('drops performance entries from before the click and keeps recent-input shi
|
||||
assert.equal(state.final, true);
|
||||
assert.equal(state.counters.layoutShiftScore, 0.125);
|
||||
assert.equal(state.counters.recentInputLayoutShiftScore, 0.25);
|
||||
// Each counted shift keeps its nodes; the pre-click ones are not listed.
|
||||
assert.deepEqual(
|
||||
state.shifts.map((shift) => [
|
||||
shift.hadRecentInput,
|
||||
shift.value,
|
||||
shift.sources.map((source) => source.deltaY),
|
||||
]),
|
||||
[
|
||||
[true, 0.25, [52]],
|
||||
[false, 0.125, []],
|
||||
]
|
||||
);
|
||||
assert.ok(state.shifts.every((shift) => shift.sinceStartMs >= 0));
|
||||
assert.match(state.shifts[0]?.sources[0]?.node ?? '', /^[a-z-]+/);
|
||||
// J2 has no settle window: the observers close at the cutoff.
|
||||
assert.equal(state.settle.status, 'disabled');
|
||||
assert.equal(state.counters.layoutShiftScoreSettled, 0);
|
||||
@@ -869,6 +890,7 @@ test('rejects a click start whose sentinel could not be sent', async () => {
|
||||
const started = {
|
||||
...state,
|
||||
sentinel: { epochMs: 1, status: 'sent' as const },
|
||||
shifts: [],
|
||||
};
|
||||
assert.throws(
|
||||
() => assertJourneyRendererProbeState(started),
|
||||
|
||||
@@ -166,6 +166,16 @@ export interface JourneyRendererProbeCounters {
|
||||
recentInputLayoutShiftScore: number;
|
||||
}
|
||||
|
||||
/** A shift counted in `layoutShiftScore` or `recentInputLayoutShiftScore`. */
|
||||
export interface JourneyRendererProbeShift {
|
||||
readonly hadRecentInput: boolean;
|
||||
/** Entry start minus the journey start (navigation start for J1). */
|
||||
readonly sinceStartMs: number;
|
||||
/** `tag.class[data-test-id]` and the vertical move of each source. */
|
||||
readonly sources: JourneyRendererProbeLateShift['sources'];
|
||||
readonly value: number;
|
||||
}
|
||||
|
||||
export interface JourneyRendererProbeLateShift {
|
||||
/** Entry start minus the first-card terminal epoch. */
|
||||
readonly afterFirstCardMs: number;
|
||||
@@ -237,6 +247,11 @@ export interface JourneyRendererProbeState {
|
||||
readonly epochMs: number | null;
|
||||
readonly status: 'bridge-missing' | 'failed' | 'not-sent' | 'sent';
|
||||
};
|
||||
/**
|
||||
* The first 20 shifts counted until the cutoff, with the nodes that
|
||||
* moved, so a layout-shift score can be traced to its components.
|
||||
*/
|
||||
shifts: JourneyRendererProbeShift[];
|
||||
/** `final` freezes the first-card counters; the settle window ends later. */
|
||||
settle: {
|
||||
/** Mutation records under the settle root after the cutoff. */
|
||||
@@ -329,6 +344,7 @@ export function journeyRendererProbeScript(
|
||||
preStart: { domMutations: 0, lastMutationEpochMs: null },
|
||||
schemaVersion: 1,
|
||||
sentinel: { epochMs: null, status: 'not-sent' },
|
||||
shifts: [],
|
||||
settle: {
|
||||
domMutations: 0,
|
||||
epochMs: null,
|
||||
@@ -389,6 +405,7 @@ export function journeyRendererProbeScript(
|
||||
for (const entry of entries) {
|
||||
const shift = entry as PerformanceEntry & {
|
||||
hadRecentInput?: boolean;
|
||||
sources?: readonly LateShiftSource[];
|
||||
value?: number;
|
||||
};
|
||||
if (
|
||||
@@ -397,6 +414,17 @@ export function journeyRendererProbeScript(
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (state.shifts.length < 20) {
|
||||
state.shifts.push({
|
||||
hadRecentInput: shift.hadRecentInput === true,
|
||||
sinceStartMs:
|
||||
performance.timeOrigin +
|
||||
entry.startTime -
|
||||
(state.start?.epochMs ?? performance.timeOrigin),
|
||||
sources: (shift.sources ?? []).map(describeSource),
|
||||
value: shift.value,
|
||||
});
|
||||
}
|
||||
if (shift.hadRecentInput === true) {
|
||||
state.counters.recentInputLayoutShiftScore += shift.value;
|
||||
continue;
|
||||
|
||||
@@ -54,6 +54,7 @@ function measurement(
|
||||
preStart: { domMutations: 0, lastMutationEpochMs: null },
|
||||
schemaVersion: 1,
|
||||
sentinel: { epochMs: 2_601, status: 'sent' },
|
||||
shifts: [],
|
||||
settle: {
|
||||
domMutations: 37,
|
||||
epochMs: 3_180.06,
|
||||
|
||||
@@ -59,6 +59,20 @@ function measurement(
|
||||
preStart: { domMutations: 4, lastMutationEpochMs: 9_100 },
|
||||
schemaVersion: 1,
|
||||
sentinel: { epochMs: 10_080.5, status: 'sent' },
|
||||
shifts: [
|
||||
{
|
||||
hadRecentInput: true,
|
||||
sinceStartMs: 41.26,
|
||||
sources: [
|
||||
{
|
||||
deltaHeight: 0,
|
||||
deltaY: 52,
|
||||
node: 'div.content[data-test-id="category-list"]',
|
||||
},
|
||||
],
|
||||
value: 0.22106,
|
||||
},
|
||||
],
|
||||
settle: {
|
||||
domMutations: 0,
|
||||
epochMs: null,
|
||||
@@ -147,6 +161,20 @@ test('maps the click-started probe, IPC window and mock ledger to exact counters
|
||||
});
|
||||
assert.deepEqual(record.evidence['layoutShift'], {
|
||||
recentInput: 0.221,
|
||||
shifts: [
|
||||
{
|
||||
hadRecentInput: true,
|
||||
sinceStartMs: 41.3,
|
||||
sources: [
|
||||
{
|
||||
deltaHeight: 0,
|
||||
deltaY: 52,
|
||||
node: 'div.content[data-test-id="category-list"]',
|
||||
},
|
||||
],
|
||||
value: 0.2211,
|
||||
},
|
||||
],
|
||||
withoutRecentInput: 0,
|
||||
});
|
||||
assert.deepEqual(record.evidence['httpRequestsByRoute'], {
|
||||
|
||||
@@ -184,6 +184,15 @@ export function toOpenSourceIterationRecord(
|
||||
recentInput: roundThousandth(
|
||||
renderer.counters.recentInputLayoutShiftScore
|
||||
),
|
||||
// The first 20 counted shifts and the nodes that moved.
|
||||
shifts: renderer.shifts.map((shift) =>
|
||||
Object.freeze({
|
||||
hadRecentInput: shift.hadRecentInput,
|
||||
sinceStartMs: roundTenth(shift.sinceStartMs),
|
||||
sources: shift.sources,
|
||||
value: Math.round(shift.value * 10_000) / 10_000,
|
||||
})
|
||||
),
|
||||
withoutRecentInput: roundThousandth(
|
||||
renderer.counters.layoutShiftScore
|
||||
),
|
||||
|
||||
@@ -74,6 +74,7 @@ function measurement(
|
||||
preStart: { domMutations: 53, lastMutationEpochMs: 8_900 },
|
||||
schemaVersion: 1,
|
||||
sentinel: { epochMs: 10_350, status: 'sent' },
|
||||
shifts: [],
|
||||
settle: {
|
||||
domMutations: 0,
|
||||
epochMs: null,
|
||||
|
||||
Reference in new issue
Block a user