@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,212 @@
1
+ //#region src/core/clock.ts
2
+ const DEFAULT_OPTIONS = { fitWindow: 16 };
3
+ /** Weight floor in ms, so a sample with zero uncertainty does not take the whole fit. */
4
+ const MIN_UNCERTAINTY_MS = .05;
5
+ /**
6
+ * Converts values of any clock to one trace clock.
7
+ *
8
+ * State model:
9
+ * - Each `clock.snapshot` adds one sample to every clock pair in it, in both directions.
10
+ * - A `clock.discontinuity` starts a new segment for each pair of that clock. Values after it use only later samples.
11
+ * A segment is found by value, so a clock must only jump forward. A clock that goes back gets a new name.
12
+ * - A conversion walks the shortest path of pairs from the clock to the trace clock, and composes the fits.
13
+ *
14
+ * The graph does not read a clock. It is a pure function of the snapshots that it received.
15
+ */
16
+ var ClockGraph = class {
17
+ traceClock;
18
+ options;
19
+ /** Units of each clock in one millisecond. A media clock in frames at 48 kHz has 48. */
20
+ unitsPerMs = /* @__PURE__ */ new Map();
21
+ pairs = /* @__PURE__ */ new Map();
22
+ paths = /* @__PURE__ */ new Map();
23
+ constructor(traceClock, options) {
24
+ this.traceClock = traceClock;
25
+ this.options = {
26
+ ...DEFAULT_OPTIONS,
27
+ ...options
28
+ };
29
+ }
30
+ /** Declares the unit of a clock. A clock without a declared unit counts in milliseconds. */
31
+ setUnitsPerMs(clock, unitsPerMs) {
32
+ this.unitsPerMs.set(clock, unitsPerMs);
33
+ }
34
+ addSnapshot(values, uncertaintyMs, estimated = false) {
35
+ for (const [index, a] of values.entries()) for (const b of values.slice(index + 1)) {
36
+ if (a.clock === b.clock) continue;
37
+ this.addSample(a.clock, b.clock, {
38
+ from: a.value,
39
+ to: b.value,
40
+ uncertaintyMs,
41
+ estimated
42
+ });
43
+ this.addSample(b.clock, a.clock, {
44
+ from: b.value,
45
+ to: a.value,
46
+ uncertaintyMs,
47
+ estimated
48
+ });
49
+ }
50
+ }
51
+ /** Starts a new segment for every pair of `clock`. Earlier samples stay for values before the jump. */
52
+ addDiscontinuity(clock) {
53
+ for (const [from, targets] of this.pairs) for (const [to, segments] of targets) if (from === clock || to === clock) segments.push({
54
+ startFrom: Number.POSITIVE_INFINITY,
55
+ samples: []
56
+ });
57
+ }
58
+ /** Forgets every unit, snapshot, and discontinuity, for example before a reader reads another trace. */
59
+ clear() {
60
+ this.unitsPerMs.clear();
61
+ this.pairs.clear();
62
+ this.paths.clear();
63
+ }
64
+ /** Converts a value of `clock` to the trace clock. Returns `undefined` when no snapshot path exists. */
65
+ convert(clock, value) {
66
+ if (clock === this.traceClock) return {
67
+ value,
68
+ estimated: false
69
+ };
70
+ const path = this.pathTo(clock);
71
+ if (!path) return void 0;
72
+ let current = value;
73
+ let estimated = false;
74
+ for (let index = 0; index < path.length - 1; index++) {
75
+ const map = this.fit(path[index], path[index + 1], current);
76
+ if (!map) return void 0;
77
+ current = current * map.slope + map.offset;
78
+ estimated ||= map.estimated;
79
+ }
80
+ return {
81
+ value: current,
82
+ estimated
83
+ };
84
+ }
85
+ addSample(from, to, sample) {
86
+ let targets = this.pairs.get(from);
87
+ if (!targets) {
88
+ targets = /* @__PURE__ */ new Map();
89
+ this.pairs.set(from, targets);
90
+ }
91
+ let segments = targets.get(to);
92
+ if (!segments) {
93
+ segments = [];
94
+ targets.set(to, segments);
95
+ this.paths.clear();
96
+ }
97
+ let segment = segments.at(-1);
98
+ if (!segment) {
99
+ segment = {
100
+ startFrom: sample.from,
101
+ samples: []
102
+ };
103
+ segments.push(segment);
104
+ }
105
+ if (segment.samples.length === 0) segment.startFrom = sample.from;
106
+ segment.samples.push(sample);
107
+ if (segment.samples.length > this.options.fitWindow) segment.samples.shift();
108
+ }
109
+ /**
110
+ * Fits `to = from * slope + offset` over the samples of the segment that holds `value`.
111
+ *
112
+ * The segment is the latest one that starts at or before `value`. This is correct when a clock only jumps forward.
113
+ * After a backward reset, a value from before the reset can match the new segment. So a clock that goes back must
114
+ * get a new name, as the `clock.discontinuity` record type says.
115
+ *
116
+ * One sample gives the offset only, with the slope from the declared units. Two or more samples give a weighted
117
+ * least squares fit, which also measures drift. Each sample weighs `1 / uncertainty²`.
118
+ */
119
+ fit(from, to, value) {
120
+ const segments = this.pairs.get(from)?.get(to);
121
+ if (!segments) return void 0;
122
+ let segment = segments.find((candidate) => candidate.samples.length > 0);
123
+ for (const candidate of segments) if (candidate.samples.length > 0 && candidate.startFrom <= value) segment = candidate;
124
+ if (!segment) return void 0;
125
+ const unitSlope = (this.unitsPerMs.get(to) ?? 1) / (this.unitsPerMs.get(from) ?? 1);
126
+ const samples = segment.samples;
127
+ const estimated = samples.some((sample) => sample.estimated);
128
+ if (samples.length === 1) {
129
+ const sample = samples[0];
130
+ return {
131
+ slope: unitSlope,
132
+ offset: sample.to - sample.from * unitSlope,
133
+ estimated: estimated || unitSlope !== 1
134
+ };
135
+ }
136
+ let weightSum = 0;
137
+ let meanFrom = 0;
138
+ let meanTo = 0;
139
+ for (const sample of samples) {
140
+ const weight = 1 / Math.max(sample.uncertaintyMs, MIN_UNCERTAINTY_MS) ** 2;
141
+ weightSum += weight;
142
+ meanFrom += weight * sample.from;
143
+ meanTo += weight * sample.to;
144
+ }
145
+ meanFrom /= weightSum;
146
+ meanTo /= weightSum;
147
+ let covariance = 0;
148
+ let variance = 0;
149
+ for (const sample of samples) {
150
+ const weight = 1 / Math.max(sample.uncertaintyMs, MIN_UNCERTAINTY_MS) ** 2;
151
+ covariance += weight * (sample.from - meanFrom) * (sample.to - meanTo);
152
+ variance += weight * (sample.from - meanFrom) ** 2;
153
+ }
154
+ const slope = variance > 0 ? covariance / variance : unitSlope;
155
+ return {
156
+ slope,
157
+ offset: meanTo - meanFrom * slope,
158
+ estimated
159
+ };
160
+ }
161
+ /** Breadth-first search from `clock` to the trace clock. The result is cached until a new pair appears. */
162
+ pathTo(clock) {
163
+ const cached = this.paths.get(clock);
164
+ if (cached !== void 0) return cached ?? void 0;
165
+ const previous = /* @__PURE__ */ new Map([[clock, clock]]);
166
+ const queue = [clock];
167
+ while (queue.length > 0) {
168
+ const current = queue.shift();
169
+ if (current === this.traceClock) break;
170
+ for (const next of this.pairs.get(current)?.keys() ?? []) {
171
+ if (previous.has(next)) continue;
172
+ previous.set(next, current);
173
+ queue.push(next);
174
+ }
175
+ }
176
+ if (!previous.has(this.traceClock)) {
177
+ this.paths.set(clock, null);
178
+ return;
179
+ }
180
+ const path = [this.traceClock];
181
+ while (path[0] !== clock) path.unshift(previous.get(path[0]));
182
+ this.paths.set(clock, path);
183
+ return path;
184
+ }
185
+ };
186
+ /**
187
+ * Runs a ping exchange with a remote clock and returns the sample with the smallest round trip.
188
+ *
189
+ * Each ping asks the remote side for its clock value. The local value at the middle of the round trip pairs with it.
190
+ * Half the round trip is the uncertainty. A failed ping is skipped. Returns `undefined` when every ping failed.
191
+ */
192
+ async function measureClockOffset(options) {
193
+ let best;
194
+ for (let index = 0; index < (options.count ?? 5); index++) {
195
+ const sentAt = options.now();
196
+ let remote;
197
+ try {
198
+ remote = await options.ping();
199
+ } catch {
200
+ continue;
201
+ }
202
+ const uncertaintyMs = (options.now() - sentAt) / 2;
203
+ if (!best || uncertaintyMs < best.uncertaintyMs) best = {
204
+ local: sentAt + uncertaintyMs,
205
+ remote,
206
+ uncertaintyMs
207
+ };
208
+ }
209
+ return best;
210
+ }
211
+ //#endregion
212
+ export { measureClockOffset as n, ClockGraph as t };
@@ -0,0 +1,207 @@
1
+ import { n as SeriesRetention, r as SeriesRing, t as DEFAULT_SERIES_RETENTION } from "../series-ring-DCb7czkl.mjs";
2
+ import { V as TraceRecordInput, c as TraceSeriesMessage, d as TraceAttributeDefinition, n as ClockGraph, o as TraceFeedMessage, z as TraceRecord } from "../clock-CewQJzn8.mjs";
3
+ //#region src/collector/types.d.ts
4
+ /** A manual save of a collector. */
5
+ interface CollectorSnapshot {
6
+ /** The pinned set and the buffer, in the order of a backlog. */
7
+ records: TraceRecord[];
8
+ /** The series rings. The buffer does not hold samples. */
9
+ series: TraceSeriesMessage[];
10
+ }
11
+ interface FlightRecorderDump {
12
+ /** The records of the window, in buffer order. With `append`, only the records after the previous dump. */
13
+ records: TraceRecord[];
14
+ /** Append to the previous dump file. The window overlaps the previous dump. */
15
+ append: boolean;
16
+ reason: 'error' | 'overdue';
17
+ /** The window on the trace clock. */
18
+ from: number;
19
+ to: number;
20
+ }
21
+ //#endregion
22
+ //#region src/collector/collector.d.ts
23
+ interface TraceCollectorOptions {
24
+ /**
25
+ * The monotonic clock of the collector. It is the trace clock: every value converts to it.
26
+ * @default 'mono:collector'
27
+ */
28
+ clock?: string;
29
+ /** The value of the collector clock now. */
30
+ now: () => number;
31
+ /** @default 30 minutes */
32
+ maxAgeMs?: number;
33
+ /** UTF-8 bytes of the JSON of the event buffer. Objects in memory are often 2 to 3 times larger. @default 16 MB */
34
+ maxBytes?: number;
35
+ /** Series rings keep the full rate for this time, then the min and max of each second. @default 5 minutes */
36
+ seriesFullRateMs?: number;
37
+ /** A record later than this behind the newest record of its source is late. @default 5 seconds */
38
+ allowedLatenessMs?: number;
39
+ /** A source without any record or ping for this time is lost. @default 60 seconds */
40
+ heartbeatTimeoutMs?: number;
41
+ /** Writes the preceding window of the buffer when a span fails or goes overdue. */
42
+ flightRecorder?: {
43
+ /** @default 60 seconds */
44
+ windowMs?: number;
45
+ /** A trigger from the same source in this time does not start a new dump. @default 60 seconds */
46
+ cooldownMs?: number;
47
+ /** UTF-8 bytes of the flight window that the collector keeps while buffering is off. @default 2 MB */
48
+ maxBytes?: number;
49
+ onDump: (dump: FlightRecorderDump) => void;
50
+ };
51
+ /** Receives the union of the tracks that viewers show. Producers skip costly series that no reader wants. */
52
+ onWantedChange?: (tracks: ReadonlySet<string> | '*') => void;
53
+ /** @default crypto.randomUUID */
54
+ createEpoch?: () => string;
55
+ }
56
+ /**
57
+ * Receives the records of every source in one place, keeps recent records, and feeds viewers.
58
+ *
59
+ * State model:
60
+ * - Epochs: one {@link EpochState} for each `source/epoch`. A record with a seen seq is dropped.
61
+ * - Heartbeats: one lease for each `source/epoch`. Each new record and each `touch` renews it. A live epoch whose
62
+ * lease expires is lost.
63
+ * - Event buffer: every record except series, limited by age and bytes. Eviction records a gap.
64
+ * - Series rings: one {@link SeriesRing} for each `track` and value key of `sample` and `sample.chunk` records.
65
+ * - Pinned set: the latest `source.started` of each live epoch, live streams, recent clock snapshots and the
66
+ * discontinuities between them, open spans with merged updates, the latest state of each state track, and the latest
67
+ * definition of each kind and key. It stays outside the limits, so a late viewer and a dump can describe themselves.
68
+ * - Clock records: a reader picks the clock segment of a value from the snapshots and discontinuities that it applied
69
+ * before. So a backlog or a dump sends a pinned clock record only when it does not also send its buffered copy, and
70
+ * the clock history keeps its arrival order.
71
+ * - The collector is a source too. It writes `source.lost` with its own identity.
72
+ *
73
+ * The host calls {@link TraceCollector.tick} about once a second. It compacts series, evicts old records, checks
74
+ * heartbeats, and checks span deadlines. The collector reads no clock except `now`.
75
+ */
76
+ declare class TraceCollector {
77
+ readonly traceClock: string;
78
+ readonly clocks: ClockGraph;
79
+ private readonly options;
80
+ private readonly self;
81
+ private readonly epochs;
82
+ /** The live epoch of each source. */
83
+ private readonly liveEpochBySource;
84
+ private readonly heartbeats;
85
+ private readonly buffer;
86
+ /** The groups that the buffer dropped in the current push or eviction. They become one gap. */
87
+ private evicted;
88
+ private gaps;
89
+ private readonly seriesRings;
90
+ private readonly pinnedStarts;
91
+ private readonly pinnedStreams;
92
+ private readonly pinnedSnapshots;
93
+ private pinnedDiscontinuities;
94
+ private arrivals;
95
+ private readonly pinnedSpans;
96
+ private readonly pinnedStates;
97
+ private readonly pinnedDefinitions;
98
+ private readonly attributeSensitivity;
99
+ private readonly feed;
100
+ /** The tracks of each viewer, by subscription ID. */
101
+ private readonly demand;
102
+ private nextSubscriptionId;
103
+ /** The open spans that already triggered an overdue dump. A key goes when its span ends or its epoch ends. */
104
+ private readonly dumpedSpans;
105
+ /** The flight recorder cooldown of each source. */
106
+ private readonly dumpCooldown;
107
+ private lastDumpEnd;
108
+ private lateCount;
109
+ private droppedCount;
110
+ /** Off: the collector forwards records to viewers but keeps only the pinned set and the flight window. */
111
+ private buffering;
112
+ /**
113
+ * The records of the last flight window while buffering is off, for the flight recorder only. Viewers never read it,
114
+ * so a view that opens while buffering is off still has no history.
115
+ */
116
+ private readonly flightWindow?;
117
+ constructor(options: TraceCollectorOptions);
118
+ get stats(): {
119
+ bufferedRecords: number;
120
+ bufferedBytes: number;
121
+ openSpans: number;
122
+ seriesTracks: number;
123
+ late: number;
124
+ dropped: number;
125
+ maxAgeMs: number;
126
+ maxBytes: number;
127
+ };
128
+ /**
129
+ * Validates a record from a trust boundary, such as IPC from a window, then ingests it.
130
+ * Returns false and counts the record as dropped when it is not a valid record.
131
+ */
132
+ ingestUntrusted(value: unknown): boolean;
133
+ /**
134
+ * Turns the event buffer and the series rings on or off. Off drops what they hold. Viewers still get live records,
135
+ * and the pinned set stays, so a viewer that opens later still knows the live sources and open spans. With a flight
136
+ * recorder, off keeps the last flight window of events for dumps only.
137
+ */
138
+ setBuffering(enabled: boolean): void;
139
+ /** Writes a record of the collector itself, for example a discontinuity of its clock after a system resume. */
140
+ record(input: TraceRecordInput): void;
141
+ /** Ingests a record of a trusted producer in the same process. */
142
+ ingest(record: TraceRecord): void;
143
+ /** Records that a source is alive without a record, for example on a clock ping. */
144
+ touch(source: string): void;
145
+ /** The transport of a source closed. Its live epoch is lost unless it stopped first. */
146
+ closeSource(source: string): void;
147
+ /**
148
+ * Adds a viewer. It receives the pinned set, the gaps, the buffer, the series, `caughtUp`, and then each later
149
+ * record, in that order. Nothing arrives between the backlog and the first live record, because one call does both.
150
+ */
151
+ subscribe(listener: (message: TraceFeedMessage) => void, tracks?: Iterable<string> | '*'): {
152
+ unsubscribe: () => void;
153
+ setTracks: (tracks: Iterable<string> | '*') => void;
154
+ };
155
+ /**
156
+ * Runs the periodic work: series compaction, buffer eviction, heartbeat checks, and span deadline checks.
157
+ * The host calls it about once a second.
158
+ */
159
+ tick(): void;
160
+ /**
161
+ * The current state for a manual save: the pinned set and the buffer as records, and the series rings in the shape
162
+ * of the backlog. Samples go to the rings, not to the buffer, so the records alone do not hold them. A reader
163
+ * replays the save with `handleRecords(records)` and then `handle(message)` for each series message.
164
+ */
165
+ snapshot(): CollectorSnapshot;
166
+ /** The sensitivity of an attribute key, from the conventions and the announced definitions. */
167
+ sensitivityOf: (key: string) => TraceAttributeDefinition["sensitivity"] | undefined;
168
+ private accept;
169
+ private createEpoch;
170
+ /** The last seq of each `source/epoch`. */
171
+ private cursor;
172
+ /** The backlog of a new viewer: the pinned set, the gaps, the buffer, and the series. */
173
+ private backlog;
174
+ /** One message for each series ring that has points. */
175
+ private seriesMessages;
176
+ /** Converts the event time of a record to the trace clock. */
177
+ private timeOf;
178
+ private updateClocks;
179
+ private updateDefinitions;
180
+ private updatePinned;
181
+ /**
182
+ * Moves the links of evicted updates into the pinned copy of their open span. The pinned copy merges the attributes
183
+ * of each update on arrival. Links append, so they move only when the update leaves the buffer, and a late viewer
184
+ * does not get a link twice.
185
+ */
186
+ private keepLinksOfEvictedUpdates;
187
+ /** A discontinuity matters only after a pinned snapshot of its clock. A reader has no earlier segment otherwise. */
188
+ private prunePinnedDiscontinuities;
189
+ private unpinEpoch;
190
+ private loseSource;
191
+ private appendSeries;
192
+ /** Turns the records that the buffer dropped since the last call into one gap, and tells the viewers. */
193
+ private reportGap;
194
+ /**
195
+ * Starts a flight recorder dump. Limits: an open span triggers one overdue dump, each source has a cooldown, and a
196
+ * window that overlaps the previous dump appends to it. An error comes from the one end of a span, so it needs no
197
+ * guard, and the guard does not keep the key of an ended span.
198
+ */
199
+ private triggerDump;
200
+ /**
201
+ * The pinned set. `sentFrom` is the arrival order of the first buffered record that the caller also sends. A clock
202
+ * record from that order on is left out, so the reader applies it once, at its place among the buffered records.
203
+ */
204
+ private pinnedRecords;
205
+ }
206
+ //#endregion
207
+ export { CollectorSnapshot, DEFAULT_SERIES_RETENTION, FlightRecorderDump, SeriesRetention, SeriesRing, TraceCollector, TraceCollectorOptions };