Skip to content

Perf/cql identifier dynamicvalues#135

Open
alexvangrootel wants to merge 2 commits into
WorldHealthOrganization:mainfrom
alexvangrootel:perf/cql-identifier-dynamicvalues
Open

Perf/cql identifier dynamicvalues#135
alexvangrootel wants to merge 2 commits into
WorldHealthOrganization:mainfrom
alexvangrootel:perf/cql-identifier-dynamicvalues

Conversation

@alexvangrootel

Copy link
Copy Markdown
Collaborator

We found a 45–130× speedup for PlanDefinition/$apply against this IG on HAPI's clinical-reasoning module.

On stock HAPI 8.8.0, $apply on any IMMZ PlanDefinition currently takes ~5–11 seconds; with this change it's ~40 ms. We came across this work when building an MCP, where we need a more interactive latency.

It looks like the six constant dynamicValue expressions in the shared PlanDef rulesets (status, intent, medication,
category.coding, priority
) use inline text/cql-expression literals. HAPI wraps each inline expression in a fresh library on every call, which the compiled-library cache can't key on. So it recompiles CQL→ELM from scratch, which is taking ~1 s per expression, per call. But the actual primary-library evaluation is ~5 ms.

Fix: reference those same constants via text/cql-identifier instead, pointing at named define statements in each PlanDefinition's primary *Logic library — the same mechanism condition and payload.contentString already use. The defines evaluate from the already-compiled library; no per-call compile.

Output is byte-identical. Same dynamicValue paths, same values; only expression.language changes.

Note that the real change is one file (input/fsh/rulesets/rulesets-plandefinition.fsh, ~14 lines). But each of the 138
*Logic.cql libraries needs the named defines appended so the identifier references resolve. Thats why there are so many file changes in this PR.

tools/add_dynamicvalue_defines.py generates these changes — reads fsh-generated/ after sushi build, appends the // @generated block per library. Commit c5d5659 is just that generator's output .

The generator is Python, placed under tools/ alongside the existing Node and Perl scripts.

Happy to discuss if there is a reason the inline form is preferred.

PlanDefinition.action.dynamicValue expressions in `text/cql-expression`
are compiled to ELM by the CQL translator on every $apply invocation.
The translator wraps each inline expression in a fresh anonymous library
with a generated name, so the compiled-library cache can never serve it.

This switches the constant dynamicValues (status, intent, medication,
category.coding, priority) from inline `text/cql-expression` literals to
`text/cql-identifier` references into named defines in the primary
library - the same mechanism `condition` and `payload.contentString`
already use - so they evaluate from the cached compiled-library context
with no per-call compile.

`tools/add_dynamicvalue_defines.py` reads the sushi-generated
PlanDefinition JSONs to discover which defines each *Logic library needs
and appends them. The regenerated libraries follow in the next commit.

No behavior change: the same dynamicValue paths are populated with the
same values; only the expression language changes.
…nical)

Output of `python3 tools/add_dynamicvalue_defines.py .` after `sushi build`.
Append-only; each block marked `// @generated dynamicValue constants`.
Review the substantive change in the previous commit.
@alexvangrootel alexvangrootel requested a review from litlfred May 21, 2026 10:24
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.

1 participant