Summary
docs/PORTAL_INTEGRATION_FINDINGS.md #24 records that a full-fiscal-year run can stall when the
GST Portal tab or its window is backgrounded. That entry states a symptom and a policy
response; it never recorded a cause. The product now instructs operators to keep the portal
foregrounded, and getRequiredGstTab() force-focuses the tab and its window, on the strength of an
undiagnosed stall.
See the correction comment below before acting on this issue. The measurement below is
sound; the causal claim it was filed with was not. Pack's primary path already waits
worker-side, and the in-page loop measured here is a one-shot fallback whose connection to the
recorded stall is unestablished.
What was measured
A standalone page — no extension, no portal, no session — running Pack's own loop shape in a tab
hidden from load and never once visible. Warm-up sleeps of 5,000ms each:
ticks 1-50: ~6000ms each (5000 requested + ~1s clamp -- mild background throttling)
tick 51: 60999ms
tick 52: 61001ms (intensive throttling)
Fifty ticks at ~6s is ~300s, so the clamp went from 1s to 60s at almost exactly five minutes
hidden — the documented onset of Chrome's intensive throttling.
Then Pack's filter-selection loop, unchanged:
polls: 1
gaps: [25876]
elapsed: 25876ms against a 15000ms deadline
One attempt. The single setTimeout(250) returned 25.9 seconds later, by which point the
deadline had already passed.
Why this happens
src/connectors/gst/main-world-filed-returns-filter-selection.ts is injected world: "MAIN", so it
runs in the page's context and inherits the page's timer throttling. Its selectOption is a
wall-clock-bounded poll:
const filterTimeoutMs = 15_000;
const filterPollMs = 250;
while (Date.now() - startedAt < filterTimeoutMs) { ...; await sleep(filterPollMs); }
Under intensive throttling a page timer fires at most once per minute, so the first poll wait
outlives the whole 15s budget. There are five in-page setTimeout call sites in that file — two
poll waits and three 1s waits.
Extension service worker timers are not throttled this way. Some of Pack's waits are already
worker-side and unaffected (for example the settle delay in
src/connectors/gst/../background/filed-returns-single-period-flow.ts), so only the in-page loops
are exposed. The change is narrower than "move the waits to the worker".
What this would reclassify, if the loop is on the stalling path
Superseded in part — see the correction comment. Where this loop is reached, the outcome is a
deterministic failure rather than a stall: one synchronous attempt, then false. The five-minute
onset would explain why only long runs are affected.
Whether a stalling run reaches this loop at all is not established. It is a one-shot fallback,
and the primary path's waits are already worker-side and unthrottled.
Proposed fix
Worker-driven polling: the wait loop lives in the service worker, and each page check becomes a
discrete executeScript. No page modification, no new permission, no visibility spoofing. It is
also more MV3-correct independently, because a worker-owned loop survives worker death in a way an
in-page loop does not.
Explicitly not proposed: spoofing document.visibilityState / document.hidden. The repo's own
precedent is that instrumentation which alters behaviour becomes the bug, and it would not help
against cause 3 below in any case.
Hard ordering constraint
Tab pinning must land with this fix, not after it.
getRequiredGstTab() is called at two sites, both phase starts, neither a recovery path. Nothing
recovers a run by re-acquiring a tab. So if the pinned tab were closed mid-run, the second site
would re-acquire a different GST tab and carry on — the identity hazard itself, wearing the
costume of resilience. Pinning one tab id for the whole run and failing closed removes an
opportunity to be wrong rather than removing a mechanism.
This matters here specifically because background running is exactly when re-acquisition could grab
a different taxpayer's tab. Shipping unattended runs first would widen that window rather than
leave it as it is.
Three caveats that must travel with the numbers
- Measured in an in-app Chromium browser on a localhost origin, not in the user's own Chrome
profile.
- It measures a plain page's timer domain, not a
world: "MAIN" injection into the portal. Same
throttling code path as far as we can tell, but that inference is a step and should be visible
as one.
- It does not rule out a second cause. If the portal also gates on
visibilitychange, or
enables a control through requestAnimationFrame — which Chrome does not schedule for hidden
tabs regardless of what the page believes — then fixing the timers moves where the run fails
rather than fixing it. rAF is the candidate that would vindicate the current design. Do not
promise unattended runs until this is settled.
Warning for anyone re-running the measurement
Do not watch the tab while it runs. An earlier run of this probe was invalidated by exactly
that: warm-up ticks read ~6000ms, then jumped to ~5003ms — unthrottled — immediately after the page
was read over CDP to check progress. Observing the throttling lifts the throttling, and periodic
checks appear to reset Chrome's own hidden-and-idle clock.
That run would have reached its gate having never banked five uninterrupted minutes, produced a
healthy poll count, and reported the hypothesis falsified — the opposite of the truth, with
clean-looking numbers and nothing downstream looking wrong. For this class of measurement a human
who walks away is a safer instrument than an agent that checks in.
The run of record above was untouched from load until after the loop returned.
Summary
docs/PORTAL_INTEGRATION_FINDINGS.md#24 records that a full-fiscal-year run can stall when theGST Portal tab or its window is backgrounded. That entry states a symptom and a policy
response; it never recorded a cause. The product now instructs operators to keep the portal
foregrounded, and
getRequiredGstTab()force-focuses the tab and its window, on the strength of anundiagnosed stall.
See the correction comment below before acting on this issue. The measurement below is
sound; the causal claim it was filed with was not. Pack's primary path already waits
worker-side, and the in-page loop measured here is a one-shot fallback whose connection to the
recorded stall is unestablished.
What was measured
A standalone page — no extension, no portal, no session — running Pack's own loop shape in a tab
hidden from load and never once visible. Warm-up sleeps of 5,000ms each:
Fifty ticks at ~6s is ~300s, so the clamp went from 1s to 60s at almost exactly five minutes
hidden — the documented onset of Chrome's intensive throttling.
Then Pack's filter-selection loop, unchanged:
One attempt. The single
setTimeout(250)returned 25.9 seconds later, by which point thedeadline had already passed.
Why this happens
src/connectors/gst/main-world-filed-returns-filter-selection.tsis injectedworld: "MAIN", so itruns in the page's context and inherits the page's timer throttling. Its
selectOptionis awall-clock-bounded poll:
Under intensive throttling a page timer fires at most once per minute, so the first poll wait
outlives the whole 15s budget. There are five in-page
setTimeoutcall sites in that file — twopoll waits and three 1s waits.
Extension service worker timers are not throttled this way. Some of Pack's waits are already
worker-side and unaffected (for example the settle delay in
src/connectors/gst/../background/filed-returns-single-period-flow.ts), so only the in-page loopsare exposed. The change is narrower than "move the waits to the worker".
What this would reclassify, if the loop is on the stalling path
Superseded in part — see the correction comment. Where this loop is reached, the outcome is a
deterministic failure rather than a stall: one synchronous attempt, then false. The five-minute
onset would explain why only long runs are affected.
Whether a stalling run reaches this loop at all is not established. It is a one-shot fallback,
and the primary path's waits are already worker-side and unthrottled.
Proposed fix
Worker-driven polling: the wait loop lives in the service worker, and each page check becomes a
discrete
executeScript. No page modification, no new permission, no visibility spoofing. It isalso more MV3-correct independently, because a worker-owned loop survives worker death in a way an
in-page loop does not.
Explicitly not proposed: spoofing
document.visibilityState/document.hidden. The repo's ownprecedent is that instrumentation which alters behaviour becomes the bug, and it would not help
against cause 3 below in any case.
Hard ordering constraint
Tab pinning must land with this fix, not after it.
getRequiredGstTab()is called at two sites, both phase starts, neither a recovery path. Nothingrecovers a run by re-acquiring a tab. So if the pinned tab were closed mid-run, the second site
would re-acquire a different GST tab and carry on — the identity hazard itself, wearing the
costume of resilience. Pinning one tab id for the whole run and failing closed removes an
opportunity to be wrong rather than removing a mechanism.
This matters here specifically because background running is exactly when re-acquisition could grab
a different taxpayer's tab. Shipping unattended runs first would widen that window rather than
leave it as it is.
Three caveats that must travel with the numbers
profile.
world: "MAIN"injection into the portal. Samethrottling code path as far as we can tell, but that inference is a step and should be visible
as one.
visibilitychange, orenables a control through
requestAnimationFrame— which Chrome does not schedule for hiddentabs regardless of what the page believes — then fixing the timers moves where the run fails
rather than fixing it. rAF is the candidate that would vindicate the current design. Do not
promise unattended runs until this is settled.
Warning for anyone re-running the measurement
Do not watch the tab while it runs. An earlier run of this probe was invalidated by exactly
that: warm-up ticks read ~6000ms, then jumped to ~5003ms — unthrottled — immediately after the page
was read over CDP to check progress. Observing the throttling lifts the throttling, and periodic
checks appear to reset Chrome's own hidden-and-idle clock.
That run would have reached its gate having never banked five uninterrupted minutes, produced a
healthy poll count, and reported the hypothesis falsified — the opposite of the truth, with
clean-looking numbers and nothing downstream looking wrong. For this class of measurement a human
who walks away is a safer instrument than an agent that checks in.
The run of record above was untouched from load until after the loop returned.