Repository navigation
feat(serialize): opt-in uncached bounded-memory encoder profile (#83) - #86
Merged
Merged
Conversation
…ile (#83) - build a one-worker, low-memory zstd encoder per call and drop it - bypass the shared encoder cache, so no encoder stays live after the call - output stays byte-identical, wire format unchanged Co-Authored-By: Claude <noreply@anthropic.com>
- bench the uncached profile in BenchmarkEncoderProfile - clamp retained heap delta at 0 to avoid uint64 underflow - append a measured section to docs/encoder-profile-benchmarks.md Co-Authored-By: Claude <noreply@anthropic.com>
…GELOG (#83) Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…rge profile tests (#83) sharedZstdEncoder is cache-only again. encodeWithLevel builds the uncached encoder itself. The uncached profile now runs through the existing bounded-memory tests instead of copies of them. Co-Authored-By: Claude <noreply@anthropic.com>
…ile (#83) sharedZstdEncoder rejects the uncached profile, so no caller can cache it by mistake. The heap test warms up with the cached profile, which lets it catch an encoder retained outside the cache. The benchmark reports a signed heap difference in place of a value clamped at 0, and the doc table has the numbers from a new run. Docs say that a decoded index carries the default profile. Co-Authored-By: Claude <noreply@anthropic.com>
…file (#83) Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
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.
Summary
Fixes #83.
EncoderProfileBoundedMemorykeeps its one-worker encoder in the shared encoder cache until the process stops. At level 15 that is 42 MB of live heap. A batch job that encodes one index per batch pays for it in every later step.This PR adds a third profile,
EncoderProfileBoundedMemoryUncached. The library builds the same one-worker encoder for one call and holds no pointer to it after the call.Callers select it with the options that exist today:
What does not change: the output bytes, the wire format (v11), the two existing profiles and their constant values,
gin.go, the CLI, the decoder. There is no new option, no newGINConfigfield and no concurrency limit.Two notes for the reviewer:
Close(). In klauspost v1.20.0,Close()returns at once for an encoder used only throughEncodeAll, and that path starts no goroutines. The memory is free when no pointer to the encoder remains.WithEncodeProfileon that call. The godoc and README now say this.Evidence
Level 15, darwin/arm64 (Apple M2 Max), GOMAXPROCS=4, klauspost v1.20.0,
-benchtime=10x. Full table indocs/encoder-profile-benchmarks.md.GINcpayload by hand.After:
make testpasses, 1244 tests, 1 skipped (testdata/test.parquetis absent, same onmain).go test -race -run 'EncoderProfile|Uncached|MixedProfiles' .passes.Issue acceptance:
encoderProfileLevels, single-block and multi-block:TestEncoderProfileBoundedOutputIdenticalToDefaultnow runs both non-default profiles.TestEncoderProfileUncachedDoesNotRetainEncoder(live heap grows less than 8 MiB, and a control with the cached profile must grow more than 20 MiB), plusTestEncoderProfileUncachedLeavesCacheEmpty.git diff main -- docs/encoder-profile-benchmarks.mdremoves 0 lines.TestEncoderProfileUncachedWireFormat.Mutations I applied by hand, then reverted, to check that the tests can fail:
serialize.goretained 44225872 bytes after encode, want under 8 MiBTestEncoderProfileBoundedUsesSingleWorker/bounded-memory-uncachedfailsLint:
GOTOOLCHAIN=go1.25.5 make lintreports 0 issues. Plainmake linton my machine (go1.27.1) fails with a typecheck error inlogging/attrs.go.mainfails the same way, so it is the local linter build, not this change.Not covered: no test calls
RebuildWithIndexor the S3 sidecar writer with the new profile. Neither had a profile test before. Both pass options to the sameEncodeWithLevelContextcall.Merge Danger
Door: two-way until a release tag, one-way after
The profile is opt-in, so a revert before the next tag affects no caller. After a release, the constant name
EncoderProfileBoundedMemoryUncachedand its value2are public API.Blast Radius: opt-in
Callers that do not select the new profile run the same code as before, with one extra integer comparison per encode. A caller that selects it and encodes from N goroutines at once builds N encoders, about N x 44 MB allocated at level 15. The library sets no limit, by decision. The docs state the cost.
Also in the diff: the
BenchmarkEncoderProfileretained metric is now a signed difference taken after two GC cycles, for all profiles. The old code subtracted unsigned values and could underflow. Planning records for this task are under.planning/quick/261006-hgl-*, including the question ledger and the 14 expectations the change was verified against.