Skip to content

Commit 1807ffa

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 46ffb18 commit 1807ffa

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
@@ -21895,24 +21895,35 @@ func (c *Checker) getPropertyOfUnionOrIntersectionType(t *Type, name string, ski
2189521895
// these partial properties when identifying discriminant properties, but otherwise they are filtered out
2189621896
// and do not appear to be present in the union type.
2189721897
func (c *Checker) getUnionOrIntersectionProperty(t *Type, name string, skipObjectFunctionPropertyAugment bool) *ast.Symbol {
21898-
var cache ast.SymbolTable
21898+
data := t.AsUnionOrIntersectionType()
2189921899
if skipObjectFunctionPropertyAugment {
21900-
cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment)
21900+
if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil {
21901+
return prop
21902+
}
2190121903
} else {
21902-
cache = ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache)
21903-
}
21904-
if prop := cache[name]; prop != nil {
21905-
return prop
21904+
if data.propertyCache != nil {
21905+
if prop := data.propertyCache[name]; prop != nil {
21906+
return prop
21907+
}
21908+
}
21909+
// Complete non-augmented properties are also valid for augmented
21910+
// lookup. Share the entry instead of retaining it in both tables.
21911+
if prop := data.propertyCacheWithoutFunctionPropertyAugment[name]; prop != nil && prop.CheckFlags()&ast.CheckFlagsPartial == 0 {
21912+
return prop
21913+
}
2190621914
}
2190721915
prop := c.createUnionOrIntersectionProperty(t, name, skipObjectFunctionPropertyAugment)
2190821916
if prop != nil {
21909-
cache[name] = prop
21910-
// Propagate an entry from the non-augmented cache to the augmented cache unless the property is partial.
21911-
if skipObjectFunctionPropertyAugment && prop.CheckFlags()&ast.CheckFlagsPartial == 0 {
21912-
augmentedCache := ast.GetSymbolTable(&t.AsUnionOrIntersectionType().propertyCache)
21913-
if augmentedCache[name] == nil {
21914-
augmentedCache[name] = prop
21917+
if skipObjectFunctionPropertyAugment {
21918+
ast.GetSymbolTable(&data.propertyCacheWithoutFunctionPropertyAugment)[name] = prop
21919+
if prop.CheckFlags()&ast.CheckFlagsPartial == 0 && data.propertyCache[name] == prop {
21920+
delete(data.propertyCache, name)
21921+
if len(data.propertyCache) == 0 {
21922+
data.propertyCache = nil
21923+
}
2191521924
}
21925+
} else {
21926+
ast.GetSymbolTable(&data.propertyCache)[name] = prop
2191621927
}
2191721928
}
2191821929
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
@@ -547,6 +547,84 @@ func TestStructuredMembersLookup(t *testing.T) {
547547
}
548548
}
549549

550+
func TestUnionIntersectionPropertyCacheSharing(t *testing.T) {
551+
t.Parallel()
552+
for _, intersection := range []bool{false, true} {
553+
for _, augmentedFirst := range []bool{false, true} {
554+
t.Run(strconv.FormatBool(intersection)+"/"+strconv.FormatBool(augmentedFirst), func(t *testing.T) {
555+
t.Parallel()
556+
c, _ := newCompositeTestChecker()
557+
members := make(ast.SymbolTable)
558+
for i := range 300 {
559+
name := strconv.Itoa(i)
560+
members[name] = c.newSymbol(ast.SymbolFlagsProperty, name)
561+
}
562+
members[""] = c.newSymbol(ast.SymbolFlagsProperty, "")
563+
object := c.newAnonymousType(nil, members, nil, nil, nil)
564+
var typ *Type
565+
if intersection {
566+
typ = c.newIntersectionType(ObjectFlagsNone, []*Type{object, object})
567+
} else {
568+
typ = c.newUnionType(ObjectFlagsNone, []*Type{object, object})
569+
}
570+
data := typ.AsUnionOrIntersectionType()
571+
for name, symbol := range members {
572+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, !augmentedFirst), symbol)
573+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, augmentedFirst), symbol)
574+
}
575+
assert.Equal(t, len(data.propertyCacheWithoutFunctionPropertyAugment), len(members))
576+
assert.Assert(t, data.propertyCache == nil)
577+
runtime.GC()
578+
for name, symbol := range members {
579+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), symbol)
580+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), symbol)
581+
}
582+
runtime.KeepAlive(c)
583+
})
584+
}
585+
}
586+
}
587+
588+
func TestUnionIntersectionPropertyCacheModes(t *testing.T) {
589+
t.Parallel()
590+
for _, intersection := range []bool{false, true} {
591+
t.Run(strconv.FormatBool(intersection), func(t *testing.T) {
592+
t.Parallel()
593+
c, _ := newCompositeTestChecker()
594+
var typ *Type
595+
if intersection {
596+
typ = c.newIntersectionType(ObjectFlagsNone, nil)
597+
} else {
598+
typ = c.newUnionType(ObjectFlagsNone, nil)
599+
}
600+
data := typ.AsUnionOrIntersectionType()
601+
assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", true) == nil)
602+
assert.Assert(t, c.getUnionOrIntersectionProperty(typ, "missing", false) == nil)
603+
assert.Assert(t, data.propertyCache == nil)
604+
assert.Assert(t, data.propertyCacheWithoutFunctionPropertyAugment == nil)
605+
data.propertyCacheWithoutFunctionPropertyAugment = make(ast.SymbolTable)
606+
data.propertyCache = make(ast.SymbolTable)
607+
for _, flags := range []ast.CheckFlags{ast.CheckFlagsReadPartial, ast.CheckFlagsWritePartial} {
608+
name := strconv.Itoa(int(flags))
609+
partial := c.newSymbolEx(ast.SymbolFlagsProperty, name, flags)
610+
complete := c.newSymbol(ast.SymbolFlagsProperty, name)
611+
data.propertyCacheWithoutFunctionPropertyAugment[name] = partial
612+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, true), partial)
613+
assert.Assert(t, c.getUnionOrIntersectionProperty(typ, name, false) == nil)
614+
assert.Equal(t, c.getPropertyOfUnionOrIntersectionType(typ, name, true) == nil, flags&ast.CheckFlagsReadPartial != 0)
615+
data.propertyCache[name] = complete
616+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, name, false), complete)
617+
}
618+
nonAugmented := c.newSymbol(ast.SymbolFlagsProperty, "different")
619+
augmented := c.newSymbol(ast.SymbolFlagsProperty, "different")
620+
data.propertyCacheWithoutFunctionPropertyAugment["different"] = nonAugmented
621+
data.propertyCache["different"] = augmented
622+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", true), nonAugmented)
623+
assert.Equal(t, c.getUnionOrIntersectionProperty(typ, "different", false), augmented)
624+
})
625+
}
626+
}
627+
550628
func TestCompactStructuredTypeLayout(t *testing.T) {
551629
t.Parallel()
552630
assert.Equal(t, unsafe.Sizeof(StructuredType{}), unsafe.Sizeof(ConstrainedType{})+5*unsafe.Sizeof(uintptr(0)))

0 commit comments

Comments
 (0)