From d523dd68807a716a83e6cba84a40e9e23de0b822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 16 Apr 2024 09:58:07 +0200 Subject: [PATCH] Signal WIP --- src/FileTime.sln | 28 +++++++ .../Avalonia/FileTime.GuiApp/build_local.sh | 3 + src/Library/Signal.Benchmark/MapBenchmark.cs | 58 ++++++++++++++ src/Library/Signal.Benchmark/Program.cs | 4 + .../Signal.Benchmark/Signal.Benchmark.csproj | 18 +++++ .../Signal.Tests.Memory/MapSignalTests.cs | 26 ++++++ .../Signal.Tests.Memory.csproj | 33 ++++++++ .../Signal.Tests/CombineLatestTests.cs | 41 ++++++++++ src/Library/Signal.Tests/MapSignalTests.cs | 79 +++++++++++++++++++ src/Library/Signal.Tests/Signal.Tests.csproj | 18 +++++ src/Library/Signal/CombineLatestSignal.cs | 36 +++++++++ src/Library/Signal/Extensions.cs | 13 +++ src/Library/Signal/Helpers.cs | 15 ++++ src/Library/Signal/IReadOnlySignal.cs | 12 +++ src/Library/Signal/ISignal.cs | 6 ++ src/Library/Signal/MapSignal.cs | 31 ++++++++ src/Library/Signal/Signal.cs | 23 ++++++ src/Library/Signal/Signal.csproj | 9 +++ src/Library/Signal/SignalBase.cs | 50 ++++++++++++ src/nuget.config | 8 ++ 20 files changed, 511 insertions(+) create mode 100755 src/GuiApp/Avalonia/FileTime.GuiApp/build_local.sh create mode 100644 src/Library/Signal.Benchmark/MapBenchmark.cs create mode 100644 src/Library/Signal.Benchmark/Program.cs create mode 100644 src/Library/Signal.Benchmark/Signal.Benchmark.csproj create mode 100644 src/Library/Signal.Tests.Memory/MapSignalTests.cs create mode 100644 src/Library/Signal.Tests.Memory/Signal.Tests.Memory.csproj create mode 100644 src/Library/Signal.Tests/CombineLatestTests.cs create mode 100644 src/Library/Signal.Tests/MapSignalTests.cs create mode 100644 src/Library/Signal.Tests/Signal.Tests.csproj create mode 100644 src/Library/Signal/CombineLatestSignal.cs create mode 100644 src/Library/Signal/Extensions.cs create mode 100644 src/Library/Signal/Helpers.cs create mode 100644 src/Library/Signal/IReadOnlySignal.cs create mode 100644 src/Library/Signal/ISignal.cs create mode 100644 src/Library/Signal/MapSignal.cs create mode 100644 src/Library/Signal/Signal.cs create mode 100644 src/Library/Signal/Signal.csproj create mode 100644 src/Library/Signal/SignalBase.cs create mode 100644 src/nuget.config diff --git a/src/FileTime.sln b/src/FileTime.sln index 5244025..e525aa4 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -155,6 +155,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.Database", "Ap EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.Database.Abstractions", "AppCommon\FileTime.App.Database.Abstractions\FileTime.App.Database.Abstractions.csproj", "{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal", "Library\Signal\Signal.csproj", "{3917AA16-F78A-460F-8214-85306E532FDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal.Tests", "Library\Signal.Tests\Signal.Tests.csproj", "{2E2DACC0-9718-47D4-B6AE-E57624E6EE62}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal.Tests.Memory", "Library\Signal.Tests.Memory\Signal.Tests.Memory.csproj", "{C2A81E12-FED1-4943-85F4-EDFE49BF32EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal.Benchmark", "Library\Signal.Benchmark\Signal.Benchmark.csproj", "{3D8097B2-0B9D-4916-BBF2-C45F7200553C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -429,6 +437,22 @@ Global {635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU {635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU {635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Release|Any CPU.Build.0 = Release|Any CPU + {3917AA16-F78A-460F-8214-85306E532FDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3917AA16-F78A-460F-8214-85306E532FDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3917AA16-F78A-460F-8214-85306E532FDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3917AA16-F78A-460F-8214-85306E532FDC}.Release|Any CPU.Build.0 = Release|Any CPU + {2E2DACC0-9718-47D4-B6AE-E57624E6EE62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E2DACC0-9718-47D4-B6AE-E57624E6EE62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E2DACC0-9718-47D4-B6AE-E57624E6EE62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E2DACC0-9718-47D4-B6AE-E57624E6EE62}.Release|Any CPU.Build.0 = Release|Any CPU + {C2A81E12-FED1-4943-85F4-EDFE49BF32EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2A81E12-FED1-4943-85F4-EDFE49BF32EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2A81E12-FED1-4943-85F4-EDFE49BF32EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2A81E12-FED1-4943-85F4-EDFE49BF32EB}.Release|Any CPU.Build.0 = Release|Any CPU + {3D8097B2-0B9D-4916-BBF2-C45F7200553C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D8097B2-0B9D-4916-BBF2-C45F7200553C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D8097B2-0B9D-4916-BBF2-C45F7200553C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D8097B2-0B9D-4916-BBF2-C45F7200553C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -502,6 +526,10 @@ Global {53E5B762-B620-4106-B481-31A478A1E14F} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} {610C9140-4B05-46A2-BFF4-501049EBA25E} = {A5291117-3001-498B-AC8B-E14F71F72570} {635DC6E5-A762-409E-BBCC-CE1D29F4DDB9} = {A5291117-3001-498B-AC8B-E14F71F72570} + {3917AA16-F78A-460F-8214-85306E532FDC} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {2E2DACC0-9718-47D4-B6AE-E57624E6EE62} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {C2A81E12-FED1-4943-85F4-EDFE49BF32EB} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {3D8097B2-0B9D-4916-BBF2-C45F7200553C} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/build_local.sh b/src/GuiApp/Avalonia/FileTime.GuiApp/build_local.sh new file mode 100755 index 0000000..065395a --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/build_local.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +dotnet publish -c Release /p:DefineConstants=VERBOSE_LOGGING diff --git a/src/Library/Signal.Benchmark/MapBenchmark.cs b/src/Library/Signal.Benchmark/MapBenchmark.cs new file mode 100644 index 0000000..0b455ad --- /dev/null +++ b/src/Library/Signal.Benchmark/MapBenchmark.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Attributes; + +namespace Signal.Benchmark; + +[MemoryDiagnoser] +[ShortRunJob] +public class MapBenchmark +{ + private static readonly Signal _signalInt = new(10); + private static readonly IReadOnlySignal _signalIntMapped; + + private static readonly Signal _signalString = new("test"); + private static readonly IReadOnlySignal _signalStringMapped; + + static MapBenchmark() + { + _signalIntMapped = _signalInt.Map(v => v * 2); + _signalStringMapped = _signalString.Map(v => v); + } + + [Benchmark] + public async ValueTask NoOpInt() + { + var signal = new Signal(10); + var mappedSignal = signal.Map(value => value * 2); + } + [Benchmark] + public async ValueTask GetValueAsyncInt() + { + var signal = new Signal(10); + var mappedSignal = signal.Map(value => value * 2); + await mappedSignal.GetValueAsync(); + } + [Benchmark] + public async ValueTask GetValueAsyncIntStatic() + { + await _signalIntMapped.GetValueAsync(); + } + + [Benchmark] + public async ValueTask NoOpString() + { + var signal = new Signal("test"); + var mappedSignal = signal.Map(value => value); + } + [Benchmark] + public async ValueTask GetValueAsyncString() + { + var signal = new Signal("test"); + var mappedSignal = signal.Map(value => value); + await mappedSignal.GetValueAsync(); + } + [Benchmark] + public async ValueTask GetValueAsyncStringStatic() + { + await _signalStringMapped.GetValueAsync(); + } +} \ No newline at end of file diff --git a/src/Library/Signal.Benchmark/Program.cs b/src/Library/Signal.Benchmark/Program.cs new file mode 100644 index 0000000..6bfd155 --- /dev/null +++ b/src/Library/Signal.Benchmark/Program.cs @@ -0,0 +1,4 @@ +using BenchmarkDotNet.Running; +using Signal.Benchmark; + +var summary = BenchmarkRunner.Run(); \ No newline at end of file diff --git a/src/Library/Signal.Benchmark/Signal.Benchmark.csproj b/src/Library/Signal.Benchmark/Signal.Benchmark.csproj new file mode 100644 index 0000000..feb9b5e --- /dev/null +++ b/src/Library/Signal.Benchmark/Signal.Benchmark.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Library/Signal.Tests.Memory/MapSignalTests.cs b/src/Library/Signal.Tests.Memory/MapSignalTests.cs new file mode 100644 index 0000000..8a7dfdd --- /dev/null +++ b/src/Library/Signal.Tests.Memory/MapSignalTests.cs @@ -0,0 +1,26 @@ +using JetBrains.dotMemoryUnit; + +namespace Signal.Tests.Memory; + +public class MapSignalTests +{ + [Fact] + [AssertTraffic] + public async Task Map_WhenNoAllocation_ShouldNoAllocationHappen() + { + // Arrange + var signal = new Signal(10); + var mapped = signal.Map(s => s); + var memorySnapShot = dotMemory.Check(); + + // Act + await mapped.GetValueAsync(); + + // Assert + //Assert.Equal(10, result); + dotMemory.Check(memory => + { + Assert.Equal(0, memory.GetDifference(memorySnapShot).GetNewObjects().ObjectsCount); + }); + } +} \ No newline at end of file diff --git a/src/Library/Signal.Tests.Memory/Signal.Tests.Memory.csproj b/src/Library/Signal.Tests.Memory/Signal.Tests.Memory.csproj new file mode 100644 index 0000000..c7bd790 --- /dev/null +++ b/src/Library/Signal.Tests.Memory/Signal.Tests.Memory.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + ..\..\..\..\..\.nuget\packages\jetbrains.dotmemoryunit\3.2.20220510\lib\netstandard2.0\dotMemory.Unit.dll + + + + + + + + diff --git a/src/Library/Signal.Tests/CombineLatestTests.cs b/src/Library/Signal.Tests/CombineLatestTests.cs new file mode 100644 index 0000000..4734b89 --- /dev/null +++ b/src/Library/Signal.Tests/CombineLatestTests.cs @@ -0,0 +1,41 @@ +using Xunit; +using static Signal.Helpers; + +namespace Signal.Tests; + +public class CombineLatestTests +{ + [Fact] + public async Task CombineLatest_With2ValidInput_ShouldCombineInputs() + { + // Arrange + var signal1 = new Signal("test1"); + var signal2 = new Signal("test2"); + var combinedSignal = CombineLatest(signal1, signal2, (t1, t2) => t1 + " " + t2); + + // Act + var result = await combinedSignal.GetValueAsync(); + + // Assert + Assert.Equal("test1 test2", result); + } + + [Fact] + public async Task CombineLatest_With2ValidInputAndAsyncCombiner_ShouldCombineInputs() + { + // Arrange + var signal1 = new Signal("test1"); + var signal2 = new Signal("test2"); + var combinedSignal = CombineLatest(signal1, signal2, async (t1, t2) => + { + await Task.Yield(); + return t1 + " " + t2; + }); + + // Act + var result = await combinedSignal.GetValueAsync(); + + // Assert + Assert.Equal("test1 test2", result); + } +} \ No newline at end of file diff --git a/src/Library/Signal.Tests/MapSignalTests.cs b/src/Library/Signal.Tests/MapSignalTests.cs new file mode 100644 index 0000000..c35b1b4 --- /dev/null +++ b/src/Library/Signal.Tests/MapSignalTests.cs @@ -0,0 +1,79 @@ +using Xunit; + +namespace Signal.Tests; + +public class MapSignalTests +{ + [Fact] + public void Map_WhenNotRead_ShouldBeDirty() + { + // Arrange + var signal = new Signal("test"); + var mapped = signal.Map(s => s); + + // Act + + // Assert + Assert.True(mapped.IsDirty); + } + + [Fact] + public async Task Map_WhenNotReadButBaseIsNotDirty_ShouldBeDirty() + { + // Arrange + var signal = new Signal("test"); + await signal.GetValueAsync(); + var mapped = signal.Map(s => s); + + // Act + + // Assert + Assert.True(mapped.IsDirty); + } + + [Fact] + public async Task Map_WhenAlreadyRead_ShouldNotBeDirty() + { + // Arrange + var signal = new Signal("test"); + var mapped = signal.Map(s => s); + + // Act + await mapped.GetValueAsync(); + + // Assert + Assert.False(mapped.IsDirty); + } + + [Fact] + public async Task GetValueAsync_WithSyncMapper_ShouldReturnCorrectData() + { + // Arrange + var signal = new Signal("tEsT"); + var mapped = signal.Map(s => s.ToUpper()); + + // Act + var result = await mapped.GetValueAsync(); + + // Assert + Assert.Equal("TEST", result); + } + + [Fact] + public async Task GetValueAsync_WithAsyncMapper_ShouldReturnCorrectData() + { + // Arrange + var signal = new Signal("tEsT"); + var mapped = signal.Map(async s => + { + await Task.Yield(); + return s.ToUpper(); + }); + + // Act + var result = await mapped.GetValueAsync(); + + // Assert + Assert.Equal("TEST", result); + } +} \ No newline at end of file diff --git a/src/Library/Signal.Tests/Signal.Tests.csproj b/src/Library/Signal.Tests/Signal.Tests.csproj new file mode 100644 index 0000000..ce0caf7 --- /dev/null +++ b/src/Library/Signal.Tests/Signal.Tests.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Library/Signal/CombineLatestSignal.cs b/src/Library/Signal/CombineLatestSignal.cs new file mode 100644 index 0000000..29e7552 --- /dev/null +++ b/src/Library/Signal/CombineLatestSignal.cs @@ -0,0 +1,36 @@ +namespace Signal; + +public class CombineLatestSignal : SignalBase +{ + private readonly Func> _combine; + private TResult _result; + + public CombineLatestSignal(IReadOnlySignal signal1, IReadOnlySignal signal2, Func combine) + : base(new IReadOnlySignal[] { signal1, signal2 }) + { + _combine = CombineAsync; + + async ValueTask CombineAsync() => combine(await signal1.GetValueAsync(), await signal2.GetValueAsync()); + + } + + public CombineLatestSignal(IReadOnlySignal signal1, IReadOnlySignal signal2, Func> combine) + : base(new IReadOnlySignal[] { signal1, signal2 }) + { + _combine = CombineAsync; + + async ValueTask CombineAsync() => await combine(await signal1.GetValueAsync(), await signal2.GetValueAsync()); + } + + public override async ValueTask GetValueAsync() + { + //TODO synchronization + if (!IsDirty) + { + return _result; + } + IsDirty = false; + _result = await _combine(); + return _result; + } +} \ No newline at end of file diff --git a/src/Library/Signal/Extensions.cs b/src/Library/Signal/Extensions.cs new file mode 100644 index 0000000..82afcac --- /dev/null +++ b/src/Library/Signal/Extensions.cs @@ -0,0 +1,13 @@ +namespace Signal; + +public static class Extensions +{ + public static IReadOnlySignal Map(this IReadOnlySignal signal, Func map) + { + return new MapSignal(signal, map); + } + public static IReadOnlySignal Map(this IReadOnlySignal signal, Func> map) + { + return new MapSignal(signal, map); + } +} \ No newline at end of file diff --git a/src/Library/Signal/Helpers.cs b/src/Library/Signal/Helpers.cs new file mode 100644 index 0000000..83a2d96 --- /dev/null +++ b/src/Library/Signal/Helpers.cs @@ -0,0 +1,15 @@ +namespace Signal; + +public static class Helpers +{ + public static IReadOnlySignal CombineLatest(IReadOnlySignal signal1, + IReadOnlySignal signal2, Func combine) + { + return new CombineLatestSignal(signal1, signal2, combine); + } + public static IReadOnlySignal CombineLatest(IReadOnlySignal signal1, + IReadOnlySignal signal2, Func> combine) + { + return new CombineLatestSignal(signal1, signal2, combine); + } +} \ No newline at end of file diff --git a/src/Library/Signal/IReadOnlySignal.cs b/src/Library/Signal/IReadOnlySignal.cs new file mode 100644 index 0000000..004e698 --- /dev/null +++ b/src/Library/Signal/IReadOnlySignal.cs @@ -0,0 +1,12 @@ +namespace Signal; + +public interface IReadOnlySignal +{ + bool IsDirty { get; } + event Action IsDirtyChanged; + internal void SetDirty(); +} +public interface IReadOnlySignal : IReadOnlySignal +{ + ValueTask GetValueAsync(); +} \ No newline at end of file diff --git a/src/Library/Signal/ISignal.cs b/src/Library/Signal/ISignal.cs new file mode 100644 index 0000000..5ac0ffe --- /dev/null +++ b/src/Library/Signal/ISignal.cs @@ -0,0 +1,6 @@ +namespace Signal; + +public interface ISignal : IReadOnlySignal +{ + void SetValue(T value); +} \ No newline at end of file diff --git a/src/Library/Signal/MapSignal.cs b/src/Library/Signal/MapSignal.cs new file mode 100644 index 0000000..838275d --- /dev/null +++ b/src/Library/Signal/MapSignal.cs @@ -0,0 +1,31 @@ +namespace Signal; + +public class MapSignal : SignalBase +{ + private readonly Func> _map; + private TResult _result; + public MapSignal(IReadOnlySignal signal, Func map) : base(signal) + { + _map = MapValueAsync; + + async ValueTask MapValueAsync() => map(await signal.GetValueAsync()); + } + public MapSignal(IReadOnlySignal signal, Func> map) : base(signal) + { + _map = MapValueAsync; + + async ValueTask MapValueAsync() => await map(await signal.GetValueAsync()); + } + + public override async ValueTask GetValueAsync() + { + //TODO synchronization + if (!IsDirty) + { + return _result; + } + IsDirty = false; + _result = await _map(); + return _result; + } +} \ No newline at end of file diff --git a/src/Library/Signal/Signal.cs b/src/Library/Signal/Signal.cs new file mode 100644 index 0000000..c5c0c74 --- /dev/null +++ b/src/Library/Signal/Signal.cs @@ -0,0 +1,23 @@ +namespace Signal; + +public class Signal : SignalBase, ISignal +{ + private T _value; + + public Signal(T value) + { + _value = value; + } + + public void SetValue(T value) + { + _value = value; + SetDirty(); + } + + public override ValueTask GetValueAsync() + { + IsDirty = false; + return new ValueTask(_value); + } +} \ No newline at end of file diff --git a/src/Library/Signal/Signal.csproj b/src/Library/Signal/Signal.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Library/Signal/Signal.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Library/Signal/SignalBase.cs b/src/Library/Signal/SignalBase.cs new file mode 100644 index 0000000..5fdcc40 --- /dev/null +++ b/src/Library/Signal/SignalBase.cs @@ -0,0 +1,50 @@ +namespace Signal; + +public abstract class SignalBase : IReadOnlySignal +{ + private readonly List> _dependentSignals = []; + public bool IsDirty { get; protected set; } = true; + public event Action? IsDirtyChanged; + + public SignalBase() + { + + } + + public SignalBase(IReadOnlySignal baseSignal) + { + HandleDependentSignal(baseSignal); + } + + public SignalBase(IEnumerable baseSignal) + { + foreach (var signal in baseSignal) + { + HandleDependentSignal(signal); + } + } + + private void HandleDependentSignal(IReadOnlySignal baseSignal) + { + baseSignal.IsDirtyChanged += isDirty => + { + if (isDirty) + { + SetDirty(); + } + }; + } + + public void SetDirty() + { + IsDirty = true; + for (var i = 0; i < _dependentSignals.Count; i++) + { + _dependentSignals[i].SetDirty(); + } + + IsDirtyChanged?.Invoke(IsDirty); + } + + public abstract ValueTask GetValueAsync(); +} \ No newline at end of file diff --git a/src/nuget.config b/src/nuget.config new file mode 100644 index 0000000..28b92e6 --- /dev/null +++ b/src/nuget.config @@ -0,0 +1,8 @@ + + + + + + + +