src/vs/platform/agentHost/common/ahpJsonlLogger.ts

261 LOC · 251 covered · 10 uncovered · 39 ranges · 12 concepts · 7 introducers · 10 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/files/common/files.ts · 1658 LOCcommon/files.tssrc/vs/platform/files/common/inMemoryFilesystemProvider.ts · 358 LOCcommon/inMemoryFilesyste…ahpJsonlLogger.ts ×4 · 17 introduced LOCahpJsonlLogger.ts ×4ahpJsonlLogger.ts ×1 · 2 introduced LOCahpJsonlLogger.ts ×1ahpJsonlLogger.test|title=AhpJsonlLogger flush waits for batched writes and ordering is preserved across drains|occurrence=1 · 0 introduced LOCahpJsonlLogger.test|titl…ahpJsonlLogger.ts ×2 · 15 introduced LOCahpJsonlLogger.ts ×2ahpJsonlLogger.test|title=AhpJsonlLogger coalesces synchronously queued log calls into a single write|occurrence=1 · 0 introduced LOCahpJsonlLogger.test|titl…ahpJsonlLogger.ts ×15 · 103 introduced LOCahpJsonlLogger.ts ×15ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry serialises URIs nested in arrays and objects|occurrence=1 · 0 introduced LOCahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry round-trips raw UriComponents marked with $mid|occurrence=1 · 0 introduced LOCahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry serialises a top-level URI as its string form|occurrence=1 · 0 introduced LOCahpJsonlLogger.test|titl…ahpJsonlLogger.ts ×1 · 2 introduced LOCahpJsonlLogger.ts ×1ahpJsonlLogger.ts ×1 · 1 introduced LOCahpJsonlLogger.ts ×1ahpJsonlLogger.ts ×15 · 119 introduced LOCahpJsonlLogger.ts ×15ahpJsonlLogger.test|title=AhpJsonlLogger coalesces synchronously queued log calls into a single write|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger coalesces synchronously queued log calls into a single write|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger elides oversized string payloads while keeping the line valid JSONL|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger elides oversized string payloads while keeping the line valid JSONL|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger flush waits for batched writes and ordering is preserved across drains|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger flush waits for batched writes and ordering is preserved across drains|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger rotates JSONL files and keeps bounded history|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger rotates JSONL files and keeps bounded history|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry does not misidentify non-URI objects that carry $mid: 1|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry does not misidentify non-URI objects that carry $mid: 1|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry leaves URI-shaped objects without $mid as plain objects|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry leaves URI-shaped objects without $mid as plain objects|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry round-trips raw UriComponents marked with $mid|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry round-trips raw UriComponents marked with $mid|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry serialises URIs nested in arrays and objects|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry serialises URIs nested in arrays and objects|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry serialises a top-level URI as its string form|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger stringifyAhpLogEntry serialises a top-level URI as its string form|occurrence=1ahpJsonlLogger.test|titl…ahpJsonlLogger.test|title=AhpJsonlLogger writes canonical JSON-RPC JSONL with metadata at the root|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/common/ahpJsonlLogger.test|title=AhpJsonlLogger writes canonical JSON-RPC JSONL with metadata at the root|occurrence=1ahpJsonlLogger.test|titl…Focused file · src/vs/platform/agentHost/common/ahpJsonlLogger.ts · 261 LOCcommon/ahpJsonlLogger.ts

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- ahpJsonlLogger.ts ×15
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { VSBuffer } from '../../../base/common/buffer.js';
7 > import { Disposable } from '../../../base/common/lifecycle.js';
8 > import { MarshalledId } from '../../../base/common/marshallingIds.js';
9 > import { joinPath } from '../../../base/common/resources.js';
10 > import { isUriComponents, URI, UriComponents } from '../../../base/common/uri.js';
11 > import { IFileService, IFileStatWithMetadata } from '../../files/common/files.js';
12 > import { ILogService } from '../../log/common/log.js';
13 >
14 > export type AhpLogDirection = 'c2s' | 's2c';
15 >
16 > interface IAhpLogMeta {
17 > readonly ts: string;
18 > readonly dir: AhpLogDirection;
19 > readonly connectionId: string;
20 > readonly transport: string;
21 > readonly byteLength?: number;
22 > /** Set when oversized string values in the entry were elided (see {@link stringifyAhpLogEntryTruncated}). */
23 > truncated?: boolean;
24 > }
25 >
26 > export interface IAhpJsonlLoggerOptions {
27 > readonly logsHome: URI;
28 > readonly connectionId: string;
29 > readonly transport: string;
30 > readonly maxFileSizeBytes?: number;
31 > readonly maxFiles?: number;
32 > }
33 >
34 > const AHP_LOG_DIR = 'ahp';
35 > const DEFAULT_MAX_FILE_SIZE_BYTES = 75 * 1024 * 1024;
36 > const DEFAULT_MAX_FILES = 5;
37 > // Cap the size of any single coalesced writeFile to avoid producing huge
38 > // concatenated VSBuffers (which would just create the GC pressure we're
39 > // trying to avoid). 1 MiB strikes a balance between amortizing IPC overhead
40 > // and keeping per-write allocations modest.
41 > const MAX_BATCH_BYTES = 1024 * 1024;
42 >
43 > // A single AHP protocol message can be enormous (e.g. a `resourceRead` carrying
44 > // a base64-encoded file, or an `action` carrying a full session snapshot). We
45 > // don't want to write hundreds of MB on a single JSONL line — it bloats the log
46 > // directory and, more importantly, building/holding that line creates exactly
47 > // the GC pressure these logs are meant to help diagnose. When a serialized
48 > // entry exceeds this size we re-serialize it with oversized string values
49 > // elided so the line stays well-formed JSONL.
50 > const MAX_LOG_LINE_LENGTH = 1024 * 1024;
51 > // When trimming an oversized entry, individual string values are capped to this
52 > // length. Generous enough to keep messages useful for debugging.
53 > const MAX_LOGGED_STRING_LENGTH = 16 * 1024;
54 >
55 >
56 > export class AhpJsonlLogger extends Disposable {
57 >
58 > private readonly _directory: URI;
59 > private readonly _baseName: string;
60 > private readonly _maxFileSizeBytes: number;
61 > private readonly _maxFiles: number;
62 > private _currentFile: URI;
63 > private _currentSize = 0;
64 > private _segment = 0;
65 > private _queue = Promise.resolve();
66 > private _pending: VSBuffer[] = [];
67 > private _drainScheduled = false;
68 > private _folderCreated: Promise<IFileStatWithMetadata> | undefined;
69 >
70 > constructor(
71 > private readonly _options: IAhpJsonlLoggerOptions, ahpJsonlLogger.ts ×15
72 > @IFileService private readonly _fileService: IFileService,
73 > @ILogService private readonly _logService: ILogService,
74 > ) {
75 > super();
76 > this._directory = joinPath(this._options.logsHome, AHP_LOG_DIR);
77 > // Truncate connectionId to avoid filesystem filename length limits (e.g. 255 on ext4/APFS)
78 > const safeConnectionId = sanitizeFilePart(this._options.connectionId).slice(0, 64);
79 > this._baseName = `ahp-${toFileTimestamp(new Date())}-${safeConnectionId}.jsonl`;
80 > this._maxFileSizeBytes = this._options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
81 > this._maxFiles = this._options.maxFiles ?? DEFAULT_MAX_FILES;
82 > this._currentFile = joinPath(this._directory, this._baseName);
83 > }
85 > get resource(): URI {
86 > return this._currentFile; ahpJsonlLogger.ts ×15
87 > }
89 > log(message: object, dir: AhpLogDirection, byteLength?: number): void {
90 > const meta: IAhpLogMeta = { ahpJsonlLogger.ts ×15
91 > ts: new Date().toISOString(),
92 > dir,
93 > connectionId: this._options.connectionId,
94 > transport: this._options.transport,
95 > ...(typeof byteLength === 'number' ? { byteLength } : {}),
96 > };
97 > const entry = { ...message, _ahpLog: meta };
98 > // Fast path: serialize once. The vast majority of messages are small, so
99 > // we only pay a single stringify and use its length to decide whether the
100 > // rare oversized-message path below is needed.
101 > let body = stringifyAhpLogEntry(entry);
102 > if (body.length > MAX_LOG_LINE_LENGTH) {
103 > // Slow path (rare): a single message carried very large payloads. Walk ahpJsonlLogger.ts ×2
104 > // the object via a replacer that elides long string values, keeping the
105 > // line valid JSONL instead of writing/holding the full multi-MB payload.
106 > meta.truncated = true;
107 > body = stringifyAhpLogEntryTruncated(entry, MAX_LOGGED_STRING_LENGTH);
108 > }
109 > const line = `${body}\n`; ahpJsonlLogger.ts ×15
110 > this._pending.push(VSBuffer.fromString(line));
111 > this._scheduleDrain();
112 > }
114 > async flush(): Promise<void> {
115 > // Pending entries always have a drain scheduled (see _scheduleDrain), so ahpJsonlLogger.ts ×15
116 > // awaiting the queue is sufficient to flush everything submitted before
117 > // this call.
118 > await this._queue;
119 > }
121 > private _scheduleDrain(): void {
122 > if (this._drainScheduled) { ahpJsonlLogger.ts ×15
123 > return;
124 > }
125 > this._drainScheduled = true;
126 > this._queue = this._queue.then(() => this._drainPending()).catch(error => {
127 this._logService.error('[AHPLog] Failed to write transport log', error);
129 > }
131 > private async _drainPending(): Promise<void> {
132 > // Clear the scheduled flag before snapshotting _pending so that any log() ahpJsonlLogger.ts ×15
133 > // calls happening during the awaits below will schedule a fresh drain.
134 > this._drainScheduled = false;
135 > if (this._pending.length === 0) {
136 return;
137 }
138 > const buffers = this._pending; ahpJsonlLogger.ts ×15
139 > this._pending = [];
140 >
141 > // Create folder once and memoize to avoid repeated filesystem calls
142 > if (!this._folderCreated) {
143 > this._folderCreated = this._fileService.createFolder(this._directory);
144 > }
145 > await this._folderCreated;
146 > if (this._currentSize === 0) {
147 > this._currentSize = await this._getFileSize(this._currentFile);
148 > }
149 >
150 > // Coalesce buffers into chunks, respecting both file-rotation size and the
151 > // per-write batch cap. Rotation is checked per-entry to preserve the
152 > // invariant that we don't exceed maxFileSizeBytes once a file has data.
153 > let chunk: VSBuffer[] = [];
154 > let chunkSize = 0;
155 > const flushChunk = async () => {
156 > if (chunk.length === 0) {
157 return;
158 }
159 > const combined = chunk.length === 1 ? chunk[0] : VSBuffer.concat(chunk, chunkSize); ahpJsonlLogger.ts ×15
160 > await this._fileService.writeFile(this._currentFile, combined, { append: true });
161 > this._currentSize += combined.byteLength;
162 > chunk = [];
163 > chunkSize = 0;
164 > };
165 >
166 > for (const buffer of buffers) {
167 > const totalInFile = this._currentSize + chunkSize;
168 > if (totalInFile > 0 && totalInFile + buffer.byteLength > this._maxFileSizeBytes) {
169 > await flushChunk(); ahpJsonlLogger.ts ×4
170 > await this._rotate();
171 > } else if (chunkSize > 0 && chunkSize + buffer.byteLength > MAX_BATCH_BYTES) { ahpJsonlLogger.ts ×15
172 // Same file but the batch is getting too large; flush early to
173 // avoid creating an oversized concatenated VSBuffer.
174 await flushChunk();
175 }
176 > chunk.push(buffer); ahpJsonlLogger.ts ×15
177 > chunkSize += buffer.byteLength;
178 > }
179 > await flushChunk();
180 > }
182 > private async _rotate(): Promise<void> {
183 > this._segment++; ahpJsonlLogger.ts ×4
184 > const oldSegment = this._segment - this._maxFiles;
185 > if (oldSegment >= 0) {
186 > await this._fileService.del(this._resourceForSegment(oldSegment)).catch(error => {
187 this._logService.trace('[AHPLog] Failed to delete old transport log', error);
189 > }
190 > this._currentFile = this._resourceForSegment(this._segment);
191 > this._currentSize = await this._getFileSize(this._currentFile);
192 > }
194 > private _resourceForSegment(segment: number): URI {
195 > if (segment === 0) { ahpJsonlLogger.ts ×4
196 > return joinPath(this._directory, this._baseName);
197 > }
198 > const currentBaseName = this._baseName.slice(0, -'.jsonl'.length);
199 > return joinPath(this._directory, `${currentBaseName}.${segment}.jsonl`);
200 > }
202 > private async _getFileSize(resource: URI): Promise<number> {
204 > return (await this._fileService.resolve(resource)).size ?? 0;
205 > } catch {
206 > return 0;
207 > }
208 > }
210 >
211 > export function getAhpLogByteLength(text: string): number {
212 > return VSBuffer.fromString(text).byteLength; ahpJsonlLogger.ts ×1
213 > }
215 > export function stringifyAhpLogEntry(value: unknown): string {
216 > return JSON.stringify(value, _ahpReplacer);
217 > }
218 >
219 > /**
220 > * Like {@link stringifyAhpLogEntry} but additionally elides any string value
221 > * longer than {@param maxStringLength}, replacing the overflow with a short
222 > * marker. The result is still well-formed JSON, so the log remains valid JSONL.
223 > * Only used for the rare oversized entry, so the extra per-value work is fine.
224 > */
225 > function stringifyAhpLogEntryTruncated(value: unknown, maxStringLength: number): string { ahpJsonlLogger.ts ×2
226 > return JSON.stringify(value, function (this: unknown, key: string, val: unknown): unknown {
227 > const revived = _ahpReplacer.call(this, key, val);
228 > if (typeof revived === 'string' && revived.length > maxStringLength) {
229 > return `${revived.slice(0, maxStringLength)}…[${revived.length - maxStringLength} more chars elided]`;
230 > }
231 > return revived;
232 > });
233 > }
235 > /**
236 > * JSON.stringify replacer that converts URI values to their canonical string
237 > * form. `URI.prototype.toJSON()` runs before this replacer is invoked and
238 > * produces a {@link UriComponents}-shaped object stamped with
239 > * `$mid: MarshalledId.Uri`, which we detect here to round-trip back through
240 > * {@link URI.revive}. This avoids the expensive deep-clone tree walk that
241 > * would otherwise be required to find every URI in a message payload.
242 > */
243 > function _ahpReplacer(this: unknown, _key: string, value: unknown): unknown {
244 > if (
245 > value
246 > && typeof value === 'object'
247 > && (value as { $mid?: number }).$mid === MarshalledId.Uri
248 > && isUriComponents(value) ahpJsonlLogger.ts ×1
250 > return URI.revive(value as UriComponents).toString(); ahpJsonlLogger.ts ×1
251 > }
252 > return value; ahpJsonlLogger.ts ×15
253 > }
254 >
255 > function toFileTimestamp(date: Date): string { ahpJsonlLogger.ts ×15
256 > return date.toISOString().replace(/[:.]/g, '-');
257 > }
259 > function sanitizeFilePart(value: string): string { ahpJsonlLogger.ts ×15
260 > return value.replace(/[\\/:\*\?"<>\|\s]+/g, '-').replace(/^-+|-+$/g, '') || 'connection';
261 > }