1 // Package accum provides self-mutating holders for long-lived lists that are
2 // filled and then cleared repeatedly.
3 //
4 // A bare package-level slice is the one growth case nothing reclaims: push
5 // grows it in whatever arena is current, and at a wasm dispatch entry - which
6 // is where a relay callback runs - that is the root arena. Nothing reclaims a
7 // root-arena allocation: arenas release whole frames, and a package-level slice
8 // lives for the process, so every backing array the growth regime abandons is
9 // permanent. A list that is filled and then cleared once per render or once per
10 // batch therefore leaks the sum of its growth steps on every cycle.
11 //
12 // A holder keeps its elements in fixed-size chunks allocated by its own
13 // mutating methods. Those methods make the holder a self-mutating type, so the
14 // chunks land in the holder's own sovereign arena, and the holder stores only a
15 // directory of them. Clearing drops the directory and leaves the chunk payloads
16 // unreachable, so they become dead records that the compaction drain reclaims
17 // (runtime.SovDrainCompactions, called at the wasm message-dispatch boundary)
18 // instead of abandoned copies of the whole list.
19 //
20 // The per-chunk capacity is fixed so a chunk is allocated once at a known size
21 // and never enters the growth regime. Growing the chunk directory is amortised,
22 // and its abandoned backings are reclaimable the same way; no allocation scales
23 // with the total element count.
24 package accum
25 26 // ChunkSize is the fixed element count per chunk. Nothing depends on the value
27 // beyond the tradeoff between directory churn and per-chunk slack.
28 const ChunkSize = 64
29 30 // Ints accumulates int32 values: callback ids, small counters.
31 type Ints struct {
32 chunks [][]int32
33 n int32
34 }
35 36 // Strings accumulates strings: ids, URLs, pubkeys.
37 type Strings struct {
38 chunks [][]string
39 n int32
40 }
41 42 // Len returns the live element count.
43 func (h *Ints) Len() (n int32) { return h.n }
44 45 // At returns element i, oldest first, for 0 <= i < Len().
46 func (h *Ints) At(i int32) (v int32) { return h.chunks[i/ChunkSize][i%ChunkSize] }
47 48 // Push appends v. The chunk payload is allocated by this method, so it is
49 // recorded in the holder's sovereign arena.
50 func (h *Ints) Push(v int32) {
51 last := int32(len(h.chunks)) - 1
52 if last < 0 || len(h.chunks[last]) == ChunkSize {
53 h.chunks = push(h.chunks, []int32{:0:ChunkSize})
54 last = int32(len(h.chunks)) - 1
55 }
56 h.chunks[last] = push(h.chunks[last], v)
57 h.n++
58 }
59 60 // Reset drops every element. The chunk payloads become unreachable and are
61 // reclaimed by the next compaction.
62 func (h *Ints) Reset() {
63 h.chunks = h.chunks[:0]
64 h.n = 0
65 }
66 67 // Len returns the live element count.
68 func (h *Strings) Len() (n int32) { return h.n }
69 70 // At returns element i, oldest first, for 0 <= i < Len().
71 func (h *Strings) At(i int32) (v string) { return h.chunks[i/ChunkSize][i%ChunkSize] }
72 73 // Push appends v. The chunk payload is allocated by this method, so it is
74 // recorded in the holder's sovereign arena.
75 func (h *Strings) Push(v string) {
76 last := int32(len(h.chunks)) - 1
77 if last < 0 || len(h.chunks[last]) == ChunkSize {
78 h.chunks = push(h.chunks, []string{:0:ChunkSize})
79 last = int32(len(h.chunks)) - 1
80 }
81 h.chunks[last] = push(h.chunks[last], v)
82 h.n++
83 }
84 85 // Reset drops every element. The chunk payloads become unreachable and are
86 // reclaimed by the next compaction.
87 func (h *Strings) Reset() {
88 h.chunks = h.chunks[:0]
89 h.n = 0
90 }
91 92 // Set replaces the contents with a copy of src. The elements are copied into
93 // the holder's chunks, so the list's storage belongs to the holder's arena and
94 // a src built in a frame can die with that frame. Nothing is allocated after
95 // the first call: Reset keeps the directory and its chunks, and Push refills
96 // the existing chunk slots.
97 func (h *Strings) Set(src []string) {
98 h.Reset()
99 for i := int32(0); i < int32(len(src)); i++ {
100 h.Push(src[i])
101 }
102 }
103 104 // Slice copies the elements into a fresh slice, for call sites that need one
105 // (a helper that takes []string). The copy is written through a cursor into an
106 // exactly-sized slice, so it never enters the growth regime, and it is
107 // allocated in the caller's arena - a Slice() result passed straight to a call
108 // dies when that call's frame does.
109 func (h *Strings) Slice() (out []string) {
110 // []T{:n} is length n, capacity n; the {:0:n} form is length 0 and would
111 // return nothing after the cursor writes below.
112 out = []string{:h.n}
113 for i := int32(0); i < h.n; i++ {
114 out[i] = h.At(i)
115 }
116 return
117 }
118