Skip to content

Commit 2a5e1cf

Browse files
authored
Add perf / project telemetry (#3257)
1 parent dea1bee commit 2a5e1cf

10 files changed

Lines changed: 1032 additions & 22 deletions

File tree

_extension/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class Client implements vscode.Disposable {
6666
traceOutputChannel: this.traceOutputChannel,
6767
initializationOptions: {
6868
codeLensShowLocationsCommandName,
69+
enableTelemetry: true,
6970
},
7071
errorHandler: new ReportingErrorHandler(this.telemetryReporter, 5),
7172
middleware: {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/Microsoft/go-winio v0.6.2
77
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433
88
github.com/google/go-cmp v0.7.0
9+
github.com/mackerelio/go-osstat v0.2.7
910
github.com/peter-evans/patience v0.3.0
1011
github.com/zeebo/xxh3 v1.1.0
1112
golang.org/x/sync v0.20.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
66
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
77
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
88
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
9+
github.com/mackerelio/go-osstat v0.2.7 h1:TCavZi10wF49bT6iQZ9eT2keGZQpC69MTDfdJej5e94=
10+
github.com/mackerelio/go-osstat v0.2.7/go.mod h1:dwpYh5pIPmvk+IEwBKNIWRFMB92mrC08CmXOhDC7nQk=
911
github.com/matryer/moq v0.6.0 h1:FCccG09c3o4cg3gnrZ+7ty5Pa/sjmN24BMHp/0pwhjQ=
1012
github.com/matryer/moq v0.6.0/go.mod h1:iEVhY/XBwFG/nbRyEf0oV+SqnTHZJ5wectzx7yT+y98=
1113
github.com/peter-evans/patience v0.3.0 h1:rX0JdJeepqdQl1Sk9c9uvorjYYzL2TfgLX1adqYm9cA=

internal/lsp/lsproto/_generate/generate.mts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ const customStructures: Structure[] = [
5656
optional: true,
5757
documentation: "userPreferences and/or formatting options if provided at initialization.",
5858
},
59+
{
60+
name: "enableTelemetry",
61+
type: { kind: "base", name: "boolean" },
62+
optional: true,
63+
documentation: "EnableTelemetry enables sending telemetry events from the server to the client.",
64+
},
5965
],
6066
documentation: "InitializationOptions contains user-provided initialization options.",
6167
},
@@ -286,6 +292,105 @@ const customStructures: Structure[] = [
286292
],
287293
documentation: "Result for the custom/projectInfo request.",
288294
},
295+
{
296+
name: "PerformanceStatsTelemetryEvent",
297+
properties: [
298+
{
299+
name: "eventName",
300+
type: { kind: "stringLiteral", value: "languageServer.performanceStats" },
301+
documentation: "The name of the telemetry event.",
302+
},
303+
{
304+
name: "telemetryPurpose",
305+
type: { kind: "stringLiteral", value: "usage" },
306+
documentation: "Indicates this is a usage telemetry event.",
307+
},
308+
{
309+
name: "measurements",
310+
type: { kind: "reference", name: "PerformanceStatsTelemetryMeasurements" },
311+
documentation: "Numeric measurements for this telemetry event.",
312+
},
313+
],
314+
documentation: "A PerformanceStatsTelemetryEvent is sent periodically with performance and resource usage statistics.",
315+
},
316+
{
317+
name: "PerformanceStatsTelemetryMeasurements",
318+
properties: [
319+
{ name: "openFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of files currently open in the editor." },
320+
{ name: "uptimeSeconds", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Seconds since the session was initialized." },
321+
{ name: "projectCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of loaded projects." },
322+
{ name: "configCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of loaded config files." },
323+
{ name: "cachedDiskFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of files cached from disk." },
324+
{ name: "memoryUsedBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total memory mapped by the Go runtime in bytes." },
325+
{ name: "goMemLimit", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "GOMEMLIMIT value in bytes, or 0 if not set." },
326+
{ name: "goGCPercent", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "GOGC percentage value configured for the GC." },
327+
{ name: "heapGoalBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap size target the GC is working toward in bytes." },
328+
{ name: "heapLiveBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Bytes of live (reachable) heap objects." },
329+
{ name: "heapObjectCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of live or unswept objects occupying heap memory." },
330+
{ name: "heapStackBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap memory reserved for goroutine stacks." },
331+
{ name: "heapReleasedBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap memory returned to the OS." },
332+
{ name: "heapFreeBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Heap memory that is free and eligible to be returned to the OS." },
333+
{ name: "gcScanHeapBytes", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total scannable heap bytes — how much the GC must traverse." },
334+
{ name: "goMaxProcs", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "The current GOMAXPROCS value." },
335+
{ name: "goroutineCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Current number of goroutines." },
336+
{ name: "gcCyclesTotal", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total completed GC cycles." },
337+
{ name: "gcCPUSeconds", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Cumulative CPU time spent in GC in seconds." },
338+
{ name: "userCPUSeconds", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Cumulative CPU time spent in user Go code in seconds." },
339+
{ name: "systemMemTotal", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total physical memory on the system in bytes." },
340+
{ name: "systemMemUsed", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Used physical memory on the system in bytes." },
341+
{ name: "autoImportProjectBucketCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of auto-import project buckets." },
342+
{ name: "autoImportNodeModulesBucketCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of auto-import node_modules buckets." },
343+
{ name: "autoImportUniquePackageCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Unique packages across all node_modules buckets." },
344+
{ name: "autoImportProjectExportCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total indexed exports from project files." },
345+
{ name: "autoImportNodeModulesExportCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total indexed exports from node_modules." },
346+
{ name: "autoImportProjectFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total files tracked across project buckets." },
347+
{ name: "autoImportNodeModulesFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Total files tracked across node_modules buckets." },
348+
{ name: "autoImportNodeModulesUnfilteredBucketCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true, documentation: "Number of node_modules buckets with no package.json filter." },
349+
],
350+
documentation: "Numeric measurements for PerformanceStatsTelemetryEvent.",
351+
},
352+
{
353+
name: "ProjectInfoTelemetryEvent",
354+
properties: [
355+
{
356+
name: "eventName",
357+
type: { kind: "stringLiteral", value: "languageServer.projectInfo" },
358+
documentation: "The name of the telemetry event.",
359+
},
360+
{
361+
name: "telemetryPurpose",
362+
type: { kind: "stringLiteral", value: "usage" },
363+
documentation: "Indicates this is a usage telemetry event.",
364+
},
365+
{
366+
name: "properties",
367+
type: { kind: "map", key: { kind: "base", name: "string" }, value: { kind: "base", name: "string" } },
368+
documentation: "String properties for this telemetry event. Complex values (compilerOptions, fileStats) are JSON-stringified.",
369+
},
370+
{
371+
name: "measurements",
372+
type: { kind: "reference", name: "ProjectInfoTelemetryMeasurements" },
373+
documentation: "Numeric measurements for this telemetry event.",
374+
},
375+
],
376+
documentation: "A ProjectInfoTelemetryEvent is sent once per project when it is first loaded.",
377+
},
378+
{
379+
name: "ProjectInfoTelemetryMeasurements",
380+
properties: [
381+
{ name: "jsFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
382+
{ name: "jsFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
383+
{ name: "jsxFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
384+
{ name: "jsxFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
385+
{ name: "tsFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
386+
{ name: "tsFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
387+
{ name: "tsxFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
388+
{ name: "tsxFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
389+
{ name: "dtsFileCount", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
390+
{ name: "dtsFileSize", type: { kind: "base", name: "decimal" }, omitzeroValue: true },
391+
],
392+
documentation: "Numeric measurements for ProjectInfoTelemetryEvent.",
393+
},
289394
];
290395

291396
const customEnumerations: Enumeration[] = [
@@ -425,6 +530,8 @@ const customTypeAliases: TypeAlias[] = [
425530
kind: "or",
426531
items: [
427532
{ kind: "reference", name: "RequestFailureTelemetryEvent" },
533+
{ kind: "reference", name: "PerformanceStatsTelemetryEvent" },
534+
{ kind: "reference", name: "ProjectInfoTelemetryEvent" },
428535
{ kind: "base", name: "null" },
429536
],
430537
},
@@ -777,6 +884,39 @@ function patchAndPreprocessModel() {
777884

778885
patchAndPreprocessModel();
779886

887+
// Validate that telemetry events in the TelemetryEvent union have properly shaped
888+
// measurements and properties fields. measurements struct fields must only contain
889+
// numeric types (decimal/integer/uinteger).
890+
function validateTelemetryEvents() {
891+
const telemetryAlias = customTypeAliases.find(a => a.name === "TelemetryEvent");
892+
if (!telemetryAlias || telemetryAlias.type.kind !== "or") return;
893+
894+
const structureMap = new Map(model.structures.map(s => [s.name, s]));
895+
896+
for (const item of telemetryAlias.type.items) {
897+
if (item.kind !== "reference") continue;
898+
const eventStruct = structureMap.get(item.name);
899+
if (!eventStruct) continue;
900+
901+
for (const prop of eventStruct.properties) {
902+
if (prop.name === "measurements" && prop.type.kind === "reference") {
903+
const measurementsStruct = structureMap.get(prop.type.name);
904+
if (!measurementsStruct) continue;
905+
for (const mp of measurementsStruct.properties) {
906+
if (mp.type.kind !== "base" || !["decimal", "integer", "uinteger"].includes(mp.type.name)) {
907+
throw new Error(
908+
`Telemetry measurements struct ${prop.type.name}.${mp.name} must be a numeric type ` +
909+
`(decimal/integer/uinteger), got ${mp.type.kind === "base" ? mp.type.name : mp.type.kind}`,
910+
);
911+
}
912+
}
913+
}
914+
}
915+
}
916+
}
917+
918+
validateTelemetryEvents();
919+
780920
interface GoType {
781921
name: string;
782922
needsPointer: boolean;

0 commit comments

Comments
 (0)