diff --git a/tsc/internal/core/arena.go b/tsc/internal/core/arena.go index bbe80246413..ed795b7eca0 100644 --- a/tsc/internal/core/arena.go +++ b/tsc/internal/core/arena.go @@ -1,6 +1,7 @@ package core import ( + "math/bits" "slices" "unsafe" ) @@ -69,3 +70,41 @@ func nextArenaSize[T any](size int) int { size = min(size*2, 256, maxSize) return size } + +// IndexedArena keeps values at stable addresses and references them with uint32 +// indexes instead of Go pointers. Index zero is nil. Unlike Arena, it retains all +// blocks so that an index alone keeps its value reachable through the arena. +type IndexedArena[T any] struct { + blocks [][]T + size uint32 +} + +const indexedArenaPageShift = 8 + +func (a *IndexedArena[T]) New() (uint32, *T) { + if a.size == ^uint32(0) { + panic("IndexedArena exhausted") + } + if len(a.blocks) == 0 || len(a.blocks[len(a.blocks)-1]) == cap(a.blocks[len(a.blocks)-1]) { + size := 1 << min(len(a.blocks), indexedArenaPageShift) + a.blocks = append(a.blocks, make([]T, 0, size)) + } + block := &a.blocks[len(a.blocks)-1] + *block = (*block)[:len(*block)+1] + a.size++ + return a.size, &(*block)[len(*block)-1] +} + +func (a *IndexedArena[T]) Get(index uint32) *T { + if index == 0 { + return nil + } + // The initial blocks have sizes 1, 2, 4, ..., 128. Subsequent + // blocks have 256 entries, avoiding geometric over-allocation. + if index < 1<>indexedArenaPageShift)][index&((1<> LinkPageShift if int(pageIndex) < len(s.pages) { if page := s.pages[pageIndex]; page != nil { - return page[key&LinkPageMask] + return s.arena.Get(page[key&LinkPageMask]) } } return nil diff --git a/tsc/internal/core/linkstore_test.go b/tsc/internal/core/linkstore_test.go new file mode 100644 index 00000000000..8b0512bf885 --- /dev/null +++ b/tsc/internal/core/linkstore_test.go @@ -0,0 +1,145 @@ +package core_test + +import ( + "fmt" + "runtime" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/core" + "gotest.tools/v3/assert" +) + +func TestLinkStore(t *testing.T) { + t.Parallel() + type value struct { + number int + text *string + } + var store core.LinkStore[int, value] + assert.Assert(t, !store.Has(0)) + assert.Assert(t, store.TryGet(0) == nil) + + values := make([]*value, 4096) + for i := range values { + v := store.Get(i) + assert.Equal(t, *v, value{}) + v.number = i + v.text = new("retained") + values[i] = v + } + runtime.GC() + for i, v := range values { + assert.Assert(t, store.Has(i)) + assert.Equal(t, store.Get(i), v) + assert.Equal(t, store.TryGet(i), v) + assert.Equal(t, v.number, i) + assert.Equal(t, *v.text, "retained") + } + assert.Assert(t, !store.Has(len(values))) + assert.Assert(t, store.TryGet(len(values)) == nil) +} + +func TestPagedLinkStore(t *testing.T) { + t.Parallel() + var store core.PagedLinkStore[int] + keys := []uint64{0, 255, 256, 65535, 16777215} + values := make([]*int, len(keys)) + for _, key := range keys { + assert.Assert(t, !store.Has(key)) + } + for i, key := range keys { + v := store.Get(key) + assert.Equal(t, *v, 0) + *v = i + 1 + values[i] = v + } + runtime.GC() + for i, key := range keys { + assert.Assert(t, store.Has(key)) + assert.Equal(t, store.Get(key), values[i]) + assert.Equal(t, *store.TryGet(key), i+1) + } + assert.Assert(t, !store.Has(1)) + assert.Assert(t, store.TryGet(1) == nil) +} + +func BenchmarkLinkStore(b *testing.B) { + type value struct { + typ *int + flags uint32 + } + b.ReportAllocs() + for b.Loop() { + var store core.LinkStore[int, value] + for i := range 10000 { + store.Get(i).flags = uint32(i) + } + for i := range 10000 { + if store.Get(i).flags != uint32(i) { + b.Fatal("link changed during arena growth") + } + } + + } +} + +func TestIndexedArena(t *testing.T) { + t.Parallel() + type value struct { + number int + text *string + } + var arena core.IndexedArena[value] + assert.Assert(t, arena.Get(0) == nil) + values := make([]*value, 4096) + for i := range values { + index, v := arena.New() + assert.Equal(t, index, uint32(i+1)) + assert.Equal(t, *v, value{}) + v.number = i + v.text = new("retained") + values[i] = v + } + runtime.GC() + for i, v := range values { + assert.Equal(t, arena.Get(uint32(i+1)), v) + assert.Equal(t, v.number, i) + assert.Equal(t, *v.text, "retained") + } +} + +func TestIndexedArenaReachability(t *testing.T) { + t.Parallel() + var arena core.IndexedArena[*string] + for i := range 4096 { + _, value := arena.New() + *value = new(fmt.Sprint(i)) + } + runtime.GC() + for i := range 4096 { + assert.Equal(t, **arena.Get(uint32(i + 1)), fmt.Sprint(i)) + } +} + +func BenchmarkPagedLinkStore(b *testing.B) { + for _, stride := range []uint64{1, 16, 256} { + b.Run(fmt.Sprint(stride), func(b *testing.B) { + type value struct { + typ *int + flags uint32 + } + b.ReportAllocs() + for b.Loop() { + var store core.PagedLinkStore[value] + for i := range 10000 { + store.Get(uint64(i) * stride).flags = uint32(i) + } + for i := range 10000 { + if store.Get(uint64(i)*stride).flags != uint32(i) { + b.Fatal("link changed during arena growth") + } + } + } + }) + } +}