Skip to content

Commit 3782578

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 a94e6ec commit 3782578

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
@@ -21916,24 +21916,35 @@ func (c *Checker) getPropertyOfUnionOrIntersectionType(t *Type, name string, ski
2191621916
// these partial properties when identifying discriminant properties, but otherwise they are filtered out
2191721917
// and do not appear to be present in the union type.
2191821918
func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol {
21919-
var cache ast.SymbolTable
21919+
data := t.AsUnionOrIntersectionType()
2192021920
if skipObjectFunctionPropertyAugment {
21921-
cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment)
21921+
if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil {
21922+
return prop
21923+
}
2192221924
} else {
21923-
cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache)
21924-
}
21925-
if prop := cache[name]; prop != nil {
21926-
return prop
21925+
if data.propertyCache != nil {
21926+
if prop := data.propertyCache[name]; prop != nil {
21927+
return prop
21928+
}
21929+
}
21930+
// Complete non-augmented properties are also valid for augmented
21931+
// lookup. Share the entry instead of retaining it in both tables.
21932+
if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil && prop.CheckFlags()&ast.CheckFlagsPartial == 0 {
21933+
return prop
21934+
}
2192721935
}
2192821936
prop := c.createUnionOrIntersectionProperty(t, name, skipObjectFunctionPropertyAugment)
2192921937
if prop != nil {
21930-
cache[name] = prop
21931-
// Propagate an entry from the non-augmented cache to the augmented cache unless the property is partial.
21932-
if skipObjectFunctionPropertyAugment && prop.CheckFlags()&ast.CheckFlagsPartial == 0 {
21933-
augmentedCache := ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache)
21934-
if augmentedCache[name] == nil {
21935-
augmentedCache[name] = prop
21938+
if skipObjectFunctionPropertyAugment {
21939+
ast.GetSymbolTable(&data.propertyCacheWithoutFunctionPropertyAugment)[name] = prop
21940+
if prop.CheckFlags()&ast.CheckFlagsPartial == 0 && data.propertyCache[name] == prop {
21941+
delete(data.propertyCache, name)
21942+
if len(data.propertyCache) == 0 {
21943+
data.propertyCache = nil
21944+
}
2193621945
}
21946+
} else {
21947+
ast.GetSymbolTable(&data.propertyCache)[name] = prop
2193721948
}
2193821949
}
2193921950
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)