@proj-airi/trace-kit 0.0.0-stage → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,766 @@
1
+ import { k as aggregate, q as matchTrackPath } from "./conventions-DLiC8rkL.mjs";
2
+ import { t as ClockGraph } from "./clock-hjTPEs21.mjs";
3
+ import { i as streamClockOf, n as recordKey, r as spanKeyOf, t as epochKeyOf } from "./keys-DD_VE8Fk.mjs";
4
+ import { n as SeriesRing, t as DEFAULT_SERIES_RETENTION } from "./series-ring-CaKfvyWQ.mjs";
5
+ import { groupBy, mapValues } from "es-toolkit";
6
+ //#region src/projections/event-log.ts
7
+ /**
8
+ * A list of records in apply order, with their converted time. The viewer filters it.
9
+ *
10
+ * Series records are left out, because they are many and the timeline draws them.
11
+ */
12
+ function createEventLogProjection(options) {
13
+ const limit = options?.limit ?? 2e4;
14
+ return {
15
+ key: "event-log",
16
+ reset: () => ({
17
+ entries: [],
18
+ keys: /* @__PURE__ */ new Set(),
19
+ dropped: 0
20
+ }),
21
+ handleRecord(state, record, context) {
22
+ if (record.type === "sample" || record.type === "sample.chunk") return state;
23
+ const key = recordKey(record);
24
+ if (state.keys.has(key)) return state;
25
+ state.keys.add(key);
26
+ state.entries.push({
27
+ key,
28
+ time: context.time,
29
+ record,
30
+ ...context.late ? { late: true } : {},
31
+ ...context.pinned ? { pinned: true } : {},
32
+ ...context.estimated ? { estimated: true } : {}
33
+ });
34
+ if (state.entries.length > limit) {
35
+ const removed = state.entries.splice(0, state.entries.length - limit);
36
+ for (const entry of removed) state.keys.delete(entry.key);
37
+ state.dropped += removed.length;
38
+ }
39
+ return state;
40
+ },
41
+ select: (state) => state
42
+ };
43
+ }
44
+ //#endregion
45
+ //#region src/projections/metrics.ts
46
+ function exclusionOf(record) {
47
+ if (record.attrs?.["censored"] === true) return "censored";
48
+ if (record.attrs?.["warmup"] === true) return "warmup";
49
+ if (record.type === "span.end" && record.status !== "ok") return "failed";
50
+ }
51
+ /**
52
+ * Collects the samples of metrics of kind `attribute`.
53
+ *
54
+ * A metric definition record names a track pattern, a record type, and an attribute. Each matching record with a number
55
+ * value adds one sample. A metric of kind `derived` comes from its own projection.
56
+ */
57
+ function createAttributeMetricsProjection() {
58
+ return {
59
+ key: "metrics.attribute",
60
+ reset: () => ({
61
+ definitions: /* @__PURE__ */ new Map(),
62
+ samples: {}
63
+ }),
64
+ handleRecord(state, record, context) {
65
+ if (record.type === "definition") {
66
+ if (record.kind === "metric") {
67
+ const definition = record.data;
68
+ if (definition.value.kind === "attribute") state.definitions.set(definition.key, definition);
69
+ }
70
+ return state;
71
+ }
72
+ if (!("track" in record) || !record.attrs) return state;
73
+ for (const definition of state.definitions.values()) {
74
+ const source = definition.value;
75
+ if (source.kind !== "attribute" || source.record !== record.type) continue;
76
+ const params = matchTrackPath(source.track, record.track);
77
+ const value = record.attrs[source.attribute];
78
+ if (!params || typeof value !== "number") continue;
79
+ const dimensions = definition.dimensions ? Object.fromEntries(definition.dimensions.map((key) => [key, String(record.ids?.[key] ?? params[key] ?? "")])) : void 0;
80
+ (state.samples[definition.key] ??= []).push({
81
+ time: context.time,
82
+ value,
83
+ dimensions,
84
+ excluded: exclusionOf(record)
85
+ });
86
+ }
87
+ return state;
88
+ },
89
+ evict(state, range) {
90
+ for (const [key, list] of Object.entries(state.samples)) {
91
+ if (state.definitions.get(key)?.scope === "session") continue;
92
+ state.samples[key] = list.filter((sample) => sample.time > range.to);
93
+ }
94
+ return state;
95
+ },
96
+ select: (state) => state.samples
97
+ };
98
+ }
99
+ /** Summarizes samples with the aggregates of their definition. The default aggregates are p50 and p95. */
100
+ function summarizeMetric(key, samples, definition) {
101
+ const kinds = definition?.aggregate ?? ["p50", "p95"];
102
+ const included = samples.filter((sample) => !sample.excluded);
103
+ const excluded = {
104
+ censored: 0,
105
+ warmup: 0,
106
+ failed: 0
107
+ };
108
+ for (const sample of samples) if (sample.excluded) excluded[sample.excluded]++;
109
+ let byDimension;
110
+ if (definition?.dimensions?.length) {
111
+ const dimensions = definition.dimensions;
112
+ const groups = groupBy(included, (sample) => dimensions.map((dimension) => sample.dimensions?.[dimension] ?? "").join("|"));
113
+ byDimension = mapValues(groups, (group) => aggregate(group.map((sample) => sample.value), kinds));
114
+ }
115
+ return {
116
+ key,
117
+ definition,
118
+ values: aggregate(included.map((sample) => sample.value), kinds),
119
+ count: included.length,
120
+ excluded,
121
+ byDimension
122
+ };
123
+ }
124
+ //#endregion
125
+ //#region src/projections/reader.ts
126
+ /**
127
+ * Records by source, epoch, and seq.
128
+ *
129
+ * A map from `source/epoch` to a map from `seq` keeps one number per record. A map with `source/epoch/seq` strings as
130
+ * keys keeps one string per record, and costs about five times the memory for the same lookups.
131
+ */
132
+ var RecordIndex = class {
133
+ epochs = /* @__PURE__ */ new Map();
134
+ count = 0;
135
+ get size() {
136
+ return this.count;
137
+ }
138
+ get(ref) {
139
+ return this.epochs.get(epochKeyOf(ref.source, ref.epoch))?.get(ref.seq);
140
+ }
141
+ has(ref) {
142
+ return this.epochs.get(epochKeyOf(ref.source, ref.epoch))?.has(ref.seq) ?? false;
143
+ }
144
+ /** Adds a record. A record with the same source, epoch, and seq stays, and the call returns false. */
145
+ add(record) {
146
+ const epochKey = epochKeyOf(record.source, record.epoch);
147
+ let records = this.epochs.get(epochKey);
148
+ if (!records) {
149
+ records = /* @__PURE__ */ new Map();
150
+ this.epochs.set(epochKey, records);
151
+ }
152
+ if (records.has(record.seq)) return false;
153
+ records.set(record.seq, record);
154
+ this.count++;
155
+ return true;
156
+ }
157
+ clear() {
158
+ this.epochs.clear();
159
+ this.count = 0;
160
+ }
161
+ };
162
+ /** The index key of a link target. */
163
+ function linkTargetKey(to) {
164
+ if ("spanId" in to) return `span:${to.spanId}`;
165
+ if ("key" in to) return `id:${to.key}=${to.id}`;
166
+ return recordKey(to);
167
+ }
168
+ function pushTo(map, key, value) {
169
+ const list = map.get(key);
170
+ if (list) list.push(value);
171
+ else map.set(key, [value]);
172
+ }
173
+ /**
174
+ * Feeds collector messages to projections in apply order.
175
+ *
176
+ * Apply order is the arrival order of the feed. For one epoch, it is the `seq` order, as the transports deliver it.
177
+ * Display order is the converted time, which each projection reads from the context.
178
+ *
179
+ * The reader keeps its own clock graph from the snapshot records in the feed, so a replay of a file converts times in
180
+ * the same way as the live view. `version` changes on each handled message. A view reads through selectors keyed on it.
181
+ */
182
+ var TraceReader = class {
183
+ projections;
184
+ clocks;
185
+ indexes = {
186
+ byKey: new RecordIndex(),
187
+ bySpanId: /* @__PURE__ */ new Map(),
188
+ linksTo: /* @__PURE__ */ new Map(),
189
+ byId: /* @__PURE__ */ new Map()
190
+ };
191
+ version = 0;
192
+ caughtUp = false;
193
+ states = /* @__PURE__ */ new Map();
194
+ constructor(projections, traceClock = "mono:collector") {
195
+ this.projections = projections;
196
+ this.clocks = new ClockGraph(traceClock);
197
+ for (const projection of projections) this.states.set(projection.key, projection.reset());
198
+ }
199
+ handle(message) {
200
+ this.version++;
201
+ switch (message.kind) {
202
+ case "record":
203
+ this.apply(message.record, message.pinned ?? false, message.late ?? false);
204
+ break;
205
+ case "series":
206
+ for (const projection of this.projections) if (projection.handleSeries) this.states.set(projection.key, projection.handleSeries(this.stateOf(projection), message));
207
+ break;
208
+ case "gap":
209
+ for (const projection of this.projections) if (projection.evict) this.states.set(projection.key, projection.evict(this.stateOf(projection), {
210
+ from: message.from,
211
+ to: message.to
212
+ }));
213
+ break;
214
+ case "caughtUp": this.caughtUp = true;
215
+ }
216
+ }
217
+ /** Feeds records without a collector, for example the lines of a file or the records of a test. */
218
+ handleRecords(records) {
219
+ for (const record of records) this.handle({
220
+ kind: "record",
221
+ record
222
+ });
223
+ this.handle({
224
+ kind: "caughtUp",
225
+ cursor: {},
226
+ traceClock: this.clocks.traceClock
227
+ });
228
+ }
229
+ /** The view of a projection of this reader. */
230
+ view(projection) {
231
+ if (!this.projections.includes(projection)) throw new Error(`No projection ${projection.key} in this reader`);
232
+ const state = this.states.get(projection.key);
233
+ return projection.select(projection.finalize ? projection.finalize(state) : state);
234
+ }
235
+ /** The state of a projection, typed for the call. Each state was created by the same projection. */
236
+ stateOf(projection) {
237
+ return this.states.get(projection.key);
238
+ }
239
+ reset() {
240
+ this.version++;
241
+ this.caughtUp = false;
242
+ this.indexes.byKey.clear();
243
+ this.indexes.bySpanId.clear();
244
+ this.indexes.linksTo.clear();
245
+ this.indexes.byId.clear();
246
+ this.clocks.clear();
247
+ for (const projection of this.projections) this.states.set(projection.key, projection.reset());
248
+ }
249
+ /** The event time of a record on the trace clock. */
250
+ timeOf(record) {
251
+ const converted = this.clocks.convert(record.clock, record.ts);
252
+ if (converted) return {
253
+ time: converted.value,
254
+ estimated: converted.estimated
255
+ };
256
+ return {
257
+ time: record.observedTs ?? record.ts,
258
+ estimated: true
259
+ };
260
+ }
261
+ apply(record, pinned, late) {
262
+ if (!this.indexes.byKey.add(record)) return;
263
+ if (record.type === "clock.snapshot") this.clocks.addSnapshot(record.values, record.uncertaintyMs, record.attrs?.estimated === true);
264
+ else if (record.type === "clock.discontinuity") this.clocks.addDiscontinuity(record.of);
265
+ else if (record.type === "stream.started" && record.rate) this.clocks.setUnitsPerMs(streamClockOf(record.streamId), record.rate / 1e3);
266
+ if ((record.type === "span.begin" || record.type === "span.complete") && !this.indexes.bySpanId.has(record.spanId)) this.indexes.bySpanId.set(record.spanId, record);
267
+ if ("links" in record) for (const link of record.links ?? []) pushTo(this.indexes.linksTo, linkTargetKey(link.to), record);
268
+ for (const [idKey, value] of Object.entries(record.ids ?? {})) pushTo(this.indexes.byId, `${idKey}=${value}`, record);
269
+ const { time, estimated } = this.timeOf(record);
270
+ const context = {
271
+ pinned,
272
+ late,
273
+ time,
274
+ estimated,
275
+ indexes: this.indexes
276
+ };
277
+ for (const projection of this.projections) this.states.set(projection.key, projection.handleRecord(this.stateOf(projection), record, context));
278
+ }
279
+ };
280
+ //#endregion
281
+ //#region src/projections/timeline.ts
282
+ /** The series rings compact at most this often, on the trace clock. The collector ticks about as often. */
283
+ const SERIES_COMPACT_INTERVAL_MS = 1e3;
284
+ function emptyState(seriesRetention = DEFAULT_SERIES_RETENTION) {
285
+ return {
286
+ tracks: /* @__PURE__ */ new Map(),
287
+ spans: /* @__PURE__ */ new Map(),
288
+ definitions: /* @__PURE__ */ new Map(),
289
+ trackDefinitions: [],
290
+ markers: [],
291
+ gaps: [],
292
+ sources: /* @__PURE__ */ new Map(),
293
+ annotations: [],
294
+ start: Number.POSITIVE_INFINITY,
295
+ end: Number.NEGATIVE_INFINITY,
296
+ counters: {
297
+ records: 0,
298
+ late: 0,
299
+ orphanUpdates: 0,
300
+ reordered: 0
301
+ },
302
+ pendingUpdates: /* @__PURE__ */ new Map(),
303
+ seriesRings: /* @__PURE__ */ new Map(),
304
+ seriesRetention,
305
+ seriesCompactedAt: Number.NEGATIVE_INFINITY
306
+ };
307
+ }
308
+ /** The ring of one series of a track. A new ring also becomes the series that the track shows. */
309
+ function seriesRingOf(state, track, valueKey) {
310
+ const ringKey = `${track.path}\u0000${valueKey}`;
311
+ let ring = state.seriesRings.get(ringKey);
312
+ if (!ring) {
313
+ const retention = state.seriesRetention;
314
+ const created = retention ? new SeriesRing(retention.maxAgeMs, retention.fullRateMs) : new SeriesRing(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
315
+ state.seriesRings.set(ringKey, created);
316
+ track.series[valueKey] = seriesView(created);
317
+ ring = created;
318
+ }
319
+ return ring;
320
+ }
321
+ function seriesView(ring) {
322
+ return {
323
+ get times() {
324
+ return ring.timesView();
325
+ },
326
+ get values() {
327
+ return ring.valuesView();
328
+ },
329
+ get revision() {
330
+ return ring.revision;
331
+ }
332
+ };
333
+ }
334
+ /** Compacts every series ring at the newest time of the trace, about once a second of trace time. */
335
+ function compactSeries(state) {
336
+ if (!state.seriesRetention || state.end - state.seriesCompactedAt < SERIES_COMPACT_INTERVAL_MS) return;
337
+ state.seriesCompactedAt = state.end;
338
+ for (const ring of state.seriesRings.values()) ring.compact(state.end);
339
+ }
340
+ function trackOf(state, path) {
341
+ let track = state.tracks.get(path);
342
+ if (!track) {
343
+ let definition;
344
+ let params = {};
345
+ for (const candidate of state.trackDefinitions) {
346
+ const match = matchTrackPath(candidate.path, path);
347
+ if (match) {
348
+ definition = candidate;
349
+ params = match;
350
+ break;
351
+ }
352
+ }
353
+ track = {
354
+ path,
355
+ definition,
356
+ params,
357
+ spans: [],
358
+ instants: [],
359
+ states: [],
360
+ series: {},
361
+ lanes: [],
362
+ hasError: false
363
+ };
364
+ state.tracks.set(path, track);
365
+ }
366
+ return track;
367
+ }
368
+ /** A new or changed track definition matches existing tracks again. */
369
+ function rematchTracks(state) {
370
+ for (const track of state.tracks.values()) {
371
+ track.definition = void 0;
372
+ track.params = {};
373
+ for (const candidate of state.trackDefinitions) {
374
+ const match = matchTrackPath(candidate.path, track.path);
375
+ if (match) {
376
+ track.definition = candidate;
377
+ track.params = match;
378
+ break;
379
+ }
380
+ }
381
+ }
382
+ }
383
+ /**
384
+ * Moves an effect after its cause in the view when clock conversion put it earlier.
385
+ * Only `caused-by` links to a known record or span count.
386
+ */
387
+ function repairCausality(time, links, state, context) {
388
+ let repaired = time;
389
+ for (const link of links ?? []) {
390
+ if (link.rel !== "caused-by") continue;
391
+ let causeTime;
392
+ if ("spanId" in link.to) {
393
+ const begin = context.indexes.bySpanId.get(link.to.spanId);
394
+ causeTime = begin ? state.spans.get(spanKeyOf(begin.source, begin.epoch, link.to.spanId))?.start : void 0;
395
+ } else if ("seq" in link.to) {
396
+ const cause = context.indexes.byKey.get(link.to);
397
+ if (cause) causeTime = findRecordTime(state, cause);
398
+ }
399
+ if (causeTime !== void 0 && causeTime > repaired) repaired = causeTime;
400
+ }
401
+ return {
402
+ time: repaired,
403
+ reordered: repaired !== time
404
+ };
405
+ }
406
+ function findRecordTime(state, record) {
407
+ const key = recordKey(record);
408
+ if ("spanId" in record && record.spanId && (record.type === "span.begin" || record.type === "span.complete")) return state.spans.get(spanKeyOf(record.source, record.epoch, record.spanId))?.start;
409
+ if (!("track" in record)) return void 0;
410
+ const track = state.tracks.get(record.track);
411
+ return track?.instants.find((instant) => instant.key === key)?.time ?? track?.states.find((change) => change.key === key)?.time;
412
+ }
413
+ /** Assigns the sub-lane of a new span: the first lane at its depth that is free at its start. */
414
+ function assignLane(track, depth, start) {
415
+ const busyUntil = track.lanes[depth] ??= [];
416
+ let lane = busyUntil.findIndex((end) => end <= start);
417
+ if (lane === -1) lane = busyUntil.length;
418
+ busyUntil[lane] = Number.POSITIVE_INFINITY;
419
+ return lane;
420
+ }
421
+ /** Frees the lane of a span that ended or was abandoned. */
422
+ function releaseLane(state, span, end) {
423
+ const busyUntil = state.tracks.get(span.track)?.lanes[span.depth];
424
+ if (busyUntil && busyUntil[span.lane] === Number.POSITIVE_INFINITY) busyUntil[span.lane] = end;
425
+ }
426
+ function abandonEpoch(state, source, epoch) {
427
+ for (const span of state.spans.values()) if (span.source === source && span.epoch === epoch && span.end === void 0 && !span.abandoned) {
428
+ span.abandoned = true;
429
+ releaseLane(state, span, state.end);
430
+ }
431
+ }
432
+ function extend(state, time) {
433
+ if (time < state.start) state.start = time;
434
+ if (time > state.end) state.end = time;
435
+ }
436
+ /**
437
+ * The empty list and the empty attributes that spans share. Most spans have no instants, annotations, or links, so
438
+ * one frozen value saves an array or an object on each span. The helpers below never change them in place.
439
+ */
440
+ const NO_ITEMS = Object.freeze([]);
441
+ const NO_ATTRS = Object.freeze({});
442
+ /** A list with the items added. It copies the list, so a shared or published list never changes. */
443
+ function appendTo(list, items) {
444
+ if (!items || items.length === 0) return list;
445
+ return list.length === 0 ? [...items] : [...list, ...items];
446
+ }
447
+ /**
448
+ * Attributes with an update merged in. Without an update the attributes stay, and attributes without a merge are the
449
+ * record's own object. Records do not change, so the span can share it.
450
+ */
451
+ function mergeAttrs(attrs, update) {
452
+ if (!update) return attrs;
453
+ return attrs === NO_ATTRS ? update : {
454
+ ...attrs,
455
+ ...update
456
+ };
457
+ }
458
+ function annotate(span, annotation) {
459
+ if (span) span.annotations = appendTo(span.annotations, [annotation]);
460
+ }
461
+ function beginSpan(state, record, context) {
462
+ const key = spanKeyOf(record.source, record.epoch, record.spanId);
463
+ const existing = state.spans.get(key);
464
+ if (existing) {
465
+ existing.attrs = mergeAttrs(existing.attrs, record.attrs);
466
+ if (existing.startMissing) {
467
+ existing.start = context.time;
468
+ existing.startMissing = false;
469
+ }
470
+ return existing;
471
+ }
472
+ const track = trackOf(state, record.track);
473
+ const parentKey = record.parentSpanId ? spanKeyOf(record.source, record.epoch, record.parentSpanId) : void 0;
474
+ const parent = parentKey ? state.spans.get(parentKey) : void 0;
475
+ const depth = parent && parent.track === record.track ? parent.depth + 1 : 0;
476
+ const { time, reordered } = repairCausality(context.time, record.links, state, context);
477
+ const pending = state.pendingUpdates.get(key)?.attrs;
478
+ state.pendingUpdates.delete(key);
479
+ const span = {
480
+ key,
481
+ spanId: record.spanId,
482
+ source: record.source,
483
+ epoch: record.epoch,
484
+ track: record.track,
485
+ name: record.name,
486
+ parentKey,
487
+ depth,
488
+ lane: assignLane(track, depth, time),
489
+ ids: record.ids,
490
+ attrs: mergeAttrs(record.attrs ?? NO_ATTRS, pending),
491
+ links: record.links ?? NO_ITEMS,
492
+ start: time,
493
+ deadlineMs: record.type === "span.begin" ? record.deadlineMs : void 0,
494
+ estimated: context.estimated || void 0,
495
+ reordered: reordered || void 0,
496
+ late: context.late || void 0,
497
+ instants: NO_ITEMS,
498
+ annotations: NO_ITEMS
499
+ };
500
+ if (reordered) state.counters.reordered++;
501
+ state.spans.set(key, span);
502
+ track.spans.push(span);
503
+ extend(state, time);
504
+ return span;
505
+ }
506
+ /** How long an update without its begin waits, and how far a watermark trails its source. */
507
+ const ALLOWED_LATENESS_MS = 5e3;
508
+ /** Drops the updates that waited for their begin longer than the allowed lateness. */
509
+ function expirePendingUpdates(state, time) {
510
+ for (const [key, pending] of state.pendingUpdates) if (pending.time < time - ALLOWED_LATENESS_MS) state.pendingUpdates.delete(key);
511
+ }
512
+ /**
513
+ * Marks the state tracks of a source unknown, because the source that owns them stopped or was lost.
514
+ * A `single` track belongs to its first writer. On an `any` track, the source of the latest state owns the current value,
515
+ * so the stop of an earlier writer leaves a newer value of another source.
516
+ */
517
+ function markOwnedStatesUnknown(state, source, key, time) {
518
+ for (const track of state.tracks.values()) {
519
+ const latest = track.states.at(-1);
520
+ if ((track.definition?.stateOwner === "any" ? latest?.source : track.stateOwner) === source && latest?.value !== "unknown") track.states.push({
521
+ key,
522
+ track: track.path,
523
+ time,
524
+ value: "unknown",
525
+ source
526
+ });
527
+ }
528
+ }
529
+ function handleRecord(state, record, context) {
530
+ state.counters.records++;
531
+ if (context.late) state.counters.late++;
532
+ if (state.pendingUpdates.size > 0) expirePendingUpdates(state, context.time);
533
+ const source = state.sources.get(record.source);
534
+ if (source && source.epoch === record.epoch) source.newestTime = Math.max(source.newestTime, context.time);
535
+ switch (record.type) {
536
+ case "source.started": {
537
+ const previous = state.sources.get(record.source);
538
+ if (previous && previous.epoch !== record.epoch) abandonEpoch(state, previous.source, previous.epoch);
539
+ state.sources.set(record.source, {
540
+ source: record.source,
541
+ epoch: record.epoch,
542
+ runtime: record.runtime,
543
+ live: true,
544
+ newestTime: context.time
545
+ });
546
+ break;
547
+ }
548
+ case "source.stopped": {
549
+ abandonEpoch(state, record.source, record.epoch);
550
+ const current = state.sources.get(record.source);
551
+ if (current?.epoch === record.epoch) current.live = false;
552
+ markOwnedStatesUnknown(state, record.source, recordKey(record), context.time);
553
+ break;
554
+ }
555
+ case "source.lost": {
556
+ abandonEpoch(state, record.of.source, record.of.epoch);
557
+ const current = state.sources.get(record.of.source);
558
+ if (current?.epoch === record.of.epoch) current.live = false;
559
+ state.markers.push({
560
+ kind: "source-lost",
561
+ time: context.time,
562
+ label: `${record.of.source}: ${record.reason}`
563
+ });
564
+ markOwnedStatesUnknown(state, record.of.source, recordKey(record), context.time);
565
+ break;
566
+ }
567
+ case "clock.discontinuity":
568
+ state.markers.push({
569
+ kind: "clock-discontinuity",
570
+ time: context.time,
571
+ label: `${record.of}: ${record.reason}`
572
+ });
573
+ break;
574
+ case "clock.snapshot":
575
+ for (const marker of state.markers) if (marker.kind === "clock-discontinuity" && marker.until === void 0 && record.values.some((value) => marker.label.startsWith(`${value.clock}:`))) marker.until = context.time;
576
+ break;
577
+ case "definition":
578
+ state.definitions.set(`${record.kind}/${record.key}`, record.data);
579
+ if (record.kind === "track") {
580
+ const definition = record.data;
581
+ state.trackDefinitions = [...state.trackDefinitions.filter((candidate) => candidate.path !== definition.path), definition];
582
+ rematchTracks(state);
583
+ }
584
+ break;
585
+ case "span.begin":
586
+ case "span.complete": {
587
+ const span = beginSpan(state, record, context);
588
+ if (record.type === "span.complete") {
589
+ span.end = span.start + record.durationMs;
590
+ releaseLane(state, span, span.end);
591
+ span.status = record.status;
592
+ span.reason = record.reason;
593
+ extend(state, span.end);
594
+ if (record.status === "error") trackOf(state, record.track).hasError = true;
595
+ }
596
+ break;
597
+ }
598
+ case "span.update": {
599
+ const key = spanKeyOf(record.source, record.epoch, record.spanId);
600
+ const span = state.spans.get(key);
601
+ if (span) {
602
+ span.attrs = mergeAttrs(span.attrs, record.attrs);
603
+ span.links = appendTo(span.links, record.links);
604
+ } else {
605
+ state.counters.orphanUpdates++;
606
+ state.pendingUpdates.set(key, {
607
+ attrs: {
608
+ ...state.pendingUpdates.get(key)?.attrs,
609
+ ...record.attrs
610
+ },
611
+ time: context.time
612
+ });
613
+ }
614
+ break;
615
+ }
616
+ case "span.end": {
617
+ const key = spanKeyOf(record.source, record.epoch, record.spanId);
618
+ let span = state.spans.get(key);
619
+ if (!span) {
620
+ const name = typeof record.attrs?.name === "string" ? record.attrs.name : record.spanId;
621
+ span = beginSpan(state, {
622
+ ...record,
623
+ type: "span.begin",
624
+ name
625
+ }, {
626
+ ...context,
627
+ time: Number.isFinite(state.start) ? state.start : context.time
628
+ });
629
+ span.startMissing = true;
630
+ }
631
+ if (span.end !== void 0) break;
632
+ span.end = Math.max(context.time, span.start);
633
+ span.status = record.status;
634
+ span.reason = record.reason;
635
+ span.message = record.message;
636
+ span.abandoned = void 0;
637
+ releaseLane(state, span, span.end);
638
+ span.attrs = mergeAttrs(span.attrs, record.attrs);
639
+ span.links = appendTo(span.links, record.links);
640
+ if (record.status === "error") trackOf(state, record.track).hasError = true;
641
+ extend(state, span.end);
642
+ break;
643
+ }
644
+ case "instant":
645
+ case "log": {
646
+ const track = trackOf(state, record.track);
647
+ const { time, reordered } = repairCausality(context.time, record.links, state, context);
648
+ const instant = {
649
+ key: recordKey(record),
650
+ name: record.type === "log" ? record.message : record.name,
651
+ track: record.track,
652
+ time,
653
+ ids: record.ids,
654
+ attrs: record.attrs,
655
+ links: record.links,
656
+ reordered: reordered || void 0,
657
+ estimated: context.estimated || void 0,
658
+ late: context.late || void 0,
659
+ level: record.type === "log" ? record.level : void 0
660
+ };
661
+ if (reordered) state.counters.reordered++;
662
+ if (record.type === "instant" && record.spanId) {
663
+ instant.spanKey = spanKeyOf(record.source, record.epoch, record.spanId);
664
+ const span = state.spans.get(instant.spanKey);
665
+ if (span) span.instants = appendTo(span.instants, [instant]);
666
+ }
667
+ if (!track.instants.some((existing) => existing.key === instant.key)) track.instants.push(instant);
668
+ extend(state, time);
669
+ break;
670
+ }
671
+ case "state": {
672
+ const track = trackOf(state, record.track);
673
+ const owner = track.definition?.stateOwner ?? "single";
674
+ track.stateOwner ??= record.source;
675
+ const { time, reordered } = repairCausality(context.time, record.links, state, context);
676
+ const change = {
677
+ key: recordKey(record),
678
+ track: record.track,
679
+ time,
680
+ value: record.value,
681
+ from: record.from,
682
+ rule: typeof record.attrs?.rule === "string" ? record.attrs.rule : void 0,
683
+ links: record.links,
684
+ source: record.source,
685
+ foreignOwner: owner === "single" && track.stateOwner !== record.source ? true : void 0
686
+ };
687
+ if (reordered) state.counters.reordered++;
688
+ let index = track.states.length;
689
+ while (index > 0 && track.states[index - 1].time > time) index--;
690
+ if (!track.states.some((existing) => existing.key === change.key)) track.states.splice(index, 0, change);
691
+ extend(state, time);
692
+ break;
693
+ }
694
+ case "sample":
695
+ case "sample.chunk": {
696
+ const track = trackOf(state, record.track);
697
+ const entries = record.type === "sample" ? Object.entries(record.values).map(([key, value]) => [key, [value]]) : Object.entries(record.values);
698
+ for (const [valueKey, values] of entries) {
699
+ const ring = seriesRingOf(state, track, valueKey);
700
+ for (const [index, value] of values.entries()) {
701
+ const offset = record.type === "sample.chunk" ? record.offsetsMs?.[index] ?? index * (record.intervalMs ?? 0) : 0;
702
+ ring.append(context.time + offset, value);
703
+ }
704
+ }
705
+ extend(state, context.time);
706
+ compactSeries(state);
707
+ break;
708
+ }
709
+ case "annotation": if (record.target.spanId) {
710
+ const begin = context.indexes.bySpanId.get(record.target.spanId);
711
+ if (begin) annotate(state.spans.get(spanKeyOf(begin.source, begin.epoch, record.target.spanId)), record);
712
+ } else if (record.target.ref) {
713
+ const target = context.indexes.byKey.get(record.target.ref);
714
+ if (target && "spanId" in target && target.spanId) annotate(state.spans.get(spanKeyOf(target.source, target.epoch, target.spanId)), record);
715
+ } else state.annotations.push(record);
716
+ }
717
+ return state;
718
+ }
719
+ /**
720
+ * The timeline: tracks with spans, instants, state changes, and series, plus global markers and gaps.
721
+ *
722
+ * Display times are converted times. A span keeps the sub-lane that it got at begin, so lanes do not jump while the
723
+ * view is live. Each series is a {@link SeriesRing} with the retention of the collector, so a live view of a long
724
+ * session stays bounded. A file replay passes `seriesRetention: false`.
725
+ */
726
+ function createTimelineProjection(options = {}) {
727
+ return {
728
+ key: "timeline",
729
+ reset: () => emptyState(options.seriesRetention),
730
+ handleRecord,
731
+ handleSeries: (state, series) => {
732
+ const ring = seriesRingOf(state, trackOf(state, series.track), series.key);
733
+ for (const [index, time] of series.times.entries()) ring.append(time, series.values[index]);
734
+ if (series.times.length > 0) extend(state, series.times.at(-1));
735
+ compactSeries(state);
736
+ return state;
737
+ },
738
+ evict: (state, range) => {
739
+ state.gaps.push(range);
740
+ return state;
741
+ },
742
+ select: (state) => state
743
+ };
744
+ }
745
+ /**
746
+ * The start of the provisional region: the earliest watermark of the live sources.
747
+ * A watermark trails the newest record of its source by the allowed lateness.
748
+ */
749
+ function provisionalFrom(state, allowedLatenessMs = ALLOWED_LATENESS_MS) {
750
+ let earliest;
751
+ for (const source of state.sources.values()) {
752
+ if (!source.live || source.source === "collector") continue;
753
+ const watermark = source.newestTime - allowedLatenessMs;
754
+ earliest = earliest === void 0 ? watermark : Math.min(earliest, watermark);
755
+ }
756
+ return earliest;
757
+ }
758
+ /** Records that link to a span, and the targets that the span links to. */
759
+ function linksOfSpan(span, indexes) {
760
+ return {
761
+ incoming: indexes.linksTo.get(linkTargetKey({ spanId: span.spanId })) ?? [],
762
+ outgoing: span.links
763
+ };
764
+ }
765
+ //#endregion
766
+ export { TraceReader as a, summarizeMetric as c, RecordIndex as i, createEventLogProjection as l, linksOfSpan as n, linkTargetKey as o, provisionalFrom as r, createAttributeMetricsProjection as s, createTimelineProjection as t };