Skip to content

Commit 1482441

Browse files
committed
Reuse ordered property counts during intersection reduction
Intersection reduction repeatedly allocates temporary property-count tables. On MUI, this one allocation site accounts for roughly a quarter of all allocated bytes, despite none of the tables needing to survive the reduction. Reusing this workspace removes substantial allocation churn without changing the first-seen property order or caching results. Each recursive reduction borrows an independent checker-local table. Returning a table clears its names and counts, including on early returns; tables exceeding 4096 properties are not retained. Against the parent commit, six fresh-process MUI single-checker pairs save 1.36 GiB of allocation per run, reduce allocation events by 7.59%, and reduce instructions by 6.02%. Default-threaded production MUI instructions decrease 8.88%. Retained heap and scannable heap are essentially unchanged: this is an allocation-churn optimization. VS Code allocation savings are smaller. Production VS Code instructions remain inconclusive after twelve pairs, with a higher point estimate; Webpack instructions are also inconclusive. Wall times are noisy on this shared host. All production diagnostic text and semantic counts match between revisions. The regression test allocated 21 times per warmed reduction before this change and now allocates zero times. Tests also cover nested borrowing, property order, clearing, oversized tables, and early-return cleanup. Six fresh-process single-checker pairs, ordinary GC: │ mui-property-counts-memory-before.txt │ mui-property-counts-memory-after.txt │ │ retained-B │ retained-B vs base │ ExternalMemory-20 1.492Gi ± 0% 1.492Gi ± 0% ~ (p=0.310 n=6) │ mui-property-counts-memory-before.txt │ mui-property-counts-memory-after.txt │ │ scan-B │ scan-B vs base │ ExternalMemory-20 1.270Gi ± 0% 1.270Gi ± 0% ~ (p=0.310 n=6) │ mui-property-counts-memory-before.txt │ mui-property-counts-memory-after.txt │ │ B/op │ B/op vs base │ ExternalMemory-20 5.122Gi ± 0% 3.757Gi ± 0% -26.64% (p=0.002 n=6) │ mui-property-counts-memory-before.txt │ mui-property-counts-memory-after.txt │ │ allocs/op │ allocs/op vs base │ ExternalMemory-20 24.77M ± 0% 22.89M ± 0% -7.59% (p=0.002 n=6) │ mui-property-counts-memory-before.instructions │ mui-property-counts-memory-after.instructions │ │ instructions/op │ instructions/op vs base │ mui-20 213.7G ± 3% 200.8G ± 3% -6.02% (p=0.002 n=6) │ vscode-property-counts-memory-before.txt │ vscode-property-counts-memory-after.txt │ │ retained-B │ retained-B vs base │ ExternalMemory-20 2.615Gi ± 0% 2.615Gi ± 0% ~ (p=0.589 n=6) │ vscode-property-counts-memory-before.txt │ vscode-property-counts-memory-after.txt │ │ scan-B │ scan-B vs base │ ExternalMemory-20 2.287Gi ± 0% 2.287Gi ± 0% ~ (p=0.589 n=6) │ vscode-property-counts-memory-before.txt │ vscode-property-counts-memory-after.txt │ │ B/op │ B/op vs base │ ExternalMemory-20 3.749Gi ± 0% 3.714Gi ± 0% -0.92% (p=0.002 n=6) │ vscode-property-counts-memory-before.txt │ vscode-property-counts-memory-after.txt │ │ allocs/op │ allocs/op vs base │ ExternalMemory-20 23.60M ± 0% 23.49M ± 0% -0.47% (p=0.002 n=6) │ vscode-property-counts-memory-before.instructions │ vscode-property-counts-memory-after.instructions │ │ instructions/op │ instructions/op vs base │ vscode-20 193.0G ± 4% 193.2G ± 5% ~ (p=0.818 n=6) Default-threaded production CLI (six pairs; VS Code twelve pairs): │ mui-property-counts-cli-before.txt │ mui-property-counts-cli-after.txt │ │ heap-B/op │ heap-B/op vs base │ mui-20 1.867Gi ± 0% 1.868Gi ± 0% ~ (p=0.589 n=6) │ mui-property-counts-cli-before.txt │ mui-property-counts-cli-after.txt │ │ allocs/op │ allocs/op vs base │ mui-20 29.52M ± 0% 27.84M ± 0% -5.69% (p=0.002 n=6) │ mui-property-counts-cli-before.txt │ mui-property-counts-cli-after.txt │ │ check-sec/op │ check-sec/op vs base │ mui-20 11.59 ± 3% 10.67 ± 5% -7.86% (p=0.004 n=6) │ mui-property-counts-cli-before.instructions │ mui-property-counts-cli-after.instructions │ │ instructions/op │ instructions/op vs base │ mui-20 274.6G ± 2% 250.2G ± 4% -8.88% (p=0.002 n=6) │ vscode-property-counts-cli-before.txt │ vscode-property-counts-cli-after.txt │ │ heap-B/op │ heap-B/op vs base │ vscode-20 2.828Gi ± 0% 2.828Gi ± 0% ~ (p=0.843 n=12) │ vscode-property-counts-cli-before.txt │ vscode-property-counts-cli-after.txt │ │ allocs/op │ allocs/op vs base │ vscode-20 36.62M ± 0% 36.54M ± 0% -0.21% (p=0.000 n=12) │ vscode-property-counts-cli-before.txt │ vscode-property-counts-cli-after.txt │ │ check-sec/op │ check-sec/op vs base │ vscode-20 8.671 ± 21% 8.409 ± 26% ~ (p=0.799 n=12) │ vscode-property-counts-cli-before.instructions │ vscode-property-counts-cli-after.instructions │ │ instructions/op │ instructions/op vs base │ vscode-20 296.5G ± 4% 307.6G ± 3% ~ (p=0.242 n=12) │ webpack-property-counts-cli-before.txt │ webpack-property-counts-cli-after.txt │ │ heap-B/op │ heap-B/op vs base │ webpack-20 663.0Mi ± 0% 663.1Mi ± 0% ~ (p=0.699 n=6) │ webpack-property-counts-cli-before.txt │ webpack-property-counts-cli-after.txt │ │ allocs/op │ allocs/op vs base │ webpack-20 6.208M ± 0% 6.096M ± 0% -1.80% (p=0.002 n=6) │ webpack-property-counts-cli-before.txt │ webpack-property-counts-cli-after.txt │ │ check-sec/op │ check-sec/op vs base │ webpack-20 1.765 ± 8% 1.711 ± 6% ~ (p=0.180 n=6) │ webpack-property-counts-cli-before.instructions │ webpack-property-counts-cli-after.instructions │ │ instructions/op │ instructions/op vs base │ webpack-20 52.37G ± 2% 51.78G ± 3% ~ (p=0.394 n=6)
1 parent 2565d17 commit 1482441

3 files changed

Lines changed: 113 additions & 1 deletion

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ type Checker struct {
691691
valueSymbolExtraArena core.Arena[valueSymbolExtra]
692692
structuredTypeExtraArena core.Arena[structuredTypeExtra]
693693
freeTypeSets *temporaryTypeSet
694+
freePropertyCounts *temporaryPropertyCounts
694695
mergedSymbols map[*ast.Symbol]*ast.Symbol
695696
mergedExportsChecked collections.Set[*ast.Symbol]
696697
factory ast.NodeFactory
@@ -22344,7 +22345,8 @@ func (c *Checker) isMappingOfSameObjectType(types []*Type) bool {
2234422345

2234522346
func (c *Checker) somePropertyReducesToNever(t *Type) bool {
2234622347
// Collect declaration counts for each property across all constituent types of the intersection.
22347-
var counts collections.OrderedMap[string, int]
22348+
counts := c.getTemporaryPropertyCounts()
22349+
defer c.releaseTemporaryPropertyCounts(counts)
2234822350
for _, t := range t.Types() {
2234922351
for _, prop := range c.getPropertiesOfType(t) {
2235022352
counts.Set(prop.Name(), counts.GetOrZero(prop.Name())+1)

‎tsc/internal/checker/types_test.go‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,88 @@ func TestTemporaryTypeSetReuse(t *testing.T) {
286286
assert.Equal(t, c.getIntersectionType([]*Type{left, right}), primitives[1])
287287
}
288288

289+
func TestIntersectionReductionAllocations(t *testing.T) { //nolint:paralleltest
290+
if race.Enabled {
291+
t.Skip("race instrumentation changes hashing allocations")
292+
}
293+
c, _ := newCompositeTestChecker()
294+
members := make(ast.SymbolTable)
295+
for i := range 128 {
296+
name := strconv.Itoa(i)
297+
members[name] = c.newSymbol(ast.SymbolFlagsProperty, name)
298+
}
299+
object := c.newAnonymousType(nil, members, nil, nil, nil)
300+
intersection := c.newIntersectionType(ObjectFlagsNone, []*Type{object, object})
301+
assert.Assert(t, !c.somePropertyReducesToNever(intersection))
302+
assert.Equal(t, testing.AllocsPerRun(10, func() {
303+
assert.Assert(t, !c.somePropertyReducesToNever(intersection))
304+
}), float64(0))
305+
}
306+
307+
func TestTemporaryPropertyCounts(t *testing.T) {
308+
t.Parallel()
309+
c := &Checker{}
310+
outer := c.getTemporaryPropertyCounts()
311+
outer.Set("second", 1)
312+
outer.Set("first", 1)
313+
outer.Set("second", 2)
314+
assert.Assert(t, slices.Equal(slices.Collect(outer.Keys()), []string{"second", "first"}))
315+
inner := c.getTemporaryPropertyCounts()
316+
assert.Assert(t, inner != outer)
317+
inner.Set("first", 4)
318+
assert.Equal(t, outer.GetOrZero("first"), 1)
319+
assert.Equal(t, outer.GetOrZero("second"), 2)
320+
c.releaseTemporaryPropertyCounts(inner)
321+
assert.Equal(t, inner.Size(), 0)
322+
assert.Assert(t, !inner.Has("first"))
323+
reused := c.getTemporaryPropertyCounts()
324+
assert.Equal(t, reused, inner)
325+
reused.Set("second", 3)
326+
reused.Set("first", 5)
327+
assert.Assert(t, slices.Equal(slices.Collect(reused.Keys()), []string{"second", "first"}))
328+
assert.Equal(t, outer.GetOrZero("first"), 1)
329+
c.releaseTemporaryPropertyCounts(reused)
330+
c.releaseTemporaryPropertyCounts(outer)
331+
runtime.GC()
332+
reused = c.getTemporaryPropertyCounts()
333+
assert.Equal(t, reused.Size(), 0)
334+
assert.Assert(t, !reused.Has("second"))
335+
for i := range 4097 {
336+
reused.Set(strconv.Itoa(i), i)
337+
}
338+
c.releaseTemporaryPropertyCounts(reused)
339+
for counts := c.freePropertyCounts; counts != nil; counts = counts.next {
340+
assert.Assert(t, counts != reused)
341+
assert.Equal(t, counts.Size(), 0)
342+
}
343+
assert.Assert(t, c.getTemporaryPropertyCounts() != reused)
344+
other := &Checker{}
345+
assert.Assert(t, other.getTemporaryPropertyCounts() != outer)
346+
}
347+
348+
func TestIntersectionReductionReentrancy(t *testing.T) {
349+
t.Parallel()
350+
c, _ := newCompositeTestChecker()
351+
property := c.newSymbol(ast.SymbolFlagsProperty, "value")
352+
object := c.newAnonymousType(nil, ast.SymbolTable{"value": property}, nil, nil, nil)
353+
inner := c.newIntersectionType(ObjectFlagsNone, []*Type{object, object})
354+
outer := c.newIntersectionType(ObjectFlagsNone, []*Type{inner, object})
355+
assert.Equal(t, c.getReducedType(outer), outer)
356+
assert.Equal(t, c.getReducedType(inner), inner)
357+
assert.Equal(t, c.getPropertyOfUnionOrIntersectionType(outer, "value", true), property)
358+
assert.Assert(t, c.freePropertyCounts != nil)
359+
assert.Assert(t, c.freePropertyCounts.next != nil)
360+
workspace := c.freePropertyCounts
361+
conflicting := c.newSymbolEx(ast.SymbolFlagsProperty, "value", ast.CheckFlagsContainsPrivate)
362+
outer.AsUnionOrIntersectionType().propertyCacheWithoutFunctionPropertyAugment["value"] = conflicting
363+
assert.Assert(t, c.somePropertyReducesToNever(outer))
364+
assert.Equal(t, c.freePropertyCounts, workspace)
365+
for counts := c.freePropertyCounts; counts != nil; counts = counts.next {
366+
assert.Equal(t, counts.Size(), 0)
367+
assert.Assert(t, !counts.Has("value"))
368+
}
369+
}
370+
289371
func BenchmarkCompositeTypeCacheHit(b *testing.B) {
290372
c, types := newCompositeTestChecker()
291373
union := c.getUnionType(types)

‎tsc/internal/checker/utilities.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/microsoft/TypeScript/tsc/internal/ast"
1010
"github.com/microsoft/TypeScript/tsc/internal/binder"
11+
"github.com/microsoft/TypeScript/tsc/internal/collections"
1112
"github.com/microsoft/TypeScript/tsc/internal/core"
1213
"github.com/microsoft/TypeScript/tsc/internal/debug"
1314
"github.com/microsoft/TypeScript/tsc/internal/diagnostics"
@@ -919,6 +920,33 @@ func (c *Checker) releaseTemporaryTypeSet(set *temporaryTypeSet) {
919920
c.freeTypeSets = set
920921
}
921922

923+
type temporaryPropertyCounts struct {
924+
collections.OrderedMap[string, int]
925+
next *temporaryPropertyCounts
926+
}
927+
928+
func (c *Checker) getTemporaryPropertyCounts() *temporaryPropertyCounts {
929+
counts := c.freePropertyCounts
930+
if counts == nil {
931+
return &temporaryPropertyCounts{}
932+
}
933+
c.freePropertyCounts = counts.next
934+
counts.next = nil
935+
return counts
936+
}
937+
938+
func (c *Checker) releaseTemporaryPropertyCounts(counts *temporaryPropertyCounts) {
939+
size := counts.Size()
940+
counts.Clear()
941+
// Recursive reductions borrow separate maps. Do not retain unusually
942+
// large property tables or references to their names in the free list.
943+
if size > 4096 {
944+
return
945+
}
946+
counts.next = c.freePropertyCounts
947+
c.freePropertyCounts = counts
948+
}
949+
922950
func (s *orderedSet[T]) contains(value T) bool {
923951
if s.valuesByKey == nil {
924952
return slices.Contains(s.values, value)

0 commit comments

Comments
 (0)