Skip to content

Commit 2879671

Browse files
authored
Fixes to --generateTrace to make analyze-trace work (#3703)
1 parent 803dcde commit 2879671

6 files changed

Lines changed: 417 additions & 50 deletions

File tree

internal/checker/tracer.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package checker
22

33
import (
4+
"maps"
5+
46
"github.com/microsoft/typescript-go/internal/ast"
57
"github.com/microsoft/typescript-go/internal/tracing"
68
)
@@ -9,26 +11,63 @@ import (
911
// is a valid no-op, so call sites can use `if tr := c.tracer; tr != nil` to
1012
// gate work that only matters under --generateTrace.
1113
type Tracer struct {
12-
tracing *tracing.Tracing
13-
recorder tracing.Tracer
14+
tracing *tracing.Tracing
15+
recorder tracing.Tracer
16+
checkerIndex int
1417
}
1518

1619
// NewTracer creates a Tracer for the given checker index that records both
1720
// type-creation events and trace events through the provided tracing session.
1821
func NewTracer(tr *tracing.Tracing, checkerIndex int) *Tracer {
19-
return &Tracer{tracing: tr, recorder: tr.NewTypeTracer(checkerIndex)}
22+
return &Tracer{tracing: tr, recorder: tr.NewTypeTracer(checkerIndex), checkerIndex: checkerIndex}
2023
}
2124

2225
func (t *Tracer) RecordType(typ *Type) {
2326
t.recorder.RecordType(wrapType(typ))
2427
}
2528

2629
func (t *Tracer) Push(phase tracing.Phase, name string, args map[string]any, separateBeginAndEnd bool) func() {
27-
return t.tracing.Push(phase, name, args, separateBeginAndEnd)
30+
if !separateBeginAndEnd {
31+
return t.tracing.Push(phase, name, t.copyWithCheckerIndex(args), separateBeginAndEnd)
32+
}
33+
34+
args, restore := t.temporarilyAddCheckerIndex(args)
35+
pop := t.tracing.Push(phase, name, args, separateBeginAndEnd)
36+
restore()
37+
38+
return func() {
39+
_, restoreEndArgs := t.temporarilyAddCheckerIndex(args)
40+
defer restoreEndArgs()
41+
pop()
42+
}
2843
}
2944

3045
func (t *Tracer) Instant(phase tracing.Phase, name string, args map[string]any) {
31-
t.tracing.Instant(phase, name, args)
46+
t.tracing.Instant(phase, name, t.copyWithCheckerIndex(args))
47+
}
48+
49+
func (t *Tracer) copyWithCheckerIndex(args map[string]any) map[string]any {
50+
withCheckerIndex := make(map[string]any, len(args)+1)
51+
maps.Copy(withCheckerIndex, args)
52+
withCheckerIndex["checkerId"] = t.checkerIndex
53+
return withCheckerIndex
54+
}
55+
56+
func (t *Tracer) temporarilyAddCheckerIndex(args map[string]any) (map[string]any, func()) {
57+
if args == nil {
58+
args = map[string]any{}
59+
}
60+
61+
previous, hadPrevious := args["checkerId"]
62+
args["checkerId"] = t.checkerIndex
63+
64+
return args, func() {
65+
if hadPrevious {
66+
args["checkerId"] = previous
67+
} else {
68+
delete(args, "checkerId")
69+
}
70+
}
3271
}
3372

3473
// tracedTypeAdapter adapts a Type to the tracing.TracedType interface

internal/checker/tracer_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package checker
2+
3+
import (
4+
"io/fs"
5+
"testing"
6+
"testing/fstest"
7+
8+
"github.com/microsoft/typescript-go/internal/json"
9+
"github.com/microsoft/typescript-go/internal/tracing"
10+
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
func TestTracerPushPreservesEndArgMutations(t *testing.T) {
15+
t.Parallel()
16+
17+
fsys := vfstest.FromMap(fstest.MapFS{
18+
"/trace": &fstest.MapFile{Mode: fs.ModeDir},
19+
}, true)
20+
21+
tr, err := tracing.StartTracing(fsys, "/trace", "", true /*deterministic*/)
22+
assert.NilError(t, err)
23+
24+
args := map[string]any{"id": 1}
25+
tracer := NewTracer(tr, 7)
26+
pop := tracer.Push(tracing.PhaseCheckTypes, "getVariancesWorker", args, true)
27+
_, hasCheckerID := args["checkerId"]
28+
assert.Assert(t, !hasCheckerID)
29+
30+
args["variances"] = []string{"out"}
31+
pop()
32+
_, hasCheckerID = args["checkerId"]
33+
assert.Assert(t, !hasCheckerID)
34+
35+
assert.NilError(t, tr.StopTracing())
36+
37+
traceText, ok := fsys.ReadFile("/trace/trace.json")
38+
assert.Assert(t, ok)
39+
40+
var events []testTraceEvent
41+
assert.NilError(t, json.Unmarshal([]byte(traceText), &events))
42+
43+
beginEvent := findTestTraceEvent(t, events, "B", "getVariancesWorker")
44+
assert.Equal(t, beginEvent.Args["checkerId"], float64(7))
45+
assert.Equal(t, beginEvent.Args["variances"], nil)
46+
47+
endEvent := findTestTraceEvent(t, events, "E", "getVariancesWorker")
48+
assert.Equal(t, endEvent.Args["checkerId"], float64(7))
49+
variances, ok := endEvent.Args["variances"].([]any)
50+
assert.Assert(t, ok)
51+
assert.DeepEqual(t, variances, []any{"out"})
52+
}
53+
54+
type testTraceEvent struct {
55+
PH string `json:"ph"`
56+
Name string `json:"name"`
57+
Args map[string]any `json:"args"`
58+
}
59+
60+
func findTestTraceEvent(t *testing.T, events []testTraceEvent, phase string, name string) testTraceEvent {
61+
t.Helper()
62+
for _, event := range events {
63+
if event.PH == phase && event.Name == name {
64+
return event
65+
}
66+
}
67+
t.Fatalf("failed to find %s event %q", phase, name)
68+
return testTraceEvent{}
69+
}

internal/tracing/tracing.go

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"maps"
66
"math"
77
"slices"
8+
"strconv"
89
"strings"
910
"sync"
1011
"sync/atomic"
@@ -15,6 +16,7 @@ import (
1516
"github.com/microsoft/typescript-go/internal/scanner"
1617
"github.com/microsoft/typescript-go/internal/tspath"
1718
"github.com/microsoft/typescript-go/internal/vfs"
19+
"github.com/zeebo/xxh3"
1820
)
1921

2022
// Tracer is an interface for recording types during type checking.
@@ -71,6 +73,7 @@ type TraceRecord struct {
7173
ConfigFilePath string `json:"configFilePath,omitzero"`
7274
TracePath string `json:"tracePath,omitzero"`
7375
TypesPath string `json:"typesPath,omitzero"`
76+
CheckerID int `json:"checkerId"`
7477
}
7578

7679
type traceEvent struct {
@@ -92,6 +95,15 @@ const sampleInterval = 10 * time.Millisecond
9295

9396
const traceFileName = "trace.json"
9497

98+
const (
99+
mainThreadID = 1
100+
firstSyntheticThreadID = 2
101+
firstFileThreadID = 1_000_000
102+
fileThreadIDHashRange = 1_000_000_000
103+
)
104+
105+
var traceThreadArgKeys = [...]string{"path", "fileName", "containingFileName", "jsFilePath", "declarationFilePath"}
106+
95107
// flushThreshold is the size at which buffered trace content is flushed to disk
96108
// via AppendFile. Keeps peak memory bounded for long-running compilations while
97109
// avoiding a syscall per event.
@@ -107,6 +119,9 @@ type Tracing struct {
107119
tracers []*typeTracer
108120
traceContent strings.Builder
109121
traceStarted atomic.Bool
122+
threadIDs map[traceThreadKey]int
123+
threadKeys map[int]traceThreadKey
124+
metadataTS float64
110125
deterministic bool // when true, use monotonic counter instead of real time
111126
timestampCounter uint64 // only used in deterministic mode
112127
startTime time.Time
@@ -142,6 +157,8 @@ func StartTracing(fs vfs.FS, traceDir string, configFilePath string, determinist
142157
configFilePath: configFilePath,
143158
legend: []TraceRecord{},
144159
tracers: []*typeTracer{},
160+
threadIDs: make(map[traceThreadKey]int),
161+
threadKeys: make(map[int]traceThreadKey),
145162
deterministic: deterministic,
146163
startTime: time.Now(),
147164
}
@@ -152,11 +169,12 @@ func StartTracing(fs vfs.FS, traceDir string, configFilePath string, determinist
152169

153170
// Write metadata events (matching TypeScript's format)
154171
metaTs := tr.timestamp()
155-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "M", Cat: "__metadata", TS: metaTs, Name: "process_name", Args: map[string]any{"name": "tsgo"}})
172+
tr.metadataTS = metaTs
173+
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "__metadata", TS: metaTs, Name: "process_name", Args: map[string]any{"name": "tsgo"}})
156174
tr.traceContent.WriteString(",\n")
157-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "M", Cat: "__metadata", TS: metaTs, Name: "thread_name", Args: map[string]any{"name": "Main"}})
175+
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "__metadata", TS: metaTs, Name: "thread_name", Args: map[string]any{"name": "Main"}})
158176
tr.traceContent.WriteString(",\n")
159-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "M", Cat: "disabled-by-default-devtools.timeline", TS: metaTs, Name: "TracingStartedInBrowser"})
177+
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "disabled-by-default-devtools.timeline", TS: metaTs, Name: "TracingStartedInBrowser"})
160178

161179
// Truncate any existing trace file with the header so subsequent AppendFile
162180
// calls extend a clean file.
@@ -226,8 +244,9 @@ func (tr *Tracing) Instant(phase Phase, name string, args map[string]any) {
226244
}
227245

228246
ts := tr.timestamp()
247+
tid := tr.threadIDLocked(args)
229248
tr.traceContent.WriteString(",\n")
230-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "I", Cat: string(phase), TS: ts, Name: name, S: "g", Args: args})
249+
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "I", Cat: string(phase), TS: ts, Name: name, S: "g", Args: args})
231250
tr.maybeFlushLocked()
232251
}
233252

@@ -257,8 +276,9 @@ func (tr *Tracing) Push(phase Phase, name string, args map[string]any, separateB
257276
return func() {}
258277
}
259278
ts := tr.timestamp()
279+
tid := tr.threadIDLocked(args)
260280
tr.traceContent.WriteString(",\n")
261-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "B", Cat: string(phase), TS: ts, Name: name, Args: args})
281+
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "B", Cat: string(phase), TS: ts, Name: name, Args: args})
262282
tr.maybeFlushLocked()
263283
tr.mu.Unlock()
264284

@@ -270,7 +290,7 @@ func (tr *Tracing) Push(phase Phase, name string, args map[string]any, separateB
270290
}
271291
endTs := tr.timestamp()
272292
tr.traceContent.WriteString(",\n")
273-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "E", Cat: string(phase), TS: endTs, Name: name, Args: args})
293+
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "E", Cat: string(phase), TS: endTs, Name: name, Args: args})
274294
tr.maybeFlushLocked()
275295
}
276296
}
@@ -295,12 +315,101 @@ func (tr *Tracing) Push(phase Phase, name string, args map[string]any, separateB
295315
if !tr.traceStarted.Load() {
296316
return
297317
}
318+
tid := tr.threadIDLocked(args)
298319
tr.traceContent.WriteString(",\n")
299-
tr.writeEvent(traceEvent{PID: 1, TID: 1, PH: "X", Cat: string(phase), TS: startMicros, Name: name, Dur: &dur, Args: args})
320+
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "X", Cat: string(phase), TS: startMicros, Name: name, Dur: &dur, Args: args})
300321
tr.maybeFlushLocked()
301322
}
302323
}
303324

325+
func (tr *Tracing) threadIDLocked(args map[string]any) int {
326+
key, ok := traceThreadKeyFromArgs(args)
327+
if !ok {
328+
return mainThreadID
329+
}
330+
331+
if tid, ok := tr.threadIDs[key]; ok {
332+
return tid
333+
}
334+
335+
tid := key.defaultThreadID()
336+
for {
337+
if existingKey, ok := tr.threadKeys[tid]; !ok || existingKey == key {
338+
break
339+
}
340+
tid++
341+
}
342+
tr.threadIDs[key] = tid
343+
tr.threadKeys[tid] = key
344+
tr.writeThreadNameEventLocked(tid, key.displayName())
345+
return tid
346+
}
347+
348+
func (tr *Tracing) writeThreadNameEventLocked(tid int, name string) {
349+
tr.traceContent.WriteString(",\n")
350+
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "M", Cat: "__metadata", TS: tr.metadataTS, Name: "thread_name", Args: map[string]any{"name": name}})
351+
}
352+
353+
type traceThreadKind string
354+
355+
const (
356+
traceThreadKindChecker traceThreadKind = "checker"
357+
traceThreadKindFile traceThreadKind = "file"
358+
)
359+
360+
type traceThreadKey struct {
361+
kind traceThreadKind
362+
text string
363+
index int
364+
hasIndex bool
365+
}
366+
367+
func traceThreadKeyFromArgs(args map[string]any) (traceThreadKey, bool) {
368+
if len(args) == 0 {
369+
return traceThreadKey{}, false
370+
}
371+
372+
if checkerID, ok := args["checkerId"].(int); ok {
373+
return traceThreadKey{kind: traceThreadKindChecker, index: checkerID, hasIndex: true}, true
374+
}
375+
376+
for _, key := range traceThreadArgKeys {
377+
if value, ok := args[key]; ok {
378+
if path, ok := value.(string); ok && path != "" {
379+
return traceThreadKey{kind: traceThreadKindFile, text: path}, true
380+
}
381+
}
382+
}
383+
384+
return traceThreadKey{}, false
385+
}
386+
387+
func (key traceThreadKey) defaultThreadID() int {
388+
if key.kind == traceThreadKindChecker && key.hasIndex && key.index >= 0 {
389+
return firstSyntheticThreadID + key.index
390+
}
391+
return stableTraceThreadID(key)
392+
}
393+
394+
func (key traceThreadKey) displayName() string {
395+
if key.hasIndex {
396+
return string(key.kind) + ":" + strconv.Itoa(key.index)
397+
}
398+
return string(key.kind) + ":" + key.text
399+
}
400+
401+
func stableTraceThreadID(key traceThreadKey) int {
402+
hash := xxh3.New()
403+
_, _ = hash.WriteString(string(key.kind))
404+
_, _ = hash.WriteString(":")
405+
if key.hasIndex {
406+
_, _ = hash.WriteString(strconv.Itoa(key.index))
407+
} else {
408+
_, _ = hash.WriteString(key.text)
409+
}
410+
return firstFileThreadID + int(hash.Sum64()%fileThreadIDHashRange)
411+
}
412+
304413
// NewTypeTracer creates a new tracer for a specific checker.
305414
// The checkerIndex is used to create unique filenames for each checker's output.
306415
func (tr *Tracing) NewTypeTracer(checkerIndex int) Tracer {
@@ -319,6 +428,7 @@ func (tr *Tracing) NewTypeTracer(checkerIndex int) Tracer {
319428
ConfigFilePath: tr.configFilePath,
320429
TracePath: tr.tracePath,
321430
TypesPath: typesPath,
431+
CheckerID: checkerIndex,
322432
})
323433
return tracer
324434
}

0 commit comments

Comments
 (0)