Signal WIP

This commit is contained in:
2024-04-16 09:58:07 +02:00
parent 8922f71130
commit d523dd6880
20 changed files with 511 additions and 0 deletions

View File

@@ -155,6 +155,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.App.Database", "Ap
EndProject 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}" 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 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -502,6 +526,10 @@ Global
{53E5B762-B620-4106-B481-31A478A1E14F} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9} {53E5B762-B620-4106-B481-31A478A1E14F} = {8C3CFEFE-78A5-4940-B388-D15FCE02ECE9}
{610C9140-4B05-46A2-BFF4-501049EBA25E} = {A5291117-3001-498B-AC8B-E14F71F72570} {610C9140-4B05-46A2-BFF4-501049EBA25E} = {A5291117-3001-498B-AC8B-E14F71F72570}
{635DC6E5-A762-409E-BBCC-CE1D29F4DDB9} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}

View File

@@ -0,0 +1,3 @@
#!/bin/sh
dotnet publish -c Release /p:DefineConstants=VERBOSE_LOGGING

View File

@@ -0,0 +1,58 @@
using BenchmarkDotNet.Attributes;
namespace Signal.Benchmark;
[MemoryDiagnoser]
[ShortRunJob]
public class MapBenchmark
{
private static readonly Signal<int> _signalInt = new(10);
private static readonly IReadOnlySignal<int> _signalIntMapped;
private static readonly Signal<string> _signalString = new("test");
private static readonly IReadOnlySignal<string> _signalStringMapped;
static MapBenchmark()
{
_signalIntMapped = _signalInt.Map(v => v * 2);
_signalStringMapped = _signalString.Map(v => v);
}
[Benchmark]
public async ValueTask NoOpInt()
{
var signal = new Signal<int>(10);
var mappedSignal = signal.Map(value => value * 2);
}
[Benchmark]
public async ValueTask GetValueAsyncInt()
{
var signal = new Signal<int>(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<string>("test");
var mappedSignal = signal.Map(value => value);
}
[Benchmark]
public async ValueTask GetValueAsyncString()
{
var signal = new Signal<string>("test");
var mappedSignal = signal.Map(value => value);
await mappedSignal.GetValueAsync();
}
[Benchmark]
public async ValueTask GetValueAsyncStringStatic()
{
await _signalStringMapped.GetValueAsync();
}
}

View File

@@ -0,0 +1,4 @@
using BenchmarkDotNet.Running;
using Signal.Benchmark;
var summary = BenchmarkRunner.Run<MapBenchmark>();

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Signal\Signal.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup>
</Project>

View File

@@ -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<int>(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);
});
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Reference Include="dotMemory.Unit">
<HintPath>..\..\..\..\..\.nuget\packages\jetbrains.dotmemoryunit\3.2.20220510\lib\netstandard2.0\dotMemory.Unit.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Signal\Signal.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<string>("test1");
var signal2 = new Signal<string>("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<string>("test1");
var signal2 = new Signal<string>("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);
}
}

View File

@@ -0,0 +1,79 @@
using Xunit;
namespace Signal.Tests;
public class MapSignalTests
{
[Fact]
public void Map_WhenNotRead_ShouldBeDirty()
{
// Arrange
var signal = new Signal<string>("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<string>("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<string>("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<string>("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<string>("tEsT");
var mapped = signal.Map(async s =>
{
await Task.Yield();
return s.ToUpper();
});
// Act
var result = await mapped.GetValueAsync();
// Assert
Assert.Equal("TEST", result);
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" Version="0.1.1-pre.396" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Signal\Signal.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,36 @@
namespace Signal;
public class CombineLatestSignal<T1, T2, TResult> : SignalBase<TResult>
{
private readonly Func<ValueTask<TResult>> _combine;
private TResult _result;
public CombineLatestSignal(IReadOnlySignal<T1> signal1, IReadOnlySignal<T2> signal2, Func<T1, T2, TResult> combine)
: base(new IReadOnlySignal[] { signal1, signal2 })
{
_combine = CombineAsync;
async ValueTask<TResult> CombineAsync() => combine(await signal1.GetValueAsync(), await signal2.GetValueAsync());
}
public CombineLatestSignal(IReadOnlySignal<T1> signal1, IReadOnlySignal<T2> signal2, Func<T1, T2, Task<TResult>> combine)
: base(new IReadOnlySignal[] { signal1, signal2 })
{
_combine = CombineAsync;
async ValueTask<TResult> CombineAsync() => await combine(await signal1.GetValueAsync(), await signal2.GetValueAsync());
}
public override async ValueTask<TResult> GetValueAsync()
{
//TODO synchronization
if (!IsDirty)
{
return _result;
}
IsDirty = false;
_result = await _combine();
return _result;
}
}

View File

@@ -0,0 +1,13 @@
namespace Signal;
public static class Extensions
{
public static IReadOnlySignal<TResult> Map<T, TResult>(this IReadOnlySignal<T> signal, Func<T, TResult> map)
{
return new MapSignal<T, TResult>(signal, map);
}
public static IReadOnlySignal<TResult> Map<T, TResult>(this IReadOnlySignal<T> signal, Func<T, Task<TResult>> map)
{
return new MapSignal<T, TResult>(signal, map);
}
}

View File

@@ -0,0 +1,15 @@
namespace Signal;
public static class Helpers
{
public static IReadOnlySignal<TResult> CombineLatest<T1, T2, TResult>(IReadOnlySignal<T1> signal1,
IReadOnlySignal<T2> signal2, Func<T1, T2, TResult> combine)
{
return new CombineLatestSignal<T1, T2, TResult>(signal1, signal2, combine);
}
public static IReadOnlySignal<TResult> CombineLatest<T1, T2, TResult>(IReadOnlySignal<T1> signal1,
IReadOnlySignal<T2> signal2, Func<T1, T2, Task<TResult>> combine)
{
return new CombineLatestSignal<T1, T2, TResult>(signal1, signal2, combine);
}
}

View File

@@ -0,0 +1,12 @@
namespace Signal;
public interface IReadOnlySignal
{
bool IsDirty { get; }
event Action<bool> IsDirtyChanged;
internal void SetDirty();
}
public interface IReadOnlySignal<T> : IReadOnlySignal
{
ValueTask<T> GetValueAsync();
}

View File

@@ -0,0 +1,6 @@
namespace Signal;
public interface ISignal<T> : IReadOnlySignal<T>
{
void SetValue(T value);
}

View File

@@ -0,0 +1,31 @@
namespace Signal;
public class MapSignal<T, TResult> : SignalBase<TResult>
{
private readonly Func<ValueTask<TResult>> _map;
private TResult _result;
public MapSignal(IReadOnlySignal<T> signal, Func<T, TResult> map) : base(signal)
{
_map = MapValueAsync;
async ValueTask<TResult> MapValueAsync() => map(await signal.GetValueAsync());
}
public MapSignal(IReadOnlySignal<T> signal, Func<T, Task<TResult>> map) : base(signal)
{
_map = MapValueAsync;
async ValueTask<TResult> MapValueAsync() => await map(await signal.GetValueAsync());
}
public override async ValueTask<TResult> GetValueAsync()
{
//TODO synchronization
if (!IsDirty)
{
return _result;
}
IsDirty = false;
_result = await _map();
return _result;
}
}

View File

@@ -0,0 +1,23 @@
namespace Signal;
public class Signal<T> : SignalBase<T>, ISignal<T>
{
private T _value;
public Signal(T value)
{
_value = value;
}
public void SetValue(T value)
{
_value = value;
SetDirty();
}
public override ValueTask<T> GetValueAsync()
{
IsDirty = false;
return new ValueTask<T>(_value);
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,50 @@
namespace Signal;
public abstract class SignalBase<T> : IReadOnlySignal<T>
{
private readonly List<SignalBase<T>> _dependentSignals = [];
public bool IsDirty { get; protected set; } = true;
public event Action<bool>? IsDirtyChanged;
public SignalBase()
{
}
public SignalBase(IReadOnlySignal baseSignal)
{
HandleDependentSignal(baseSignal);
}
public SignalBase(IEnumerable<IReadOnlySignal> 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<T> GetValueAsync();
}

8
src/nuget.config Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="xunit-ci" value="https://f.feedz.io/xunit/xunit/nuget/index.json" />
</packageSources>
</configuration>