vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View 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,
},
}
}

View 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))
}
}