Repository navigation
Dispatch operations to address space fragments without Streams - #2120
Merged
kevinherron merged 2 commits intoOct 12, 2026
Merged
kevinherron merged 2 commits into
kevinherron merged 2 commits into
Conversation
kevinherron
added this pull request to stack #2124
October 12, 2026 15:28
|
AddressSpaceComposite finds the fragment for each operation of a request, such as each ReadValueId of a Read, and GroupMapCollate groups the operations by fragment and puts the results back in request order. Both ran a Stream pipeline per call: a findFirst over the fragments for every lookup, an anyMatch over child filters for every nested composite test, and groupingBy plus three collects per request. In profiles of 100-node Reads these pipelines, not the attribute reads, were the largest per-operation cost after the access check. getAddressSpace and CompositeAddressSpaceFilter now use plain loops, and GroupMapCollate groups item indices in a LinkedHashMap and collates into an array. Routing is unchanged: the first fragment whose filter accepts an operation handles it, and an empty fragment is built only when none does. GroupMapCollate keeps its contract: each mapper is called once with a new mutable list of its group's items in request order, results come back in request order, a mapper that returns the wrong number of results is rejected, and a null key still throws NullPointerException. Groups are now visited in first-seen order rather than HashMap order, which was never specified. The new GroupMapCollateTest covers this contract, which had no tests.
read and write logged every operation at DEBUG, and evaluated the arguments first whatever the log level: getNodeId through the attribute filter chain, and an AttributeId lookup mapped through an Optional. At the default INFO level that work was thrown away for every attribute of every Read and Write, about 2% of server CPU in a 100-node Read benchmark. The arguments are now built only when DEBUG is enabled.
kevinherron
force-pushed
the
perf/address-space-routing-loops
branch
from
October 12, 2026 18:15
8f94593 to
0ca952d
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 3 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
Every service that takes a list of operations routes each one to the address space fragment that owns it, then puts the results back in request order. For a Read of 100 Variables that is 100 lookups plus the access check's internal Read of 400 more attributes. Both steps ran a
Streampipeline per call:AddressSpaceComposite.getAddressSpaceran afilter().findFirst()over the fragments for every operation, andCompositeAddressSpaceFilterran ananyMatchover its children for every nested test.GroupMapCollate.groupMapCollate, used by 33 call sites across the services, rangroupingByand threecollectcalls per request.In a profile of 10 clients reading batches of 100 Variables, taken with part 2 (#2119) applied, the routing lookups were about 12% of server CPU and
GroupMapCollate's stream collection about 11%, more than the attribute reads themselves. Separately,ManagedAddressSpace.readandwritebuilt their DEBUG log arguments for every operation even at INFO:getNodeId()through the attribute filter chain, and anAttributeIdlookup mapped through anOptional. That was about 2% of server CPU.Change
Two commits.
Dispatch operations to address space fragments without Streams.
getAddressSpaceand everyCompositeAddressSpaceFiltermethod use plain loops.GroupMapCollategroups item indices in aLinkedHashMapand collates the results into an array. Routing is unchanged: the first fragment whose filter accepts an operation handles it, and an empty fragment is built only when none does (the fix from #2098 is kept).GroupMapCollatekeeps its contract, which had no tests: each mapper is called once with a new mutable list of its group's items in request order; results come back in request order; a mapper that returns the wrong number of results is rejected, even if it changed the list it was given; a null key still throwsNullPointerException. One thing changes: groups are now visited in first-seen order rather thanHashMaporder, which was never specified.Build ManagedAddressSpace read and write log arguments only for DEBUG. The DEBUG messages are unchanged. With DEBUG off, a custom attribute filter no longer sees the incidental
getNodeId()call those arguments made.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
GroupMapCollateTest(sdk-core): results follow request order across interleaved groups, including a group larger than the initial index capacity; each mapper sees its group once, in order; a mapper that clears its input still has its results collated; a mapper that returns too few results is rejected; a null key is rejected.AddressSpaceCompositeRoutingTestcovers first-match routing and the lazily built empty fragment.Verification
GroupMapCollateTest: 6 tests pass.AddressSpaceCompositeRoutingTest: 8 tests pass.GroupMapCollatecallers for dependence on group order, list type, or key semantics. Its round-1 finding, that a mapper which mutated its input could defeat the result-count check, is fixed and covered by the two mutation tests.