Skip to content

Reuse this-independent interface member instantiations - #64924

Open
Илия (777genius) wants to merge 1 commit into
microsoft:mainfrom
777genius:perf/this-independent-interface-members
Open

Илия (777genius) wants to merge 1 commit into
microsoft:mainfrom
777genius:perf/this-independent-interface-members

Conversation

@777genius

@777genius Илия (777genius) commented Oct 22, 2026 •

Copy link
Copy Markdown

Fixes #64925.

Analysis

Inherited interfaces with concrete mapped members can repeatedly materialize those members with descendant synthetic this arguments, even when the members do not depend on that argument. The package-free N200 reproduction below isolates that allocation cost.

This is separate from the base-search cost in #63555 and Mitch Fultz's #64737. That change is excluded from this PR and is used only as a separately attributed comparison input.

Fix

Record independence only for completed, nongeneric interfaces with nonempty all-interface declarations, no declared this, and suitable completed ancestry. Classes, mixed class/interface symbols, outer/local generics and unproved bases retain the existing path.

A mapped base must retain identity, have a finalized member table and no recorded recursive mapped-property error, and satisfy the existing instantiateTypeWithAlias no-instantiation predicate, including alias arguments. Property types remain lazy. For proved references, reuse the target's own synthetic this through the existing instantiation path.

The focused test covers captured/foreign/fluent this, merged and generic declarations, recursive lookup, mapped errors, conflicts, and call/index members. All three baselines were generated with the unmodified selected checker and match the candidate. Production changes are +35/-1 in two Go files; the remaining changes are the test and its generated baselines.

Copilot Checklist

I successfully ran the applicable command on the published candidate, and it completed without error:

  • npx hereby validate
  • npx hereby validate --api — N/A: no JavaScript API implementation changes. API tests also passed separately through test:all and the package test command.

PR template checklist

Validation

Linux x64; Go 1.27.2, Node 24.20.0, pinned npm 11.19.1. The complete CONTRIBUTING before-PR command list passed, including generation/build, one full test:all, package builds/tests and Go tidy/workspace checks. The compiler/module suite reported 168,194 tests and 2,139 skips. The additional validate run passed with zero lint issues and left the six-file patch unchanged.

CLA passed. Upstream CI and CodeQL await maintainer approval and have not executed.

Performance

Exact inputs:

Five alternating-order rounds, fresh processes, --singleThreaded --extendedDiagnostics --noEmit; GOMAXPROCS=4, GOMEMLIMIT=4GiB. Median compiler-reported values for mapped N200:

Compiler input Check time Memory used (K) Allocation count
Base 3.841 s 196783 808176
This PR alone 3.176 s 47828 255332
Base + #64737 0.596 s 196783 814506
Base + #64737 + this PR 0.138 s 47855 261666

All 60 runs succeeded, with matching diagnostics across variants. The inline control with #64737/stack checked in 0.139/0.146 s with equal type/instantiation counts. Five cold runs per variant of one owner compiler fixture gave base/PR medians 0.898/0.885 s and #64737/stack 0.907/0.933 s; each used an initially absent unique build-info file. These are local samples, not representative-project performance guarantees.

Package-free reproduction generator

Requirements: Go 1.27 and Node 24. From each selected compiler checkout, build an embedded-libraries executable with:

go -C ./tsc build -o /absolute/path/compiler ./cmd/tsc

Save the generator below as generate-repro.mts in an empty directory and run node generate-repro.mts mapped. Compile its output with /absolute/path/compiler --project tsconfig.json --singleThreaded --extendedDiagnostics --noEmit. Run the generator with inline for the control. The generated witness sources match the measured inputs byte-for-byte.

import assert from "node:assert/strict";
import { writeFileSync } from "node:fs";

const kind = process.argv[2] ?? "mapped";
assert.ok(kind === "mapped" || kind === "inline");
const count = 200;
const lines = [
  'type Descriptor<K extends string> = { readonly id: K; readonly value: number };',
  'type Capabilities<T extends { readonly id: string }> = { readonly [X in T as X["id"]]: X };',
];
for (let i = 0; i < count; i++) {
  const bases = [...new Set([Math.floor(i / 2), i - 1])]
    .filter(n => n >= 0 && n < i).map(n => `I${n}`);
  if (kind === "mapped") bases.push(`Capabilities<Descriptor<"p${i}">>`);
  lines.push(`interface I${i}${bases.length ? ` extends ${bases.join(", ")}` : ""} ${kind === "mapped" ? "{}" : `{ readonly p${i}: Descriptor<"p${i}">; }`}`);
}
lines.push(
  `interface Root extends ${Array.from({ length: count }, (_, i) => `I${i}`).join(", ")} {}`,
  "declare const root: Root;",
  'const first: Descriptor<"p0"> = root.p0;',
  `const last: Descriptor<"p${count - 1}"> = root.p${count - 1};`,
  "export { first, last };",
);
writeFileSync("witness.ts", lines.join("\n") + "\n");
writeFileSync("package.json", JSON.stringify({ type: "module" }));
writeFileSync("tsconfig.json", JSON.stringify({
  compilerOptions: { target: "ES2022", module: "NodeNext", strict: true, noEmit: true, types: [] },
  files: ["witness.ts"],
}));

The current-base private two-component stack also passed the unchanged N1000 compiler/runtime growth run in 17.55 s aggregate. This is supplemental consumer evidence; it does not establish standalone full-workload success or supported compiler adoption.

Related work

Open #64915 changes the same member-resolution path to lazy shared layouts. This patch targets the selected current main; it does not apply unchanged to that proposal. If #64915 lands first, the proof placement and performance benefit need re-evaluation on its lazy layout before integration. A separate five-round N200 comparison of exact #64915 head 10ba045ea637cf10d59aef3e1a107b8f50464db3 plus #64737 retained 70,507 types and used 64,441 K, versus 30,307 types and 47,833 K for current-main + #64737 + this PR. All 40 mapped/inline runs had empty diagnostics. This comparison does not establish the benefit of combining this patch with lazy layouts. #64873 separately avoids resolving mapped members for signature-only queries.

AI assistance

Codex assisted with investigating this specific Get Modular scaling problem, implementing the patch/tests, validation and review.

Avoid repeated synthetic-this member instantiation for proved completed nongeneric interfaces. Preserve captured and polymorphic this, generic interfaces, and unresolved bases.

Refs microsoft#63555
Copilot AI balanced review requested due to automatic review settings October 22, 2026 12:50
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Oct 22, 2026
@typescript-automation

Copy link
Copy Markdown
Contributor

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Oct 22, 2026
@typescript-automation typescript-automation Bot added For Backlog Bug PRs that fix a backlog bug For Uncommitted Bug PR for untriaged, rejected, closed or missing bug and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug For Backlog Bug PRs that fix a backlog bug labels Oct 22, 2026

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.

🔵 Needs a closer look

Changes to shared type-reference identity and recursive resolution invariants warrant compiler-maintainer review despite no confirmed defects.

No open issues 🎉

What changed in this PR

This PR reduces inherited-member instantiation costs in the native TypeScript checker by reusing synthetic this arguments when interface members are proven independent of them. It complements, but does not include, the base-search optimization in #64737.

Changes:

  • Adds conservative independence checks before reusing interface member instantiations.
  • Tracks mapped-member finalization to exclude incomplete resolution state.
  • Adds regression coverage and type, symbol, and JavaScript baselines.
File Description
tsc/​testdata/​tests/​cases/​compiler/​thisIndependentInterfaceMembers.ts Tests this preservation, generics, recursion, errors, and concrete members.
tsc/​testdata/​baselines/​reference/​compiler/​thisIndependentInterfaceMembers.types Records expected inferred types.
tsc/​testdata/​baselines/​reference/​compiler/​thisIndependentInterfaceMembers.symbols Records expected symbol bindings.
tsc/​testdata/​baselines/​reference/​compiler/​thisIndependentInterfaceMembers.js Records expected JavaScript output.
tsc/​internal/​checker/​types.go Adds independence and mapped-member finalization flags.
tsc/​internal/​checker/​checker.go Certifies independence and reuses eligible synthetic this arguments.

Review effort: Balanced

@777genius

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@777genius
Илия (777genius) marked this pull request as draft October 22, 2026 12:59
@777genius
Илия (777genius) marked this pull request as ready for review October 22, 2026 14:24
@jakebailey

Copy link
Copy Markdown
Member

Are there any real world codebases that hit this? Note how none of our benchmarks were affected by that other PR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Excess synthetic-this member allocations in nongeneric mapped interface inheritance

3 participants