Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 23 additions & 0 deletions packages/typespec-ts/src/context-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ class ContextManager {
public getContext<K extends ContextKey>(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.
Expand Down Expand Up @@ -99,3 +112,13 @@ export function useContext<K extends ContextKey>(key: K): Contexts[K] {
export function provideContext<K extends ContextKey>(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();
}
14 changes: 14 additions & 0 deletions packages/typespec-ts/src/framework/hooks/sdk-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ export const flattenPropertyModelMap: Map<SdkModelPropertyType, SdkModelType> =
*/
export const pagedModelsKeptPublic = new Set<SdkType>();

/**
* 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<Type, SdkServiceMethod<SdkHttpOperation>>;
types: Map<Type, SdkType>;
Expand Down
13 changes: 11 additions & 2 deletions packages/typespec-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
Loading