progress: show hashing workers as idle when the queue starves - #143
Merged
Conversation
A csum worker keeps its progress slot for its whole life and rolls it from file to file, so between files the line keeps whatever the last file left behind - "commit", the status set for the final DB write. That is invisible on a hashing-bound run (the next file overwrites it within microseconds) but wrong the moment the walk becomes the bottleneck: on a big NAS tree where only a handful of files need hashing, all four workers sat on "commit <last file>" for the entire rest of the run, reading like a hang in the commit path. Park the slot as idle once a worker has waited SCAN_IDLE_PARK_MS (250ms, > one 100ms redraw) for work: long enough that the sub-millisecond gaps between small files still never flicker, short enough that a genuinely starved pool stops lying within a blink. The first wait of a starvation is bounded, the rest is the plain blocking wait as before. A parked slot stays *owned*, so pscan_claim_slot() cannot hand it to a sibling worker while its owner is only waiting - pscan_slot_idle() now means "released for good" (drain, or the churning dedupe pool finishing a work item) and clears that ownership. pscan_register_thread() sets status/owned before publishing the slot for the same reason, and is now static (nothing outside progress.c used it). Co-Authored-By: Claude <[email protected]>
Follow-up cleanup of the previous commit's mechanism (a /simplify pass). It parked the line from inside scan_workq_pop(): a bounded first wait, then a call into the progress module to overwrite the slot's status. That put display timing in a data-structure primitive, needed a display slot threaded through a queue pop, and needed an `owned` flag so that a parked-but-still-busy slot wasn't handed to a sibling worker - two fields encoding one allocation state. Publish the fact instead of acting on it: the worker records when it started waiting (pscan_slot_waiting, one atomic store bracketing the pop), and the renderer draws any line whose wait outlasts a couple of redraws as idle. Same behaviour, and it deletes the timed wait, the park constant, the `owned` field, pscan_slot_wait() - a near-duplicate of pscan_slot_idle() - and the pscan_thread parameter on scan_workq_pop(), whose signature and unit test go back to what they were. The display threshold now lives next to the redraw interval it is derived from, both named rather than bare literals. The unit test loses its helper thread's 500 ms sleep (the whole C suite was 0.50 s wall for 0.6 ms of CPU): with the rule a pure function of a timestamp, backdating waiting_since tests it exactly, in microseconds. Co-Authored-By: Claude <[email protected]>
martinus
added a commit
that referenced
this pull request
Jul 26, 2026
…gure (#151) The README was last substantively updated in #118, two releases ago, so it was missing everything shipped since and carried one number that no longer matched the benchmark doc. Fix a wrong figure: the path-hash row claimed "41 vs 73 MiB on the benchmark tree". Neither number appears in docs/benchmarks.md, which measures 39.7 vs 70.9 MiB -- and that figure comes from the larger-than-RAM tree, not the 2.07M-file tree the surrounding table describes. Correct both the numbers and the attribution, and reword the table intro, which named only one of the two benchmarks the rows actually draw from. Add the missing user-facing work: - paths beyond PATH_MAX are hashed and deduped (#117/#124/#128) - the streaming dedupe pipeline (#116), which had no bullet at all - the O(extents^2) fragmented-file scan fix (#134) - the two dedupe-phase races (#123, #129) - clang ASAN/UBSAN/TSAN CI legs, warnings-as-errors, make check-all - --cpu-threads, absent from both CLI lists despite being in --help - progress polish: scan-phase throughput (#120), idle workers (#143) Tighten for readability: drop the standalone larger-than-RAM NOTE, which stated the same ~13x claim a third time; its unique content (RSS, hashfile size) moves into the speedups table where the reader is already comparing figures. Mark upstream issue references as "upstream #NNN" throughout -- bare markfasheh#331/markfasheh#374/markfasheh#376/markfasheh#387 now read as oans issues, since oans has its own numbers in that range. Docs-only; no code touched. Co-authored-by: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
On a big NAS tree replayed from a huge hashfile, the worker lines froze on
commitfor the whole run:Those 4 files were hashed in milliseconds; the walk then ran for minutes with nothing left to hash.
A csum worker is persistent and keeps its progress line across files (deliberately — re-claiming per file made the display flash "idle" between small files), so between files the line keeps whatever the last file left behind:
thread_committing, set for the final DB write. Invisible when hashing is the bottleneck, plainly wrong — and it reads like a hang in the commit path — when the walk is.The fix
Split it where the knowledge actually is. Only the queue can tell "blocked with no work" from "blocked doing work", so the worker publishes when it started waiting —
pscan_slot_waiting(slot, true/false), one atomic store bracketing the pop. Only the renderer should own display timing, so the renderer draws any line whose wait has outlasted a couple of redraws asidle(slot_is_idle()), against a now-namedREDRAW_MSinstead of two bare literals.Nothing is polled and no timer is involved: the cost is one atomic store per file (plus one vDSO clock read), and the line flips within one redraw of the threshold. The slot's
statusis untouched while it waits, so it still looks claimed and no sibling worker takes over its line.Also:
pscan_register_thread()published a fresh slot intopscan.threadswhile it was stillthread_idle— i.e. stealable by a concurrent claim — so it now sets the status before publishing, and is static (nothing outsideprogress.cused it).The second commit is a
/simplifypass over the first, which had done this from insidescan_workq_pop()with a bounded wait, a park call into the progress module and anownedflag. That worked but put display policy in a queue primitive; the rewrite deletes the timed wait, the park constant,owned, a near-duplicatepscan_slot_wait()and the extrascan_workq_pop()parameter (its signature and unit test are back to what they were). Happy to squash the two.Verification
Reproduced with a 300k-file tree, cold cache, 3 files touched (
--io-threads=1, captured through a pty):1 commit .../hashme2 (size: 97.7 KiB)in every one of the 23 frames;commitfor 2 frames (~200 ms), then1 idlefor the remaining 2 s.No-flicker check on a hashing-bound run (94k-file cold scan, 46 frames): an idle worker line appears in exactly one frame, the drain at the end.
test_starved_worker_line_reads_idlepins both directions (a fresh wait still shows the file's status; a backdated one reads idle) plus the no-steal rule — with no sleep, so the C unit suite is back to sub-millisecond.scripts/verify.shpasses (clean build, 120 integration tests, valgrind smoke), as does the full suite under ThreadSanitizer.🤖 Generated with Claude Code