@@ -10,25 +10,42 @@ import (
1010)
1111
1212// Mapping is the compact in-memory representation of a Header's mapping list.
13- // A merged Header is cached for up to 25h, so on snapshot-heavy nodes these
14- // slices dominate host RAM. It shrinks each entry from 40 bytes (a BuildMap) to
15- // 14 by encoding offset/length/storage as uint32 block indices, deduplicating
16- // BuildId into a per-header table addressed by uint16, and storing the columns
17- // as parallel slices. Immutable; read via At / All / Slice.
13+ // A merged Header is cached for hours, so on snapshot-heavy nodes these slices
14+ // dominate host RAM. It shrinks each entry from 40 bytes (a BuildMap) to 9 or
15+ // 10 by encoding offset/storage as uint32 block indices, deduplicating BuildId
16+ // into a per-header table addressed by one byte (two once a header references
17+ // more than 255 builds), storing the columns as parallel slices, and deriving
18+ // each entry's length from where the next one starts. Immutable; read via
19+ // At / All / Slice.
1820type Mapping struct {
1921 blockSize uint64
2022 builds []uuid.UUID
2123 offsets []uint32
22- lengths []uint32
23- storage []uint32
24- buildIdx []uint16
24+ // lengths is nil when every entry starts where the previous one ends: the
25+ // length is then offsets[i+1]-offsets[i], and endBlocks-offsets[i] for the
26+ // last entry. Only a mapping with gaps or overlaps between entries (legacy
27+ // headers predating NormalizeFixVersion) stores the column explicitly.
28+ lengths []uint32
29+ storage []uint32
30+ // Exactly one build-index column is set for a non-empty mapping: buildIdx8
31+ // while the header references at most maxBuilds8 builds, buildIdx16
32+ // otherwise. Each addresses builds, with its own sentinel for an empty
33+ // (zero) region.
34+ buildIdx8 []uint8
35+ buildIdx16 []uint16
36+ // endBlocks is the block at which the last entry ends.
37+ endBlocks uint64
2538}
2639
2740const (
28- nilBuildIdx = math .MaxUint16
29-
30- // maxBuildsPerHeader leaves nilBuildIdx reserved for empty regions.
31- maxBuildsPerHeader = nilBuildIdx
41+ nilBuildIdx8 = math .MaxUint8
42+ nilBuildIdx16 = math .MaxUint16
43+
44+ // maxBuilds8 is how many builds the one-byte index column can address,
45+ // with nilBuildIdx8 reserved for empty regions.
46+ maxBuilds8 = nilBuildIdx8
47+ // maxBuildsPerHeader leaves nilBuildIdx16 reserved for empty regions.
48+ maxBuildsPerHeader = nilBuildIdx16
3249)
3350
3451// maxBlockIdx is the largest block index representable by the uint32 columns.
@@ -50,10 +67,13 @@ func NewMapping(blockSize uint64, src []BuildMap) (Mapping, error) {
5067 idxByBuild := make (map [uuid.UUID ]uint16 , 8 )
5168 builds := make ([]uuid.UUID , 0 , 8 )
5269 offsets := make ([]uint32 , len (src ))
53- lengths := make ([]uint32 , len (src ))
5470 storage := make ([]uint32 , len (src ))
71+ // Wide scratch column; narrowed to one byte per entry once the build count
72+ // is known (see setBuildIdx).
5573 buildIdx := make ([]uint16 , len (src ))
5674
75+ contiguous := true
76+ var prevEnd uint64
5777 for i , m := range src {
5878 if m .Offset % blockSize != 0 {
5979 return Mapping {}, fmt .Errorf ("compact mapping: offset %d at index %d not block-aligned to %d" , m .Offset , i , blockSize )
@@ -71,8 +91,12 @@ func NewMapping(blockSize uint64, src []BuildMap) (Mapping, error) {
7191 if offBlocks > maxBlockIdx || lenBlocks > maxBlockIdx || stoBlocks > maxBlockIdx {
7292 return Mapping {}, fmt .Errorf ("compact mapping: block index out of uint32 range at entry %d" , i )
7393 }
94+ if i > 0 && offBlocks != prevEnd {
95+ contiguous = false
96+ }
97+ prevEnd = offBlocks + lenBlocks
7498
75- idx := uint16 (nilBuildIdx )
99+ idx := uint16 (nilBuildIdx16 )
76100 if m .BuildId != uuid .Nil {
77101 var ok bool
78102 idx , ok = idxByBuild [m .BuildId ]
@@ -87,46 +111,98 @@ func NewMapping(blockSize uint64, src []BuildMap) (Mapping, error) {
87111 }
88112
89113 offsets [i ] = uint32 (offBlocks )
90- lengths [i ] = uint32 (lenBlocks )
91114 storage [i ] = uint32 (stoBlocks )
92115 buildIdx [i ] = idx
93116 }
94117
95- return Mapping {
118+ out := Mapping {
96119 blockSize : blockSize ,
97120 builds : builds ,
98121 offsets : offsets ,
99- lengths : lengths ,
100122 storage : storage ,
101- buildIdx : buildIdx ,
102- }, nil
123+ endBlocks : prevEnd ,
124+ }
125+ out .buildIdx8 , out .buildIdx16 = buildIdxColumns (len (builds ), buildIdx )
126+ if ! contiguous {
127+ out .lengths = make ([]uint32 , len (src ))
128+ for i , m := range src {
129+ out .lengths [i ] = uint32 (m .Length / blockSize )
130+ }
131+ }
132+
133+ return out , nil
103134}
104135
105- // newMappingFromColumns builds a Mapping from already-decoded columns,
106- // avoiding the []BuildMap intermediate on the deserialize path. All column
107- // slices must have the same length, and every buildIdx must index builds.
108- func newMappingFromColumns (blockSize uint64 , builds []uuid.UUID , offsets , lengths , storage []uint32 , buildIdx []uint16 ) (Mapping , error ) {
136+ // buildIdxColumns chooses the retained build-index column from its wide form:
137+ // one byte per entry when every build fits, which every memfile header and
138+ // nearly every rootfs header does, otherwise the wide column as is. Exactly
139+ // one of the returned columns is non-nil.
140+ func buildIdxColumns (nBuilds int , wide []uint16 ) ([]uint8 , []uint16 ) {
141+ if nBuilds > maxBuilds8 {
142+ return nil , wide
143+ }
144+
145+ narrow := make ([]uint8 , len (wide ))
146+ for i , idx := range wide {
147+ if idx == nilBuildIdx16 {
148+ narrow [i ] = nilBuildIdx8
149+ } else {
150+ narrow [i ] = uint8 (idx )
151+ }
152+ }
153+
154+ return narrow , nil
155+ }
156+
157+ // buildIndex returns the i-th entry's index into builds, or -1 for an empty
158+ // region.
159+ func (m Mapping ) buildIndex (i int ) int {
160+ if m .buildIdx8 != nil {
161+ if idx := m .buildIdx8 [i ]; idx != nilBuildIdx8 {
162+ return int (idx )
163+ }
164+
165+ return - 1
166+ }
167+ if idx := m .buildIdx16 [i ]; idx != nilBuildIdx16 {
168+ return int (idx )
169+ }
170+
171+ return - 1
172+ }
173+
174+ // newMappingFromColumns builds a Mapping from already-decoded columns, avoiding
175+ // the []BuildMap intermediate on the deserialize path. The entries must be
176+ // contiguous, with the last one ending at endBlocks, so no lengths column is
177+ // stored. All column slices must have the same length, and every buildIdx must
178+ // index builds.
179+ func newMappingFromColumns (blockSize uint64 , builds []uuid.UUID , offsets , storage []uint32 , buildIdx []uint16 , endBlocks uint64 ) (Mapping , error ) {
109180 n := len (offsets )
110- if len (lengths ) != n || len (storage ) != n || len (buildIdx ) != n {
111- return Mapping {}, fmt .Errorf ("compact mapping: column length mismatch (offsets=%d lengths=%d storage=%d buildIdx=%d)" , n , len (lengths ), len (storage ), len (buildIdx ))
181+ if len (storage ) != n || len (buildIdx ) != n {
182+ return Mapping {}, fmt .Errorf ("compact mapping: column length mismatch (offsets=%d storage=%d buildIdx=%d)" , n , len (storage ), len (buildIdx ))
183+ }
184+ if n > 0 && uint64 (offsets [n - 1 ]) > endBlocks {
185+ return Mapping {}, fmt .Errorf ("compact mapping: last offset block %d beyond end block %d" , offsets [n - 1 ], endBlocks )
112186 }
113187 for i , bi := range buildIdx {
114- if bi == nilBuildIdx {
188+ if bi == nilBuildIdx16 {
115189 continue
116190 }
117191 if int (bi ) >= len (builds ) {
118192 return Mapping {}, fmt .Errorf ("compact mapping: buildIdx %d at entry %d out of range (%d builds)" , bi , i , len (builds ))
119193 }
120194 }
121195
122- return Mapping {
196+ out := Mapping {
123197 blockSize : blockSize ,
124198 builds : builds ,
125199 offsets : offsets ,
126- lengths : lengths ,
127200 storage : storage ,
128- buildIdx : buildIdx ,
129- }, nil
201+ endBlocks : endBlocks ,
202+ }
203+ out .buildIdx8 , out .buildIdx16 = buildIdxColumns (len (builds ), buildIdx )
204+
205+ return out , nil
130206}
131207
132208// Len returns the number of entries.
@@ -137,11 +213,19 @@ func (m Mapping) BlockSize() uint64 { return m.blockSize }
137213
138214// ByteSize returns the approximate heap footprint of the mapping's columns, so
139215// callers can gauge how much RAM cached headers hold without knowing the
140- // encoding. It counts the four parallel columns (4+4+4+2 bytes per entry) plus
141- // the deduplicated build table (16 bytes per UUID); it excludes the struct
142- // header itself and any slice capacity beyond len.
216+ // encoding. It counts the per-entry columns — 9 bytes for the contiguous form,
217+ // 4 more where lengths are stored explicitly and one more where the header
218+ // needs the wide build index — plus the deduplicated build table (16 bytes per
219+ // UUID); it excludes the struct header itself and any slice capacity beyond
220+ // len.
143221func (m Mapping ) ByteSize () int {
144- const bytesPerEntry = 4 + 4 + 4 + 2 // offsets, lengths, storage, buildIdx
222+ bytesPerEntry := 4 + 4 + 1 // offsets, storage, build index
223+ if m .lengths != nil {
224+ bytesPerEntry += 4
225+ }
226+ if m .buildIdx16 != nil {
227+ bytesPerEntry ++
228+ }
145229
146230 return len (m .offsets )* bytesPerEntry + len (m .builds )* 16
147231}
@@ -164,17 +248,34 @@ func (m Mapping) SharesStorageWith(other Mapping) bool {
164248// returned slice is shared with the Mapping; callers must not mutate it.
165249func (m Mapping ) Builds () []uuid.UUID { return m .builds }
166250
251+ // Contiguous reports whether every entry starts where the previous one ends.
252+ // Such a mapping derives its lengths from the offsets column instead of
253+ // storing them.
254+ func (m Mapping ) Contiguous () bool { return m .lengths == nil }
255+
256+ // lengthBlocks returns the i-th entry's length in blocks.
257+ func (m Mapping ) lengthBlocks (i int ) uint32 {
258+ if m .lengths != nil {
259+ return m .lengths [i ]
260+ }
261+ if i + 1 < len (m .offsets ) {
262+ return m .offsets [i + 1 ] - m .offsets [i ]
263+ }
264+
265+ return uint32 (m .endBlocks - uint64 (m .offsets [i ]))
266+ }
267+
167268// At materializes the i-th entry as a BuildMap. Panics if i is out of range,
168269// matching `mapping[i]` semantics.
169270func (m Mapping ) At (i int ) BuildMap {
170271 buildID := uuid .Nil
171- if m . buildIdx [ i ] != nilBuildIdx {
172- buildID = m.builds [m. buildIdx [ i ] ]
272+ if bi := m . buildIndex ( i ); bi >= 0 {
273+ buildID = m .builds [bi ]
173274 }
174275
175276 return BuildMap {
176277 Offset : uint64 (m .offsets [i ]) * m .blockSize ,
177- Length : uint64 (m .lengths [ i ] ) * m .blockSize ,
278+ Length : uint64 (m .lengthBlocks ( i ) ) * m .blockSize ,
178279 BuildId : buildID ,
179280 BuildStorageOffset : uint64 (m .storage [i ]) * m .blockSize ,
180281 }
@@ -193,15 +294,15 @@ func (m Mapping) All() iter.Seq2[int, BuildMap] {
193294}
194295
195296// BytesByBuild sums the bytes attributed to each referenced build. It scans the
196- // length and buildIdx columns directly (no BuildMap materialization or per-entry
197- // uuid hashing), accumulating into a small per-build slice, so it stays cheap
198- // even for mappings with millions of entries. Empty (nil-build) regions are
199- // skipped. The returned map is non-nil and addressable by the caller.
297+ // columns directly (no BuildMap materialization or per-entry uuid hashing),
298+ // accumulating into a small per-build slice, so it stays cheap even for
299+ // mappings with millions of entries. Empty (nil-build) regions are skipped.
300+ // The returned map is non-nil and addressable by the caller.
200301func (m Mapping ) BytesByBuild () map [uuid.UUID ]uint64 {
201302 sums := make ([]uint64 , len (m .builds ))
202- for i , bi := range m .buildIdx {
203- if bi != nilBuildIdx {
204- sums [bi ] += uint64 (m .lengths [ i ] )
303+ for i := range m .offsets {
304+ if bi := m . buildIndex ( i ); bi >= 0 {
305+ sums [bi ] += uint64 (m .lengthBlocks ( i ) )
205306 }
206307 }
207308
@@ -242,7 +343,7 @@ func (m Mapping) Validate(size, blockSize uint64) error {
242343 var currentOffset uint64
243344 for i := range m .offsets {
244345 offset := uint64 (m .offsets [i ]) * m .blockSize
245- length := uint64 (m .lengths [ i ] ) * m .blockSize
346+ length := uint64 (m .lengthBlocks ( i ) ) * m .blockSize
246347 if currentOffset != offset {
247348 return fmt .Errorf ("mapping validation failed at index %d: expected offset %d (block %d), got %d (block %d)" , i , currentOffset , currentOffset / blockSize , offset , offset / blockSize )
248349 }
0 commit comments