diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core/Runs/Scripts/CheckpointCache.cs | |
| download | godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip | |
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core/Runs/Scripts/CheckpointCache.cs')
| -rw-r--r-- | src/Godosa.Core/Runs/Scripts/CheckpointCache.cs | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/src/Godosa.Core/Runs/Scripts/CheckpointCache.cs b/src/Godosa.Core/Runs/Scripts/CheckpointCache.cs new file mode 100644 index 0000000..1e1dd3a --- /dev/null +++ b/src/Godosa.Core/Runs/Scripts/CheckpointCache.cs @@ -0,0 +1,112 @@ +// Provenance: own design — shared run scripts (spec §5.4) +using System.Buffers.Binary; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Godosa.Core.Runs.Scripts; + +public sealed record LabelState(string Name, long Start, string File, int Line); + +public sealed record RunnerState(long Frame, ulong Digest, List<ScopeState> Scopes, List<LabelState> Labels); + +public sealed record CachedCheckpoint(RunnerState State, byte[] Game); + +public static class CheckpointKeys +{ + public const string RunnerVersion = "run-scripts 1"; + + public static IReadOnlyDictionary<int, string> Compute(ScriptProgram p, string contentVersion, ulong seed) + { + var d = new StateDigest(); + d.Add(RunnerVersion); + d.Add(contentVersion); + d.Add(seed); + d.Add((int)p.Mode); + foreach (var (k, v) in p.Vars.OrderBy(kv => kv.Key, StringComparer.Ordinal)) + d.Add($"{k}={v}"); + var keys = new Dictionary<int, string>(); + for (var i = 0; i < p.Code.Count; i++) + { + var instr = p.Code[i]; + d.Add((int)instr.Op); + d.Add(instr.Format); + d.Add(instr.At.File); + d.Add(instr.Text); + if (instr.Op == Op.Checkpoint) + keys[i] = d.Hex; + } + return keys; + } +} + +public sealed class CheckpointCache(string dir, long capBytes) +{ + private static readonly byte[] Magic = "GRS1"u8.ToArray(); + private static readonly JsonSerializerOptions Json = new() { Converters = { new JsonStringEnumConverter() } }; + + public CachedCheckpoint? Load(string key) + { + var path = Path.Combine(dir, key + ".snap"); + if (!File.Exists(path)) + return null; + byte[] bytes; + try + { + bytes = File.ReadAllBytes(path); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + return null; + } + if (bytes.Length < 8 || !bytes.AsSpan(0, 4).SequenceEqual(Magic)) + return null; + var length = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4)); + if (length < 0 || 8 + length > bytes.Length) + return null; + RunnerState? state; + try + { + state = JsonSerializer.Deserialize<RunnerState>(Encoding.UTF8.GetString(bytes, 8, length), Json); + } + catch (JsonException) + { + return null; // a corrupt entry is a miss + } + return state == null ? null : new CachedCheckpoint(state, bytes[(8 + length)..]); + } + + public void Save(string key, CachedCheckpoint cp) + { + Directory.CreateDirectory(dir); + var json = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(cp.State, Json)); + var path = Path.Combine(dir, key + ".snap"); + var tmp = path + ".tmp"; + using (var f = File.Create(tmp)) + { + f.Write(Magic); + Span<byte> len = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(len, json.Length); + f.Write(len); + f.Write(json); + f.Write(cp.Game); + } + File.Move(tmp, path, overwrite: true); + Evict(path); + } + + private void Evict(string keep) + { + var files = new DirectoryInfo(dir).GetFiles("*.snap").OrderBy(f => f.LastWriteTimeUtc).ToList(); + var total = files.Sum(f => f.Length); + foreach (var f in files) + { + if (total <= capBytes) + break; + if (f.FullName == Path.GetFullPath(keep)) + continue; + total -= f.Length; + f.Delete(); + } + } +} |
