Skip to content

Commit e67b60e

Browse files
authored
Bound arena retention by bytes instead of object count (#64802)
1 parent 158a080 commit e67b60e

2 files changed

Lines changed: 115 additions & 8 deletions

File tree

‎tsc/internal/core/arena.go‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
package core
22

3-
import "slices"
3+
import (
4+
"slices"
5+
"unsafe"
6+
)
47

58
// Arena allocator
69

710
type Arena[T any] struct {
811
data []T
912
}
1013

11-
// Allocate a single element in the arena and return a pointer to the element. If the arena is at capacity,
12-
// a new arena of the next size up is allocated.
14+
// Allocate a single element at a stable address. Small chunks bound the unused
15+
// capacity and unrelated values retained by a surviving pointer into a chunk.
1316
func (a *Arena[T]) New() *T {
1417
if len(a.data) == cap(a.data) {
15-
nextSize := nextArenaSize(len(a.data))
18+
nextSize := nextArenaSize[T](len(a.data))
1619
// Use the same trick as slices.Concat; Grow rounds up to the next size class.
1720
a.data = slices.Grow[[]T](nil, nextSize)
1821
}
@@ -30,7 +33,7 @@ func (a *Arena[T]) NewSlice(size int) []T {
3033
return nil
3134
}
3235
if len(a.data)+size > cap(a.data) {
33-
nextSize := nextArenaSize(len(a.data))
36+
nextSize := nextArenaSize[T](len(a.data))
3437
if size > nextSize {
3538
return make([]T, size)
3639
}
@@ -58,9 +61,11 @@ func (a *Arena[T]) Clone(t []T) []T {
5861
return slice
5962
}
6063

61-
func nextArenaSize(size int) int {
62-
// This compiles down branch-free.
64+
func nextArenaSize[T any](size int) int {
65+
// Target 512 bytes before allocator size-class rounding, with at least one
66+
// value. A surviving interior pointer retains and scans the whole chunk.
67+
maxSize := max(512/max(int(unsafe.Sizeof(*new(T))), 1), 1)
6368
size = max(size, 1)
64-
size = min(size*2, 256)
69+
size = min(size*2, 256, maxSize)
6570
return size
6671
}

‎tsc/internal/core/arena_test.go‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package core_test
2+
3+
import (
4+
"runtime"
5+
"strconv"
6+
"testing"
7+
8+
"github.com/microsoft/TypeScript/tsc/internal/core"
9+
"gotest.tools/v3/assert"
10+
)
11+
12+
func TestArenaBoundsSingleValueRetention(t *testing.T) { //nolint:paralleltest
13+
type value struct {
14+
pointer *int
15+
padding [65528]byte
16+
}
17+
runtime.GC()
18+
var before runtime.MemStats
19+
runtime.ReadMemStats(&before)
20+
var arena core.Arena[value]
21+
var retained *value
22+
for i := range 300 {
23+
retained = arena.New()
24+
retained.pointer = new(i)
25+
}
26+
arena = core.Arena[value]{}
27+
runtime.GC()
28+
runtime.GC()
29+
var after runtime.MemStats
30+
runtime.ReadMemStats(&after)
31+
assert.Equal(t, *retained.pointer, 299)
32+
assert.Assert(t, after.HeapAlloc < before.HeapAlloc+1024*1024, "one live value retained more than 1 MiB")
33+
runtime.KeepAlive(retained)
34+
}
35+
36+
func TestArenaMixedSingleValuesAndSlices(t *testing.T) {
37+
t.Parallel()
38+
type value struct {
39+
number int
40+
text *string
41+
}
42+
var arena core.Arena[value]
43+
assert.Assert(t, arena.NewSlice(0) == nil)
44+
first := arena.New()
45+
first.number, first.text = 1, new("first")
46+
small := arena.NewSlice1(value{number: 3, text: new("small")})
47+
copyOfSmall := arena.Clone(small)
48+
small[0].number = 4
49+
slices := make([][]value, 0, 7)
50+
for _, size := range []int{1, 2, 31, 32, 33, 64, 100} {
51+
slice := arena.NewSlice(size)
52+
assert.Equal(t, len(slice), size)
53+
assert.Equal(t, cap(slice), size)
54+
for i := range slice {
55+
assert.Equal(t, slice[i], value{})
56+
slice[i] = value{number: i + size, text: new(strconv.Itoa(size))}
57+
}
58+
slices = append(slices, slice)
59+
assert.Equal(t, *arena.New(), value{})
60+
}
61+
values := arena.NewSlice(100)
62+
assert.Equal(t, len(values), 100)
63+
assert.Equal(t, cap(values), 100)
64+
for i := range values {
65+
values[i].number = i
66+
}
67+
last := arena.New()
68+
last.number, last.text = 2, new("last")
69+
for range 1024 {
70+
arena.New()
71+
}
72+
arena = core.Arena[value]{}
73+
runtime.GC()
74+
assert.Equal(t, first.number, 1)
75+
assert.Equal(t, *first.text, "first")
76+
assert.Equal(t, last.number, 2)
77+
assert.Equal(t, *last.text, "last")
78+
assert.Equal(t, small[0].number, 4)
79+
assert.Equal(t, *small[0].text, "small")
80+
assert.Equal(t, copyOfSmall[0].number, 3)
81+
assert.Equal(t, *copyOfSmall[0].text, "small")
82+
for _, slice := range slices {
83+
for i := range slice {
84+
assert.Equal(t, slice[i].number, i+len(slice))
85+
assert.Equal(t, *slice[i].text, strconv.Itoa(len(slice)))
86+
}
87+
}
88+
for i := range values {
89+
assert.Equal(t, values[i].number, i)
90+
}
91+
}
92+
93+
func TestArenaZeroSizedValues(t *testing.T) {
94+
t.Parallel()
95+
var arena core.Arena[struct{}]
96+
for range 1024 {
97+
assert.Assert(t, arena.New() != nil)
98+
}
99+
values := arena.NewSlice(300)
100+
assert.Equal(t, len(values), 300)
101+
assert.Equal(t, cap(values), 300)
102+
}

0 commit comments

Comments
 (0)