Files
romanett 36194f6a02 Avoid boxing and per-value copies on encoder, enum and server hot paths (#4715)
## Summary
Allocation audit of the per-message / per-value paths (binary codec,
built-in types, server request path, monitored items, client
notification dispatch) looking for boxing and avoidable per-value
copies. Measured with `GC.GetAllocatedBytesForCurrentThread` probe loops
on net10.0 **and net48**, and with `dotnet-trace` allocation traces of
the server load harness (#4707) for read/write/browse/subscription,
server and client side — a value type in the "allocated types" list is a
box.

Wire output and error behaviour are unchanged.

### Fixed
| Where | Cause | Frequency | Before → after |
|---|---|---|---|
| `Polyfills/System.IO.cs` (net48/netstandard2.0) |
`BinaryWriter.Write(ReadOnlySpan<byte>)` polyfill copied with
`ToArray()` — every string, array, ByteString, Guid write | per value |
32–64 B → 0; BrowseResponse×50 encode 15.3 → 10.9 KB |
| `EnumHelper` (net48/netstandard2.0) |
`EnumToInt32`/`Int32ToEnum`/`EnumToInt64` boxed outside
`NET8_0_OR_GREATER` (every enum field, `Variant.From<TEnum>`,
`EnumValue.From`) | per value | 24 B → 0 |
| `BinaryDecoder.ReadString` + stream raw reads (net48) | temp `byte[]`
per string, `BinaryReader.ReadBytes` | per value | BrowseResponse×50
decode 15.7 → 11.3 KB |
| `WriteGuid` / `ReadGuid` (all) | `ToByteArray()` / `byte[16]` | per
value (also Guid NodeIds) | 40 B → 0 each |
| `VariantHelper.TryCastTo<TEnum>` (`DataValue.GetValue<TEnum>`),
`EventRecordFieldReaders.GetEnum` | `Enum.ToObject` box | per call / per
event field | 24 B → 0 |
| `NodeManagerServiceDispatcher.GetManagerHandleAsync` | interface
enumerator per validated node | per node | Write-100: 151.8 → 148.5
KB/request |

### Cold paths (third commit)
- `Server.Auditing` write handler: `Convert.ToBoolean(object)` got a
boxed `Variant` (not `IConvertible`) and threw `InvalidCastException` on
any in-process write; now takes the boolean, `BadTypeMismatch` otherwise
(new test).
- `UpdateServerServiceLevel`: `Convert.ToByte(byte, IFormatProvider)`
bound to the object overload.
- Client publish-state / recovery-policy `HasFlag` (boxes on net48) →
`(x & F) == F`.
- `SequenceReader` big-endian float on net48/netstandard2.0 used
`Convert.ToSingle(int)` (numeric conversion) instead of reinterpreting
the bits.
- JSON enum strings: one `Enum.GetName` + `string.Concat` instead of
`IsDefined` + boxing `CoreUtils.Format`; same output (new test for the
`EnumValue` overload).

Left as is: `SamplingGroup` start-up `Convert.ToInt32(double, provider)`
(rewritten by #4707), `SecureChannelRenewed` formatting (transport files
in flight on the client perf branch), `Utils.IsEqual<T>` object path for
structs (cold callers; changing it would mix `CompareTo` and `Equals`
semantics), Machinery `HasFlag` (startup config).

### Not changed here
- `BinaryEncoder.GetNodeIdEncoding` `Convert.ToUInt32(object)` /
`Convert.ToByte(enum)` (72 B per NodeId encode, largest single item) and
`ChannelAsyncOperation.Complete` boxing the `int` result are handled on
the client performance branch.
- By design: `Variant` / `NodeId` store `ArrayOf<T>`, Guid, ByteString
identifiers boxed (`Variant.Copy` re-boxes RolePermissions during
permission checks).

## Tests
- New `BinaryCodecAllocationTests`: round trips in buffer and stream
decoder mode, truncation errors, allocation per call (< 1 B/call over
20k calls; the counter can be off by an allocation quantum when other
tests GC).
- `Enum.ToObject` equivalence for `TryCastTo` and
`EventRecordFieldReaders.GetEnum` across all underlying types.
- net10.0 (third commit): Types 11465, affected Server 87, Client
subscription 642 passed; net48 Types 11458 passed, `UA.slnx` 0 warnings.
- net10.0: Types 11464, Core.Encoders 2947, Server 8386 passed; Core
6197 passed, 1 unrelated
(`ActivityTraceFlags_Metrics_ShouldRecordMetricsOnly`, passes alone).
- net48: Types 11457, Core.Encoders 2947, EventRecordFieldReaders 25
passed; `UA.slnx` builds with 0 warnings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:35:57 +02:00
..