Skip to content

SharedMemory v2.3.4-rc.1

Pre-release
Pre-release

Choose a tag to compare

@justinstenning justinstenning released this 24 Oct 09:49
· 22 commits to main since this release
8539e0a

Release candidate for v2.3.4.

Bug fixes. No public members are added, removed or changed, and the target frameworks and dependencies are the same as 2.3.3.

Upgrading from 2.3.3

2.3.4 changes the size or layout of some element types in shared memory, and gives most types one layout on every runtime (#81). Depending on the type and the runtime, a 2.3.3 process reads and writes them at different offsets. If processes share a buffer of these types, upgrade them all together:

  • bool, char, and structures that hold them;
  • structures that hold a DateTime or a decimal;
  • a few rarer cases, listed in the README.

The README's element sizes section says which types change on which runtimes, what FastStructure<T>.Size means for them, and how to keep sharing bool and char data with 2.3.3.

2.3.3 and 2.3.4 processes open each other's buffers, and their RpcBuffers talk to each other. The #78 fix is on the receiving side only, so a 2.3.3 process still zeroes the last packet of a message whose length is a multiple of the packet size. #87's Compatibility section lists every other difference a 2.3.3 process can see.

Fixed

CircularBuffer

  • A buffer opened while its owner was still creating it could fail its first Write, which returned 0 after the full timeout (#77).

RpcBuffer

  • A request or response whose length was a multiple of the packet size, bufferCapacity - 32 bytes, arrived with its last packet zeroed (#78).
  • A send that found no room for a packet in time dropped the packet and still reported success (#52).
  • Code after await RemoteRequestAsync(...) could run on the RPC read thread, where a blocking RemoteRequest made from that code timed out and slow code stalled every response (#52).
  • One malformed packet could stop the channel reading for good, so every later request timed out (#52).
  • Timed-out and cancelled requests stayed in memory, and RpcStatistics miscounted (#52).
  • A send waiting for room kept waiting for up to 1 s after the owner closed the channel, holding up other sends. It now stops within about 50 ms (#52).
  • A restarted client could receive the late reply meant for its previous instance (#79).
  • A constructor that failed part-way, for example because another process still held the channel's name, left the name unusable until a garbage collection (#86).

SharedArray, FastStructure and array copies

  • Arrays of bool, char or structures holding them were copied with the wrong number of bytes. bool arrays could crash the process, char arrays came back half copied, and SharedArray<char> elements overlapped (#81, #57).
  • Structures holding a DateTime or a decimal could not be shared between runtimes that lay them out differently, such as .NET Framework and .NET 10, and arrays of them were copied with the wrong size on some runtimes (#81).
  • A garbage collection that moved an array during a read or write could make the copy use a stale address (#82).
  • SharedArray<T>.Write(T[], int) with a startIndex above 0 wrote at element 0, from the wrong part of the source array, and some valid calls threw ArgumentOutOfRangeException (#80).
  • SharedArray<T> sizes and offsets could overflow: an array over 2 GB got the wrong size, and a startIndex near Int32.MaxValue could read the buffer's header (#83).
  • List<T>.AddRange(sharedArray) threw on .NET 10 when the list already held items, and ArraySlice<T>.ToArray() and ToList() threw NotSupportedException (#83).
  • FastStructure.CopyTo and FromBytes wrote or read past the end of a buffer too small for the structure, and threw IndexOutOfRangeException for empty arrays (#84).

.NET Framework builds

  • The .NET Framework builds (net35 to net47) threw EntryPointNotFoundException at their first copy when loaded into .NET Core 3.0 or later, including .NET 5 to 10 (#74). The same cause lay behind #37 in Unity.
  • net35: a buffer of 2 GB or more reserved more than its own size, 4 GB and a byte for a 2 GB buffer, and views at 4 GB or more did not map (#85).

Behaviour changes

RpcBuffer

  • A send that cannot write a packet in time fails with Success = false, where it used to drop the packet and report success. A receiver drops a message that lost a packet.
  • timeoutMs covers the whole request: the send lock, the waits for room and the wait for the response. A reply still waits up to 1 s a packet for room. So does a request with a timeout of 0, which also waits for the send lock without limit.
  • Code after await RemoteRequestAsync(...) runs on the thread pool, never on the RPC read thread.
  • Dispose normally finishes within 100 ms, and requests still waiting for a response complete with Success = false. It no longer waits for running request handlers, so their replies are not sent.
  • The message ids a handler receives as msgId start at a random 64-bit value in each instance, instead of 1.
  • RpcStatistics.Timeouts counts requests that timed out and packets that found no room. DiscardedResponses counts replies that arrive after their request timed out, including every reply to a request sent with a timeout of 0.

CircularBuffer

  • new CircularBuffer(name), and so an RpcBuffer client, waits up to 1 s for an owner still creating the buffer, then throws InvalidOperationException. It used to return an instance that could not read or write.
  • A writer waiting for room checks the ring every 10 ms, instead of relying on a signal alone.

SharedArray and FastStructure

  • FastStructure.SizeOf<T>() and FastStructure<T>.Size return the size in shared memory: 1 byte for bool (was 4) and 2 for char (was 1). Blittable types are unchanged. CircularBuffer.Read<T> and Write<T> use this size too.
  • Single-structure reads and writes, such as the SharedArray<T> indexer, copy through FastStructure instead of the view accessor.
  • FastStructure.CopyTo and FromBytes throw ArgumentException when the structure does not fit in the buffer.
  • SharedArray<T>.Write(T[], int) waits at the write gate, as the indexer does, where it used to write at once. While another instance holds the read lock, it waits up to ReadWriteTimeout, then throws TimeoutException.
  • new SharedArray<T>(...) throws TypeInitializationException for a T with reference-type fields. 2.3.3 constructed some of these and failed at the first read or write.
  • ICollection<T>.CopyTo, called through the interface, copies every element into the destination from arrayIndex. SharedArray<T>.CopyTo(T[], int) called directly still starts reading at startIndex.
  • new SharedArray<T>(name, length) throws ArgumentOutOfRangeException for an array over Int32.MaxValue bytes, and new SharedArray<T>(name) for one over Int32.MaxValue elements. Write and CopyTo throw it when the range from startIndex does not fit.

Smaller changes

  • RpcBuffer: with a timeoutMs of Timeout.Infinite, the waits for the send lock, for room and for the response have no limit.
  • RpcBuffer: a request with a timeout of 0 is sent and returns Success = false without waiting for the response.
  • RpcBuffer: when a reply cannot be sent, its request waits out its timeout.
  • RpcBuffer: a blocking RemoteRequest still completes on the read thread, which runs none of its caller's code, unless a subclass overrides SendMessage.
  • RpcBuffer.Dispose waits at most about 1 s. If the read thread or a send has not let go of the buffers by then, it returns and they close in the background. DisposeFinished turns true once they have closed.
  • RpcBuffer.Dispose cuts short a reply being sent.
  • A new RpcBuffer on the same name, made straight after both ends are disposed, becomes the owner.
  • The RpcBuffer read thread checks for disposal every 100 ms, instead of every 500 ms.
  • Message ids count up by one from their random start. RpcStatistics.LastDiscardedResponseId holds these ids too.
  • RpcStatistics.Timeouts also counts reply packets that found no room, and requests that got no send lock.
  • FastStructure.SizeOf<T>() is the size of the layout 64-bit .NET 10 gives T: for example 32 bytes instead of 24 for an int, a Currency decimal and an int. Where T is copied field by field, that is not the size of T in the process's own memory.
  • The single-structure reads and writes that copy through FastStructure include BufferReadWrite.Read<T>(out T, long) and Write<T>(ref T, long), and CircularBuffer.Read<T>(out T) and Write<T>(ref T). They throw the same exception types as before for positions out of range.
  • ICollection<T>.CopyTo on a SharedArray<T> throws ArgumentException if the elements do not fit. It used to copy as many elements as the destination held, from the shared array's element arrayIndex.
  • Reflection reports SharedArray<T>.CopyTo(T[], int) as non-virtual.

Speed

Time taken, against 2.3.3 on one Windows machine:

RpcBuffer .NET 10 .NET Framework 4.8
Awaited requests, one thread about 20% less 6 to 8% less
Awaited requests, many threads about 80% less about 40% less
Synchronous requests within 3% 1 to 6% less
Two-process round trips 5 to 6% less 5 to 6% less
  • SharedArray<T> indexer: 6 to 14% less.
  • CircularBuffer ping-pong and array copies of blittable types: unchanged, within 4%.
  • Array copies of structures the library copies field by field: up to about twice as long as a blittable structure of the same size, in a quick check on .NET Framework 4.8.