Documentation
¶
Overview ¶
Package trace is v5's cross-thread devtools timeline (plan item P4.3).
The criterion is that a single timeline correlates render-thread and domain-worker activity by correlation ID, and that a command's full lifecycle is traceable across the boundary.
The thing that makes this harder than appending two logs: THE TWO THREADS DO NOT SHARE A CLOCK. performance.now() is relative to a time origin that a worker sets when it starts, so a worker span timestamped 12.4 and a main thread span timestamped 12.4 did not happen at the same moment, and nothing in the numbers says so. Merging by timestamp produces a timeline that looks authoritative and is wrong — commands appearing to complete before they were sent, work appearing to overlap that could not have.
So ordering is CAUSAL, not chronological. A span records which thread it came from and what caused it, and the timeline reconstructs the order from that. Timestamps are kept, because within one thread they are meaningful and are the only source of durations, but they never order across threads. A caller who genuinely needs cross-thread wall-clock alignment must supply a measured ClockOffset, and the error bound comes with it.
Index ¶
- Constants
- type ClockOffset
- type CorrelationID
- type LifecycleSummary
- type Recorder
- func (parseRecorder *Recorder) Dropped() int
- func (parseRecorder *Recorder) End(parseSpanID SpanID, parseEndNanos int64, parseErr string) error
- func (parseRecorder *Recorder) Spans() []Span
- func (parseRecorder *Recorder) Start(parseName string, parseCorrelation CorrelationID, parseParent SpanID, ...) (SpanID, error)
- type Span
- type SpanID
- type Thread
- type Timeline
- func (parseTimeline *Timeline) Correlations() []CorrelationID
- func (parseTimeline *Timeline) Dropped() int
- func (parseTimeline *Timeline) Lifecycle(parseCorrelation CorrelationID) []Span
- func (parseTimeline *Timeline) Summarize(parseCorrelation CorrelationID) (LifecycleSummary, bool)
- func (parseTimeline *Timeline) WithClockOffset(parseOffset ClockOffset) *Timeline
Constants ¶
const DefaultCapacity = 4096
DefaultCapacity is how many spans a recorder keeps.
Bounded because R6 says diagnostics carry an allocation budget and because a tracing buffer that grows for the life of a session is a leak with a friendly name. Oldest spans are dropped first: a devtools panel is nearly always asked about something that just happened.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ClockOffset ¶
type ClockOffset struct {
// Thread whose clock is being converted.
Thread Thread
// OffsetNanos is added to that thread's timestamps to reach the render
// thread's clock.
OffsetNanos int64
// ErrorNanos bounds the uncertainty. A gap smaller than this is noise.
ErrorNanos int64
}
ClockOffset converts one thread's clock to another's.
Supplying one is optional and its accuracy is the caller's problem, which is why the error bound travels with it: a round-trip estimate is only as good as the round trip was symmetric, and on a busy worker it is not. The timeline never needs an offset to ORDER events — that is what causality is for — and uses it only to report cross-thread gaps, always with the bound attached.
type CorrelationID ¶
type CorrelationID string
CorrelationID ties together every span belonging to one logical operation, across every thread it touches.
type LifecycleSummary ¶
type LifecycleSummary struct {
Correlation CorrelationID
// Threads lists every thread the operation touched, in causal order.
Threads []Thread
// SpanCount is how many spans make it up.
SpanCount int
// CrossedBoundary reports whether the operation involved more than one
// thread, which is the case P4.3 exists for.
CrossedBoundary bool
// Incomplete reports that some span never ended — a command still in flight,
// or one whose worker died before closing it.
Incomplete bool
// Failed reports that some span ended with an error.
Failed bool
// ThreadDurations is time spent per thread, each on its own clock. NOT
// summed into a total: adding two clocks' durations produces a number that
// looks like a wall-clock elapsed time and is not one, since the threads
// overlap by construction.
ThreadDurations map[Thread]int64
}
LifecycleSummary describes one operation end to end.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder collects spans on one thread.
Not safe for concurrent use, which is not a limitation here: there is one recorder per thread and each thread is single-threaded in wasm.
func NewRecorder ¶
NewRecorder creates a recorder for one thread.
func (*Recorder) Dropped ¶
Dropped reports how many spans were evicted.
Surfaced rather than silent: a timeline missing its beginning looks like an operation that started in the middle, and a reader has no other way to tell.
func (*Recorder) End ¶
End closes a span.
Ending an unknown span is an error rather than a no-op: it means the id was wrong, or the span was evicted, and both make the resulting timeline misleading in ways nothing downstream can detect.
func (*Recorder) Start ¶
func (parseRecorder *Recorder) Start(parseName string, parseCorrelation CorrelationID, parseParent SpanID, parseStartNanos int64) (SpanID, error)
Start records the beginning of a span and returns its id.
The timestamp is the caller's, not read from a clock here: this package is compiled for both native tests and wasm, and the meaningful clock differs. Taking it as a parameter also lets a test produce a deterministic timeline.
type Span ¶
type Span struct {
ID SpanID
Parent SpanID
// Correlation is the operation this span belongs to. Spans on different
// threads sharing a correlation are the same logical work.
Correlation CorrelationID
Thread Thread
Name string
// StartNanos and EndNanos are on the RECORDING THREAD'S clock and are only
// comparable to other spans from that same thread. See the package comment.
StartNanos int64
EndNanos int64
// Seq is a per-thread monotonic counter, assigned by the recorder.
//
// It exists because timestamps can tie — two spans in the same microsecond
// are ordinary — and a timeline that reorders equal timestamps arbitrarily
// is unreadable. Seq breaks the tie the way the thread actually ran.
Seq uint64
// Err is non-empty when the span ended in failure.
Err string
}
Span is one interval of work on one thread.
type Timeline ¶
type Timeline struct {
// contains filtered or unexported fields
}
Timeline is a merged, causally-ordered view across threads.
func (*Timeline) Correlations ¶
func (parseTimeline *Timeline) Correlations() []CorrelationID
Correlations lists every correlation in the timeline, in first-seen order.
func (*Timeline) Lifecycle ¶
func (parseTimeline *Timeline) Lifecycle(parseCorrelation CorrelationID) []Span
Lifecycle returns every span for one correlation, in causal order.
This is P4.3's criterion: a command's full lifecycle traceable across the boundary. The order is derived from parentage first and per-thread sequence second — never from comparing timestamps across threads, which is the mistake that makes a merged timeline confidently wrong.
func (*Timeline) Summarize ¶
func (parseTimeline *Timeline) Summarize(parseCorrelation CorrelationID) (LifecycleSummary, bool)
Summarize describes one operation's lifecycle.
func (*Timeline) WithClockOffset ¶
func (parseTimeline *Timeline) WithClockOffset(parseOffset ClockOffset) *Timeline
WithClockOffset records a measured offset for one thread.