From 1526143b449fb1625a90142c39560d5e80864883 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:20:26 -0700 Subject: [PATCH] Reuse name sets when enumerating combined type properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enumerating the properties of a union or intersection tracks which names have already been combined. A fresh set is allocated for every enumeration, and on large projects those sets account for a sizable share of all allocated bytes, adding GC work for no lasting benefit. Borrow the set from a per-checker free list instead. Enumeration can recurse into other combined types, so each call still takes its own set. mui-docs: │ before │ after │ │ allocated-B │ allocated-B vs base │ MUIDocs 4.389Gi ± 1% 4.092Gi ± 0% -6.76% (p=0.002 n=6) │ before │ after │ │ allocs │ allocs vs base │ MUIDocs 23.32M ± 1% 23.18M ± 0% -0.60% (p=0.002 n=6) --- tsc/internal/checker/checker.go | 8 +++++--- tsc/internal/checker/utilities.go | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index efe984f219a4f..a4c1366c52387 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -694,6 +694,7 @@ type Checker struct { memberIndexSlotsArena core.Arena[uint32] freeTypeSets *temporaryTypeSet freePropertyCounts *temporaryPropertyCounts + freeNameSets []map[string]struct{} mergedSymbols map[*ast.Symbol]*ast.Symbol mergedExportsChecked collections.Set[*ast.Symbol] factory ast.NodeFactory @@ -19328,12 +19329,13 @@ func (c *Checker) getPropertiesOfObjectType(t *Type) []*ast.Symbol { func (c *Checker) getPropertiesOfUnionOrIntersectionType(t *Type) []*ast.Symbol { d := t.AsUnionOrIntersectionType() if d.resolvedProperties == nil { - var checked collections.Set[string] + checked := c.getTemporaryNameSet() + defer c.releaseTemporaryNameSet(checked) props := []*ast.Symbol{} for _, current := range d.types { for _, prop := range c.getPropertiesOfType(current) { - if !checked.Has(prop.Name()) { - checked.Add(prop.Name()) + if _, ok := checked[prop.Name()]; !ok { + checked[prop.Name()] = struct{}{} combinedProp := c.getPropertyOfUnionOrIntersectionType(t, prop.Name(), t.flags&TypeFlagsIntersection != 0 /*skipObjectFunctionPropertyAugment*/) if combinedProp != nil { props = append(props, combinedProp) diff --git a/tsc/internal/checker/utilities.go b/tsc/internal/checker/utilities.go index 517ffcba86cc2..2c0c4055ce114 100644 --- a/tsc/internal/checker/utilities.go +++ b/tsc/internal/checker/utilities.go @@ -957,6 +957,26 @@ func (c *Checker) releaseTemporaryPropertyCounts(counts *temporaryPropertyCounts c.freePropertyCounts = counts } +// Property enumeration of a combined type may recursively enumerate other +// combined types, so each call borrows a separate name set. +func (c *Checker) getTemporaryNameSet() map[string]struct{} { + if n := len(c.freeNameSets); n != 0 { + set := c.freeNameSets[n-1] + c.freeNameSets = c.freeNameSets[:n-1] + return set + } + return make(map[string]struct{}) +} + +func (c *Checker) releaseTemporaryNameSet(set map[string]struct{}) { + // Do not retain unusually large tables in the free list. + if len(set) > 4096 { + return + } + clear(set) + c.freeNameSets = append(c.freeNameSets, set) +} + func (s *orderedSet[T]) contains(value T) bool { if s.valuesByKey == nil { return slices.Contains(s.values, value)