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
7679type traceEvent struct {
@@ -92,6 +95,15 @@ const sampleInterval = 10 * time.Millisecond
9295
9396const 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.
306415func (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