Skip to content
Open
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
46 changes: 27 additions & 19 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ type Checker struct {
ctx context.Context
packagesMap map[string]bool
activeMappers []*TypeMapper
activeTypeMappersCaches []map[CacheHashKey]*Type
activeTypeMappersCaches []clearableMap[*Type]
ambientModulesOnce sync.Once
ambientModules []*ast.Symbol
withinUnreachableCode bool
Expand Down Expand Up @@ -22624,16 +22624,15 @@ func (c *Checker) instantiateTypeWithAlias(t *Type, m *TypeMapper, alias *TypeAl
return c.errorType
}
index := c.findActiveMapper(m)
var key CacheHashKey
if index == -1 {
// A newly pushed mapper has an empty cache, and the result is not cached below.
c.pushActiveMapper(m)
}
var b keyBuilder
b.writeType(t)
b.writeAlias(alias)
key := b.hash()
cache := c.activeTypeMappersCaches[core.IfElse(index != -1, index, len(c.activeTypeMappersCaches)-1)]
if cachedType, ok := cache[key]; ok {
return cachedType
} else {
key = getActiveMapperCacheKey(t, alias)
if cachedType, ok := c.activeTypeMappersCaches[index].get(key); ok {
return cachedType
}
}
c.TotalInstantiationCount++
c.instantiationCount++
Expand All @@ -22642,13 +22641,25 @@ func (c *Checker) instantiateTypeWithAlias(t *Type, m *TypeMapper, alias *TypeAl
if index == -1 {
c.popActiveMapper()
} else {
cache[key] = result
c.activeTypeMappersCaches[index].set(key, result)
}
c.instantiationStack[len(c.instantiationStack)-1] = nil
c.instantiationStack = c.instantiationStack[:len(c.instantiationStack)-1]
return result
}

// Without an alias the key is just the type ID, which avoids hashing. Hashed keys have
// a nonzero high half with overwhelming probability, so the two kinds do not collide.
func getActiveMapperCacheKey(t *Type, alias *TypeAlias) CacheHashKey {
if alias == nil {
return CacheHashKey{Lo: uint64(t.id)}
}
var b keyBuilder
b.writeType(t)
b.writeAlias(alias)
return b.hash()
}

func (c *Checker) getCircularTypeNames() []string {
typeCounts := make(map[*Type]int)
var circularTypeNames []string
Expand All @@ -22674,23 +22685,20 @@ func (c *Checker) pushActiveMapper(mapper *TypeMapper) {

lastIndex := len(c.activeTypeMappersCaches)
if cap(c.activeTypeMappersCaches) > lastIndex {
// The cap may contain an empty map from popActiveMapper; reuse it.
// The cap may contain an empty cache from popActiveMapper; reuse it.
c.activeTypeMappersCaches = c.activeTypeMappersCaches[:lastIndex+1]
if c.activeTypeMappersCaches[lastIndex] == nil {
c.activeTypeMappersCaches[lastIndex] = make(map[CacheHashKey]*Type, 1)
}
} else {
c.activeTypeMappersCaches = append(c.activeTypeMappersCaches, make(map[CacheHashKey]*Type, 1))
c.activeTypeMappersCaches = append(c.activeTypeMappersCaches, clearableMap[*Type]{})
}
}

func (c *Checker) popActiveMapper() {
c.activeMappers[len(c.activeMappers)-1] = nil
c.activeMappers = c.activeMappers[:len(c.activeMappers)-1]

// Clear the map, but leave it in the list for later reuse.
// Clear the cache, but leave it in the list for later reuse.
lastIndex := len(c.activeTypeMappersCaches) - 1
clear(c.activeTypeMappersCaches[lastIndex])
c.activeTypeMappersCaches[lastIndex].clear()
c.activeTypeMappersCaches = c.activeTypeMappersCaches[:lastIndex]
}

Expand All @@ -22699,8 +22707,8 @@ func (c *Checker) findActiveMapper(mapper *TypeMapper) int {
}

func (c *Checker) clearActiveMapperCaches() {
for _, cache := range c.activeTypeMappersCaches {
clear(cache)
for i := range c.activeTypeMappersCaches {
c.activeTypeMappersCaches[i].clear()
}
}

Expand Down
87 changes: 87 additions & 0 deletions tsc/internal/checker/clearablemap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package checker

import "math/bits"

// clearableMap is a hash table keyed by 128-bit keys that can be cleared in constant time. It
// is used for short-lived caches that are cleared and reused many times: a Go map takes time
// proportional to its capacity to clear, which stays large once it has grown. Here each entry
// records the generation it was written in, and clearing starts a new one.
type clearableMap[V any] struct {
entries []clearableMapEntry[V]
generation uint32
count int
shift uint8
}

type clearableMapEntry[V any] struct {
key CacheHashKey
value V
generation uint32
}

func (m *clearableMap[V]) slot(key CacheHashKey) int {
return int(((key.Lo ^ key.Hi) * 0x9e3779b97f4a7c15) >> m.shift)
}

func (m *clearableMap[V]) get(key CacheHashKey) (V, bool) {
var zero V
if m.count == 0 {
return zero, false
}
mask := len(m.entries) - 1
for i := m.slot(key); ; i = (i + 1) & mask {
e := &m.entries[i]
if e.generation != m.generation {
return zero, false
}
if e.key == key {
return e.value, true
}
}
}

func (m *clearableMap[V]) set(key CacheHashKey, value V) {
if (m.count+1)*4 > len(m.entries)*3 {
m.grow()
}
mask := len(m.entries) - 1
for i := m.slot(key); ; i = (i + 1) & mask {
e := &m.entries[i]
if e.generation != m.generation {
*e = clearableMapEntry[V]{key: key, value: value, generation: m.generation}
m.count++
return
}
if e.key == key {
e.value = value
return
}
}
}

func (m *clearableMap[V]) grow() {
old := m.entries
oldGeneration := m.generation
size := max(len(old)*2, 8)
m.entries = make([]clearableMapEntry[V], size)
m.shift = uint8(64 - bits.TrailingZeros(uint(size)))
m.generation = 1
m.count = 0
for i := range old {
if e := &old[i]; e.generation == oldGeneration {
m.set(e.key, e.value)
}
}
}

func (m *clearableMap[V]) clear() {
if m.count == 0 {
return
}
m.count = 0
m.generation++
if m.generation == 0 {
clear(m.entries)
m.generation = 1
}
}
18 changes: 9 additions & 9 deletions tsc/internal/checker/inference.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type InferenceState struct {
bivariant bool
expandingFlags ExpandingFlags
propagationType *Type
visited map[InferenceKey]InferencePriority
visited clearableMap[InferencePriority]
sourceStack []*Type
targetStack []*Type
next *InferenceState
Expand All @@ -42,7 +42,7 @@ func (c *Checker) getInferenceState() *InferenceState {
}

func (c *Checker) putInferenceState(n *InferenceState) {
clear(n.visited)
n.visited.clear()
*n = InferenceState{
inferences: n.inferences[:0],
visited: n.visited,
Expand Down Expand Up @@ -344,15 +344,15 @@ func (c *Checker) inferFromContravariantTypesIfStrictFunctionTypes(n *InferenceS
// such that we would go on inferring forever, even though we would never infer
// between the same pair of types.
func (c *Checker) invokeOnce(n *InferenceState, source *Type, target *Type, action func(c *Checker, n *InferenceState, source *Type, target *Type)) {
key := InferenceKey{source: source.id, target: target.id, priority: n.priority, contravariant: n.contravariant, bivariant: n.bivariant}
if status, ok := n.visited[key]; ok {
key := CacheHashKey{
Lo: uint64(source.id)<<32 | uint64(target.id),
Hi: uint64(uint32(n.priority))<<2 | uint64(core.IfElse(n.contravariant, 2, 0)) | uint64(core.IfElse(n.bivariant, 1, 0)),
}
if status, ok := n.visited.get(key); ok {
n.inferencePriority = min(n.inferencePriority, status)
return
}
if n.visited == nil {
n.visited = make(map[InferenceKey]InferencePriority)
}
n.visited[key] = InferencePriorityCircularity
n.visited.set(key, InferencePriorityCircularity)
saveInferencePriority := n.inferencePriority
n.inferencePriority = InferencePriorityMaxValue
// We stop inferring and report a circularity if we encounter duplicate recursion identities on both
Expand All @@ -374,7 +374,7 @@ func (c *Checker) invokeOnce(n *InferenceState, source *Type, target *Type, acti
n.targetStack = n.targetStack[:len(n.targetStack)-1]
n.sourceStack = n.sourceStack[:len(n.sourceStack)-1]
n.expandingFlags = saveExpandingFlags
n.visited[key] = n.inferencePriority
n.visited.set(key, n.inferencePriority)
n.inferencePriority = min(n.inferencePriority, saveInferencePriority)
}

Expand Down
Loading