Public: anyone can read this; the crew can change it.
Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:39at56z5zh is past its budget: trevato/forge: "the Files page of a 40-file diff: fastest cold view of 3 and fastest repeat view of 5 stay under their … #7897
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7886-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 2 commits fork
1 file
changed +21
−62
test/pull-timing.test.ts +21 −62
| @@ -128,13 +128,13 @@ test("a pull's Files page names session, diff, checks, reviews and render, in th | ||
| 128 | 128 | expect(names.slice(names.indexOf("app") + 1)).toEqual(["session", "diff", "diff-git", "diff-parse", "diff-highlight", "git", "checks", "reviews", "render"]); |
| 129 | 129 | }); |
| 130 | 130 | |
| 131 | /** The `app` duration of one Files-page view, in ms. */ | |
| 132 | async function filesApp(): Promise<number> { | |
| 131 | /** The Server-Timing phase names of one Files-page view, and whether its diff came from the cache. */ | |
| 132 | async function filesView(): Promise<{ names: string[]; hit: boolean }> { | |
| 133 | 133 | const res = await fetch(`${servers.url}/alice/r/pulls/${number}/files`, { headers: { cookie } }); |
| 134 | 134 | expect(res.status).toBe(200); |
| 135 | 135 | await res.arrayBuffer(); |
| 136 | const app = (res.headers.get("server-timing") ?? "").match(/(?:^|, )app;dur=([\d.]+)/); | |
| 137 | return Number(app![1]); | |
| 136 | const timing = res.headers.get("server-timing") ?? ""; | |
| 137 | return { names: timing.split(", ").map((p) => p.split(";")[0]!), hit: /(?:^|, )diff;dur=[\d.]+;desc="hit"/.test(timing) }; | |
| 138 | 138 | } |
| 139 | 139 | |
| 140 | 140 | /** 40 files of 50 lines each, new content per `i` so each is a fresh head. */ |
| @@ -150,67 +150,26 @@ async function commitHead(i: number): Promise<void> { | ||
| 150 | 150 | if (!done.ok) throw new Error(JSON.stringify(done.error)); |
| 151 | 151 | } |
| 152 | 152 | |
| 153 | // Measured over 3 runs of this test (40 files x 50 lines): the cold median of 3 was 125-135 ms | |
| 154 | // (a single cold view up to 244 ms); the repeat median of 5 was 55 ms (single views 49-76 ms). | |
| 155 | // 250 ms is ~2x the cold median; 100 ms is the issue's bar for a repeat view. | |
| 156 | const COLD_MS = 250; | |
| 157 | ||
| 158 | /** | |
| 159 | * A fixed CPU workload, in ms. The `x < 0` is never true; it only keeps the optimiser from | |
| 160 | * deleting the loop as dead code. | |
| 161 | */ | |
| 162 | const spin = (): number => { | |
| 163 | const t = performance.now(); | |
| 164 | let x = 0; | |
| 165 | for (let n = 0; n < 3e6; n++) x = (x + Math.sqrt(n)) % 1e9; | |
| 166 | return x < 0 ? 0 : performance.now() - t; | |
| 167 | }; | |
| 168 | const fastest = (n: number) => Math.min(...Array.from({ length: n }, spin)); | |
| 169 | /** This machine's speed, taken once at module load: the idle reference (scale 1.0 beside itself). */ | |
| 170 | const idle = (spin(), fastest(5)); | |
| 171 | ||
| 172 | /** | |
| 173 | * How much slower the machine was around one view than at load, from the spin times taken just | |
| 174 | * before and just after it: the larger of the two, so contention that starts or stops mid-view | |
| 175 | * still counts (#5000). Never below 1.0, so an idle machine meets the ceilings as written; capped | |
| 176 | * at 8x so a stalled machine cannot excuse anything. | |
| 177 | */ | |
| 178 | const loadScale = (before: number, after: number, idleMs: number): number => | |
| 179 | Math.max(1, Math.min(Math.max(before, after) / idleMs, 8)); | |
| 180 | ||
| 181 | /** One view's ms divided by the load around it: what it would have taken on the idle machine. */ | |
| 182 | async function idleEquivalent(view: () => Promise<number>): Promise<number> { | |
| 183 | const before = fastest(2); | |
| 184 | const ms = await view(); | |
| 185 | return ms / loadScale(before, fastest(2), idle); | |
| 186 | } | |
| 187 | ||
| 188 | test("loadScale: idle is 1.0, the slower side of a view wins, and 8x is the cap", () => { | |
| 189 | expect(loadScale(10, 10, 10)).toBe(1); | |
| 190 | // Faster than at load is still 1.0: an idle machine's 105 ms repeat view fails the 100 ms bar. | |
| 191 | expect(loadScale(8, 9, 10)).toBe(1); | |
| 192 | expect(105 / loadScale(8, 9, 10)).toBeGreaterThan(100); | |
| 193 | // Load that arrives after the view started is seen by the after-sample (#5000's 105 ms run). | |
| 194 | expect(loadScale(10, 15, 10)).toBe(1.5); | |
| 195 | expect(loadScale(15, 10, 10)).toBe(1.5); | |
| 196 | expect(105 / loadScale(10, 15, 10)).toBeLessThan(100); | |
| 197 | expect(loadScale(10, 1000, 10)).toBe(8); | |
| 198 | }); | |
| 199 | ||
| 200 | test("the Files page of a 40-file diff: fastest cold view of 3 and fastest repeat view of 5 stay under their ceilings", async () => { | |
| 201 | // Each view is scaled by the load measured beside it, not once up front: contention that begins | |
| 202 | // partway through the eight views would otherwise leave the ceilings unstretched (#5000). | |
| 203 | const cold: number[] = []; | |
| 153 | test("the Files page of a 40-file diff: a new head does the diff work, a repeat view reuses it", async () => { | |
| 154 | // The verdict is the work done, not the milliseconds it took: wall time depends on the check | |
| 155 | // machine's load (#7886), while a lost cache or a re-parse on every view shows in the phases. | |
| 204 | 156 | for (let i = 0; i < 3; i++) { |
| 205 | 157 | // A new head each time, so nothing rendered before is reused. |
| 206 | 158 | await commitHead(i); |
| 207 | cold.push(await idleEquivalent(filesApp)); | |
| 159 | const view = await filesView(); | |
| 160 | expect(view.hit).toBe(false); | |
| 161 | expect(view.names).toContain("diff-git"); | |
| 162 | expect(view.names).toContain("diff-parse"); | |
| 163 | expect(view.names).toContain("diff-highlight"); | |
| 208 | 164 | } |
| 209 | 165 | // The head is unchanged now: every view reuses the rendered diff. |
| 210 | const repeat: number[] = []; | |
| 211 | for (let i = 0; i < 5; i++) repeat.push(await idleEquivalent(filesApp)); | |
| 212 | // The fastest view, not the median: contention only ever adds time, so the minimum is the | |
| 213 | // measurement least about the machine's load, and still fails if the work itself got slower. | |
| 214 | expect(Math.min(...cold)).toBeLessThan(COLD_MS); | |
| 215 | expect(Math.min(...repeat)).toBeLessThan(100); | |
| 166 | for (let i = 0; i < 5; i++) { | |
| 167 | const view = await filesView(); | |
| 168 | expect(view.hit).toBe(true); | |
| 169 | // A hit still names every other phase, so a dropped phase name is caught. | |
| 170 | for (const n of ["session", "diff", "git", "checks", "reviews", "render"]) expect(view.names).toContain(n); | |
| 171 | expect(view.names).not.toContain("diff-git"); | |
| 172 | expect(view.names).not.toContain("diff-parse"); | |
| 173 | expect(view.names).not.toContain("diff-highlight"); | |
| 174 | } | |
| 216 | 175 | }); |
new above
offline