Skip to content

perf(storage/tigris): cap the zstd encoder window at 1 MiB - #27

Open
Xe wants to merge 1 commit into
mainfrom
perf/zstd-window
Open

Xe wants to merge 1 commit into
mainfrom
perf/zstd-window

Conversation

@Xe

@Xe Xe commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Set the window of the encoder() zstd singleton to inMemoryCap (1 MiB). It used the default window, which kept 64 MiB of match history for the life of the process.
  • Record the measurements in docs/plans/push-memory.md and remove the item from the phase 3 list.

Details

After #25, the zstd history was most of the heap that a push leaves live. Every input of the singleton is at most inMemoryCap: an object, a probeWindow head, or a block of a .cue or packed-refs. A 1 MiB window finds almost all of the same matches. Objects larger than inMemoryCap use streamEncPool, and this PR does not change them.

Measured by feeding a real pack through ReadPack over a fake S3 that throws away .bin bodies. Xe/x is the median of 3 runs. golang/go is 2 runs.

Measurement Default window 1 MiB window
Live heap after a push (both repos) 72 MiB 16 MiB
Xe/x, resolve peak heap 213 MiB 79 MiB
Xe/x, peak RSS 204 MiB 123 MiB
golang/go, resolve peak heap 450 to 478 MiB 366 to 372 MiB
golang/go, peak RSS 466 to 487 MiB 394 to 397 MiB
.bin bytes (both repos) identical identical
.cue bytes +0.09% (Xe/x), +0.22% (golang/go)

Wall time does not change by a measurable amount. Existing data is unaffected: a smaller encoder window only changes new writes, and the decoder accepts both.

Test plan

  • Code compiles with go build ./...
  • Code formatted (prettier and goimports on the changed files, through the commit hook)
  • go test ./... passes
  • Manual testing: memory and stored bytes measured on Xe/x and golang/go, against 1b1ad2f as the baseline

The encoder() singleton used the default zstd window, so it kept 64 MiB
of match history for the life of the process. After the bounded-memory
pack writer, that history was most of the heap a push leaves live.

Every input of the singleton is an object of at most inMemoryCap, a
probeWindow head, or a block of a .cue or packed-refs. A window of
inMemoryCap finds almost all of the same matches. Objects larger than
inMemoryCap use streamEncPool, which this does not change.

Measured with a real pack through ReadPack over a fake S3:

- Live heap after a push: 72 MiB to 16 MiB, for Xe/x and golang/go.
- Xe/x peak RSS: 204 MiB to 123 MiB.
- golang/go peak RSS: about 476 MiB to 395 MiB.
- .bin bytes: identical for both repositories.
- .cue bytes: +0.09% for Xe/x, +0.22% for golang/go.

Wall time does not change by a measurable amount. The plan records
the full table.

Assisted-by: Claude Opus 5.5 via Claude Code
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
@Xe
Xe requested a balanced review from Copilot October 8, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused change is consistent with encoder input bounds and preserves decoder compatibility.

0 open findings

What changed in this PR

Caps the shared zstd encoder window at 1 MiB to reduce retained memory during pushes.

Changes:

  • Configures the singleton encoder with a 1 MiB window.
  • Documents memory and compression measurements and updates the optimization plan.
File Description
internal/​storage/​tigris/​compress.go Caps the shared encoder window.
docs/​plans/​push-memory.md Records results and marks the work complete.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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