Skip to content

Stream printed diffs instead of buffering them three times - #101

Merged
keegancsmith merged 1 commit into
masterfrom
k/streaming-print
Oct 20, 2026
Merged

keegancsmith merged 1 commit into
masterfrom
k/streaming-print

Conversation

@keegancsmith

Copy link
Copy Markdown
Member

Printing a big diff used to keep about three copies of it in memory. PrintHunks built a growing buffer per file, PrintFileDiff copied that into a second one, and PrintMultiFileDiff copied each file into a third. Each buffer grew by doubling. Sourcegraph's frontend prints whole repo comparisons this way, and diffs of hundreds of MiB have contributed to OOMs there.

This adds WriteMultiFileDiff(w, ds) and WriteFileDiff(w, d), which write straight to an io.Writer. Hunk bodies go through as-is, and header lines are built in one reused scratch slice instead of with fmt. The Print* functions now use the same code. They do a quick counting pass first, then fill a buffer allocated at exactly the output size. Output is byte-for-byte the same, including nil for empty output. I checked that against the old implementation on all of testdata, a set of edge cases and 20k random diffs.

Numbers from a 200k-file diff and a single huge hunk, each run in its own process:

Case Version Allocated Allocations Peak memory growth Time
200k files, 148 MiB output before 816 MiB 3.4M ~475 MiB ~1.4 s
after 148 MiB 5 148.5 MiB ~0.14 s
streaming to io.Discard ~0 1 0 22 ms
One 121 MiB hunk before 363 MiB 19 363 MiB ~0.9 s
after 121 MiB 7 121 MiB ~0.08 s
streaming to io.Discard ~0 2 0 <1 ms

Yep, it's also about 10× faster even though it formats twice. Most of the old time went into filling freshly allocated memory.

Callers that can stream, like an HTTP handler, should use WriteMultiFileDiff and skip holding the diff in memory at all. That also gets rid of the extra copy from string(b).

Printing a large diff used to hold several copies of it at once: PrintHunks
built a growing buffer per file, PrintFileDiff copied it into a second buffer
and PrintMultiFileDiff copied every file into a third, each growing by
doubling. Sourcegraph's frontend prints whole repository comparisons this way,
and diffs of hundreds of MiB have contributed to OOMs there.

WriteMultiFileDiff and WriteFileDiff now write directly to an io.Writer, with
hunk bodies passed through without copying and header lines assembled in one
reused scratch slice instead of through fmt. Callers that can stream (such as
an HTTP response) no longer need the printed diff in memory at all.

The Print functions are built on the same code and keep their output
byte-for-byte, including returning nil for empty output. They first measure
the output with a counting writer and then fill a buffer of exactly that
size, so the result is allocated once. For a 148 MiB diff of 200k files this
cuts allocation from 816 MiB in 3.4M mallocs to 148 MiB in 5, and peak RSS
growth from ~475 MiB to 148 MiB; for a single 121 MiB hunk, from 363 MiB to
121 MiB. Despite formatting twice, printing is about 10x faster.

Amp-Thread-ID: https://ampcode.com/threads/T-01a15907-a942-77c9-ab83-3ff2545b0c9d
Co-authored-by: Amp <amp@ampcode.com>
@keegancsmith

Copy link
Copy Markdown
Member Author

aah I can merge without review, will do that. Will respond to post merge feedback :)

@keegancsmith
keegancsmith merged commit af960bf into master Oct 20, 2026
1 check passed

@eseliger eseliger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very good 👏

@eseliger
eseliger deleted the k/streaming-print branch October 20, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants