Skip to content

perf(response): pool on-the-fly compression writers#4385

Open
jvoisin wants to merge 1 commit into
miniflux:mainfrom
jvoisin:poule
Open

perf(response): pool on-the-fly compression writers#4385
jvoisin wants to merge 1 commit into
miniflux:mainfrom
jvoisin:poule

Conversation

@jvoisin

@jvoisin jvoisin commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

For every dynamic response above the 1 KiB compression threshold the builder previously did one of:
brotli.NewWriterV2(b.w, brotli.DefaultCompression)
gzip.NewWriter(b.w)
flate.NewWriter(b.w, -1)

Each constructor allocates the encoder's working set from scratch: brotli's sliding window + hash tables (~3.5 MiB at quality 6), gzip's CRC32 + deflate state (~800 KiB), flate's hash chains (~800 KiB). On a busy server that's per-request churn the GC then has to clean up.

All three writer types expose Reset(dst io.Writer), which rebinds the destination without touching the internal buffers. So put each behind a sync.Pool and Get/Reset/Put around the existing Write+Close pair. The constructor signatures didn't change; only Pool.Get + Reset are new.

Note that brotli.NewWriterV2 (kept from before this change) returns *matchfinder.Writer, not *brotli.Writer, as V2 is the pure-Go encoder built on top of github.com/andybalholm/brotli/matchfinder, where the actual Writer type lives. Hence the matchfinder import.

On a local artificial benchmarks of a 130 KiB HTML-like payload consisting of 250 entry-list items, on a single-core it improves performances by around 10%, and for multicore under GC pressure, ns/op is reduces by ~80% and B/op by ~99%.

For every dynamic response above the 1 KiB compression threshold the
builder previously did one of:
    brotli.NewWriterV2(b.w, brotli.DefaultCompression)
    gzip.NewWriter(b.w)
    flate.NewWriter(b.w, -1)

Each constructor allocates the encoder's working set from scratch:
brotli's sliding window + hash tables (~3.5 MiB at quality 6), gzip's
CRC32 + deflate state (~800 KiB), flate's hash chains (~800 KiB).
On a busy server that's per-request churn the GC then has to clean up.

All three writer types expose Reset(dst io.Writer), which rebinds the
destination without touching the internal buffers. So put each behind a
sync.Pool and Get/Reset/Put around the existing Write+Close pair. The
constructor signatures didn't change; only Pool.Get + Reset are new.

Note that brotli.NewWriterV2 (kept from before this change) returns
*matchfinder.Writer, not *brotli.Writer, as V2 is the pure-Go encoder
built on top of github.com/andybalholm/brotli/matchfinder, where the
actual Writer type lives. Hence the matchfinder import.

On a local artificial benchmarks of a 130 KiB HTML-like payload consisting of
250 entry-list items, on a single-core it improves performances by around 10%,
and for multicore under GC pressure, ns/op is reduces by ~80% and B/op by ~99%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant