Perf: Dedupe tool-definition JSON across telemetry/OTel sites#318477
Merged
Conversation
Each LLM round previously serialized the tool list multiple times (OTel inference span, OTel agent span, 'tools_available' event, GH 'request.options.tools' enhanced telemetry, and per-key 'request.option.tools' property), producing several independent multi-MB strings retained by buffered spans and telemetry queues. Add stringifyToolDefinitionsForOTel and stringifyToolsRawForTelemetry in messageFormatters: a WeakMap by-reference cache plus a single-slot content intern so identical tool lists across consecutive agent rounds collapse to one string instance. Wire all producer sites (chatMLFetcher GH telemetry + Object.entries loops, toolCallingLoop agent span and tools_available event, anthropic/gemini byok providers, genAiEvents inference event) through the helpers.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces extension host heap retention by deduplicating multi-megabyte tool-definition JSON strings across OTel spans and telemetry events in the Copilot extension. It introduces memoized stringifiers and routes multiple call sites through them to avoid repeated JSON.stringify(...) allocations of identical content.
Changes:
- Added
stringifyToolDefinitionsForOTelandstringifyToolsRawForTelemetrywith WeakMap memoization and single-slot content interning. - Updated OTel/telemetry producers (inference/agent spans,
tools_available, and GH telemetry events) to use the new helpers. - Added unit tests for the new helpers.
Show a summary per file
| File | Description |
|---|---|
| extensions/copilot/src/platform/otel/common/messageFormatters.ts | Adds memoized tool JSON stringifiers (OTel-normalized + raw telemetry). |
| extensions/copilot/src/platform/otel/common/test/messageFormatters.spec.ts | Adds tests for the new stringifiers. |
| extensions/copilot/src/platform/otel/common/index.ts | Re-exports the new stringifier helpers. |
| extensions/copilot/src/platform/otel/common/genAiEvents.ts | Uses raw stringifier for inference-details tool payload capture. |
| extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts | Routes OTel span attrs + GH telemetry tool payloads through the new helpers. |
| extensions/copilot/src/extension/intents/node/toolCallingLoop.ts | Uses normalized stringifier for agent-span tool definitions and tools_available. |
| extensions/copilot/src/extension/byok/vscode-node/geminiNativeProvider.ts | Uses normalized stringifier for tool defs span attribute. |
| extensions/copilot/src/extension/byok/vscode-node/anthropicProvider.ts | Uses normalized stringifier for tool defs span attribute. |
Copilot's findings
- Files reviewed: 8/8 changed files
- Comments generated: 2
Those properties are sent synchronously and released; they weren't among the retained copies. The other helper sites still dedupe the long-lived strings.
…h JSON.stringify fallback to preserve empty-array byte equality
zhichli
approved these changes
May 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Heap dumps of the extension host showed ~35 MB retained by ~10 duplicate copies of the same ~3.49 MB tool-definition JSON string. Each LLM round serialized the tool list multiple times — once for the OTel inference span, once for the OTel agent span, once for the
tools_availableevent, once for the GHrequest.options.toolsenhanced telemetry event, and once again from the per-keyObject.entries(request)loop intorequest.option.tools. Each call produced an independent multi-MBstringand was retained by buffered spans / telemetry queues.Fix
Add two memoized stringifiers in
messageFormattersand route every producer through them:stringifyToolDefinitionsForOTel(tools)— normalizes viatoToolDefinitionsthenJSON.stringify; used forgen_ai.tool.definitions.stringifyToolsRawForTelemetry(tools)— rawJSON.stringify(tools); used by legacy GH telemetry that needs exact request-shape preservation. MirrorsJSON.stringifysemantics: returnsundefinedonly when the input isundefined, and'[]'for an empty array.Both use:
Wired sites:
chatMLFetcher.ts— both GHrequest.options.toolscalls + bothObject.entries(request)loops (helper drives thetoolsbranch;?? 'undefined'preserves prior byte-for-byte output).toolCallingLoop.ts— agent-spangen_ai.tool.definitions+tools_availableevent.genAiEvents.ts— inference details event.byok/anthropicProvider.ts,byok/geminiNativeProvider.ts— per-request span attribute.Behavioral notes
toolCallingLoop'stools_availableevent is now suppressed when no nameable tools are present (previously emitted with possibly nameless entries). This matches OTel spec and the other call sites.genAiEvents.spec.tsassertingattrs[TOOL_DEFINITIONS] === JSON.stringify(tools)still passes — the raw helper returns exactly that.Tests
messageFormatters.spec.ts— added coverage for the helpers, including:JSON.stringify.JSON.stringifyconfirming repeated same-ref calls do not re-serialize, and equal-content distinct-ref calls re-serialize exactly once before interning.All 65 tests in
messageFormatters.spec.tspass.