perf: Analyze rules before running to limit context [DHIS2-21245]#23506
Open
enricocolasante wants to merge 8 commits intomasterfrom
Open
perf: Analyze rules before running to limit context [DHIS2-21245]#23506enricocolasante wants to merge 8 commits intomasterfrom
enricocolasante wants to merge 8 commits intomasterfrom
Conversation
88865f3 to
4451ac3
Compare
4734788 to
45ed06a
Compare
45ed06a to
709c8b0
Compare
926bbb0 to
93b0327
Compare
93b0327 to
bcf595a
Compare
teleivo
approved these changes
Apr 14, 2026
...acker/src/main/java/org/hisp/dhis/tracker/imports/programrule/DefaultProgramRuleService.java
Show resolved
Hide resolved
...racker/src/main/java/org/hisp/dhis/tracker/imports/programrule/engine/ProgramRuleEngine.java
Outdated
Show resolved
Hide resolved
| String groupUid = rs.getString("uid"); | ||
| String groupCode = rs.getString("code"); | ||
| String ouUid = rs.getString("ou_uid"); | ||
| orgUnitGroupData.computeIfAbsent(groupUid, k -> new ArrayList<>()).add(ouUid); |
Contributor
There was a problem hiding this comment.
maybe unlikely but what if a groups code matches anothers uid?
Contributor
Author
There was a problem hiding this comment.
Bad things will happen 😨
We need to work with the assumption that it will not happen because the grammar itself has no way to differentiate between a UID and a CODE.
If we have an orgUnitGroupA with UID h4w96yEMlzO and orgUnitGroupB with CODE h4w96yEMlzO, then a rule using a condition like d2:inOrgUnitGroup('h4w96yEMlzO') has no way to know what the user meant.
...acker/src/main/java/org/hisp/dhis/tracker/imports/programrule/DefaultProgramRuleService.java
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



DHIS2-21245 — Use rule engine analyzer to build
SupplementaryDataandRuleEngineContextonce per programSummary
This PR optimises the tracker import rule engine by building the
RuleEngineContextonce perprogram per request (instead of once per enrollment/event), gating expensive DB fetches behind
flags from the rule-engine-jvm analyzer, and replacing the org unit group lookup strategy with
one driven by the org units in the import payload.
Changes
Rule engine context built once per program
DefaultProgramRuleServicenow callsprogramRuleEngine.analyzeContextRequirements(rules, variables)once per program to obtain a
RuleContextRequirementsobject. TheRuleEngineContext(rules,variables, supplementary data, constants) is constructed once and reused for every enrollment and
event belonging to that program.
needsAllEventsgateRuleContextRequirements.getNeedsAllEvents()istrueonly when rules referencenewest-event / previous-event variables or
V{event_count}. Saved events are fetched from thedatabase only for programs where this flag is set, eliminating the DB call entirely for programs
that don't need it.
Supplementary data driven by payload org units
RuleContextRequirements.getNeedsOrgUnitGroups()is aboolean. Whentrue,SupplementaryDataProvidernow:preheat-resolved saved enrollments.
WHERE ou.uid = ANY(:orgUnitUids)— returning, for each org unit,the groups it belongs to (keyed by both group UID and group code).
The old approach extracted group identifiers from rule condition strings via regex and loaded each
group through
OrganisationUnitGroupService.getOrganisationUnitGroup(String), which delegates togetByUidand pulls full Hibernate entity graphs.Bug fix:
d2:inOrgUnitGroupnow resolves by UID or codePreviously,
d2:inOrgUnitGroup('identifier')silently returnedfalsefor any non-UIDidentifier. The old implementation called
getOrganisationUnitGroup(String)which delegates togetByUid, so only exact UID matches worked.The new JDBC query matches on both
oug.uidandoug.code, so rule authors can now writed2:inOrgUnitGroup('MY_GROUP_CODE')and it will work as expected.Performance
The load profile runs 4 concurrent import users, 15 s ramp + 180 s sustained, across three
import scenarios — 60 requests each, using custom TrackerTest version with different metadata:
lxAQ7Zs9VYRIpHINAT79UWuy2gU8kT1jFBefore the test runs, all program rule variables for ANC (
lxAQ7Zs9VYR) andChild Programme (
IpHINAT79UW) are patched to source typeDATAELEMENT_CURRENT_EVENT.This ensures
needsAllEvents = falsefor those programs, so the test exercises the fastpath introduced by this PR.
Three program rules using
d2:inOrgUnitGroupwere added to the SL demo DB to exercise the newsupplementary data path:
lxAQ7Zs9VYR)d2:inOrgUnitGroup('oRVt7g429ZO')SHOWWARNINGlxAQ7Zs9VYR)d2:inOrgUnitGroup('GGghZsfu7qV')SHOWWARNINGIpHINAT79UW)d2:inOrgUnitGroup('f25dqv3Y7Z0')SHOWWARNINGMNCH has no program rules, so the rule engine is skipped for it entirely. It serves as a control:
any latency change there reflects general import overhead, not rule engine behaviour.
Allocation (run 24345151024)
The 91% reduction in
DefaultProgramRuleServiceallocations comes from two sources:SupplementaryDataProviderno longer loads Hibernate entity graphs for org unit groups onevery call — a single JDBC query replaces repeated
getByUidcalls that each inflated anOrganisationUnitGroupwith all its associations.needsAllEventsgate eliminates thefetchSavedRuleEventsByEnrollmentDB fetch forprograms (ANC, Child Programme) whose rules don't reference event-history variables.
CPU (run 24348612905)
Notable decreases by DHIS2 leaf frame:
Calculator.evalFunctionTrackerBundle$$Lambda…testCalculator.evalFunctiondisappearing entirely from the candidate confirms theneedsAllEventsgate is working — the rule expression evaluator is no longer called for programs that don't need
saved events.
Latency (run 24345151024, run 24348612905)
ANC and Child Programme see consistent 25–47% p95 improvements. MNCH is essentially flat — it
has more complex rules and its saved-event fetch path is still exercised.
Open question: SL demo DB program rule variable type
The performance test patches program rule variables for ANC and Child Programme to
DATAELEMENT_CURRENT_EVENTat runtime to exercise theneedsAllEvents = falsepath. Should weupdate the SL demo DB to permanently set the existing program rule variables for those programs to
DATAELEMENT_CURRENT_EVENT, so that the test no longer needs to patch them and the demo datareflects a more typical real-world configuration?