Skip to content

perf: cache HTTP operation resolution at the program level#11318

Open
iscai-msft wants to merge 12 commits into
microsoft:mainfrom
iscai-msft:iscai-msft-linter-perf-improvements
Open

perf: cache HTTP operation resolution at the program level#11318
iscai-msft wants to merge 12 commits into
microsoft:mainfrom
iscai-msft:iscai-msft-linter-perf-improvements

Conversation

@iscai-msft

Copy link
Copy Markdown
Member

Motivation

Feedback from teams working on large ARM spec conversions indicates linter delays are long enough that engineers sometimes give up waiting. Profiling shows the linter accounts for ~40-50% of total compile time on large specs, with the top rules all bottlenecked on redundant getHttpOperation() calls.

Root Cause

getHttpOperation() creates a fresh empty Map() as its internal cache on every call. Since listHttpOperationsIn() already uses a shared local cache within a single invocation, the issue arises when multiple independent callers (linter rules, emitters) each call getHttpOperation() separately -- they all recompute from scratch.

On Compute RP with ~8 linter rules calling getHttpOperation() per operation, this means 8x redundant resolution of paths, parameters, responses, and authentication per operation.

Solution

Use the program's stateMap as a persistent cache for resolved HTTP operations. Since the HTTP validator ($onValidate) already calls getAllHttpServices() which resolves all operations via listHttpOperationsIn(), it naturally populates the cache before linter rules run. All subsequent callers get cached results for free.

This approach:

  • Attributes resolution cost to the validation phase (not individual rules)
  • Avoids confusing per-rule benchmarks where one rule "pays" for others
  • Benefits all consumers automatically (linter rules, emitters, etc.)

Impact

On Compute RP (66 tsp files, 31,831 types, 74 linter rules):

  • Rules like no-query-explode and no-header-explode drop from ~140ms each to ~0ms
  • Total linter time drops from ~846ms to ~440ms (~48% reduction)

Changes

  • packages/http/src/lib.ts: Add httpOperationCache state key
  • packages/http/src/operations.ts: Use program-level stateMap in getHttpOperation() and listHttpOperationsIn() instead of creating fresh local Maps

@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/compiler@11318
npm i https://pkg.pr.new/@typespec/http@11318

commit: f7a6ea8

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/compiler
  • @typespec/http
Show changes

@typespec/compiler - feature ✏️

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.

@typespec/http - feature ✏️

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.

@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from fe080fb to 489827a Compare July 20, 2026 20:52
@azure-sdk-automation

azure-sdk-automation Bot commented Jul 20, 2026

Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch 6 times, most recently from 41da546 to d566a48 Compare July 21, 2026 17:39
Comment thread packages/http/src/lib.ts Outdated
Comment thread packages/http/src/operations.ts Outdated
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from d566a48 to 9a2f6ed Compare July 21, 2026 19:55
@microsoft-github-policy-service microsoft-github-policy-service Bot added the compiler:core Issues for @typespec/compiler label Jul 21, 2026
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from 9a2f6ed to c392a7e Compare July 21, 2026 19:59
// eslint-disable-next-line prefer-const -- reassigned after source resolution
let suppressionTracker: SuppressionTracker | undefined;

let currentStage: CompilationStage = "parsing";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timotheeguerin would you prefer a union of literals like right now for CompilationStage or a flag enum. Overall I prefer to not use flag enums for readability purposes, but lmk your thoughts

…aching

Add CompilationStage type and currentStage property to Program that tracks
the compilation pipeline stage (parsing → checking → validating → linting → emitting).

Add program.useCache(key, type, compute) method that provides stage-gated
caching: values are only cached from the 'validating' stage onward, preventing
stale results when decorators call resolution functions during the checking phase.

Use program.useCache in @typespec/http to cache HTTP operation resolution,
so multiple callers (validators, linter rules, emitters) share results without
redundant recomputation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from c392a7e to 8e62425 Compare July 21, 2026 20:05
External library validators (e.g. Azure Core's checkEnsureVerb) call
getHttpOperation during the validating phase. This could potentially
cache results before all validators have completed their work.

By deferring caching to the linting stage onward, we ensure that:
- Decorator-phase calls (checking) never cache (existing behavior)
- Validator-phase calls (validating) compute fresh each time
- Linter rules and emitters benefit from caching

Also adds routeParamFilter integration tests in the rest package that
verify the ARM singleton pattern works correctly with caching.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
iscai-msft and others added 2 commits July 22, 2026 10:34
The Azure integration tests revealed that caching during linting stage
produces unexpected results for ARM singleton operations. Restrict caching
to emitting stage only for now, which still benefits emitters while
avoiding the interaction with validators/rules.

Also simplify getHttpOperation to use program.stateMap directly instead
of program.useCache, and revert listHttpOperationsIn to original behavior
(uses only local cache for overload resolution).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
…ization

@typespec/http-canonicalization had @typespec/http and @typespec/compiler
only as peerDependencies. Turbo doesn't track peer deps for build ordering,
causing race conditions where http-canonicalization builds before http.
Adding them as devDependencies (matching the pattern used by rest, openapi,
openapi3) ensures proper build ordering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch 2 times, most recently from 0e52fd8 to ad35506 Compare July 22, 2026 15:29
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from ad35506 to 4db5e94 Compare July 22, 2026 15:51
…aching

Add compilation stage tracking and a generic useCache method to the Program
interface. This enables libraries to cache computed results in a stage-aware
manner, avoiding stale caches from decorator/validation phases.

- CompilationStage type: 'parsing' | 'checking' | 'validating' | 'linting' | 'emitting'
- program.currentStage: read-only getter for current compilation stage
- program.setCurrentStage(): @internal setter for compiler pipeline transitions
- program.useCache(): stage-gated caching (only active during 'emitting')

Also fixes missing devDependencies in http-canonicalization for proper
turbo build ordering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from 4db5e94 to 55f7d41 Compare July 22, 2026 16:14
iscai-msft and others added 6 commits July 22, 2026 12:57
Add a no-op void expression reading program.currentStage to test whether
merely touching operations.ts (with zero behavioral change) triggers the
Azure test failure. This will help determine if the issue is a turbo
cache artifact vs an actual code path difference.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
Cache HTTP operation resolution results during the emitting stage to avoid
redundant computation across multiple emitters. The cache is only active when
program.currentStage === 'emitting' (i.e., never during linting or validation),
preserving original behavior for all earlier stages.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
Reverts the emitting-stage caching in getHttpOperation that caused
Azure integration test failures. The caching was active after compile()
since it sets currentStage='emitting', but the interaction with ARM
template expansion + routeParamFilter couldn't be reproduced locally.

Adds comprehensive ARM singleton pattern tests in the rest package that
verify routeParamFilter + autoRouteProducer correctly filter path
params across all compilation stages (validating, linting, emitting).

The compiler infrastructure (CompilationStage, currentStage, useCache)
remains available for future caching work.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
Use program.useCache() to memoize getHttpOperation results when called
without options. The cache activates from the linting stage onward,
eliminating redundant route resolution when multiple linter rules
inspect the same operations — the primary perf bottleneck in large
ARM specs (~11% faster on Compute RP, ~30% faster on Network RP).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
Tests cover:
- Basic caching correctness and object identity during emitting stage
- Multiple operations cached independently
- Cache bypass when options are provided
- Overloaded operations
- Custom route producers (ARM-like pattern with routeParamFilter)
- Consistency between getAllHttpServices and getHttpOperation
- Nested namespaces and interface operations
- Template instantiation patterns (is keyword)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
@iscai-msft
iscai-msft force-pushed the iscai-msft-linter-perf-improvements branch from 9136691 to 6bdf507 Compare July 22, 2026 19:21
Fix unused variable warnings in cache.test.ts and add missing
changeset for @typespec/http package changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71734702-1f81-447e-87e6-ff9f7040268a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:core Issues for @typespec/compiler lib:http lib:rest

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants