diff --git a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs index 278c557d..d9fa3518 100644 --- a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs @@ -73,7 +73,7 @@ } else { - lastExecEvaluatorTriggerResult = systemRunner.RunEmulatorOneFrame(out _); + lastExecEvaluatorTriggerResult = systemRunner.RunEmulatorOneFrame(); if (lastExecEvaluatorTriggerResult.Triggered) { startMonitor = true; diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/InstrumentationBag.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/InstrumentationBag.cs deleted file mode 100644 index 16bbf811..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/InstrumentationBag.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; - -namespace Highbyte.DotNet6502.App.SilkNetNative.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public static class InstrumentationBag -{ - private static readonly List<(string Name, IStat Stat)> s_stats = new(); - public static IEnumerable<(string Name, IStat Stat)> Stats => s_stats.AsReadOnly(); - public static T Add(string name, T stat) where T : IStat - { - s_stats.Add((name, stat)); - return stat; - } - public static T Add(string name) where T : IStat, new() => Add(name, new T()); - - public static void Remove(string name) => s_stats.RemoveAll(s => s.Name == name); -} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/AveragedStat.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/AveragedStat.cs deleted file mode 100644 index 5c3966ab..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/AveragedStat.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public abstract class AveragedStat : IStat -{ - public double? Value { get; private set; } - private readonly int _sampleCount; - public AveragedStat(int sampleCount) - { - _sampleCount = sampleCount; - } - protected void SetValue(double value) - { - if (this.Value == null) - { - this.Value = value; - } - else - { - this.Value = (this.Value * (_sampleCount - 1) + value) / _sampleCount; - } - } - public abstract string GetDescription(); - - public bool ShouldShow() => this.Value.HasValue; -} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/DisposableCallback.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/DisposableCallback.cs deleted file mode 100644 index e887b776..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/DisposableCallback.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public class DisposableCallback : IDisposable -{ - public event EventHandler? Disposing; - public void Dispose() - { - Disposing?.Invoke(this, EventArgs.Empty); - } -} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/ElapsedMillisecondsStat.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/ElapsedMillisecondsStat.cs deleted file mode 100644 index 86de5e4f..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/ElapsedMillisecondsStat.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; - -public class ElapsedMillisecondsStat : AveragedStat -{ - private double _currentMs; - public ElapsedMillisecondsStat() - : base(10) // Average over 10 samples - { - _currentMs = 0; - } - public void Add(double ms) => _currentMs += ms; - public void Set(double ms) => _currentMs = ms; - public void UpdateStat() - { - SetValue(_currentMs); - _currentMs = 0; - } - - public override string GetDescription() - { - if (this.Value == null) - { - return "null"; - } - //double ms = Value.Value / TimeSpan.TicksPerMillisecond; // 10000 ticks per millisecond - double ms = Value.Value; - - if (ms < 0.01) - { - return "< 0.01ms"; - } - return Math.Round(ms, 2).ToString("0.00") + "ms"; - - } -} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs deleted file mode 100644 index ca5f622a..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Diagnostics; - -namespace Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Dispose is used to measure")] -public class ElapsedMillisecondsTimedStat : AveragedStat -{ - private readonly Stopwatch _sw; - private readonly DisposableCallback _disposableCallback; - public ElapsedMillisecondsTimedStat() - : base(10) // Average over x samples - { - _sw = new Stopwatch(); - _disposableCallback = new DisposableCallback(); - _disposableCallback.Disposing += (o, e) => Stop(); - } - public void Start() => _sw.Restart(); - public void Stop() - { - _sw.Stop(); - //SetValue(_sw.ElapsedMilliseconds); - SetValue(_sw.Elapsed.Ticks); - } - public IDisposable Measure() - { - Start(); - return _disposableCallback; - } - public override string GetDescription() - { - if (this.Value == null) - { - return "null"; - } - - //double ms = Value.Value / 10000.0d; // 10000 ticks per millisecond - double ms = Value.Value / TimeSpan.TicksPerMillisecond; // 10000 ticks per millisecond - - if (ms < 0.01) - { - return "< 0.01ms"; - } - return Math.Round(ms, 2).ToString("0.00") + "ms"; - } -} diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/IStat.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/IStat.cs deleted file mode 100644 index 904606e0..00000000 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/IStat.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public interface IStat -{ - string GetDescription(); - - bool ShouldShow(); -} \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiStatsPanel.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiStatsPanel.cs index d7b4ec35..926eaee8 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiStatsPanel.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiStatsPanel.cs @@ -1,6 +1,5 @@ using System.Numerics; -using Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; -using Highbyte.DotNet6502.App.SilkNetNative.Stats; +using Highbyte.DotNet6502.Instrumentation.Stats; namespace Highbyte.DotNet6502.App.SilkNetNative; @@ -11,12 +10,15 @@ public class SilkNetImGuiStatsPanel : ISilkNetImGuiWindow private const int POS_X = 600; private const int POS_Y = 2; - private const int WIDTH = 400; + private const int WIDTH = 500; private const int HEIGHT = 300; static Vector4 s_LabelColor = new Vector4(0.7f, 0.7f, 0.7f, 1.0f); - public SilkNetImGuiStatsPanel() + private readonly Func> _getStats; + + public SilkNetImGuiStatsPanel(Func> getStats) { + _getStats = getStats; } public void PostOnRender() @@ -28,7 +30,7 @@ public void PostOnRender() //ImGui.SetWindowSize(new Vector2(WIDTH, HEIGHT)); var strings = new List(); - foreach ((string name, IStat stat) in InstrumentationBag.Stats.OrderBy(i => i.Name)) + foreach ((string name, IStat stat) in _getStats().OrderBy(i => i.Name)) { if (stat.ShouldShow()) { diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs index a4fc35e5..2b768792 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetWindow.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; using AutoMapper; -using Highbyte.DotNet6502.App.SilkNetNative; -using Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; -using Highbyte.DotNet6502.App.SilkNetNative.Stats; using Highbyte.DotNet6502.Impl.NAudio; using Highbyte.DotNet6502.Impl.NAudio.NAudioOpenALProvider; using Highbyte.DotNet6502.Impl.SilkNet; using Highbyte.DotNet6502.Impl.Skia; +using Highbyte.DotNet6502.Instrumentation; +using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Logging; using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; @@ -54,16 +52,17 @@ public float CanvasScale public EmulatorState EmulatorState { get; set; } = EmulatorState.Uninitialized; - private readonly ElapsedMillisecondsTimedStat _inputTime = InstrumentationBag.Add("SilkNet-InputTime"); - private readonly ElapsedMillisecondsTimedStat _systemTime = InstrumentationBag.Add("Emulator-SystemTime"); - private readonly ElapsedMillisecondsStat _systemTimeAudio = InstrumentationBag.Add("Emulator-SystemTime-Audio"); // Detailed part of system time - private readonly ElapsedMillisecondsTimedStat _renderTime = InstrumentationBag.Add("SilkNet-RenderTime"); - private readonly PerSecondTimedStat _updateFps = InstrumentationBag.Add("SilkNet-OnUpdateFPS"); - private readonly PerSecondTimedStat _renderFps = InstrumentationBag.Add("SilkNet-OnRenderFPS"); - - private const string CustomSystemStatNamePrefix = "Emulator-SystemTime-Custom-"; - private const string CustomRenderStatNamePrefix = "SilkNet-RenderTime-Custom-"; - private Dictionary _customStats = new(); + private const string HostStatRootName = "SilkNet"; + private const string SystemTimeStatName = "Emulator-SystemTime"; + private const string RenderTimeStatName = "RenderTime"; + private const string InputTimeStatName = "InputTime"; + private const string AudioTimeStatName = "AudioTime"; + private readonly ElapsedMillisecondsTimedStat _systemTime = InstrumentationBag.Add($"{HostStatRootName}-{SystemTimeStatName}"); + private readonly ElapsedMillisecondsTimedStat _renderTime = InstrumentationBag.Add($"{HostStatRootName}-{RenderTimeStatName}"); + private readonly ElapsedMillisecondsTimedStat _inputTime = InstrumentationBag.Add($"{HostStatRootName}-{InputTimeStatName}"); + //private readonly ElapsedMillisecondsTimedStat _audioTime = InstrumentationBag.Add($"{HostStatRootName}-AudioTimeStatName}"); + private readonly PerSecondTimedStat _updateFps = InstrumentationBag.Add($"{HostStatRootName}-OnUpdateFPS"); + private readonly PerSecondTimedStat _renderFps = InstrumentationBag.Add($"{HostStatRootName}-OnRenderFPS"); // Render context container for SkipSharp (surface/canvas) and SilkNetOpenGl private SilkNetRenderContextContainer _silkNetRenderContextContainer; @@ -80,7 +79,7 @@ public float CanvasScale private SilkNetImGuiMonitor _monitor; public SilkNetImGuiMonitor Monitor => _monitor; - // Stats panel + // Instrumentations panel private SilkNetImGuiStatsPanel _statsPanel; public SilkNetImGuiStatsPanel StatsPanel => _statsPanel; @@ -168,7 +167,7 @@ protected void OnLoad() protected void OnClosing() { - // Dispose Monitor/Stats panel + // Dispose Monitor/Instrumentations panel // _monitor.Cleanup(); // _statsPanel.Cleanup(); DestroyImGuiController(); @@ -249,33 +248,6 @@ public void SetCurrentSystem(string systemName) } } - private void InitCustomSystemStats() - { - // Remove any existing custom system stats - foreach (var existingCustomStatName in _customStats.Keys) - { - if (existingCustomStatName.StartsWith(CustomSystemStatNamePrefix) - || existingCustomStatName.StartsWith(CustomRenderStatNamePrefix)) - { - InstrumentationBag.Remove(existingCustomStatName); - _customStats.Remove(existingCustomStatName); - } - } - // Add any custom system stats for selected system - var system = _systemRunner.System; - foreach (var customStatName in system.DetailedStatNames) - { - _customStats.Add($"{CustomSystemStatNamePrefix}{customStatName}", InstrumentationBag.Add($"{CustomSystemStatNamePrefix}{customStatName}")); - } - - // Add any custom system stats for selected renderer - var renderer = _systemRunner.Renderer; - foreach (var customStatName in renderer.DetailedStatNames) - { - _customStats.Add($"{CustomRenderStatNamePrefix}{customStatName}", InstrumentationBag.Add($"{CustomRenderStatNamePrefix}{customStatName}")); - } - } - public void SetVolumePercent(float volumePercent) { _defaultAudioVolumePercent = volumePercent; @@ -294,8 +266,6 @@ public void Start() if (EmulatorState == EmulatorState.Uninitialized) _systemRunner = _systemList.BuildSystemRunner(_currentSystemName).Result; - InitCustomSystemStats(); - _monitor.Init(_systemRunner!); _systemRunner!.AudioHandler.StartPlaying(); @@ -364,25 +334,7 @@ private void RunEmulator() ExecEvaluatorTriggerResult execEvaluatorTriggerResult; using (_systemTime.Measure()) { - execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(out var detailedStats); - - if (detailedStats.ContainsKey("Audio")) - { - _systemTimeAudio.Set(detailedStats["Audio"]); - _systemTimeAudio.UpdateStat(); - } - - // Update custom system stats - // TODO: Make custom system stats less messy? - foreach (var detailedStatName in detailedStats.Keys) - { - var statLookup = _customStats.Keys.SingleOrDefault(x => x.EndsWith(detailedStatName)); - if (statLookup != null) - { - _customStats[$"{CustomSystemStatNamePrefix}{detailedStatName}"].Set(detailedStats[detailedStatName]); - _customStats[$"{CustomSystemStatNamePrefix}{detailedStatName}"].UpdateStat(); - } - } + execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(); } // Show monitor if we encounter breakpoint or other break @@ -419,28 +371,14 @@ private void RenderEmulator(double deltaTime) using (_renderTime.Measure()) { // Render emulator system screen - _systemRunner!.Draw(out var detailedStats); + _systemRunner!.Draw(); // Flush the SkiaSharp Context _silkNetRenderContextContainer.SkiaRenderContext.GetGRContext().Flush(); - - // Update custom system stats - // TODO: Make custom system stats less messy? - foreach (var detailedStatName in detailedStats.Keys) - { - var statLookup = _customStats.Keys.SingleOrDefault(x => x.EndsWith(detailedStatName)); - if (statLookup != null) - { - _customStats[$"{CustomRenderStatNamePrefix}{detailedStatName}"].Set(detailedStats[detailedStatName]); - _customStats[$"{CustomRenderStatNamePrefix}{detailedStatName}"].UpdateStat(); - } - } - } emulatorRendered = true; - // SilkNet windows are what's known as "double-buffered". In essence, the window manages two buffers. // One is rendered to while the other is currently displayed by the window. // This avoids screen tearing, a visual artifact that can happen if the buffer is modified while being displayed. @@ -545,7 +483,17 @@ private SilkNetImGuiMonitor CreateMonitorUI(SilkNetImGuiStatsPanel statsPanel, M private SilkNetImGuiStatsPanel CreateStatsUI() { - return new SilkNetImGuiStatsPanel(); + return new SilkNetImGuiStatsPanel(GetStats); + } + + private List<(string name, IStat stat)> GetStats() + { + return InstrumentationBag.Stats + .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{SystemTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.Renderer.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{RenderTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.AudioHandler.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{AudioTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.InputHandler.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{InputTimeStatName}-{x.Name}", x.Stat))) + .ToList(); } private SilkNetImGuiLogsPanel CreateLogsUI(DotNet6502InMemLogStore logStore, DotNet6502InMemLoggerConfiguration logConfig) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/InstrumentationBag.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/InstrumentationBag.cs deleted file mode 100644 index 40eb3998..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/InstrumentationBag.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; - -namespace Highbyte.DotNet6502.App.WASM; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public static class InstrumentationBag -{ - private static readonly List<(string Name, IStat Stat)> s_stats = new(); - public static IEnumerable<(string Name, IStat Stat)> Stats => s_stats.AsReadOnly(); - public static T Add(string name, T stat) where T : IStat - { - s_stats.Add((name, stat)); - return stat; - } - public static T Add(string name) where T : IStat, new() => Add(name, new T()); - public static void Clear() => s_stats.Clear(); - - public static void Remove(string name) => s_stats.RemoveAll(s => s.Name == name); -} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/DisposableCallback.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/DisposableCallback.cs deleted file mode 100644 index c06b0787..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/DisposableCallback.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public class DisposableCallback : IDisposable -{ - public event EventHandler? Disposing; - public void Dispose() - { - Disposing?.Invoke(this, EventArgs.Empty); - } -} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/ElapsedMillisecondsStat.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/ElapsedMillisecondsStat.cs deleted file mode 100644 index b5e5f617..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/ElapsedMillisecondsStat.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; - -public class ElapsedMillisecondsStat : AveragedStat -{ - private double _currentMs; - public ElapsedMillisecondsStat() - : base(10) // Average over 10 samples - { - _currentMs = 0; - } - public void Add(double ms) => _currentMs += ms; - public void Set(double ms) => _currentMs = ms; - public void UpdateStat() - { - SetValue(_currentMs); - _currentMs = 0; - } - - public override string GetDescription() - { - if (this.Value == null) - { - return "null"; - } - //double ms = Value.Value / TimeSpan.TicksPerMillisecond; // 10000 ticks per millisecond - double ms = Value.Value; - - if (ms < 0.01) - { - return "< 0.01ms"; - } - return Math.Round(ms, 2).ToString("0.00") + "ms"; - - } -} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs deleted file mode 100644 index 4044a23d..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Diagnostics; - -namespace Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Dispose is used to measure")] -public class ElapsedMillisecondsTimedStat : AveragedStat -{ - private readonly Stopwatch _sw; - private readonly DisposableCallback _disposableCallback; - public ElapsedMillisecondsTimedStat() - : base(10) // Average over 10 samples - { - _sw = new Stopwatch(); - _disposableCallback = new DisposableCallback(); - _disposableCallback.Disposing += (o, e) => Stop(); - } - public void Start() => _sw.Restart(); - public void Stop() - { - _sw.Stop(); - SetValue(_sw.ElapsedMilliseconds); - } - public IDisposable Measure() - { - Start(); - return _disposableCallback; - } - public override string GetDescription() - { - if (this.Value == null) - { - return "null"; - } - if (this.Value < 0.01) - { - return "< 0.01ms"; - } - return Math.Round(this.Value ?? 0, 2).ToString("0.00") + "ms"; - } -} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/PerSecondTimedStat.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/PerSecondTimedStat.cs deleted file mode 100644 index 6b55ca14..00000000 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/PerSecondTimedStat.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Diagnostics; - -namespace Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; - -// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET -public class PerSecondTimedStat : AveragedStat -{ - private readonly Stopwatch _sw; - public PerSecondTimedStat() - : base(60) // Average over 60 samples - { - _sw = new Stopwatch(); - } - - public void Update() - { - if (_sw.IsRunning) - { - //var elapsedMs = _sw.ElapsedMilliseconds; - var elapsedMs = _sw.Elapsed.TotalMilliseconds; -#if DEBUG - if (elapsedMs == 0) - throw new NotImplementedException("Elapsed 0.0 milliseconds, cannot handle division by 0"); -#endif - double perSecond = 1000.0 / elapsedMs; - SetValue(perSecond); - } - _sw.Restart(); - } - - public override string GetDescription() - { - if (this.Value == null) - { - return "null"; - } - if (this.Value < 0.01) - { - return "< 0.01"; - } - return Math.Round(this.Value ?? 0, 2).ToString(); - } -} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs index 0e318159..0dc0218c 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -97,7 +97,7 @@ private double Scale private WasmHost? _wasmHost = default!; public WasmHost WasmHost => _wasmHost!; - private string _statsString = "Stats: calculating..."; + private string _statsString = "Instrumentations: calculating..."; private string _debugString = ""; private string _windowWidthStyle = "0px"; diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs index e398ef68..8a0f3726 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Skia/WasmHost.cs @@ -1,7 +1,8 @@ -using Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; using Highbyte.DotNet6502.Impl.AspNet; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Highbyte.DotNet6502.Impl.Skia; +using Highbyte.DotNet6502.Instrumentation; +using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Systems; using Toolbelt.Blazor.Gamepad; @@ -39,18 +40,17 @@ public class WasmHost : IDisposable public WasmMonitor Monitor { get; private set; } - private readonly ElapsedMillisecondsTimedStat _inputTime; + private const string HostStatRootName = "WASM"; + private const string SystemTimeStatName = "Emulator-SystemTime"; + private const string RenderTimeStatName = "RenderTime"; + private const string InputTimeStatName = "InputTime"; + private const string AudioTimeStatName = "AudioTime"; private readonly ElapsedMillisecondsTimedStat _systemTime; - private readonly ElapsedMillisecondsStat _systemTimeAudio; // Part of systemTime, but we want to show it separately private readonly ElapsedMillisecondsTimedStat _renderTime; + private readonly ElapsedMillisecondsTimedStat _inputTime; private readonly PerSecondTimedStat _updateFps; private readonly PerSecondTimedStat _renderFps; - private const string CustomSystemStatNamePrefix = "Emulator-SystemTime-Custom-"; - private const string CustomRenderStatNamePrefix = "WASMSkiaSharp-RenderTime-Custom-"; - private Dictionary _customStats = new(); - - private const int STATS_EVERY_X_FRAME = 60 * 1; private int _statsFrameCount = 0; @@ -84,14 +84,12 @@ public WasmHost( // Init stats InstrumentationBag.Clear(); - _inputTime = InstrumentationBag.Add("WASM-InputTime"); - - _systemTime = InstrumentationBag.Add("Emulator-SystemTime"); - _systemTimeAudio = InstrumentationBag.Add("Emulator-SystemTime-Audio"); - - _renderTime = InstrumentationBag.Add("WASMSkiaSharp-RenderTime"); - _updateFps = InstrumentationBag.Add("WASMSkiaSharp-OnUpdateFPS"); - _renderFps = InstrumentationBag.Add("WASMSkiaSharp-OnRenderFPS"); + _systemTime = InstrumentationBag.Add($"{HostStatRootName}-{SystemTimeStatName}"); + _inputTime = InstrumentationBag.Add($"{HostStatRootName}-{InputTimeStatName}"); + //_audioTime = InstrumentationBag.Add($"{HostStatRootName}-{AudioTimeStatName}"); + _renderTime = InstrumentationBag.Add($"{HostStatRootName}-{RenderTimeStatName}"); + _updateFps = InstrumentationBag.Add($"{HostStatRootName}-OnUpdateFPS"); + _renderFps = InstrumentationBag.Add($"{HostStatRootName}-OnRenderFPS"); Initialized = false; } @@ -111,8 +109,6 @@ public async Task Init(SKCanvas canvas, GRContext grContext, AudioContextSync au Monitor = new WasmMonitor(_jsRuntime, _systemRunner, _emulatorConfig, _setMonitorState); - InitCustomSystemStats(); - Initialized = true; } @@ -143,33 +139,6 @@ public void Start() _updateTimer!.Start(); } - private void InitCustomSystemStats() - { - // Remove any existing custom system stats - foreach (var existingCustomStatName in _customStats.Keys) - { - if (existingCustomStatName.StartsWith(CustomSystemStatNamePrefix) - || existingCustomStatName.StartsWith(CustomRenderStatNamePrefix)) - { - InstrumentationBag.Remove(existingCustomStatName); - _customStats.Remove(existingCustomStatName); - } - } - // Add any custom system stats for selected system - var system = _systemRunner.System; - foreach (var customStatName in system.DetailedStatNames) - { - _customStats.Add($"{CustomSystemStatNamePrefix}{customStatName}", InstrumentationBag.Add($"{CustomSystemStatNamePrefix}{customStatName}")); - } - - // Add any custom system stats for selected renderer - var renderer = _systemRunner.Renderer; - foreach (var customStatName in renderer.DetailedStatNames) - { - _customStats.Add($"{CustomRenderStatNamePrefix}{customStatName}", InstrumentationBag.Add($"{CustomRenderStatNamePrefix}{customStatName}")); - } - } - public void Cleanup() { if (_updateTimer != null) @@ -224,25 +193,7 @@ private void EmulatorRunOneFrame() ExecEvaluatorTriggerResult execEvaluatorTriggerResult; using (_systemTime.Measure()) { - execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(out Dictionary detailedStats); - - if (detailedStats.ContainsKey("Audio")) - { - _systemTimeAudio.Set(detailedStats["Audio"]); - _systemTimeAudio.UpdateStat(); - } - - // Update custom system stats - // TODO: Make custom system stats less messy? - foreach (var detailedStatName in detailedStats.Keys) - { - var statLookup = _customStats.Keys.SingleOrDefault(x => x.EndsWith(detailedStatName)); - if (statLookup != null) - { - _customStats[$"{CustomSystemStatNamePrefix}{detailedStatName}"].Set(detailedStats[detailedStatName]); - _customStats[$"{CustomSystemStatNamePrefix}{detailedStatName}"].UpdateStat(); - } - } + execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(); } _statsFrameCount++; @@ -272,23 +223,11 @@ public void Render(SKCanvas canvas, GRContext grContext) _skCanvas.Scale((float)_emulatorConfig.CurrentDrawScale); using (_renderTime.Measure()) { - _systemRunner.Draw(out Dictionary detailedStats); + _systemRunner.Draw(); //using (new SKAutoCanvasRestore(skCanvas)) //{ // _systemRunner.Draw(skCanvas); //} - - // Update custom system stats - // TODO: Make custom system stats less messy? - foreach (var detailedStatName in detailedStats.Keys) - { - var statLookup = _customStats.Keys.SingleOrDefault(x => x.EndsWith(detailedStatName)); - if (statLookup != null) - { - _customStats[$"{CustomRenderStatNamePrefix}{detailedStatName}"].Set(detailedStats[detailedStatName]); - _customStats[$"{CustomRenderStatNamePrefix}{detailedStatName}"].UpdateStat(); - } - } } } @@ -306,7 +245,13 @@ private string GetStats() { string stats = ""; - foreach ((string name, IStat stat) in InstrumentationBag.Stats.OrderBy(i => i.Name)) + var allStats = InstrumentationBag.Stats + .Union(_systemRunner.System.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{SystemTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.Renderer.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{RenderTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.AudioHandler.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{AudioTimeStatName}-{x.Name}", x.Stat))) + .Union(_systemRunner.InputHandler.Instrumentations.Stats.Select(x => (Name: $"{HostStatRootName}-{InputTimeStatName}-{x.Name}", x.Stat))) + .ToList(); + foreach ((string name, IStat stat) in allStats.OrderBy(i => i.Name)) { if (stat.ShouldShow()) { @@ -322,18 +267,18 @@ private string GetDebugMessages() { string debugMessages = ""; - var inputStats = _systemRunner.InputHandler.GetStats(); - var inputStatsOneString = string.Join(" # ", inputStats); + var inputDebugInfo = _systemRunner.InputHandler.GetDebugInfo(); + var inputStatsOneString = string.Join(" # ", inputDebugInfo); debugMessages += $"{BuildHtmlString("INPUT", "header")}: {BuildHtmlString(inputStatsOneString, "value")} "; - //foreach (var message in inputStats) + //foreach (var message in inputDebugInfo) //{ // if (debugMessages != "") // debugMessages += "
"; // debugMessages += $"{BuildHtmlString("DEBUG INPUT", "header")}: {BuildHtmlString(message, "value")} "; //} - var audioStats = _systemRunner.AudioHandler.GetStats(); - foreach (var message in audioStats) + var audioDebugInfo = _systemRunner.AudioHandler.GetDebugInfo(); + foreach (var message in audioDebugInfo) { if (debugMessages != "") debugMessages += "
"; diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMAudioHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMAudioHandler.cs index 09436400..92e40252 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMAudioHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Audio/C64WASMAudioHandler.cs @@ -3,6 +3,7 @@ using Highbyte.DotNet6502.Systems.Commodore64.Audio; using Highbyte.DotNet6502.Impl.AspNet.JSInterop.BlazorWebAudioSync; using Microsoft.Extensions.Logging; +using Highbyte.DotNet6502.Instrumentation; namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Audio; @@ -39,20 +40,18 @@ public class C64WASMAudioHandler : IAudioHandler {3, new C64WASMVoiceContext(3) }, }; - private readonly List _stats = new(); - private readonly ILogger _logger; + public List GetDebugInfo() => new(); + + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + public C64WASMAudioHandler(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(typeof(C64WASMAudioHandler).Name); } - public List GetStats() - { - return _stats; - } - public void Init(C64 system, WASMAudioHandlerContext audioHandlerContext) { _audioHandlerContext = audioHandlerContext; @@ -271,15 +270,6 @@ private void AddDebugMessage(string msg, int? voice = null, SidVoiceWaveForm? si } _logger.LogDebug(formattedMsg); - - //var time = DateTime.Now.ToString("HH:mm:ss.fff"); - //formattedMsg = $"{time}: {formattedMsg}"; - ////var threadId = Environment.CurrentManagedThreadId; - ////_stats.Insert(0, $"{time} ({threadId}): {msg}"); - //_stats.Insert(0, formattedMsg); - - //if (_stats.Count > MAX_DEBUG_MESSAGES) - // _stats.RemoveAt(MAX_DEBUG_MESSAGES); } //private Task[] CreateSoundTasks(InternalSidState sidInternalStateClone) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs index 30aadafc..eaab239d 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; @@ -13,6 +14,9 @@ public class C64AspNetInputHandler : IInputHandler(); @@ -152,7 +156,7 @@ private HashSet GetC64JoystickActionsFromAspNetGamepad(HashSe } - public List GetStats() + public List GetDebugInfo() { List list = new(); if (_inputHandlerContext == null) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Generic/Input/GenericComputerAspNetInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Generic/Input/GenericComputerAspNetInputHandler.cs index a7086641..158e14ab 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Generic/Input/GenericComputerAspNetInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Generic/Input/GenericComputerAspNetInputHandler.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Generic.Config; @@ -8,7 +9,11 @@ public class GenericComputerAspNetInputHandler : IInputHandler _stats = new(); + + public List GetDebugInfo() => new(); + + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); public GenericComputerAspNetInputHandler(EmulatorInputConfig emulatorInputConfig) { @@ -57,9 +62,4 @@ private void CaptureKeyboard(GenericComputer genericComputer) // Only way to tell the "GenericComputer" that a Key is no longer pressed is to set KeyDownAddress to 0... genericComputer.Mem[_emulatorInputConfig.KeyDownAddress] = 0x00; } - - public List GetStats() - { - return _stats; - } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs index 1256a42b..e40dba7f 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Audio; @@ -31,8 +32,6 @@ public class C64NAudioAudioHandler : IAudioHandler _stats = new(); - private readonly ILogger _logger; public C64NAudioAudioHandler(ILoggerFactory loggerFactory) @@ -40,10 +39,11 @@ public C64NAudioAudioHandler(ILoggerFactory loggerFactory) _logger = loggerFactory.CreateLogger(typeof(C64NAudioAudioHandler).Name); } - public List GetStats() - { - return _stats; - } + public List GetDebugInfo() => new(); + + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + public void Init(C64 system, NAudioAudioHandlerContext audioHandlerContext) { diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Input/C64SadConsoleInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Input/C64SadConsoleInputHandler.cs index 9d5904c8..05ad13cf 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Input/C64SadConsoleInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Input/C64SadConsoleInputHandler.cs @@ -1,4 +1,5 @@ using Highbyte.DotNet6502; +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; @@ -9,10 +10,13 @@ namespace Highbyte.DotNet6502.Impl.SadConsole.Commodore64.Input; public class C64SadConsoleInputHandler : IInputHandler { private SadConsoleInputHandlerContext? _inputHandlerContext; - private readonly List _stats = new(); + private readonly List _debugInfo = new(); private readonly C64SadConsoleKeyboard _c64SadConsoleKeyboard; private readonly ILogger _logger; + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + public C64SadConsoleInputHandler(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); @@ -99,8 +103,8 @@ private List GetC64KeysFromSadConsoleKeys(List keysDown, out bool return c64KeysDown; } - public List GetStats() + public List GetDebugInfo() { - return _stats; + return _debugInfo; } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs index a64e4402..660a5c10 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Commodore64/Video/C64SadConsoleRenderer.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Video; @@ -9,8 +10,8 @@ public class C64SadConsoleRenderer : IRenderer private SadConsoleRenderContext _sadConsoleRenderContext = default!; private C64SadConsoleColors _c64SadConsoleColors = default!; - public bool HasDetailedStats => false; - public List DetailedStatNames => new List(); + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); public C64SadConsoleRenderer() { @@ -27,15 +28,15 @@ public void Init(ISystem system, IRenderContext renderContext) Init((C64)system, (SadConsoleRenderContext)renderContext); } - public void Draw(C64 c64, Dictionary detailedStats) + public void Draw(C64 c64) { RenderMainScreen(c64); RenderBorder(c64); } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { - Draw((C64)system, detailedStats); + Draw((C64)system); } private void RenderMainScreen(C64 c64) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Input/GenericSadConsoleInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Input/GenericSadConsoleInputHandler.cs index c7201bea..f0a13016 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Input/GenericSadConsoleInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Input/GenericSadConsoleInputHandler.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Generic.Config; @@ -11,7 +12,12 @@ public class GenericSadConsoleInputHandler : IInputHandler _stats = new(); + + public List GetDebugInfo() => new(); + + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + public GenericSadConsoleInputHandler(EmulatorInputConfig emulatorInputConfig, ILoggerFactory loggerFactory) { @@ -69,8 +75,4 @@ private void CaptureRandomNumber(GenericComputer system) var rnd = (byte)new Random().Next(0, 255); emulatorMem[_emulatorInputConfig.RandomValueAddress] = rnd; } - public List GetStats() - { - return _stats; - } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs index 18ecce5a..4c09a14e 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/Generic/Video/GenericSadConsoleRenderer.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Generic.Config; @@ -10,8 +11,7 @@ public class GenericSadConsoleRenderer : IRenderer false; - public List DetailedStatNames => new List(); + public Instrumentations Instrumentations { get; } = new(); public GenericSadConsoleRenderer(EmulatorScreenConfig emulatorScreenConfig) { @@ -30,16 +30,16 @@ public void Init(ISystem system, IRenderContext renderContext) Init((GenericComputer)system, (SadConsoleRenderContext)renderContext); } - public void Draw(GenericComputer system, Dictionary detailedStats) + public void Draw(GenericComputer system) { RenderMainScreen(system); if (_emulatorScreenConfig.BorderCols > 0 || _emulatorScreenConfig.BorderRows > 0) RenderBorder(system); } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { - Draw((GenericComputer)system, detailedStats); + Draw((GenericComputer)system); } private void RenderMainScreen(GenericComputer system) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs index 7717b35c..f321cca7 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SadConsole/SadConsoleMain.cs @@ -87,10 +87,10 @@ private void UpdateSadConsole(object sender, GameHost e) _systemRunner.ProcessInput(); // Run CPU for one frame - var execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(out _); + var execEvaluatorTriggerResult = _systemRunner.RunEmulatorOneFrame(); // Update SadConsole screen - _systemRunner.Draw(out Dictionary detailedStats); + _systemRunner.Draw(); if (execEvaluatorTriggerResult.Triggered) { diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs index 314da6f8..8a4576f8 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; @@ -8,12 +9,16 @@ namespace Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Input; public class C64SilkNetInputHandler : IInputHandler { private SilkNetInputHandlerContext? _inputHandlerContext; - private readonly List _stats = new(); private readonly C64SilkNetKeyboard _c64SilkNetKeyboard; //private readonly C64SilkNetGamepad _c64SilkNetGamepad; private readonly ILogger _logger; private readonly C64SilkNetConfig _c64SilkNetConfig; + public List GetDebugInfo() => new(); + + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + public C64SilkNetInputHandler(ILoggerFactory loggerFactory, C64SilkNetConfig c64SilkNetConfig) { @@ -154,9 +159,4 @@ private HashSet GetC64JoystickActionsFromSilkNetGamepad(HashS } return c64JoystickActions; } - - public List GetStats() - { - return _stats; - } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRenderer.cs index fa4cc1fb..941eeee5 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Video/C64SilkNetOpenGlRenderer.cs @@ -1,5 +1,7 @@ using System.Numerics; using Highbyte.DotNet6502.Impl.SilkNet.OpenGLHelpers; +using Highbyte.DotNet6502.Instrumentation; +using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Config; @@ -17,8 +19,7 @@ public class C64SilkNetOpenGlRenderer : IRenderer true; - public List DetailedStatNames => new List() { }; + public Instrumentations Instrumentations { get; } = new(); // Types for Uniform Buffer Objects, must align to 16 bytes. public struct TextData @@ -191,13 +192,8 @@ public void Init(ISystem system, IRenderContext renderContext) Init((C64)system, (SilkNetOpenGlRenderContext)renderContext); } - public void Draw(C64 c64, Dictionary detailedStats) + public void Draw(C64 c64) { - foreach (var detailedStatName in DetailedStatNames) - { - detailedStats[detailedStatName] = 0; - } - var vic2 = c64.Vic2; var vic2Mem = vic2.Vic2Mem; var vic2Screen = vic2.Vic2Screen; @@ -330,9 +326,9 @@ public void Draw(C64 c64, Dictionary detailedStats) _gl.DrawArrays(GLEnum.Triangles, 0, 6); } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { - Draw((C64)system, detailedStats); + Draw((C64)system); } private TextData[] BuildTextScreenData(C64 c64) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Generic/Input/GenericComputerSilkNetInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Generic/Input/GenericComputerSilkNetInputHandler.cs index cc6d5ae9..0efcdf64 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Generic/Input/GenericComputerSilkNetInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Generic/Input/GenericComputerSilkNetInputHandler.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Generic.Config; @@ -8,7 +9,10 @@ public class GenericComputerSilkNetInputHandler : IInputHandler _stats = new(); + public List GetDebugInfo() => new(); + + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); public GenericComputerSilkNetInputHandler(EmulatorInputConfig emulatorInputConfig) { @@ -50,9 +54,4 @@ private void CaptureKeyboard(GenericComputer genericComputer) genericComputer.Mem[_emulatorInputConfig.KeyDownAddress] = 0x00; } } - - public List GetStats() - { - return _stats; - } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.Skia/Commodore64/Video/C64SkiaRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.Skia/Commodore64/Video/C64SkiaRenderer.cs index b404ff0b..c367bb85 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.Skia/Commodore64/Video/C64SkiaRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.Skia/Commodore64/Video/C64SkiaRenderer.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Highbyte.DotNet6502.Instrumentation; +using Highbyte.DotNet6502.Instrumentation.Stats; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Config; @@ -33,30 +34,23 @@ public class C64SkiaRenderer : IRenderer // Sprite drawing variables private readonly SKImage[] _spriteImages; - public bool HasDetailedStats => true; - public List DetailedStatNames => new List() - { - DrawBorderStatName, - DrawBackgroundStatName, - DrawTextScreenStatName, - DrawSpritesStatName, - }; - private const string DrawBorderStatName = "Border"; - private const string DrawBackgroundStatName = "Background"; - private const string DrawTextScreenStatName = "TextScreen"; - private const string DrawSpritesStatName = "Sprites"; - private Dictionary _detailedStatStopwatches = new() - { - {DrawBorderStatName, new Stopwatch() }, - {DrawBackgroundStatName, new Stopwatch() }, - {DrawTextScreenStatName, new Stopwatch() }, - {DrawSpritesStatName, new Stopwatch() } - }; + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + private const string StatsCategory = "SkiaSharp-Custom"; + private readonly ElapsedMillisecondsTimedStat _borderStat; + private readonly ElapsedMillisecondsTimedStat _backgroundStat; + private readonly ElapsedMillisecondsTimedStat _textScreenStat; + private readonly ElapsedMillisecondsTimedStat _spritesStat; public C64SkiaRenderer() { _charGen = new CharGen(); _spriteImages = new SKImage[Vic2SpriteManager.NUMBERS_OF_SPRITES]; + + _backgroundStat = Instrumentations.Add($"{StatsCategory}-Background"); + _borderStat = Instrumentations.Add($"{StatsCategory}-Border"); + _spritesStat = Instrumentations.Add($"{StatsCategory}-Sprites"); + _textScreenStat = Instrumentations.Add($"{StatsCategory}-TextScreen"); } public void Init(C64 c64, SkiaRenderContext skiaRenderContext) @@ -73,51 +67,42 @@ public void Init(ISystem system, IRenderContext renderContext) Init((C64)system, (SkiaRenderContext)renderContext); } - public void Draw(C64 c64, Dictionary detailedStats) + public void Draw(C64 c64) { - foreach (var detailedStatName in DetailedStatNames) + var canvas = _getSkCanvas(); + canvas.Clear(); + + using (_borderStat.Measure()) { - detailedStats[detailedStatName] = 0; + //DrawSimpleBorder(c64, canvas); + DrawRasterLinesBorder(c64, canvas); } - var canvas = _getSkCanvas(); - canvas.Clear(); + using (_backgroundStat.Measure()) + { + //DrawSimpleBackground(c64, canvas); + DrawRasterLinesBackground(c64, canvas); + } - var swBorder = _detailedStatStopwatches[DrawBorderStatName]; - swBorder.Restart(); - //DrawSimpleBorder(c64, canvas); - DrawRasterLinesBorder(c64, canvas); - swBorder.Stop(); - detailedStats[DrawBorderStatName] = swBorder.Elapsed.TotalMilliseconds; - - var swBg = _detailedStatStopwatches[DrawBackgroundStatName]; - swBg.Restart(); - //DrawSimpleBackground(c64, canvas); - DrawRasterLinesBackground(c64, canvas); - swBg.Stop(); - detailedStats[DrawBackgroundStatName] = swBg.Elapsed.TotalMilliseconds; - - var swSprites = _detailedStatStopwatches[DrawSpritesStatName]; - swSprites.Restart(); - RenderSprites(c64, canvas, spritesWithPriorityOverForeground: false); - swSprites.Stop(); - detailedStats[DrawSpritesStatName] = swBg.Elapsed.TotalMilliseconds; - - var swTextScreen = _detailedStatStopwatches[DrawTextScreenStatName]; - swTextScreen.Restart(); - RenderMainScreen(c64, canvas); - swTextScreen.Stop(); - detailedStats[DrawTextScreenStatName] = swTextScreen.Elapsed.TotalMilliseconds; - - swSprites.Restart(); - RenderSprites(c64, canvas, spritesWithPriorityOverForeground: true); - swSprites.Stop(); - detailedStats[DrawBackgroundStatName] += swSprites.Elapsed.TotalMilliseconds; + using (_spritesStat.Measure()) + { + RenderSprites(c64, canvas, spritesWithPriorityOverForeground: false); + } + + using (_textScreenStat.Measure()) + { + RenderMainScreen(c64, canvas); + } + + using (_spritesStat.Measure(cont: true)) + { + RenderSprites(c64, canvas, spritesWithPriorityOverForeground: true); + } } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { - Draw((C64)system, detailedStats); + Draw((C64)system); } private void InitCharset(C64 c64) diff --git a/src/libraries/Highbyte.DotNet6502.Impl.Skia/Generic/Video/GenericComputerSkiaRenderer.cs b/src/libraries/Highbyte.DotNet6502.Impl.Skia/Generic/Video/GenericComputerSkiaRenderer.cs index d1e2115f..6fb381de 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.Skia/Generic/Video/GenericComputerSkiaRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.Skia/Generic/Video/GenericComputerSkiaRenderer.cs @@ -1,4 +1,6 @@ using System.Reflection; +using Highbyte.DotNet6502.Instrumentation.Stats; +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Generic.Config; @@ -16,8 +18,7 @@ public class GenericComputerSkiaRenderer : IRenderer false; - public List DetailedStatNames => new List(); + public Instrumentations Instrumentations { get; } = new(); public GenericComputerSkiaRenderer(EmulatorScreenConfig emulatorScreenConfig) { @@ -43,7 +44,7 @@ public void Init(ISystem system, IRenderContext renderContext) Init((GenericComputer)system, (SkiaRenderContext)renderContext); } - public void Draw(GenericComputer genericComputer, Dictionary detailedStats) + public void Draw(GenericComputer genericComputer) { var mem = genericComputer.Mem; var canvas = _getSkCanvas(); @@ -82,9 +83,9 @@ public void Draw(GenericComputer genericComputer, Dictionary det } } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { - Draw((GenericComputer)system, detailedStats); + Draw((GenericComputer)system); } private string GetDrawTextFromCharacter(byte chr) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/C64.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/C64.cs index 38859ba9..d977afe4 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/C64.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/C64.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using Highbyte.DotNet6502.Instrumentation.Stats; +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Monitor.SystemSpecific; using Highbyte.DotNet6502.Systems.Commodore64.Audio; using Highbyte.DotNet6502.Systems.Commodore64.Config; @@ -41,17 +43,11 @@ public class C64 : ISystem, ISystemMonitor private readonly ILogger _logger; public const ushort BASIC_LOAD_ADDRESS = 0x0801; - // Detailed stats - public bool HasDetailedStats => true; - public List DetailedStatNames => new List() - { - SpriteCollisionStatName - }; - private const string SpriteCollisionStatName = "SpriteCollision"; - private Dictionary _detailedStatStopwatches = new() - { - {SpriteCollisionStatName, new Stopwatch() } - }; + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + private const string StatsCategory = "Custom"; + private readonly ElapsedMillisecondsTimedStat _spriteCollisionStat; + private readonly ElapsedMillisecondsTimedStat _audioStat; //public static ROM[] ROMS = new ROM[] //{ @@ -73,20 +69,16 @@ public class C64 : ISystem, ISystemMonitor /// public ExecEvaluatorTriggerResult ExecuteOneFrame( SystemRunner systemRunner, - Dictionary detailedStats, IExecEvaluator? execEvaluator = null) { - foreach (var detailedStatName in DetailedStatNames) - { - detailedStats[detailedStatName] = 0; - } + _audioStat.Reset(); // Reset audio stat, will be continiously updated after each instruction ulong cyclesToExecute = (Vic2.Vic2Model.CyclesPerFrame - Vic2.CyclesConsumedCurrentVblank); _logger.LogTrace($"Executing one frame, {cyclesToExecute} CPU cycles."); ulong totalCyclesConsumed = 0; while (totalCyclesConsumed < cyclesToExecute) { - ExecEvaluatorTriggerResult execEvaluatorTriggerResult = ExecuteOneInstruction(systemRunner, out InstructionExecResult instructionExecResult, detailedStats, execEvaluator); + ExecEvaluatorTriggerResult execEvaluatorTriggerResult = ExecuteOneInstruction(systemRunner, out InstructionExecResult instructionExecResult, execEvaluator); totalCyclesConsumed += instructionExecResult.CyclesConsumed; if (execEvaluatorTriggerResult.Triggered) @@ -95,12 +87,13 @@ public ExecEvaluatorTriggerResult ExecuteOneFrame( } } + _audioStat.Stop(); // Stop audio stat (was continiously updated after each instruction) + // Update sprite collision state - var sw = _detailedStatStopwatches[SpriteCollisionStatName]; - sw.Restart(); - Vic2.SpriteManager.SetCollitionDetectionStatesAndIRQ(); - sw.Stop(); - detailedStats[SpriteCollisionStatName] += sw.Elapsed.TotalMilliseconds; + using (_spriteCollisionStat.Measure()) + { + Vic2.SpriteManager.SetCollitionDetectionStatesAndIRQ(); + } return ExecEvaluatorTriggerResult.NotTriggered; } @@ -116,7 +109,6 @@ public ExecEvaluatorTriggerResult ExecuteOneFrame( public ExecEvaluatorTriggerResult ExecuteOneInstruction( SystemRunner systemRunner, out InstructionExecResult instructionExecResult, - Dictionary detailedStats, IExecEvaluator? execEvaluator = null) { // Execute one CPU instruction @@ -131,7 +123,12 @@ public ExecEvaluatorTriggerResult ExecuteOneInstruction( // Handle output processing needed after each instruction. if (AudioEnabled) - systemRunner.GenerateAudio(detailedStats); + { + using (_audioStat.Measure(cont: true)) + { + systemRunner.GenerateAudio(); + } + } // Check for debugger breakpoints (or other possible IExecEvaluator implementations used). if (execEvaluator != null) @@ -148,6 +145,11 @@ public ExecEvaluatorTriggerResult ExecuteOneInstruction( private C64(ILogger logger) { _logger = logger; + _spriteCollisionStat = Instrumentations.Add($"{StatsCategory}-SpriteCollision"); + + //_audioStat = new ElapsedMillisecondsTimedStat(samples: 1); + //Instrumentations.Add($"{StatsCategory}-Audio", _audioStat); + _audioStat = Instrumentations.Add($"{StatsCategory}-Audio"); } public static C64 BuildC64(C64Config c64Config, ILoggerFactory loggerFactory) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Generic/GenericComputer.cs b/src/libraries/Highbyte.DotNet6502.Systems/Generic/GenericComputer.cs index 6fe060b7..fe4b37e7 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Generic/GenericComputer.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Generic/GenericComputer.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems.Generic.Config; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -36,13 +37,14 @@ public class GenericComputer : ISystem, ITextMode, IScreen public int VisibleTopBottomBorderHeight => (VisibleHeight - DrawableAreaHeight) / 2; public float RefreshFrequencyHz => _genericComputerConfig.ScreenRefreshFrequencyHz; - public bool HasDetailedStats => false; - public List DetailedStatNames => new(); - private ILogger _logger; private GenericComputerConfig _genericComputerConfig; private readonly LegacyExecEvaluator _oneFrameExecEvaluator; + // Instrumentations + public Instrumentations Instrumentations { get; } = new(); + + public GenericComputer() : this(new GenericComputerConfig(), new NullLoggerFactory()) { } public GenericComputer(GenericComputerConfig genericComputerConfig, ILoggerFactory loggerFactory) { @@ -71,7 +73,6 @@ public void Run(IExecEvaluator? execEvaluator = null) public ExecEvaluatorTriggerResult ExecuteOneFrame( SystemRunner systemRunner, - Dictionary detailedStats, IExecEvaluator? execEvaluator = null) { // If we already executed cycles in current frame, reduce it from total. @@ -110,7 +111,7 @@ public ExecEvaluatorTriggerResult ExecuteOneFrame( SetFrameCompleted(); // Wait for CPU 6502 code has acknowledged that it knows a frame has completed. - bool waitOk = WaitFrameCompletedAcknowledged(systemRunner, detailedStats); + bool waitOk = WaitFrameCompletedAcknowledged(systemRunner); if (!waitOk) return ExecEvaluatorTriggerResult.CreateTrigger(ExecEvaluatorTriggerReasonType.Other, "WaitFrame failed"); ; } @@ -122,7 +123,6 @@ public ExecEvaluatorTriggerResult ExecuteOneFrame( public ExecEvaluatorTriggerResult ExecuteOneInstruction( SystemRunner systemRunner, out InstructionExecResult instructionExecResult, - Dictionary detailedStats, IExecEvaluator? execEvaluator = null) { var execState = CPU.ExecuteOneInstruction(Mem); @@ -147,12 +147,12 @@ private void SetFrameCompleted() Mem.SetBit(_genericComputerConfig.Memory.Screen.ScreenRefreshStatusAddress, (int)ScreenStatusBitFlags.HostNewFrame); } - private bool WaitFrameCompletedAcknowledged(SystemRunner systemRunner, Dictionary detailedStats) + private bool WaitFrameCompletedAcknowledged(SystemRunner systemRunner) { // Keep on executing instructions until CPU 6502 code has cleared bit 0 in ScreenRefreshStatusAddress while (Mem.IsBitSet(_genericComputerConfig.Memory.Screen.ScreenRefreshStatusAddress, (int)ScreenStatusBitFlags.HostNewFrame)) { - var execEvaluatorTriggerResult = ExecuteOneInstruction(systemRunner, out _, detailedStats); + var execEvaluatorTriggerResult = ExecuteOneInstruction(systemRunner, out _); // If an unhandled instruction or other configured trigger has activated, return false if (execEvaluatorTriggerResult.Triggered) return false; diff --git a/src/libraries/Highbyte.DotNet6502/Data.cs b/src/libraries/Highbyte.DotNet6502/Data.cs deleted file mode 100644 index b2dee4fa..00000000 --- a/src/libraries/Highbyte.DotNet6502/Data.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Highbyte.DotNet6502; - -public static class Data -{ - public static byte[] GetResetVectorCode(ushort userCodeAddress) - { - List list = new() - { - // TODO: Add init code for setting SP, I flag, etc, as a normal 6502 would do in the reset vector - // End with jumping to address where our actual user code lives. Typically this would be the address of Basic. - (byte)OpCodeId.JMP_ABS - }; - List code = list; - code.AddRange(userCodeAddress.ToLittleEndianBytes()); - return code.ToArray(); - } -} diff --git a/src/libraries/Highbyte.DotNet6502/Instrumentation/InstrumentationBag.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/InstrumentationBag.cs new file mode 100644 index 00000000..2fb4d895 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/InstrumentationBag.cs @@ -0,0 +1,18 @@ +using Highbyte.DotNet6502.Instrumentation.Stats; + +namespace Highbyte.DotNet6502.Instrumentation; + +// Credit for basis of instrumentation/stat code to: https://github.com/davidwengier/Trains.NET +public static class InstrumentationBag +{ + private static readonly Instrumentations s_instrumentations = new(); + public static IEnumerable<(string Name, IStat Stat)> Stats => s_instrumentations.Stats; + public static T Add(string name, T stat) where T : IStat + { + s_instrumentations.Add(name, stat); + return stat; + } + public static T Add(string name) where T : IStat, new() => Add(name, new T()); + public static void Remove(string name) => s_instrumentations.Remove(name); + public static void Clear() => s_instrumentations.Clear(); +} diff --git a/src/libraries/Highbyte.DotNet6502/Instrumentation/Instrumentations.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/Instrumentations.cs new file mode 100644 index 00000000..4e05b230 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/Instrumentations.cs @@ -0,0 +1,17 @@ +using Highbyte.DotNet6502.Instrumentation.Stats; + +namespace Highbyte.DotNet6502.Instrumentation; + +public class Instrumentations +{ + private readonly List<(string Name, IStat Stat)> _stats = new(); + public IEnumerable<(string Name, IStat Stat)> Stats => _stats.AsReadOnly(); + public T Add(string name, T stat) where T : IStat + { + _stats.Add((name, stat)); + return stat; + } + public T Add(string name) where T : IStat, new() => Add(name, new T()); + public void Remove(string name) => _stats.RemoveAll(s => s.Name == name); + public void Clear() => _stats.Clear(); +} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/AveragedStat.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/AveragedStat.cs similarity index 58% rename from src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/AveragedStat.cs rename to src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/AveragedStat.cs index ceee03a5..778bdc43 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/AveragedStat.cs +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/AveragedStat.cs @@ -1,4 +1,4 @@ -namespace Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; +namespace Highbyte.DotNet6502.Instrumentation.Stats; // Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET public abstract class AveragedStat : IStat @@ -11,16 +11,12 @@ public AveragedStat(int sampleCount) } protected void SetValue(double value) { - if (this.Value == null) - { - this.Value = value; - } + if (Value == null) + Value = value; else - { - this.Value = (this.Value * (_sampleCount - 1) + value) / _sampleCount; - } + Value = (Value * (_sampleCount - 1) + value) / _sampleCount; } public abstract string GetDescription(); - public bool ShouldShow() => this.Value.HasValue; + public bool ShouldShow() => Value.HasValue; } diff --git a/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/DisposableCallback.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/DisposableCallback.cs new file mode 100644 index 00000000..d6801d1b --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/DisposableCallback.cs @@ -0,0 +1,18 @@ +namespace Highbyte.DotNet6502.Instrumentation.Stats; + +// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET +public class DisposableCallback : IDisposable +{ + public event EventHandler? Disposing; + public bool Cont; + + public void Dispose() + { + Disposing?.Invoke(this, new DisposableCallbackEventArgs { Cont = Cont }); + } +} +public class DisposableCallbackEventArgs : EventArgs +{ + public bool Cont { get; set; } +} + diff --git a/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs new file mode 100644 index 00000000..289d217b --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/ElapsedMillisecondsTimedStat.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; + +namespace Highbyte.DotNet6502.Instrumentation.Stats; + +// Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Dispose is used to measure")] +public class ElapsedMillisecondsTimedStat : AveragedStat +{ + private readonly Stopwatch _sw; + private readonly DisposableCallback _disposableCallback; + + public ElapsedMillisecondsTimedStat() : this(10) { } + + public ElapsedMillisecondsTimedStat(int samples) + : base(samples) // Average over x samples + { + _sw = new Stopwatch(); + _disposableCallback = new DisposableCallback(); + _disposableCallback.Disposing += (object? o, EventArgs e) => Stop(((DisposableCallbackEventArgs)e).Cont); + } + + public void Reset() + { + _sw.Reset(); + } + + public void Start(bool cont = false) + { + if (cont) + _sw.Start(); + else + _sw.Restart(); + } + + public void Stop(bool cont = false) + { + _sw.Stop(); + if (!cont) + { + //SetValue(_sw.ElapsedMilliseconds); + SetValue(_sw.Elapsed.Ticks); + } + } + + public IDisposable Measure(bool cont = false) + { + Start(cont); + _disposableCallback.Cont = cont; + return _disposableCallback; + } + + public double? GetStatMilliseconds() + { + if (Value == null) + return null; + return Value.Value / TimeSpan.TicksPerMillisecond; // 10000 ticks per millisecond + } + + public override string GetDescription() + { + var ms = GetStatMilliseconds(); + if (ms == null) + return "null"; + + if (ms < 0.01) + return "< 0.01ms"; + return Math.Round(ms.Value, 2).ToString("0.00") + "ms"; + } + + public void SetFakeMSValue(double ms) + { + SetValue(ms); + } +} diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/IStat.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/IStat.cs similarity index 72% rename from src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/IStat.cs rename to src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/IStat.cs index efdf2ebb..3bf9c364 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Instrumentation/Stats/IStat.cs +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/IStat.cs @@ -1,9 +1,8 @@ -namespace Highbyte.DotNet6502.App.WASM.Instrumentation.Stats; +namespace Highbyte.DotNet6502.Instrumentation.Stats; // Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET public interface IStat { string GetDescription(); - bool ShouldShow(); } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/PerSecondTimedStat.cs b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/PerSecondTimedStat.cs similarity index 73% rename from src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/PerSecondTimedStat.cs rename to src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/PerSecondTimedStat.cs index 2baa6df4..5aada09c 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Instrumentation/Stats/PerSecondTimedStat.cs +++ b/src/libraries/Highbyte.DotNet6502/Instrumentation/Stats/PerSecondTimedStat.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Highbyte.DotNet6502.App.SilkNetNative.Instrumentation.Stats; +namespace Highbyte.DotNet6502.Instrumentation.Stats; // Credit to instrumentation/stat code to: https://github.com/davidwengier/Trains.NET public class PerSecondTimedStat : AveragedStat @@ -22,22 +22,25 @@ public void Update() if (elapsedMs == 0) throw new NotImplementedException("Elapsed 0.0 milliseconds, cannot handle division by 0"); #endif - double perSecond = 1000.0 / elapsedMs; + var perSecond = 1000.0 / elapsedMs; SetValue(perSecond); } _sw.Restart(); } + public override string GetDescription() { - if (this.Value == null) - { + if (Value == null) return "null"; - } - if (this.Value < 0.01) - { + if (Value < 0.01) return "< 0.01"; - } - return Math.Round(this.Value ?? 0, 2).ToString(); + return Math.Round(Value ?? 0, 2).ToString(); + } + + // For unit testing + public void SetFakeFPSValue(double fps) + { + SetValue(fps); } } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandler.cs b/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandler.cs index bdc06543..0d97b820 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandler.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/IAudioHandler.cs @@ -1,3 +1,5 @@ +using Highbyte.DotNet6502.Instrumentation; + namespace Highbyte.DotNet6502.Systems; public interface IAudioHandler @@ -8,9 +10,8 @@ public interface IAudioHandler void StartPlaying(); void StopPlaying(); void PausePlaying(); - - List GetStats(); - + List GetDebugInfo(); + Instrumentations Instrumentations { get; } } public interface IAudioHandler : IAudioHandler @@ -48,8 +49,7 @@ public void StopPlaying() { } - private readonly List _stats = new List(); - - public List GetStats() => _stats; + public List GetDebugInfo() => new(); + public Instrumentations Instrumentations { get; } = new(); } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/IInputHandler.cs b/src/libraries/Highbyte.DotNet6502/Systems/IInputHandler.cs index 9b9f045c..4cbf1b4f 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/IInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/IInputHandler.cs @@ -1,3 +1,5 @@ +using Highbyte.DotNet6502.Instrumentation; + namespace Highbyte.DotNet6502.Systems; public interface IInputHandler @@ -5,7 +7,8 @@ public interface IInputHandler void Init(ISystem system, IInputHandlerContext inputContext); void ProcessInput(ISystem system); - List GetStats(); + List GetDebugInfo(); + Instrumentations Instrumentations { get; } } public interface IInputHandler : IInputHandler @@ -26,7 +29,7 @@ public void Init(ISystem system, IInputHandlerContext inputHandlerContext) public void ProcessInput(ISystem system) { } + public List GetDebugInfo() => new(); - private readonly List _stats = new List(); - public List GetStats() => _stats; + public Instrumentations Instrumentations { get; } = new(); } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/IRenderer.cs b/src/libraries/Highbyte.DotNet6502/Systems/IRenderer.cs index 37d6191b..81a3da8c 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/IRenderer.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/IRenderer.cs @@ -1,13 +1,13 @@ +using Highbyte.DotNet6502.Instrumentation; + namespace Highbyte.DotNet6502.Systems; public interface IRenderer { void Init(ISystem system, IRenderContext renderContext); - void Draw(ISystem system, Dictionary detailedStats); - - public bool HasDetailedStats { get; } - public List DetailedStatNames { get; } + void Draw(ISystem system); + Instrumentations Instrumentations { get; } } public interface IRenderer : IRenderer @@ -15,19 +15,18 @@ public interface IRenderer : IRenderer where TRenderContext : IRenderContext { void Init(TSystem system, TRenderContext renderContext); - void Draw(TSystem system, Dictionary detailedStats); + void Draw(TSystem system); } public class NullRenderer : IRenderer { - public bool HasDetailedStats => false; - public List DetailedStatNames => new List(); + public Instrumentations Instrumentations { get; } = new(); public void Init(ISystem system, IRenderContext renderContext) { } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { } } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/ISystem.cs b/src/libraries/Highbyte.DotNet6502/Systems/ISystem.cs index b303e0ab..619be940 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/ISystem.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/ISystem.cs @@ -1,3 +1,5 @@ +using Highbyte.DotNet6502.Instrumentation; + namespace Highbyte.DotNet6502.Systems; public interface ISystem @@ -9,17 +11,14 @@ public interface ISystem Memory Mem { get; } IScreen Screen { get; } - public ExecEvaluatorTriggerResult ExecuteOneFrame( + ExecEvaluatorTriggerResult ExecuteOneFrame( SystemRunner systemRunner, - Dictionary detailedStats, IExecEvaluator? execEvaluator = null); - public ExecEvaluatorTriggerResult ExecuteOneInstruction( + ExecEvaluatorTriggerResult ExecuteOneInstruction( SystemRunner systemRunner, out InstructionExecResult instructionExecResult, - Dictionary detailedStats, IExecEvaluator? execEvaluator = null); - public bool HasDetailedStats { get; } - public List DetailedStatNames { get; } + Instrumentations Instrumentations { get; } } diff --git a/src/libraries/Highbyte.DotNet6502/Systems/SystemRunner.cs b/src/libraries/Highbyte.DotNet6502/Systems/SystemRunner.cs index dd7bb3c7..f54a13d8 100644 --- a/src/libraries/Highbyte.DotNet6502/Systems/SystemRunner.cs +++ b/src/libraries/Highbyte.DotNet6502/Systems/SystemRunner.cs @@ -53,44 +53,29 @@ public void ProcessInput() /// Called by host app by a timer (or similar) that runs the emulator, tied to the update frequency of the emulated system. /// Typically called before after ProcessInput is called. /// - public ExecEvaluatorTriggerResult RunEmulatorOneFrame(out Dictionary detailedStats) + public ExecEvaluatorTriggerResult RunEmulatorOneFrame() { - detailedStats = new() - { - ["Audio"] = 0 - }; - - var execEvaluatorTriggerResult = _system.ExecuteOneFrame(this, detailedStats, _customExecEvaluator); + var execEvaluatorTriggerResult = _system.ExecuteOneFrame(this, _customExecEvaluator); return execEvaluatorTriggerResult; } /// /// Called by host app that runs the emulator, typically once per frame tied to the host app rendering frequency. /// - public void Draw(out Dictionary detailedStats) + public void Draw() { - detailedStats = new() - { - }; - - _renderer?.Draw(_system, detailedStats); + _renderer?.Draw(_system); } /// /// Called by the specific ISystem implementation after each instruction or entire frame worth of instructions, depending how audio is implemented. /// /// - public void GenerateAudio(Dictionary detailedStats) + public void GenerateAudio() { - if (_audioHandler is not null) - { - _audioSw.Restart(); - _audioHandler.GenerateAudio(_system); - //var t = new Task(() => _audioHandler?.GenerateAudio(system)); - //t.RunSynchronously(); - _audioSw.Stop(); + _audioHandler?.GenerateAudio(_system); - detailedStats["Audio"] += _audioSw.Elapsed.TotalMilliseconds; - } + //var t = new Task(() => _audioHandler?.GenerateAudio(system)); + //t.RunSynchronously(); } } diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs new file mode 100644 index 00000000..3af053a7 --- /dev/null +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/AveragedStatTest.cs @@ -0,0 +1,34 @@ +namespace Highbyte.DotNet6502.Tests.Instrumentation +{ + public class AveragedStatTest + { + [Fact] + public void Value_WhenCalled_ReturnsValue() + { + // Arrange + var stat = new TestAveragedStat(10); + stat.UpdateStat(1.0); + + // Act + var value = stat.Value; + + // Assert + Assert.Equal(1.0, value); + } + + [Fact] + public void Value_WhenCalled_ReturnsValue_Average() + { + // Arrange + var stat = new TestAveragedStat(sampleCount: 2); + stat.UpdateStat(1.0); + stat.UpdateStat(2.0); + + // Act + var value = stat.Value; + + // Assert + Assert.Equal(1.5, value); + } + } +} diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs new file mode 100644 index 00000000..17734238 --- /dev/null +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/ElapsedMillisecondsTimedStatTest.cs @@ -0,0 +1,101 @@ +using Highbyte.DotNet6502.Instrumentation.Stats; + +namespace Highbyte.DotNet6502.Tests.Instrumentation +{ + public class ElapsedMillisecondsTimedStatTest + { + [Fact] + public void Measure_WhenUsed_Returns_ExecutionTime_In_GetStatMilliseconds() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + int sleepMs = 2; + using (stat.Measure()) + { + Thread.Sleep(sleepMs); + } + + // Act + var elapsedMs = stat.GetStatMilliseconds(); + + // Assert + Assert.True(elapsedMs >= sleepMs); +#if !DEBUG + // In debug mode, the elapsed time may not accurate enough to make this test pass (if breakpoint is hit during sleep) + Assert.True(elapsedMs < sleepMs + 10); +#endif + } + + [Fact] + public void Measure_WhenUsed_With_Cont_Returns_ExecutionTime_In_GetStatMilliseconds() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + int sleepMs = 2; + using (stat.Measure(cont: true)) + { + Thread.Sleep(sleepMs); + } + int sleepNextMs = 3; + using (stat.Measure(cont: true)) + { + Thread.Sleep(sleepNextMs); + } + stat.Stop(); + + // Act + var elapsedMs = stat.GetStatMilliseconds(); + + // Assert + Assert.True(elapsedMs >= (sleepMs + sleepNextMs)); + +#if !DEBUG + // In debug mode, the elapsed time may not accurate enough to make this test pass (if breakpoint is hit during sleep) + Assert.True(elapsedMs < (sleepMs + sleepNextMs) + 10); +#endif + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Null_When_No_Data_Yet() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + // Act + // Assert + Assert.Equal("null", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Special_String_When_Duration_Is_Less_Than_OneHundreds_Of_A_Millisecond() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + // Act + stat.SetFakeMSValue(0.0099); + + // Assert + Assert.Equal("< 0.01ms", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_String_With_Milliseconds() + { + // Arrange + var stat = new ElapsedMillisecondsTimedStat(samples: 1); + + // Act + int sleepMs = 2; + using (stat.Measure()) + { + Thread.Sleep(sleepMs); + } + // Assert + var ms = stat.GetStatMilliseconds(); + Assert.Equal($"{Math.Round(ms.Value, 2).ToString("0.00")}ms", stat.GetDescription()); + } + } +} diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs new file mode 100644 index 00000000..7252e3e7 --- /dev/null +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationBagTest.cs @@ -0,0 +1,74 @@ +using Highbyte.DotNet6502.Instrumentation; + +namespace Highbyte.DotNet6502.Tests.Instrumentation +{ + /// + /// InstrumentationBag is a static class that uses Instrumentations class under the hood. + /// + public class InstrumentationBagTest + { + + [Fact] + public void Add_WhenCalled_AddsStatToStatsList() + { + // Arrange + InstrumentationBag.Clear(); + + // Act + var stat = new TestStat(); + var addedStat = InstrumentationBag.Add("stat1", stat); + + // Assert + Assert.Single(InstrumentationBag.Stats); + Assert.Equal(addedStat, stat); + Assert.Equal(addedStat, InstrumentationBag.Stats.First().Stat); + Assert.Equal("stat1", InstrumentationBag.Stats.First().Name); + } + + [Fact] + public void Add_WhenCalled_AddsStatToStatsList2() + { + // Arrange + InstrumentationBag.Clear(); + + // Act + var addedStat = InstrumentationBag.Add("stat1"); + + // Assert + Assert.Single(InstrumentationBag.Stats); + Assert.Equal(addedStat, InstrumentationBag.Stats.First().Stat); + Assert.Equal("stat1", InstrumentationBag.Stats.First().Name); + } + + [Fact] + public void Remove_WhenCalled_RemovesStatFromStatsList() + { + // Arrange + InstrumentationBag.Clear(); + InstrumentationBag.Add("stat1", new TestStat()); + InstrumentationBag.Add("stat2", new TestStat()); + + // Act + InstrumentationBag.Remove("stat1"); + + // Assert + Assert.Single(InstrumentationBag.Stats); + } + + [Fact] + public void Clear_WhenCalled_ClearsStatsList() + { + // Arrange + InstrumentationBag.Clear(); + + InstrumentationBag.Add("stat1", new TestStat()); + InstrumentationBag.Add("stat2", new TestStat()); + + // Act + InstrumentationBag.Clear(); + + // Assert + Assert.Empty(InstrumentationBag.Stats); + } + } +} diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs new file mode 100644 index 00000000..7fd84cc0 --- /dev/null +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/InstrumentationsTest.cs @@ -0,0 +1,70 @@ +using Highbyte.DotNet6502.Instrumentation; + +namespace Highbyte.DotNet6502.Tests.Instrumentation +{ + public class InstrumentationsTest + { + + [Fact] + public void Add_WhenCalled_AddsStatToStatsList() + { + // Arrange + var instrumentations = new Instrumentations(); + + // Act + var stat = new TestStat(); + var addedStat = instrumentations.Add("stat1", stat); + + // Assert + Assert.Single(instrumentations.Stats); + Assert.Equal(addedStat, stat); + Assert.Equal(addedStat, instrumentations.Stats.First().Stat); + Assert.Equal("stat1", instrumentations.Stats.First().Name); + } + + [Fact] + public void Add_WhenCalled_AddsStatToStatsList2() + { + // Arrange + var instrumentations = new Instrumentations(); + + // Act + var addedStat = instrumentations.Add("stat1"); + + // Assert + Assert.Single(instrumentations.Stats); + Assert.Equal(addedStat, instrumentations.Stats.First().Stat); + Assert.Equal("stat1", instrumentations.Stats.First().Name); + } + + [Fact] + public void Remove_WhenCalled_RemovesStatFromStatsList() + { + // Arrange + var instrumentations = new Instrumentations(); + instrumentations.Add("stat1", new TestStat()); + instrumentations.Add("stat2", new TestStat()); + + // Act + instrumentations.Remove("stat1"); + + // Assert + Assert.Single(instrumentations.Stats); + } + + [Fact] + public void Clear_WhenCalled_ClearsStatsList() + { + // Arrange + var instrumentations = new Instrumentations(); + instrumentations.Add("stat1", new TestStat()); + instrumentations.Add("stat2", new TestStat()); + + // Act + instrumentations.Clear(); + + // Assert + Assert.Empty(instrumentations.Stats); + } + } +} diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs new file mode 100644 index 00000000..d11f0bbc --- /dev/null +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/PerSecondTimedStatTest.cs @@ -0,0 +1,63 @@ +using Highbyte.DotNet6502.Instrumentation.Stats; +using Newtonsoft.Json.Linq; + +namespace Highbyte.DotNet6502.Tests.Instrumentation +{ + public class PerSecondTimedStatTest + { + // Test PerSecondTimedStatTest that it calculates per second correctly. + // This test is not very accurate, but it should be good enough to catch any major errors. + [Fact] + public void Update_WhenUsed_Returns_Correct_PerSecond_Value() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + stat.SetFakeFPSValue(60); + + // Assert + Assert.Equal(60, stat.Value); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Null_When_No_Data_Yet() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + // Assert + Assert.Equal("null", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_Special_String_When_FPS_Is_Less_Than_OneHundreds_Of_A_Second() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + stat.SetFakeFPSValue(0.009); + + // Assert + Assert.Equal("< 0.01", stat.GetDescription()); + } + + [Fact] + public void GetDescription_WhenUsed_Returns_String_With_FPS() + { + // Arrange + var stat = new PerSecondTimedStat(); + + // Act + stat.Update(); + Thread.Sleep(16); + stat.Update(); + + // Assert + var fps = stat.Value; + Assert.Equal(Math.Round(fps ?? 0, 2).ToString(), stat.GetDescription()); + } + } +} diff --git a/tests/Highbyte.DotNet6502.Tests/Instrumentation/TestStat.cs b/tests/Highbyte.DotNet6502.Tests/Instrumentation/TestStat.cs new file mode 100644 index 00000000..5e1bf73d --- /dev/null +++ b/tests/Highbyte.DotNet6502.Tests/Instrumentation/TestStat.cs @@ -0,0 +1,24 @@ +using Highbyte.DotNet6502.Instrumentation.Stats; + +namespace Highbyte.DotNet6502.Tests.Instrumentation +{ + internal class TestStat : IStat + { + public string GetDescription() => "TestStat"; + public bool ShouldShow() => true; + } + + internal class TestAveragedStat : AveragedStat + { + public TestAveragedStat(int sampleCount) : base(sampleCount) + { + } + + public void UpdateStat(double value) + { + SetValue(value); + } + + public override string GetDescription() => "TestAveragedStat"; + } +} diff --git a/tests/Highbyte.DotNet6502.Tests/Systems/SystemRunnerBuilderTests.cs b/tests/Highbyte.DotNet6502.Tests/Systems/SystemRunnerBuilderTests.cs index 8c13c188..ca5e3a2b 100644 --- a/tests/Highbyte.DotNet6502.Tests/Systems/SystemRunnerBuilderTests.cs +++ b/tests/Highbyte.DotNet6502.Tests/Systems/SystemRunnerBuilderTests.cs @@ -1,3 +1,4 @@ +using Highbyte.DotNet6502.Instrumentation; using Highbyte.DotNet6502.Systems; namespace Highbyte.DotNet6502.Tests.Systems; @@ -112,16 +113,14 @@ public class TestSystem : ISystem public IScreen Screen => throw new NotImplementedException(); - public bool HasDetailedStats => false; + public Instrumentations Instrumentations { get; } = new(); - public List DetailedStatNames => new List(); - - public ExecEvaluatorTriggerResult ExecuteOneFrame(SystemRunner systemRunner, Dictionary detailedStats, IExecEvaluator? execEvaluator = null) + public ExecEvaluatorTriggerResult ExecuteOneFrame(SystemRunner systemRunner, IExecEvaluator? execEvaluator = null) { return new ExecEvaluatorTriggerResult(); } - public ExecEvaluatorTriggerResult ExecuteOneInstruction(SystemRunner systemRunner, out InstructionExecResult instructionExecResult, Dictionary detailedStats, IExecEvaluator? execEvaluator = null) + public ExecEvaluatorTriggerResult ExecuteOneInstruction(SystemRunner systemRunner, out InstructionExecResult instructionExecResult, IExecEvaluator? execEvaluator = null) { instructionExecResult = new InstructionExecResult(); return new ExecEvaluatorTriggerResult(); @@ -130,10 +129,10 @@ public ExecEvaluatorTriggerResult ExecuteOneInstruction(SystemRunner systemRunne public class TestRenderer : IRenderer { - public void Draw(TestSystem system, Dictionary detailedStats) + public void Draw(TestSystem system) { } - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { } public void Init(TestSystem system, IRenderContext renderContext) @@ -142,29 +141,21 @@ public void Init(TestSystem system, IRenderContext renderContext) public void Init(ISystem system, IRenderContext renderContext) { } - - public bool HasDetailedStats => false; - public List DetailedStatNames => new List(); + public Instrumentations Instrumentations { get; } = new(); } public class TestRendererNonGeneric : IRenderer { - public void Draw(ISystem system, Dictionary detailedStats) + public void Draw(ISystem system) { } public void Init(ISystem system, IRenderContext renderContext) { } - - public bool HasDetailedStats => false; - public List DetailedStatNames => new List(); + public Instrumentations Instrumentations { get; } = new(); } public class TestInputHandler : IInputHandler { - public List GetStats() - { - return new List(); - } public void Init(TestSystem system, IInputHandlerContext inputContext) { } @@ -177,22 +168,23 @@ public void ProcessInput(TestSystem system) public void ProcessInput(ISystem system) { } + + public List GetDebugInfo() => new(); + + public Instrumentations Instrumentations { get; } = new(); } public class TestInputHandlerNonGeneric : IInputHandler { - public List GetStats() - { - return new List(); - } public void Init(ISystem system, IInputHandlerContext inputContext) { } - public void ProcessInput(ISystem system) { } + public List GetDebugInfo() => new List(); + public Instrumentations Instrumentations { get; } = new(); } public class TestAudioHandler : IAudioHandler @@ -203,10 +195,6 @@ public void GenerateAudio(TestSystem system) public void GenerateAudio(ISystem system) { } - public List GetStats() - { - return new List(); - } public void Init(TestSystem system, IAudioHandlerContext audioHandlerContext) { } @@ -222,6 +210,10 @@ public void StartPlaying() public void StopPlaying() { } + public List GetDebugInfo() => new(); + + public Instrumentations Instrumentations { get; } = new(); + } public class TestAudioHandlerNonGeneric : IAudioHandler @@ -229,10 +221,6 @@ public class TestAudioHandlerNonGeneric : IAudioHandler public void GenerateAudio(ISystem system) { } - public List GetStats() - { - return new List(); - } public void Init(ISystem system, IAudioHandlerContext audioHandlerContext) { } @@ -245,4 +233,8 @@ public void StartPlaying() public void StopPlaying() { } + public List GetDebugInfo() => new(); + + public Instrumentations Instrumentations { get; } = new(); + }