Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions tsc/internal/checker/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ func (s *nodeLinkStore[V]) Get(node *ast.Node) *V {
}

func (s *nodeLinkStore[V]) Has(node *ast.Node) bool {
return s.TryGet(node) != nil
id := uint64(s.gen.GetNodeId(node))
if id >= ast.BlockIdOffset && id < ast.BlockIdOffset+maxPageLinkCount {
return s.pages.Has(id - ast.BlockIdOffset)
}
return s.links.Has(id)
}

func (s *nodeLinkStore[V]) TryGet(node *ast.Node) *V {
Expand All @@ -54,7 +58,11 @@ func (s *symbolLinkStore[V]) Get(symbol *ast.Symbol) *V {
}

func (s *symbolLinkStore[V]) Has(symbol *ast.Symbol) bool {
return s.TryGet(symbol) != nil
id := uint64(s.gen.GetSymbolId(symbol))
if id >= ast.BlockIdOffset && id < ast.BlockIdOffset+maxPageLinkCount {
return s.pages.Has(id - ast.BlockIdOffset)
}
return s.links.Has(id)
}

func (s *symbolLinkStore[V]) TryGet(symbol *ast.Symbol) *V {
Expand Down
39 changes: 39 additions & 0 deletions tsc/internal/core/arena.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package core

import (
"math/bits"
"slices"
"unsafe"
)
Expand Down Expand Up @@ -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 {
block := bits.Len32(index) - 1
return &a.blocks[block][index-(1<<block)]
}
index -= 1 << indexedArenaPageShift
return &a.blocks[indexedArenaPageShift+int(index>>indexedArenaPageShift)][index&((1<<indexedArenaPageShift)-1)]
}
26 changes: 14 additions & 12 deletions tsc/internal/core/linkstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ const (
)

// PagedLinkStore implements a sparse-array-like structure for storing elements keyed by dense uint64 keys.
// Elements are allocated in an area, element references are stored in fixed-size pages of 256 entries, and
// an index of pages is maintained in a growable list.
// Elements are allocated in an arena, and compressed indexes are stored in fixed-size pages of 256 entries.
// The pages contain no Go pointers, halving their size on 64-bit hosts and avoiding GC scans of each entry.

type PagedLinkStore[V any] struct {
pages []*[LinkPageSize]*V
arena Arena[V]
pages []*[LinkPageSize]uint32
arena IndexedArena[V]
}

func (s *PagedLinkStore[V]) Get(key uint64) *V {
Expand All @@ -56,26 +56,28 @@ func (s *PagedLinkStore[V]) Get(key uint64) *V {
}
page := s.pages[pageIndex]
if page == nil {
page = new([LinkPageSize]*V)
page = new([LinkPageSize]uint32)
s.pages[pageIndex] = page
}
link := page[key&LinkPageMask]
if link == nil {
link = s.arena.New()
page[key&LinkPageMask] = link
index := page[key&LinkPageMask]
if index == 0 {
id, value := s.arena.New()
page[key&LinkPageMask] = id
return value
}
return link
return s.arena.Get(index)
}

func (s *PagedLinkStore[V]) Has(key uint64) bool {
return s.TryGet(key) != nil
pageIndex := key >> LinkPageShift
return int(pageIndex) < len(s.pages) && s.pages[pageIndex] != nil && s.pages[pageIndex][key&LinkPageMask] != 0
}

func (s *PagedLinkStore[V]) TryGet(key uint64) *V {
pageIndex := key >> 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
Expand Down
145 changes: 145 additions & 0 deletions tsc/internal/core/linkstore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package core_test

import (
"runtime"
"strconv"
"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(strconv.Itoa(i))
}
runtime.GC()
for i := range 4096 {
assert.Equal(t, **arena.Get(uint32(i + 1)), strconv.Itoa(i))
}
}

func BenchmarkPagedLinkStore(b *testing.B) {
for _, stride := range []uint64{1, 16, 256} {
b.Run(strconv.FormatUint(stride, 10), 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")
}
}
}
})
}
}
Loading