From c9c9bbe68b3914d2cdc7464da1506cbe9a24e85f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 18:19:10 +0200 Subject: [PATCH] feat(analysis): add coverage profiler and report --- src/Chip8.Analysis/CoverageProfiler.cs | 69 +++++++++++++++++++ src/Chip8.Analysis/CoverageReport.cs | 92 ++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/Chip8.Analysis/CoverageProfiler.cs create mode 100644 src/Chip8.Analysis/CoverageReport.cs diff --git a/src/Chip8.Analysis/CoverageProfiler.cs b/src/Chip8.Analysis/CoverageProfiler.cs new file mode 100644 index 0000000..ff8c3c7 --- /dev/null +++ b/src/Chip8.Analysis/CoverageProfiler.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using Chip8.Core; + +namespace Chip8.Analysis; + +public sealed class CoverageProfiler +{ + public CoverageProfiler(ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress) + { + this.entry = entry; + this.baseAddress = baseAddress; + } + + public ushort entry { get; } + public ushort baseAddress { get; } + + public CoverageReport profile(byte[] program, int maxSteps, Platform platform = Platform.Chip8, Quirks? quirks = null, int seed = 1, bool stopOnSpin = true) + { + var cfg = ControlFlowGraph.build(program, entry, baseAddress); + var reachable = new SortedSet(cfg.reachableAddresses); + + var machine = new Machine(quirks, seed, platform); + machine.loadRom(program); + + var executionCounts = new Dictionary(); + var opcodeHistogram = new Dictionary(); + int steps = 0; + int spinRun = 0; + bool haltedOnSpin = false; + + for (int index = 0; index < maxSteps; index++) + { + if (machine.exited) + { + break; + } + + ushort address = machine.pc; + ushort opcode = (ushort)((machine.memory[address] << 8) | machine.memory[address + 1]); + + increment(executionCounts, address); + increment(opcodeHistogram, opcode); + steps++; + + machine.step(); + + if (stopOnSpin && machine.pc == address) + { + spinRun++; + if (spinRun >= 2) + { + haltedOnSpin = true; + break; + } + } + else + { + spinRun = 0; + } + } + + return new CoverageReport(steps, haltedOnSpin, executionCounts, opcodeHistogram, reachable); + } + + static void increment(Dictionary counter, ushort key) + { + counter[key] = counter.TryGetValue(key, out int value) ? value + 1 : 1; + } +} diff --git a/src/Chip8.Analysis/CoverageReport.cs b/src/Chip8.Analysis/CoverageReport.cs new file mode 100644 index 0000000..55b1787 --- /dev/null +++ b/src/Chip8.Analysis/CoverageReport.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Chip8.Analysis; + +public sealed class CoverageReport +{ + readonly Dictionary executionCounts; + readonly Dictionary opcodeHistogram; + readonly SortedSet reachable; + + public CoverageReport( + int totalSteps, + bool haltedOnSpin, + Dictionary executionCounts, + Dictionary opcodeHistogram, + SortedSet reachable) + { + this.totalSteps = totalSteps; + this.haltedOnSpin = haltedOnSpin; + this.executionCounts = executionCounts; + this.opcodeHistogram = opcodeHistogram; + this.reachable = reachable; + } + + public int totalSteps { get; } + public bool haltedOnSpin { get; } + + public IReadOnlyDictionary executionCountByAddress => executionCounts; + public IReadOnlyDictionary opcodeCounts => opcodeHistogram; + + public IReadOnlyCollection executedAddresses => executionCounts.Keys; + public IReadOnlyCollection reachableAddresses => reachable; + + public int executedInstructionCount => executionCounts.Count; + public int reachableInstructionCount => reachable.Count; + + public int executionCount(ushort address) => + executionCounts.TryGetValue(address, out int value) ? value : 0; + + public IReadOnlyCollection coveredReachableAddresses => + executionCounts.Keys.Where(address => reachable.Contains(address)).ToList(); + + public IReadOnlyCollection executedOutsideReachable => + executionCounts.Keys.Where(address => !reachable.Contains(address)).OrderBy(address => address).ToList(); + + public double coveragePercent + { + get + { + if (reachable.Count == 0) + { + return 0.0; + } + int covered = executionCounts.Keys.Count(address => reachable.Contains(address)); + return 100.0 * covered / reachable.Count; + } + } + + public IReadOnlyList reachableButNotExecuted => + reachable.Where(address => !executionCounts.ContainsKey(address)).ToList(); + + public (ushort address, int count) hotspot() + { + ushort best = 0; + int bestCount = -1; + foreach (var pair in executionCounts) + { + if (pair.Value > bestCount) + { + bestCount = pair.Value; + best = pair.Key; + } + } + return (best, bestCount < 0 ? 0 : bestCount); + } + + public string renderHeatmap() + { + var builder = new StringBuilder(); + builder.Append("coverage: ").Append(coveragePercent.ToString("F1", CultureInfo.InvariantCulture)).Append("% over "); + builder.Append(totalSteps).Append(" steps\n"); + foreach (var address in executionCounts.Keys.OrderBy(value => value)) + { + builder.Append(" 0x").Append(address.ToString("X4", CultureInfo.InvariantCulture)); + builder.Append(" x").Append(executionCounts[address].ToString(CultureInfo.InvariantCulture)).Append('\n'); + } + return builder.ToString(); + } +}