Repository navigation
Key write access results by WriteValue identity - #2123
Merged
kevinherron merged 1 commit intoOct 12, 2026
Merged
kevinherron merged 1 commit into
kevinherron merged 1 commit into
Conversation
kevinherron
added this pull request to stack #2124
October 12, 2026 15:28
This was referenced Oct 12, 2026
|
DefaultAccessController.checkWriteAccess collected its results into a HashMap keyed by WriteValue, and DefaultAttributeServiceSet looked each result up with the same WriteValue. WriteValue.hashCode includes the DataValue being written, and Variant hashes arrays element by element, so every Write hashed its whole payload just to file the access result. For a Write of a 921,600-element Int32 array that was about 6% of the server's CPU time, and the cost grows with the size of every written array, ByteString and structure. The results are now in an IdentityHashMap. The service set looks them up with the instances it passed in, so it finds the same results. The difference for other callers of DefaultAccessController is documented: equal WriteValues each get their own entry, and a lookup with an equal but different instance finds nothing. Custom AccessController implementations are unaffected.
kevinherron
force-pushed
the
perf/write-access-identity-keys
branch
from
October 12, 2026 18:15
4a0ba4b to
54793e2
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 6 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
DefaultAccessController.checkWriteAccesscollected its results into aHashMapkeyed byWriteValue, andDefaultAttributeServiceSetlooked each result up with the sameWriteValue.WriteValue.hashCodeincludes theDataValuebeing written, andVarianthashes arrays element by element. So every Write hashed its whole payload just to file and find its access result: a Write of a 921,600-element Int32 array spent about 6% of the server's CPU time there, and the cost grows with the size of every written array, ByteString and structure.Change
checkWriteAccessreturns anIdentityHashMap. The service set looks results up with the instances it passed in, so it finds the same results. The Javadoc ofDefaultAccessController.checkWriteAccessdocuments what changes for other callers: equalWriteValues each get their own entry, and a lookup with an equal but different instance finds nothing.The only production caller is
DefaultAttributeServiceSet. CustomAccessControllerimplementations, which return their own maps, are unaffected, as are the read and other access checks:ReadAccessCacheand others may look up equal but differentReadValueIds, and those keys are cheap to hash.Results
Same benchmark setup as the rest of the stack, default configuration (the benchmark server stores
Integer[]; Writes decode toInteger[]).The 10-client result is stable: all six runs with this PR land between 270 and 297, against 226 to 233 before. Single-client sync HD Writes are bimodal on every build, each run landing near 70 to 90 or near 100 to 130, so the single-client median does not show a reliable difference.
Most of a large Write's remaining cost is decoding into a boxed
Integer[], which allocates about 18 MB ofIntegerobjects per Write and keeps the garbage collector busy.Tests
DefaultAccessControllerTest:checkWriteAccess_DoesNotHashTheWriteValuepasses aWriteValuewhosehashCodethrows. It fails on the parent commit.checkWriteAccess_ResultsAreKeyedByIdentitychecks two equalWriteValues get two entries, each found by its own instance, and an equal copy finds nothing.Follow-ups
checkCallAccesshas the same cost for large Method input arguments: its map is keyed byCallMethodRequest, which hashes itsinputArguments. It is not changed here because it was not measured; its callers would need the same review.Verification
DefaultAccessControllerTest: 38 tests pass; withOpcUaServerServiceSetsTest, 41.The whole stack
Requests per second, median of 6 measured runs from 3 interleaved rounds per build. open62541 C clients against the Milo benchmark server, default configuration, Ryzen 9 7950X with the server pinned to 16 CPUs that share physical cores with the clients' 16.
The single-client scalar rows vary by about ±10% between runs of the same build, and single-client HD Reads and sync HD Writes are bimodal (see parts 5 and 6), so read those rows as unchanged except where a part says otherwise. The 10-client rows are stable.
Single-client latency is the largest remaining gap to the open62541 C server. Each request crosses from the Netty event loop to the service executor and back, about 8 µs per request; that is not addressed in this stack.