Repository navigation
perf(recrawl): bound the batch in bytes and stop holding the body slack - #204
Merged
Merged
Conversation
The heap profile that found the zstd encoders left bytes.growSlice as the largest line at 210 MB, and it was response bodies. Two things were holding them. A writer's batch was 500 rows and nothing else, which is the same mistake the write queue was making one layer out: a count cannot bound bytes when a body runs from a kilobyte to the 10 MB cap. 500 rows of the URL corpus is about 72 MB, so three writers sat on 217 MB of bodies waiting to reach a count. A batch now hands over on whichever of the count or 16 MB fills first, and flushBatch clears the slice before truncating it, since the backing array was keeping a Capture alive after the row had been written. The bodies carried their own slack on top of that. A bytes.Buffer grows by doubling, so a body that stops just past a doubling keeps an array up to twice its length, and that array travels into the row, the queue and the batch. readBody now reserves from Content-Length where there is one, capped at 512 KB so that a host claiming 10 MB cannot make 96 workers each hold ten megabytes of nothing, and copies a finished body to its own length when the slack is at least 32 KB. Measured on server2 at 96 workers and 3 writers over 15000 pages, no extraction, run A B B A so drift on a shared box cancels: a1 merged 55.0 pages/s 1.64 GB peak b1 this branch 68.1 pages/s 0.93 GB peak b2 this branch 70.0 pages/s 0.85 GB peak a2 merged 67.7 pages/s 1.69 GB peak a1 is the low reading of the four and the box was still settling, so the rate is fair to call the same or a little better rather than a 25 percent win. The memory is not ambiguous. Heap diff of a2 against b2: in use 493 MB against 136 MB, bytes.growSlice 222 MB against 1 MB, Parquet column buffers 65 MB against 13 MB. Worker time queueing to write also fell from 9 and 7 percent to 2 percent, which is the batch letting go sooner. Refs #200
This was referenced Aug 24, 2026
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.
Fourth box of #200. The heap profile in #203 left
bytes.growSliceas the largest single line at 210 MB and said it was response bodies, so this is that line.Two things were holding them.
A writer's batch was
batchRows = 500and nothing else, which is the same mistake the write queue was making one layer further out.A count cannot bound bytes when a body runs from a kilobyte to the 10 MB cap, and 500 rows of the URL corpus is about 72 MB, so three writers sat on 217 MB of bodies waiting to reach a count.
A batch now hands over on whichever of the count or 16 MB fills first.
The count stays as the other half of the pair, because a corpus of tiny pages wants to hand over on the count rather than sit on ten thousand rows waiting to reach a byte figure.
flushBatchalso clears the slice before truncating it, since the backing array was keeping aCapturealive after the row had been written, and a buffer that lets go of the rows but not the bodies flushes on schedule and frees nothing.The bodies carried their own slack on top of that.
A
bytes.Buffergrows by doubling, so a body that stops just past a doubling keeps an array up to twice its length, and that array travels into the row, into the queue and into the batch.readBodynow reserves fromContent-Lengthwhere there is one, capped at 512 KB, sinceContent-Lengthis a claim by the far end and a host saying 10 MB should not be able to make 96 workers each hold ten megabytes of nothing.A decoded body skips the hint entirely, because the number on the wire is for the compressed bytes.
A finished body is copied to its own length when the slack is at least 32 KB, and left alone below that, where the copy costs more than the slack is worth.
Measurement
server2, 96 workers, 3 writers, 15000 pages, no extraction, run A B B A so that drift on a shared box cancels.
a1 is the low reading of the four and the box was still settling into the run, so the honest reading of the rate is the same or a little better, not a 25 percent win.
The memory is not ambiguous: both A runs sat near 1.7 GB and both B runs under 1 GB, which is 46 percent off the peak.
Heap diff of a2 against b2, in use space:
bytes.growSlicezstd.ensureHistWorker time spent queueing to write fell from 9 and 7 percent to 2 percent across the pair, which is the batch letting go of its rows sooner and the gate therefore holding less.
Tests
ccrawl/capture_batch_test.gocovers the batch flushing on bytes with eight 4 MB pages, still flushing on the row count when the pages are small, and the flushed buffer letting go of the bodies in the slots past its length.ccrawl/crawl_body_test.gocovers the hint against aContent-Length, no length, a zero length, a host claiming the whole cap and a decoded body, the right sizing threshold in both directions, andreadBodycoming back byte for byte both with an explicit length and chunked, plus still decoding and still truncating.Docs
docs/content/guides/recrawl-fleet.mdgains a paragraph after the zstd finding, since it is the same shape of bug found by the same profile and somebody reading aboutCCRAWL_WRITERSshould see both.