From 8c14b0f8538b4748beb3ad99f6a7368cc3667ba2 Mon Sep 17 00:00:00 2001 From: Jeff Fisher Date: Tue, 30 Jun 2026 16:07:42 -0500 Subject: [PATCH] Release typespec-ts emitter singleton state after $onEmit Clear the context manager and module-level SDK-type collections at the end of $onEmit so the previously emitted program graph can be collected between emits instead of lingering until the next emit overwrites it. This is bounded-to-one-program hygiene, not a fix for an unbounded leak (the real leak tracked in microsoft/typespec#11120 is upstream in the compiler's experimental mutators). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...r-contexts-after-emit-2026-6-30-15-44-0.md | 7 ++++++ packages/typespec-ts/src/context-manager.ts | 23 +++++++++++++++++++ .../src/framework/hooks/sdk-types.ts | 14 +++++++++++ packages/typespec-ts/src/index.ts | 13 +++++++++-- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .chronus/changes/ts-emitter-clear-contexts-after-emit-2026-6-30-15-44-0.md diff --git a/.chronus/changes/ts-emitter-clear-contexts-after-emit-2026-6-30-15-44-0.md b/.chronus/changes/ts-emitter-clear-contexts-after-emit-2026-6-30-15-44-0.md new file mode 100644 index 0000000000..7df2f590f1 --- /dev/null +++ b/.chronus/changes/ts-emitter-clear-contexts-after-emit-2026-6-30-15-44-0.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-ts" +--- + +Proactively release the emitter's process-wide singleton state at the end of `$onEmit` as a hygiene measure. The context manager and the module-level SDK-type collections otherwise keep the most recently emitted program graph reachable until the next emit overwrites them; clearing them after each emit lets that memory be collected sooner when many emits run in one process (test suites, watch mode). This is bounded-to-one-program cleanup and is not a fix for any unbounded leak. diff --git a/packages/typespec-ts/src/context-manager.ts b/packages/typespec-ts/src/context-manager.ts index 1001a6d9fd..7771653451 100644 --- a/packages/typespec-ts/src/context-manager.ts +++ b/packages/typespec-ts/src/context-manager.ts @@ -72,6 +72,19 @@ class ContextManager { public getContext(key: K): Contexts[K] | undefined { return this.contexts.get(key) as Contexts[K] | undefined; } + + /** + * Clears all stored contexts. + * + * The manager is a process-wide singleton, so the values provided during an + * emit (the `EmitContext`/`Program`, the TCGC `SdkContext`, the ts-morph + * `Project`, etc.) stay reachable from this map until the next emit overwrites + * them. Clearing at the end of an emit lets the whole previous program graph + * be collected instead of being retained until the following emit. + */ + public clearContexts(): void { + this.contexts.clear(); + } } // Expose the singleton instance of the context manager. @@ -99,3 +112,13 @@ export function useContext(key: K): Contexts[K] { export function provideContext(key: K, value: Contexts[K]): void { contextManager.setContext(key, value); } + +/** + * Clears all contexts held by the singleton context manager. Call this once an + * emit has finished writing its output so the retained program graph (compiler + * `EmitContext`/`Program`, TCGC `SdkContext`, ts-morph `Project`, binder, …) can + * be garbage collected instead of lingering until the next emit overwrites it. + */ +export function clearContexts(): void { + contextManager.clearContexts(); +} diff --git a/packages/typespec-ts/src/framework/hooks/sdk-types.ts b/packages/typespec-ts/src/framework/hooks/sdk-types.ts index 97647a6231..7219456e03 100644 --- a/packages/typespec-ts/src/framework/hooks/sdk-types.ts +++ b/packages/typespec-ts/src/framework/hooks/sdk-types.ts @@ -29,6 +29,20 @@ export const flattenPropertyModelMap: Map = */ export const pagedModelsKeptPublic = new Set(); +/** + * Releases the module-level state that accumulates while visiting a package's + * types. These collections are cleared at the start of every + * {@link visitPackageTypes} call, but because they live at module scope they + * otherwise keep the most recently emitted program's SDK types reachable until + * the next emit runs. Call this once an emit has fully finished so that state + * can be garbage collected. + */ +export function resetSdkTypesState(): void { + emitQueue.clear(); + flattenPropertyModelMap.clear(); + pagedModelsKeptPublic.clear(); +} + export interface SdkTypeContext { operations: Map>; types: Map; diff --git a/packages/typespec-ts/src/index.ts b/packages/typespec-ts/src/index.ts index 68a1aa716e..721afcbdc3 100644 --- a/packages/typespec-ts/src/index.ts +++ b/packages/typespec-ts/src/index.ts @@ -10,7 +10,7 @@ import { resolvePath, type CompilerHost, } from "@typespec/compiler"; -import { provideContext, useContext } from "./context-manager.js"; +import { clearContexts, provideContext, useContext } from "./context-manager.js"; import { buildRootIndex, buildSubClientIndexFile } from "./modular/build-root-index.js"; import { AzureCoreDependencies, @@ -43,7 +43,7 @@ import { } from "@azure-tools/typespec-client-generator-core"; import { Project } from "ts-morph"; import { provideBinder } from "./framework/hooks/binder.js"; -import { provideSdkTypes } from "./framework/hooks/sdk-types.js"; +import { provideSdkTypes, resetSdkTypesState } from "./framework/hooks/sdk-types.js"; import { loadStaticHelpers } from "./framework/load-static-helpers.js"; import { ClientModel, ClientOptions } from "./interfaces.js"; import { EmitterOptions } from "./lib.js"; @@ -188,6 +188,15 @@ export async function $onEmit(context: EmitContext) { await generateMetadataAndTest(dpgContext); + // Release the emitter's process-wide singleton state now that this emit has + // finished writing output. Without this, the context manager keeps the whole + // previous program graph (compiler `EmitContext`/`Program`, TCGC `SdkContext`, + // ts-morph `Project`, binder, …) reachable until the next emit overwrites it. + // Clearing here lets it be collected between emits, which matters for hosts + // that run many emits in one process (test suites, benchmarks, watch mode). + clearContexts(); + resetSdkTypesState(); + async function enrichDpgContext() { const generationPathDetail: GenerationDirDetail = await calculateGenerationDir(); dpgContext.generationPathDetail = generationPathDetail;