@hydranium/protocol 1.0.0-next.36 → 1.0.0-next.361
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/README.md +44 -83
- package/lib/abstract-logger.d.ts +11 -0
- package/lib/abstract-logger.d.ts.map +1 -1
- package/lib/abstract-logger.js +16 -0
- package/lib/abstract-logger.js.map +1 -1
- package/lib/client/data-connection.d.ts +303 -0
- package/lib/client/data-connection.d.ts.map +1 -0
- package/lib/client/data-connection.js +534 -0
- package/lib/client/data-connection.js.map +1 -0
- package/lib/client/data-events.d.ts +18 -1
- package/lib/client/data-events.d.ts.map +1 -1
- package/lib/client/data-events.js +26 -0
- package/lib/client/data-events.js.map +1 -1
- package/lib/client/data-port.d.ts +34 -24
- package/lib/client/data-port.d.ts.map +1 -1
- package/lib/client/data-session.d.ts +691 -89
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +1155 -115
- package/lib/client/data-session.js.map +1 -1
- package/lib/client/index.d.ts +15 -9
- package/lib/client/index.d.ts.map +1 -1
- package/lib/client/index.js +15 -9
- package/lib/client/index.js.map +1 -1
- package/lib/client/message-relay.d.ts +6 -4
- package/lib/client/message-relay.d.ts.map +1 -1
- package/lib/client/message-relay.js +4 -4
- package/lib/client/message-relay.js.map +1 -1
- package/lib/client/post-message-transport.d.ts +69 -3
- package/lib/client/post-message-transport.d.ts.map +1 -1
- package/lib/client/post-message-transport.js +175 -1
- package/lib/client/post-message-transport.js.map +1 -1
- package/lib/client/rpc-connection.d.ts +173 -0
- package/lib/client/rpc-connection.d.ts.map +1 -0
- package/lib/client/rpc-connection.js +225 -0
- package/lib/client/rpc-connection.js.map +1 -0
- package/lib/client/worker-handshake.d.ts +94 -0
- package/lib/client/worker-handshake.d.ts.map +1 -0
- package/lib/client/worker-handshake.js +51 -0
- package/lib/client/worker-handshake.js.map +1 -0
- package/lib/client-ids.d.ts +45 -0
- package/lib/client-ids.d.ts.map +1 -0
- package/lib/client-ids.js +48 -0
- package/lib/client-ids.js.map +1 -0
- package/lib/clock.d.ts +50 -10
- package/lib/clock.d.ts.map +1 -1
- package/lib/clock.js +36 -1
- package/lib/clock.js.map +1 -1
- package/lib/console-logger.d.ts +23 -0
- package/lib/console-logger.d.ts.map +1 -0
- package/lib/console-logger.js +39 -0
- package/lib/console-logger.js.map +1 -0
- package/lib/data/data-protocol-methods.d.ts +4 -4
- package/lib/data/data-protocol-methods.d.ts.map +1 -1
- package/lib/data/data-protocol-methods.js +13 -1
- package/lib/data/data-protocol-methods.js.map +1 -1
- package/lib/data/data-server-protocol.d.ts +171 -48
- package/lib/data/data-server-protocol.d.ts.map +1 -1
- package/lib/data/events.d.ts +144 -33
- package/lib/data/events.d.ts.map +1 -1
- package/lib/data/requests.d.ts +76 -11
- package/lib/data/requests.d.ts.map +1 -1
- package/lib/debouncer.d.ts +7 -4
- package/lib/debouncer.d.ts.map +1 -1
- package/lib/debouncer.js +10 -10
- package/lib/debouncer.js.map +1 -1
- package/lib/errors.d.ts +188 -30
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +269 -32
- package/lib/errors.js.map +1 -1
- package/lib/format-log-arg.d.ts +33 -0
- package/lib/format-log-arg.d.ts.map +1 -0
- package/lib/format-log-arg.js +158 -0
- package/lib/format-log-arg.js.map +1 -0
- package/lib/glsp-edits-settled-actions.d.ts +54 -0
- package/lib/glsp-edits-settled-actions.d.ts.map +1 -0
- package/lib/glsp-edits-settled-actions.js +28 -0
- package/lib/glsp-edits-settled-actions.js.map +1 -0
- package/lib/glsp-request-model-args.d.ts +16 -0
- package/lib/glsp-request-model-args.d.ts.map +1 -0
- package/lib/glsp-request-model-args.js +19 -0
- package/lib/glsp-request-model-args.js.map +1 -0
- package/lib/glsp-save-model-actions.d.ts +55 -0
- package/lib/glsp-save-model-actions.d.ts.map +1 -0
- package/lib/glsp-save-model-actions.js +28 -0
- package/lib/glsp-save-model-actions.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/latency-collector.d.ts +10 -4
- package/lib/latency-collector.d.ts.map +1 -1
- package/lib/latency-collector.js +2 -0
- package/lib/latency-collector.js.map +1 -1
- package/lib/logger.d.ts +38 -7
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js +42 -4
- package/lib/logger.js.map +1 -1
- package/lib/messages/index.d.ts +4 -2
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +15 -4
- package/lib/messages/index.js.map +1 -1
- package/lib/messages/primitives.d.ts +59 -4
- package/lib/messages/primitives.d.ts.map +1 -1
- package/lib/messages/primitives.js +97 -4
- package/lib/messages/primitives.js.map +1 -1
- package/lib/model-server.d.ts +60 -13
- package/lib/model-server.d.ts.map +1 -1
- package/lib/model-server.js +4 -2
- package/lib/model-server.js.map +1 -1
- package/lib/model-service/base-version.d.ts +67 -0
- package/lib/model-service/base-version.d.ts.map +1 -0
- package/lib/model-service/base-version.js +44 -0
- package/lib/model-service/base-version.js.map +1 -0
- package/lib/model-service/index.d.ts +1 -1
- package/lib/model-service/index.d.ts.map +1 -1
- package/lib/model-service/index.js +4 -5
- package/lib/model-service/index.js.map +1 -1
- package/lib/model-service/reference-candidate.d.ts +5 -3
- package/lib/model-service/reference-candidate.d.ts.map +1 -1
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +29 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/process-memory.d.ts +66 -0
- package/lib/node/process-memory.d.ts.map +1 -0
- package/lib/node/process-memory.js +291 -0
- package/lib/node/process-memory.js.map +1 -0
- package/lib/noop-logger.d.ts.map +1 -1
- package/lib/noop-logger.js.map +1 -1
- package/lib/observable-value.js.map +1 -1
- package/lib/patch-merge.d.ts +63 -35
- package/lib/patch-merge.d.ts.map +1 -1
- package/lib/patch-merge.js +171 -25
- package/lib/patch-merge.js.map +1 -1
- package/lib/pending-work-tracker.d.ts +30 -0
- package/lib/pending-work-tracker.d.ts.map +1 -0
- package/lib/pending-work-tracker.js +47 -0
- package/lib/pending-work-tracker.js.map +1 -0
- package/lib/profile-session.d.ts +24 -8
- package/lib/profile-session.d.ts.map +1 -1
- package/lib/profile-session.js +13 -4
- package/lib/profile-session.js.map +1 -1
- package/lib/profiling.d.ts +15 -2
- package/lib/profiling.d.ts.map +1 -1
- package/lib/profiling.js.map +1 -1
- package/lib/project.d.ts +4 -1
- package/lib/project.d.ts.map +1 -1
- package/lib/project.js.map +1 -1
- package/lib/random-uuid.d.ts +14 -0
- package/lib/random-uuid.d.ts.map +1 -0
- package/lib/random-uuid.js +24 -0
- package/lib/random-uuid.js.map +1 -0
- package/lib/reconcile-write.d.ts +70 -0
- package/lib/reconcile-write.d.ts.map +1 -0
- package/lib/reconcile-write.js +69 -0
- package/lib/reconcile-write.js.map +1 -0
- package/lib/rpc/bind-rpc-methods.d.ts +6 -2
- package/lib/rpc/bind-rpc-methods.d.ts.map +1 -1
- package/lib/rpc/bind-rpc-methods.js +17 -7
- package/lib/rpc/bind-rpc-methods.js.map +1 -1
- package/lib/rpc/create-rpc-proxy.d.ts +3 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -1
- package/lib/rpc/create-rpc-proxy.js +6 -1
- package/lib/rpc/create-rpc-proxy.js.map +1 -1
- package/lib/rpc/index.d.ts +1 -0
- package/lib/rpc/index.d.ts.map +1 -1
- package/lib/rpc/index.js +1 -0
- package/lib/rpc/index.js.map +1 -1
- package/lib/rpc/send-by-method-name.d.ts +76 -0
- package/lib/rpc/send-by-method-name.d.ts.map +1 -0
- package/lib/rpc/send-by-method-name.js +120 -0
- package/lib/rpc/send-by-method-name.js.map +1 -0
- package/lib/rpc/wire-prefix.js.map +1 -1
- package/lib/save-text-document.d.ts +40 -0
- package/lib/save-text-document.d.ts.map +1 -0
- package/lib/save-text-document.js +34 -0
- package/lib/save-text-document.js.map +1 -0
- package/lib/testing/catalogue-audit.d.ts +1 -1
- package/lib/testing/catalogue-audit.js +1 -1
- package/lib/testing/catalogue-audit.js.map +1 -1
- package/lib/testing/data-doubles.d.ts +41 -12
- package/lib/testing/data-doubles.d.ts.map +1 -1
- package/lib/testing/data-doubles.js +56 -8
- package/lib/testing/data-doubles.js.map +1 -1
- package/lib/testing/fake-clock.d.ts +17 -2
- package/lib/testing/fake-clock.d.ts.map +1 -1
- package/lib/testing/fake-clock.js +61 -46
- package/lib/testing/fake-clock.js.map +1 -1
- package/lib/testing/index.d.ts +1 -0
- package/lib/testing/index.d.ts.map +1 -1
- package/lib/testing/index.js +4 -3
- package/lib/testing/index.js.map +1 -1
- package/lib/testing/node/duplex-connection.d.ts.map +1 -1
- package/lib/testing/node/duplex-connection.js +3 -2
- package/lib/testing/node/duplex-connection.js.map +1 -1
- package/lib/testing/node/duplex-stream.js.map +1 -1
- package/lib/testing/node/index.d.ts +1 -0
- package/lib/testing/node/index.d.ts.map +1 -1
- package/lib/testing/node/index.js +2 -2
- package/lib/testing/node/index.js.map +1 -1
- package/lib/testing/node/message-port-pair.d.ts +25 -0
- package/lib/testing/node/message-port-pair.d.ts.map +1 -0
- package/lib/testing/node/message-port-pair.js +26 -0
- package/lib/testing/node/message-port-pair.js.map +1 -0
- package/lib/testing/transfer-node-builder.d.ts +81 -0
- package/lib/testing/transfer-node-builder.d.ts.map +1 -0
- package/lib/testing/transfer-node-builder.js +47 -0
- package/lib/testing/transfer-node-builder.js.map +1 -0
- package/lib/testing/wait-for.d.ts +9 -3
- package/lib/testing/wait-for.d.ts.map +1 -1
- package/lib/testing/wait-for.js +57 -11
- package/lib/testing/wait-for.js.map +1 -1
- package/lib/tracer.d.ts.map +1 -1
- package/lib/tracer.js +5 -9
- package/lib/tracer.js.map +1 -1
- package/lib/transfer-diagnostic.js.map +1 -1
- package/lib/transfer-document.d.ts +96 -32
- package/lib/transfer-document.d.ts.map +1 -1
- package/lib/transfer-document.js +17 -9
- package/lib/transfer-document.js.map +1 -1
- package/lib/transfer-element.d.ts +4 -4
- package/lib/types.d.ts +32 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/{model-service/args.js → types.js} +2 -2
- package/lib/types.js.map +1 -0
- package/lib/uri.d.ts.map +1 -1
- package/lib/uri.js.map +1 -1
- package/lib/util.d.ts +8 -2
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +32 -4
- package/lib/util.js.map +1 -1
- package/package.json +24 -40
- package/src/abstract-logger.ts +18 -0
- package/src/client/data-connection.ts +654 -0
- package/src/client/data-events.ts +38 -1
- package/src/client/data-port.ts +36 -25
- package/src/client/data-session.ts +1476 -137
- package/src/client/index.ts +15 -9
- package/src/client/message-relay.ts +6 -4
- package/src/client/post-message-transport.ts +224 -4
- package/src/client/rpc-connection.ts +302 -0
- package/src/client/worker-handshake.ts +121 -0
- package/src/client-ids.ts +49 -0
- package/src/clock.ts +68 -10
- package/src/console-logger.ts +39 -0
- package/src/data/data-protocol-methods.ts +14 -4
- package/src/data/data-server-protocol.ts +198 -48
- package/src/data/events.ts +150 -33
- package/src/data/requests.ts +82 -11
- package/src/debouncer.ts +11 -11
- package/src/errors.ts +331 -40
- package/src/format-log-arg.ts +176 -0
- package/src/glsp-edits-settled-actions.ts +62 -0
- package/src/glsp-request-model-args.ts +16 -0
- package/src/glsp-save-model-actions.ts +64 -0
- package/src/index.ts +11 -0
- package/src/latency-collector.ts +10 -3
- package/src/logger.ts +44 -8
- package/src/messages/index.ts +4 -2
- package/src/messages/primitives.ts +129 -5
- package/src/model-server.ts +64 -19
- package/src/model-service/base-version.ts +75 -0
- package/src/model-service/index.ts +4 -5
- package/src/model-service/reference-candidate.ts +5 -3
- package/src/node/index.ts +14 -0
- package/src/node/process-memory.ts +299 -0
- package/src/patch-merge.ts +230 -45
- package/src/pending-work-tracker.ts +47 -0
- package/src/profile-session.ts +31 -11
- package/src/profiling.ts +15 -2
- package/src/project.ts +4 -1
- package/src/random-uuid.ts +21 -0
- package/src/reconcile-write.ts +131 -0
- package/src/rpc/README.md +6 -6
- package/src/rpc/bind-rpc-methods.ts +18 -8
- package/src/rpc/create-rpc-proxy.ts +6 -1
- package/src/rpc/index.ts +1 -0
- package/src/rpc/send-by-method-name.ts +140 -0
- package/src/save-text-document.ts +39 -0
- package/src/testing/catalogue-audit.ts +1 -1
- package/src/testing/data-doubles.ts +141 -19
- package/src/testing/fake-clock.ts +76 -48
- package/src/testing/index.ts +4 -3
- package/src/testing/node/duplex-connection.ts +3 -2
- package/src/testing/node/index.ts +2 -2
- package/src/testing/node/message-port-pair.ts +40 -0
- package/src/testing/transfer-node-builder.ts +107 -0
- package/src/testing/wait-for.ts +61 -13
- package/src/tracer.ts +5 -9
- package/src/transfer-document.ts +116 -34
- package/src/transfer-element.ts +4 -4
- package/src/types.ts +30 -0
- package/src/util.ts +33 -6
- package/lib/model-service/args.d.ts +0 -64
- package/lib/model-service/args.d.ts.map +0 -1
- package/lib/model-service/args.js.map +0 -1
- package/src/model-service/args.ts +0 -67
package/src/model-server.ts
CHANGED
|
@@ -14,17 +14,19 @@
|
|
|
14
14
|
* over `TRoot`.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { TransferClientArgs } from './model-service/args';
|
|
18
17
|
import type { ReferenceCandidate } from './model-service/reference-candidate';
|
|
19
18
|
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Client / server arguments
|
|
22
21
|
// ---------------------------------------------------------------------------
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
/** Identifies a client-document binding. */
|
|
24
|
+
export interface TransferClientArgs {
|
|
25
|
+
/** Document URI. */
|
|
26
|
+
uri: string;
|
|
27
|
+
/** Stable identifier for the client invoking the operation. */
|
|
28
|
+
clientId: string;
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
/** Open a document on behalf of a client. */
|
|
30
32
|
export interface OpenModelArgs extends TransferClientArgs {
|
|
@@ -47,14 +49,22 @@ export interface OpenModelArgs extends TransferClientArgs {
|
|
|
47
49
|
* means read from the filesystem, which is the normal case.
|
|
48
50
|
*
|
|
49
51
|
* Honoured only on the first open of a URI: opening an already-open document
|
|
50
|
-
*
|
|
51
|
-
* through the update path instead.
|
|
52
|
+
* attaches the caller to the existing shared entry and does not replace its
|
|
53
|
+
* content with this seed. Write through the update path instead. The open
|
|
54
|
+
* response reads the current shared content after registration; a textual
|
|
55
|
+
* `didOpen` attach may separately refresh the build.
|
|
52
56
|
*/
|
|
53
57
|
text?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Kept on the server for this client's open until it closes, for the
|
|
60
|
+
* server's own open path to read. Only a client session's open keeps them,
|
|
61
|
+
* and a repeat open keeps the first open's.
|
|
62
|
+
*/
|
|
63
|
+
options?: object;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/** Close a previously-opened document for the client. */
|
|
57
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- close names its own args; it adds no field to the base
|
|
58
68
|
export interface CloseModelArgs extends TransferClientArgs {}
|
|
59
69
|
|
|
60
70
|
// ---------------------------------------------------------------------------
|
|
@@ -63,9 +73,25 @@ export interface CloseModelArgs extends TransferClientArgs {}
|
|
|
63
73
|
|
|
64
74
|
export interface TransferUpdatedEvent<TDocument> {
|
|
65
75
|
document: TDocument;
|
|
76
|
+
/**
|
|
77
|
+
* The client whose write this event echoes: the author of the version on a
|
|
78
|
+
* `'changed'`, and the unknown-client id on a `'rebuilt'`, which echoes no
|
|
79
|
+
* write. A recipient compares it against its own id to recognise its echo.
|
|
80
|
+
*/
|
|
66
81
|
sourceClientId: string;
|
|
67
|
-
/** See `
|
|
68
|
-
reason: 'changed' | '
|
|
82
|
+
/** See `TransferDocumentUpdateReason` in `./data/events` for the canonical reason set + semantics. */
|
|
83
|
+
reason: 'changed' | 'rebuilt' | 'saved';
|
|
84
|
+
/**
|
|
85
|
+
* The client whose write caused the build that produced this event, or the
|
|
86
|
+
* unknown-client id when no single client's did. On a `'changed'` it is the
|
|
87
|
+
* version's author. A `'rebuilt'` names the writer of another document, so
|
|
88
|
+
* it is a cause, never an echo: its diagnostics and references are new to
|
|
89
|
+
* that writer too.
|
|
90
|
+
*
|
|
91
|
+
* In-process only: the data head does not send it, and an event built
|
|
92
|
+
* elsewhere may leave it out, which a reader takes as "not known".
|
|
93
|
+
*/
|
|
94
|
+
causedBy?: string;
|
|
69
95
|
}
|
|
70
96
|
|
|
71
97
|
export interface TransferSavedEvent<TDocument> {
|
|
@@ -147,10 +173,10 @@ export function isElementSource(object: unknown): object is ElementSource {
|
|
|
147
173
|
/** An element of a document that does not yet exist on disk — used during element creation flows. */
|
|
148
174
|
export interface SyntheticSource {
|
|
149
175
|
/**
|
|
150
|
-
* The document the element would belong to.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
176
|
+
* The document the element would belong to. It need not be loaded, or
|
|
177
|
+
* exist — an unloaded URI materialises a transient empty document, so a
|
|
178
|
+
* folder is a valid anchor. A URI that IS loaded contributes that
|
|
179
|
+
* document's parse root as the container, putting its contents in scope.
|
|
154
180
|
*/
|
|
155
181
|
uri: string;
|
|
156
182
|
/**
|
|
@@ -159,6 +185,16 @@ export interface SyntheticSource {
|
|
|
159
185
|
* it is what scoping filters candidates against.
|
|
160
186
|
*/
|
|
161
187
|
type: string;
|
|
188
|
+
/**
|
|
189
|
+
* The grammar the element will belong to, as a language id.
|
|
190
|
+
*
|
|
191
|
+
* Optional because `uri` and `type` each answer on their own when
|
|
192
|
+
* unambiguous. Set it when neither is: a URI naming no file carries no
|
|
193
|
+
* extension to route on, and a `type` two grammars can produce identifies
|
|
194
|
+
* neither. Unset, such a source reaches the server's own policy, which
|
|
195
|
+
* answers the same way for every caller.
|
|
196
|
+
*/
|
|
197
|
+
language?: string;
|
|
162
198
|
}
|
|
163
199
|
|
|
164
200
|
export function isSyntheticSource(object: unknown): object is SyntheticSource {
|
|
@@ -183,8 +219,10 @@ export namespace ReferenceSource {
|
|
|
183
219
|
export function element(name: string, type?: string): ElementSource {
|
|
184
220
|
return { name, type };
|
|
185
221
|
}
|
|
186
|
-
export function synthetic(uri: string, type: string): SyntheticSource {
|
|
187
|
-
|
|
222
|
+
export function synthetic(uri: string, type: string, language?: string): SyntheticSource {
|
|
223
|
+
// The key is omitted rather than set to `undefined`, so a source built
|
|
224
|
+
// without a language stays deep-equal to the two-argument form.
|
|
225
|
+
return language === undefined ? { uri, type } : { uri, type, language };
|
|
188
226
|
}
|
|
189
227
|
}
|
|
190
228
|
|
|
@@ -457,9 +495,10 @@ export interface FindNextNameArgs {
|
|
|
457
495
|
*/
|
|
458
496
|
uri: string;
|
|
459
497
|
/**
|
|
460
|
-
* The AST `$type` of the element being named. Collisions are
|
|
461
|
-
* among elements of that
|
|
462
|
-
*
|
|
498
|
+
* The AST `$type` of the element being named. Collisions are looked for
|
|
499
|
+
* among elements of that type and of any subtype of it, so a supertype
|
|
500
|
+
* names a uniqueness scope its concrete types share; the returned name may
|
|
501
|
+
* still be taken by an element outside that hierarchy.
|
|
463
502
|
*/
|
|
464
503
|
type: string;
|
|
465
504
|
/**
|
|
@@ -476,4 +515,10 @@ export interface FindNextNameArgs {
|
|
|
476
515
|
* See {@link NameTier} for what each tier covers.
|
|
477
516
|
*/
|
|
478
517
|
tier?: NameTier;
|
|
518
|
+
/**
|
|
519
|
+
* The grammar to name under, routed as {@link SyntheticSource.language} —
|
|
520
|
+
* this call builds one. A caller that sets it there must set it here, or
|
|
521
|
+
* candidates and the name it proposes come from different grammars.
|
|
522
|
+
*/
|
|
523
|
+
language?: string;
|
|
479
524
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
declare const snapshotMarker: unique symbol;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The version of the text a model was parsed from: a snapshot of the store's
|
|
14
|
+
* {@link TextVersion}, not a counter of its own. A document envelope carries
|
|
15
|
+
* it, and it cannot move once the envelope exists. A create's answer carries
|
|
16
|
+
* a second snapshot of the same kind, the version the create's text took,
|
|
17
|
+
* which its creator holds because it sent the text.
|
|
18
|
+
*
|
|
19
|
+
* A plain number at runtime, usable anywhere a number is, and it travels the
|
|
20
|
+
* wire as one. The marker exists only so the compiler can tell it apart from a
|
|
21
|
+
* version read off a LIVE document, which is the same number type and is the
|
|
22
|
+
* defect the conflict gate exists to prevent: read at write time it is whatever
|
|
23
|
+
* the server is at now, which is the number the gate is about to compare it
|
|
24
|
+
* against, so the gate passes unconditionally and a concurrent edit is
|
|
25
|
+
* overwritten with nothing logged.
|
|
26
|
+
*/
|
|
27
|
+
export type ModelVersion = number & { readonly [snapshotMarker]: true };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The store's version of a document's text, which moves with every change of it.
|
|
31
|
+
* Unbranded, so a write refuses it as `baseVersion`: it can name text newer than
|
|
32
|
+
* the caller's model, and a write based on it would pass the gate with older content.
|
|
33
|
+
*/
|
|
34
|
+
export type TextVersion = number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The version a write was authored against, from a snapshot read, or `'any'`: a
|
|
38
|
+
* precondition that always holds, so the write overwrites whatever the server has.
|
|
39
|
+
* `'any'` is a lie for a write authored against a document a read handed over.
|
|
40
|
+
*/
|
|
41
|
+
export type BaseVersion = ModelVersion | 'any';
|
|
42
|
+
|
|
43
|
+
/** Whether `baseVersion` names a version, and so arms the gate. */
|
|
44
|
+
export function isModelVersion(baseVersion: BaseVersion): baseVersion is ModelVersion {
|
|
45
|
+
return typeof baseVersion === 'number';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mark a version as having come from a snapshot read.
|
|
50
|
+
*
|
|
51
|
+
* **For the envelope constructors, not for callers.** Every door that builds a
|
|
52
|
+
* document envelope applies it on the way in, the create's answer included
|
|
53
|
+
* for the version its text took, so anything a read returns already carries
|
|
54
|
+
* it and a writer never needs this. A caller reaching for it is
|
|
55
|
+
* asserting a provenance the compiler was about to deny — the read-late defect
|
|
56
|
+
* written out where a reviewer can see it.
|
|
57
|
+
*/
|
|
58
|
+
export function asModelVersion(version: number): ModelVersion {
|
|
59
|
+
return version as ModelVersion;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The model version of a root no document factory path built, so nothing
|
|
64
|
+
* records the text it came from. No write matches it, so a write based on it
|
|
65
|
+
* conflicts.
|
|
66
|
+
*/
|
|
67
|
+
export const UNRECORDED_VERSION: ModelVersion = asModelVersion(-2);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The model version of a root parsed from text the store holds under no
|
|
71
|
+
* version: the file, read while the store held the document open with other
|
|
72
|
+
* text. Below every text version, so the model reads as behind and a write
|
|
73
|
+
* based on it conflicts.
|
|
74
|
+
*/
|
|
75
|
+
export const STALE_VERSION: ModelVersion = asModelVersion(-1);
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// protocol can structurally agree on the lifecycle shape.
|
|
10
|
+
// Generic payload types the data protocol shares with core: the base
|
|
11
|
+
// version, which the server's model service checks writes against, and the
|
|
12
|
+
// reference candidates, which core's scope and completion code produce.
|
|
14
13
|
|
|
15
|
-
export * from './
|
|
14
|
+
export * from './base-version';
|
|
16
15
|
export * from './reference-candidate';
|
|
@@ -50,9 +50,11 @@ export interface ReferenceCandidate {
|
|
|
50
50
|
* node's subtree, NOT the whole document root — callers that want the full
|
|
51
51
|
* document fetch it via `getModelDocument(uri)`.
|
|
52
52
|
*
|
|
53
|
-
*
|
|
53
|
+
* `TElement` is the type of the target node, not of a document root. A server
|
|
54
|
+
* answers with the structural base, since a reference can name any node; a
|
|
55
|
+
* caller that knows what the reference targets may narrow it.
|
|
54
56
|
*/
|
|
55
|
-
export interface ReferenceTarget<
|
|
57
|
+
export interface ReferenceTarget<TElement extends TransferElement = TransferElement> extends ReferenceCandidate {
|
|
56
58
|
/** The resolved target node, encoded as a transfer subtree. */
|
|
57
|
-
element:
|
|
59
|
+
element: TElement;
|
|
58
60
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
// Subpath barrel for `@hydranium/protocol/node` — the diagnostics helpers that
|
|
11
|
+
// need a Node runtime, kept out of the root barrel so the root stays
|
|
12
|
+
// bundleable for a browser.
|
|
13
|
+
|
|
14
|
+
export * from './process-memory';
|
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
* Node-only process- and pod-level memory introspection plus heap-snapshot
|
|
12
|
+
* writing. Everything here is generic OS-level code (V8 stats, cgroup v2/v1,
|
|
13
|
+
* `/proc`); the only host-specific part is naming the processes, which a head
|
|
14
|
+
* supplies through a `ProcessClassifier`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as os from 'node:os';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import * as v8 from 'node:v8';
|
|
21
|
+
import { Format } from '../logger';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format a one-shot memory snapshot of the CURRENT process (heap, rss, external, V8 limit).
|
|
25
|
+
* Suited to a process that hosts no Langium documents (e.g. a host backend); for the
|
|
26
|
+
* language-server process with document counts, see `formatServerState`.
|
|
27
|
+
*/
|
|
28
|
+
export function formatProcessMemory(label: string): string {
|
|
29
|
+
const mem = process.memoryUsage();
|
|
30
|
+
const heap = v8.getHeapStatistics();
|
|
31
|
+
const heapPercent = Math.round((mem.heapUsed / heap.heap_size_limit) * 100);
|
|
32
|
+
return [
|
|
33
|
+
`${label}:`,
|
|
34
|
+
` heap ${Format.bytes(mem.heapUsed)} used / ${Format.bytes(mem.heapTotal)} total / ` +
|
|
35
|
+
`${Format.bytes(heap.heap_size_limit)} limit (${heapPercent}% of limit)`,
|
|
36
|
+
` rss ${Format.bytes(mem.rss)}, external ${Format.bytes(mem.external)}, arrayBuffers ${Format.bytes(mem.arrayBuffers)}`,
|
|
37
|
+
` uptime ${Math.round(process.uptime())}s, pid ${process.pid}`
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compose an absolute artifact file path of the form `<prefix>[-<label>].<ext>` inside
|
|
43
|
+
* {@link directory} (e.g. the workspace folder, so it lands on the persistent volume in cloud and
|
|
44
|
+
* is visible in the file explorer), falling back to the OS temp dir when the directory is missing
|
|
45
|
+
* or does not exist. The {@link prefix} carries the origin-first stem (e.g. `server-cpu`) and, when
|
|
46
|
+
* a non-empty {@link label} is given (a named capture window), it is sanitised (non-word characters
|
|
47
|
+
* → `_`, truncated to 40 chars) and appended as `-<label>`; an empty label yields the bare
|
|
48
|
+
* `<prefix>.<ext>` so the manifest `kind` equals the filename stem. No timestamp — a fresh session
|
|
49
|
+
* directory per run keeps names stable and collision-free. Shared by the heap-snapshot writer and
|
|
50
|
+
* the sampled-profile capture so heap / CPU / allocation artefacts land identically, differing only
|
|
51
|
+
* by extension.
|
|
52
|
+
*/
|
|
53
|
+
export function snapshotFilePath(directory: string | undefined, label: string, prefix: string, ext: string): string {
|
|
54
|
+
const safeLabel = label ? label.replace(/[^\w.-]+/g, '_').slice(0, 40) : '';
|
|
55
|
+
const dir = directory && fs.existsSync(directory) ? directory : os.tmpdir();
|
|
56
|
+
const stem = safeLabel ? `${prefix}-${safeLabel}` : prefix;
|
|
57
|
+
return path.join(dir, `${stem}.${ext}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write a V8 heap snapshot of the CURRENT process to {@link directory}, falling back to the OS temp
|
|
62
|
+
* dir when it is missing (see {@link snapshotFilePath}). `v8.writeHeapSnapshot` runs a full GC first and
|
|
63
|
+
* briefly pauses the process; it also transiently inflates RSS while serialising. Returns the
|
|
64
|
+
* absolute file path. The {@link prefix} (default `server-heap`, the origin-first stem) and
|
|
65
|
+
* {@link label} are folded into the name.
|
|
66
|
+
*/
|
|
67
|
+
export function writeHeapSnapshotToDir(directory: string | undefined, label: string, prefix = 'server-heap'): string {
|
|
68
|
+
const filePath = snapshotFilePath(directory, label, prefix, 'heapsnapshot');
|
|
69
|
+
v8.writeHeapSnapshot(filePath);
|
|
70
|
+
return filePath;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const CGROUP_ROOT = '/sys/fs/cgroup';
|
|
74
|
+
|
|
75
|
+
function readNumber(file: string): number | undefined {
|
|
76
|
+
try {
|
|
77
|
+
const text = fs.readFileSync(file, 'utf8').trim();
|
|
78
|
+
if (text === 'max') {
|
|
79
|
+
return Number.POSITIVE_INFINITY;
|
|
80
|
+
}
|
|
81
|
+
const value = Number(text);
|
|
82
|
+
return Number.isFinite(value) ? value : undefined;
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Parse a `key value` per-line file (cgroup memory.stat) into a map. */
|
|
89
|
+
function readKeyedBytes(file: string): Record<string, number> {
|
|
90
|
+
const out: Record<string, number> = {};
|
|
91
|
+
try {
|
|
92
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
93
|
+
const [key, value] = line.trim().split(/\s+/);
|
|
94
|
+
if (key && value !== undefined) {
|
|
95
|
+
const num = Number(value);
|
|
96
|
+
if (Number.isFinite(num)) {
|
|
97
|
+
out[key] = num;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// file absent or unreadable; caller handles the empty map.
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface ProcInfo {
|
|
108
|
+
pid: number;
|
|
109
|
+
rssBytes: number;
|
|
110
|
+
label: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface RawProc extends ProcInfo {
|
|
114
|
+
ppid: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A readable role for a process, whether it belongs to the app, and whether exactly one is expected. */
|
|
118
|
+
export interface ProcessRole {
|
|
119
|
+
/** Human-readable role shown in the snapshot. */
|
|
120
|
+
role: string;
|
|
121
|
+
/** Whether this process is part of the application (vs. tooling/debugger), counted in the app total. */
|
|
122
|
+
isApp: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Set when at most one instance of this role should exist; `formatPodMemory` warns when more than
|
|
125
|
+
* one is found (e.g. a duplicated/ghost language server).
|
|
126
|
+
*/
|
|
127
|
+
singletonExpected?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Map a raw command line to a {@link ProcessRole}. A head supplies this to label its own processes. */
|
|
131
|
+
export type ProcessClassifier = (cmdline: string) => ProcessRole;
|
|
132
|
+
|
|
133
|
+
/** Generic fallback: every process is shown under the app heading, labelled by its (truncated) command line. */
|
|
134
|
+
const defaultProcessClassifier: ProcessClassifier = cmdline => ({ role: cmdline ? cmdline.slice(0, 60) : 'process', isApp: true });
|
|
135
|
+
|
|
136
|
+
/** Read /proc for every visible process: pid, ppid, RSS, and a command label. */
|
|
137
|
+
function readAllProcs(): RawProc[] {
|
|
138
|
+
const procs: RawProc[] = [];
|
|
139
|
+
let entries: string[] = [];
|
|
140
|
+
try {
|
|
141
|
+
entries = fs.readdirSync('/proc');
|
|
142
|
+
} catch {
|
|
143
|
+
return procs;
|
|
144
|
+
}
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (!/^\d+$/.test(entry)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let status = '';
|
|
150
|
+
try {
|
|
151
|
+
status = fs.readFileSync(`/proc/${entry}/status`, 'utf8');
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const rssMatch = status.match(/^VmRSS:\s+(\d+)\s+kB/m);
|
|
156
|
+
if (!rssMatch) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const ppid = Number(status.match(/^PPid:\s+(\d+)/m)?.[1] ?? 0);
|
|
160
|
+
let label = '';
|
|
161
|
+
try {
|
|
162
|
+
label = fs.readFileSync(`/proc/${entry}/cmdline`, 'utf8').replace(/\0/g, ' ').trim();
|
|
163
|
+
} catch {
|
|
164
|
+
// fall back to the process name from status below.
|
|
165
|
+
}
|
|
166
|
+
if (!label) {
|
|
167
|
+
label = status.match(/^Name:\s+(.+)$/m)?.[1] ?? `pid ${entry}`;
|
|
168
|
+
}
|
|
169
|
+
procs.push({ pid: Number(entry), ppid, rssBytes: Number(rssMatch[1]) * 1024, label });
|
|
170
|
+
}
|
|
171
|
+
return procs;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Sum and list per-process RSS. When {@link rootPid} is given, restrict to that process and its
|
|
176
|
+
* descendants (the app's own process tree), which keeps the figure meaningful in local dev where
|
|
177
|
+
* /proc otherwise shows the whole machine. In a container's PID namespace the tree is effectively
|
|
178
|
+
* all the pod's processes anyway.
|
|
179
|
+
*/
|
|
180
|
+
function collectProcessRss(rootPid?: number): { total: number; processes: ProcInfo[]; scoped: boolean } {
|
|
181
|
+
const all = readAllProcs();
|
|
182
|
+
let selected: RawProc[] = all;
|
|
183
|
+
let scoped = false;
|
|
184
|
+
if (rootPid !== undefined) {
|
|
185
|
+
const childrenByPpid = new Map<number, RawProc[]>();
|
|
186
|
+
for (const proc of all) {
|
|
187
|
+
(childrenByPpid.get(proc.ppid) ?? childrenByPpid.set(proc.ppid, []).get(proc.ppid)!).push(proc);
|
|
188
|
+
}
|
|
189
|
+
const tree: RawProc[] = [];
|
|
190
|
+
const queue = all.filter(proc => proc.pid === rootPid);
|
|
191
|
+
while (queue.length > 0) {
|
|
192
|
+
const proc = queue.shift()!;
|
|
193
|
+
tree.push(proc);
|
|
194
|
+
queue.push(...(childrenByPpid.get(proc.pid) ?? []));
|
|
195
|
+
}
|
|
196
|
+
if (tree.length > 0) {
|
|
197
|
+
selected = tree;
|
|
198
|
+
scoped = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const total = selected.reduce((sum, proc) => sum + proc.rssBytes, 0);
|
|
202
|
+
selected.sort((a, b) => b.rssBytes - a.rssBytes);
|
|
203
|
+
return { total, processes: selected, scoped };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Options for {@link formatPodMemory}. */
|
|
207
|
+
export interface PodMemoryOptions {
|
|
208
|
+
/** Map a process command line to a readable role; lets a head label its own processes. */
|
|
209
|
+
classify?: ProcessClassifier;
|
|
210
|
+
/** Heading for the in-app process group (default `Application`). */
|
|
211
|
+
appLabel?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Format a pod/container-level memory snapshot read from the cgroup this process belongs to
|
|
216
|
+
* (cgroup v2 `memory.current`/`peak`/`max`/`stat`, falling back to cgroup v1). This is the figure
|
|
217
|
+
* the Kubernetes OOM-killer watches — it covers every process in the pod plus page cache and kernel
|
|
218
|
+
* memory, so it is higher than any single `process.memoryUsage().rss`. Also sums per-process RSS so
|
|
219
|
+
* the per-process split is visible. Explains itself when no cgroup memory controller is present
|
|
220
|
+
* (e.g. local dev outside a container).
|
|
221
|
+
*/
|
|
222
|
+
export function formatPodMemory(options: PodMemoryOptions = {}): string {
|
|
223
|
+
const classify = options.classify ?? defaultProcessClassifier;
|
|
224
|
+
const appLabel = options.appLabel ?? 'Application';
|
|
225
|
+
const lines: string[] = ['Pod memory snapshot:'];
|
|
226
|
+
|
|
227
|
+
const v2Current = path.join(CGROUP_ROOT, 'memory.current');
|
|
228
|
+
const v1Usage = path.join(CGROUP_ROOT, 'memory', 'memory.usage_in_bytes');
|
|
229
|
+
|
|
230
|
+
if (fs.existsSync(v2Current)) {
|
|
231
|
+
const current = readNumber(v2Current);
|
|
232
|
+
const peak = readNumber(path.join(CGROUP_ROOT, 'memory.peak'));
|
|
233
|
+
const max = readNumber(path.join(CGROUP_ROOT, 'memory.max'));
|
|
234
|
+
const stat = readKeyedBytes(path.join(CGROUP_ROOT, 'memory.stat'));
|
|
235
|
+
lines.push(' cgroup v2');
|
|
236
|
+
lines.push(` current ${current !== undefined ? Format.bytes(current) : 'n/a'}`);
|
|
237
|
+
lines.push(` peak ${peak !== undefined ? Format.bytes(peak) : 'n/a (kernel too old for memory.peak)'}`);
|
|
238
|
+
lines.push(` limit ${max === Number.POSITIVE_INFINITY ? 'unlimited' : max !== undefined ? Format.bytes(max) : 'n/a'}`);
|
|
239
|
+
if (Object.keys(stat).length > 0) {
|
|
240
|
+
lines.push(
|
|
241
|
+
` breakdown anon ${Format.bytes(stat.anon ?? 0)} (heaps/stacks), file ${Format.bytes(stat.file ?? 0)} (page cache), ` +
|
|
242
|
+
`kernel ${Format.bytes(stat.kernel ?? stat.slab ?? 0)}, sock ${Format.bytes(stat.sock ?? 0)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
} else if (fs.existsSync(v1Usage)) {
|
|
246
|
+
const v1 = path.join(CGROUP_ROOT, 'memory');
|
|
247
|
+
const current = readNumber(v1Usage);
|
|
248
|
+
const peak = readNumber(path.join(v1, 'memory.max_usage_in_bytes'));
|
|
249
|
+
const limit = readNumber(path.join(v1, 'memory.limit_in_bytes'));
|
|
250
|
+
const stat = readKeyedBytes(path.join(v1, 'memory.stat'));
|
|
251
|
+
lines.push(' cgroup v1');
|
|
252
|
+
lines.push(` current ${current !== undefined ? Format.bytes(current) : 'n/a'}`);
|
|
253
|
+
lines.push(` peak ${peak !== undefined ? Format.bytes(peak) : 'n/a'}`);
|
|
254
|
+
// v1 reports a huge sentinel for "unlimited"; treat anything >= 2^60 as unlimited.
|
|
255
|
+
lines.push(` limit ${limit !== undefined && limit < 2 ** 60 ? Format.bytes(limit) : 'unlimited'}`);
|
|
256
|
+
if (Object.keys(stat).length > 0) {
|
|
257
|
+
lines.push(
|
|
258
|
+
` breakdown rss ${Format.bytes(stat.rss ?? 0)}, cache ${Format.bytes(stat.cache ?? 0)} (page cache), ` +
|
|
259
|
+
`kernel ${Format.bytes(stat.kernel ?? 0)}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
lines.push(' cgroup no memory controller under /sys/fs/cgroup (not running in a container?)');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Root at this process so local dev reports only the app's tree, not the whole machine.
|
|
267
|
+
const { processes } = collectProcessRss(process.pid);
|
|
268
|
+
if (processes.length > 0) {
|
|
269
|
+
const classified = processes.map(proc => ({ ...proc, ...classify(proc.label) }));
|
|
270
|
+
const appProcs = classified.filter(proc => proc.isApp);
|
|
271
|
+
const otherProcs = classified.filter(proc => !proc.isApp);
|
|
272
|
+
const appTotal = appProcs.reduce((sum, proc) => sum + proc.rssBytes, 0);
|
|
273
|
+
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push(` ${appLabel}: ${Format.bytes(appTotal)} across ${appProcs.length} process(es)`);
|
|
276
|
+
for (const proc of appProcs) {
|
|
277
|
+
lines.push(` ${Format.bytes(proc.rssBytes).padStart(10)} ${proc.role} (pid ${proc.pid})`);
|
|
278
|
+
}
|
|
279
|
+
const singletonCounts = new Map<string, number>();
|
|
280
|
+
for (const proc of appProcs) {
|
|
281
|
+
if (proc.singletonExpected) {
|
|
282
|
+
singletonCounts.set(proc.role, (singletonCounts.get(proc.role) ?? 0) + 1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const [role, count] of singletonCounts) {
|
|
286
|
+
if (count > 1) {
|
|
287
|
+
lines.push(` WARNING: ${count} instances of "${role}" running - likely ghost process(es); expected 1`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (otherProcs.length > 0) {
|
|
291
|
+
const otherTotal = otherProcs.reduce((sum, proc) => sum + proc.rssBytes, 0);
|
|
292
|
+
lines.push(` Other (tooling/debugger, not counted above): ${Format.bytes(otherTotal)} across ${otherProcs.length} process(es)`);
|
|
293
|
+
}
|
|
294
|
+
lines.push(' Note: in a real pod the cgroup figure above is the OOM ceiling; this per-process');
|
|
295
|
+
lines.push(' split is for attribution. Local dev may include debugger processes (excluded here).');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return lines.join('\n');
|
|
299
|
+
}
|