detached 2dca0c2

fix(snapshot): release published chunk encodings

commit
2dca0c2bb4acbae8f9c5a83a3d852ee4e39f6d50
author
brice · · 2026-07-27 12:46 UTC
parent
790011a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +43 -2
M internal/usecase/daemon/snapshot_incremental.go +10 -2
@@ -211,8 +211,16 @@ func markSnapshotCaptureObjectsPublished(capture *snapshotCapture) {
211211 return
212212 }
213213 for chunk, ref := range capture.sealedRefs {
214- if chunk != nil {
215- cache.persisted[chunk] = ref
214+ if chunk == nil {
215+ continue
216+ }
217+ cache.persisted[chunk] = ref
218+ if entry, ok := cache.byPtr[chunk]; ok {
219+ delete(cache.byPtr, chunk)
220+ cache.used -= len(entry.object.Data)
221+ if cache.used < 0 {
222+ cache.used = 0
223+ }
216224 }
217225 }
218226 }
@@ -211,8 +211,16 @@ func markSnapshotCaptureObjectsPublished(capture *snapshotCapture) {
211 return211 return
212 }212 }
213 for chunk, ref := range capture.sealedRefs {213 for chunk, ref := range capture.sealedRefs {
214- if chunk != nil {214+ if chunk == nil {
215- cache.persisted[chunk] = ref215+ continue
216+ }
217+ cache.persisted[chunk] = ref
218+ if entry, ok := cache.byPtr[chunk]; ok {
219+ delete(cache.byPtr, chunk)
220+ cache.used -= len(entry.object.Data)
221+ if cache.used < 0 {
222+ cache.used = 0
223+ }
216 }224 }
217 }225 }
218 }226 }
M internal/usecase/daemon/snapshot_incremental_test.go +33 -0
@@ -139,6 +139,39 @@ func TestSnapshotChunkCacheResistsWarmedOverCapacityHistoryScans(t *testing.T) {
139139 require.Equal(t, 1, isolatedEncodes, "a session cache must not share encoded history with another session")
140140 }
141141
142+func TestMarkSnapshotCaptureObjectsPublishedReleasesCachedEncoding(t *testing.T) {
143+ history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
144+ appendHistoryRow(t, history, []renderer.Cell{{Rune: 'x'}})
145+ view := history.SnapshotView()
146+ chunk := view.Chunk(0)
147+ transcript := vt.NewScreen(1, 1).RecoveryTranscriptSnapshot()
148+ d := New(nil, nil, nil)
149+ sess := newSnapshotTestSession(t, "work", false, "/work")
150+ capture := &snapshotCapture{session: sess, name: "work", incarnation: domain.IncarnationID{1}, generation: 1, tabs: []snapshotCaptureTab{{stableID: "t", cols: 1, rows: 1, panes: []snapshotCapturePane{{id: "p", stableID: "p", sealed: view, tail: view.Tail(), transcript: transcript}}}}}
151+
152+ _, err := d.incrementalPublication(capture)
153+ require.NoError(t, err)
154+ entry, ok := sess.snapshotChunkCache.byPtr[chunk]
155+ require.True(t, ok)
156+ usedBefore := sess.snapshotChunkCache.used
157+ ref := capture.sealedRefs[chunk]
158+
159+ markSnapshotCaptureObjectsPublished(capture)
160+
161+ require.NotContains(t, sess.snapshotChunkCache.byPtr, chunk)
162+ require.Equal(t, usedBefore-len(entry.object.Data), sess.snapshotChunkCache.used)
163+ require.Equal(t, ref, sess.snapshotChunkCache.persisted[chunk])
164+ gotRef, object, err := sess.snapshotChunkCache.objectLocked(chunk)
165+ require.NoError(t, err)
166+ require.Equal(t, ref, gotRef)
167+ require.Nil(t, object)
168+
169+ // Repeated acknowledgement must not release the same bytes twice.
170+ markSnapshotCaptureObjectsPublished(capture)
171+ require.Equal(t, usedBefore-len(entry.object.Data), sess.snapshotChunkCache.used)
172+ require.Equal(t, ref, sess.snapshotChunkCache.persisted[chunk])
173+}
174+
142175 func TestIncrementalPublicationReusesSealedChunkObject(t *testing.T) {
143176 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
144177 appendHistoryRow(t, history, []renderer.Cell{renderer.BlankCell()})
@@ -139,6 +139,39 @@ func TestSnapshotChunkCacheResistsWarmedOverCapacityHistoryScans(t *testing.T) {
139 require.Equal(t, 1, isolatedEncodes, "a session cache must not share encoded history with another session")139 require.Equal(t, 1, isolatedEncodes, "a session cache must not share encoded history with another session")
140 }140 }
141 141
142+func TestMarkSnapshotCaptureObjectsPublishedReleasesCachedEncoding(t *testing.T) {
143+ history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
144+ appendHistoryRow(t, history, []renderer.Cell{{Rune: 'x'}})
145+ view := history.SnapshotView()
146+ chunk := view.Chunk(0)
147+ transcript := vt.NewScreen(1, 1).RecoveryTranscriptSnapshot()
148+ d := New(nil, nil, nil)
149+ sess := newSnapshotTestSession(t, "work", false, "/work")
150+ capture := &snapshotCapture{session: sess, name: "work", incarnation: domain.IncarnationID{1}, generation: 1, tabs: []snapshotCaptureTab{{stableID: "t", cols: 1, rows: 1, panes: []snapshotCapturePane{{id: "p", stableID: "p", sealed: view, tail: view.Tail(), transcript: transcript}}}}}
151+
152+ _, err := d.incrementalPublication(capture)
153+ require.NoError(t, err)
154+ entry, ok := sess.snapshotChunkCache.byPtr[chunk]
155+ require.True(t, ok)
156+ usedBefore := sess.snapshotChunkCache.used
157+ ref := capture.sealedRefs[chunk]
158+
159+ markSnapshotCaptureObjectsPublished(capture)
160+
161+ require.NotContains(t, sess.snapshotChunkCache.byPtr, chunk)
162+ require.Equal(t, usedBefore-len(entry.object.Data), sess.snapshotChunkCache.used)
163+ require.Equal(t, ref, sess.snapshotChunkCache.persisted[chunk])
164+ gotRef, object, err := sess.snapshotChunkCache.objectLocked(chunk)
165+ require.NoError(t, err)
166+ require.Equal(t, ref, gotRef)
167+ require.Nil(t, object)
168+
169+ // Repeated acknowledgement must not release the same bytes twice.
170+ markSnapshotCaptureObjectsPublished(capture)
171+ require.Equal(t, usedBefore-len(entry.object.Data), sess.snapshotChunkCache.used)
172+ require.Equal(t, ref, sess.snapshotChunkCache.persisted[chunk])
173+}
174+
142 func TestIncrementalPublicationReusesSealedChunkObject(t *testing.T) {175 func TestIncrementalPublicationReusesSealedChunkObject(t *testing.T) {
143 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})176 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
144 appendHistoryRow(t, history, []renderer.Cell{renderer.BlankCell()})177 appendHistoryRow(t, history, []renderer.Cell{renderer.BlankCell()})

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 12

Tags 11

↑↓ moveenter openesc close