accum.mx raw

   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