Skip to content

Commit ae113ed

Browse files
committed
Memoize repeated inference walks
Some inference walks are long and are made again and again with the same inputs. With Effect Schema, every call such as Schema.optional(x) inside a Struct({ ... }) infers from the contextual Constraint type to the combinator's return type, optional<S>, whose members span the large schema interfaces. Each call repeats that walk from fresh inference infos and always ends in the same candidates. Effect-heavy code like t3code spends a large share of its check time this way. When every inference info is still in its initial state, the outcome of a walk is now remembered under its source, target, priority, contravariance and type parameters, as long as the walk computed nothing new and observed no transient state, so walking it again would take the same path. Transient state is any read of an in-progress resolution, signature resolution, variance computation, flow loop or flow type cache, which are now counted. Measured with --checkers 4 (n=8): │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ Tsc/bluesky 3.069 ± 4% 3.024 ± 1% -1.47% (p=0.038 n=8) Tsc/sentry 12.74 ± 2% 12.62 ± 1% ~ (p=0.382 n=8) Tsc/t3code-server 16.81 ± 3% 16.12 ± 4% -4.12% (p=0.028 n=8) geomean 8.694 8.505 -2.17% │ before.txt │ after.txt │ │ rss-B │ rss-B vs base │ Tsc/bluesky 1.422Gi ± 2% 1.413Gi ± 1% ~ (p=0.959 n=8) Tsc/sentry 4.571Gi ± 8% 4.617Gi ± 4% ~ (p=0.721 n=8) Tsc/t3code-server 4.378Gi ± 2% 4.650Gi ± 6% +6.21% (p=0.038 n=8) geomean 3.053Gi 3.119Gi +2.16% │ before.txt │ after.txt │ │ heap-B │ heap-B vs base │ Tsc/bluesky 1.152Gi ± 0% 1.153Gi ± 0% ~ (p=0.195 n=8) Tsc/sentry 3.620Gi ± 0% 3.622Gi ± 0% ~ (p=0.195 n=8) Tsc/t3code-server 3.761Gi ± 0% 3.763Gi ± 0% +0.04% (p=0.000 n=8) geomean 2.504Gi 2.504Gi +0.03%
1 parent 8d9b6da commit ae113ed

5 files changed

Lines changed: 159 additions & 2 deletions

File tree

‎tsc/internal/checker/checker.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,10 @@ type Checker struct {
599599
TotalInstantiationCount uint32
600600
instantiationCount uint32
601601
memberLayoutCount uint32
602+
instantiationCountResets uint32
603+
diagnosticAdds uint32
604+
transientReads uint32
605+
inferMemo inferMemo
602606
instantiationStack []*Type
603607
conditionalConstraintDepth uint32
604608
inlineLevel int
@@ -2316,6 +2320,7 @@ func (c *Checker) checkSourceElement(node *ast.Node) bool {
23162320
saveWithinUnreachableCode := c.withinUnreachableCode
23172321
c.currentNode = node
23182322
c.instantiationCount = 0
2323+
c.instantiationCountResets++
23192324
c.checkSourceElementWorker(node)
23202325
c.currentNode = saveCurrentNode
23212326
c.withinUnreachableCode = saveWithinUnreachableCode
@@ -2579,6 +2584,7 @@ func (c *Checker) checkDeferredNode(node *ast.Node) {
25792584
saveCurrentNode := c.currentNode
25802585
c.currentNode = node
25812586
c.instantiationCount = 0
2587+
c.instantiationCountResets++
25822588
switch node.Kind {
25832589
case ast.KindCallExpression, ast.KindNewExpression, ast.KindTaggedTemplateExpression, ast.KindDecorator, ast.KindJsxOpeningElement:
25842590
// These node kinds are deferred checked when overload resolution fails. To save on work,
@@ -7545,6 +7551,7 @@ func (c *Checker) getTypeOfExpression(node *ast.Node) *Type {
75457551
}
75467552
// If a type has been cached for the node, return it.
75477553
if cachedType := c.flowTypeCache[node]; cachedType != nil {
7554+
c.noteTransientRead()
75487555
return cachedType
75497556
}
75507557
startInvocationCount := c.flowInvocationCount
@@ -7764,6 +7771,7 @@ func (c *Checker) checkExpressionEx(node *ast.Node, checkMode CheckMode) *Type {
77647771
saveCurrentNode := c.currentNode
77657772
c.currentNode = node
77667773
c.instantiationCount = 0
7774+
c.instantiationCountResets++
77677775
uninstantiatedType := c.checkExpressionWorker(node, checkMode)
77687776
t := c.instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode)
77697777
if isConstEnumObjectType(t) {
@@ -8623,6 +8631,9 @@ func (c *Checker) getResolvedSignature(node *ast.Node, candidatesOutArray *[]*Si
86238631
if cached != nil && cached != c.resolvingSignature && candidatesOutArray == nil {
86248632
return cached
86258633
}
8634+
if cached == c.resolvingSignature {
8635+
c.noteTransientRead()
8636+
}
86268637
saveResolutionStart := c.resolutionStart
86278638
if cached == nil {
86288639
// If we haven't already done so, temporarily reset the resolution stack. This allows us to
@@ -14246,6 +14257,7 @@ func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic {
1424614257
}
1424714258

1424814259
func (c *Checker) addDeferredDiagnostic(callback func()) {
14260+
c.diagnosticAdds++
1424914261
c.deferredDiagnosticCallbacks = append(c.deferredDiagnosticCallbacks, callback)
1425014262
}
1425114263

@@ -14258,6 +14270,7 @@ func (c *Checker) produceDeferredDiagnostics() {
1425814270

1425914271
func (c *Checker) addDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic {
1426014272
// Discard diagnostics created while at the maximum number of recursive TypeToString invocations.
14273+
c.diagnosticAdds++
1426114274
if c.serializationLevel < maxSerializationLevel {
1426214275
return c.diagnostics.Add(diagnostic)
1426314276
}
@@ -14266,6 +14279,7 @@ func (c *Checker) addDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic {
1426614279

1426714280
func (c *Checker) addSuggestionDiagnostic(diagnostic *ast.Diagnostic) *ast.Diagnostic {
1426814281
// Discard diagnostics created while at the maximum number of recursive TypeToString invocations.
14282+
c.diagnosticAdds++
1426914283
if c.serializationLevel < maxSerializationLevel {
1427014284
return c.suggestionDiagnostics.Add(diagnostic)
1427114285
}
@@ -19228,6 +19242,7 @@ func (c *Checker) findResolutionCycleStartIndex(target TypeSystemEntity, propert
1922819242
return -1
1922919243
}
1923019244
if resolution.target == target && resolution.propertyName == propertyName {
19245+
c.noteTransientRead()
1923119246
return i
1923219247
}
1923319248
}
@@ -30554,6 +30569,7 @@ func (c *Checker) getContextualTypeForArgumentAtIndex(callTarget *ast.Node, argI
3055430569
// that could cause infinite recursion. Instead, return anySignature.
3055530570
var signature *Signature
3055630571
if c.signatureLinks.Get(callTarget).resolvedSignature == c.resolvingSignature {
30572+
c.noteTransientRead()
3055730573
signature = c.resolvingSignature
3055830574
} else {
3055930575
signature = c.getResolvedSignature(callTarget, nil, CheckModeNormal)

‎tsc/internal/checker/flow.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func (c *Checker) getFlowTypeOfReference(reference *ast.Node, declaredType *Type
8080

8181
func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Type, initialType *Type, flowContainer *ast.Node, flowNode *ast.FlowNode) *Type {
8282
if c.flowAnalysisDisabled {
83+
c.noteTransientRead()
8384
return c.errorType
8485
}
8586
if flowNode == nil {
@@ -122,6 +123,7 @@ func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType {
122123
tr.Instant(tracing.PhaseCheckTypes, "getTypeAtFlowNode_DepthLimit", map[string]any{"depth": f.depth})
123124
}
124125
c.flowAnalysisDisabled = true
126+
c.noteTransientRead()
125127
c.reportFlowControlError(f.reference)
126128
return FlowType{t: c.errorType}
127129
}
@@ -383,7 +385,10 @@ func (c *Checker) narrowType(f *FlowState, t *Type, expr *ast.Node, assumeTrue b
383385
case ast.KindIdentifier:
384386
// When narrowing a reference to a const variable, non-assigned parameter, or readonly property, we inline
385387
// up to five levels of aliased conditional expressions that are themselves declared as const variables.
386-
if !c.isMatchingReference(f.reference, expr) && c.inlineLevel < 5 {
388+
if c.inlineLevel >= 5 {
389+
// Whether this inlines depends on how deep the enclosing inlining is.
390+
c.noteTransientRead()
391+
} else if !c.isMatchingReference(f.reference, expr) {
387392
symbol := c.getResolvedSymbol(expr)
388393
if c.isConstantVariable(symbol) {
389394
declaration := symbol.ValueDeclaration()
@@ -1346,6 +1351,7 @@ func (c *Checker) getTypeAtFlowLoopLabel(f *FlowState, flow *ast.FlowNode) FlowT
13461351
// path that leads to the top.
13471352
for _, loopInfo := range c.flowLoopStack {
13481353
if loopInfo.key == key && len(loopInfo.types) != 0 {
1354+
c.noteTransientRead()
13491355
return c.newFlowType(c.getUnionOrEvolvingArrayType(f, loopInfo.types, UnionReductionLiteral), true /*incomplete*/)
13501356
}
13511357
}

‎tsc/internal/checker/inference.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ func (c *Checker) inferTypes(inferences []*InferenceInfo, originalSource *Type,
6161
n.priority = priority
6262
n.inferencePriority = InferencePriorityMaxValue
6363
n.contravariant = contravariant
64-
c.inferFromTypes(n, originalSource, originalTarget)
64+
c.inferFromTypesMemo(n, originalSource, originalTarget)
6565
c.putInferenceState(n)
6666
}
6767

6868
func (c *Checker) inferFromTypes(n *InferenceState, source *Type, target *Type) {
69+
c.inferMemo.steps++
6970
if !c.couldContainTypeVariables(target) || c.isNoInferType(target) {
7071
return
7172
}

‎tsc/internal/checker/infermemo.go‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package checker
2+
3+
import (
4+
"encoding/binary"
5+
"slices"
6+
7+
"github.com/microsoft/TypeScript/tsc/internal/core"
8+
)
9+
10+
// The inference walk memo.
11+
//
12+
// Some walks at the top of inferTypes are long and are repeated with the same inputs, for example
13+
// inferring from a schema constraint type to the return type of a schema combinator, once per
14+
// call to that combinator. When every inference info is still in its initial state, a walk's
15+
// inputs are just the source, the target, the priority, the contravariance and the type
16+
// parameters, and its outcome is the candidates, priority and topLevel it writes to each info.
17+
//
18+
// A walk's outcome is stored only when the walk computed nothing new and read no transient
19+
// state: it created no type, symbol or signature, instantiated nothing, added no diagnostic,
20+
// observed no in-progress resolution, signature resolution, flow loop or flow type cache entry,
21+
// and did not start inside an instantiation or variance computation. Such a walk read only
22+
// finished values besides its inputs, so the same inputs take the same path again and write
23+
// the same outcome. Walks shorter than inferMemoMinSteps are cheaper to repeat than to store.
24+
25+
const inferMemoMinSteps = 16
26+
27+
type inferMemoOutcome struct {
28+
candidates []*Type
29+
contraCandidates []*Type
30+
priority InferencePriority
31+
topLevel bool
32+
}
33+
34+
type inferMemo struct {
35+
entries map[string][]inferMemoOutcome
36+
key []byte
37+
steps uint32
38+
}
39+
40+
// The checker state a walk must leave alone to be stored.
41+
type inferMemoEffects struct {
42+
created uint32
43+
instantiationCount uint32
44+
totalInstantiationCount uint32
45+
diagnostics uint32
46+
transientReads uint32
47+
instantiationCountReset uint32
48+
}
49+
50+
func (c *Checker) inferMemoEffects() inferMemoEffects {
51+
return inferMemoEffects{
52+
created: c.TypeCount + c.SymbolCount + c.SignatureCount,
53+
instantiationCount: c.instantiationCount,
54+
totalInstantiationCount: c.TotalInstantiationCount,
55+
diagnostics: c.diagnosticAdds,
56+
transientReads: c.transientReads,
57+
instantiationCountReset: c.instantiationCountResets,
58+
}
59+
}
60+
61+
// Records that the current computation observed state that is only valid for the moment, such
62+
// as an in-progress resolution. Results computed with such reads must not be memoized.
63+
func (c *Checker) noteTransientRead() {
64+
c.transientReads++
65+
}
66+
67+
func (c *Checker) inferFromTypesMemo(n *InferenceState, source *Type, target *Type) {
68+
m := &c.inferMemo
69+
// Relations made inside a variance computation report reliability to it.
70+
if len(c.varianceStack) != 0 || c.skipDirectInferenceNodes.Len() != 0 ||
71+
!core.Every(n.inferences, isInitialInferenceInfo) {
72+
c.inferFromTypes(n, source, target)
73+
return
74+
}
75+
m.key = binary.LittleEndian.AppendUint32(m.key[:0], uint32(source.id))
76+
m.key = binary.LittleEndian.AppendUint32(m.key, uint32(target.id))
77+
m.key = binary.LittleEndian.AppendUint32(m.key, uint32(n.priority))
78+
if n.contravariant {
79+
m.key = append(m.key, 1)
80+
} else {
81+
m.key = append(m.key, 0)
82+
}
83+
for _, info := range n.inferences {
84+
m.key = binary.LittleEndian.AppendUint32(m.key, uint32(info.typeParameter.id))
85+
}
86+
if outcomes, ok := m.entries[string(m.key)]; ok {
87+
changed := false
88+
for i, info := range n.inferences {
89+
outcome := &outcomes[i]
90+
if outcome.candidates != nil || outcome.contraCandidates != nil ||
91+
outcome.priority != info.priority || outcome.topLevel != info.topLevel {
92+
changed = true
93+
}
94+
info.candidates = outcome.candidates
95+
info.contraCandidates = outcome.contraCandidates
96+
info.priority = outcome.priority
97+
info.topLevel = outcome.topLevel
98+
}
99+
if changed {
100+
clearCachedInferences(n.inferences)
101+
}
102+
return
103+
}
104+
// The walk may infer into nested calls that reuse the key buffer.
105+
key := string(m.key)
106+
// Inside an instantiation the walk may take cached instantiations without counting them,
107+
// while the same walk elsewhere would make and count them.
108+
inInstantiation := len(c.activeMappers) != 0
109+
before := c.inferMemoEffects()
110+
stepsBefore := m.steps
111+
c.inferFromTypes(n, source, target)
112+
if m.steps-stepsBefore < inferMemoMinSteps || inInstantiation || c.inferMemoEffects() != before {
113+
return
114+
}
115+
outcomes := make([]inferMemoOutcome, len(n.inferences))
116+
for i, info := range n.inferences {
117+
outcomes[i] = inferMemoOutcome{
118+
candidates: slices.Clip(slices.Clone(info.candidates)),
119+
contraCandidates: slices.Clip(slices.Clone(info.contraCandidates)),
120+
priority: info.priority,
121+
topLevel: info.topLevel,
122+
}
123+
}
124+
if m.entries == nil {
125+
m.entries = make(map[string][]inferMemoOutcome)
126+
}
127+
m.entries[key] = outcomes
128+
}
129+
130+
func isInitialInferenceInfo(info *InferenceInfo) bool {
131+
return info.typeParameter != nil && info.candidates == nil && info.contraCandidates == nil && !info.isFixed &&
132+
info.priority == InferencePriorityMaxValue && info.topLevel && info.impliedArity == -1
133+
}

‎tsc/internal/checker/relater.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,7 @@ func (c *Checker) getVariancesWorker(symbol *ast.Symbol, typeParameters []*Type)
14101410
c.resolutionStart = saveResolutionStart
14111411
}
14121412
} else {
1413+
c.noteTransientRead()
14131414
// We've detected a circularity. Since we may compute different variances depending on where
14141415
// we enter a circularity, we find the generic type with the "smallest" symbol in the circular
14151416
// region of the variance stack and restart the computation from there if necessary. This

0 commit comments

Comments
 (0)