Skip to content

Commit 1df4781

Browse files
xirzecCopilot
andauthored
Release typespec-ts emitter singleton state after $onEmit (#4787)
## What Clears the emitter's process-wide singleton state at the end of `$onEmit`: - `ContextManager.clearContexts()` — drops the stored `EmitContext`/`Program`, TCGC `SdkContext`, ts-morph `Project`, binder, etc. - `resetSdkTypesState()` — clears the module-level `emitQueue` / `flattenPropertyModelMap` / `pagedModelsKeptPublic` collections. ## Why These live at module scope, so after an emit they keep the most recently emitted program graph reachable until the *next* emit overwrites them. Clearing right after the emit finishes lets that memory be collected sooner when many emits run in one process (test suites, watch mode). ## Scope / honesty This is **bounded-to-one-program hygiene** — it is **not** a fix for an unbounded leak. The actual unbounded leak behind #4536 is upstream in the compiler's experimental mutators (`seen` cache pins every mutated program's type-graph for the process lifetime) and is tracked separately in microsoft/typespec#11120. This PR is just good housekeeping on the typespec-ts side and is independent of that fix. Changeset is marked `internal` accordingly. Related: #4536, microsoft/typespec#11120 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 20de12e commit 1df4781

4 files changed

Lines changed: 55 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: internal
3+
packages:
4+
- "@azure-tools/typespec-ts"
5+
---
6+
7+
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.

packages/typespec-ts/src/context-manager.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ class ContextManager {
7272
public getContext<K extends ContextKey>(key: K): Contexts[K] | undefined {
7373
return this.contexts.get(key) as Contexts[K] | undefined;
7474
}
75+
76+
/**
77+
* Clears all stored contexts.
78+
*
79+
* The manager is a process-wide singleton, so the values provided during an
80+
* emit (the `EmitContext`/`Program`, the TCGC `SdkContext`, the ts-morph
81+
* `Project`, etc.) stay reachable from this map until the next emit overwrites
82+
* them. Clearing at the end of an emit lets the whole previous program graph
83+
* be collected instead of being retained until the following emit.
84+
*/
85+
public clearContexts(): void {
86+
this.contexts.clear();
87+
}
7588
}
7689

7790
// Expose the singleton instance of the context manager.
@@ -99,3 +112,13 @@ export function useContext<K extends ContextKey>(key: K): Contexts[K] {
99112
export function provideContext<K extends ContextKey>(key: K, value: Contexts[K]): void {
100113
contextManager.setContext(key, value);
101114
}
115+
116+
/**
117+
* Clears all contexts held by the singleton context manager. Call this once an
118+
* emit has finished writing its output so the retained program graph (compiler
119+
* `EmitContext`/`Program`, TCGC `SdkContext`, ts-morph `Project`, binder, …) can
120+
* be garbage collected instead of lingering until the next emit overwrites it.
121+
*/
122+
export function clearContexts(): void {
123+
contextManager.clearContexts();
124+
}

packages/typespec-ts/src/framework/hooks/sdk-types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ export const flattenPropertyModelMap: Map<SdkModelPropertyType, SdkModelType> =
2929
*/
3030
export const pagedModelsKeptPublic = new Set<SdkType>();
3131

32+
/**
33+
* Releases the module-level state that accumulates while visiting a package's
34+
* types. These collections are cleared at the start of every
35+
* {@link visitPackageTypes} call, but because they live at module scope they
36+
* otherwise keep the most recently emitted program's SDK types reachable until
37+
* the next emit runs. Call this once an emit has fully finished so that state
38+
* can be garbage collected.
39+
*/
40+
export function resetSdkTypesState(): void {
41+
emitQueue.clear();
42+
flattenPropertyModelMap.clear();
43+
pagedModelsKeptPublic.clear();
44+
}
45+
3246
export interface SdkTypeContext {
3347
operations: Map<Type, SdkServiceMethod<SdkHttpOperation>>;
3448
types: Map<Type, SdkType>;

packages/typespec-ts/src/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
resolvePath,
1111
type CompilerHost,
1212
} from "@typespec/compiler";
13-
import { provideContext, useContext } from "./context-manager.js";
13+
import { clearContexts, provideContext, useContext } from "./context-manager.js";
1414
import { buildRootIndex, buildSubClientIndexFile } from "./modular/build-root-index.js";
1515
import {
1616
AzureCoreDependencies,
@@ -43,7 +43,7 @@ import {
4343
} from "@azure-tools/typespec-client-generator-core";
4444
import { Project } from "ts-morph";
4545
import { provideBinder } from "./framework/hooks/binder.js";
46-
import { provideSdkTypes } from "./framework/hooks/sdk-types.js";
46+
import { provideSdkTypes, resetSdkTypesState } from "./framework/hooks/sdk-types.js";
4747
import { loadStaticHelpers } from "./framework/load-static-helpers.js";
4848
import { ClientModel, ClientOptions } from "./interfaces.js";
4949
import { EmitterOptions } from "./lib.js";
@@ -188,6 +188,15 @@ export async function $onEmit(context: EmitContext) {
188188

189189
await generateMetadataAndTest(dpgContext);
190190

191+
// Release the emitter's process-wide singleton state now that this emit has
192+
// finished writing output. Without this, the context manager keeps the whole
193+
// previous program graph (compiler `EmitContext`/`Program`, TCGC `SdkContext`,
194+
// ts-morph `Project`, binder, …) reachable until the next emit overwrites it.
195+
// Clearing here lets it be collected between emits, which matters for hosts
196+
// that run many emits in one process (test suites, benchmarks, watch mode).
197+
clearContexts();
198+
resetSdkTypesState();
199+
191200
async function enrichDpgContext() {
192201
const generationPathDetail: GenerationDirDetail = await calculateGenerationDir();
193202
dpgContext.generationPathDetail = generationPathDetail;

0 commit comments

Comments
 (0)