Release unused file buffers for empty artifacts

This commit is contained in:
2026-10-09 01:52:06 +00:00
parent 389ef26db3
commit ea6c7d5f0f
5 changed files with 34 additions and 8 deletions

View File

@@ -131,9 +131,10 @@ fallback.
Regular files are read synchronously in bounded chunks. The opened file's size Regular files are read synchronously in bounded chunks. The opened file's size
is only an initial-capacity hint, capped at 1 MiB; reads continue to EOF, and is only an initial-capacity hint, capped at 1 MiB; reads continue to EOF, and
the returned body determines artifact size and hash. If an overreported hint the returned body determines artifact size and hash. If an overreported hint
leaves more than twice the actual body length unused, the completed body is leaves unused capacity greater than the actual body length, the completed body
copied to release the excess backing storage. Cancellation is checked before is copied to release the excess backing storage, including when the body is
opening, before and after every read, and before publishing the artifact, so a empty. Cancellation is checked before opening, before and after every read,
and before publishing the artifact, so a
canceled read never publishes partial content. The ordinary reader does not canceled read never publishes partial content. The ordinary reader does not
detach file reads into background goroutines. detach file reads into background goroutines.

View File

@@ -1,7 +1,6 @@
# Implementation Review Remediation # Implementation Review Remediation
Status: One computational-efficiency finding awaits remediation. Earlier Status: All recorded findings have been resolved in separate remediation units.
findings below remain resolved.
## Computational Efficiency Review ## Computational Efficiency Review
@@ -10,7 +9,16 @@ Review baseline: `8a5c0de`, compared with planning commit `3fde1f6`, the
### Finding 1 — Release A Stale File Reservation For An Empty Body (P3) ### Finding 1 — Release A Stale File Reservation For An Empty Body (P3)
**Status: Open.** **Status: Resolved.** Completed empty bodies now release stale file-size
reservations using the existing compaction path. The stale-size table covers
empty content, metadata, and hashes; the retained-artifact benchmark includes
an empty-body fixture without enforcing capacity or allocation thresholds.
Three offline, one-iteration samples for four retained empty artifacts changed
from 4,194,928–4,194,976 retained bytes to 624–640 bytes. Initial reservation
allocations still occur; this fix reduces retained storage, not cumulative
allocation. The nonempty benchmark remains near 1 MiB for four 256 KiB bodies.
Artifact package race tests and vet passed, and the internal documentation
now accurately describes the compaction condition.
Location: [file reader](../../internal/artifact/reader.go), lines 137–166. Location: [file reader](../../internal/artifact/reader.go), lines 137–166.

View File

@@ -158,7 +158,7 @@ func readFileArtifact(ctx context.Context, path string, open func(string) (artif
return nil, fmt.Errorf("failed to read file %s: %w", path, readErr) return nil, fmt.Errorf("failed to read file %s: %w", path, readErr)
} }
} }
if len(data) > 0 && cap(data)-len(data) > len(data) { if cap(data)-len(data) > len(data) {
// A stale size hint should not make a small completed artifact retain a // A stale size hint should not make a small completed artifact retain a
// disproportionately large backing array. // disproportionately large backing array.
compact := make([]byte, len(data)) compact := make([]byte, len(data))

View File

@@ -42,7 +42,20 @@ func BenchmarkArtifactFileRead(b *testing.B) {
// BenchmarkRetainedArtifactFileRead measures live artifact bodies when an // BenchmarkRetainedArtifactFileRead measures live artifact bodies when an
// opened file reports a size larger than the content that reaches EOF. // opened file reports a size larger than the content that reaches EOF.
func BenchmarkRetainedArtifactFileRead(b *testing.B) { func BenchmarkRetainedArtifactFileRead(b *testing.B) {
body := bytes.Repeat([]byte("x"), 256<<10) for _, fixture := range []struct {
name string
body []byte
}{
{name: "small", body: bytes.Repeat([]byte("x"), 256<<10)},
{name: "empty", body: []byte{}},
} {
b.Run(fixture.name, func(b *testing.B) {
benchmarkRetainedArtifactFileRead(b, fixture.body)
})
}
}
func benchmarkRetainedArtifactFileRead(b *testing.B, body []byte) {
path := filepath.Join(b.TempDir(), "overreported.txt") path := filepath.Join(b.TempDir(), "overreported.txt")
if err := os.WriteFile(path, body, 0o600); err != nil { if err := os.WriteFile(path, body, 0o600); err != nil {
b.Fatal(err) b.Fatal(err)

View File

@@ -241,6 +241,7 @@ func TestFileReaderUsesOpenedSizeOnlyAsACapacityHint(t *testing.T) {
reportedSize int64 reportedSize int64
}{ }{
{name: "overreported size", body: []byte("actual file content"), reportedSize: 128}, {name: "overreported size", body: []byte("actual file content"), reportedSize: 128},
{name: "overreported size with empty content", body: []byte{}, reportedSize: 16 << 20},
{name: "underreported size", body: bytes.Repeat([]byte("x"), fileReadChunkSize+17), reportedSize: 1}, {name: "underreported size", body: bytes.Repeat([]byte("x"), fileReadChunkSize+17), reportedSize: 1},
{name: "very large hint with small content", body: []byte("small actual content"), reportedSize: int64(^uint64(0) >> 1)}, {name: "very large hint with small content", body: []byte("small actual content"), reportedSize: int64(^uint64(0) >> 1)},
} }
@@ -280,6 +281,9 @@ func TestFileReaderUsesOpenedSizeOnlyAsACapacityHint(t *testing.T) {
if artifact.Size != int64(len(tc.body)) { if artifact.Size != int64(len(tc.body)) {
t.Fatalf("artifact size=%d, want %d", artifact.Size, len(tc.body)) t.Fatalf("artifact size=%d, want %d", artifact.Size, len(tc.body))
} }
if artifact.Name != filepath.Base(filePath) || artifact.URI != filePath || artifact.ContentType == "" {
t.Fatalf("unexpected artifact metadata: name=%q URI=%q content type=%q", artifact.Name, artifact.URI, artifact.ContentType)
}
wantHash := fmt.Sprintf("%x", sha256.Sum256(tc.body)) wantHash := fmt.Sprintf("%x", sha256.Sum256(tc.body))
if artifact.Hash != wantHash { if artifact.Hash != wantHash {
t.Fatalf("artifact hash=%q, want %q", artifact.Hash, wantHash) t.Fatalf("artifact hash=%q, want %q", artifact.Hash, wantHash)