From 845378c0204962967df09ad8ab904610a79efd24 Mon Sep 17 00:00:00 2001 From: Perry Dale Date: Sun, 23 Aug 2026 02:35:14 +0000 Subject: [PATCH] Walk & Test: add 0.25s and 0.5s round intervals Both were measured before being offered rather than assumed to work. The browser sustains it. At a 250 ms gap the loop settles at 0.306 s per round, and the number does not move as the walk deepens: 0.306 s over the first fifteen seconds, 0.309 s two minutes and 3,890 samples later. Sort stayed responsive at 159 ms and opening a new spot at 280 ms with that many samples behind them. The concern going in was that the per-round aggregation rescans every sample, so cost would grow with walk length; at these depths it does not, and the measurement says so rather than an optimisation nobody needed. It holds because the loop awaits a whole round before starting the interval timer, so the setting is a gap between rounds rather than a fixed cadence. Requests cannot pile up, and a slow link stretches the gap instead. What it is not is polite. Sustained, a quarter-second gap is up to about 2,000 requests to each of ten third parties over ten minutes, so selecting either sub-second option raises a notice saying so. It also names the failure mode that matters for reading the result: a destination whose bot protection starts refusing appears as a dead card when it is really a rate limit. SUB_SECOND_INTERVAL_MS carries the threshold and the reasoning, and a test pins which choices it flags. The disclosure list and the README privacy table carry the revised request volume, since the previous figures predated anything faster than one second. --- README.md | 13 ++++++++++--- src/components/PrivacySafetyModal.tsx | 5 +++-- src/components/WalkTest.tsx | 19 +++++++++++++++++++ src/utils/walkTest.test.ts | 18 +++++++++++++++--- src/utils/walkTest.ts | 22 ++++++++++++++++++++-- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1f20d02..f84a872 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,16 @@ What the numbers are, stated permanently on screen rather than in a footnote: The opening two rounds and each destination's first answer pay for DNS, TCP and TLS, so they are counted but kept out of every timing statistic and every chart. Left in, they pinned the top of the y-axis near a second for the rest of the session and flattened every real measurement into a line -along the bottom. Rounds fire every 1, 2, 3, 5 or 10 seconds; the interval is the pause *between* -rounds, and a round waits for all ten destinations to answer or time out first, so a struggling link -stretches the gap rather than piling requests up. Spots are compared by the **median of each destination's own median**, over only +along the bottom. Rounds fire every 0.25, 0.5, 1, 2, 3, 5 or 10 seconds; the interval is the +pause *between* rounds, and a round waits for all ten destinations to answer or time out first, so a +struggling link stretches the gap rather than piling requests up. That back-pressure is what makes +the sub-second settings safe for the browser: measured at **0.306 s per round** at a 250 ms gap, flat +from the first round to the four-thousandth sample. + +Safe for the browser is not the same as polite to ten other people's servers, so choosing 0.25 s or +0.5 s raises a notice saying what it costs — up to roughly 2,000 requests to each destination over +ten minutes, and a destination whose bot protection starts refusing will show up as a dead card when +it is really a rate limit. It is for a short sweep of a problem area, not a whole building. Spots are compared by the **median of each destination's own median**, over only the destinations that produced a median at *every* spot — pooling raw samples instead would make the figure lurch when a destination dropped out, reporting a change in which destinations answered as though it were a change in latency. diff --git a/src/components/PrivacySafetyModal.tsx b/src/components/PrivacySafetyModal.tsx index 2b5d0aa..e5c7530 100644 --- a/src/components/PrivacySafetyModal.tsx +++ b/src/components/PrivacySafetyModal.tsx @@ -69,8 +69,9 @@ export const THIRD_PARTY_DISCLOSURES: { host: string; receives: string }[] = [ 'www.atlassian.com', receives: 'Your IP, repeatedly, for as long as a Walk & Test run lasts — every one of them is probed ' + - 'once per round, so a ten-minute walk is a few hundred requests to each, up to roughly six ' + - 'hundred at the one-second interval. Each request is a HEAD for one small public file and ' + + 'once per round, so a ten-minute walk is a few hundred requests to each, and closer to two ' + + 'thousand at the quarter-second interval. Each request is a HEAD for one small public file ' + + 'and ' + 'carries no cookies (credentials: omit), so these hosts see an address and a TLS handshake ' + 'rather than a logged-in user. Several of them are advertising businesses; the IP and the ' + 'timing pattern are still theirs to log.', diff --git a/src/components/WalkTest.tsx b/src/components/WalkTest.tsx index b44d213..d4a1293 100644 --- a/src/components/WalkTest.tsx +++ b/src/components/WalkTest.tsx @@ -37,6 +37,7 @@ import { INTERVAL_CHOICES, MAX_STORED_SAMPLES, PROBE_TIMEOUT_MS, + SUB_SECOND_INTERVAL_MS, WALK_TARGETS, WARMUP_ROUNDS, buildWalkConclusions, @@ -619,6 +620,24 @@ export const WalkTest: React.FC = ({ onHistoryUpdate }) => { )} + {/* Not a warning about the browser, which copes. A warning about what + this sends to ten other people's servers. */} + {intervalMs <= SUB_SECOND_INTERVAL_MS && ( +
+ + + At {intervalMs / 1000}s this stops sampling these ten services and starts putting + sustained traffic on them: up to about{' '} + {(Math.round(600 / ((intervalMs + 50) / 1000) / 100) * 100).toLocaleString()} requests + to each over ten minutes, fewer on a slow link because each round waits for the + previous one. The browser copes, and the loop waits for each round to finish so it + cannot pile requests up. Their bot protection is the thing to watch — a destination + that starts refusing will show here as a dead card when it is really a rate limit. + Use it for a short sweep of a problem area, not for a whole building. + +
+ )} + {/* The one control that has to be reachable one-handed, mid-walk. */} {isWalking && (
diff --git a/src/utils/walkTest.test.ts b/src/utils/walkTest.test.ts index 7c14970..32b7cb9 100644 --- a/src/utils/walkTest.test.ts +++ b/src/utils/walkTest.test.ts @@ -4,6 +4,7 @@ import { DEFAULT_INTERVAL_MS, INTERVAL_CHOICES, MAX_STORED_SAMPLES, + SUB_SECOND_INTERVAL_MS, WALK_TARGETS, WARMUP_ROUNDS, buildWalkConclusions, @@ -485,12 +486,23 @@ describe('WALK_TARGETS', () => { }); describe('INTERVAL_CHOICES', () => { - it('offers a one-second tick', () => { - // Safe because the loop awaits a whole round before starting the timer, so - // the interval is a gap between rounds rather than a fixed cadence. + it('offers sub-second ticks', () => { + // Safe for the browser because the loop awaits a whole round before starting + // the timer, so the interval is a gap between rounds rather than a fixed + // cadence and requests cannot pile up. Measured at 0.306s per round at a + // 250ms gap, flat from the first round to the four-thousandth. + expect(INTERVAL_CHOICES).toContain(250); + expect(INTERVAL_CHOICES).toContain(500); expect(INTERVAL_CHOICES).toContain(1000); }); + it('flags exactly the choices that generate sustained third-party traffic', () => { + // Safe for the browser is not the same as polite to ten other people's + // servers, and the UI warns on the second question, not the first. + const flagged = INTERVAL_CHOICES.filter((ms) => ms <= SUB_SECOND_INTERVAL_MS); + expect(flagged).toEqual([250, 500]); + }); + it('is ordered fastest first and holds the default', () => { expect([...INTERVAL_CHOICES]).toEqual([...INTERVAL_CHOICES].sort((a, b) => a - b)); expect(INTERVAL_CHOICES).toContain(DEFAULT_INTERVAL_MS); diff --git a/src/utils/walkTest.ts b/src/utils/walkTest.ts index 336bd94..736d542 100644 --- a/src/utils/walkTest.ts +++ b/src/utils/walkTest.ts @@ -65,9 +65,14 @@ export const DEFAULT_INTERVAL_MS = 3000; * in a round to answer or time out before it starts the timer. A round in which * everything times out therefore takes `PROBE_TIMEOUT_MS`, and the effective * tick stretches to match rather than requests piling up on a struggling link. - * That is what makes the one-second option safe. + * That back-pressure is what makes the sub-second options safe for the browser. + * + * It does not make them polite. At a quarter-second gap a ten-minute walk is + * well over a thousand requests to each of ten third parties, which is a + * different kind of cost from the one the browser pays — see + * `SUB_SECOND_INTERVAL_MS` and the notice the UI shows when one is selected. */ -export const INTERVAL_CHOICES = [1000, 2000, 3000, 5000, 10000] as const; +export const INTERVAL_CHOICES = [250, 500, 1000, 2000, 3000, 5000, 10000] as const; /** * Rounds discarded from every timing statistic and every chart at the start of @@ -82,6 +87,19 @@ export const INTERVAL_CHOICES = [1000, 2000, 3000, 5000, 10000] as const; */ export const WARMUP_ROUNDS = 2; +/** + * At or below this gap, the walk is generating sustained traffic to ten third + * parties rather than sampling them, and the UI says so. + * + * A quarter-second gap resolves to roughly three rounds a second once each + * round's own duration is added, so ten minutes is on the order of 1,800 + * requests per destination. Nothing breaks, and several of these operators run + * bot protection that may reasonably start refusing — which would then show up + * on the cards as a dead destination that is really a rate limit. Worth + * knowing before reading the result. + */ +export const SUB_SECOND_INTERVAL_MS = 500; + /** * Rounds visible in the live charts. A single bad spike would otherwise hold the * y-axis at its height for the rest of the walk. The tables and the saved record