@hydranium/protocol 1.0.0-next.10
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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/lib/abstract-logger.d.ts +66 -0
- package/lib/abstract-logger.d.ts.map +1 -0
- package/lib/abstract-logger.js +110 -0
- package/lib/abstract-logger.js.map +1 -0
- package/lib/browser-runtime.d.ts +124 -0
- package/lib/browser-runtime.d.ts.map +1 -0
- package/lib/browser-runtime.js +11 -0
- package/lib/browser-runtime.js.map +1 -0
- package/lib/client/data-events.d.ts +49 -0
- package/lib/client/data-events.d.ts.map +1 -0
- package/lib/client/data-events.js +60 -0
- package/lib/client/data-events.js.map +1 -0
- package/lib/client/data-port.d.ts +95 -0
- package/lib/client/data-port.d.ts.map +1 -0
- package/lib/client/data-port.js +11 -0
- package/lib/client/data-port.js.map +1 -0
- package/lib/client/data-session.d.ts +129 -0
- package/lib/client/data-session.d.ts.map +1 -0
- package/lib/client/data-session.js +179 -0
- package/lib/client/data-session.js.map +1 -0
- package/lib/client/index.d.ts +39 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +55 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/message-relay.d.ts +111 -0
- package/lib/client/message-relay.d.ts.map +1 -0
- package/lib/client/message-relay.js +139 -0
- package/lib/client/message-relay.js.map +1 -0
- package/lib/client/post-message-transport.d.ts +77 -0
- package/lib/client/post-message-transport.d.ts.map +1 -0
- package/lib/client/post-message-transport.js +95 -0
- package/lib/client/post-message-transport.js.map +1 -0
- package/lib/clock.d.ts +111 -0
- package/lib/clock.d.ts.map +1 -0
- package/lib/clock.js +74 -0
- package/lib/clock.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +39 -0
- package/lib/data/data-protocol-methods.d.ts.map +1 -0
- package/lib/data/data-protocol-methods.js +61 -0
- package/lib/data/data-protocol-methods.js.map +1 -0
- package/lib/data/data-server-protocol.d.ts +264 -0
- package/lib/data/data-server-protocol.d.ts.map +1 -0
- package/lib/data/data-server-protocol.js +11 -0
- package/lib/data/data-server-protocol.js.map +1 -0
- package/lib/data/diagnostics.d.ts +97 -0
- package/lib/data/diagnostics.d.ts.map +1 -0
- package/lib/data/diagnostics.js +25 -0
- package/lib/data/diagnostics.js.map +1 -0
- package/lib/data/events.d.ts +107 -0
- package/lib/data/events.d.ts.map +1 -0
- package/lib/data/events.js +11 -0
- package/lib/data/events.js.map +1 -0
- package/lib/data/index.d.ts +15 -0
- package/lib/data/index.d.ts.map +1 -0
- package/lib/data/index.js +38 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/methods.d.ts +57 -0
- package/lib/data/methods.d.ts.map +1 -0
- package/lib/data/methods.js +60 -0
- package/lib/data/methods.js.map +1 -0
- package/lib/data/requests.d.ts +65 -0
- package/lib/data/requests.d.ts.map +1 -0
- package/lib/data/requests.js +11 -0
- package/lib/data/requests.js.map +1 -0
- package/lib/debouncer.d.ts +75 -0
- package/lib/debouncer.d.ts.map +1 -0
- package/lib/debouncer.js +91 -0
- package/lib/debouncer.js.map +1 -0
- package/lib/errors.d.ts +84 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +113 -0
- package/lib/errors.js.map +1 -0
- package/lib/host-diagnostics.d.ts +58 -0
- package/lib/host-diagnostics.d.ts.map +1 -0
- package/lib/host-diagnostics.js +18 -0
- package/lib/host-diagnostics.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/latency-collector.d.ts +126 -0
- package/lib/latency-collector.d.ts.map +1 -0
- package/lib/latency-collector.js +137 -0
- package/lib/latency-collector.js.map +1 -0
- package/lib/logger.d.ts +119 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +126 -0
- package/lib/logger.js.map +1 -0
- package/lib/model-server.d.ts +352 -0
- package/lib/model-server.d.ts.map +1 -0
- package/lib/model-server.js +160 -0
- package/lib/model-server.js.map +1 -0
- package/lib/model-service/args.d.ts +64 -0
- package/lib/model-service/args.d.ts.map +1 -0
- package/lib/model-service/args.js +11 -0
- package/lib/model-service/args.js.map +1 -0
- package/lib/model-service/index.d.ts +11 -0
- package/lib/model-service/index.d.ts.map +1 -0
- package/lib/model-service/index.js +31 -0
- package/lib/model-service/index.js.map +1 -0
- package/lib/model-service/reference-candidate.d.ts +56 -0
- package/lib/model-service/reference-candidate.d.ts.map +1 -0
- package/lib/model-service/reference-candidate.js +11 -0
- package/lib/model-service/reference-candidate.js.map +1 -0
- package/lib/noop-logger.d.ts +25 -0
- package/lib/noop-logger.d.ts.map +1 -0
- package/lib/noop-logger.js +36 -0
- package/lib/noop-logger.js.map +1 -0
- package/lib/observable-value.d.ts +72 -0
- package/lib/observable-value.d.ts.map +1 -0
- package/lib/observable-value.js +52 -0
- package/lib/observable-value.js.map +1 -0
- package/lib/patch-merge.d.ts +123 -0
- package/lib/patch-merge.d.ts.map +1 -0
- package/lib/patch-merge.js +104 -0
- package/lib/patch-merge.js.map +1 -0
- package/lib/profile-session.d.ts +87 -0
- package/lib/profile-session.d.ts.map +1 -0
- package/lib/profile-session.js +98 -0
- package/lib/profile-session.js.map +1 -0
- package/lib/profiling.d.ts +83 -0
- package/lib/profiling.d.ts.map +1 -0
- package/lib/profiling.js +20 -0
- package/lib/profiling.js.map +1 -0
- package/lib/project.d.ts +121 -0
- package/lib/project.d.ts.map +1 -0
- package/lib/project.js +34 -0
- package/lib/project.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +100 -0
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -0
- package/lib/rpc/bind-rpc-methods.js +110 -0
- package/lib/rpc/bind-rpc-methods.js.map +1 -0
- package/lib/rpc/create-rpc-proxy.d.ts +188 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -0
- package/lib/rpc/create-rpc-proxy.js +158 -0
- package/lib/rpc/create-rpc-proxy.js.map +1 -0
- package/lib/rpc/index.d.ts +12 -0
- package/lib/rpc/index.d.ts.map +1 -0
- package/lib/rpc/index.js +31 -0
- package/lib/rpc/index.js.map +1 -0
- package/lib/rpc/wire-prefix.d.ts +37 -0
- package/lib/rpc/wire-prefix.d.ts.map +1 -0
- package/lib/rpc/wire-prefix.js +49 -0
- package/lib/rpc/wire-prefix.js.map +1 -0
- package/lib/testing/data-doubles.d.ts +121 -0
- package/lib/testing/data-doubles.d.ts.map +1 -0
- package/lib/testing/data-doubles.js +102 -0
- package/lib/testing/data-doubles.js.map +1 -0
- package/lib/testing/fake-clock.d.ts +35 -0
- package/lib/testing/fake-clock.d.ts.map +1 -0
- package/lib/testing/fake-clock.js +86 -0
- package/lib/testing/fake-clock.js.map +1 -0
- package/lib/testing/harness.d.ts +24 -0
- package/lib/testing/harness.d.ts.map +1 -0
- package/lib/testing/harness.js +11 -0
- package/lib/testing/harness.js.map +1 -0
- package/lib/testing/index.d.ts +13 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +41 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/node/duplex-connection.d.ts +25 -0
- package/lib/testing/node/duplex-connection.d.ts.map +1 -0
- package/lib/testing/node/duplex-connection.js +34 -0
- package/lib/testing/node/duplex-connection.js.map +1 -0
- package/lib/testing/node/duplex-stream.d.ts +32 -0
- package/lib/testing/node/duplex-stream.d.ts.map +1 -0
- package/lib/testing/node/duplex-stream.js +35 -0
- package/lib/testing/node/duplex-stream.js.map +1 -0
- package/lib/testing/node/index.d.ts +11 -0
- package/lib/testing/node/index.d.ts.map +1 -0
- package/lib/testing/node/index.js +37 -0
- package/lib/testing/node/index.js.map +1 -0
- package/lib/testing/wait-for.d.ts +69 -0
- package/lib/testing/wait-for.d.ts.map +1 -0
- package/lib/testing/wait-for.js +67 -0
- package/lib/testing/wait-for.js.map +1 -0
- package/lib/tracer.d.ts +115 -0
- package/lib/tracer.d.ts.map +1 -0
- package/lib/tracer.js +205 -0
- package/lib/tracer.js.map +1 -0
- package/lib/transfer-diagnostic.d.ts +73 -0
- package/lib/transfer-diagnostic.d.ts.map +1 -0
- package/lib/transfer-diagnostic.js +45 -0
- package/lib/transfer-diagnostic.js.map +1 -0
- package/lib/transfer-document.d.ts +73 -0
- package/lib/transfer-document.d.ts.map +1 -0
- package/lib/transfer-document.js +46 -0
- package/lib/transfer-document.js.map +1 -0
- package/lib/transfer-element.d.ts +39 -0
- package/lib/transfer-element.d.ts.map +1 -0
- package/lib/transfer-element.js +11 -0
- package/lib/transfer-element.js.map +1 -0
- package/lib/uri.d.ts +62 -0
- package/lib/uri.d.ts.map +1 -0
- package/lib/uri.js +28 -0
- package/lib/uri.js.map +1 -0
- package/lib/util.d.ts +147 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +211 -0
- package/lib/util.js.map +1 -0
- package/package.json +100 -0
- package/src/abstract-logger.ts +120 -0
- package/src/browser-runtime.ts +135 -0
- package/src/client/data-events.ts +71 -0
- package/src/client/data-port.ts +99 -0
- package/src/client/data-session.ts +209 -0
- package/src/client/index.ts +40 -0
- package/src/client/message-relay.ts +216 -0
- package/src/client/post-message-transport.ts +167 -0
- package/src/clock.ts +173 -0
- package/src/data/data-protocol-methods.ts +79 -0
- package/src/data/data-server-protocol.ts +296 -0
- package/src/data/diagnostics.ts +116 -0
- package/src/data/events.ts +127 -0
- package/src/data/index.ts +23 -0
- package/src/data/methods.ts +59 -0
- package/src/data/requests.ts +70 -0
- package/src/debouncer.ts +118 -0
- package/src/errors.ts +127 -0
- package/src/host-diagnostics.ts +63 -0
- package/src/index.ts +41 -0
- package/src/latency-collector.ts +231 -0
- package/src/logger.ts +172 -0
- package/src/model-server.ts +479 -0
- package/src/model-service/args.ts +67 -0
- package/src/model-service/index.ts +16 -0
- package/src/model-service/reference-candidate.ts +58 -0
- package/src/noop-logger.ts +36 -0
- package/src/observable-value.ts +92 -0
- package/src/patch-merge.ts +165 -0
- package/src/profile-session.ts +164 -0
- package/src/profiling.ts +113 -0
- package/src/project.ts +125 -0
- package/src/rpc/README.md +163 -0
- package/src/rpc/bind-rpc-methods.ts +180 -0
- package/src/rpc/create-rpc-proxy.ts +292 -0
- package/src/rpc/index.ts +16 -0
- package/src/rpc/wire-prefix.ts +48 -0
- package/src/testing/data-doubles.ts +178 -0
- package/src/testing/fake-clock.ts +106 -0
- package/src/testing/harness.ts +24 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/node/duplex-connection.ts +50 -0
- package/src/testing/node/duplex-stream.ts +51 -0
- package/src/testing/node/index.ts +22 -0
- package/src/testing/wait-for.ts +94 -0
- package/src/tracer.ts +277 -0
- package/src/transfer-diagnostic.ts +92 -0
- package/src/transfer-document.ts +99 -0
- package/src/transfer-element.ts +43 -0
- package/src/uri.ts +61 -0
- package/src/util.ts +250 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Per-operation latency/throughput accumulator — the time/throughput axis of a
|
|
12
|
+
* profiling run (the CPU profile says "where time went in aggregate"; this says
|
|
13
|
+
* "method X ran N times at p99 Y ms"). It sits on the framework's timing
|
|
14
|
+
* primitives (`Clock`), not on a Node inspector session, so it is neutral and
|
|
15
|
+
* lives here in `protocol`: the RPC binder feeds it at one chokepoint and the
|
|
16
|
+
* LSP connection decorator feeds it at another, both cross-head. A running head
|
|
17
|
+
* holds one collector and reads `report()` out (e.g. over the diagnostics wire
|
|
18
|
+
* or into a bundle's `server-latency.json`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type Clock, type Stopwatch, SystemClock } from './clock.js';
|
|
22
|
+
|
|
23
|
+
/** Latency/throughput for one method over the collection window. */
|
|
24
|
+
export interface MethodLatency {
|
|
25
|
+
/**
|
|
26
|
+
* Whatever key the caller passed to {@link LatencyCollector.record} or
|
|
27
|
+
* {@link LatencyCollector.time} — the framework feeds it RPC and LSP method
|
|
28
|
+
* names, but nothing enforces that, so two feeds using different spellings
|
|
29
|
+
* for one operation report as two methods.
|
|
30
|
+
*/
|
|
31
|
+
method: string;
|
|
32
|
+
/**
|
|
33
|
+
* Calls since construction or the last {@link LatencyCollector.reset}, which
|
|
34
|
+
* is the only thing that clears it. Unaffected by retention: a ring-buffer
|
|
35
|
+
* collector keeps counting long after it stops keeping samples. Failed calls
|
|
36
|
+
* are counted — a call that threw still consumed time.
|
|
37
|
+
*/
|
|
38
|
+
count: number;
|
|
39
|
+
/**
|
|
40
|
+
* Milliseconds, nearest-rank over the RETAINED samples rather than over
|
|
41
|
+
* {@link count} calls, so under `ring-buffer` retention it describes only the
|
|
42
|
+
* trailing window and can sit well below {@link maxMs}. `0` means no samples
|
|
43
|
+
* were retained, not that calls were instant.
|
|
44
|
+
*/
|
|
45
|
+
p50Ms: number;
|
|
46
|
+
/** Milliseconds, nearest-rank; same retention caveat as {@link p50Ms}. */
|
|
47
|
+
p99Ms: number;
|
|
48
|
+
/**
|
|
49
|
+
* Milliseconds, the slowest single call since the last reset. A lifetime
|
|
50
|
+
* figure, so a spike that has aged out of a ring buffer still shows here
|
|
51
|
+
* after the percentiles have forgotten it.
|
|
52
|
+
*/
|
|
53
|
+
maxMs: number;
|
|
54
|
+
/**
|
|
55
|
+
* Milliseconds summed across every call since the last reset, retention
|
|
56
|
+
* notwithstanding. This is what {@link LatencyCollector.report} ranks on, so
|
|
57
|
+
* it orders by total cost rather than by per-call slowness.
|
|
58
|
+
*/
|
|
59
|
+
totalMs: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A collector snapshot: the window length and the per-method latencies, hottest first. */
|
|
63
|
+
export interface LatencyReport {
|
|
64
|
+
/**
|
|
65
|
+
* Milliseconds elapsed since construction or the last
|
|
66
|
+
* {@link LatencyCollector.reset}, off the monotonic stopwatch. It bounds the
|
|
67
|
+
* throughput a caller can derive; it is not the sum of the method totals,
|
|
68
|
+
* which overlap and can exceed it under concurrency.
|
|
69
|
+
*/
|
|
70
|
+
windowMs: number;
|
|
71
|
+
/**
|
|
72
|
+
* Ordered by {@link MethodLatency.totalMs} descending. A method that was
|
|
73
|
+
* never called is absent rather than present with zeroes, so an empty array
|
|
74
|
+
* means nothing ran in the window.
|
|
75
|
+
*/
|
|
76
|
+
methods: MethodLatency[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How many samples a collector retains for percentile estimation. `keep-all`
|
|
81
|
+
* (the default) stores every duration for exact nearest-rank percentiles — sized
|
|
82
|
+
* for a bounded diagnostics window that gets read then {@link LatencyCollector.reset}.
|
|
83
|
+
* `ring-buffer` caps retention at `maxSamplesPerMethod`, keeping only the most
|
|
84
|
+
* recent samples so a collector left running indefinitely cannot grow without
|
|
85
|
+
* bound; percentiles then cover that trailing window while `count`/`totalMs`/
|
|
86
|
+
* `maxMs` remain lifetime totals.
|
|
87
|
+
*/
|
|
88
|
+
export type LatencyRetention = { readonly kind: 'keep-all' } | { readonly kind: 'ring-buffer'; readonly maxSamplesPerMethod: number };
|
|
89
|
+
|
|
90
|
+
/** Per-method state: retained samples (bounded in ring-buffer mode) plus lifetime totals. */
|
|
91
|
+
interface MethodAccumulator {
|
|
92
|
+
/** Retained durations for percentile estimation; capped in ring-buffer mode. Order is irrelevant (report sorts). */
|
|
93
|
+
readonly samples: number[];
|
|
94
|
+
/** Next slot to overwrite once the ring buffer is full (ring-buffer mode only). */
|
|
95
|
+
writeIndex: number;
|
|
96
|
+
/** Lifetime call count, unaffected by retention. */
|
|
97
|
+
count: number;
|
|
98
|
+
/** Lifetime total duration, unaffected by retention. */
|
|
99
|
+
totalMs: number;
|
|
100
|
+
/** Lifetime maximum duration, so a spike that ages out of the window still shows. */
|
|
101
|
+
maxMs: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Nearest-rank percentile of an ascending-sorted array (empty → 0). */
|
|
105
|
+
function percentile(sortedAscending: readonly number[], percent: number): number {
|
|
106
|
+
if (sortedAscending.length === 0) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const rank = Math.ceil((percent / 100) * sortedAscending.length);
|
|
110
|
+
return sortedAscending[Math.min(sortedAscending.length - 1, Math.max(0, rank - 1))];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Accumulates per-method call durations and reports count + latency percentiles.
|
|
115
|
+
* Exact (keeps every duration per method) — sized for a diagnostics window, not
|
|
116
|
+
* unbounded production telemetry; {@link reset} drops the samples and restarts
|
|
117
|
+
* the window. Feed it via {@link time} (wraps a call) or {@link record} (a
|
|
118
|
+
* pre-measured duration). Timing rides an injectable {@link Clock} so tests are
|
|
119
|
+
* deterministic on a fake clock.
|
|
120
|
+
*/
|
|
121
|
+
export class LatencyCollector {
|
|
122
|
+
protected readonly durations = new Map<string, MethodAccumulator>();
|
|
123
|
+
protected window: Stopwatch;
|
|
124
|
+
|
|
125
|
+
constructor(
|
|
126
|
+
protected readonly clock: Clock = new SystemClock(),
|
|
127
|
+
protected readonly retention: LatencyRetention = { kind: 'keep-all' }
|
|
128
|
+
) {
|
|
129
|
+
if (retention.kind === 'ring-buffer' && retention.maxSamplesPerMethod < 1) {
|
|
130
|
+
throw new RangeError(`ring-buffer maxSamplesPerMethod must be >= 1, got ${retention.maxSamplesPerMethod}`);
|
|
131
|
+
}
|
|
132
|
+
this.window = this.clock.stopwatch();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Record a pre-measured call duration (ms) for `method`. */
|
|
136
|
+
record(method: string, durationMs: number): void {
|
|
137
|
+
let accumulator = this.durations.get(method);
|
|
138
|
+
if (!accumulator) {
|
|
139
|
+
accumulator = { samples: [], writeIndex: 0, count: 0, totalMs: 0, maxMs: 0 };
|
|
140
|
+
this.durations.set(method, accumulator);
|
|
141
|
+
}
|
|
142
|
+
accumulator.count++;
|
|
143
|
+
accumulator.totalMs += durationMs;
|
|
144
|
+
if (durationMs > accumulator.maxMs) {
|
|
145
|
+
accumulator.maxMs = durationMs;
|
|
146
|
+
}
|
|
147
|
+
if (this.retention.kind === 'ring-buffer' && accumulator.samples.length >= this.retention.maxSamplesPerMethod) {
|
|
148
|
+
// Buffer full: overwrite the oldest slot in O(1) (percentiles sort, so order does not matter).
|
|
149
|
+
accumulator.samples[accumulator.writeIndex] = durationMs;
|
|
150
|
+
accumulator.writeIndex = (accumulator.writeIndex + 1) % this.retention.maxSamplesPerMethod;
|
|
151
|
+
} else {
|
|
152
|
+
accumulator.samples.push(durationMs);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run `call`, record how long it took under `method`, and return its result.
|
|
158
|
+
* The duration is recorded even when the call throws/rejects (a failed call
|
|
159
|
+
* still consumed time), then the error propagates.
|
|
160
|
+
*/
|
|
161
|
+
time<T>(method: string, call: () => Promise<T>): Promise<T>;
|
|
162
|
+
time<T>(method: string, call: () => T): T;
|
|
163
|
+
time<T>(method: string, call: () => T | Promise<T>): T | Promise<T> {
|
|
164
|
+
const stopwatch = this.clock.stopwatch();
|
|
165
|
+
const finish = (): void => this.record(method, stopwatch.elapsedMs);
|
|
166
|
+
let result: T | Promise<T>;
|
|
167
|
+
try {
|
|
168
|
+
result = call();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
finish();
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (result instanceof Promise) {
|
|
174
|
+
return result.then(
|
|
175
|
+
value => {
|
|
176
|
+
finish();
|
|
177
|
+
return value;
|
|
178
|
+
},
|
|
179
|
+
error => {
|
|
180
|
+
finish();
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
finish();
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Non-destructive snapshot: per-method latencies ordered by total time descending (use {@link reset} to drain). */
|
|
190
|
+
report(): LatencyReport {
|
|
191
|
+
const methods: MethodLatency[] = [];
|
|
192
|
+
for (const [method, accumulator] of this.durations) {
|
|
193
|
+
const sorted = [...accumulator.samples].sort((left, right) => left - right);
|
|
194
|
+
methods.push({
|
|
195
|
+
method,
|
|
196
|
+
count: accumulator.count,
|
|
197
|
+
totalMs: accumulator.totalMs,
|
|
198
|
+
maxMs: accumulator.maxMs,
|
|
199
|
+
p50Ms: percentile(sorted, 50),
|
|
200
|
+
p99Ms: percentile(sorted, 99)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
methods.sort((left, right) => right.totalMs - left.totalMs);
|
|
204
|
+
return { windowMs: this.window.elapsedMs, methods };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Drop all samples and restart the window. */
|
|
208
|
+
reset(): void {
|
|
209
|
+
this.durations.clear();
|
|
210
|
+
this.window = this.clock.stopwatch();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Render a {@link LatencyReport} as a compact ranked table for a log line or a
|
|
216
|
+
* command channel — the latency counterpart to `formatProfileReport`.
|
|
217
|
+
*/
|
|
218
|
+
export function formatLatencyReport(report: LatencyReport, label = 'RPC/LSP latency'): string {
|
|
219
|
+
const lines = [`${label} (window ${Math.round(report.windowMs)}ms):`];
|
|
220
|
+
if (report.methods.length === 0) {
|
|
221
|
+
lines.push(' (no calls recorded)');
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
for (const methodLatency of report.methods) {
|
|
225
|
+
lines.push(
|
|
226
|
+
` ${methodLatency.method} n=${methodLatency.count} total ${Math.round(methodLatency.totalMs)}ms ` +
|
|
227
|
+
`p50 ${Math.round(methodLatency.p50Ms)}ms p99 ${Math.round(methodLatency.p99Ms)}ms max ${Math.round(methodLatency.maxMs)}ms`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
11
|
+
export type LogThreshold = 'off' | LogLevel;
|
|
12
|
+
|
|
13
|
+
export const LEVEL_ORDER: Record<LogThreshold, number> = { off: 0, error: 1, warn: 2, info: 3, debug: 4, trace: 5 };
|
|
14
|
+
|
|
15
|
+
/** Default labels rendered in the log line, padded to 5 chars so timestamps line up across sources. */
|
|
16
|
+
export const LEVEL_LABELS: Record<LogLevel, string> = {
|
|
17
|
+
error: 'Error',
|
|
18
|
+
warn: 'Warn',
|
|
19
|
+
info: 'Info',
|
|
20
|
+
debug: 'Debug',
|
|
21
|
+
trace: 'Trace'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Module-global threshold; shared by every `AbstractLogger` instance so derived child
|
|
25
|
+
* loggers pick up live updates without each having to subscribe to a configuration source. */
|
|
26
|
+
let currentLevel: LogThreshold = 'info';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cross-side public logger contract. Every consumer of the framework — browser
|
|
30
|
+
* client extensions, LSP servers, headless CLIs, tests — programs against this
|
|
31
|
+
* interface; concrete implementations supply platform-specific delivery
|
|
32
|
+
* (`AbstractLogger` is the abstract base class that satisfies this contract).
|
|
33
|
+
*
|
|
34
|
+
* Global threshold configuration lives on the {@link Logger} namespace
|
|
35
|
+
* (`Logger.setLevel(...)`, `Logger.getLevel()`, `Logger.isLevelEnabled(...)`).
|
|
36
|
+
*
|
|
37
|
+
* Every emit returns this logger so calls can chain (`logger.error(a).debug(b)`),
|
|
38
|
+
* and every `for` / `sub` / `with` derivation preserves the implementation type.
|
|
39
|
+
*/
|
|
40
|
+
export interface Logger {
|
|
41
|
+
/** Log a message at error level. */
|
|
42
|
+
error(message?: string, ...args: unknown[]): this;
|
|
43
|
+
/** Log a message at warn level. */
|
|
44
|
+
warn(message?: string, ...args: unknown[]): this;
|
|
45
|
+
/** Log a message at info level. */
|
|
46
|
+
info(message?: string, ...args: unknown[]): this;
|
|
47
|
+
/** Log a message at debug level. */
|
|
48
|
+
debug(message?: string, ...args: unknown[]): this;
|
|
49
|
+
/** High-volume diagnostic lines, only emitted at the `'trace'` threshold. */
|
|
50
|
+
trace(message?: string, ...args: unknown[]): this;
|
|
51
|
+
/** Log a message at info-level threshold with a `Log` label (rather than `Info`). */
|
|
52
|
+
log(message?: string, ...args: unknown[]): this;
|
|
53
|
+
/** Derive a child logger with `component` as a `Foo :: bar`-style suffix. */
|
|
54
|
+
for(component: string): this;
|
|
55
|
+
/** Derive a child logger by deepening the existing `Foo :: bar :: baz` chain. */
|
|
56
|
+
sub(component: string): this;
|
|
57
|
+
/** Derive a child logger appending `component` as a separate bracket: `[a] [b]`. */
|
|
58
|
+
with(component: string): this;
|
|
59
|
+
/**
|
|
60
|
+
* Derive a child logger labelled with the given URI string. Default behaviour
|
|
61
|
+
* appends the URI as a bracket via {@link with}; subclasses with workspace
|
|
62
|
+
* awareness override to display workspace-relative paths. Subclasses MAY
|
|
63
|
+
* widen the parameter type to accept URI objects directly (TypeScript method
|
|
64
|
+
* parameter bivariance permits this) — callers typed against the {@link Logger}
|
|
65
|
+
* interface stringify their URI at the call site.
|
|
66
|
+
*/
|
|
67
|
+
withUri(uri: string): this;
|
|
68
|
+
/**
|
|
69
|
+
* Emit `message` at `threshold`, or do nothing when `threshold === 'off'`.
|
|
70
|
+
* Lets a caller holding a {@link LogThreshold} emit without a hand-written
|
|
71
|
+
* `if (level === 'off') return` guard.
|
|
72
|
+
*/
|
|
73
|
+
logAt(threshold: LogThreshold, message: string): this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Companion namespace for the {@link Logger} interface holding global
|
|
78
|
+
* threshold configuration.
|
|
79
|
+
*
|
|
80
|
+
* The threshold is process-wide: every {@link Logger} instance shares one
|
|
81
|
+
* value, so adopters don't have to wire it through each logger they
|
|
82
|
+
* construct. The timing facilities that build on a logger — `Tracer.time` /
|
|
83
|
+
* `Tracer.startTimer` — consult {@link Logger.isLevelEnabled} internally to
|
|
84
|
+
* short-circuit when the configured level suppresses output.
|
|
85
|
+
*/
|
|
86
|
+
export namespace Logger {
|
|
87
|
+
/** Set the process-wide log threshold. Every {@link Logger} instance picks up the new value live. */
|
|
88
|
+
export function setLevel(level: LogThreshold): void {
|
|
89
|
+
currentLevel = level;
|
|
90
|
+
}
|
|
91
|
+
/** Read the current process-wide log threshold. */
|
|
92
|
+
export function getLevel(): LogThreshold {
|
|
93
|
+
return currentLevel;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether a message at `level` would be emitted at the current threshold.
|
|
97
|
+
* Use to guard expensive log-line construction:
|
|
98
|
+
* `if (Logger.isLevelEnabled('trace')) logger.trace(buildPayload())`.
|
|
99
|
+
*/
|
|
100
|
+
export function isLevelEnabled(level: LogLevel): boolean {
|
|
101
|
+
return LEVEL_ORDER[level] <= LEVEL_ORDER[currentLevel];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a message at `threshold` would be emitted: `false` for `'off'`,
|
|
105
|
+
* otherwise tracks {@link isLevelEnabled}. Use to gate work behind a
|
|
106
|
+
* {@link LogThreshold} (which {@link isLevelEnabled} cannot accept).
|
|
107
|
+
*/
|
|
108
|
+
export function isThresholdEnabled(threshold: LogThreshold): boolean {
|
|
109
|
+
return threshold !== 'off' && isLevelEnabled(threshold);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** True when `value` is one of `'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'`. */
|
|
114
|
+
export function isLogThreshold(value: unknown): value is LogThreshold {
|
|
115
|
+
return typeof value === 'string' && value in LEVEL_ORDER;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse a string-typed configuration value into a {@link LogThreshold}, returning
|
|
120
|
+
* `undefined` for unset or invalid inputs. Case-insensitive — `'WARN'` / `'warn'`
|
|
121
|
+
* / `'Warn'` all normalise to `'warn'`. Use to consume LSP / env-var / CLI inputs
|
|
122
|
+
* without exposing callers to the case-fold detail.
|
|
123
|
+
*/
|
|
124
|
+
export function parseLogLevel(value: unknown): LogThreshold | undefined {
|
|
125
|
+
const normalised = typeof value === 'string' ? value.toLowerCase() : undefined;
|
|
126
|
+
return normalised && isLogThreshold(normalised) ? normalised : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Names of the environment variables the server consults for its launch-time
|
|
131
|
+
* log configuration. They live in the shared protocol layer because they are
|
|
132
|
+
* the *contract* between whoever launches a server (the CLI, a test harness, a
|
|
133
|
+
* container) and the server that reads them — both sides reference one constant
|
|
134
|
+
* instead of duplicating the literal string.
|
|
135
|
+
*/
|
|
136
|
+
export const DEFAULT_LOG_LEVEL_ENV = 'HYDRANIUM_LOG_LEVEL';
|
|
137
|
+
/** Env var the server reads its log file-tee target from. See {@link DEFAULT_LOG_LEVEL_ENV}. */
|
|
138
|
+
export const DEFAULT_LOG_FILE_ENV = 'HYDRANIUM_LOG_FILE';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Human-readable formatting helpers used in log lines and diagnostic output.
|
|
142
|
+
*/
|
|
143
|
+
export namespace Format {
|
|
144
|
+
/** Format a date as `HH:MM:SS.mmm`. Defaults to `now`. */
|
|
145
|
+
export function timestamp(d: Date = new Date()): string {
|
|
146
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
147
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
148
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
149
|
+
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
150
|
+
return `${hh}:${mm}:${ss}.${ms}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Format a millisecond duration: `<1s` as `Nms`, `>=1s` as `N.NNs`. */
|
|
154
|
+
export function elapsed(ms: number): string {
|
|
155
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Format a byte count with a human-readable unit suffix (B/KB/MB/GB/TB). */
|
|
159
|
+
export function bytes(count: number): string {
|
|
160
|
+
if (count < 1024) {
|
|
161
|
+
return `${count}B`;
|
|
162
|
+
}
|
|
163
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
164
|
+
let value = count / 1024;
|
|
165
|
+
let unit = 0;
|
|
166
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
167
|
+
value /= 1024;
|
|
168
|
+
unit++;
|
|
169
|
+
}
|
|
170
|
+
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)}${units[unit]}`;
|
|
171
|
+
}
|
|
172
|
+
}
|