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