From 8f3ee02c562b312ae9772f1c7a389da6b9d6fc16 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:43:04 -0700 Subject: [PATCH 1/2] Store ordinary relationship keys as exact type ID pairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ordinary type comparisons need only two 32-bit IDs and intersection context, but retained a 128-bit hash for every cached result. Hashing and storing twice the necessary key width wastes allocations and memory. Keep exact 64-bit pairs in separate maps for the four intersection states. Generic-reference normalization still uses its original full-width hash. An explicit key tag separates both namespaces, including recursion guards, without sacrificing hash bits or changing constrained generic behavior. Six fresh-process interleaved VS Code comparisons against the parent commit, using normal GC, reduce retained heap 0.94% (roughly 33 MiB) and total allocated bytes 1.29%. GC scanning, allocation events, and instruction counts are not statistically different. Tests cover directional and identity comparisons, all intersection contexts, result replacement, exact high-bit IDs, namespace isolation, generic parameter normalization, and constrained versus unconstrained recursion keys. Production diagnostics and all semantic counts match exactly. goos: linux goarch: amd64 pkg: github.com/microsoft/TypeScript/tsc/internal/checker cpu: Intel(R) Core(TM) i9-10900K CPU @ 3.70GHz │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ retained-B │ retained-B vs base │ ExternalProgram-20 3.456Gi ± 0% 3.424Gi ± 0% -0.94% (p=0.002 n=6) │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ scan-B │ scan-B vs base │ ExternalProgram-20 2.998Gi ± 0% 2.997Gi ± 0% ~ (p=0.310 n=6) │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ B/op │ B/op vs base │ ExternalProgram-20 5.021Gi ± 0% 4.956Gi ± 0% -1.29% (p=0.002 n=6) │ vscode-relation-before-6.txt │ vscode-relation-6.txt │ │ allocs/op │ allocs/op vs base │ ExternalProgram-20 31.08M ± 0% 31.07M ± 0% ~ (p=0.240 n=6) │ relation-before-instructions.txt │ relation-instructions.txt │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 269.3G ± 2% 270.0G ± 3% ~ (p=0.937 n=6) --- tsc/internal/checker/checker.go | 13 ++++-- tsc/internal/checker/relater.go | 41 +++++++++++++---- tsc/internal/checker/types_test.go | 73 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index e58e96048e94b..7119649688a3d 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -18068,7 +18068,7 @@ func getConditionalTypeKey(typeArguments []*Type, alias *TypeAlias, forConstrain return b.hash() } -func getRelationKey(source *Type, target *Type, intersectionState IntersectionState, isIdentity bool, ignoreConstraints bool) (CacheHashKey, bool) { +func getRelationKey(source *Type, target *Type, intersectionState IntersectionState, isIdentity bool, ignoreConstraints bool) (relationKey, bool) { if isIdentity && source.id > target.id { source, target = target, source } @@ -18078,12 +18078,15 @@ func getRelationKey(source *Type, target *Type, intersectionState IntersectionSt b.writeByte('g') constrained = b.writeGenericTypeReferences(source, target, ignoreConstraints) } else { - b.writeByte('s') - b.writeType(source) - b.writeType(target) + // Ordinary relationships need only the two exact IDs. Intersection + // states select separate maps, leaving generic hashes unmodified. + return relationKey{value: CacheHashKey{ + Lo: uint64(source.id)<<32 | uint64(target.id), + Hi: uint64(intersectionState), + }}, false } b.writeUint32(uint32(intersectionState)) - return b.hash(), constrained + return relationKey{value: b.hash(), generic: true}, constrained } func getNodeListKey(nodes []*ast.Node) CacheHashKey { diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index d3b6c86014498..a47dfbcb8a971 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -96,22 +96,43 @@ func asRecursionId[T *ast.Node | *ast.Symbol | *Type](value T) RecursionId { } type Relation struct { - results map[CacheHashKey]RelationComparisonResult + genericResults map[CacheHashKey]RelationComparisonResult + simpleResults [4]map[uint64]RelationComparisonResult } -func (r *Relation) get(key CacheHashKey) RelationComparisonResult { - return r.results[key] +type relationKey struct { + value CacheHashKey + generic bool } -func (r *Relation) set(key CacheHashKey, result RelationComparisonResult) { - if r.results == nil { - r.results = make(map[CacheHashKey]RelationComparisonResult) +func (r *Relation) get(key relationKey) RelationComparisonResult { + if key.generic { + return r.genericResults[key.value] + } + return r.simpleResults[key.value.Hi][key.value.Lo] +} + +func (r *Relation) set(key relationKey, result RelationComparisonResult) { + if key.generic { + if r.genericResults == nil { + r.genericResults = make(map[CacheHashKey]RelationComparisonResult) + } + r.genericResults[key.value] = result + } else { + results := &r.simpleResults[key.value.Hi] + if *results == nil { + *results = make(map[uint64]RelationComparisonResult) + } + (*results)[key.value.Lo] = result } - r.results[key] = result } func (r *Relation) size() int { - return len(r.results) + size := len(r.genericResults) + for _, results := range r.simpleResults { + size += len(results) + } + return size } func (c *Checker) isTypeIdenticalTo(source *Type, target *Type) bool { @@ -2584,8 +2605,8 @@ type Relater struct { errorNode *ast.Node errorChain *ErrorChain relatedInfo []*ast.Diagnostic - maybeKeys []CacheHashKey - maybeKeysSet collections.Set[CacheHashKey] + maybeKeys []relationKey + maybeKeysSet collections.Set[relationKey] sourceStack []*Type targetStack []*Type maybeCount int diff --git a/tsc/internal/checker/types_test.go b/tsc/internal/checker/types_test.go index 86de2b089e06e..d0a04bf6d2fc9 100644 --- a/tsc/internal/checker/types_test.go +++ b/tsc/internal/checker/types_test.go @@ -142,6 +142,79 @@ func TestKeyBuilder(t *testing.T) { } } +func TestRelationKeys(t *testing.T) { + t.Parallel() + source := &Type{id: 1} + target := &Type{id: ^TypeId(0)} + var relation Relation + assert.Equal(t, relation.size(), 0) + for state := IntersectionStateNone; state <= IntersectionStateSource|IntersectionStateTarget; state++ { + key, constrained := getRelationKey(source, target, state, false, false) + assert.Assert(t, !constrained) + assert.Equal(t, relation.get(key), RelationComparisonResultNone) + relation.set(key, RelationComparisonResultSucceeded) + reverse, _ := getRelationKey(target, source, state, false, false) + assert.Assert(t, key != reverse) + assert.Equal(t, relation.get(reverse), RelationComparisonResultNone) + identity, _ := getRelationKey(target, source, state, true, false) + assert.Equal(t, key, identity) + relation.set(key, RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + assert.Equal(t, relation.get(key), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + } + assert.Equal(t, relation.size(), 4) + for state := IntersectionStateNone; state <= IntersectionStateSource|IntersectionStateTarget; state++ { + key, _ := getRelationKey(source, target, state, false, false) + assert.Equal(t, relation.get(key), RelationComparisonResultFailed|RelationComparisonResultReportsUnreliable) + } +} + +func TestRelationKeyNamespaces(t *testing.T) { + t.Parallel() + simple, _ := getRelationKey(&Type{id: 1}, &Type{id: 2}, IntersectionStateNone, false, false) + generic := relationKey{value: simple.value, generic: true} + var relation Relation + relation.set(simple, RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(generic), RelationComparisonResultNone) + relation.set(generic, RelationComparisonResultFailed) + assert.Equal(t, relation.get(simple), RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(generic), RelationComparisonResultFailed) + assert.Equal(t, relation.size(), 2) +} + +func TestGenericRelationKeys(t *testing.T) { + t.Parallel() + c, types := newCompositeTestChecker() + c.noConstraintType = c.newIntrinsicType(TypeFlagsNever, "no constraint") + root := types[0] + newParameter := func(constraint *Type) *Type { + parameter := &TypeParameter{constraint: constraint} + return c.newType(TypeFlagsTypeParameter, ObjectFlagsNone, parameter.AsType(), typeKindTypeParameter) + } + newReference := func(argument *Type) *Type { + reference := &TypeReference{resolvedTypeArguments: []*Type{argument}} + reference.target = root + return c.newType(TypeFlagsObject, ObjectFlagsReference, reference.AsType(), typeKindTypeReference) + } + left := newReference(newParameter(c.noConstraintType)) + right := newReference(newParameter(c.noConstraintType)) + equivalentLeft := newReference(newParameter(c.noConstraintType)) + equivalentRight := newReference(newParameter(c.noConstraintType)) + key, constrained := getRelationKey(left, right, IntersectionStateNone, false, false) + assert.Assert(t, !constrained) + equivalent, _ := getRelationKey(equivalentLeft, equivalentRight, IntersectionStateNone, false, false) + assert.Equal(t, key, equivalent) + var relation Relation + relation.set(key, RelationComparisonResultSucceeded) + assert.Equal(t, relation.get(equivalent), RelationComparisonResultSucceeded) + constrainedLeft := newReference(newParameter(types[1])) + constrainedKey, constrained := getRelationKey(constrainedLeft, right, IntersectionStateNone, false, false) + assert.Assert(t, constrained) + assert.Assert(t, constrainedKey != key) + broadest, constrained := getRelationKey(constrainedLeft, right, IntersectionStateNone, false, true) + assert.Assert(t, !constrained) + assert.Equal(t, broadest, key) +} + func TestTypeListKeyAllocations(t *testing.T) { //nolint:paralleltest if race.Enabled { t.Skip("race instrumentation changes hashing allocations") From eabd9ff9bbbb078b3e0756152b291bd7ac07dfe5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:36:28 -0800 Subject: [PATCH 2/2] Size relation result maps from the intersection state mask The array of ordinary relation maps is indexed by the raw intersection state, so its length must cover every combination of state flags. Deriving it from the flags keeps it correct if a flag is added. --- tsc/internal/checker/relater.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index a47dfbcb8a971..be975263a7821 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -41,6 +41,7 @@ const ( IntersectionStateNone IntersectionState = 0 IntersectionStateSource IntersectionState = 1 << 0 // Source type is a constituent of an outer intersection IntersectionStateTarget IntersectionState = 1 << 1 // Target type is a constituent of an outer intersection + IntersectionStateAll = IntersectionStateSource | IntersectionStateTarget ) type RecursionFlags uint32 @@ -97,7 +98,9 @@ func asRecursionId[T *ast.Node | *ast.Symbol | *Type](value T) RecursionId { type Relation struct { genericResults map[CacheHashKey]RelationComparisonResult - simpleResults [4]map[uint64]RelationComparisonResult + // Ordinary relationships, keyed by type ID pair, with one map per + // intersection state. + simpleResults [IntersectionStateAll + 1]map[uint64]RelationComparisonResult } type relationKey struct {