Skip to content

Optimize layer checker memory use - #327953

Draft
roblourens wants to merge 2 commits into
mainfrom
roblou/agents/optimize-layer-checker
Draft

Optimize layer checker memory use#327953
roblourens wants to merge 2 commits into
mainfrom
roblou/agents/optimize-layer-checker

Conversation

@roblourens

@roblourens roblourens commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

Reduce the custom layer checker's memory usage while preserving and strengthening the checks it performs.

The immediate heap-limit fix landed in #327942. This follow-up addresses why the checker needs so much memory in the first place:

  • avoid semantic symbol resolution for identifiers that cannot reference a forbidden type
  • exclude files covered by skip rules from the TypeScript program's root files
  • match rules against source-root-relative paths instead of absolute checkout paths
  • resolve import/export aliases and containing type declarations using public TypeScript APIs

The 8 GB heap limit remains in place as growth headroom.

Why the checker used so much memory

layersChecker.ts creates one TypeScript program for the full src/tsconfig.json source tree. Before this change, it then called getSymbolAtLocation for every identifier in every file matched by a layer rule.

On the source tree used during the investigation, that meant:

  • 8,712 source files in the program
  • 4,725 files traversed by the custom checker
  • 2,497,748 identifier symbol resolutions
  • only 9 identifiers whose text matched one of the currently forbidden names

Calling getSymbolAtLocation lazily expands and retains TypeScript's semantic graph. Resolving nearly 2.5 million symbols therefore pushed the checker past Node's default ~4 GB old-space limit even though the configured rules only care about a small set of native types and ipcMain.

This was gradual source-tree growth crossing a fixed threshold, not a single recent TypeScript regression. With fixed dependencies, the checker's peak footprint grew from approximately 3.55 GB in December to over 5 GB in July.

What changed

1. Limit semantic lookups to plausible candidates

The checker now resolves symbols only for:

  • identifiers whose text is a forbidden type name
  • local aliases of forbidden import/export names
  • property-access member names

The property-access case is important. A forbidden type may be inferred without its name appearing in the consuming file:

import { getService } from './service.js';
getService().run();

The checker still resolves run and follows its declaration container back to the forbidden interface. This preserves detection of inferred forbidden types instead of applying a naive text-only filter.

2. Handle aliases explicitly

A preliminary syntax pass records aliases such as:

import { ForbiddenService as Service } from './service.js';

Both the alias declaration and later Service references remain candidates. Alias symbols are resolved through TypeChecker.getAliasedSymbol, so renaming an import or export cannot bypass the rule.

3. Follow containing declarations through public APIs

The old implementation relied on TypeScript's private Symbol.parent field. The new implementation follows aliases and uses symbol declarations to find containing classes, interfaces, enums, and modules.

This is type-safe, uses public compiler APIs, and makes member detection explicit. It also required making two existing exceptions explicit:

  • the files that define the native service interfaces
  • the validated IPC wrapper that must call Electron's raw ipcMain internally

4. Exclude skipped roots before program creation

Files matched by a skip rule, currently tests, were previously excluded only from the final AST traversal. They were still root files in the TypeScript program and therefore still parsed and bound.

They are now removed from rootFileNames before createProgram. This is behavior-safe because those files were never checked. If a non-skipped source file imports one of them, TypeScript still includes it through normal module resolution; it is only removed as an unconditional root.

5. Match source-relative paths

Rules previously matched absolute file names. A checkout under a hidden directory could cause minimatch to reject every rule because wildcard matching does not consume leading dot-prefixed path segments by default.

Rules now match paths relative to the tsconfig directory, making behavior independent of the checkout's parent directories and path separators.

Why this is safe

The optimization preserves all routes by which the current rules can be violated:

Usage shape Detection after this change
Direct forbidden type name Exact-name candidate and semantic symbol resolution
Renamed import/export Alias collection plus getAliasedSymbol
Member of an inferred forbidden type Property-access candidate plus containing declaration traversal
Direct or namespace ipcMain access Exact/property-access candidate; validated wrapper explicitly exempted
Skipped test file Remains unchecked, as before
Skipped file imported by checked code Still included by TypeScript module resolution

The six layer-specific tsc invocations are unchanged and continue to enforce the broader browser, worker, Node.js, and Electron library boundaries.

Focused tests cover:

  • matching from a checkout beneath a hidden parent path
  • excluding skipped root files
  • detecting aliased forbidden types
  • detecting members of inferred forbidden types

Performance

Measured locally on the same source tree:

  • before: default heap OOM; approximately 5.25 GB peak footprint with an 8 GB heap
  • after: default heap succeeds; approximately 4.05 GB peak footprint

This removes roughly 1.2 GB of peak footprint while retaining the 8 GB CI allowance for future growth.

Validation

  • node --test build/lib/test/layersChecker.test.ts
  • npm --prefix build test -- --test-name-pattern='layersChecker' (239 tests)
  • npm --prefix build run typecheck
  • node build/eslint.ts build/lib/test/layersChecker.test.ts build/checker/layersChecker.ts
  • npm run valid-layers-check
  • npm run precommit

(Written by Copilot)

(Written by Copilot)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 29, 2026 03:21
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Screenshot Changes

Base: 073436dd Current: 4d26e990

Changed (6)

agentsVoice/voiceModeOnboarding/Voice Mode onboarding (panel)/Dark
Before After
before after
agentsVoice/voiceModeOnboarding/Voice Mode onboarding (panel)/Light
Before After
before after
agentsVoice/voiceModeOnboarding/Voice Mode onboarding (wide)/Dark
Before After
before after
agentsVoice/voiceModeOnboarding/Voice Mode onboarding (wide)/Light
Before After
before after
agentsVoice/voiceModeOnboarding/Voice Mode onboarding (narrow)/Dark
Before After
before after
agentsVoice/voiceModeOnboarding/Voice Mode onboarding (narrow)/Light
Before After
before after

1 insignificant change(s) omitted (≤20 px, Δ≤2). See CI logs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Reduces layersChecker memory usage by limiting semantic symbol resolution, excluding skipped roots from TS program creation, and matching rules relative to the configured source root, with new focused tests.

Changes:

  • Refactors layersChecker to collect violations programmatically (checkProgram, runLayerChecker) and to use relative-path rule matching (getRule)
  • Filters skipped rule roots out of ts.Program creation to reduce work and peak memory
  • Adds node:test-based coverage for rule matching, skipped roots, aliased forbidden types, and inferred forbidden-type member detection
Show a summary per file
File Description
build/checker/layersChecker.ts Refactors checker to use relative-path rule selection, reduce symbol resolution work, exclude skipped roots from the program, and centralize violation reporting.
build/lib/test/layersChecker.test.ts Adds targeted tests for the new rule matching, program root filtering, and forbidden-type detection behavior.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines +117 to 141
function checkFile(checker: ts.TypeChecker, sourceFile: ts.SourceFile, rule: IRule, violations: ILayerViolation[]): void {
const disallowedTypes = new Set(rule.disallowedTypes);
const candidateNames = new Set(disallowedTypes);

collectAliases(sourceFile);
checkNode(sourceFile);

function collectAliases(node: ts.Node): void {
if ((ts.isImportSpecifier(node) || ts.isExportSpecifier(node)) && disallowedTypes.has((node.propertyName ?? node.name).text)) {
candidateNames.add(node.name.text);
}

ts.forEachChild(node, collectAliases);
}

function checkNode(node: ts.Node): void {
if (node.kind !== ts.SyntaxKind.Identifier) {
if (!ts.isIdentifier(node)) {
return ts.forEachChild(node, checkNode); // recurse down
}

const checker = program.getTypeChecker();
if (!candidateNames.has(node.text) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)) {
return;
}

const symbol = checker.getSymbolAtLocation(node);
Comment on lines +233 to +235
for (const violation of violations) {
console.log(`[build/checker/layersChecker.ts]: Reference to type '${violation.type}' violates layer '${violation.target}' (${violation.fileName} (${violation.line},${violation.character}). Learn more about our source code organization at https://github.com/microsoft/vscode/wiki/Source-Code-Organization.`);
}
(Written by Copilot)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants