Skip to content

Commit c684744

Browse files
committed
Keep hot node accessors within the inlining budget
On main, Node.Symbol, Locals, Body, Expression and similar accessors are small enough to inline, so the checker reads a member with a few loads. Here they were all out of line: reading a member took a kind table lookup, a ref decode and a store lookup, and several also built a panic message inline, which the inliner charges as a call. Symbol, Locals and Body now use the same offset tables as Name and Expression; the JSDoc tags whose Type is their type expression, and the kinds that never had a Body, are expressed as overrides of the tables. Side tables keep their zero value in their first slot so that reading one needs no test, deref finds the store without the chunk helpers, and panics carry the kind and build their message only when printed. Wall-clock runs were too noisy to separate the change; single-threaded runs with the collector off measure it: │ old.txt │ new.txt │ │ insns │ insns vs base │ VSCode 114.9G ± ∞ ¹ 113.7G ± ∞ ¹ -1.11% (p=0.008 n=5) ¹ need >= 6 samples for confidence interval at level 0.95 │ old.txt │ new.txt │ │ task-sec │ task-sec vs base │ VSCode 24.79 ± ∞ ¹ 24.53 ± ∞ ¹ -1.03% (p=0.008 n=5) ¹ need >= 6 samples for confidence interval at level 0.95
1 parent a4fca73 commit c684744

6 files changed

Lines changed: 186 additions & 50 deletions

File tree

‎tools/scripts/tsc/generate-go-ast.ts‎

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,16 +1260,20 @@ function hasMember(node: NodeType, name: string): boolean {
12601260
return schemaMembers(node).some(member => member.name === name);
12611261
}
12621262

1263-
function generateMemberOffsetTable(w: CodeWriter, table: string, member: string, refType: string) {
1264-
const storage = goStorageName(member);
1263+
// Overrides map a kind's Go constant to the storage of the field read for
1264+
// the member instead, or to null if the kind has none.
1265+
function generateMemberOffsetTable(w: CodeWriter, table: string, member: string, refType: string, overrides: Record<string, string | null> = {}) {
12651266
w.write(`var ${table} = [kindFlowReduceLabelData + 1]uint8{`);
12661267
w.push();
12671268
for (const node of api.nodes()) {
1268-
const field = nodeRecordFields(node).find(f => f.storage === storage);
1269-
// Only refs can be read the same way for every kind.
1270-
if (field?.goType !== refType) continue;
12711269
for (const kind of node.allKinds()) {
1272-
w.write(`${kind.formatGoConstant()}: uint8(unsafe.Offsetof(${dataName(node)}{}.${storage})),`);
1270+
const constant = kind.formatGoConstant();
1271+
const storage = constant in overrides ? overrides[constant] : goStorageName(member);
1272+
if (storage === null) continue;
1273+
const field = nodeRecordFields(node).find(f => f.storage === storage);
1274+
// Only refs can be read the same way for every kind.
1275+
if (field?.goType !== refType) continue;
1276+
w.write(`${constant}: uint8(unsafe.Offsetof(${dataName(node)}{}.${storage})),`);
12731277
}
12741278
}
12751279
w.pop();
@@ -1297,20 +1301,28 @@ function generateMemberOffsetAccessor(w: CodeWriter, method: string, ret: string
12971301

12981302
// Members read through Node by hand-written accessors, which find them by
12991303
// these tables.
1300-
const MEMBER_OFFSET_TABLES: { member: string; refType: string; }[] = [
1304+
const MEMBER_OFFSET_TABLES: { member: string; refType: string; overrides?: Record<string, string | null>; }[] = [
13011305
{ member: "Expression", refType: "nodeRef" },
13021306
{ member: "Initializer", refType: "nodeRef" },
1303-
{ member: "Type", refType: "nodeRef" },
1307+
{
1308+
member: "Type",
1309+
refType: "nodeRef",
1310+
overrides: { KindJSDocParameterTag: "typeExpression", KindJSDocPropertyTag: "typeExpression", KindJSDocVariadicType: null },
1311+
},
13041312
{ member: "PostfixToken", refType: "nodeRef" },
13051313
{ member: "QuestionToken", refType: "nodeRef" },
13061314
{ member: "QuestionDotToken", refType: "nodeRef" },
13071315
{ member: "TypeArguments", refType: "listRef" },
13081316
{ member: "Arguments", refType: "listRef" },
1317+
// Node.Body has never read the body of a class static block.
1318+
{ member: "Body", refType: "nodeRef", overrides: { KindClassStaticBlockDeclaration: null } },
1319+
{ member: "Symbol", refType: "sideRef" },
1320+
{ member: "Locals", refType: "sideRef" },
13091321
];
13101322

13111323
function generateMemberOffsetTables(w: CodeWriter) {
1312-
for (const { member, refType } of MEMBER_OFFSET_TABLES) {
1313-
generateMemberOffsetTable(w, `node${member}Offsets`, member, refType);
1324+
for (const { member, refType, overrides } of MEMBER_OFFSET_TABLES) {
1325+
generateMemberOffsetTable(w, `node${member}Offsets`, member, refType, overrides);
13141326
}
13151327
}
13161328

‎tsc/internal/ast/ast.go‎

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,8 @@ type MutableNode struct{ Node }
220220
func (n Node) AsMutable() MutableNode { return MutableNode{n} }
221221

222222
func (n Node) Symbol() *Symbol {
223-
data := n.DeclarationData()
224-
if !data.IsNil() {
225-
return data.Symbol()
223+
if off := nodeSymbolOffsets[n.Kind()]; off != 0 {
224+
return n.store().symbols.items[*(*sideRef)(unsafe.Add(n.p, off))]
226225
}
227226
return nil
228227
}
@@ -236,17 +235,15 @@ func (n Node) LocalSymbol() *Symbol {
236235
}
237236

238237
func (n Node) Locals() SymbolTable {
239-
data := n.LocalsContainerData()
240-
if !data.IsNil() {
241-
return data.Locals()
238+
if off := nodeLocalsOffsets[n.Kind()]; off != 0 {
239+
return n.store().symbolTables.items[*(*sideRef)(unsafe.Add(n.p, off))]
242240
}
243241
return nil
244242
}
245243

246244
func (n Node) Body() Node {
247-
data := n.BodyData()
248-
if !data.IsNil() {
249-
return data.Body()
245+
if off := nodeBodyOffsets[n.Kind()]; off != 0 {
246+
return n.refAt(off)
250247
}
251248
return Node{}
252249
}
@@ -289,11 +286,24 @@ func (n Node) Text() string {
289286
panic(fmt.Sprintf("Unhandled case in Node.Text: %s", n.Kind()))
290287
}
291288

289+
// unhandledKind is the panic value of accessors given a kind without the
290+
// member. Its message is built only when printed, which keeps the accessors
291+
// small enough to inline.
292+
type unhandledKind struct {
293+
method string
294+
kind Kind
295+
}
296+
297+
func (e unhandledKind) Error() string {
298+
return "Unhandled case in " + e.method + ": " + e.kind.String()
299+
}
300+
292301
func (n Node) Expression() Node {
293-
if off := nodeExpressionOffsets[n.Kind()]; off != 0 {
302+
k := n.Kind()
303+
if off := nodeExpressionOffsets[k]; off != 0 {
294304
return n.refAt(off)
295305
}
296-
panic("Unhandled case in Node.Expression: " + n.Kind().String())
306+
panic(unhandledKind{"Node.Expression", k})
297307
}
298308

299309
func (n Node) RawText() string {
@@ -387,10 +397,11 @@ func (m MutableNode) SetExpression(expr Node) {
387397
}
388398

389399
func (n Node) ArgumentList() NodeList {
390-
if off := nodeArgumentsOffsets[n.Kind()]; off != 0 {
400+
k := n.Kind()
401+
if off := nodeArgumentsOffsets[k]; off != 0 {
391402
return n.listAt(off)
392403
}
393-
panic("Unhandled case in Node.Arguments: " + n.Kind().String())
404+
panic(unhandledKind{"Node.Arguments", k})
394405
}
395406

396407
func (n Node) Arguments() []Node {
@@ -518,16 +529,11 @@ func (n Node) ModifierNodes() []Node {
518529
return nil
519530
}
520531

532+
// Type reads the type expression of JSDoc parameter and property tags, and
533+
// nothing for JSDocVariadicType, whose type is its operand.
521534
func (n Node) Type() Node {
522-
switch k := n.Kind(); k {
523-
case KindJSDocParameterTag, KindJSDocPropertyTag:
524-
return n.AsJSDocParameterOrPropertyTag().TypeExpression()
525-
case KindJSDocVariadicType:
526-
return Node{}
527-
default:
528-
if off := nodeTypeOffsets[k]; off != 0 {
529-
return n.refAt(off)
530-
}
535+
if off := nodeTypeOffsets[n.Kind()]; off != 0 {
536+
return n.refAt(off)
531537
}
532538
return Node{}
533539
}
@@ -913,10 +919,11 @@ func (n Node) QuestionToken() TokenNode {
913919
}
914920

915921
func (n Node) QuestionDotToken() Node {
916-
if off := nodeQuestionDotTokenOffsets[n.Kind()]; off != 0 {
922+
k := n.Kind()
923+
if off := nodeQuestionDotTokenOffsets[k]; off != 0 {
917924
return n.refAt(off)
918925
}
919-
panic("Unhandled case in Node.QuestionDotToken: " + n.Kind().String())
926+
panic(unhandledKind{"Node.QuestionDotToken", k})
920927
}
921928

922929
func (n Node) TypeExpression() Node {

‎tsc/internal/ast/ast_generated.go‎

Lines changed: 106 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tsc/internal/ast/list.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func (n Node) derefList(f *listRef) NodeList {
222222
if r&refNear != 0 {
223223
return NodeList{unsafe.Add(unsafe.Pointer(f), int(int32(r))-refNear)}
224224
}
225-
return n.store().farLists[r>>1]
225+
return (*chunkHeader)(unsafe.Add(n.p, -int((*nodeBaseData)(n.p).chunk))).store.farLists[r>>1]
226226
}
227227

228228
func (n Node) derefModifiers(f *listRef) ModifierList {
@@ -263,5 +263,5 @@ func setListRef(c *chunkHeader, f *listRef, v NodeList) {
263263
// listAt returns the list that the field at offset off of n's record
264264
// refers to.
265265
func (n Node) listAt(off uint8) NodeList {
266-
return n.derefList((*listRef)(unsafe.Add(n.rec(), off)))
266+
return n.derefList((*listRef)(unsafe.Add(n.p, off)))
267267
}

‎tsc/internal/ast/store.go‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,16 @@ const (
8080
func newNodeStore() *nodeStore {
8181
// The first far slots are the absent node and list, which zero refs
8282
// refer to.
83-
return &nodeStore{far: make([]Node, 1), farLists: make([]NodeList, 1)}
83+
return &nodeStore{
84+
far: make([]Node, 1),
85+
farLists: make([]NodeList, 1),
86+
symbols: newSideTable[*Symbol](),
87+
symbolTables: newSideTable[SymbolTable](),
88+
stringLists: newSideTable[[]string](),
89+
nodeSlices: newSideTable[[]Node](),
90+
values: newSideTable[any](),
91+
sourceFiles: newSideTable[*SourceFile](),
92+
}
8493
}
8594

8695
// nodeBaseData is the header at the start of every node record.
@@ -133,13 +142,15 @@ func (n Node) deref(f *nodeRef) Node {
133142
if r&refNear != 0 {
134143
return Node{unsafe.Add(unsafe.Pointer(f), int(int32(r))-refNear)}
135144
}
136-
return n.store().far[r>>1]
145+
// The store is found by hand rather than by n.store() to keep callers
146+
// within the inlining budget.
147+
return (*chunkHeader)(unsafe.Add(n.p, -int((*nodeBaseData)(n.p).chunk))).store.far[r>>1]
137148
}
138149

139150
// refAt returns the node that the field at offset off of n's record refers
140151
// to.
141152
func (n Node) refAt(off uint8) Node {
142-
return n.deref((*nodeRef)(unsafe.Add(n.rec(), off)))
153+
return n.deref((*nodeRef)(unsafe.Add(n.p, off)))
143154
}
144155

145156
// setRef makes the field f of n's record refer to v.
@@ -355,8 +366,9 @@ func (s *nodeStore) compactSide() {
355366
s.farLists = slices.Clip(slices.Clone(s.farLists))
356367
}
357368

358-
// A sideRef is a record field's slot in a side table, biased by one so that
359-
// zero means the zero value without a slot.
369+
// A sideRef is a record field's slot in a side table. The first slot holds
370+
// the zero value and is never written, so a zero ref reads as the zero value
371+
// without a slot of its own.
360372
type sideRef uint32
361373

362374
// A sideTable is appended to only by its store's owner, before the store
@@ -365,23 +377,23 @@ type sideTable[T any] struct {
365377
items []T
366378
}
367379

380+
func newSideTable[T any]() sideTable[T] {
381+
return sideTable[T]{items: make([]T, 1)}
382+
}
383+
368384
func (t *sideTable[T]) get(r sideRef) T {
369-
if r == 0 {
370-
var zero T
371-
return zero
372-
}
373-
return t.items[r-1]
385+
return t.items[r]
374386
}
375387

376388
// set stores v in r's slot, allocating one unless v is the zero value.
377389
func (t *sideTable[T]) set(r *sideRef, v T, isZero bool) {
378390
if *r != 0 {
379-
t.items[*r-1] = v
391+
t.items[*r] = v
380392
return
381393
}
382394
if !isZero {
383-
t.items = append(t.items, v)
384395
*r = sideRef(len(t.items))
396+
t.items = append(t.items, v)
385397
}
386398
}
387399

‎tsc/internal/ast/store_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func TestNodeSideTableZeroValues(t *testing.T) {
106106
assert.Assert(t, node.AsBlock().Locals() != nil)
107107
node.AsBlock().SetLocals(nil)
108108
assert.Assert(t, node.AsBlock().Locals() == nil)
109-
assert.Equal(t, len(factory.store.symbolTables.items), 1)
109+
assert.Equal(t, len(factory.store.symbolTables.items), 2)
110110
}
111111

112112
func TestNodeTextRefs(t *testing.T) {

0 commit comments

Comments
 (0)