Skip to content

Excess synthetic-this member allocations in nongeneric mapped interface inheritance #64925

Open

Description

@777genius

Search terms

synthetic this, getTypeWithThisArgument, inherited interface members, mapped interface ancestry, member instantiation allocations, nongeneric interfaces.

Related but distinct: #63555 and #64737 address repeated base-graph traversal; #54260 discusses a different generic/variance cost. I did not find this specific completed-member optimization in those discussions or the duplicate searches. I reviewed the FAQ entries concerning mapped types, internal identity and instantiation-based inference; this report requests a resource improvement without changing language behavior.

Version and regression information

Reproduced with the source-built native compiler at current main 98add1bbed1c814e97457587c2da407dc3f78330 (reporting 7.1.0-dev), Linux x64, Go 1.27.2, Node 24.20.0. No regression boundary is claimed. The same reduced input was also investigated on the earlier official nightly 7.1.0-dev.20261021.1; current numbers below are from the specified current-main source build, not relabeled nightly numbers.

Code

The package-free fixture has 200 nongeneric interfaces plus a Root interface, shared inherited bases, concrete mapped capability descriptors and two typed property reads. It has no real or polymorphic this dependency. Generate it in an empty directory and compile with --project tsconfig.json --singleThreaded --extendedDiagnostics:

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"],
}));

Actual behavior

The mapped case repeatedly materializes inherited-member/type structures for descendant synthetic-this arguments. Five alternating samples on the same Linux x64 host, with GOMAXPROCS=4 and GOMEMLIMIT=4GiB, produced these medians. The separate #64737 input uses its exact head 3200b90a617bcf9ad26543b347cabad8ff0dad14 diff against PR base aa8149273b23401f0a79a5f0384c42de51888693, applied to the selected main revision above:

Compiler input Check time Memory used (compiler-reported K) Allocation count Types
Current main 3.841 s 196783 808176 70507
Current main + proposed member reuse 3.176 s 47828 255332 30307
Current main + separately attributed #64737 0.596 s 196783 814506 70507
Both changes 0.138 s 47855 261666 30307

Generic instantiations remain 31478. Diagnostics agree in every variant. The inline-property control keeps the inheritance graph while removing this mapped-member cost: with #64737 alone/with both changes it has the same 29305 types and 27468 instantiations, check medians 0.139/0.146 s and memory 46383K. These are local samples, not general compiler performance guarantees.

Expected behavior

For completed nongeneric interfaces whose members can be proved independent of the descendant synthetic-this argument, avoid the redundant member instantiations while preserving fluent, captured, foreign and generic this behavior. This is an optimization request; the fixture's diagnostics are already correct.

Additional information

Proposed implementation: #64924. Its focused semantic case covers captured/foreign/fluent this, generic and outer-generic declarations, merges, mapped aliases, recursive lookup and error cases. Its snapshots were independently generated with the unmodified current-main checker. The six-file candidate passed the owner's entire before-PR command list, including one complete test:all aggregate; it is separate from #64737 and does not claim to fix its base-search mechanism.

Codex assisted with investigating this specific scaling problem, preparing the candidate and running validation. No private compiler package or supported-release claim is proposed.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions