Repository navigation
Build read access checks without Streams - #2121
Merged
kevinherron merged 1 commit intoOct 12, 2026
Merged
Conversation
kevinherron
added this pull request to stack #2124
October 12, 2026 15:28
|
DefaultAccessController checks every Read, and every sampling cycle on the default read-access policy, before the AddressSpace is read. For each check it built the internal request that fetches the access-control attributes, NodeClass, AccessRestrictions, AccessLevel and UserAccessLevel, with six Stream pipelines: one each for the pending results, the NodeIds, the attributes to read, the distinct NodeIds, a flatMap of NodeIds by attributes, and the final toMap. In a profile of 10 clients reading batches of 100 Variables those pipelines were about a tenth of the server's CPU time. checkReadAccess and DefaultAccessControlContext.readAccessControlAttributes now build the same lists with loops. The internal request is unchanged: the same ReadValueIds in the same order go through AddressSpaceManager.read, so routing, fragment grouping, each fragment's ReadContext, and the order in which each AddressSpace reads its attributes are all as before. Equal ReadValueIds still share one result entry, holding the last one's result.
kevinherron
force-pushed
the
perf/read-access-check-loops
branch
from
October 12, 2026 18:15
bbf43f5 to
817f3f4
Compare
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.
Part 4 of 6 (#2118 to #2123) of a stack of server performance changes, found by profiling the Milo server in the o6-automation/opcua-benchmarks suite.
Problem
DefaultAccessControllerchecks every Read, and every sampling cycle on the default read-access policy, before the address space is read. To decide, it reads NodeClass, AccessRestrictions, AccessLevel and UserAccessLevel for each node through an internalAddressSpaceManager.read. Building that internal request and the result map took sixStreampipelines per check: the pending results, the NodeIds, the attributes to read, the distinct NodeIds, aflatMapof NodeIds by attributes, and the finaltoMap. In a profile of 10 clients reading batches of 100 Variables, those pipelines were about a tenth of the server's CPU time.Change
checkReadAccessandDefaultAccessControlContext.readAccessControlAttributesbuild the same lists with loops. The behavior is intended to be identical:ACCESS_ATTRIBUTE_ORDERthat the check needs. Routing, fragment grouping, each fragment'sReadContext, and the order in which each address space reads its attributes are all unchanged.Collectors.toMap(..., (a, b) -> b)built it: a default-capacityHashMapfilled withmerge. Equal ReadValueIds still share one entry holding the last result, and the iteration order is the same.ReadAccessCacheiterates this map, so its order matters (see Follow-ups).The other access checks (Write, Browse, Call, references, nodes) are unchanged; they did not show up in the profiles.
An approach that was tried and dropped
An earlier version of this PR skipped the internal Read for nodes held by a
ManagedAddressSpacewith the defaultread, and read the attributes straight from the node. It was about as fast as this version. Three review rounds found cases where its different execution order could turn a denial into an allowance: applicationAttributeFilters with side effects, nodes replaced during the check, and statefulfilterReadimplementations. Reproducing the generic path's order exactly would have meant re-implementing it, so this version keeps the generic path and removes only the overhead around it.Results
Same benchmark setup as the rest of the stack: open62541 C clients, default configuration, Ryzen 9 7950X with the server on 16 CPUs, median of 6 runs.
Tests
The existing
DefaultAccessControllerTest,ReadAccessCacheTestandOpcUaServerServiceSetsTestcover the decisions and pass unchanged. No new test: the change has no intended behavior difference.Follow-ups
ReadAccessCachecollapses the access controller's results to (NodeId, AttributeId), ignoring index range and data encoding, and keeps whichever entry its iteration of the returnedHashMapvisits last. With stateful role answers, two ReadValueIds that differ only in index range can therefore cache different decisions depending on map order. That predates this stack. This PR keeps the map's order identical so it does not change which entry wins.Verification
DefaultAccessControllerTest,ReadAccessCacheTest,OpcUaServerServiceSetsTest: 50 tests pass.putchanged its iteration order, whichReadAccessCacheconsumes; this version builds it astoMapdid. Round 2 compared the two constructions against the JDK 17 and 21Collectorssources and in 7,710 generated cases per runtime, including resize boundaries and hash collisions.