Skip to content
C.W.K.
Stream
Lesson 02 of 04 · published

Three Clocks, and Why No Default Works

~12 min · capture, timing, async, readiness

Level 0Cold Workshop
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Deceptively Simple Task

Take a screenshot of a running application. It sounds like one operation, and in practice it is a timing problem with no universally correct answer, because the only hard question is when is the page finished? — and different applications answer that in incompatible ways.

Clock One: Real Time

Wait a fixed number of seconds, then shoot. Crude, and correct for a specific class of application: anything that polls continuously. A page that schedules work forever has no moment of quiescence to detect, so any cleverer method waits indefinitely. For these, a blunt real-time timeout is the only thing that terminates.

Clock Two: Virtual Time

Rather than waiting in real seconds, let the browser run its own clock as fast as it can until a budget of virtual time is exhausted. This is correct for load-once applications, especially those whose startup restores state asynchronously — a session restore that takes an unpredictable real duration but a bounded amount of scheduled work. A fixed real-time wait either cuts these off mid-restore or wastes time on every run to accommodate the worst case.

Point this at a polling application and it never terminates: there is always more scheduled work, so the virtual clock never drains.

Clock Three: Ask the Page

Some applications defeat both. Interfaces that hydrate in waves are ready in a real sense — nothing is scheduled, real time has passed — but visibly incomplete, showing half-populated rows or skeletons. Neither blind clock can see this, because both are measuring time and the actual question is about state.

The answer is to poll a readiness condition from inside the page and shoot when it reports true. That means each application needs its own definition of ready, which is more work and is the only thing that actually answers the question being asked.

When a resource has no universal readiness signal, the readiness definition belongs to the caller. The temptation is to find one timeout that works everywhere; there isn't one, and the search costs more than accepting per-application knowledge. Encode which clock each target needs, and treat a new target with no entry as a question rather than as a default.

Code

Three strategies, and the property that selects between them·python
class Clock(Enum):
    REAL = "real"        # polls forever -> only a blunt timeout terminates
    VIRTUAL = "virtual"  # loads once -> let its own clock run out
    HYDRATED = "ready"   # loads in waves -> ask the page directly


def capture(url: str, out: Path, clock: Clock, ready_js: str | None = None):
    if clock is Clock.REAL:
        args = ["--timeout=8000"]

    elif clock is Clock.VIRTUAL:
        # never point this at a polling app: the budget never drains
        args = ["--virtual-time-budget=20000"]

    elif clock is Clock.HYDRATED:
        # neither blind clock can see a half-populated list
        assert ready_js, "hydration capture needs a readiness predicate"
        wait_until(url, predicate=ready_js, timeout=30)
        args = ["--timeout=500"]

    shoot(url, out, args)


# The clock is a property of the TARGET, not a setting to standardize.
# A new target with no entry here is a question, not a default.

External links

Exercise

Find a wait in your codebase expressed as a fixed sleep before reading some state. Work out what condition that sleep is actually approximating, then replace it with a poll on that condition plus a timeout as a backstop. Note how long the poll actually takes in practice compared to the sleep — usually far less, and occasionally far more, which is the case the sleep was silently getting wrong.
Hint
A fixed sleep encodes two guesses at once: that the condition will be true by then, and that waiting that long is acceptable. Both are usually wrong in opposite directions — too short under load, wastefully long the rest of the time — and separating the condition from the backstop fixes both.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.