Skip to content

Commit a4f0e31

Browse files
committed
Share complete union and intersection property cache entries
Complete non-augmented properties are valid for augmented lookup, but copying each result into both caches retains duplicate name tables. MUI keeps 2,734,919 identical entries this way, nearly doubling its union/intersection cache storage without adding semantic information. Preserve independent augmented results and partial-property behavior while sharing complete entries. A failed lookup needs no empty table. The MUI census falls from 230242 tables and 5628201 entries to 116635 tables and 2893282 entries; all distinct cached identities remain. Six fresh-process interleaved single-checker comparisons against the parent commit, with ordinary GC, save 135.63 MiB of retained MUI heap (-8.15%) and 132.00 MiB of GC scanning (-9.22%). Total allocated bytes fall 4.45% and allocation events fall 1.46%. VS Code's corresponding reductions are small: retained -0.15%, scan -0.17%, allocated -0.16%. Instruction differences in these single-checker comparisons are inconclusive; elapsed time is noisy on the shared machine. Six default-threaded production CLI pairs confirm MUI's reported heap falls 9.29% and instructions fall 2.60%. VS Code and Webpack have small heap reductions and inconclusive instruction differences. These are incremental savings on top of the earlier branch optimizations. CLI-reported heap is not the forced-GC retained-heap measure above. MUI uses the CI-pinned source and restored scenario dependencies at 6780195595c12252a130aa5ae5c5bd461229c6b7. VS Code uses 8bf178e1e1c2be3723f14683f7b1e5fdb3f780e4. Webpack uses an isolated 2289141381f2c1c965cb65118fa0ffe2e9e19834 with restored dependencies. Production diagnostics and all semantic counts match exactly. Tests cover both query orders, both composite kinds, cache growth and GC reachability, missing entries, read/write partial flags, differing mode-specific identities, and real function-property augmentation. The storage regressions fail before this change. Single-checker retained heap and allocation measurements: │ mui-shared-union-before.txt │ mui-shared-union-shared.txt │ │ retained-B │ retained-B vs base │ ExternalMemory-20 1.624Gi ± 0% 1.492Gi ± 0% -8.15% (p=0.002 n=6) │ mui-shared-union-before.txt │ mui-shared-union-shared.txt │ │ scan-B │ scan-B vs base │ ExternalMemory-20 1.398Gi ± 0% 1.270Gi ± 0% -9.22% (p=0.002 n=6) │ mui-shared-union-before.txt │ mui-shared-union-shared.txt │ │ B/op │ B/op vs base │ ExternalMemory-20 5.364Gi ± 0% 5.125Gi ± 0% -4.45% (p=0.002 n=6) │ mui-shared-union-before.txt │ mui-shared-union-shared.txt │ │ allocs/op │ allocs/op vs base │ ExternalMemory-20 25.17M ± 0% 24.80M ± 1% -1.46% (p=0.002 n=6) │ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │ │ retained-B │ retained-B vs base │ ExternalMemory-20 2.619Gi ± 0% 2.615Gi ± 0% -0.15% (p=0.002 n=6) │ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │ │ scan-B │ scan-B vs base │ ExternalMemory-20 2.291Gi ± 0% 2.287Gi ± 0% -0.17% (p=0.002 n=6) │ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │ │ B/op │ B/op vs base │ ExternalMemory-20 3.754Gi ± 0% 3.748Gi ± 0% -0.16% (p=0.002 n=6) │ vscode-shared-union-before.txt │ vscode-shared-union-shared.txt │ │ allocs/op │ allocs/op vs base │ ExternalMemory-20 23.61M ± 0% 23.59M ± 0% -0.08% (p=0.002 n=6) Single-checker instructions: │ mui-shared-union-before.instructions │ mui-shared-union-shared.instructions │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 218.9G ± 3% 217.3G ± 2% ~ (p=0.310 n=6) │ vscode-shared-union-before.instructions │ vscode-shared-union-shared.instructions │ │ instructions/op │ instructions/op vs base │ ExternalInstructions-20 188.8G ± 3% 192.1G ± 2% ~ (p=0.394 n=6) Default-threaded production CLI: │ shared-union-cli-before.txt │ shared-union-cli-after.txt │ │ instructions/op │ instructions/op vs base │ mui-20 283.3G ± 1% 275.9G ± 2% -2.60% (p=0.015 n=6) vscode-20 308.2G ± 7% 293.0G ± 6% ~ (p=0.818 n=6) webpack-20 51.96G ± 5% 51.82G ± 4% ~ (p=1.000 n=6) geomean 165.6G 161.2G -2.62% │ shared-union-cli-before.txt │ shared-union-cli-after.txt │ │ heap-B/op │ heap-B/op vs base │ mui-20 2.059Gi ± 0% 1.868Gi ± 0% -9.29% (p=0.002 n=6) vscode-20 2.835Gi ± 0% 2.829Gi ± 0% -0.21% (p=0.004 n=6) webpack-20 666.9Mi ± 0% 663.0Mi ± 0% -0.57% (p=0.002 n=6) geomean 1.561Gi 1.507Gi -3.45% │ shared-union-cli-before.txt │ shared-union-cli-after.txt │ │ allocs/op │ allocs/op vs base │ mui-20 29.84M ± 0% 29.55M ± 0% -0.97% (p=0.002 n=6) vscode-20 36.67M ± 2% 36.64M ± 0% ~ (p=0.132 n=6) webpack-20 6.235M ± 2% 6.208M ± 0% -0.43% (p=0.002 n=6) geomean 18.97M 18.87M -0.49% │ shared-union-cli-before.txt │ shared-union-cli-after.txt │ │ check-sec/op │ check-sec/op vs base │ mui-20 8.986 ± 6% 8.559 ± 4% -4.75% (p=0.026 n=6) vscode-20 8.345 ± 4% 8.900 ± 10% ~ (p=0.132 n=6) webpack-20 1.851 ± 8% 1.818 ± 8% ~ (p=0.310 n=6) geomean 5.178 5.173 -0.08%
1 parent f7addbf commit a4f0e31

4 files changed

Lines changed: 148 additions & 13 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21896,24 +21896,35 @@ func (c *Checker) getPropertyOfUnionOrIntersectionType(t *Type, name string, ski
2189621896
// these partial properties when identifying discriminant properties, but otherwise they are filtered out
2189721897
// and do not appear to be present in the union type.
2189821898
func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol {
21899-
var cache ast.SymbolTable
21899+
data := t.AsUnionOrIntersectionType()
2190021900
if skipObjectFunctionPropertyAugment {
21901-
cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment)
21901+
if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil {
21902+
return prop
21903+
}
2190221904
} else {
21903-
cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache)
21904-
}
21905-
if prop := cache[name]; prop != nil {
21906-
return prop
21905+
if data.propertyCache != nil {
21906+
if prop := data.propertyCache[name]; prop != nil {
21907+
return prop
21908+
}
21909+
}
21910+
// Complete non-augmented properties are also valid for augmented
21911+
// lookup. Share the entry instead of retaining it in both tables.
21912+
if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil && prop.CheckFlags()&ast.CheckFlagsPartial == 0 {
21913+
return prop
21914+
}
2190721915
}
2190821916
prop := c.createUnionOrIntersectionProperty(t, name, skipObjectFunctionPropertyAugment)
2190921917
if prop != nil {
21910-
cache[name] = prop
21911-
// Propagate an entry from the non-augmented cache to the augmented cache unless the property is partial.
21912-
if skipObjectFunctionPropertyAugment && prop.CheckFlags()&ast.CheckFlagsPartial == 0 {
21913-
augmentedCache := ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache)
21914-
if augmentedCache[name] == nil {
21915-
augmentedCache[name] = prop
21918+
if skipObjectFunctionPropertyAugment {
21919+
ast.GetSymbolTable(&data.propertyCacheWithoutFunctionPropertyAugment)[name] = prop
21920+
if prop.CheckFlags()&ast.CheckFlagsPartial == 0 && data.propertyCache[name] == prop {
21921+
delete(data.propertyCache, name)
21922+
if len(data.propertyCache) == 0 {
21923+
data.propertyCache = nil
21924+
}
2191621925
}
21926+
} else {
21927+
ast.GetSymbolTable(&data.propertyCache)[name] = prop
2191721928
}
2191821929
}
2191921930
return prop

‎tsc/internal/checker/checker_test.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package checker_test
33
import (
44
"fmt"
55
"path/filepath"
6+
"strconv"
67
"strings"
78
"testing"
89

@@ -117,6 +118,51 @@ export type E = D;`,
117118
assert.Equal(t, defaultClause, defaultReference)
118119
}
119120

121+
func TestUnionPropertyFunctionAugmentation(t *testing.T) {
122+
t.Parallel()
123+
for _, lookupFirst := range []bool{false, true} {
124+
t.Run(strconv.FormatBool(lookupFirst), func(t *testing.T) {
125+
t.Parallel()
126+
fs := bundled.WrapFS(vfstest.FromMap(map[string]string{
127+
"/main.ts": `type Callable = { (): string; shared: string };
128+
type Other = { call: number; shared: number };
129+
declare const instance: Callable | Other;
130+
instance;`,
131+
"/function.d.ts": `interface Function {
132+
call(thisArg: any, ...args: any[]): any;
133+
}
134+
interface CallableFunction extends Function {}`,
135+
"/tsconfig.json": `{"compilerOptions":{"strict":true,"noLib":true},"files":["main.ts","function.d.ts"]}`,
136+
}, tspath.CaseInsensitive))
137+
host := compiler.NewCompilerHost(fs, bundled.LibPath(), nil, nil, nil)
138+
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, fs, nil)
139+
assert.Equal(t, len(errors), 0)
140+
program := compiler.NewProgram(compiler.ProgramOptions{Config: parsed, Host: host})
141+
program.BindSourceFiles()
142+
c, done := program.GetTypeChecker(t.Context())
143+
defer done()
144+
file := program.GetSourceFile("/main.ts")
145+
typ := c.GetTypeAtLocation(file.Statements.Nodes[3].Expression())
146+
var call *ast.Symbol
147+
if lookupFirst {
148+
call = c.GetPropertyOfType(typ, "call")
149+
assert.Assert(t, call != nil)
150+
}
151+
properties := c.GetPropertiesOfType(typ)
152+
assert.Equal(t, len(properties), 1)
153+
assert.Equal(t, properties[0].Name(), "shared")
154+
if call == nil {
155+
call = c.GetPropertyOfType(typ, "call")
156+
assert.Assert(t, call != nil)
157+
}
158+
assert.Equal(t, c.GetPropertyOfType(typ, "call"), call)
159+
assert.Equal(t, c.GetPropertyOfType(typ, "shared"), properties[0])
160+
assert.Equal(t, len(c.GetPropertiesOfType(typ)), 1)
161+
assert.Equal(t, len(c.GetDiagnostics(t.Context(), file)), 0)
162+
})
163+
}
164+
}
165+
120166
func BenchmarkNewChecker(b *testing.B) {
121167
fs := bundled.WrapFS(osvfs.FS())
122168
rootPath := tspath.RootedDirectoryPathFromAbsolute(filepath.Join(repo.TestDataPath(), "fixtures/compiler"))

‎tsc/internal/checker/types.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1464,7 +1464,7 @@ type EvolvingArrayType struct {
14641464
type UnionOrIntersectionType struct {
14651465
StructuredType
14661466
types []*Type
1467-
propertyCache ast.SymbolTable
1467+
propertyCache ast.SymbolTable // Augmented entries not shared with the non-augmented cache.
14681468
propertyCacheWithoutFunctionPropertyAugment ast.SymbolTable
14691469
resolvedProperties []*ast.Symbol
14701470
}

‎tsc/internal/checker/types_test.go‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,84 @@ func TestStructuredMembersLookup(t *testing.T) {
658658
}
659659
}
660660

661+
func TestUnionIntersectionPropertyCacheSharing(t *testing.T) {
662+
t.Parallel()
663+
for _, intersection := range []bool{false, true} {
664+
for _, augmentedFirst := range []bool{false, true} {
665+
t.Run(strconv.FormatBool(intersection)+"/"+strconv.FormatBool(augmentedFirst), func(t *testing.T) {
666+
t.Parallel()
667+
c, _ := newCompositeTestChecker()
668+
members := make(ast.SymbolTable)
669+
for i := range 300 {
670+
name := strconv.Itoa(i)
671+
members[name] = c.newSymbol(ast.SymbolFlagsProperty, name)
672+
}
673+
members[""] = c.newSymbol(ast.SymbolFlagsProperty, "")
674+
object := c.newAnonymousType(nil, members, nil, nil, nil)
675+
var typ *Type
676+
if intersection {
677+
typ = c.newIntersectionType(ObjectFlagsNone, []*Type{object, object})
678+
} else {
679+
typ = c.newUnionType(ObjectFlagsNone, []*Type{object, object})
680+
}
681+
data := typ.AsUnionOrIntersectionType()
682+
for name, symbol := range members {
683+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, !augmentedFirst), symbol)
684+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, augmentedFirst), symbol)
685+
}
686+
assert.Equal(t, len(data.propertyCacheWithoutFunctionPropertyAugment), len(members))
687+
assert.Assert(t, data.propertyCache == nil)
688+
runtime.GC()
689+
for name, symbol := range members {
690+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), symbol)
691+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), symbol)
692+
}
693+
runtime.KeepAlive(c)
694+
})
695+
}
696+
}
697+
}
698+
699+
func TestUnionIntersectionPropertyCacheModes(t *testing.T) {
700+
t.Parallel()
701+
for _, intersection := range []bool{false, true} {
702+
t.Run(strconv.FormatBool(intersection), func(t *testing.T) {
703+
t.Parallel()
704+
c, _ := newCompositeTestChecker()
705+
var typ *Type
706+
if intersection {
707+
typ = c.newIntersectionType(ObjectFlagsNone, nil)
708+
} else {
709+
typ = c.newUnionType(ObjectFlagsNone, nil)
710+
}
711+
data := typ.AsUnionOrIntersectionType()
712+
assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", true) == nil)
713+
assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", false) == nil)
714+
assert.Assert(t, data.propertyCache == nil)
715+
assert.Assert(t, data.propertyCacheWithoutFunctionPropertyAugment == nil)
716+
data.propertyCacheWithoutFunctionPropertyAugment = make(ast.SymbolTable)
717+
data.propertyCache = make(ast.SymbolTable)
718+
for _, flags := range []ast.CheckFlags{ast.CheckFlagsReadPartial, ast.CheckFlagsWritePartial} {
719+
name := strconv.Itoa(int(flags))
720+
partial := c.newSymbolEx(ast.SymbolFlagsProperty, name, flags)
721+
complete := c.newSymbol(ast.SymbolFlagsProperty, name)
722+
data.propertyCacheWithoutFunctionPropertyAugment[name] = partial
723+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), partial)
724+
assert.Assert(t, c.getUnionOrIntersectionProperty(typ, name, false) == nil)
725+
assert.Equal(t, c.getPropertyOfUnionOrIntersectionType(typ, name, true) == nil, flags&ast.CheckFlagsReadPartial != 0)
726+
data.propertyCache[name] = complete
727+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), complete)
728+
}
729+
nonAugmented := c.newSymbol(ast.SymbolFlagsProperty, "different")
730+
augmented := c.newSymbol(ast.SymbolFlagsProperty, "different")
731+
data.propertyCacheWithoutFunctionPropertyAugment["different"] = nonAugmented
732+
data.propertyCache["different"] = augmented
733+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", true), nonAugmented)
734+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", false), augmented)
735+
})
736+
}
737+
}
738+
661739
func TestCompactStructuredTypeLayout(t *testing.T) {
662740
t.Parallel()
663741
assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0)))

0 commit comments

Comments
 (0)