Skip to content

Commit 11c12e2

Browse files
committed
Caching
1 parent 2fefe2c commit 11c12e2

10 files changed

Lines changed: 183 additions & 62 deletions

File tree

Solver.Core/AStarSolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class AStarSolver<TState, TStep, TPriority>(Func<TState, IEnumerable<TSte
1010

1111
protected override Solution<TState, TStep> GetNextSolution() => _queue.Dequeue();
1212
protected override bool CanGetNextSolution() => _queue.Count > 0;
13-
protected override void StoreSolution(Solution<TState, TStep> solution)
13+
protected override void EnqueueSolution(Solution<TState, TStep> solution)
1414
{
1515
var priority = heuristic(solution.State);
1616
_queue.Enqueue(solution, priority);

Solver.Core/Base/Solver.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
namespace Solver.Core.Base;
1+
using Solver.Core.Cache;
2+
3+
namespace Solver.Core.Base;
24

35
public abstract class Solver<TState, TStep>(Func<TState, IEnumerable<TStep>> generateSteps, Func<TState, TStep, TState> performStep,
4-
Func<TState, bool> solvedTest, IEqualityComparer<TState>? comparer = null)
6+
Func<TState, bool> solvedTest, ISolutionCache<TState, TStep> solutionCache, IEqualityComparer<TState>? comparer = null)
57
{
68
protected abstract Solution<TState, TStep> GetNextSolution();
79
protected abstract bool CanGetNextSolution();
8-
protected abstract void StoreSolution(Solution<TState, TStep> solution);
10+
protected abstract void EnqueueSolution(Solution<TState, TStep> solution);
911
protected internal abstract IEnumerable<Solution<TState, TStep>> GetAllSolutions();
1012

1113
public Solution<TState, TStep>? TrySolve(TState initialState, CancellationToken token = default)
1214
{
13-
StoreSolution(new Solution<TState, TStep>(initialState));
15+
var initial = new Solution<TState, TStep>(initialState) { Id = Guid.Empty };
16+
EnqueueSolution(initial);
17+
solutionCache.RememberSolution(initial);
1418

1519
var visitedStates = comparer == null
1620
? new HashSet<TState> {initialState}
@@ -25,7 +29,7 @@ public abstract class Solver<TState, TStep>(Func<TState, IEnumerable<TStep>> gen
2529
if (!visitedStates.Add(state)) continue;
2630
var newSolution = solution.AddStep(state, step);
2731
if (solvedTest(state)) return newSolution;
28-
StoreSolution(newSolution);
32+
EnqueueSolution(newSolution);
2933
}
3034
}
3135

Solver.Core/BreadthFirstSolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ public class BreadthFirstSolver<TState, TStep>(Func<TState, IEnumerable<TStep>>
1010

1111
protected override Solution<TState, TStep> GetNextSolution() => _queue.Dequeue();
1212
protected override bool CanGetNextSolution() => _queue.Count > 0;
13-
protected override void StoreSolution(Solution<TState, TStep> solution) => _queue.Enqueue(solution);
13+
protected override void EnqueueSolution(Solution<TState, TStep> solution) => _queue.Enqueue(solution);
1414
protected internal override IEnumerable<Solution<TState, TStep>> GetAllSolutions() => _queue;
1515
}

Solver.Core/Cache/DiskCache.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.IO.Abstractions;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
5+
namespace Solver.Core.Cache;
6+
7+
public class DiskCache<TState, TStep>(IFileSystem fileSystem, IOptions<DiskCacheOptions> options, ILogger<DiskCache<TState, TStep>> logger) : ISolutionCache<TState, TStep>
8+
{
9+
10+
11+
#region ISolutionCache<TState,TStep>
12+
13+
public Solution<TState, TStep> GetSolution(Guid solutionId)
14+
{
15+
throw new NotImplementedException();
16+
}
17+
18+
public void RememberSolution(Solution<TState, TStep> solution)
19+
{
20+
throw new NotImplementedException();
21+
}
22+
23+
#endregion
24+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Solver.Core.Cache;
4+
5+
public class DiskCacheOptions
6+
{
7+
public const string ConfigurationSectionName = "Cache";
8+
9+
[Required]
10+
public string Root { get; init; }
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Solver.Core.Cache;
2+
3+
public interface ISolutionCache<TState, TStep>
4+
{
5+
Solution<TState, TStep> GetSolution(Guid solutionId);
6+
7+
void RememberSolution(Solution<TState, TStep> solution);
8+
}

Solver.Core/Cache/InMemoryCache.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace Solver.Core.Cache;
4+
5+
internal class InMemoryCache<TState, TStep>(ILogger<InMemoryCache<TState, TStep>> logger) : ISolutionCache<TState, TStep>
6+
{
7+
private readonly IDictionary<Guid, Solution<TState, TStep>> _cache = new Dictionary<Guid, Solution<TState, TStep>>();
8+
9+
#region ISolutionCache<TState,TStep>
10+
11+
public Solution<TState, TStep> GetSolution(Guid solutionId)
12+
{
13+
logger.LogTrace("Retrieving solution under ID {Id:D}", solutionId);
14+
if (_cache.TryGetValue(solutionId, out var solution))
15+
{
16+
logger.LogTrace("Solution located. Returning...");
17+
return solution;
18+
}
19+
20+
logger.LogTrace("Could not locate solution.");
21+
throw new ArgumentException($"Solution with ID {solutionId:D} has not been cached yet.", nameof(solutionId));
22+
}
23+
24+
public void RememberSolution(Solution<TState, TStep> solution)
25+
{
26+
var key = solution.Id;
27+
logger.LogTrace("Caching solution with ID {Id:D}", key);
28+
if (_cache.ContainsKey(key))
29+
{
30+
throw new InvalidOperationException("Solution ID collision detected");
31+
}
32+
_cache[key] = solution;
33+
}
34+
35+
#endregion
36+
}
Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,100 @@
1-
using System.Runtime.CompilerServices;
2-
using Solver.Core.Base;
1+
using Microsoft.Extensions.Logging;
32

43
namespace Solver.Core.Serialization;
54

6-
public class SolutionSerializer<TState, TStep>(IStateSerializer<TState, TStep> stateSerializer)
5+
public class SolutionSerializer<TState, TStep>(IStateSerializer<TState, TStep> stateSerializer, ILogger<SolutionSerializer<TState, TStep>>? logger = null)
76
{
8-
public async Task Serialize(IEnumerable<Solution<TState, TStep>> solutions, Stream stream, CancellationToken token = default)
7+
8+
public async Task SerializeSolutionAsync(Solution<TState, TStep> solution, Stream stream, CancellationToken token = default)
99
{
10-
var storedSolutions = new HashSet<Guid>();
11-
foreach (var solution in solutions)
12-
{
13-
if (!storedSolutions.Add(solution.Id)) continue;
10+
logger?.LogDebug("Serializing solution {Guid:D}", solution.Id);
1411

15-
var idBuffer = solution.Id.ToByteArray();
12+
var idBuffer = solution.Id.ToByteArray();
13+
logger?.LogTrace("GUID bytes: {Bytes}", PrintBytes(idBuffer));
1614

17-
var previousStepBuffer = new byte[16 + stateSerializer.SerializedStepLength];
18-
var stepBuffer = new byte[stateSerializer.SerializedStepLength];
19-
if (solution.PreviousStep != null)
20-
{
21-
Array.Copy(solution.PreviousStep.Value.Solution.Id.ToByteArray(), previousStepBuffer, 16);
22-
stateSerializer.SerializeStep(solution.PreviousStep.Value.Step, stepBuffer);
23-
Array.Copy(stepBuffer, 0, previousStepBuffer, 16, stateSerializer.SerializedStepLength);
24-
}
25-
else
26-
{
27-
Array.Fill(previousStepBuffer, byte.MinValue);
28-
}
15+
var solutionLengthBuffer = BitConverter.GetBytes(solution.Length);
16+
logger?.LogTrace("Solution length bytes: {Bytes}", PrintBytes(solutionLengthBuffer));
2917

30-
var stateBuffer = new byte[1024];
31-
var stateLength = stateSerializer.SerializeState(solution.State, stateBuffer);
18+
var previousStepBuffer = new byte[16 + stateSerializer.SerializedStepLength];
19+
if (solution.Previous != null)
20+
{
21+
var stepBuffer = new byte[stateSerializer.SerializedStepLength];
22+
Array.Copy(solution.Previous.Value.Id.ToByteArray(), previousStepBuffer, 16);
23+
stateSerializer.SerializeStep(solution.Previous.Value.Step, stepBuffer);
24+
logger?.LogTrace("Step bytes: {Bytes}", PrintBytes(stepBuffer));
25+
Array.Copy(stepBuffer, 0, previousStepBuffer, 16, stateSerializer.SerializedStepLength);
3226

33-
var totalLength = idBuffer.Length + previousStepBuffer.Length + stateBuffer.Length;
34-
await stream.WriteAsync(BitConverter.GetBytes(totalLength), token);
35-
await stream.WriteAsync(idBuffer, token);
36-
await stream.WriteAsync(previousStepBuffer, token);
37-
await stream.WriteAsync(stateBuffer.AsMemory(0, stateLength), token);
38-
await stream.FlushAsync(token);
3927
}
40-
}
28+
else
29+
{
30+
Array.Fill(previousStepBuffer, byte.MinValue);
31+
}
4132

42-
public async Task Serialize(Solver<TState, TStep> solver, Stream stream, CancellationToken token = default)
43-
{
44-
await Serialize(solver.GetAllSolutions(), stream, token);
33+
var stateBuffer = new byte[1024];
34+
var stateLength = stateSerializer.SerializeState(solution.State, stateBuffer);
35+
logger?.LogTrace("State length: {Length}", stateLength);
36+
37+
var totalLength = idBuffer.Length + previousStepBuffer.Length + solutionLengthBuffer.Length + stateLength;
38+
var lengthBytes = BitConverter.GetBytes(totalLength);
39+
logger?.LogTrace("Total length: {Length} ({Bytes})", totalLength, PrintBytes(lengthBytes));
40+
41+
logger?.LogDebug("Writing bytes to file...");
42+
await stream.WriteAsync(lengthBytes, token);
43+
await stream.WriteAsync(idBuffer, token);
44+
await stream.WriteAsync(solutionLengthBuffer, token);
45+
await stream.WriteAsync(previousStepBuffer, token);
46+
await stream.WriteAsync(stateBuffer.AsMemory(0, stateLength), token);
47+
await stream.FlushAsync(token);
4548
}
4649

47-
public async IAsyncEnumerable<(Guid id, Guid parent, TStep step, TState state)> Deserialize(Stream stream, [EnumeratorCancellation] CancellationToken token = default)
50+
public async Task<Solution<TState, TStep>> DeserializeSolutionAsync(Stream stream, CancellationToken token = default)
4851
{
49-
while (stream.CanRead)
52+
void CheckStreamLength(int length)
5053
{
51-
var lengthBuffer = new byte[4];
52-
await stream.ReadExactlyAsync(lengthBuffer, token);
53-
var length = BitConverter.ToInt32(lengthBuffer);
54-
var solutionBuffer = new byte[length];
55-
await stream.ReadExactlyAsync(solutionBuffer, token);
56-
var id = new Guid(new ArraySegment<byte>(solutionBuffer, 0, 16));
57-
var parent = new Guid(new ArraySegment<byte>(solutionBuffer, 16, 16));
58-
var step = stateSerializer.DeserializeStep(new ArraySegment<byte>(solutionBuffer, 32,
59-
stateSerializer.SerializedStepLength));
60-
var start = 32 + stateSerializer.SerializedStepLength;
61-
var stateLength = length - start;
62-
var state = stateSerializer.DeserializeState(new ArraySegment<byte>(solutionBuffer, start, stateLength));
63-
yield return (id, parent, step, state);
54+
if (stream.Position + length > stream.Length)
55+
{
56+
throw new IOException("Input stream too short.");
57+
}
6458
}
59+
logger?.LogDebug("Deserializing solution...");
60+
logger?.LogTrace("Stream length: {Length}", stream.Length);
61+
logger?.LogTrace("Stream at byte {Byte} ({Byte:X8})", stream.Position, stream.Position);
62+
CheckStreamLength(4);
63+
var lengthBuffer = new byte[4];
64+
await stream.ReadExactlyAsync(lengthBuffer, 0, 4, token);
65+
logger?.LogTrace("Length bytes: {Bytes}", PrintBytes(lengthBuffer));
66+
var length = BitConverter.ToInt32(lengthBuffer);
67+
logger?.LogTrace("Read length: {Length} bytes", length);
68+
logger?.LogTrace("Stream at byte {Byte}", stream.Position);
69+
CheckStreamLength(length);
70+
var solutionBuffer = new byte[length];
71+
await stream.ReadExactlyAsync(solutionBuffer, 0, length, token);
72+
var idBytes = new ArraySegment<byte>(solutionBuffer, 0, 16);
73+
var id = new Guid(idBytes);
74+
logger?.LogTrace("Read ID: {Guid:D} ({Bytes})", id, PrintBytes(idBytes));
75+
var parentBytes = new ArraySegment<byte>(solutionBuffer, 16, 16);
76+
var parent = new Guid(parentBytes);
77+
logger?.LogTrace("Parent: {Guid:D} ({Bytes})", parent, PrintBytes(parentBytes));
78+
var solutionLengthBytes = new ArraySegment<byte>(solutionBuffer, 32, 4);
79+
var solutionLength = BitConverter.ToUInt32(solutionLengthBytes);
80+
logger?.LogTrace("Read solution length: {Length}", solutionLength);
81+
var step = parent == Guid.Empty
82+
? default
83+
: stateSerializer.DeserializeStep(new ArraySegment<byte>(solutionBuffer, 32,
84+
stateSerializer.SerializedStepLength));
85+
var start = 32 + stateSerializer.SerializedStepLength;
86+
var stateLength = length - start;
87+
var state = stateSerializer.DeserializeState(new ArraySegment<byte>(solutionBuffer, start,
88+
stateLength));
89+
90+
return new Solution<TState, TStep>(state)
91+
{
92+
Id = id,
93+
Length = solutionLength,
94+
Previous = parent == Guid.Empty ? default : (parent, step!)
95+
};
6596
}
97+
98+
private static string PrintBytes(byte[] bytes) => string.Join("", bytes.Select(b => b.ToString("X2")));
99+
private static string PrintBytes(ReadOnlySpan<byte> bytes) => PrintBytes(bytes.ToArray());
66100
}

Solver.Core/Solution.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@
22

33
public class Solution<TState, TStep>
44
{
5-
public Guid Id { get; init; }
5+
public Guid Id { get; init; } = Guid.NewGuid();
66
public TState State { get; init; }
7-
public (Solution<TState, TStep> Solution, TStep Step)? PreviousStep { get; init; }
8-
public uint Length { get; }
7+
8+
public (Guid Id, TStep Step)? Previous { get; init; }
9+
10+
public uint Length { get; internal init; }
911

1012
public Solution(TState state)
1113
{
1214
State = state;
1315
Length = 0;
1416
}
1517

16-
private Solution(TState state, (Solution<TState, TStep> Solution, TStep step) previousStep)
18+
public Solution(TState state, Solution<TState, TStep> previousSolution, TStep previousStep)
1719
{
1820
State = state;
19-
PreviousStep = previousStep;
20-
Length = 1 + previousStep.Solution.Length;
21+
Previous = (previousSolution.Id, previousStep);
22+
Length = 1 + previousSolution.Length;
2123
}
2224

23-
public Solution<TState, TStep> AddStep(TState state, TStep step) => new(state, (this, step));
25+
public Solution<TState, TStep> AddStep(TState state, TStep step) => new(state, this, step);
2426
}

Solver.Core/Solver.Core.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
11+
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
1012
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133">
1113
<PrivateAssets>all</PrivateAssets>
1214
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

0 commit comments

Comments
 (0)