Skip to content

perf(check): flow access and entity-access calls read a flow without decoding it - #2194

Merged
ako merged 4 commits into
mainfrom
claude/trusting-wright-cqa3bb
Nov 13, 2026
Merged

ako merged 4 commits into
mainfrom
claude/trusting-wright-cqa3bb

Conversation

@ako

@ako ako commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Refs #2160 and #2188 (mirror of mendixlabs#1842): the per-run cost of check.

What was slow

Rerunning the reporter's experiment on v0.28.0, v0.31.0 and main (numbers on #2188) showed that exec of an update is now nearly flat in project size, but check has a cost per run that none of the three versions had reduced: about 2 s on a 5,373-unit project for a script of five statements, with a warm file cache.

Two whole-script checks run once per check, and each read every microflow of the project in full:

Check What it needs of each stored flow How it got it
CheckFlowAccess name, Excluded, allowed module roles ListMicroflows / ListNanoflows: every flow's body decoded and converted
CheckEntityAccessCalls (MDL-SEC22) name, Excluded, ApplyEntityAccess, the call activities every microflow and rule unmarshalled into a bson.D, including the flows that hold no call

The change

  • Backend.ListFlowAccess reads the three values from each stored flow's top level, over the units ListMicroflows and ListNanoflows cover and in their order. loadStoredFlows uses it when the backend has it and the decoded listings otherwise, which is what mock backends serve.
  • storedEAFlow reads the name and flags the same way and decodes a document only when its bytes contain Microflows$MicroflowCall or Microflows$RuleCall. A $Type is stored as its name, so a document with a call always contains one.
  • The old reader is kept as storedEAFlowDecoded, which the test compares against.

Evidence

  • The listing lists what the flow listings list: TestListFlowAccess_ListsWhatTheFlowListingsList on PedApp, after a script gives one microflow roles and excludes another. 18 microflows and 13 nanoflows, 22 with roles, 1 excluded; identical, in order. It fails if the fixture holds no flow with roles or none excluded.
  • The reader reads what decoding read: TestStoredEAFlow_ReadsWhatDecodingTheDocumentRead on every stored microflow and rule of the fixture, with and without calls, and on bytes that are not a document. Only the document with a call is decoded.
  • The fixture is thin for that (16 stored flows, one with a call), so the two readers were also compared, as a one-off not in this PR, on 5,632 flows of two generated projects, 301 of them with calls: identical.
  • The executor's access and check tests pass.
  • Against the previous binary: identical check --format json output for scripts of 5, 100 and 200 updates.
check at 5,373 units Before After
5 updates 1.98 s 0.91 s
100 updates 2.91 s 1.75 s
200 updates 3.85 s 2.83 s
20 new microflows 1.95 s 0.77 s

The first three rows are best of three with a warm file cache; the last is a single run.

Not done

  • Reading each unit file the first time. A first check after the project was copied took about 4 s; this does not change that.
  • loadStoredUses still unmarshals every page, snippet and navigation document to collect flow references. It was 0.03 s here because the test project has few pages; a page-heavy project will show it.

Generated by Claude Code

…decoding it

Two whole-script checks run once per `check`, and each read every
microflow of the project in full. CheckFlowAccess listed every microflow
and nanoflow, decoding and converting each flow's body, for its name,
Excluded flag and allowed module roles. CheckEntityAccessCalls
unmarshalled every microflow and rule for the name, two flags and the
call activities, including the flows that hold no call. A check of five
statements cost 2 s on a 5,373-unit project whatever the script held,
and had since v0.28.0 (mendixlabs#1842).

Backend.ListFlowAccess reads the three values from each stored flow's
top level, over the units ListMicroflows and ListNanoflows cover; a
backend without it is asked for the listings as before. storedEAFlow
reads the name and flags the same way and decodes a document only when
its bytes hold a call type's name, which a document with a call always
does.

Both are compared with what they replace: the listing on the fixture's
31 flows after a script gives one roles and excludes another, the
reader on every stored microflow and rule and on bytes that are not a
document.

At 5,373 units, best of three, warm: check of 5 updates 1.98 s ->
0.91 s, of 200 updates 3.85 s -> 2.83 s. Output identical.

Refs #2160

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82u4HRCKfX2BBvhgsNm
Both sides added a changelog entry at the same place; both are kept.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82u4HRCKfX2BBvhgsNm
Both sides added a changelog entry at the same place; both are kept.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82u4HRCKfX2BBvhgsNm
@ako
ako marked this pull request as ready for review November 12, 2026 23:46
The changelog conflicted for the third time with an entry added at the
same place. Main's entry is kept where it is and this branch's entry
moves below the other entries of this series, so the next entry added at
the top of the list no longer touches it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oK82u4HRCKfX2BBvhgsNm
@ako
ako merged commit 039d7d6 into main Nov 13, 2026
19 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.

2 participants