Repository navigation
Improve bulk-read throughput and reduce transport allocations - #20
Merged
Merged
Conversation
Large symbolic read batches were split twice: first at the PLC's negotiated item limit, then again to fit each logical request into a single transport frame. Transport fragmentation already supports larger requests, so the second split added avoidable request/response exchanges and cycle latency. A captured 10,000-signal cycle previously required 200 responses; it now requires 100 while preserving the PLC's 100-item request limit. Build read batches against the PLC item limit and the protocol's 16-bit PDU data-length bound. Reserve space for the legacy digest and growth of the integrity identifier assigned during send. Size ordinary batches with one serialization and retain the bounded payload search for unusually long addresses. Send GetMultiVariables requests as one logical PDU and leave fragmentation to the transport. Remove the TLS callback's additional fixed 960-byte splitting. Fragment records once at the negotiated COTP boundary, preserve the supplied data length, and set EOT only on the final fragment. Write batch sizing and the explicit write-enable gate remain unchanged. Add regression coverage for full and final partial read batches across transport frames, protocol length limits with long addresses, and payload and EOT preservation for both plain sends and TLS callback data. Validation: - 301 tests passed on .NET 8; 296 passed on .NET Framework 4.8. - Release builds passed for net48, net6.0, net8.0 and net9.0 with no warnings. - Read-only live validation covered 21,464 signals across 59 data blocks. Median cycle time decreased from 499 ms to 331 ms across 20 measured cycles. No value mismatches were found among 21,421 unchanged signals; 43 signals changed between comparison reads. No PLC writes were made.
Add opt-in ReadPipelineDepth (1-16, default 1) to keep a bounded number of read requests outstanding on the existing TCP/TLS connection. Pass complete bulk reads to the protocol session so high-level batching does not serialize the pipeline prematurely. Each request still respects the PLC item limit and protocol PDU length limit. Match responses by sequence number, validate integrity identifiers, and restore values and per-item errors to caller order even when responses arrive out of order. Abort the logical read and close the transport on transport or validation failure, preventing pending responses from being reused by a subsequent operation. Do not publish partial tag updates when the bulk operation fails. Reserve response dispatch before sending acknowledged exchanges and throughout pipelined reads. This prevents subscription polling from consuming foreground responses; encountered notifications remain queued for their subscribers. Other operations retain existing serialization and write safety gates. Remove per-field UInt32 VLQ scratch-array allocations with a one-byte fast path and stack-based encoding. Preserve wire bytes and support all target frameworks, including the .NET Framework stream API. Document configuration, per-CPU measurement, and refreshing communication resources after connecting, especially for legacy fallback item limits. Validation: - 329 tests passed on net8.0 and 323 on net48, with no skips. - Release driver builds passed for net48/net6.0/net8.0/net9.0 with no warnings or errors; staged whitespace checks passed. - Cover bounded windows, out-of-order responses, sequence wrap, partial batches, item failures, malformed responses, integrity/transport failures, option validation, and response-dispatch reservation. - Verify UInt32 VLQ boundary encodings and allocation-free modern path. - On a TLS CPU, 21,464 identical signals improved from a 338 ms median sequential cycle to 181 ms at depth two across 20 samples per mode. - On a legacy CPU, 7,787 signals measured 1,094 ms sequentially versus 1,136 ms at depth two across 10 samples; retain depth one there. - Stable live values matched, and foreground reads with tag/alarm subscriptions succeeded on the same connection on both CPUs. - Live validation performed reads only, without writes or alarm acknowledgements. Active alarm text was not exercised.
Decode bulk-read responses directly into ordered staging buffers instead of building per-response dictionaries. Validate item indices and duplicates, preserve per-item errors and missing values, and retain sequence/integrity checks before publishing the complete read result. Replace the asynchronous channel bridge with a monitor-based receive FIFO for the synchronous protocol worker. Preserve timeout, completion and receive-error behavior while removing per-response task and cancellation-source allocations. Remove the now-unused net48 channel dependency. Calculate serialized read-request lengths without encoding temporary payloads, preallocate request/result buffers, send the active MemoryStream buffer directly, and reuse immutable flag-zero scalar boolean values. Remove numeric and VLQ scratch arrays with targeted spans, stack storage and allocation-free IEEE bit conversion. Decode UTF-8 directly from exposed stream storage; use stack or pooled buffers for UTF-8 encoding. Preserve byte order, VLQ boundary behavior, stream origins and floating-point bit patterns. Deliver TLS plaintext and encrypted record slices through internal synchronous borrowed-buffer callbacks. Preserve owned exact-length buffers for public callbacks. Pool COTP fragments, send only their actual wire lengths, and validate active receive lengths so spare buffer capacity cannot become parsed payload. Fix CreateObject response parsing to honor the optional response-object flag. After long read runs, an integrity counter can begin with an object-like marker such as 0xAB; it must not be decoded as a variable-type list when no object is present. Keep diagnostic formatting safe for responses without objects. Validation: - 822 tests passed across net8.0 and net48, with no skips. - Release builds passed for net48/net6.0/net8.0/net9.0 without warnings or errors. - All 110 captured CreateObject response fixtures replayed successfully. - Read-only live checks passed on TLS and legacy PLCs, with zero stable value mismatches and 20/5 additional subscription create/read/dispose cycles. - The 21,464-signal depth-two workload allocated approximately 4.29 MB per cycle versus 6.93 MB before the span/buffer pass, a reduction of about 38%. - Latest read medians were 167.9 ms at depth two and 304.6 ms sequentially. Separate live runs vary; these timings do not establish a universal speedup. No PLC values were written or alarms acknowledged.
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.
Large symbolic reads previously added avoidable PLC exchanges by splitting logical requests at transport-frame boundaries. This change batches against the negotiated item limit and protocol length bound, lets the transport fragment once, and adds opt-in pipelining on the existing TCP/TLS connection.
Changes
ReadPipelineDepthfrom 1–16, defaulting to 1. Match responses by sequence number, validate integrity, and restore caller order even when responses arrive out of order. Close the transport on pipeline failure and publish no partial tag updates.0xABmust not be mistaken for a variable-type list.Validation
Live timings vary with PLC and host load, so these results do not establish a universal speedup. This PR contains the three committed driver changes and regression tests; local investigation artifacts are excluded.