Skip to content

Improve/Fix ViewableProperty#591

Open
Iliya-usov wants to merge 4 commits intomasterfrom
usov/viewable-property
Open

Improve/Fix ViewableProperty#591
Iliya-usov wants to merge 4 commits intomasterfrom
usov/viewable-property

Conversation

@Iliya-usov
Copy link
Collaborator

  • Implement Memory.IsReadWriteAtomic
  • Make maybe return consistent value with hasValue
  • Fix tearing issues with myValue if T is long struct
  • Enforce memory ordering between myValue = value and myChange.Fire(value);

- Make maybe return consistent value with hasValue

- Fix tearing issues with myValue if T is long struct

- Enforce memory ordering between myValue = value and myChange.Fire(value);
@Iliya-usov Iliya-usov requested a review from ForNeVeR February 16, 2026 21:13
## Memory.SizeOf<T>()

Add a public unconstrained `SizeOf<T>()` method that returns the managed
size of any type. Results are cached in a static generic class
`SizeOfCache<T>`.

Implementation per TFM:
- net5+: delegates to `Unsafe.SizeOf<T>()`
- net472: emits a `DynamicMethod` with the `sizeof` IL opcode
- netstandard2.0: same DynamicMethod approach, enabled by adding a
  conditional `System.Reflection.Emit.Lightweight` package reference

## IsReadWriteAtomicCache<T> cleanup

Replace the old approximate field-sum size calculation (which used
`Marshal.SizeOf` for primitives and recursive field enumeration) with
exact `SizeOf<T>()`. This fixes edge cases where padding/alignment
caused the old heuristic to return incorrect sizes (e.g.
`PaddedSequentialStruct` field sum = 8 but actual size = 12).

The Pack check `(Pack != 0 && Pack != MaxAtomicSize)` is intentionally
conservative: it rejects any non-default Pack that doesn't match the
atomic word size, even if the Pack provides sufficient alignment (e.g.
Pack=16). This avoids false positives for atomicity on architectures
where misaligned access is non-atomic (ARM) or may cross cache lines
(x86/x64).

## MaxAtomicSize simplification

Use `IntPtr.Size` directly instead of checking `Is64BitOperatingSystem`,
since 8-byte atomic reads are not safe in 32-bit processes even on a
64-bit OS.

## Tests (157 total)

- Primitives, reference types, enums, pointer types
- Struct layout edge cases: padding, field ordering, explicit layout
  with large offsets and overlapping fields (unions)
- Pack variations: Pack=1/2/4/8/16 with various field combinations
- StructLayout.Size attribute: forced sizes at boundary values
- Generic structs: Wrapper<T>, Pair<T1,T2>, Triple<T1,T2,T3> with
  value types, reference types, nested generics, constrained generics
- BCL generics: Nullable<T>, KeyValuePair<K,V>, ValueTuple
- SizeOf<T>() for all of the above including reference types
- Consistency checks between SizeOf and IsReadWriteAtomic
- Endianness documentation: tests confirming atomicity is determined
  by size and alignment, not byte ordering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves ViewableProperty<T> correctness under concurrency by adding a type-based atomicity check in Memory, using it to avoid torn reads for non-atomic value types, and strengthening memory ordering between value updates and change notifications.

Changes:

  • Add Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>() (with caching) and wire required dependency for netstandard2.0.
  • Update ViewableProperty<T> to store value/hasValue separately, guard non-atomic reads with a lock, and add an explicit memory barrier before firing change notifications.
  • Add extensive unit tests covering SizeOf<T>() and IsReadWriteAtomic<T>() across many type/layout scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.

File Description
rd-net/Test.Lifetimes/Utils/MemoryTest.cs New test suite for Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>() across primitives, structs, layout/pack edge cases
rd-net/Lifetimes/Util/Memory.cs Adds SizeOf<T>() and IsReadWriteAtomic<T>() implementations with caching and runtime-specific sizing strategy
rd-net/Lifetimes/Lifetimes.csproj Adds System.Reflection.Emit.Lightweight for netstandard2.0 to support DynamicMethod sizing path
rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs Uses atomicity check + locking to avoid torn reads and adds ordering barrier between state update and Fire()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +423 to +429
Assert.IsTrue(Memory.IsReadWriteAtomic<long>());
}

[Test]
public void IsReadWriteAtomic_ULong_ReturnsTrue()
{
Assert.IsTrue(Memory.IsReadWriteAtomic<ulong>());
Comment on lines +423 to +429
Assert.IsTrue(Memory.IsReadWriteAtomic<long>());
}

[Test]
public void IsReadWriteAtomic_ULong_ReturnsTrue()
{
Assert.IsTrue(Memory.IsReadWriteAtomic<ulong>());
[Test]
public void IsReadWriteAtomic_Double_ReturnsTrue()
{
Assert.IsTrue(Memory.IsReadWriteAtomic<double>());
Comment on lines +711 to +719
// DateTime is 8 bytes (single ulong internally)
Assert.IsTrue(Memory.IsReadWriteAtomic<DateTime>());
}

[Test]
public void IsReadWriteAtomic_TimeSpan_ReturnsTrue()
{
// TimeSpan is 8 bytes (single long internally)
Assert.IsTrue(Memory.IsReadWriteAtomic<TimeSpan>());
Comment on lines +768 to +770
// OptimalSequentialStruct: int + short + byte + 1pad = 8 bytes → atomic on 64-bit
// PaddedSequentialStruct: short + 2pad + int + byte + 3pad = 12 bytes → non-atomic
Assert.IsTrue(Memory.IsReadWriteAtomic<OptimalSequentialStruct>());
Comment on lines +882 to +883
// Size=8 <= MaxAtomicSize on 64-bit, so this is atomic.
Assert.IsTrue(Memory.IsReadWriteAtomic<Pack16IntShort>());

// Auto layout, mixed types: int(4) + byte(1) + long(8) = 13 bytes of fields
// Non-atomic regardless of reordering (field sum alone > 8)
[StructLayout(LayoutKind.Sequential)]
public void IsReadWriteAtomic_TimeSpan_ReturnsTrue()
{
// TimeSpan is 8 bytes (single long internally)
Assert.IsTrue(Memory.IsReadWriteAtomic<TimeSpan>());
Comment on lines +784 to +785
// int + byte, worst-case padded to 8 → always atomic
Assert.IsTrue(Memory.IsReadWriteAtomic<AutoLayoutMixedSmall>());
Replace unconditional Assert.IsTrue for 8-byte types with
Assert.AreEqual(MaxAtomicSize >= 8, ...) so tests are correct
when IntPtr.Size is 4. Also fix SizeOf_DateTimeOffset which
expects 16 bytes but gets 12 on 32-bit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves ViewableProperty<T> correctness under concurrency by avoiding torn reads for non-atomic value types and enforcing ordering between storing the new value and firing change notifications. It also adds Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>() utilities plus extensive tests covering many layout/size edge cases.

Changes:

  • Added Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>() with caching and a Reflection.Emit fallback.
  • Updated ViewableProperty<T> to store myValue/myHasValue separately, use atomicity detection to avoid tearing, and add an explicit memory barrier before firing change events.
  • Added a large new NUnit test suite validating SizeOf<T>() and IsReadWriteAtomic<T>() across many types and layouts.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
rd-net/Test.Lifetimes/Utils/MemoryTest.cs Adds comprehensive tests for Memory.SizeOf<T>() and Memory.IsReadWriteAtomic<T>().
rd-net/Lifetimes/Util/Memory.cs Implements SizeOf<T>() and IsReadWriteAtomic<T>() with per-type caching and NET5+/fallback paths.
rd-net/Lifetimes/Lifetimes.csproj Adds System.Reflection.Emit.Lightweight for netstandard2.0 to support the fallback DynamicMethod path.
rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs Uses atomicity detection to avoid torn reads, makes Maybe consistent with HasValue, and enforces ordering before firing change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 63 to +65
{
if (Maybe.HasValue && EqualityComparer<T>.Default.Equals(Maybe.Value, value)) return;
Maybe = new Maybe<T>(value);
myValue = value;

// Auto layout, mixed types: int(4) + byte(1) + long(8) = 13 bytes of fields
// Non-atomic regardless of reordering (field sum alone > 8)
[StructLayout(LayoutKind.Sequential)]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants