detached 92e0c36

perf(history): pool zlib writers for idle page compression

Each BestSpeed writer allocates ~600 KiB of deflate state. Reuse them
across pages instead of allocating one per compressed page.

BenchmarkColdHistoryRetained10Kx120/plain-ascii: 66.3 -> 37.0 MB/op
commit
92e0c365a3fe589fac04fa54cfc4d0d64a0b33fb
author
brice · · 2026-10-07 16:24 UTC
parent
adadad1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +19 -5
M history_storage.go +16 -5
@@ -113,6 +113,16 @@ func (c *HistoryChunk) Restore() error {
113113 return err
114114 }
115115
116+// pageCompressorPool reuses BestSpeed writers; each one owns ~600 KiB of
117+// deflate state that would otherwise be allocated per compressed page.
118+var pageCompressorPool = sync.Pool{New: func() any {
119+ w, err := zlib.NewWriterLevel(io.Discard, zlib.BestSpeed)
120+ if err != nil {
121+ panic(err) // BestSpeed is a valid level
122+ }
123+ return w
124+}}
125+
116126 func (p *sealedPage) compressIfIdle() (bool, error) {
117127 p.mu.Lock()
118128 defer p.mu.Unlock()
@@ -143,12 +153,13 @@ func (p *sealedPage) compressIfIdle() (bool, error) {
143153 return false, errors.New("history page exceeds encoded backing limit")
144154 }
145155 var out bytes.Buffer
146- w, err := zlib.NewWriterLevel(&out, zlib.BestSpeed)
147- if err != nil {
148- return false, err
149- }
156+ w := pageCompressorPool.Get().(*zlib.Writer)
157+ w.Reset(&out)
158+ defer func() {
159+ w.Reset(io.Discard)
160+ pageCompressorPool.Put(w)
161+ }()
150162 if _, err := w.Write(raw); err != nil {
151- _ = w.Close()
152163 return false, err
153164 }
154165 if err := w.Close(); err != nil {
@@ -113,6 +113,16 @@ func (c *HistoryChunk) Restore() error {
113 return err113 return err
114 }114 }
115 115
116+// pageCompressorPool reuses BestSpeed writers; each one owns ~600 KiB of
117+// deflate state that would otherwise be allocated per compressed page.
118+var pageCompressorPool = sync.Pool{New: func() any {
119+ w, err := zlib.NewWriterLevel(io.Discard, zlib.BestSpeed)
120+ if err != nil {
121+ panic(err) // BestSpeed is a valid level
122+ }
123+ return w
124+}}
125+
116 func (p *sealedPage) compressIfIdle() (bool, error) {126 func (p *sealedPage) compressIfIdle() (bool, error) {
117 p.mu.Lock()127 p.mu.Lock()
118 defer p.mu.Unlock()128 defer p.mu.Unlock()
@@ -143,12 +153,13 @@ func (p *sealedPage) compressIfIdle() (bool, error) {
143 return false, errors.New("history page exceeds encoded backing limit")153 return false, errors.New("history page exceeds encoded backing limit")
144 }154 }
145 var out bytes.Buffer155 var out bytes.Buffer
146- w, err := zlib.NewWriterLevel(&out, zlib.BestSpeed)156+ w := pageCompressorPool.Get().(*zlib.Writer)
147- if err != nil {157+ w.Reset(&out)
148- return false, err158+ defer func() {
149- }159+ w.Reset(io.Discard)
160+ pageCompressorPool.Put(w)
161+ }()
150 if _, err := w.Write(raw); err != nil {162 if _, err := w.Write(raw); err != nil {
151- _ = w.Close()
152 return false, err163 return false, err
153 }164 }
154 if err := w.Close(); err != nil {165 if err := w.Close(); err != nil {
M storage_benchmark_test.go +3 -0
@@ -78,6 +78,9 @@ func BenchmarkColdHistoryRetained10Kx120(b *testing.B) {
7878 }
7979 }
8080 benchmarkStorageHistorySink = h
81+ // Two cycles drain the compressor sync.Pool, so only the
82+ // history itself is measured.
83+ runtime.GC()
8184 runtime.GC()
8285 runtime.ReadMemStats(&after)
8386 if after.HeapAlloc >= before.HeapAlloc {
@@ -78,6 +78,9 @@ func BenchmarkColdHistoryRetained10Kx120(b *testing.B) {
78 }78 }
79 }79 }
80 benchmarkStorageHistorySink = h80 benchmarkStorageHistorySink = h
81+ // Two cycles drain the compressor sync.Pool, so only the
82+ // history itself is measured.
83+ runtime.GC()
81 runtime.GC()84 runtime.GC()
82 runtime.ReadMemStats(&after)85 runtime.ReadMemStats(&after)
83 if after.HeapAlloc >= before.HeapAlloc {86 if after.HeapAlloc >= before.HeapAlloc {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close