Skip to content

fix: avoid eager OptionSet.getOptions() dead work in analytics MetadataItemsHandler - #24876

Draft
jason-p-pickering wants to merge 3 commits into
masterfrom
fix/metadataitemshandler-eager-itemoptions-n1
Draft

fix: avoid eager OptionSet.getOptions() dead work in analytics MetadataItemsHandler#24876
jason-p-pickering wants to merge 3 commits into
masterfrom
fix/metadataitemshandler-eager-itemoptions-n1

Conversation

@jason-p-pickering

Copy link
Copy Markdown
Contributor

Summary

MetadataItemsHandler.handle() computed itemOptions (a full OptionSet.getOptions()
collection reload for every option-set-backed query item) unconditionally on every
request, but getOptionItems() only ever reads it on the empty-grid branch — the
grid-scoped optionsPresentInGrid is what's actually used whenever the grid has rows
(the common case). So on every request that returns any rows at all, the full option
set (e.g. ~10,000 rows for a large option set) was reloaded and immediately discarded.

Found while live-validating #24850 via a Glowroot trace on /analytics/events/aggregate
for a 10k-option ICD-10 data element: only 12 SQL executions (~187ms DB time) but a
~1.13s total transaction — ~945ms of unaccounted Java-side work from this eager-then-
discarded reload plus its in-memory stream/filter work, invisible to JDBC-only
instrumentation.

  • Wraps the itemOptions computation in a Supplier<Set<Option>>, only invoked inside
    getOptionItems()'s empty-grid branch.
  • No behavior change on the empty-grid path (still computes and uses the same result,
    just lazily).
  • Removes a full OptionSet collection reload from the has-results path entirely.

Not in scope (follow-up candidate): even the grid-scoped path resolves the entire
option set via optionSet.getOptions() and filters down to matching codes in Java —
pushing a code IN (:codes) filter into the query would avoid materializing unused
rows for very large option sets.

Test plan

  • Added MetadataItemsHandlerTest#handleDoesNotEagerlyReloadFullOptionSetWhenGridHasResults
    — spies on the query item's OptionSet, asserts getOptions() is never invoked
    when the grid already has rows. Verified red (fails on the eager call) before the
    fix, green after.
  • Added MetadataItemsHandlerTest#handleStillSurfacesFilterOptionsWhenGridHasNoResults
    — proves the empty-grid path (used to surface filter-referenced option metadata
    when a query returns zero rows) is unchanged by the fix. Mutation-tested by
    temporarily breaking the lazy path and confirming this test fails for the right
    reason.
  • Full dhis-service-analytics module test suite passes locally.

…taItemsHandler

MetadataItemsHandler.handle() computed itemOptions (a full OptionSet.getOptions()
collection reload per option-set-backed query item) unconditionally on every
request, but getOptionItems() only ever reads it on the empty-grid branch - the
grid-scoped optionsPresentInGrid is what's used whenever the grid has rows (the
common case). Make the computation lazy via a Supplier so it only runs when the
grid actually has no results, removing a full option-set reload (e.g. ~10k rows
for large option sets) from the hot has-results path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jason-p-pickering jason-p-pickering added the run-api-analytics-tests Enables analytics e2e tests label Aug 17, 2026
@jason-p-pickering
jason-p-pickering marked this pull request as draft August 17, 2026 07:01
@jason-p-pickering
jason-p-pickering marked this pull request as ready for review August 18, 2026 07:15
@jason-p-pickering
jason-p-pickering requested a review from a team August 18, 2026 07:15
@jason-p-pickering
jason-p-pickering marked this pull request as draft August 19, 2026 07:24
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

run-api-analytics-tests Enables analytics e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant