vendor tsgo
This commit is contained in:
763
tools/tsgo/internal/tracing/tracing.go
Normal file
763
tools/tsgo/internal/tracing/tracing.go
Normal file
@@ -0,0 +1,763 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/scanner"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/zeebo/xxh3"
|
||||
)
|
||||
|
||||
// Tracer is an interface for recording types during type checking.
|
||||
// Each checker should have its own Tracer instance to avoid sharing types between checkers.
|
||||
type Tracer interface {
|
||||
// RecordType records a type for later dumping.
|
||||
RecordType(t TracedType)
|
||||
// DumpTypes writes all recorded types to disk.
|
||||
DumpTypes() error
|
||||
}
|
||||
|
||||
// TracedType is an interface that represents a type that can be traced.
|
||||
// This allows the tracing package to work with types from the checker package
|
||||
// without creating a circular dependency.
|
||||
type TracedType interface {
|
||||
Id() uint32
|
||||
FormatFlags() []string
|
||||
IsConditional() bool
|
||||
Symbol() *ast.Symbol
|
||||
AliasSymbol() *ast.Symbol
|
||||
AliasTypeArguments() []TracedType
|
||||
|
||||
// Type-specific data accessors
|
||||
IntrinsicName() string
|
||||
UnionTypes() []TracedType
|
||||
IntersectionTypes() []TracedType
|
||||
IndexType() TracedType
|
||||
IndexedAccessObjectType() TracedType
|
||||
IndexedAccessIndexType() TracedType
|
||||
ConditionalCheckType() TracedType
|
||||
ConditionalExtendsType() TracedType
|
||||
ConditionalTrueType() TracedType
|
||||
ConditionalFalseType() TracedType
|
||||
SubstitutionBaseType() TracedType
|
||||
SubstitutionConstraintType() TracedType
|
||||
ReferenceTarget() TracedType
|
||||
ReferenceTypeArguments() []TracedType
|
||||
ReferenceNode() *ast.Node
|
||||
ReverseMappedSourceType() TracedType
|
||||
ReverseMappedMappedType() TracedType
|
||||
ReverseMappedConstraintType() TracedType
|
||||
EvolvingArrayElementType() TracedType
|
||||
EvolvingArrayFinalType() TracedType
|
||||
IsTuple() bool
|
||||
Pattern() *ast.Node
|
||||
RecursionIdentity() any
|
||||
|
||||
// Display is an optional string representation of the type
|
||||
Display() string
|
||||
}
|
||||
|
||||
// TraceRecord represents metadata about a single trace file
|
||||
type TraceRecord struct {
|
||||
ConfigFilePath string `json:"configFilePath,omitzero"`
|
||||
TracePath string `json:"tracePath,omitzero"`
|
||||
TypesPath string `json:"typesPath,omitzero"`
|
||||
CheckerID int `json:"checkerId"`
|
||||
}
|
||||
|
||||
type traceEvent struct {
|
||||
PID int `json:"pid"`
|
||||
TID int `json:"tid"`
|
||||
PH string `json:"ph"`
|
||||
Cat string `json:"cat"`
|
||||
TS float64 `json:"ts"`
|
||||
Name string `json:"name,omitzero"`
|
||||
S string `json:"s,omitzero"` // scope, only set for instant events ("g" = global)
|
||||
Dur *float64 `json:"dur,omitzero"`
|
||||
Args map[string]any `json:"args,omitzero"`
|
||||
}
|
||||
|
||||
// sampleInterval matches TypeScript's 10ms sampling interval.
|
||||
// Events with separateBeginAndEnd=false are only recorded if their
|
||||
// duration crosses a 10ms sampling boundary.
|
||||
const sampleInterval = 10 * time.Millisecond
|
||||
|
||||
const traceFileName = "trace.json"
|
||||
|
||||
const (
|
||||
mainThreadID = 1
|
||||
firstSyntheticThreadID = 2
|
||||
firstFileThreadID = 1_000_000
|
||||
fileThreadIDHashRange = 1_000_000_000
|
||||
)
|
||||
|
||||
var traceThreadArgKeys = [...]string{"path", "fileName", "containingFileName", "jsFilePath", "declarationFilePath"}
|
||||
|
||||
// flushThreshold is the size at which buffered trace content is flushed to disk
|
||||
// via AppendFile. Keeps peak memory bounded for long-running compilations while
|
||||
// avoiding a syscall per event.
|
||||
const flushThreshold = 256 * 1024
|
||||
|
||||
// Tracing manages the overall tracing session including all checkers
|
||||
type Tracing struct {
|
||||
fs vfs.FS
|
||||
traceDir string
|
||||
tracePath string
|
||||
configFilePath string
|
||||
legend []TraceRecord
|
||||
tracers []*typeTracer
|
||||
traceContent strings.Builder
|
||||
traceStarted atomic.Bool
|
||||
threadIDs map[traceThreadKey]int
|
||||
threadKeys map[int]traceThreadKey
|
||||
metadataTS float64
|
||||
deterministic bool // when true, use monotonic counter instead of real time
|
||||
timestampCounter uint64 // only used in deterministic mode
|
||||
startTime time.Time
|
||||
mu sync.Mutex
|
||||
// flushErr holds the first error encountered while appending the trace buffer
|
||||
// to disk. Once set, subsequent flushes become no-ops and the error is
|
||||
// surfaced from StopTracing so that transient I/O failures (disk full,
|
||||
// permission denied, etc.) don't crash the compiler.
|
||||
flushErr error
|
||||
}
|
||||
|
||||
// Phase represents a tracing phase
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
PhaseParse Phase = "parse"
|
||||
PhaseProgram Phase = "program"
|
||||
PhaseBind Phase = "bind"
|
||||
PhaseCheck Phase = "check"
|
||||
PhaseCheckTypes Phase = "checkTypes"
|
||||
PhaseEmit Phase = "emit"
|
||||
PhaseSession Phase = "session"
|
||||
)
|
||||
|
||||
// StartTracing creates a new tracing session.
|
||||
// When deterministic is true, timestamps use a monotonic counter instead of
|
||||
// real wall-clock time, producing stable output for test baselines.
|
||||
func StartTracing(fs vfs.FS, traceDir string, configFilePath string, deterministic bool) (*Tracing, error) {
|
||||
tr := &Tracing{
|
||||
fs: fs,
|
||||
traceDir: traceDir,
|
||||
tracePath: tspath.CombinePaths(traceDir, traceFileName),
|
||||
configFilePath: configFilePath,
|
||||
legend: []TraceRecord{},
|
||||
tracers: []*typeTracer{},
|
||||
threadIDs: make(map[traceThreadKey]int),
|
||||
threadKeys: make(map[int]traceThreadKey),
|
||||
deterministic: deterministic,
|
||||
startTime: time.Now(),
|
||||
}
|
||||
tr.traceStarted.Store(true)
|
||||
|
||||
// Write the trace file header with metadata events
|
||||
tr.traceContent.WriteString("[\n")
|
||||
|
||||
// Write metadata events (matching TypeScript's format)
|
||||
metaTs := tr.timestamp()
|
||||
tr.metadataTS = metaTs
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "__metadata", TS: metaTs, Name: "process_name", Args: map[string]any{"name": "tsgo"}})
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "__metadata", TS: metaTs, Name: "thread_name", Args: map[string]any{"name": "Main"}})
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: mainThreadID, PH: "M", Cat: "disabled-by-default-devtools.timeline", TS: metaTs, Name: "TracingStartedInBrowser"})
|
||||
|
||||
// Truncate any existing trace file with the header so subsequent AppendFile
|
||||
// calls extend a clean file.
|
||||
if err := tr.fs.WriteFile(tr.tracePath, tr.traceContent.String()); err != nil {
|
||||
return nil, fmt.Errorf("failed to write trace file header: %w", err)
|
||||
}
|
||||
tr.traceContent.Reset()
|
||||
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
// timestamp returns the current timestamp in microseconds.
|
||||
// In deterministic mode it returns a monotonically increasing counter;
|
||||
// otherwise it returns the real elapsed wall-clock time since tracing started,
|
||||
// matching TypeScript's 1000 * timestamp() (microseconds).
|
||||
func (tr *Tracing) timestamp() float64 {
|
||||
if tr.deterministic {
|
||||
tr.timestampCounter++
|
||||
return float64(tr.timestampCounter)
|
||||
}
|
||||
return float64(time.Since(tr.startTime).Nanoseconds()) / 1000.0
|
||||
}
|
||||
|
||||
func writeEventTo(buf *strings.Builder, event traceEvent) {
|
||||
if err := json.MarshalWrite(buf, event, json.Deterministic(true)); err != nil {
|
||||
panic(fmt.Sprintf("failed to marshal trace event: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *Tracing) writeEvent(event traceEvent) {
|
||||
writeEventTo(&tr.traceContent, event)
|
||||
}
|
||||
|
||||
// maybeFlushLocked appends the buffered trace content to disk if it has grown
|
||||
// past the flush threshold. Caller must hold tr.mu. If a previous flush failed,
|
||||
// or this flush fails, the error is recorded in tr.flushErr and subsequent
|
||||
// writes become no-ops; the error is surfaced from StopTracing.
|
||||
func (tr *Tracing) maybeFlushLocked() {
|
||||
if tr.flushErr != nil {
|
||||
tr.traceContent.Reset()
|
||||
return
|
||||
}
|
||||
if tr.traceContent.Len() < flushThreshold {
|
||||
return
|
||||
}
|
||||
if err := tr.fs.AppendFile(tr.tracePath, tr.traceContent.String()); err != nil {
|
||||
tr.flushErr = fmt.Errorf("failed to flush trace file: %w", err)
|
||||
}
|
||||
tr.traceContent.Reset()
|
||||
}
|
||||
|
||||
// Instant records an instant event in the trace.
|
||||
// Safe to call on nil receiver.
|
||||
func (tr *Tracing) Instant(phase Phase, name string, args map[string]any) {
|
||||
if tr == nil || !tr.traceStarted.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
|
||||
// Re-check under the lock: StopTracing may have run between the load above
|
||||
// and acquiring the lock. Once stopped, further writes would land in a buffer
|
||||
// that has already been flushed and the closing "]" written.
|
||||
if !tr.traceStarted.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
ts := tr.timestamp()
|
||||
tid := tr.threadIDLocked(args)
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "I", Cat: string(phase), TS: ts, Name: name, S: "g", Args: args})
|
||||
tr.maybeFlushLocked()
|
||||
}
|
||||
|
||||
// Push starts a trace event block on the shared trace buffer.
|
||||
// Safe to call on nil receiver. Safe to call from multiple goroutines.
|
||||
//
|
||||
// When separateBeginAndEnd is true, a "B" (begin) event is written immediately and
|
||||
// the returned function writes a matching "E" (end) event. This is used for events
|
||||
// that must always appear in the trace (e.g. checkSourceFile, createProgram, emit).
|
||||
//
|
||||
// When separateBeginAndEnd is false (the default in TypeScript), the event is only
|
||||
// recorded if its duration crosses a 10ms sampling boundary, matching TypeScript's
|
||||
// behavior of sampling short-lived events to avoid trace bloat.
|
||||
//
|
||||
// Returns a function that should be called (typically deferred) to end the event.
|
||||
// Each returned function is self-contained and does not depend on a shared stack,
|
||||
// making it safe for concurrent use across goroutines.
|
||||
func (tr *Tracing) Push(phase Phase, name string, args map[string]any, separateBeginAndEnd bool) func() {
|
||||
if tr == nil || !tr.traceStarted.Load() {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
if separateBeginAndEnd {
|
||||
tr.mu.Lock()
|
||||
if !tr.traceStarted.Load() {
|
||||
tr.mu.Unlock()
|
||||
return func() {}
|
||||
}
|
||||
ts := tr.timestamp()
|
||||
tid := tr.threadIDLocked(args)
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "B", Cat: string(phase), TS: ts, Name: name, Args: args})
|
||||
tr.maybeFlushLocked()
|
||||
tr.mu.Unlock()
|
||||
|
||||
return func() {
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
if !tr.traceStarted.Load() {
|
||||
return
|
||||
}
|
||||
endTs := tr.timestamp()
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "E", Cat: string(phase), TS: endTs, Name: name, Args: args})
|
||||
tr.maybeFlushLocked()
|
||||
}
|
||||
}
|
||||
|
||||
// Sampled event: only record if duration crosses a sampling boundary.
|
||||
// In deterministic mode, sampled events are skipped entirely to avoid flaky baselines,
|
||||
// so avoid the cost of cloning args / capturing the start time.
|
||||
if tr.deterministic {
|
||||
return func() {}
|
||||
}
|
||||
startTime := time.Now()
|
||||
args = maps.Clone(args)
|
||||
return func() {
|
||||
dur := float64(time.Since(startTime).Nanoseconds()) / 1000.0
|
||||
startMicros := float64(startTime.Sub(tr.startTime).Nanoseconds()) / 1000.0
|
||||
intervalMicros := float64(sampleInterval.Nanoseconds()) / 1000.0
|
||||
if intervalMicros-math.Mod(startMicros, intervalMicros) > dur {
|
||||
return
|
||||
}
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
if !tr.traceStarted.Load() {
|
||||
return
|
||||
}
|
||||
tid := tr.threadIDLocked(args)
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "X", Cat: string(phase), TS: startMicros, Name: name, Dur: &dur, Args: args})
|
||||
tr.maybeFlushLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *Tracing) threadIDLocked(args map[string]any) int {
|
||||
key, ok := traceThreadKeyFromArgs(args)
|
||||
if !ok {
|
||||
return mainThreadID
|
||||
}
|
||||
|
||||
if tid, ok := tr.threadIDs[key]; ok {
|
||||
return tid
|
||||
}
|
||||
|
||||
tid := key.defaultThreadID()
|
||||
for {
|
||||
if existingKey, ok := tr.threadKeys[tid]; !ok || existingKey == key {
|
||||
break
|
||||
}
|
||||
tid++
|
||||
}
|
||||
tr.threadIDs[key] = tid
|
||||
tr.threadKeys[tid] = key
|
||||
tr.writeThreadNameEventLocked(tid, key.displayName())
|
||||
return tid
|
||||
}
|
||||
|
||||
func (tr *Tracing) writeThreadNameEventLocked(tid int, name string) {
|
||||
tr.traceContent.WriteString(",\n")
|
||||
tr.writeEvent(traceEvent{PID: 1, TID: tid, PH: "M", Cat: "__metadata", TS: tr.metadataTS, Name: "thread_name", Args: map[string]any{"name": name}})
|
||||
}
|
||||
|
||||
type traceThreadKind string
|
||||
|
||||
const (
|
||||
traceThreadKindChecker traceThreadKind = "checker"
|
||||
traceThreadKindFile traceThreadKind = "file"
|
||||
)
|
||||
|
||||
type traceThreadKey struct {
|
||||
kind traceThreadKind
|
||||
text string
|
||||
index int
|
||||
hasIndex bool
|
||||
}
|
||||
|
||||
func traceThreadKeyFromArgs(args map[string]any) (traceThreadKey, bool) {
|
||||
if len(args) == 0 {
|
||||
return traceThreadKey{}, false
|
||||
}
|
||||
|
||||
if checkerID, ok := args["checkerId"].(int); ok {
|
||||
return traceThreadKey{kind: traceThreadKindChecker, index: checkerID, hasIndex: true}, true
|
||||
}
|
||||
|
||||
for _, key := range traceThreadArgKeys {
|
||||
if value, ok := args[key]; ok {
|
||||
if path, ok := value.(string); ok && path != "" {
|
||||
return traceThreadKey{kind: traceThreadKindFile, text: path}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return traceThreadKey{}, false
|
||||
}
|
||||
|
||||
func (key traceThreadKey) defaultThreadID() int {
|
||||
if key.kind == traceThreadKindChecker && key.hasIndex && key.index >= 0 {
|
||||
return firstSyntheticThreadID + key.index
|
||||
}
|
||||
return stableTraceThreadID(key)
|
||||
}
|
||||
|
||||
func (key traceThreadKey) displayName() string {
|
||||
if key.hasIndex {
|
||||
return string(key.kind) + ":" + strconv.Itoa(key.index)
|
||||
}
|
||||
return string(key.kind) + ":" + key.text
|
||||
}
|
||||
|
||||
func stableTraceThreadID(key traceThreadKey) int {
|
||||
hash := xxh3.New()
|
||||
_, _ = hash.WriteString(string(key.kind))
|
||||
_, _ = hash.WriteString(":")
|
||||
if key.hasIndex {
|
||||
_, _ = hash.WriteString(strconv.Itoa(key.index))
|
||||
} else {
|
||||
_, _ = hash.WriteString(key.text)
|
||||
}
|
||||
return firstFileThreadID + int(hash.Sum64()%fileThreadIDHashRange)
|
||||
}
|
||||
|
||||
// NewTypeTracer creates a new tracer for a specific checker.
|
||||
// The checkerIndex is used to create unique filenames for each checker's output.
|
||||
func (tr *Tracing) NewTypeTracer(checkerIndex int) Tracer {
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
|
||||
typesPath := tspath.CombinePaths(tr.traceDir, fmt.Sprintf("types_%d.json", checkerIndex))
|
||||
tracer := &typeTracer{
|
||||
fs: tr.fs,
|
||||
checkerIndex: checkerIndex,
|
||||
typesPath: typesPath,
|
||||
types: []TracedType{},
|
||||
}
|
||||
tr.tracers = append(tr.tracers, tracer)
|
||||
tr.legend = append(tr.legend, TraceRecord{
|
||||
ConfigFilePath: tr.configFilePath,
|
||||
TracePath: tr.tracePath,
|
||||
TypesPath: typesPath,
|
||||
CheckerID: checkerIndex,
|
||||
})
|
||||
return tracer
|
||||
}
|
||||
|
||||
// StopTracing finalizes the tracing session and writes all output files
|
||||
func (tr *Tracing) StopTracing() error {
|
||||
// Dump types from all tracers BEFORE acquiring the lock, because
|
||||
// DumpTypes → buildTypeDescriptor → Display() → TypeToString can
|
||||
// re-enter the checker which calls Push/Pop (which need tr.mu).
|
||||
for _, tracer := range tr.tracers {
|
||||
if err := tracer.DumpTypes(); err != nil {
|
||||
return fmt.Errorf("failed to dump types for checker %d: %w", tracer.checkerIndex, err)
|
||||
}
|
||||
}
|
||||
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
|
||||
// Close the trace file(s)
|
||||
if tr.traceStarted.Load() {
|
||||
// Surface any buffered flush failure before attempting the final write.
|
||||
if tr.flushErr != nil {
|
||||
tr.traceContent.Reset()
|
||||
tr.traceStarted.Store(false)
|
||||
return tr.flushErr
|
||||
}
|
||||
// Flush any remaining buffered content and close the JSON array.
|
||||
if err := tr.fs.AppendFile(tr.tracePath, tr.traceContent.String()+"\n]\n"); err != nil {
|
||||
return fmt.Errorf("failed to write trace file: %w", err)
|
||||
}
|
||||
tr.traceContent.Reset()
|
||||
tr.traceStarted.Store(false)
|
||||
}
|
||||
|
||||
// Sort legend entries by typesPath for deterministic output
|
||||
slices.SortFunc(tr.legend, func(a, b TraceRecord) int {
|
||||
return strings.Compare(a.TypesPath, b.TypesPath)
|
||||
})
|
||||
|
||||
// Write the legend file
|
||||
legendPath := tspath.CombinePaths(tr.traceDir, "legend.json")
|
||||
legendData, err := json.MarshalIndent(tr.legend, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal legend file: %w", err)
|
||||
}
|
||||
if err := tr.fs.WriteFile(legendPath, string(legendData)); err != nil {
|
||||
return fmt.Errorf("failed to write legend file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// typeTracer is the per-checker tracer implementation
|
||||
type typeTracer struct {
|
||||
fs vfs.FS
|
||||
checkerIndex int
|
||||
typesPath string
|
||||
types []TracedType
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *typeTracer) RecordType(typ TracedType) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.types = append(t.types, typ)
|
||||
}
|
||||
|
||||
func (t *typeTracer) DumpTypes() error {
|
||||
// Copy the types slice under lock, then release so Display() calls during
|
||||
// buildTypeDescriptor don't deadlock when they create new types
|
||||
t.mu.Lock()
|
||||
types := slices.Clone(t.types)
|
||||
t.mu.Unlock()
|
||||
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
// Write opening bracket (no newline so type ID matches line number)
|
||||
sb.WriteString("[")
|
||||
|
||||
recursionIdentityMap := make(map[any]int)
|
||||
|
||||
for i, typ := range types {
|
||||
descriptor := t.buildTypeDescriptor(typ, recursionIdentityMap)
|
||||
|
||||
if err := json.MarshalWrite(&sb, descriptor); err != nil {
|
||||
return fmt.Errorf("failed to marshal type %d: %w", typ.Id(), err)
|
||||
}
|
||||
|
||||
if i < len(types)-1 {
|
||||
sb.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("]\n")
|
||||
|
||||
return t.fs.WriteFile(t.typesPath, sb.String())
|
||||
}
|
||||
|
||||
// TypeDescriptor represents a type in the output JSON
|
||||
type TypeDescriptor struct {
|
||||
ID uint32 `json:"id"`
|
||||
IntrinsicName string `json:"intrinsicName,omitzero"`
|
||||
SymbolName string `json:"symbolName,omitzero"`
|
||||
RecursionID *int `json:"recursionId,omitzero"`
|
||||
IsTuple bool `json:"isTuple,omitzero"`
|
||||
UnionTypes []uint32 `json:"unionTypes,omitzero"`
|
||||
IntersectionTypes []uint32 `json:"intersectionTypes,omitzero"`
|
||||
AliasTypeArguments []uint32 `json:"aliasTypeArguments,omitzero"`
|
||||
KeyofType *uint32 `json:"keyofType,omitzero"`
|
||||
IndexedAccessObjectType *uint32 `json:"indexedAccessObjectType,omitzero"`
|
||||
IndexedAccessIndexType *uint32 `json:"indexedAccessIndexType,omitzero"`
|
||||
ConditionalCheckType *uint32 `json:"conditionalCheckType,omitzero"`
|
||||
ConditionalExtendsType *uint32 `json:"conditionalExtendsType,omitzero"`
|
||||
// ConditionalTrueType and ConditionalFalseType are *int32 (not *uint32) because
|
||||
// unresolved conditional branches are serialized as -1, matching TypeScript's behavior.
|
||||
ConditionalTrueType *int32 `json:"conditionalTrueType,omitzero"`
|
||||
ConditionalFalseType *int32 `json:"conditionalFalseType,omitzero"`
|
||||
SubstitutionBaseType *uint32 `json:"substitutionBaseType,omitzero"`
|
||||
ConstraintType *uint32 `json:"constraintType,omitzero"`
|
||||
InstantiatedType *uint32 `json:"instantiatedType,omitzero"`
|
||||
TypeArguments []uint32 `json:"typeArguments,omitzero"`
|
||||
ReferenceLocation *Location `json:"referenceLocation,omitzero"`
|
||||
ReverseMappedSourceType *uint32 `json:"reverseMappedSourceType,omitzero"`
|
||||
ReverseMappedMappedType *uint32 `json:"reverseMappedMappedType,omitzero"`
|
||||
ReverseMappedConstraintType *uint32 `json:"reverseMappedConstraintType,omitzero"`
|
||||
EvolvingArrayElementType *uint32 `json:"evolvingArrayElementType,omitzero"`
|
||||
EvolvingArrayFinalType *uint32 `json:"evolvingArrayFinalType,omitzero"`
|
||||
DestructuringPattern *Location `json:"destructuringPattern,omitzero"`
|
||||
FirstDeclaration *Location `json:"firstDeclaration,omitzero"`
|
||||
Flags []string `json:"flags"`
|
||||
Display string `json:"display,omitzero"`
|
||||
}
|
||||
|
||||
// Location represents a source code location
|
||||
type Location struct {
|
||||
Path string `json:"path"`
|
||||
Start *LineAndChar `json:"start,omitzero"`
|
||||
End *LineAndChar `json:"end,omitzero"`
|
||||
}
|
||||
|
||||
// LineAndChar represents a line and character position (1-indexed)
|
||||
type LineAndChar struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
}
|
||||
|
||||
func (t *typeTracer) buildTypeDescriptor(typ TracedType, recursionIdentityMap map[any]int) TypeDescriptor {
|
||||
symbol := typ.Symbol()
|
||||
aliasSymbol := typ.AliasSymbol()
|
||||
|
||||
desc := TypeDescriptor{
|
||||
ID: typ.Id(),
|
||||
Flags: typ.FormatFlags(),
|
||||
}
|
||||
|
||||
// Assign a unique integer token per recursion identity, matching TypeScript's behavior.
|
||||
// This lets trace analysis tools detect which types share the same recursion identity.
|
||||
if identity := typ.RecursionIdentity(); identity != nil {
|
||||
token, ok := recursionIdentityMap[identity]
|
||||
if !ok {
|
||||
token = len(recursionIdentityMap)
|
||||
recursionIdentityMap[identity] = token
|
||||
}
|
||||
desc.RecursionID = &token
|
||||
}
|
||||
|
||||
// Intrinsic name
|
||||
if name := typ.IntrinsicName(); name != "" {
|
||||
desc.IntrinsicName = name
|
||||
}
|
||||
|
||||
// Symbol name - escape the internal symbol name prefix for valid JSON
|
||||
if sym := aliasSymbol; sym != nil {
|
||||
desc.SymbolName = ast.EscapeAllInternalSymbolNames(sym.Name)
|
||||
} else if symbol != nil {
|
||||
desc.SymbolName = ast.EscapeAllInternalSymbolNames(symbol.Name)
|
||||
}
|
||||
|
||||
// Tuple flag
|
||||
if typ.IsTuple() {
|
||||
desc.IsTuple = true
|
||||
}
|
||||
|
||||
// Union types
|
||||
if types := typ.UnionTypes(); len(types) > 0 {
|
||||
desc.UnionTypes = mapTypeIds(types)
|
||||
}
|
||||
|
||||
// Intersection types
|
||||
if types := typ.IntersectionTypes(); len(types) > 0 {
|
||||
desc.IntersectionTypes = mapTypeIds(types)
|
||||
}
|
||||
|
||||
// Alias type arguments
|
||||
if args := typ.AliasTypeArguments(); len(args) > 0 {
|
||||
desc.AliasTypeArguments = mapTypeIds(args)
|
||||
}
|
||||
|
||||
// Index type (keyof)
|
||||
if indexType := typ.IndexType(); indexType != nil {
|
||||
desc.KeyofType = new(indexType.Id())
|
||||
}
|
||||
|
||||
// Indexed access type
|
||||
if objType := typ.IndexedAccessObjectType(); objType != nil {
|
||||
desc.IndexedAccessObjectType = new(objType.Id())
|
||||
}
|
||||
if idxType := typ.IndexedAccessIndexType(); idxType != nil {
|
||||
desc.IndexedAccessIndexType = new(idxType.Id())
|
||||
}
|
||||
|
||||
// Conditional type
|
||||
if typ.IsConditional() {
|
||||
if checkType := typ.ConditionalCheckType(); checkType != nil {
|
||||
desc.ConditionalCheckType = new(checkType.Id())
|
||||
}
|
||||
if extendsType := typ.ConditionalExtendsType(); extendsType != nil {
|
||||
desc.ConditionalExtendsType = new(extendsType.Id())
|
||||
}
|
||||
if trueType := typ.ConditionalTrueType(); trueType != nil {
|
||||
desc.ConditionalTrueType = new(int32(trueType.Id()))
|
||||
} else {
|
||||
desc.ConditionalTrueType = new(int32(-1))
|
||||
}
|
||||
if falseType := typ.ConditionalFalseType(); falseType != nil {
|
||||
desc.ConditionalFalseType = new(int32(falseType.Id()))
|
||||
} else {
|
||||
desc.ConditionalFalseType = new(int32(-1))
|
||||
}
|
||||
}
|
||||
|
||||
// Substitution type
|
||||
if baseType := typ.SubstitutionBaseType(); baseType != nil {
|
||||
desc.SubstitutionBaseType = new(baseType.Id())
|
||||
}
|
||||
if constraint := typ.SubstitutionConstraintType(); constraint != nil {
|
||||
desc.ConstraintType = new(constraint.Id())
|
||||
}
|
||||
|
||||
// Reference type
|
||||
if target := typ.ReferenceTarget(); target != nil {
|
||||
desc.InstantiatedType = new(target.Id())
|
||||
}
|
||||
if args := typ.ReferenceTypeArguments(); len(args) > 0 {
|
||||
desc.TypeArguments = mapTypeIds(args)
|
||||
}
|
||||
if node := typ.ReferenceNode(); node != nil {
|
||||
desc.ReferenceLocation = getLocation(node)
|
||||
}
|
||||
|
||||
// Reverse mapped type
|
||||
if sourceType := typ.ReverseMappedSourceType(); sourceType != nil {
|
||||
desc.ReverseMappedSourceType = new(sourceType.Id())
|
||||
}
|
||||
if mappedType := typ.ReverseMappedMappedType(); mappedType != nil {
|
||||
desc.ReverseMappedMappedType = new(mappedType.Id())
|
||||
}
|
||||
if constraintType := typ.ReverseMappedConstraintType(); constraintType != nil {
|
||||
desc.ReverseMappedConstraintType = new(constraintType.Id())
|
||||
}
|
||||
|
||||
// Evolving array type
|
||||
if elemType := typ.EvolvingArrayElementType(); elemType != nil {
|
||||
desc.EvolvingArrayElementType = new(elemType.Id())
|
||||
}
|
||||
if finalType := typ.EvolvingArrayFinalType(); finalType != nil {
|
||||
desc.EvolvingArrayFinalType = new(finalType.Id())
|
||||
}
|
||||
|
||||
// Pattern (destructuring)
|
||||
if pattern := typ.Pattern(); pattern != nil {
|
||||
desc.DestructuringPattern = getLocation(pattern)
|
||||
}
|
||||
|
||||
// First declaration - prefer aliasSymbol, matching TypeScript's `aliasSymbol ?? symbol`
|
||||
firstDeclSymbol := aliasSymbol
|
||||
if firstDeclSymbol == nil {
|
||||
firstDeclSymbol = symbol
|
||||
}
|
||||
if firstDeclSymbol != nil && len(firstDeclSymbol.Declarations) > 0 {
|
||||
desc.FirstDeclaration = getLocation(firstDeclSymbol.Declarations[0])
|
||||
}
|
||||
|
||||
// Display text
|
||||
if display := typ.Display(); display != "" {
|
||||
desc.Display = display
|
||||
}
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
func mapTypeIds(types []TracedType) []uint32 {
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint32, len(types))
|
||||
for i, t := range types {
|
||||
if t != nil {
|
||||
ids[i] = t.Id()
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func getLocation(node *ast.Node) *Location {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
file := ast.GetSourceFileOfNode(node)
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
startPos := scanner.GetTokenPosOfNode(node, file, false)
|
||||
startLine, startChar := scanner.GetECMALineAndUTF16CharacterOfPosition(file, startPos)
|
||||
endLine, endChar := scanner.GetECMALineAndUTF16CharacterOfPosition(file, node.End())
|
||||
|
||||
return &Location{
|
||||
Path: string(tspath.ToPath(file.FileName(), "", false)),
|
||||
Start: &LineAndChar{
|
||||
Line: startLine + 1,
|
||||
Character: int(startChar) + 1,
|
||||
},
|
||||
End: &LineAndChar{
|
||||
Line: endLine + 1,
|
||||
Character: int(endChar) + 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
140
tools/tsgo/internal/tracing/tracing_test.go
Normal file
140
tools/tsgo/internal/tracing/tracing_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/json"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestConcurrentDurationEventsUseSeparateThreadIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fsys := vfstest.FromMap(fstest.MapFS{
|
||||
"/trace": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
}, true)
|
||||
|
||||
tr, err := StartTracing(fsys, "/trace", "", true /*deterministic*/)
|
||||
assert.NilError(t, err)
|
||||
|
||||
endA := tr.Push(PhaseParse, "createSourceFile", map[string]any{"path": "/a.ts"}, true)
|
||||
endB := tr.Push(PhaseParse, "createSourceFile", map[string]any{"path": "/b.ts"}, true)
|
||||
endA()
|
||||
endB()
|
||||
|
||||
endCheck := tr.Push(PhaseCheck, "checkSourceFile", map[string]any{"checkerId": 0, "path": "/a.ts"}, true)
|
||||
endVariance := tr.Push(PhaseCheckTypes, "getVariancesWorker", map[string]any{"checkerId": 0, "id": 1}, true)
|
||||
endVariance()
|
||||
endCheck()
|
||||
|
||||
assert.NilError(t, tr.StopTracing())
|
||||
|
||||
traceText, ok := fsys.ReadFile("/trace/trace.json")
|
||||
assert.Assert(t, ok)
|
||||
|
||||
var events []traceEvent
|
||||
assert.NilError(t, json.Unmarshal([]byte(traceText), &events))
|
||||
|
||||
aBegin := findEvent(t, events, "B", "createSourceFile", "path", "/a.ts")
|
||||
aEnd := findEvent(t, events, "E", "createSourceFile", "path", "/a.ts")
|
||||
bBegin := findEvent(t, events, "B", "createSourceFile", "path", "/b.ts")
|
||||
bEnd := findEvent(t, events, "E", "createSourceFile", "path", "/b.ts")
|
||||
assert.Equal(t, aBegin.TID, aEnd.TID)
|
||||
assert.Equal(t, bBegin.TID, bEnd.TID)
|
||||
assert.Assert(t, aBegin.TID != bBegin.TID)
|
||||
assertThreadName(t, events, aBegin.TID, "file:/a.ts")
|
||||
assertThreadName(t, events, bBegin.TID, "file:/b.ts")
|
||||
|
||||
checkBegin := findEvent(t, events, "B", "checkSourceFile", "path", "/a.ts")
|
||||
varianceBegin := findEvent(t, events, "B", "getVariancesWorker", "id", float64(1))
|
||||
assert.Equal(t, checkBegin.TID, varianceBegin.TID)
|
||||
assertThreadName(t, events, checkBegin.TID, "checker:0")
|
||||
|
||||
assertDurationEventsAreWellNestedByThread(t, events)
|
||||
}
|
||||
|
||||
func TestThreadIDsAreStableAcrossFirstSeenOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first := traceThreadIDsForPaths(t, []string{"/a.ts", "/b.ts"})
|
||||
second := traceThreadIDsForPaths(t, []string{"/b.ts", "/a.ts"})
|
||||
|
||||
assert.DeepEqual(t, first, second)
|
||||
}
|
||||
|
||||
func traceThreadIDsForPaths(t *testing.T, paths []string) map[string]int {
|
||||
t.Helper()
|
||||
|
||||
fsys := vfstest.FromMap(fstest.MapFS{
|
||||
"/trace": &fstest.MapFile{Mode: fs.ModeDir},
|
||||
}, true)
|
||||
|
||||
tr, err := StartTracing(fsys, "/trace", "", true /*deterministic*/)
|
||||
assert.NilError(t, err)
|
||||
|
||||
for _, path := range paths {
|
||||
end := tr.Push(PhaseParse, "createSourceFile", map[string]any{"path": path}, true)
|
||||
end()
|
||||
}
|
||||
|
||||
assert.NilError(t, tr.StopTracing())
|
||||
|
||||
traceText, ok := fsys.ReadFile("/trace/trace.json")
|
||||
assert.Assert(t, ok)
|
||||
|
||||
var events []traceEvent
|
||||
assert.NilError(t, json.Unmarshal([]byte(traceText), &events))
|
||||
|
||||
threadIDs := make(map[string]int)
|
||||
for _, path := range paths {
|
||||
threadIDs[path] = findEvent(t, events, "B", "createSourceFile", "path", path).TID
|
||||
}
|
||||
return threadIDs
|
||||
}
|
||||
|
||||
func findEvent(t *testing.T, events []traceEvent, phase string, name string, argName string, argValue any) traceEvent {
|
||||
t.Helper()
|
||||
for _, event := range events {
|
||||
if event.PH == phase && event.Name == name && event.Args[argName] == argValue {
|
||||
return event
|
||||
}
|
||||
}
|
||||
t.Fatalf("failed to find %s event %q with %s=%v", phase, name, argName, argValue)
|
||||
return traceEvent{}
|
||||
}
|
||||
|
||||
func assertThreadName(t *testing.T, events []traceEvent, tid int, name string) {
|
||||
t.Helper()
|
||||
for _, event := range events {
|
||||
if event.PH == "M" && event.Name == "thread_name" && event.TID == tid && event.Args["name"] == name {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("failed to find thread_name metadata for thread %d named %q", tid, name)
|
||||
}
|
||||
|
||||
func assertDurationEventsAreWellNestedByThread(t *testing.T, events []traceEvent) {
|
||||
t.Helper()
|
||||
|
||||
stacks := make(map[int][]traceEvent)
|
||||
for _, event := range events {
|
||||
switch event.PH {
|
||||
case "B":
|
||||
stacks[event.TID] = append(stacks[event.TID], event)
|
||||
case "E":
|
||||
stack := stacks[event.TID]
|
||||
assert.Assert(t, len(stack) > 0, "unmatched end event %q on thread %d", event.Name, event.TID)
|
||||
begin := stack[len(stack)-1]
|
||||
assert.Equal(t, begin.Cat, event.Cat)
|
||||
assert.Equal(t, begin.Name, event.Name)
|
||||
stacks[event.TID] = stack[:len(stack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
for tid, stack := range stacks {
|
||||
assert.Assert(t, len(stack) == 0, "thread %d has %d unterminated events", tid, len(stack))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user