Skip to content

perf(client-generator-ts,client-generator-js): read the search queue by index instead of shift() - #30199

Open
lpbonomi wants to merge 1 commit into
prisma:v7from
lpbonomi:generic-args-info-linear
Open

perf(client-generator-ts,client-generator-js): read the search queue by index instead of shift()#30199
lpbonomi wants to merge 1 commit into
prisma:v7from
lpbonomi:generic-args-info-linear

Conversation

@lpbonomi

@lpbonomi lpbonomi commented Sep 2, 2026

Copy link
Copy Markdown

Problem

prisma generate spends almost all of its time in GenericArgsInfo.typeNeedsGenericModelArg on schemas with a few hundred models. A CPU profile (node --cpu-prof) of prisma generate --generator client on a 316-model schema, Prisma 7.9.1, on Machine A below: 241 s of 272 s in that function; the same schema takes 165 s there without the profiler and 139 s on 7.10.0. #29308 reports the same shape (530 models, 235 s on 7.3.0).

The function runs a breadth-first search over the input types reachable from a type and consumes its queue with Array.prototype.shift(). The search from a create input visits most of the nested create inputs of the schema, so the queue grows to more than a hundred thousand items, and at that size V8's shift() is linear in the array length: the search becomes quadratic in its own queue. The traversal itself is already linear in the number of types and references; the negative result caches every visited type, and only a few dozen types of a real DMMF are positive because the *WhereInput family carries meta.source.

Change

Read the queue through an index instead of shift(). Same order, same visits, same cache behaviour; one line in each of the two generator packages, which carry the same file.

Correctness

  • 316-model schema (192 enums, 37k input types, 3,899 FieldRef sites): generated client byte-identical for all 326 files (diff -r against the released generator's output).
  • packages/internals/src/__tests__/__fixtures__/odoo.prisma (168 models) and synthetic schemas at 100, 200, 300 and 500 models: byte-identical at every size.
  • The existing GenericsArgsInfo.test.ts tests and both packages' suites pass unchanged (see below).

Performance

Generator time reported by the CLI ("Generated Prisma Client in ..."), measured by applying this same one-line change to the released prisma@7.9.1 bundle and running it against the unmodified bundle on the same schema and machine. The synthetic schemas come from the script below (N models, each with K relations to random other models, seeded); odoo.prisma is packages/internals/src/__tests__/__fixtures__/odoo.prisma.

Machine A — AWS r7i VM: Intel Xeon Platinum 8488C, 8 vCPU, 61 GB, Ubuntu 24.04 (glibc 2.39), Node 24.18.

schema models relations/model stock patched speedup peak RSS stock / patched
private schema 316 165 s 6.1 s 27× 2.06 / 2.02 GB
odoo.prisma fixture 168 73.1 s 3.6 s 20× 1606 / 1509 MB
synthetic 100 4 2.1 s 0.9 s 2.3× 558 / 550 MB
synthetic 200 4 8.0 s 2.0 s 4.0× 749 / 900 MB
synthetic 300 4 18.4 s 3.1 s 5.9× 1054 / 1028 MB
synthetic 500 4 55.0 s 5.6 s 9.8× 1668 / 1643 MB
synthetic 300 8 113.7 s 6.3 s 18.0× 1808 / 2225 MB

Machine B — MacBook Pro 16-inch 2023, Apple M3 Pro (6 performance + 6 efficiency cores), 36 GB, macOS 26.4.1.

Node 24.18 (V8 13.6), median of 3 runs:

schema models relations/model stock patched speedup peak RSS stock / patched
odoo.prisma fixture 168 19.5 s 1.97 s 9.9× 1778 / 1748 MB
synthetic 100 4 0.53 s 0.47 s 1.1× 581 / 582 MB
synthetic 200 4 1.35 s 1.00 s 1.4× 902 / 853 MB
synthetic 300 4 2.53 s 1.74 s 1.5× 1208 / 1209 MB
synthetic 500 4 14.5 s 2.85 s 5.1× 1846 / 1837 MB
synthetic 300 8 10.0 s 3.05 s 3.3× 1997 / 2038 MB

Node 22.22, single run:

schema models relations/model stock patched speedup
odoo.prisma fixture 168 34.3 s 2.00 s 17.1×
synthetic 500 4 13.7 s 2.85 s 4.8×
synthetic 300 8 42.6 s 3.21 s 13.3×

Machine dependence. The stock cost is the per-element cost of shift() once the queue passes the 128 KB large-object threshold, which V8 no longer left-trims: a memmove with an 8-byte overlap plus per-slot bookkeeping. That constant depends on the V8 version and on the CPU and libc. On Machine B, Node 24 (V8 13.6) is 2–4× faster than Node 22 on the same shapes; on Machine A, Node 22, 24 and 26 are within 15% of each other because its CPU/libc pair handles the 8-byte-overlap memmove at 2.9 GB/s against 51 GB/s for any other offset (rep movsb small-overlap path; with GLIBC_TUNABLES=glibc.cpu.x86_rep_movsb_threshold=1000000000 its 316-model generation takes 87 s instead of 165 s). A 4-vCPU Blacksmith CI runner (Node 26.8.1) generates the same private schema in 7.9 s stock. The change removes the shift() cost on every machine and version; the patched times are the rest of the generator, flat at 2–6 s across all of the above.

Peak RSS is within a few percent in every case but the densest synthetic schema on Machine A, where the default V8 heap grows 23% larger (the consumed part of the queue stays referenced until the search returns). That is GC slack, not retained data: with --max-old-space-size=1400 the same run finishes in the same time at 1837 MB, the stock figure, and it still completes under a 1000 MB cap.

Reproduction: synthetic schema generator
// node gen-schema.mjs <models> [relationsPerModel=4] [seed=1] > schema.prisma
const N = Number(process.argv[2] ?? 100);
const K = Number(process.argv[3] ?? 4);
let seed = Number(process.argv[4] ?? 1);
const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648);
const lines = [];
lines.push(`generator client {\n  provider = "prisma-client"\n  output   = "./out"\n}\n`);
lines.push(`datasource db {\n  provider = "postgresql"\n}\n`);
lines.push(`enum Kind {\n  ALPHA\n  BETA\n  GAMMA\n}\n`);
const back = Array.from({ length: N }, () => []);
const fwd = Array.from({ length: N }, () => []);
for (let i = 0; i < N; i++) {
  const targets = new Set();
  while (targets.size < Math.min(K, N - 1)) { const j = Math.floor(rand() * N); if (j !== i) targets.add(j); }
  for (const j of targets) {
    const name = `r${i}_${j}`;
    fwd[i].push(`  to${j} M${j}? @relation("${name}", fields: [to${j}Id], references: [id])\n  to${j}Id Int?`);
    back[j].push(`  from${i} M${i}[] @relation("${name}")`);
  }
}
for (let i = 0; i < N; i++) {
  lines.push(`model M${i} {\n  id        Int      @id @default(autoincrement())\n  name      String\n  count     Int      @default(0)\n  score     Float?\n  flag      Boolean  @default(false)\n  kind      Kind     @default(ALPHA)\n  meta      Json?\n  createdAt DateTime @default(now())\n${fwd[i].join("\n")}\n${back[i].join("\n")}\n}\n`);
}
process.stdout.write(lines.join("\n"));

With a prisma.config.ts next to it (datasource: { url: "postgresql://" }), run prisma generate with the released CLI and with a CLI that has this change, then diff -r the two out directories and compare the "Generated Prisma Client in ..." lines. The odoo.prisma fixture in this repository reproduces the effect without a synthetic schema.

Package suites

pnpm exec vitest run after turbo run build --filter=@prisma/client:

  • client-generator-js: 4 files, 20 tests, all pass (includes the end-to-end generation snapshot tests).
  • client-generator-ts: 4 files, 49 of 50 pass. The one failure, workerd - issue prisma#28073, fails identically on the unmodified v7 source in this environment: it needs query_compiler_fast_bg.sqlite.wasm from a built packages/cli.
  • GenericsArgsInfo.test.ts: 9 tests in each package, unchanged.

Related: #29308.

🤖 Generated with Claude Code

…by index instead of shift()

`GenericArgsInfo.typeNeedsGenericModelArg` runs a breadth-first search over
the input types reachable from a type and consumed its queue with
`Array.prototype.shift()`. On a schema with a few hundred models the search
from a create input visits most of the nested create inputs, and the queue
grows to more than a hundred thousand items; V8's `shift()` is linear in the
array length at that size, so the search is quadratic in its own queue.
A CPU profile of `prisma generate` on a 316-model schema put 241 s of 272 s
in that loop.

Reading the queue through an index keeps the same order and the same visits.
Generator time on the released 7.9.1 CLI with this change applied to its
bundle, generated output byte-identical:

  316-model schema (37k input types)        165 s -> 6 s
  odoo.prisma fixture (168 models)          73 s -> 3.6 s
  synthetic, N models x 4 relations each:
    100 models x 4 relations       2.1 s -> 0.9 s
    200 models x 4 relations       8.0 s -> 2.0 s
    300 models x 4 relations      18.4 s -> 3.1 s
    500 models x 4 relations      55.0 s -> 5.6 s
    300 models x 8 relations     113.7 s -> 6.3 s

Both generator packages carry the same file; both get the same change. The
existing tests and the generation snapshot tests pass unchanged.

Signed-off-by: luisopine <luis@tryopine.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Sep 2, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Team

Run ID: 603cd847-a988-448d-aad8-1009fab1c707

📥 Commits

Reviewing files that changed from the base of the PR and between 3dcc5b3 and a7d8ce2.

📒 Files selected for processing (2)
  • packages/client-generator-js/src/GenericsArgsInfo.ts
  • packages/client-generator-ts/src/GenericsArgsInfo.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Both client generators now process the typeNeedsGenericModelArg breadth-first queue with an incrementing index instead of shift(). Traversal order remains unchanged.

Changes

Generic model traversal

Layer / File(s) Summary
Indexed BFS queue traversal
packages/client-generator-js/src/GenericsArgsInfo.ts, packages/client-generator-ts/src/GenericsArgsInfo.ts
The queue uses a monotonically increasing index. The breadth-first traversal order remains unchanged.

Estimated code review effort: 1 (Trivial) | ~2 minutes

Merge Risk: ⚪ Minimal · up to a7d8c

This change replaces queue shifting with indexed reads in the client generators while preserving traversal behavior and generated output; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the performance change and names both affected generator packages. It accurately summarizes the main change from Array.prototype.shift() to indexed queue reads.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2 files.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

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