// Package accum provides self-mutating holders for long-lived lists that are // filled and then cleared repeatedly. // // A bare package-level slice is the one growth case nothing reclaims: push // grows it in whatever arena is current, and at a wasm dispatch entry - which // is where a relay callback runs - that is the root arena. Nothing reclaims a // root-arena allocation: arenas release whole frames, and a package-level slice // lives for the process, so every backing array the growth regime abandons is // permanent. A list that is filled and then cleared once per render or once per // batch therefore leaks the sum of its growth steps on every cycle. // // A holder keeps its elements in fixed-size chunks allocated by its own // mutating methods. Those methods make the holder a self-mutating type, so the // chunks land in the holder's own sovereign arena, and the holder stores only a // directory of them. Clearing drops the directory and leaves the chunk payloads // unreachable, so they become dead records that the compaction drain reclaims // (runtime.SovDrainCompactions, called at the wasm message-dispatch boundary) // instead of abandoned copies of the whole list. // // The per-chunk capacity is fixed so a chunk is allocated once at a known size // and never enters the growth regime. Growing the chunk directory is amortised, // and its abandoned backings are reclaimable the same way; no allocation scales // with the total element count. package accum // ChunkSize is the fixed element count per chunk. Nothing depends on the value // beyond the tradeoff between directory churn and per-chunk slack. const ChunkSize = 64 // Ints accumulates int32 values: callback ids, small counters. type Ints struct { chunks [][]int32 n int32 } // Strings accumulates strings: ids, URLs, pubkeys. type Strings struct { chunks [][]string n int32 } // Len returns the live element count. func (h *Ints) Len() (n int32) { return h.n } // At returns element i, oldest first, for 0 <= i < Len(). func (h *Ints) At(i int32) (v int32) { return h.chunks[i/ChunkSize][i%ChunkSize] } // Push appends v. The chunk payload is allocated by this method, so it is // recorded in the holder's sovereign arena. func (h *Ints) Push(v int32) { last := int32(len(h.chunks)) - 1 if last < 0 || len(h.chunks[last]) == ChunkSize { h.chunks = push(h.chunks, []int32{:0:ChunkSize}) last = int32(len(h.chunks)) - 1 } h.chunks[last] = push(h.chunks[last], v) h.n++ } // Reset drops every element. The chunk payloads become unreachable and are // reclaimed by the next compaction. func (h *Ints) Reset() { h.chunks = h.chunks[:0] h.n = 0 } // Len returns the live element count. func (h *Strings) Len() (n int32) { return h.n } // At returns element i, oldest first, for 0 <= i < Len(). func (h *Strings) At(i int32) (v string) { return h.chunks[i/ChunkSize][i%ChunkSize] } // Push appends v. The chunk payload is allocated by this method, so it is // recorded in the holder's sovereign arena. func (h *Strings) Push(v string) { last := int32(len(h.chunks)) - 1 if last < 0 || len(h.chunks[last]) == ChunkSize { h.chunks = push(h.chunks, []string{:0:ChunkSize}) last = int32(len(h.chunks)) - 1 } h.chunks[last] = push(h.chunks[last], v) h.n++ } // Reset drops every element. The chunk payloads become unreachable and are // reclaimed by the next compaction. func (h *Strings) Reset() { h.chunks = h.chunks[:0] h.n = 0 } // Set replaces the contents with a copy of src. The elements are copied into // the holder's chunks, so the list's storage belongs to the holder's arena and // a src built in a frame can die with that frame. Nothing is allocated after // the first call: Reset keeps the directory and its chunks, and Push refills // the existing chunk slots. func (h *Strings) Set(src []string) { h.Reset() for i := int32(0); i < int32(len(src)); i++ { h.Push(src[i]) } } // Slice copies the elements into a fresh slice, for call sites that need one // (a helper that takes []string). The copy is written through a cursor into an // exactly-sized slice, so it never enters the growth regime, and it is // allocated in the caller's arena - a Slice() result passed straight to a call // dies when that call's frame does. func (h *Strings) Slice() (out []string) { // []T{:n} is length n, capacity n; the {:0:n} form is length 0 and would // return nothing after the cursor writes below. out = []string{:h.n} for i := int32(0); i < h.n; i++ { out[i] = h.At(i) } return }