Skip to content

Commit 99fbd74

Browse files
authored
[api] Add option to collect timing info (#4512)
1 parent 2483843 commit 99fbd74

20 files changed

Lines changed: 1201 additions & 22 deletions

File tree

_packages/native-preview/src/api/async/api.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ import {
6767
toUpdateSnapshotRequest,
6868
} from "../proto.ts";
6969
import { SourceFileCache } from "../sourceFileCache.ts";
70+
import type {
71+
RequestTiming,
72+
TimingAccumulators,
73+
TimingInfo,
74+
} from "../timing.ts";
7075
import {
7176
Client,
7277
type ClientSocketOptions,
@@ -109,10 +114,9 @@ import type {
109114
UnionType,
110115
} from "./types.ts";
111116

112-
export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind };
113-
export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions, SourceFileMetadata };
114-
export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, NumberLiteralType, ObjectType, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
115117
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";
118+
export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind };
119+
export type { APIOptions, AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, DocumentIdentifier, DocumentPosition, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, RequestTiming, SourceFileMetadata, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TimingAccumulators, TimingInfo, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
116120

117121
export class API<FromLSP extends boolean = false> {
118122
private client: Client;
@@ -203,6 +207,26 @@ export class API<FromLSP extends boolean = false> {
203207
clearSourceFileCache(): void {
204208
this.sourceFileCache.clear();
205209
}
210+
211+
/**
212+
* Returns a snapshot of collected timing information for requests made
213+
* through this API instance: client-measured round-trip latency and bytes
214+
* transferred, folded together with the server's own per-request processing
215+
* time and an estimated transport overhead (round-trip minus server time).
216+
*
217+
* Fetching the snapshot issues a lightweight request to the server to
218+
* retrieve its timing collection. Collection must be enabled via the
219+
* `collectTiming` option; when it is not, the returned snapshot has
220+
* `enabled: false` and zeroed totals.
221+
*/
222+
getTimingInfo(): Promise<TimingInfo> {
223+
return this.client.getTimingInfo();
224+
}
225+
226+
/** Clears all accumulated timing totals and recent-request history, on both the client and the server. */
227+
resetTimingInfo(): Promise<void> {
228+
return this.client.resetTimingInfo();
229+
}
206230
}
207231

208232
export class InternalAPI {
@@ -612,7 +636,7 @@ export class Program {
612636
const parseOptionsKey = readParseOptionsKey(view);
613637

614638
// Create a new RemoteSourceFile and cache it (set returns existing if hash matches)
615-
const sourceFile = new RemoteSourceFile(binaryData, this.decoder) as unknown as SourceFile;
639+
const sourceFile = new RemoteSourceFile(binaryData, this.decoder, this.client.getTimingCollector()) as unknown as SourceFile;
616640
return this.sourceFileCache.set(path, sourceFile, parseOptionsKey, contentHash, this.snapshotId, this.project.id);
617641
}
618642

_packages/native-preview/src/api/async/client.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import {
2020
isSpawnOptions,
2121
resolveExePath,
2222
} from "../options.ts";
23+
import {
24+
combineTimingInfo,
25+
disabledServerTimingInfo,
26+
disabledTimingInfo,
27+
type ServerTimingInfo,
28+
TimingCollector,
29+
type TimingInfo,
30+
} from "../timing.ts";
2331

2432
export type { ClientOptions, ClientSocketOptions, ClientSpawnOptions };
2533

@@ -33,9 +41,13 @@ export class Client {
3341
private connection: MessageConnection | undefined;
3442
private options: ClientOptions;
3543
private connected = false;
44+
private timing: TimingCollector | undefined;
3645

3746
constructor(options: ClientOptions) {
3847
this.options = options;
48+
if (isSpawnOptions(options) && options.collectTiming) {
49+
this.timing = new TimingCollector();
50+
}
3951
}
4052

4153
async connect(): Promise<void> {
@@ -60,6 +72,10 @@ export class Client {
6072
options.cwd ?? process.cwd(),
6173
];
6274

75+
if (options.collectTiming) {
76+
args.push("--timing");
77+
}
78+
6379
// Enable virtual FS callbacks for each provided FS function
6480
const enabledCallbacks: string[] = [];
6581
if (options.fs) {
@@ -142,7 +158,27 @@ export class Client {
142158
}
143159

144160
const requestType = new RequestType<unknown, T, void>(method);
145-
return this.connection.sendRequest(requestType, params);
161+
if (!this.timing) {
162+
return this.connection.sendRequest(requestType, params);
163+
}
164+
165+
// Round-trip latency is measured here; byte counts approximate the wire
166+
// payload via the serialized JSON. Server-side processing time is not
167+
// carried on the response; it is retrieved separately (via a
168+
// getServerTiming request) and folded in by getTimingInfo().
169+
const bytesSent = params === undefined ? 0 : Buffer.byteLength(JSON.stringify(params), "utf-8");
170+
const start = performance.now();
171+
const result = await this.connection.sendRequest(requestType, params);
172+
const roundTripMs = performance.now() - start;
173+
this.timing.record({
174+
method,
175+
roundTripMs,
176+
bytesSent,
177+
bytesReceived: result === undefined || result === null
178+
? 0
179+
: Buffer.byteLength(JSON.stringify(result), "utf-8"),
180+
});
181+
return result;
146182
}
147183

148184
async apiRequestBinary(method: string, params?: unknown): Promise<Uint8Array | undefined> {
@@ -152,6 +188,53 @@ export class Client {
152188
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
153189
}
154190

191+
/**
192+
* Returns the timing collector that per-node materialization is reported
193+
* into, or undefined when timing collection is disabled. The returned
194+
* collector is the same one folded into {@link getTimingInfo}, so
195+
* materialization totals surface alongside request timings.
196+
*/
197+
getTimingCollector(): TimingCollector | undefined {
198+
return this.timing;
199+
}
200+
201+
/**
202+
* Returns a combined timing snapshot: client-measured round-trip and byte
203+
* counts folded together with the server's own per-request processing time
204+
* (fetched via a getServerTiming request) and estimated transport overhead.
205+
*/
206+
async getTimingInfo(): Promise<TimingInfo> {
207+
if (!this.timing) {
208+
return disabledTimingInfo();
209+
}
210+
const local = this.timing.getInfo();
211+
// No requests have been sent yet: nothing to fetch from the server.
212+
if (!this.connected || !this.connection) {
213+
return local;
214+
}
215+
return combineTimingInfo(local, await this.fetchServerTiming());
216+
}
217+
218+
async resetTimingInfo(): Promise<void> {
219+
if (!this.timing) return;
220+
this.timing.reset();
221+
if (this.connected && this.connection) {
222+
// Keep the server's collection in sync so combined totals stay meaningful.
223+
const requestType = new RequestType<unknown, void, void>("resetServerTiming");
224+
await this.connection.sendRequest(requestType, undefined);
225+
}
226+
}
227+
228+
private async fetchServerTiming(): Promise<ServerTimingInfo> {
229+
if (!this.connection) {
230+
return disabledServerTimingInfo();
231+
}
232+
// Fetch the server's own timing collection via a dedicated request. This
233+
// bypasses the client-side collector so the query does not pollute it.
234+
const requestType = new RequestType<unknown, ServerTimingInfo, void>("getServerTiming");
235+
return this.connection.sendRequest(requestType, undefined);
236+
}
237+
155238
async close(): Promise<void> {
156239
if (this.connection) {
157240
this.connection.dispose();

_packages/native-preview/src/api/node/node.generated.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,10 @@ export class RemoteNodeList extends Array<RemoteNode> implements NodeArray<Remot
180180
if (kind === KIND_NODE_LIST) {
181181
throw new Error("NodeList cannot directly contain another NodeList");
182182
}
183-
child = new RemoteNode(this.view, index, this.parent, this.sourceFile, this.sourceFile._offsetNodes);
184-
this.sourceFile.nodes[index] = child;
183+
const sf = this.sourceFile;
184+
child = new RemoteNode(this.view, index, this.parent, sf, sf._offsetNodes);
185+
sf.nodes[index] = child;
186+
sf._timing?.recordMaterialization();
185187
}
186188
return child;
187189
}
@@ -308,13 +310,14 @@ export class RemoteNode extends RemoteNodeBase implements Node {
308310
private getOrCreateChildAtNodeIndex(index: number): RemoteNode | RemoteNodeList {
309311
let child = this.sourceFile.nodes[index];
310312
if (!child) {
311-
const offsetNodes = this.sourceFile._offsetNodes;
312-
const kind = this.view.getUint32(offsetNodes + index * NODE_LEN + NODE_OFFSET_KIND, true);
313313
const sf = this.sourceFile;
314+
const offsetNodes = sf._offsetNodes;
315+
const kind = this.view.getUint32(offsetNodes + index * NODE_LEN + NODE_OFFSET_KIND, true);
314316
child = kind === KIND_NODE_LIST
315317
? new RemoteNodeList(this.view, index, this, sf, offsetNodes)
316318
: new RemoteNode(this.view, index, this, sf, offsetNodes);
317319
sf.nodes[index] = child;
320+
sf._timing?.recordMaterialization();
318321
}
319322
return child;
320323
}

_packages/native-preview/src/api/node/node.infrastructure.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type Node,
55
SyntaxKind,
66
} from "../../ast/index.ts";
7+
import type { TimingCollector } from "../timing.ts";
78
import {
89
HEADER_OFFSET_HASH_HI0,
910
HEADER_OFFSET_HASH_HI1,
@@ -52,6 +53,13 @@ export interface SourceFileInfo {
5253
readonly _decoder: TextDecoder;
5354
nodes: any[];
5455
readonly path?: string;
56+
/**
57+
* The timing collector that per-node materialization is reported into, and
58+
* that this source file registered itself with when fetched. Present only
59+
* when timing collection is enabled; when undefined, materialization is not
60+
* timed and no clock is read.
61+
*/
62+
readonly _timing?: TimingCollector | undefined;
5563
readFileReferences(offset: number): readonly FileReference[];
5664
readNodeIndexArray(offset: number): readonly Node[];
5765
readStringArray(offset: number): readonly string[];

_packages/native-preview/src/api/node/node.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SyntaxKind,
77
TokenFlags,
88
} from "../../ast/index.ts";
9+
import type { TimingCollector } from "../timing.ts";
910
import { MsgpackReader } from "./msgpack.ts";
1011
import {
1112
RemoteNode,
@@ -47,6 +48,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
4748
readonly _offsetExtendedData: number;
4849
readonly _offsetStructuredData: number;
4950
readonly _decoder: TextDecoder;
51+
readonly _timing: TimingCollector | undefined;
5052
private _cachedText: string | undefined;
5153
private _cachedReferencedFiles: readonly FileReference[] | undefined;
5254
private _cachedTypeReferenceDirectives: readonly FileReference[] | undefined;
@@ -55,7 +57,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
5557
private _cachedModuleAugmentations: readonly Node[] | undefined;
5658
private _cachedAmbientModuleNames: readonly string[] | undefined;
5759

58-
constructor(data: Uint8Array, decoder: TextDecoder) {
60+
constructor(data: Uint8Array, decoder: TextDecoder, timing?: TimingCollector) {
5961
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
6062
const offsetNodes = view.getUint32(HEADER_OFFSET_NODES, true);
6163
super(view, 1, undefined!, undefined!, offsetNodes);
@@ -66,8 +68,12 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
6668
this._offsetExtendedData = view.getUint32(HEADER_OFFSET_EXTENDED_DATA, true);
6769
this._offsetStructuredData = view.getUint32(HEADER_OFFSET_STRUCTURED_DATA, true);
6870
this._decoder = decoder;
71+
this._timing = timing;
6972
this.nodes = Array((view.byteLength - offsetNodes) / NODE_LEN);
7073
this.nodes[1] = this;
74+
// Every node slot is materializable on demand except the nil sentinel at
75+
// index 0 and the source-file node at index 1, which is pre-materialized.
76+
timing?.recordSourceFileFetched(Math.max(0, this.nodes.length - 2));
7177
}
7278

7379
readFileReferences(structuredDataOffset: number): readonly FileReference[] {
@@ -139,6 +145,7 @@ export class RemoteSourceFile extends RemoteNode implements SourceFileInfo {
139145
const parent = parentIndex === index ? this : this.getOrCreateNodeAtIndex(parentIndex) as RemoteNode;
140146
node = new RemoteNode(this.view, index, parent, this, this._offsetNodes);
141147
this.nodes[index] = node;
148+
this._timing?.recordMaterialization();
142149
}
143150
return node as Node;
144151
}

_packages/native-preview/src/api/options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export interface ClientSpawnOptions {
1717
cwd?: string;
1818
/** Virtual filesystem callbacks */
1919
fs?: FileSystem;
20+
/**
21+
* When true, collect timing information for each request. The client
22+
* measures round-trip latency and bytes sent/received, and the server
23+
* measures its own per-request processing time; both are combined (along
24+
* with an estimated transport overhead) in the snapshot returned by
25+
* {@link API.getTimingInfo}.
26+
*/
27+
collectTiming?: boolean;
2028
}
2129

2230
export type ClientOptions = ClientSocketOptions | ClientSpawnOptions;

_packages/native-preview/src/api/sync/api.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ import {
7575
toUpdateSnapshotRequest,
7676
} from "../proto.ts";
7777
import { SourceFileCache } from "../sourceFileCache.ts";
78+
import type {
79+
RequestTiming,
80+
TimingAccumulators,
81+
TimingInfo,
82+
} from "../timing.ts";
7883
import {
7984
Client,
8085
type ClientSocketOptions,
@@ -117,10 +122,9 @@ import type {
117122
UnionType,
118123
} from "./types.ts";
119124

120-
export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind };
121-
export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions, SourceFileMetadata };
122-
export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, NumberLiteralType, ObjectType, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
123125
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";
126+
export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind };
127+
export type { APIOptions, AssertsIdentifierTypePredicate, AssertsThisTypePredicate, BigIntLiteralType, BooleanLiteralType, ClientSocketOptions, ClientSpawnOptions, CompilerOptions, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, DocumentIdentifier, DocumentPosition, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, RequestTiming, SourceFileMetadata, StringLiteralType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TimingAccumulators, TimingInfo, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType };
124128

125129
export class API<FromLSP extends boolean = false> {
126130
private client: Client;
@@ -211,6 +215,26 @@ export class API<FromLSP extends boolean = false> {
211215
clearSourceFileCache(): void {
212216
this.sourceFileCache.clear();
213217
}
218+
219+
/**
220+
* Returns a snapshot of collected timing information for requests made
221+
* through this API instance: client-measured round-trip latency and bytes
222+
* transferred, folded together with the server's own per-request processing
223+
* time and an estimated transport overhead (round-trip minus server time).
224+
*
225+
* Fetching the snapshot issues a lightweight request to the server to
226+
* retrieve its timing collection. Collection must be enabled via the
227+
* `collectTiming` option; when it is not, the returned snapshot has
228+
* `enabled: false` and zeroed totals.
229+
*/
230+
getTimingInfo(): TimingInfo {
231+
return this.client.getTimingInfo();
232+
}
233+
234+
/** Clears all accumulated timing totals and recent-request history, on both the client and the server. */
235+
resetTimingInfo(): void {
236+
return this.client.resetTimingInfo();
237+
}
214238
}
215239

216240
export class InternalAPI {
@@ -620,7 +644,7 @@ export class Program {
620644
const parseOptionsKey = readParseOptionsKey(view);
621645

622646
// Create a new RemoteSourceFile and cache it (set returns existing if hash matches)
623-
const sourceFile = new RemoteSourceFile(binaryData, this.decoder) as unknown as SourceFile;
647+
const sourceFile = new RemoteSourceFile(binaryData, this.decoder, this.client.getTimingCollector()) as unknown as SourceFile;
624648
return this.sourceFileCache.set(path, sourceFile, parseOptionsKey, contentHash, this.snapshotId, this.project.id);
625649
}
626650

0 commit comments

Comments
 (0)