fix(snapshot): release published chunk encodings
- commit
- 2dca0c2bb4acbae8f9c5a83a3d852ee4e39f6d50
- author
- brice · · 2026-07-27 12:46 UTC
- parent
- 790011a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 211 | 211 | return | |
| 212 | 212 | } | |
| 213 | 213 | 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 | + | } | |
| 216 | 224 | } | |
| 217 | 225 | } | |
| 218 | 226 | } | |
| @@ -211,8 +211,16 @@ func markSnapshotCaptureObjectsPublished(capture *snapshotCapture) { | |||||
| 211 | return | 211 | 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] = ref | 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 | + | } | |||
| 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) { | |||
| 139 | 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 | 175 | func TestIncrementalPublicationReusesSealedChunkObject(t *testing.T) { | |
| 143 | 176 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1}) | |
| 144 | 177 | 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()}) | ||