diff --git a/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs b/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs
index 2c0ec3457..9b2a2aabf 100644
--- a/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs
+++ b/rd-net/Lifetimes/Collections/Viewable/ViewableProperty.cs
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using JetBrains.Core;
using JetBrains.Diagnostics;
using JetBrains.Lifetimes;
+using JetBrains.Util.Internal;
namespace JetBrains.Collections.Viewable
{
@@ -12,11 +14,35 @@ namespace JetBrains.Collections.Viewable
///
public class ViewableProperty : IViewableProperty
{
+ private static readonly bool ourIsReadWriteAtomic = Memory.IsReadWriteAtomic();
+
private readonly Signal myChange = new Signal();
+ private T myValue = default!;
+ private volatile bool myHasValue;
+
public ISource Change => myChange;
- public Maybe Maybe { get; private set; }
+ public Maybe Maybe
+ {
+ get
+ {
+ if (myHasValue)
+ {
+ if (ourIsReadWriteAtomic)
+ {
+ return new Maybe(myValue);
+ }
+
+ lock (myChange)
+ {
+ return new Maybe(myValue);
+ }
+ }
+
+ return Maybe.None;
+ }
+ }
public ViewableProperty() {}
@@ -36,7 +62,13 @@ public virtual T Value
lock (myChange)
{
if (Maybe.HasValue && EqualityComparer.Default.Equals(Maybe.Value, value)) return;
- Maybe = new Maybe(value);
+ myValue = value;
+ myHasValue = true;
+
+ // After optimizing signal, `Fire` no longer provides a full memory fence (triggered by `Interlocked.CompareExchange`).
+ // This caused our tests to become flaky because we rely on `Fire(value)` being observed strictly after `myValue = value`;
+ // To enforce this ordering, we explicitly add a memory barrier here.
+ Interlocked.MemoryBarrier();
myChange.Fire(value);
}
}
diff --git a/rd-net/Lifetimes/Lifetimes.csproj b/rd-net/Lifetimes/Lifetimes.csproj
index eb3a314ff..09e3405ad 100644
--- a/rd-net/Lifetimes/Lifetimes.csproj
+++ b/rd-net/Lifetimes/Lifetimes.csproj
@@ -46,6 +46,7 @@
+
diff --git a/rd-net/Lifetimes/Util/Memory.cs b/rd-net/Lifetimes/Util/Memory.cs
index 59d272f42..3ff2fecf5 100644
--- a/rd-net/Lifetimes/Util/Memory.cs
+++ b/rd-net/Lifetimes/Util/Memory.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -9,7 +11,7 @@ public class Memory
public static unsafe void CopyMemory(byte* src, byte* dest, int len)
{
-
+
if(len >= 0x10)
{
do
@@ -63,7 +65,7 @@ public static T VolatileRead(ref T location) where T : class
{
return Volatile.Read(ref location);
}
-
+
[MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)]
public static void VolatileWrite(ref T location, T value) where T : class
{
@@ -76,23 +78,73 @@ public static int VolatileRead(ref int location)
{
return Volatile.Read(ref location);
}
-
+
[MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)]
public static void VolatileWrite(ref int location, int value)
{
Volatile.Write(ref location, value);
}
-
+
[MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)]
public static bool VolatileRead(ref bool location)
{
return Volatile.Read(ref location);
}
-
+
[MethodImpl(MethodImplAdvancedOptions.AggressiveInlining)]
public static void VolatileWrite(ref bool location, bool value)
{
Volatile.Write(ref location, value);
}
+
+ ///
+ /// Returns the managed slot size of : for value types, this is the struct size
+ /// including trailing padding; for reference types, this is (the reference
+ /// slot size, not the heap object size).
+ ///
+ public static int SizeOf() => SizeOfCache.Size;
+
+ public static bool IsReadWriteAtomic()
+ {
+ return IsReadWriteAtomicCache.IsReadWriteAtomic;
+ }
+
+ private static readonly int MaxAtomicSize = IntPtr.Size;
+
+ private static class SizeOfCache
+ {
+ public static readonly int Size = Compute();
+
+ private static int Compute()
+ {
+#if NET5_0_OR_GREATER
+ return Unsafe.SizeOf();
+#else
+ var dm = new DynamicMethod("SizeOf", typeof(int), Type.EmptyTypes, typeof(Memory).Module, true);
+ var il = dm.GetILGenerator();
+ il.Emit(OpCodes.Sizeof, typeof(T));
+ il.Emit(OpCodes.Ret);
+ return ((Func)dm.CreateDelegate(typeof(Func)))();
+#endif
+ }
+ }
+
+ private static class IsReadWriteAtomicCache
+ {
+ public static readonly bool IsReadWriteAtomic = Compute();
+
+ private static bool Compute()
+ {
+ var type = typeof(T);
+ if (!type.IsValueType) return true;
+
+ var layoutAttr = type.StructLayoutAttribute;
+ if (layoutAttr != null && layoutAttr.Pack != 0 && layoutAttr.Pack < MaxAtomicSize)
+ return false;
+
+ var size = SizeOf();
+ return size <= MaxAtomicSize;
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/rd-net/Test.Lifetimes/Utils/MemoryTest.cs b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs
new file mode 100644
index 000000000..9df797f43
--- /dev/null
+++ b/rd-net/Test.Lifetimes/Utils/MemoryTest.cs
@@ -0,0 +1,1665 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using JetBrains.Util.Internal;
+using NUnit.Framework;
+
+namespace Test.Lifetimes.Utils
+{
+ public class MemoryTest
+ {
+ private static int MaxAtomicSize => IntPtr.Size;
+
+ #region Test Types
+
+ private enum ByteEnum : byte { Value = 1 }
+ private enum IntEnum : int { Value = 1 }
+ private enum LongEnum : long { Value = 1 }
+
+ private struct SmallStruct
+ {
+ public int Value;
+ }
+
+ private struct TwoIntStruct
+ {
+ public int A;
+ public int B;
+ }
+
+ private struct LargeStruct
+ {
+ public long A;
+ public long B;
+ public long C;
+ }
+
+ private struct StructWithReference
+ {
+ public object Ref;
+ public int Value;
+ }
+
+ private struct StructWithString
+ {
+ public string Text;
+ public int Length;
+ }
+
+ private struct StructWithNullableInt
+ {
+ public int? NullableValue;
+ }
+
+ private struct StructWithNullableLong
+ {
+ public long? NullableValue;
+ }
+
+ private struct NestedSmallStruct
+ {
+ public SmallStruct Inner;
+ }
+
+ private struct NestedLargeStruct
+ {
+ public TwoIntStruct Inner;
+ public int Extra;
+ }
+
+ private struct MixedStruct
+ {
+ public object Ref;
+ public int IntValue;
+ public byte ByteValue;
+ }
+
+ private class SampleClass
+ {
+ public int Value;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct ExplicitLayoutLargeOffset
+ {
+ [FieldOffset(0)]
+ public byte A;
+ [FieldOffset(300)]
+ public byte B;
+ }
+
+ private struct PaddedSequentialStruct
+ {
+ public short C;
+ public int A;
+ public byte B;
+ }
+
+ // Same fields as PaddedSequentialStruct but ordered largest-first → no padding waste
+ private struct OptimalSequentialStruct
+ {
+ public int A;
+ public short C;
+ public byte B;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct PackedStruct
+ {
+ public byte A;
+ public int B;
+ public short C;
+ }
+
+ // Auto layout, mixed types: int(4) + byte(1) = 5 bytes of fields
+ // Worst-case with padding: int(4) + byte(1) + 3pad = 8 → always ≤ 8 → atomic
+ [StructLayout(LayoutKind.Auto)]
+ private struct AutoLayoutMixedSmall
+ {
+ public int A;
+ public byte B;
+ }
+
+ // 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)]
+ private struct AutoLayoutMixedLarge
+ {
+ public int A;
+ public byte B;
+ public long C;
+ }
+
+ // Sequential, mixed types: short(2) + pad(2) + int(4) + short(2) + pad(2) = 12 bytes
+ // Field sum = 8 but actual = 12 → non-atomic
+ private struct SequentialMixedPadded
+ {
+ public short A;
+ public int B;
+ public short C;
+ }
+
+ // --- Generic structs ---
+
+ private struct Wrapper
+ {
+ public T Value;
+ }
+
+ private struct Pair
+ {
+ public T1 First;
+ public T2 Second;
+ }
+
+ private struct Triple
+ {
+ public T1 A;
+ public T2 B;
+ public T3 C;
+ }
+
+ // --- Layout variations ---
+
+ private struct EmptyStruct { }
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct Union32
+ {
+ [FieldOffset(0)] public int AsInt;
+ [FieldOffset(0)] public float AsFloat;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct Union64
+ {
+ [FieldOffset(0)] public long AsLong;
+ [FieldOffset(0)] public double AsDouble;
+ [FieldOffset(0)] public int AsInt;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct ExplicitPadded
+ {
+ [FieldOffset(0)] public byte A;
+ [FieldOffset(8)] public byte B;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 2)]
+ private struct Pack2Struct
+ {
+ public byte A;
+ public int B;
+ public byte C;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct Pack4Struct
+ {
+ public byte A;
+ public long B;
+ public byte C;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 8)]
+ private struct Pack8Struct
+ {
+ public byte A;
+ public long B;
+ public byte C;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = 32)]
+ private struct FixedSizeStruct
+ {
+ public int A;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = 4)]
+ private struct SmallFixedSizeStruct
+ {
+ public byte A;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 3)]
+ private struct Pack1Size3Struct
+ {
+ public byte A;
+ public byte B;
+ public byte C;
+ }
+
+ // --- Pack variations for atomicity testing ---
+
+ // Pack=1 single byte: size=1, but Pack=1 → non-default Pack
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Pack1SingleByte
+ {
+ public byte A;
+ }
+
+ // Pack=1 with int: 5 bytes total (byte + int, no padding), misaligned int
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Pack1ByteInt
+ {
+ public byte A;
+ public int B;
+ }
+
+ // Pack=1 single int: size=4, but fields could be at odd addresses
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private struct Pack1SingleInt
+ {
+ public int A;
+ }
+
+ // Pack=2 single short: size=2
+ [StructLayout(LayoutKind.Sequential, Pack = 2)]
+ private struct Pack2SingleShort
+ {
+ public short A;
+ }
+
+ // Pack=2 with int: byte(1) + pad(1) + int(4) = 6 bytes, int at 2-byte alignment
+ [StructLayout(LayoutKind.Sequential, Pack = 2)]
+ private struct Pack2ByteInt
+ {
+ public byte A;
+ public int B;
+ }
+
+ // Pack=4, single int: size=4, alignment=4
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct Pack4SingleInt
+ {
+ public int A;
+ }
+
+ // Pack=4, single long: size=8, but long is at 4-byte alignment (not 8-byte)
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct Pack4SingleLong
+ {
+ public long A;
+ }
+
+ // Pack=16 (more than natural alignment) — effectively same as default
+ [StructLayout(LayoutKind.Sequential, Pack = 16)]
+ private struct Pack16SingleInt
+ {
+ public int A;
+ }
+
+ // Pack=16 with int+short: same as default layout, should be safe
+ [StructLayout(LayoutKind.Sequential, Pack = 16)]
+ private struct Pack16IntShort
+ {
+ public int A;
+ public short B;
+ }
+
+ // --- Explicit layout atomicity edge cases ---
+
+ // Explicit layout, naturally aligned, fits in word
+ [StructLayout(LayoutKind.Explicit)]
+ private struct ExplicitAligned4
+ {
+ [FieldOffset(0)] public int Value;
+ }
+
+ // Explicit layout, 8-byte union — should be atomic on 64-bit
+ [StructLayout(LayoutKind.Explicit)]
+ private struct ExplicitUnion8
+ {
+ [FieldOffset(0)] public long AsLong;
+ [FieldOffset(0)] public double AsDouble;
+ }
+
+ // Explicit layout with odd field offset — potential misalignment
+ [StructLayout(LayoutKind.Explicit)]
+ private struct ExplicitOddOffset
+ {
+ [FieldOffset(0)] public byte A;
+ [FieldOffset(1)] public int B; // int at offset 1 — misaligned!
+ }
+
+ // Explicit layout, 3 bytes — odd size, but fits in word
+ [StructLayout(LayoutKind.Explicit)]
+ private struct Explicit3Bytes
+ {
+ [FieldOffset(0)] public byte A;
+ [FieldOffset(1)] public byte B;
+ [FieldOffset(2)] public byte C;
+ }
+
+ // Explicit layout, size exactly MaxAtomicSize on 64-bit
+ [StructLayout(LayoutKind.Explicit)]
+ private struct ExplicitExact8
+ {
+ [FieldOffset(0)] public int A;
+ [FieldOffset(4)] public int B;
+ }
+
+ // --- Size= attribute edge cases ---
+
+ // Struct with Size forcing it to exactly MaxAtomicSize
+ [StructLayout(LayoutKind.Sequential, Size = 8)]
+ private struct FixedSize8
+ {
+ public int A;
+ }
+
+ // Struct with Size = 9 — one byte over the atomic limit
+ [StructLayout(LayoutKind.Sequential, Size = 9)]
+ private struct FixedSize9
+ {
+ public int A;
+ }
+
+ // Struct with Size = 1
+ [StructLayout(LayoutKind.Sequential, Size = 1)]
+ private struct FixedSize1
+ {
+ public byte A;
+ }
+
+ // --- Generic struct with constrained T ---
+
+ private struct WrapperWithExtra
+ {
+ public T Value;
+ public int Extra;
+ }
+
+ // Generic struct containing a reference and a value type
+ private struct RefValuePair where TRef : class where TVal : struct
+ {
+ public TRef Ref;
+ public TVal Val;
+ }
+
+ #endregion
+
+ #region Primitive Types
+
+ [Test]
+ public void IsReadWriteAtomic_Byte_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_SByte_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Short_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_UShort_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Int_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_UInt_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Long_DependsOnArchitecture()
+ {
+ Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_ULong_DependsOnArchitecture()
+ {
+ Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Float_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Double_DependsOnArchitecture()
+ {
+ Assert.AreEqual(MaxAtomicSize >= 8, Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Bool_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ [Test]
+ public void IsReadWriteAtomic_Char_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic());
+ }
+
+ #endregion
+
+ #region Reference Types
+
+ [Test]
+ public void IsReadWriteAtomic_Object_ReturnsTrue()
+ {
+ Assert.IsTrue(Memory.IsReadWriteAtomic