Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8e62425
feat: add program.currentStage and program.useCache for stage-aware c…
Jul 20, 2026
ab122d7
fix: only cache from linting stage onward (not during validating)
Jul 21, 2026
11aad48
fix: restrict HTTP operation cache to emitting stage only
Jul 22, 2026
3c85835
fix: add missing devDependencies for build ordering in http-canonical…
Jul 22, 2026
55f7d41
feat: add program.currentStage and program.useCache for stage-aware c…
Jul 22, 2026
dc508ff
test: minimal change to isolate Azure CI failure cause
Jul 22, 2026
2cb3820
perf: cache getHttpOperation during emitting stage
Jul 22, 2026
ae04583
fix: revert getHttpOperation caching, add ARM singleton repro tests
Jul 22, 2026
4289ed9
perf: cache getHttpOperation results during linting and emitting
Jul 22, 2026
b5a17a2
chore: remove unrelated changes
Jul 22, 2026
6bdf507
test: add getHttpOperation caching tests
Jul 22, 2026
f7a6ea8
fix: lint warnings and add @typespec/http changeset
Jul 22, 2026
dde38a3
Merge branch 'main' into iscai-msft-linter-perf-improvements
iscai-msft Jul 23, 2026
4136d89
perf: use module-level WeakMap cache for getHttpOperation (emitting o…
Jul 23, 2026
f89438a
Revert "perf: use module-level WeakMap cache for getHttpOperation (em…
Jul 23, 2026
94f770a
feat: invalidate useCache on type graph mutation
Jul 23, 2026
2ce428d
refactor: move useCache/invalidateCaches to experimental API
Jul 23, 2026
0bfe54b
refactor: remove currentStage from public Program interface
Jul 23, 2026
b4c6bd9
chore: remove accidental vitest build artifacts from PR
Jul 23, 2026
96ec6f1
refactor: surgical cache invalidation on mutation
Jul 23, 2026
f4ee80c
refactor: make invalidateCaches require types parameter
Jul 23, 2026
74951d4
refactor: remove invalidateCaches and bypass cache for unfinished types
Jul 23, 2026
6dfdab1
Merge remote-tracking branch 'upstream/main' into iscai-msft-linter-p…
Jul 24, 2026
1813e88
chore: merge upstream main and format
Jul 24, 2026
d32a767
refactor: move useCache logic from program.ts to cache.ts
Jul 24, 2026
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
7 changes: 7 additions & 0 deletions .chronus/changes/http-operation-cache-2026-7-20-16-43-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add `currentStage` property and `useCache` method to `Program` for stage-aware caching. `currentStage` tracks the compilation pipeline stage (parsing → checking → validating → linting → emitting), and `useCache` provides a generic caching mechanism that libraries can use to avoid redundant computation during later stages.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/http"
---

Cache `getHttpOperation` results during linting and emitting stages using `program.useCache()`. This eliminates redundant route resolution when multiple linter rules inspect the same operations, improving linter performance on large specs.
23 changes: 23 additions & 0 deletions packages/compiler/src/core/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ import {
TypeSpecScriptNode,
} from "./types.js";

/**
* The current stage of the compilation pipeline.
* Stages progress in order: parsing → checking → validating → linting → emitting.
*/
export type CompilationStage = "parsing" | "checking" | "validating" | "linting" | "emitting";

export interface Program {
compilerOptions: CompilerOptions;
/** @internal */
Expand Down Expand Up @@ -138,6 +144,11 @@ export interface Program {
* Project root. If a tsconfig was found/specified this is the directory for the tsconfig.json. Otherwise directory where the entrypoint is located.
*/
readonly projectRoot: string;

/** @internal */
setCurrentStage(stage: CompilationStage): void;
/** @internal */
readonly currentStage: CompilationStage;
}

interface EmitterRef {
Expand Down Expand Up @@ -192,6 +203,7 @@ export async function compile(
};
const timer = perf.startTimer();
// Emitter stage
program.setCurrentStage("emitting");
for (const emitter of program.emitters) {
// If in dry mode run and an emitter doesn't support it we have to skip it.
if (program.compilerOptions.dryRun && !emitter.library.definition?.capabilities?.dryRun) {
Expand Down Expand Up @@ -229,6 +241,8 @@ async function createProgram(
// eslint-disable-next-line prefer-const -- reassigned after source resolution
let suppressionTracker: SuppressionTracker | undefined;

let currentStage: CompilationStage = "parsing";
Comment thread
iscai-msft marked this conversation as resolved.

const logger = createLogger({ sink: host.logSink });
const tracer = createTracer(logger, { filter: options.trace });
const resolvedMain = await resolveTypeSpecEntrypoint(host, mainFile, reportDiagnostic);
Expand Down Expand Up @@ -260,6 +274,12 @@ async function createProgram(
get suppressionTracker() {
return suppressionTracker;
},
get currentStage() {
Comment thread
iscai-msft marked this conversation as resolved.
return currentStage;
},
setCurrentStage(stage: CompilationStage) {
currentStage = stage;
},
hasError() {
return error;
},
Expand Down Expand Up @@ -326,6 +346,7 @@ async function createProgram(
}

program.checker = createChecker(program, resolver);
currentStage = "checking";
runtimeStats.checker = perf.time(() => program.checker.checkProgram());

complexityStats.createdTypes = program.checker.stats.createdTypes;
Expand All @@ -336,6 +357,7 @@ async function createProgram(
}

// onValidate stage
currentStage = "validating";
await runValidators();

validateRequiredImports();
Expand All @@ -347,6 +369,7 @@ async function createProgram(
}

// Linter stage
currentStage = "linting";
const lintResult = await linter.lint();
runtimeStats.linter = lintResult.stats.runtime;
program.reportDiagnostics(lintResult.diagnostics);
Expand Down
43 changes: 43 additions & 0 deletions packages/compiler/src/experimental/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Program } from "../core/program.js";
import type { Type } from "../core/types.js";

/**
* Get a cached value for the given key, computing it if not already cached.
* Caching is only active from the "validating" stage onward and only for
* finished types. During "parsing" and "checking", decorators are still being
* applied. Unfinished types (during decorator application or inside mutators)
* are never cached because the type graph may not yet be in a stable state.
*
* @param program The program instance.
* @param key A unique symbol identifying this cache namespace.
* @param type The type to use as the cache key within this namespace.
* @param compute A function that computes the value if not cached.
* @returns The cached or freshly computed value.
*
* @experimental
*/
export function useCache<T>(program: Program, key: symbol, type: Type, compute: () => T): T {
const stage = program.currentStage;
// Only cache from "validating" onward. During "parsing" and "checking",
// decorators are still being applied and may not have finished setting up
// route options, filters, or other state that affects resolution. By
// "validating" all decorators have completed and types are fully resolved.
if (stage !== "validating" && stage !== "linting" && stage !== "emitting") {
return compute();
}
// Don't cache results for unfinished types. Types are unfinished during
// decorator application (including late template instantiation in the
// emitting stage) and inside mutators. Caching at those points risks
// storing results computed against an incomplete type graph.
if (!type.isFinished) {
return compute();
}
const map = program.stateMap(key);
const existing = map.get(type);
if (existing !== undefined) {
return existing as T;
}
const value = compute();
map.set(type, value);
return value;
}
1 change: 1 addition & 0 deletions packages/compiler/src/experimental/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { createSourceLoader as unsafe_createSourceLoader } from "../core/source-loader.js";
export { useCache as unsafe_useCache } from "./cache.js";
export {
MutableType as unsafe_MutableType,
Mutator as unsafe_Mutator,
Expand Down
8 changes: 8 additions & 0 deletions packages/http/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Operation,
Program,
} from "@typespec/compiler";
import { unsafe_useCache as useCache } from "@typespec/compiler/experimental";
import { getAuthenticationForOperation } from "./auth.js";
import { getAuthentication } from "./decorators.js";
import { isSharedRoute } from "./decorators/shared-route.js";
Expand All @@ -26,6 +27,8 @@ import {
RouteResolutionOptions,
} from "./types.js";

const httpOperationCacheKey = Symbol.for("@typespec/http.httpOperationCache");

/**
* Return the Http Operation details for a given TypeSpec operation.
* @param operation Operation
Expand All @@ -36,6 +39,11 @@ export function getHttpOperation(
operation: Operation,
options?: RouteResolutionOptions,
): [HttpOperation, readonly Diagnostic[]] {
if (!options) {
return useCache(program, httpOperationCacheKey, operation, () =>
getHttpOperationInternal(program, operation, options, new Map()),
);
}
return getHttpOperationInternal(program, operation, options, new Map());
}

Expand Down
Loading
Loading