effect-inspect 0.1.1 → 0.3.0

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.
Files changed (34) hide show
  1. package/README.md +68 -0
  2. package/app/dist/client/assets/{index-smV05cfr.js → index-T-bCzOWw.js} +2 -2
  3. package/app/dist/client/assets/routes-C2qO8k2W.js +5 -0
  4. package/app/dist/server/assets/{_tanstack-start-manifest_v-CFQ3DEVN.js → _tanstack-start-manifest_v-B2BMiICr.js} +3 -3
  5. package/app/dist/server/assets/{router-CN98Ramo.js → router-dMcw-pHq.js} +1 -1
  6. package/app/dist/server/assets/{routes-eZ4XqxE9.js → routes-BmnrEVoN.js} +203 -157
  7. package/app/dist/server/server.js +2 -2
  8. package/dist/cli/QueryCommands.d.ts +127 -0
  9. package/dist/cli/QueryCommands.js +823 -0
  10. package/dist/cli.d.ts +1 -2
  11. package/dist/cli.js +165 -8
  12. package/dist/client/Client.d.ts +42 -1
  13. package/dist/client/Client.js +88 -4
  14. package/dist/collector/QueryApi.d.ts +29 -0
  15. package/dist/collector/QueryApi.js +91 -0
  16. package/dist/collector/Server.d.ts +1 -1
  17. package/dist/collector/Server.js +15 -8
  18. package/dist/collector/Store.d.ts +56 -16
  19. package/dist/collector/Store.js +37 -11
  20. package/dist/protocol/Codec.d.ts +10 -0
  21. package/dist/protocol/Schema.d.ts +118 -1
  22. package/dist/protocol/Schema.js +53 -1
  23. package/dist/query/Client.d.ts +30 -0
  24. package/dist/query/Client.js +74 -0
  25. package/dist/query/Query.d.ts +689 -0
  26. package/dist/query/Query.js +1017 -0
  27. package/dist/trace/Timing.d.ts +18 -0
  28. package/dist/trace/Timing.js +57 -0
  29. package/dist/trace/TraceFile.d.ts +54 -0
  30. package/dist/trace/TraceFile.js +86 -0
  31. package/dist/trace/TraceStore.d.ts +202 -0
  32. package/dist/trace/TraceStore.js +328 -0
  33. package/package.json +3 -1
  34. package/app/dist/client/assets/routes-TKgeFdSW.js +0 -5
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Span timing derived the same way for the chart, the tooltip, the log, the
3
+ * drawer and the headless query core.
4
+ */
5
+ import type { TraceSpan, TraceStore } from './TraceStore.ts';
6
+ /**
7
+ * Total and self time for a span, in millis.
8
+ *
9
+ * Self time is the span's own duration minus the time covered by its direct
10
+ * children. `from`/`to` clip both to a time window, which is how the
11
+ * aggregation tabs follow the viewport the way Chrome does: a span half inside
12
+ * the window contributes only its visible half, and only the child time that
13
+ * overlaps that half is subtracted.
14
+ */
15
+ export declare const timings: (store: TraceStore, span: TraceSpan, now: number, from?: number, to?: number) => {
16
+ readonly total: number;
17
+ readonly self: number;
18
+ };
@@ -0,0 +1,57 @@
1
+ /** A span's end, or `now` while it is still open. */
2
+ const spanEnd = (span, now) => span.end ?? now;
3
+ /**
4
+ * Time covered by the union of `span`'s direct children, clipped to `[from, to]`.
5
+ *
6
+ * A **union**, not a sum: two children running concurrently for 10ms each
7
+ * occupy 10ms of their parent, not 20ms. Summing would make a heavily
8
+ * concurrent span's self time read as zero (or negative, and clamp to zero),
9
+ * which is exactly the number the aggregation tabs are built to show.
10
+ *
11
+ * Children are sorted by start and swept once, so this is O(k log k) in the
12
+ * number of direct children — not in the subtree, and never in the trace.
13
+ */
14
+ const childUnion = (store, span, now, from, to) => {
15
+ const intervals = [];
16
+ for (const id of span.children) {
17
+ const child = store.spans.get(id);
18
+ if (child === undefined)
19
+ continue;
20
+ const lo = Math.max(child.start, from);
21
+ const hi = Math.min(spanEnd(child, now), to);
22
+ if (hi > lo)
23
+ intervals.push([lo, hi]);
24
+ }
25
+ if (intervals.length === 0)
26
+ return 0;
27
+ intervals.sort((a, b) => a[0] - b[0]);
28
+ let covered = 0;
29
+ let [runStart, runEnd] = intervals[0];
30
+ for (let i = 1; i < intervals.length; i++) {
31
+ const [lo, hi] = intervals[i];
32
+ if (lo > runEnd) {
33
+ covered += runEnd - runStart;
34
+ runStart = lo;
35
+ runEnd = hi;
36
+ }
37
+ else if (hi > runEnd)
38
+ runEnd = hi;
39
+ }
40
+ return covered + (runEnd - runStart);
41
+ };
42
+ /**
43
+ * Total and self time for a span, in millis.
44
+ *
45
+ * Self time is the span's own duration minus the time covered by its direct
46
+ * children. `from`/`to` clip both to a time window, which is how the
47
+ * aggregation tabs follow the viewport the way Chrome does: a span half inside
48
+ * the window contributes only its visible half, and only the child time that
49
+ * overlaps that half is subtracted.
50
+ */
51
+ export const timings = (store, span, now, from = -Infinity, to = Infinity) => {
52
+ const lo = Math.max(span.start, from);
53
+ const hi = Math.min(spanEnd(span, now), to);
54
+ if (hi <= lo)
55
+ return { total: 0, self: 0 };
56
+ return { total: hi - lo, self: Math.max(hi - lo - childUnion(store, span, now, lo, hi), 0) };
57
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Saving a trace to a file and loading it back.
3
+ *
4
+ * The file **is** the protocol message stream: line 1 is a
5
+ * {@link Protocol.TraceFileHeader}, every line after it is a `ClientMessage`
6
+ * exactly as `clientCodec` writes it on the wire. So a new protocol message
7
+ * variant is carried by a saved trace for free — the format version only moves
8
+ * if the header or the line layout changes, never because the protocol grew.
9
+ *
10
+ * Parsing is deliberately lenient about the *tail* and strict about the
11
+ * *head*: a header that will not decode means this is not a trace file and
12
+ * there is nothing to show, while a truncated last line means the writer died
13
+ * mid-save and everything before it is still a real trace worth rendering.
14
+ */
15
+ import { Result } from 'effect';
16
+ import { type ClientMessage, type Session, type TraceCapture, type TraceFileHeader } from '../protocol/Schema.ts';
17
+ /** File extension for a saved trace. */
18
+ export declare const traceFileExtension = ".eitrace";
19
+ /** Why a file could not be read as a trace. `message` is shown to the user verbatim. */
20
+ export interface TraceFileError {
21
+ readonly _tag: 'TraceFileError';
22
+ readonly message: string;
23
+ }
24
+ /** A parsed trace file: its header, its messages, and what was lost at the tail. */
25
+ export interface LoadedTrace {
26
+ readonly header: TraceFileHeader;
27
+ readonly messages: ReadonlyArray<ClientMessage>;
28
+ /**
29
+ * Trailing lines that would not decode, almost always a truncated save.
30
+ *
31
+ * Non-zero is surfaced in the UI rather than thrown: the spans before the cut
32
+ * are real, and a partial trace beats a blank chart.
33
+ */
34
+ readonly truncatedLines: number;
35
+ }
36
+ /**
37
+ * Serializes a session and its messages to trace-file text.
38
+ *
39
+ * `messages` is the raw protocol stream in arrival order — not a re-derivation
40
+ * from the rendered trace model, which would silently drop every message the
41
+ * model does not draw. `capture` is written only when the caller knows the
42
+ * collector's loss counters; omitting it marks completeness as unknown.
43
+ */
44
+ export declare const serializeTraceFile: (session: Session, messages: Iterable<ClientMessage>, savedAtEpochMillis: number, capture?: TraceCapture) => string;
45
+ /**
46
+ * Parses trace-file text.
47
+ *
48
+ * Fails only when the file is not a trace file at all — an unreadable header,
49
+ * a format version from the future, or a body whose *interior* is corrupt. A
50
+ * bad line that is not the last one means the file was edited or mangled, not
51
+ * merely cut short, and rendering a trace with a hole in the middle would be a
52
+ * lie; a bad final line is reported as {@link LoadedTrace.truncatedLines}.
53
+ */
54
+ export declare const parseTraceFile: (text: string) => Result.Result<LoadedTrace, TraceFileError>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Saving a trace to a file and loading it back.
3
+ *
4
+ * The file **is** the protocol message stream: line 1 is a
5
+ * {@link Protocol.TraceFileHeader}, every line after it is a `ClientMessage`
6
+ * exactly as `clientCodec` writes it on the wire. So a new protocol message
7
+ * variant is carried by a saved trace for free — the format version only moves
8
+ * if the header or the line layout changes, never because the protocol grew.
9
+ *
10
+ * Parsing is deliberately lenient about the *tail* and strict about the
11
+ * *head*: a header that will not decode means this is not a trace file and
12
+ * there is nothing to show, while a truncated last line means the writer died
13
+ * mid-save and everything before it is still a real trace worth rendering.
14
+ */
15
+ import { Result } from 'effect';
16
+ import { clientCodec, traceFileHeaderCodec } from '../protocol/Codec.js';
17
+ import { protocolVersion, traceFileFormatVersion, } from '../protocol/Schema.js';
18
+ /** File extension for a saved trace. */
19
+ export const traceFileExtension = '.eitrace';
20
+ const fail = (message) => Result.fail({ _tag: 'TraceFileError', message });
21
+ /**
22
+ * Serializes a session and its messages to trace-file text.
23
+ *
24
+ * `messages` is the raw protocol stream in arrival order — not a re-derivation
25
+ * from the rendered trace model, which would silently drop every message the
26
+ * model does not draw. `capture` is written only when the caller knows the
27
+ * collector's loss counters; omitting it marks completeness as unknown.
28
+ */
29
+ export const serializeTraceFile = (session, messages, savedAtEpochMillis, capture) => {
30
+ const header = traceFileHeaderCodec.encode({
31
+ _tag: 'TraceFileHeader',
32
+ formatVersion: traceFileFormatVersion,
33
+ protocolVersion,
34
+ session,
35
+ savedAtEpochMillis,
36
+ ...(capture === undefined ? {} : { capture }),
37
+ });
38
+ const body = [];
39
+ for (const message of messages)
40
+ body.push(clientCodec.encode(message));
41
+ return header + body.join('');
42
+ };
43
+ /**
44
+ * Parses trace-file text.
45
+ *
46
+ * Fails only when the file is not a trace file at all — an unreadable header,
47
+ * a format version from the future, or a body whose *interior* is corrupt. A
48
+ * bad line that is not the last one means the file was edited or mangled, not
49
+ * merely cut short, and rendering a trace with a hole in the middle would be a
50
+ * lie; a bad final line is reported as {@link LoadedTrace.truncatedLines}.
51
+ */
52
+ export const parseTraceFile = (text) => {
53
+ const lines = text.split('\n');
54
+ const headerLine = lines[0];
55
+ if (headerLine === undefined || headerLine.trim() === '') {
56
+ return fail('The file is empty.');
57
+ }
58
+ const headerResult = traceFileHeaderCodec.decode(headerLine);
59
+ if (Result.isFailure(headerResult)) {
60
+ return fail('This is not an effect-inspect trace file: its header could not be read.');
61
+ }
62
+ const header = headerResult.success;
63
+ if (header.formatVersion > traceFileFormatVersion) {
64
+ return fail(`This trace file is format version ${header.formatVersion}; this build understands up to ${traceFileFormatVersion}.`);
65
+ }
66
+ const messages = [];
67
+ let truncatedLines = 0;
68
+ for (let index = 1; index < lines.length; index++) {
69
+ const line = lines[index];
70
+ if (line.trim() === '')
71
+ continue;
72
+ const decoded = clientCodec.decode(line);
73
+ if (Result.isSuccess(decoded)) {
74
+ messages.push(decoded.success);
75
+ continue;
76
+ }
77
+ // Only the final line may be a casualty of a truncated write; a bad line
78
+ // with good lines after it means the file is corrupt, not cut short.
79
+ const isLast = lines.slice(index + 1).every((rest) => rest.trim() === '');
80
+ if (!isLast) {
81
+ return fail(`This trace file is corrupt: line ${index + 1} could not be read.`);
82
+ }
83
+ truncatedLines = 1;
84
+ }
85
+ return Result.succeed({ header, messages, truncatedLines });
86
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The in-memory trace model for one session.
3
+ *
4
+ * This is deliberately **not** React state and **not** an atom. A dense trace
5
+ * is 10k+ spans and the flame chart redraws on every pan/zoom frame; putting a
6
+ * span in React state would cost a reconciliation per span, and putting one in
7
+ * an atom would cost a subscription per span. Instead the store is a plain
8
+ * mutable structure that the canvas renderer reads directly during its draw
9
+ * call, and React only ever learns that *something* changed — via
10
+ * {@link TraceStore.version} — never what.
11
+ *
12
+ * The shapes here are tuned for the renderer's access pattern: it walks spans
13
+ * in depth-then-start order once per frame, so `rows` is maintained
14
+ * incrementally rather than rebuilt, and every field the renderer touches is a
15
+ * number rather than a `bigint` (see {@link TraceSpan.start}).
16
+ */
17
+ import type { Attributes, Json, LogLevel, SpanKind, SpanOutcome } from '../protocol/Schema.ts';
18
+ import type { ClientMessage } from '../protocol/Schema.ts';
19
+ /**
20
+ * A span in renderer-facing form.
21
+ *
22
+ * Times are **milliseconds relative to the session's first observed event**,
23
+ * as `number`, not the protocol's absolute `bigint` nanos. The renderer does
24
+ * arithmetic on these every frame and `bigint` maths is both slower and
25
+ * unusable in canvas coordinates; the conversion happens once, here, at ingest.
26
+ * {@link TraceStore.epochOrigin} keeps the absolute anchor for display.
27
+ */
28
+ export interface TraceSpan {
29
+ readonly spanId: string;
30
+ readonly traceId: string;
31
+ readonly name: string;
32
+ readonly kind: SpanKind;
33
+ /** Parent span id, or `undefined` for a root (or externally-parented) span. */
34
+ readonly parentId: string | undefined;
35
+ /** Milliseconds since {@link TraceStore.origin}. */
36
+ readonly start: number;
37
+ /** Milliseconds since {@link TraceStore.origin}, or `undefined` while open. */
38
+ end: number | undefined;
39
+ /** Nesting depth; 0 for a root span. */
40
+ depth: number;
41
+ /** Set once `SpanEnd` arrives. */
42
+ outcome: SpanOutcome | undefined;
43
+ /** `SpanStart` attributes merged with the late ones from `SpanEnd`. */
44
+ attributes: Record<string, Json>;
45
+ /** Child span ids, in arrival order. */
46
+ readonly children: Array<string>;
47
+ /** Point-in-time events on this span, in arrival order. */
48
+ readonly events: Array<TraceSpanEvent>;
49
+ readonly fiberId: number | undefined;
50
+ /** True when the parent id was seen but the parent span has not arrived. */
51
+ orphaned: boolean;
52
+ }
53
+ /** A point-in-time event recorded against a span. */
54
+ export interface TraceSpanEvent {
55
+ readonly name: string;
56
+ /** Milliseconds since {@link TraceStore.origin}. */
57
+ readonly time: number;
58
+ readonly attributes: Attributes;
59
+ }
60
+ /** A log record, kept in arrival order alongside the spans. */
61
+ export interface TraceLog {
62
+ /** Milliseconds since {@link TraceStore.origin}. */
63
+ readonly time: number;
64
+ readonly level: LogLevel;
65
+ readonly message: Json;
66
+ readonly spanId: string | undefined;
67
+ readonly fiberId: number | undefined;
68
+ readonly annotations: Attributes;
69
+ }
70
+ /**
71
+ * One `process.memoryUsage()` reading, in the chart's time base.
72
+ *
73
+ * Figures stay in bytes; the track formats them. `time` is relative millis like
74
+ * everything else here, so the memory curve and the flame bars share an x-axis
75
+ * by construction rather than by two pieces of code agreeing.
76
+ */
77
+ export interface TraceMemorySample {
78
+ /** Milliseconds since {@link TraceStore.origin}. */
79
+ readonly time: number;
80
+ readonly heapUsed: number;
81
+ readonly heapTotal: number;
82
+ readonly rss: number;
83
+ readonly external: number;
84
+ }
85
+ /** Cheap counters for the header, so the UI never walks the span map to count. */
86
+ export interface TraceStats {
87
+ readonly spans: number;
88
+ readonly openSpans: number;
89
+ readonly errors: number;
90
+ readonly logs: number;
91
+ readonly events: number;
92
+ /** Milliseconds since {@link TraceStore.origin} of the latest observed time. */
93
+ readonly duration: number;
94
+ }
95
+ /**
96
+ * Mutable span index for one session.
97
+ *
98
+ * Ingest is `apply`, one protocol message at a time, in arrival order. Reads
99
+ * are direct field access — `spans`, `roots` and `rows` are live structures,
100
+ * not copies, so the renderer must treat them as read-only and must re-read
101
+ * them (not cache them) whenever {@link version} changes.
102
+ */
103
+ export declare class TraceStore {
104
+ /** Every span seen, by span id. Includes spans that are still open. */
105
+ readonly spans: Map<string, TraceSpan>;
106
+ /** Root span ids in arrival order — the renderer's entry points. */
107
+ readonly roots: Array<string>;
108
+ /** Span ids that have no `SpanEnd` yet, so the renderer can draw them open-ended. */
109
+ readonly openSpans: Set<string>;
110
+ /** Logs in arrival order. */
111
+ readonly logs: Array<TraceLog>;
112
+ /**
113
+ * Memory samples in arrival order, which is also time order.
114
+ *
115
+ * A plain array rather than anything indexed: the track draws the whole
116
+ * series each frame by walking it once, and at the client's 100ms interval a
117
+ * ten-minute trace is 6,000 entries — a scan the renderer does not notice.
118
+ * ponytail: linear scan, swap for a binary search into the viewport if a
119
+ * trace ever runs long enough for it to show up in a frame budget.
120
+ */
121
+ readonly memory: Array<TraceMemorySample>;
122
+ /**
123
+ * Largest `heapUsed` seen — the memory track's y-axis top, kept here so the
124
+ * renderer never re-scans the series to scale a frame.
125
+ */
126
+ memoryPeak: number;
127
+ /** Smallest `heapUsed` seen — the memory track's y-axis floor. */
128
+ memoryTrough: number;
129
+ /** Largest `rss` seen; the secondary line has its own scale. */
130
+ memoryRssPeak: number;
131
+ /**
132
+ * Every message ingested, in arrival order — the source for saving to a file.
133
+ *
134
+ * The rendered model above is lossy on purpose (relative millis, merged
135
+ * attributes, `Metrics`/`FiberEvent` dropped), so a file written from it
136
+ * would quietly lose whatever the chart does not draw. Keeping the decoded
137
+ * messages costs one array slot each — they are already allocated — and
138
+ * makes save a copy rather than a re-derivation.
139
+ */
140
+ readonly raw: Array<ClientMessage>;
141
+ /**
142
+ * Span ids bucketed by depth: `rows[2]` is every span nested two levels deep.
143
+ *
144
+ * This is the flame chart's row layout. It is maintained incrementally on
145
+ * ingest so the renderer never has to traverse the tree to find a row, and
146
+ * so drawing a viewport means scanning only the rows it covers.
147
+ */
148
+ readonly rows: Array<Array<string>>;
149
+ /**
150
+ * Spans waiting on a parent that has not arrived, keyed by the missing
151
+ * parent id.
152
+ *
153
+ * A child can legitimately precede its parent: the backlog preserves arrival
154
+ * order, and a parent's `SpanStart` is emitted when it opens, which a
155
+ * concurrent fiber's child can beat to the wire. Rather than drop such a
156
+ * span, it is parked here and re-linked when the parent shows up.
157
+ */
158
+ private readonly pendingChildren;
159
+ /** Monotonic nanos of the first event seen; the zero point for `start`/`end`. */
160
+ origin: bigint | undefined;
161
+ /** Wall-clock millis matching {@link origin}, from the session's `Hello`. */
162
+ epochOrigin: number | undefined;
163
+ /**
164
+ * Bumped on every mutation.
165
+ *
166
+ * This is the *only* value React is allowed to observe. The renderer polls
167
+ * it per frame to decide whether to redraw; the UI mirrors it into an atom
168
+ * on a timer so counters update without a render per span.
169
+ */
170
+ version: number;
171
+ private spanCount;
172
+ private errorCount;
173
+ private eventCount;
174
+ private maxTime;
175
+ /** Snapshot of the counters — allocates, so call it per repaint, not per span. */
176
+ stats(): TraceStats;
177
+ /** Drops everything — used when switching sessions. */
178
+ clear(): void;
179
+ /** Ingests one client message. Unknown/undrawn variants are ignored, not errors. */
180
+ apply(message: ClientMessage): void;
181
+ /** Ingests a batch, bumping `version` once rather than per message. */
182
+ applyAll(messages: Iterable<ClientMessage>): void;
183
+ private anchor;
184
+ private relative;
185
+ private applySpanStart;
186
+ /** Re-links children that arrived before this span did. */
187
+ private adoptPending;
188
+ /**
189
+ * Moves a subtree to a new depth after a late parent arrives.
190
+ *
191
+ * Iterative rather than recursive: a deeply-nested Effect program can stack
192
+ * hundreds of spans and a blown call stack during ingest would take the
193
+ * whole webapp down.
194
+ */
195
+ private redepth;
196
+ private addToRow;
197
+ private removeFromRow;
198
+ private applySpanEnd;
199
+ private applySpanEvent;
200
+ private applyMemorySample;
201
+ private applyLog;
202
+ }