Skip to content

perf(schema-compiler): Deduplicate join tree edges by reference - #12560

Merged
paveltiunov merged 1 commit into
masterfrom
pavel-claude/tender-lamport-a9zjc3
Nov 2, 2026
Merged

paveltiunov merged 1 commit into
masterfrom
pavel-claude/tender-lamport-a9zjc3

Conversation

@paveltiunov

Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Issue Reference this PR resolves

Part of CORE-1177 (Pre-Aggregations page and refresh worker performance on large models).

Description of Changes Made

JoinGraph.buildJoinTreeForRoot deduplicated the joins of every join tree with R.uniq. R.uniq deep-compares the join edges, including their join definitions and SQL functions, and Ramda stringifies a function to compare it. Every edge comes from this.edges[\${from}-${to}`], one object per from-to pair with distinct from/to, so two edges are deep-equal only when they are the same object. Deduplicating with a Set` gives the same result, in the same first-occurrence order, without the deep comparison.

Why it matters: the refresh scheduler plans every scheduled pre-aggregation synchronously after each compile, and on large models this deep comparison dominated that pass. Until it finishes the refresh worker answers nothing else, including the Pre-Aggregations page's /cubejs-system/v1/pre-aggregations/partitions requests.

Measurements (generated model: 350 cubes of ~40 members, each joined to three neighbours, 330 year-partitioned rollups pulling measures from four joined cubes; the scheduler's per-pre-aggregation getSql(..., { preAggregationsOnly: true }) replayed for all of them):

before after
First scheduler pass after a compile, master 33.8 s 8.5 s
Same, Cloud runtime native on v1.8.15 30.0–31.0 s 6.2–6.5 s
Slowest single pre-aggregation 1.1–1.2 s 0.07–0.1 s

In a CPU profile of that pass, buildJoinTreeForRoot was 75% of the time, almost all of it Ramda equals / _functionName. Running the refresh worker on the same model, /livez stalled for up to 27 s with scheduled refresh on and under 50 ms with it off.

What remains is Tesseract planning: one native call to match each pre-aggregation and one to build its load SQL.

Testing

  • cubejs-schema-compiler unit suite: 62 suites, 1,257 tests pass. The join-path suites (join-alias-paths, duplicate-cube-joins, join-hints-cache-pollution, rollup-join-time-dimension-hints) cover the join trees this builds.
  • oxlint is clean on the changed file.

Generated by Claude Code

@paveltiunov
paveltiunov requested a review from a team as a code owner November 2, 2026 17:27
@github-actions github-actions Bot added the javascript Pull requests that update Javascript code label Nov 2, 2026
@claude

claude Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 43s —— View job


LGTM: 0 high, 0 medium, 1 low. Deduplicating by reference gives the same result as R.uniq here.

Review details
  • Read diff and JoinGraph.ts
  • Verify reference dedup is equivalent to R.uniq
  • Review threads
  • Post review

Correctness

The change is safe. joinsByPath (packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts:428) only ever returns objects from this.edges. That map has one entry per ${from}-${to} key, and each entry carries its own from/to (JoinGraph.ts:147-148). So two edges can only be deep-equal if they are the same object. A Set also keeps the first occurrence, so after the sortBy on index the order is unchanged. If an edge were ever missing, R.uniq would have collapsed the undefined entries and the Set does the same. buildJoin and findMultiplicationFactorFor don't depend on anything else about the array.

Findings

  • Low, JoinGraph.ts:389-391: the comment's second and third sentences describe the change, not the code. Inline suggestion posted with a 2-line version.

Other areas

  • Tests: the change doesn't alter behaviour, and the existing join-path suites cover these join trees. A dedicated test isn't needed.
  • Security: nothing to flag.
  • Docs: none needed. Users can't see this change except as faster pre-aggregation planning.
  • Planner: this is the JS join-graph path. Tesseract and the legacy planner both get join trees from it, so both benefit.

Review threads

The listing returned 0 of my own threads. Nothing was resolved or re-affirmed.

Copy link
Copy Markdown
Member Author

Drivers tests / build fails, and the failure isn't from this PR.

testing-drivers.Dockerfile fails at yarn install with Couldn't find any versions for "@cubejs-backend/testing" that matches "1.8.17". Since the v1.8.17 bump, cubejs-dbt-schema-extension dev-depends on @cubejs-backend/testing@1.8.17. The Dockerfile copies that package's package.json but not cubejs-testing's, so yarn resolves it from npm. The 1.8.17 release is still publishing there: @cubejs-client/playground@1.8.17 is already up, while @cubejs-backend/*@1.8.17 aren't yet.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Build / unit (24.x, 3.13) fails too, and that failure isn't from this PR either. It's a break on master.

Lerna tsc fails in packages/cubejs-model-schema: Module "@cubejs-backend/schema-compiler" has no exported member 'describeModelSchemas'. #12511 merged after the v1.8.17 bump but still pins "@cubejs-backend/schema-compiler": "1.8.16". Because the workspace package is now 1.8.17, yarn installs the published 1.8.16 from npm, which doesn't have the new export.

  • Master's own Build run on 929b0d3 fails the same step: unit (24.x), unit (26.x), and Build & Test :dev for Debian.
  • It won't clear when the release finishes publishing, because the pin keeps selecting 1.8.16.

There's no fix on master yet. This patch fixes it, and I've verified it locally on master:

--- a/packages/cubejs-model-schema/package.json
-  "version": "1.8.16",
+  "version": "1.8.17",
-    "@cubejs-backend/linter": "1.8.16",
-    "@cubejs-backend/schema-compiler": "1.8.16",
+    "@cubejs-backend/linter": "1.8.17",
+    "@cubejs-backend/schema-compiler": "1.8.17",

With it, yarn install --frozen-lockfile passes with no lockfile changes, schema-compiler links to the workspace package, and yarn tsc finishes with 0 errors. It belongs in its own PR against master rather than this one. Once it lands, I'll bring master into this branch so CI re-runs.


Generated by Claude Code

buildJoinTreeForRoot deduplicated the joins of every join tree with R.uniq,
which deep-compares join definitions, including their SQL functions (Ramda
stringifies a function to compare it). Every edge is one object per from-to
pair, so equal edges are the same object and a Set gives the same result.

On a 350-cube model with 330 rollups that join four cubes each, this was 75%
of the refresh scheduler's first pass after a compile, which runs synchronously
and blocks the refresh worker: 35s -> 8s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgekqAsnzjzDLeNzsQJE5Q
@paveltiunov
paveltiunov force-pushed the pavel-claude/tender-lamport-a9zjc3 branch from eeead5e to b4baf8a Compare November 2, 2026 20:20
@claude

claude Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @paveltiunov's task in 40s —— View job


LGTM: no issues found (0 high, 0 medium, 0 low)

Review details
  • Read diff
  • Verify the edge-identity invariant in JoinGraph.ts
  • Review threads
  • Post review

Correctness

The Set dedupe is equivalent to R.uniq here. It keeps the same output and the same first-occurrence order.

  • All edges come from this.edges. It is built once per compile at packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts:72, keyed by `${cube.name}-${join.name}` (:153), and Object.fromEntries keeps exactly one object per key.
  • joinsByPath (:427) only looks objects up in that map. It never clones them. Two edges with different keys differ in from/to, so they can never be deep-equal. That makes deduplicating by reference give the same result as the deep comparison.
  • A missing edge (undefined) is also deduplicated the same way by both approaches, so that edge case doesn't change either.
  • The trimmed comment at :389-390 is 2 lines and states the invariant a later edit would need to keep, for example if someone cloned edges in joinsByPath. No comment finding.

Performance

This removes Ramda's deep equals from every join-tree build, including the function stringification. Behaviour is unchanged otherwise.

Testing / docs

  • Existing join-path suites already cover how these join trees are built. A new test would only assert the same output, so none is needed.
  • Users can't notice any change apart from the speed-up, so no docs are needed.

Review threads

The listing returned 0 of my own threads (no prior threads), so I resolved 0 and re-affirmed 0.

CI

The Drivers tests / build and Build / unit failures are unrelated to this PR, as the author explained in the earlier comments (the 1.8.17 publish and the cubejs-model-schema version pin on master).

@codecov

codecov Bot commented Nov 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.73%. Comparing base (7dbef2f) to head (b4baf8a).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #12560   +/-   ##
=======================================
  Coverage   65.73%   65.73%           
=======================================
  Files         255      255           
  Lines       21507    21507           
  Branches     5157     5157           
=======================================
  Hits        14137    14137           
  Misses       6601     6601           
  Partials      769      769           
Flag Coverage Δ
cube-backend 65.73% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@paveltiunov
paveltiunov merged commit 26c703f into master Nov 2, 2026
111 checks passed
@paveltiunov
paveltiunov deleted the pavel-claude/tender-lamport-a9zjc3 branch November 2, 2026 21:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants