skip to content

A desktop window freezes while a chain reads a large file; adding a switch that moves only the stages after it changed nothing — why?

level: seniorimportance: must knowfreq 55%

answer

  1. which worker attached the subscriber
  2. the freeze is the source's own work
  3. forward-only reach cannot help upstream
  4. match the switch to the pass
  5. relocate producing, then one hop back

basics

~20 s

The freeze is the read itself, and the read runs on whatever worker the subscription reached the source from — the window worker here. A switch that moves only later stages never touches it; it just adds a hop.

solid answer

~40 s

The window worker assembled the chain and attached the subscriber, so the subscription walked up to the source on that worker and the file read happened there. That read is the freeze. A switch that relocates only what follows it queues each arriving value for another worker, which moved the cheap decoding and formatting and left the expensive read exactly where it was — an extra handoff bought, nothing that mattered moved. The fix is the other switch: relocate the producing work, so the subscription reaches the source on a background worker and the read runs there. Then add one deliberate downstream boundary at the end, so progress values come back to the window worker for painting. Two switches, each acting on the pass it was meant for.

code

pseudocode · 16 lines
pseudocode
// broken: only the cheap stages moved
chain =
    source: read_large_file(path)
    deliver_following_on(background_worker)   // forward-only boundary
    then:   decode(chunk)
    then:   to_progress_percent(chunk)
subscribe(chain, on_value = paint)            // from the window worker -> still frozen

// fixed: move the producing work, then come back once
chain =
    source: read_large_file(path)
    then:   decode(chunk)
    then:   to_progress_percent(chunk)
    run_producing_on(background_worker)       // acts on the subscription pass
    deliver_following_on(window_worker)       // one deliberate hop back
    then:   paint(percent)

go deeper

for a junior

Remember that a chain uses whichever worker attached the subscriber. If that is the worker owning the window, then the source's work happens on it and the window cannot repaint meanwhile.

for a middle

Explain the mechanics: forward-only reach on the value pass versus reach to the source on the subscription pass, and say precisely which stages each one moved in this chain.

for a senior

Demonstrate the diagnosis: confirm what the frozen worker is doing, notice the reassuring but irrelevant background activity the wrong fix creates, and land both moves — relocate the source, then one boundary back.

for a principal

Own the pattern rather than the incident: decide where in the codebase relocation is allowed to happen, and what a chain crossing a team boundary must state about the worker its values arrive on.

## The symptom and what it rules out The window is unresponsive for as long as the file takes to read, and it recovers the moment the read ends. That shape points at one thing: the worker that owns the window is inside the read, not waiting on it. Nothing about the chain's shape changes this — a chain does not introduce a worker of its own, so whichever worker attached the subscriber is the worker the whole pipeline uses until something moves it. ## Where the read actually ran When the subscriber attached, the subscription walked **upward** from it through every stage to the source, on the window worker. The source then started producing on the worker it was subscribed from. So: - the read ran on the window worker; - each chunk it emitted continued downward on the window worker; - every stage after it ran there too, because a value keeps travelling on the worker it arrived on until some stage queues it elsewhere. ## Why the switch that was added moved nothing that mattered The switch that relocates what follows acts on the **downward** pass. It queues each arriving value for its worker, so its reach is strictly forward. Placed immediately after the source it relocates the decoding and the percentage arithmetic — microseconds of work — while the read that caused the freeze has already happened, upstream of the boundary, on the window worker. The change is not neutral either: every chunk now crosses a handoff. | | before the change | after adding the forward-only switch | |---|---|---| | Where the read runs | window worker | window worker — unchanged | | Where decoding runs | window worker | background worker | | Handoffs per chunk | none | one | | Window responsive during the read | no | no | That table is the whole defect: one column moved, and it was the cheap one. ## The fix, in two moves 1. **Relocate the producing work.** Add the switch that acts on the subscription pass. The subscription now reaches the source from a background worker, so the read happens there and the window worker returns to its event handling immediately after attaching the subscriber. Because that switch is reached on the upward pass, it works wherever it is written in the chain. 2. **Come back deliberately at the end.** Painting a progress bar is usually legal on exactly one worker. Add a forward-only switch as the last boundary, so the values produced in the background arrive on the window worker for the final stage. This is a boundary you want: it is the one hop that buys something. The result is one chain with two segments — everything from the source down to the final boundary in the background, and the painting on the window worker. ## Confirming it rather than guessing - While the window is frozen, look at what the window worker is doing. If it is inside the read, the diagnosis is settled; if it is idle, the stall is somewhere else and this leaf's fix will not help. - Log or record the worker identity in the first stage after the source and in the painting stage. Two identities where you expected two, and the right ones, is the check. - Beware of the reassuring middle state: after the wrong switch is added, a snapshot *does* show a background worker busy. It is busy with the trivial part. ## The generalisation Ask two questions of every relocation, in this order: 1. **Which pass does the expensive work happen on?** Work done by the source, or done while attaching to it, is on the subscription pass. Work done to values already emitted is on the value pass. 2. **Which switch acts on that pass?** Match them. A mismatch is not a small inefficiency: it is a change that fixes nothing while looking like it should, which is why this bug survives review. Reviewers see a switch, see a background worker in the snapshot, and stop reading. The same reasoning covers the mirror-image mistake. If the chain's expensive part is a per-value computation rather than the source, relocating the producing work moves the source that had no problem and leaves the heavy stage exactly where the values arrive.

  • After the fix, why is the final boundary back to the window worker not the same mistake repeated?
    Because this time the pass matches the need. Painting acts on values that already exist, which is the downward pass, and painting is legal on one worker only. The hop buys a correctness requirement rather than relocating work that was never the problem.
  • The chain's expensive part is a per-value computation, not the read. Which switch is wrong then?
    Relocating the producing work is wrong: it moves a source that was never the problem and leaves the heavy per-value stage running wherever values arrive. The forward-only boundary placed just before that stage is what moves it.
  • Why does this defect survive code review so often?
    Because the wrong fix produces evidence that looks right. A switch is present, and a snapshot really does show a background worker busy. Nobody checks that the busy worker is doing the trivial stage while the expensive one stayed behind.

saying these in an interview costs you the question

  • Says any switch anywhere is enough to get work off the window worker
  • Believes the chain introduces a background worker of its own
  • Thinks the forward-only boundary can relocate work already done upstream of it
  • Blames the window worker for painting rather than for running the read
  • Adds a second forward-only boundary instead of relocating the producing work
  • Treats the extra handoff per chunk as free