Skip to content

Perf: cache the ontology list, removing ~97% of /search response time#245

Open
mdorf wants to merge 2 commits into
developfrom
perf/ontology-list-cache
Open

Perf: cache the ontology list, removing ~97% of /search response time#245
mdorf wants to merge 2 commits into
developfrom
perf/ontology-list-cache

Conversation

@mdorf

@mdorf mdorf commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Production tracer data from #243 (deployed in v7.4.3) shows restricted_ontologies averages 876 ms of the 899 ms /search pipeline (~97%), and the search_scoped attribute revealed why both request styles pay it: the scoped path (98% of traffic) loads all ~1,200 ontologies with full attributes via ontology_objects_from_params and selects the requested ones in Ruby, while the site-wide path loads them all directly. Full baseline: #244 (comment).

This PR introduces OntologyListCache, a process-level TTL cache of the loaded ontology list, and points the shared helpers at it. Expected effect: /search average drops from ~900 ms to roughly the 15-60 ms the rest of the pipeline costs; annotator, recommender, mappings, analytics, and properties search shed the same per-request tax.

Addresses #244.

Design (agreed with @mdorf)

  • In-process, per-worker cache of the loaded Ontology objects; no Redis (goo objects don't marshal cleanly; rebuilding objects re-pays the materialization this removes).
  • TTL configurable: LinkedData::OntologiesAPI.settings.ontology_list_cache_ttl, default 300 s; 0 disables caching entirely (debug kill switch). Override per environment in the deployment config if needed.
  • Staleness contract: ontology add/remove and ACL/viewingRestriction changes take up to TTL per worker to appear. Per-user visibility (slice, custom sets, ACL via filter_access) is still computed per request; only the raw list is cached.
  • Defensive copies: every read returns a dup because get_term_search_query filters the array destructively. Cached objects are shared within a worker: bring on them is safe, attribute assignment is not (documented in the module).
  • Miss/expiry behavior = current behavior: same load call as today, executed once per TTL window instead of per request. Load failures propagate and are never cached.
  • The load includes the attribute union all consumers need (serialization defaults + access-control attrs + viewOf), which lets the scoped branch drop its second query and moves the site-wide view filter into Ruby.

Observability

OntologyListCache.all (per-request, ~0 ms on hit) and .load_ontologies (fires only on miss) are added to the #243 tracer registry, so cache behavior is directly visible in production segments; the registry regression test covers them automatically.

Evidence

  • Production baseline (18 h): restricted_ontologies 876 ms avg; scoped 962 ms / site-wide 878 ms transaction averages.
  • Local microbenchmark (test backend, small dataset, directional only): cold load ≈ old per-request cost; warm hit 0.008 ms.

Test plan

  • New test/lib/test_ontology_list_cache.rb (8 tests): caching across calls, defensive-copy isolation against destructive callers, TTL expiry (deterministic clock swap), TTL=0 bypass, default value, explicit invalidation, failed-load-not-cached, and a real-loader test asserting all consumer-needed attributes are preloaded.
  • Test harness invalidates the cache per test method (tests create/delete ontologies at will; test/test_case.rb).
  • Green locally: search (13 tests/147 assertions), search models, properties search, annotator, recommender, mappings, ontologies, slices, application helper, tracer registry.

Rollback / kill switches

Set ontology_list_cache_ttl = 0 (config + restart) to disable the cache without reverting; NEWRELIC_CUSTOM_TRACERS=off remains available for the tracers themselves.

mdorf added 2 commits July 24, 2026 14:36
Loading all ~1,200 ontologies from the backend on every request is
~97% of /search response time (876 ms of 899 ms avg in production
tracer data). The list changes on the scale of days, so cache it
per-process with a configurable TTL
(LinkedData::OntologiesAPI.settings.ontology_list_cache_ttl, default
300 s, 0 disables caching).

Returns a defensive copy on every read because some callers filter
the array destructively. Load failures propagate and are never
cached. Both cache methods are added to the New Relic tracer registry
so hits (~0 ms) and misses are visible in production segments. Tests
invalidate per test method since they create ontologies at will.
Both branches of restricted_ontologies reloaded the full ontology
list per request: the scoped path (98% of /search traffic) via
ontology_objects_from_params plus a second access-control query, the
site-wide path directly with a Goo viewOf filter. All of them now
read from OntologyListCache, which preloads the attribute union those
callers need; views are excluded in Ruby, and per-request
slice/user/ACL filtering is unchanged. The acronym/URI maps also read
from the cache instead of issuing their own per-request query.
@codecov-commenter

codecov-commenter commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.76%. Comparing base (4b26187) to head (5753d3c).

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #245      +/-   ##
===========================================
+ Coverage    78.60%   78.76%   +0.15%     
===========================================
  Files           67       68       +1     
  Lines         3754     3777      +23     
===========================================
+ Hits          2951     2975      +24     
+ Misses         803      802       -1     
Flag Coverage Δ
ag 78.76% <100.00%> (+0.15%) ⬆️
fs 78.76% <100.00%> (+0.15%) ⬆️
gd 78.76% <100.00%> (+0.15%) ⬆️
unittests 78.76% <100.00%> (+0.15%) ⬆️
vo 78.76% <100.00%> (+0.15%) ⬆️

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.

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