Skip to content

Skip loadLinks for prerender-issued searches; remove dead per-instance cache#5128

Merged
habdelra merged 4 commits into
mainfrom
cs-11393-skip-loadlinks-prerender-searches
Jun 5, 2026
Merged

Skip loadLinks for prerender-issued searches; remove dead per-instance cache#5128
habdelra merged 4 commits into
mainfrom
cs-11393-skip-loadlinks-prerender-searches

Conversation

@habdelra

@habdelra habdelra commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Problem

When a _federated-search is issued from a prerender / indexing render context, the realm server runs the full loadLinks()populateQueryFields() relationship-assembly pass: for each query-backed linksTo / linksToMany field on each result it runs an extra SQL sub-query to build the relationships.{field}.data "umbrella," then expands the linked resources transitively into included[]. On the realms we measured, that pass is ~95% of the search handler's wall-clock (sql is ~3%). It's the root cause behind indexing searches being slow.

The catch: for a prerender search, all of that assembly is thrown away.

Why it's throwaway (how a prerender search is actually consumed)

A prerender search is one-shot (isLive=false) and the render store resolves cards from raw source, not from search response bodies:

  • The rendered card is loaded via card+source (the raw file). In a render store, Store.getCardInstance always fetches getSource(<url>.json) (Accept: application/vnd.card+source), which the realm serves as the raw local file. A raw source file has no computed query-field data.
  • A query-backed field therefore can't resolve from the parent's source — it falls through to a one-shot _federated-search. That search is the one this PR optimizes.
  • Only data[].id survives. SearchResource hands each result to the store, but when the card is actually rendered it is re-resolved through getCardInstancegetSource (the raw-source path above). The result's attributes and per-result query-field relationships are superseded by that re-fetch; the only thing consumed is which ids matched.
  • included[] was already skipped in prerender (omitIncluded). The remaining waste was the relationship-umbrella assembly inside loadLinks.

The change

Gate on the existing prerender flag (omitIncluded — the same signal that already suppressed included[]). When set, searchCardsUncoalesced skips the loadLinks / populateQueryFields pass entirely and returns the matching result identifiers + page meta (pristine attributes and static-link relationships from each index row; no query-field umbrellas, no included[]).

Live / external host-SPA searches are untouched — they run genuinely live searches and still receive fully-assembled compound documents.

Correctness boundary

There is one host path that consumes relationship.data ids without issuing a search — applySeed / captureQueryFieldSeedData. It reads the umbrella from a parent card document's own indexer-resolved relationships, i.e. from the card-document prerender path, which still runs loadLinks with skipQueryBackedExpansion and is left intact by this PR. It never sourced those umbrellas from a one-shot search response. So skipping loadLinks on the search removes nothing that path relies on.

Also removed (now-dead)

  • The job-scoped per-instance wire-format cache (job_scoped_instance_cache). Its read and write lived entirely inside loadLinks and only activated for job-scoped (indexer/prerender) searches — exactly the searches that no longer run loadLinks. It also never earned its keep independently: it measured zero hits (every read missed, because with included[] omitted a result URL rarely recurs within a job). Removed: the JobScopedInstanceCache class, the engine's cache members + call sites + instrumentation, the main.ts wiring, and its participation in JobsFinishedListener. The whole-doc JobScopedSearchCache and the JobsFinishedListener itself are untouched — the listener still evicts the search-doc cache on jobs_finished. A new migration drops the table and the sqlite schema is regenerated.
  • loadLinksForResource() — no callers.
  • The now-dead jobIdentity and search-path skipQueryBackedExpansion threading. The card-document GET/POST/PATCH path keeps skipQueryBackedExpansion (it still uses it).

Tests

  • The omitIncluded engine test now asserts the new contract: ids only, no query-field umbrella, no included[].
  • The live-search test (compound included[] for non-prerender callers) is unchanged and guards that the live path still assembles everything.
  • The JobsFinishedListener tests pass unchanged (it never depended on the per-instance cache); the per-instance-cache tests are removed.

Verifying the win

The mechanism removes provably-unused work rather than chasing a hotspot, so the direction holds regardless of magnitude. The before/after on realm:search-timing (the loadLinks stage collapsing toward ~0 for prerender requests) will be confirmed with a from-scratch reindex on staging after deploy.

…e cache

Prerender `_federated-search` no longer runs the post-SQL loadLinks /
populateQueryFields relationship-assembly pass. The prerender host re-resolves
every result from its raw card+source file and consumes only the matching
result ids, so the per-result query-field umbrellas and the transitive
included[] expansion were throwaway work — loadLinks was ~95% of the search
handler's wall-clock. Gated on the existing prerender flag (omitIncluded); live
host-SPA searches still receive fully-assembled compound documents.

Also removes the now-dead job-scoped per-instance wire-format cache
(job_scoped_instance_cache): its read/write lived inside loadLinks and only
activated for the job-scoped searches that no longer run it (and it measured
zero hits independently). Drops the class, its engine members + call sites,
main.ts wiring, its participation in JobsFinishedListener (the listener and the
whole-doc search cache are untouched), its tests, and the table (new migration
+ regenerated sqlite schema). Deletes dead loadLinksForResource() and the
now-dead jobIdentity / search-path skipQueryBackedExpansion threading; the
card-document path keeps skipQueryBackedExpansion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   1h 53m 59s ⏱️
2 936 tests 2 921 ✅ 15 💤 0 ❌
2 955 runs  2 940 ✅ 15 💤 0 ❌

Results for commit b16596c.

Realm Server Test Results

    1 files      1 suites   8m 37s ⏱️
1 551 tests 1 550 ✅ 1 💤 0 ❌
1 642 runs  1 641 ✅ 1 💤 0 ❌

Results for commit b16596c.

The migration body is only `pgm.sql(...)` template strings (no JS identifiers
that violate camelcase), so the generated `/* eslint-disable camelcase */`
directive is unused and `eslint --report-unused-disable-directives` rejects it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

This pull request optimizes prerender-driven _federated-search performance by skipping the expensive post-SQL relationship assembly (loadLinks() / populateQueryFields()) when the caller signals prerender behavior (omitIncluded). It also removes the job-scoped per-instance wire-format cache and its storage/wiring because that cache only operated inside the removed relationship-assembly path.

Changes:

  • Treat omitIncluded prerender searches as “no relationship assembly”: return the raw search rows (plus page meta) and skip loadLinks() entirely.
  • Remove the job-scoped per-instance cache implementation, its realm-server wiring, tests, and the Postgres/SQLite schema surface.
  • Update tests and internal diagnostics docs to reflect the prerender search contract and updated timing breakdown.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/runtime-common/search-utils.ts Removes job-identity threading from SearchOpts and timing output for federated search.
packages/runtime-common/realm.ts Stops threading job identity / query-expansion flags through the search path; keeps card-document prerender behavior.
packages/runtime-common/realm-index-query-engine.ts Skips loadLinks entirely when omitIncluded is set; deletes per-instance cache logic and loadLinksForResource().
packages/realm-server/handlers/handle-search.ts Sets omitIncluded for prerender searches and removes now-dead skipQueryBackedExpansion/jobIdentity search threading.
packages/realm-server/lib/jobs-finished-listener.ts Stops evicting the removed per-instance cache; only evicts the job-scoped search cache.
packages/realm-server/main.ts Removes JobScopedInstanceCache wiring and lifecycle management.
packages/realm-server/job-scoped-instance-cache.ts Deletes the per-instance cache implementation (no remaining callers).
packages/realm-server/tests/skip-query-backed-expansion-test.ts Updates omitIncluded expectations to “skip loadLinks entirely”; keeps card-document skip behavior coverage.
packages/realm-server/tests/per-instance-cache-test.ts Deletes tests that exercised the removed per-instance cache.
packages/realm-server/tests/job-scoped-instance-cache-test.ts Deletes tests for the removed JobScopedInstanceCache eviction surface.
packages/realm-server/tests/index.ts Removes deleted test files from the test manifest.
packages/postgres/migrations/1780627635271_drop-job-scoped-instance-cache.js Drops the job_scoped_instance_cache table (with reversible down).
packages/host/config/schema/1780627635271_schema.sql Regenerates SQLite schema snapshot to remove job_scoped_instance_cache.
.claude/skills/indexing-diagnostics/SKILL.md Updates diagnostics documentation for prerender search timing now that loadLinks is skipped and the per-instance cache is gone.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/runtime-common/realm-index-query-engine.ts Outdated
Comment thread packages/realm-server/tests/skip-query-backed-expansion-test.ts Outdated
Comment thread .claude/skills/indexing-diagnostics/SKILL.md Outdated
Comment thread .claude/skills/indexing-diagnostics/SKILL.md Outdated
habdelra and others added 2 commits June 5, 2026 04:09
The prerender search skips the loadLinks relationship-assembly pass but still
returns each result's pristine row (id + attributes + static-link
relationships) + page meta — it omits only the query-field umbrellas and
included[]. Reword the omitIncluded option comment, the test name, the
handle-search comment, and the indexing-diagnostics skill so they don't read as
"ids only", and drop the temporal "no longer" phrasing in the skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
State the prerender-search timeline as current behavior (sql carries most of
the handler because the post-SQL assembly doesn't run on that path) rather than
"now the common case, since the post-SQL assembly is gone".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@habdelra habdelra requested a review from a team June 5, 2026 08:33
@habdelra habdelra merged commit a42e25f into main Jun 5, 2026
73 checks passed
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.

3 participants