From 3a4e688376824446b275e074b8a959cefa2576d4 Mon Sep 17 00:00:00 2001
From: ConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>
Date: Mon, 4 Jul 2022 02:03:10 -0500
Subject: [PATCH] Measure analysis times
---
.../TestStatistics.cs | 22 +++
.../Configuration/configPage.html | 44 ++++++
.../Controllers/VisualizationController.cs | 11 ++
.../Data/AnalysisStatistics.cs | 133 ++++++++++++++++++
.../Plugin.cs | 5 +
.../ScheduledTasks/FingerprinterTask.cs | 31 +++-
6 files changed, 243 insertions(+), 3 deletions(-)
create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs
create mode 100644 ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs
new file mode 100644
index 0000000..66b2e34
--- /dev/null
+++ b/ConfusedPolarBear.Plugin.IntroSkipper.Tests/TestStatistics.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Text.Json;
+using System.Text;
+using Xunit;
+
+namespace ConfusedPolarBear.Plugin.IntroSkipper.Tests;
+
+public class TestStatistics
+{
+ [Fact]
+ public void TestTSISerialization()
+ {
+ var expected = "\"TotalAnalyzedEpisodes\":42,";
+
+ var stats = new AnalysisStatistics();
+ stats.TotalAnalyzedEpisodes.Add(42);
+
+ var actual = Encoding.UTF8.GetString(JsonSerializer.SerializeToUtf8Bytes(stats));
+
+ Assert.Contains(expected, actual, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
index 3730c82..1666214 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Configuration/configPage.html
@@ -105,6 +105,12 @@
+
+ Analysis Statistics
+
+
+
+
Fingerprint Visualizer
@@ -187,6 +193,7 @@
// settings elements
var visualizer = document.querySelector("details#visualizer");
+ var statistics = document.querySelector("details#statistics");
var btnEraseTimestamps = document.querySelector("button#btnEraseTimestamps");
// visualizer elements
@@ -225,6 +232,42 @@
Dashboard.hideLoadingMsg();
}
+ async function statisticsToggled() {
+ if (!statistics.open) {
+ return;
+ }
+
+ // Blank any old statistics
+ const text = document.querySelector("pre#statisticsText");
+ text.textContent = "";
+
+ Dashboard.showLoadingMsg();
+
+ // Load the statistics from the server
+ let stats = await getJson("Intros/Statistics");
+
+ // Select which fields to print and label them with more friendly descriptions
+ let fields = "TotalCPUTime,FingerprintCPUTime,FirstPassCPUTime,SecondPassCPUTime,QuickScans,FullScans," +
+ "TotalQueuedEpisodes";
+
+ let friendlyNames = {
+ TotalCPUTime: "Total CPU time (ms)",
+ FingerprintCPUTime: "Fingerprint time (ms)",
+ FirstPassCPUTime: "First pass (ms)",
+ SecondPassCPUTime: "Second pass (ms)",
+ QuickScans: "Quick scans",
+ FullScans: "Full scans",
+ TotalQueuedEpisodes: "Episodes queued",
+ };
+
+ // Print all friendly names and data points
+ for (var f of fields.split(",")) {
+ text.textContent += friendlyNames[f].padEnd(25) + stats[f] + "\n";
+ }
+
+ Dashboard.hideLoadingMsg();
+ }
+
// show changed, populate seasons
async function showChanged() {
clearSelect(selectSeason);
@@ -533,6 +576,7 @@
});
visualizer.addEventListener("toggle", visualizerToggled);
+ statistics.addEventListener("toggle", statisticsToggled);
txtOffset.addEventListener("change", renderTroubleshooter);
selectShow.addEventListener("change", showChanged);
selectSeason.addEventListener("change", seasonChanged);
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
index 0aa5bde..5dbb778 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Controllers/VisualizationController.cs
@@ -150,6 +150,17 @@ public class VisualizationController : ControllerBase
return NoContent();
}
+ ///
+ /// Returns the statistics for the most recent analysis.
+ ///
+ /// Analysis statistics.
+ /// AnalysisStatistics.
+ [HttpGet("Statistics")]
+ public ActionResult GetAnalysisStatistics()
+ {
+ return Plugin.Instance!.AnalysisStatistics;
+ }
+
private string GetSeasonName(QueuedEpisode episode)
{
return "Season " + episode.SeasonNumber.ToString(CultureInfo.InvariantCulture);
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
new file mode 100644
index 0000000..8ca0e27
--- /dev/null
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Data/AnalysisStatistics.cs
@@ -0,0 +1,133 @@
+namespace ConfusedPolarBear.Plugin.IntroSkipper;
+
+using System;
+using System.Threading;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+///
+/// Detailed statistics about the last analysis operation performed. All times are represented as milliseconds.
+///
+public class AnalysisStatistics
+{
+ ///
+ /// Gets the number of episodes that have been analyzed so far.
+ ///
+ public ThreadSafeInteger TotalAnalyzedEpisodes { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets or sets the number of episodes that need to be analyzed.
+ ///
+ public int TotalQueuedEpisodes { get; set; }
+
+ ///
+ /// Gets the number of times a quick scan successfully located a pair of introductions.
+ ///
+ public ThreadSafeInteger QuickScans { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets the number of times a full scan successfully located a pair of introductions.
+ ///
+ public ThreadSafeInteger FullScans { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets the total CPU time spent waiting for audio fingerprints to be generated.
+ ///
+ public ThreadSafeInteger FingerprintCPUTime { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets the total CPU time spent analyzing fingerprints in the initial pass.
+ ///
+ public ThreadSafeInteger FirstPassCPUTime { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets the total CPU time spent analyzing fingerprints in the second pass.
+ ///
+ public ThreadSafeInteger SecondPassCPUTime { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets the total task runtime across all threads.
+ ///
+ public ThreadSafeInteger TotalCPUTime { get; } = new ThreadSafeInteger();
+
+ ///
+ /// Gets the total task runtime as measured by a clock.
+ ///
+ public ThreadSafeInteger TotalTaskTime { get; } = new ThreadSafeInteger();
+}
+
+///
+/// Convenience wrapper around a thread safe integer.
+///
+[JsonConverter(typeof(ThreadSafeIntegerJsonConverter))]
+public class ThreadSafeInteger
+{
+ private int value = 0;
+
+ ///
+ /// Gets the current value stored by this integer.
+ ///
+ public int Value
+ {
+ get
+ {
+ return value;
+ }
+ }
+
+ ///
+ /// Increment the value of this integer by 1.
+ ///
+ public void Increment()
+ {
+ Add(1);
+ }
+
+ ///
+ /// Adds the total milliseconds elapsed since a start time.
+ ///
+ /// Start time.
+ public void AddDuration(DateTime start)
+ {
+ var elapsed = DateTime.Now.Subtract(start);
+ Add((int)elapsed.TotalMilliseconds);
+ }
+
+ ///
+ /// Adds the provided amount to this integer.
+ ///
+ /// Amount to add.
+ public void Add(int amount)
+ {
+ Interlocked.Add(ref value, amount);
+ }
+}
+
+///
+/// Serialize thread safe integers to a regular integer (instead of an object with a Value property).
+///
+public class ThreadSafeIntegerJsonConverter : JsonConverter
+{
+ ///
+ /// Deserialization of TSIs is not supported and will always throw a NotSupportedException.
+ ///
+ /// Reader.
+ /// Type.
+ /// Options.
+ /// Never returns.
+ public override ThreadSafeInteger? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Serialize the provided TSI.
+ ///
+ /// Writer.
+ /// TSI.
+ /// Options.
+ public override void Write(Utf8JsonWriter writer, ThreadSafeInteger value, JsonSerializerOptions options)
+ {
+ writer.WriteNumberValue(value.Value);
+ }
+}
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
index d69d790..1e2ad9d 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/Plugin.cs
@@ -74,6 +74,11 @@ public class Plugin : BasePlugin, IHasWebPages
///
public int TotalQueued { get; set; }
+ ///
+ /// Gets or sets the statistics from the most recent analysis run.
+ ///
+ public AnalysisStatistics AnalysisStatistics { get; set; } = new AnalysisStatistics();
+
///
/// Gets the directory to cache fingerprints in.
///
diff --git a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs
index a162394..3598997 100644
--- a/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs
+++ b/ConfusedPolarBear.Plugin.IntroSkipper/ScheduledTasks/FingerprinterTask.cs
@@ -68,6 +68,11 @@ public class FingerprinterTask : IScheduledTask
///
private Dictionary> _fingerprintCache;
+ ///
+ /// Statistics for the currently running analysis task.
+ ///
+ private AnalysisStatistics analysisStatistics = new AnalysisStatistics();
+
///
/// Initializes a new instance of the class.
///
@@ -137,14 +142,18 @@ public class FingerprinterTask : IScheduledTask
// Include the previously processed episodes in the percentage reported to the UI.
var totalProcessed = CountProcessedEpisodes();
-
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = Plugin.Instance!.Configuration.MaxParallelism
};
+ var taskStart = DateTime.Now;
+ analysisStatistics = new AnalysisStatistics();
+ analysisStatistics.TotalQueuedEpisodes = Plugin.Instance!.TotalQueued;
+
Parallel.ForEach(queue, options, (season) =>
{
+ var workerStart = DateTime.Now;
var first = season.Value[0];
try
@@ -181,8 +190,14 @@ public class FingerprinterTask : IScheduledTask
}
progress.Report((totalProcessed * 100) / Plugin.Instance!.TotalQueued);
+
+ analysisStatistics.TotalCPUTime.AddDuration(workerStart);
+ Plugin.Instance!.AnalysisStatistics = analysisStatistics;
});
+ analysisStatistics.TotalTaskTime.AddDuration(taskStart);
+ Plugin.Instance!.AnalysisStatistics = analysisStatistics;
+
return Task.CompletedTask;
}
@@ -292,7 +307,6 @@ public class FingerprinterTask : IScheduledTask
break;
}
- // TODO: add retry logic
if (Plugin.Instance!.Intros.ContainsKey(lhs.EpisodeId) && Plugin.Instance!.Intros.ContainsKey(rhs.EpisodeId))
{
_logger.LogTrace(
@@ -308,9 +322,9 @@ public class FingerprinterTask : IScheduledTask
_logger.LogTrace("Analyzing {LHS} and {RHS}", lhs.Path, rhs.Path);
var (lhsIntro, rhsIntro) = FingerprintEpisodes(lhs, rhs);
-
seasonIntros[lhsIntro.EpisodeId] = lhsIntro;
seasonIntros[rhsIntro.EpisodeId] = rhsIntro;
+ analysisStatistics.TotalAnalyzedEpisodes.Add(2);
if (!lhsIntro.Valid)
{
@@ -338,8 +352,12 @@ public class FingerprinterTask : IScheduledTask
// Only run the second pass if the user hasn't requested cancellation and we found an intro
if (!cancellationToken.IsCancellationRequested && everFoundIntro)
{
+ var start = DateTime.Now;
+
// Run a second pass over this season to remove outliers and fix episodes that failed in the first pass.
RunSecondPass(season.Value);
+
+ analysisStatistics.SecondPassCPUTime.AddDuration(start);
}
lock (_introsLock)
@@ -358,8 +376,10 @@ public class FingerprinterTask : IScheduledTask
/// Intros for the first and second episodes.
public (Intro Lhs, Intro Rhs) FingerprintEpisodes(QueuedEpisode lhsEpisode, QueuedEpisode rhsEpisode)
{
+ var start = DateTime.Now;
var lhsFingerprint = Chromaprint.Fingerprint(lhsEpisode);
var rhsFingerprint = Chromaprint.Fingerprint(rhsEpisode);
+ analysisStatistics.FingerprintCPUTime.AddDuration(start);
// Cache the fingerprints for quicker recall in the second pass (if one is needed).
lock (_fingerprintCacheLock)
@@ -389,6 +409,7 @@ public class FingerprinterTask : IScheduledTask
Guid rhsId,
ReadOnlyCollection rhsPoints)
{
+ var start = DateTime.Now;
var lhsRanges = new List();
var rhsRanges = new List();
@@ -404,6 +425,7 @@ public class FingerprinterTask : IScheduledTask
if (lhsRanges.Count == 0)
{
_logger.LogTrace("quick scan unsuccessful, falling back to full scan (±{Limit})", limit);
+ analysisStatistics.FullScans.Increment();
(lhsContiguous, rhsContiguous) = ShiftEpisodes(lhsPoints, rhsPoints, -1 * limit, limit);
lhsRanges.AddRange(lhsContiguous);
@@ -412,6 +434,7 @@ public class FingerprinterTask : IScheduledTask
else
{
_logger.LogTrace("quick scan successful");
+ analysisStatistics.QuickScans.Increment();
}
if (lhsRanges.Count == 0)
@@ -421,6 +444,7 @@ public class FingerprinterTask : IScheduledTask
lhsId,
rhsId);
+ analysisStatistics.FirstPassCPUTime.AddDuration(start);
return (new Intro(lhsId), new Intro(rhsId));
}
@@ -442,6 +466,7 @@ public class FingerprinterTask : IScheduledTask
rhsIntro.Start = 0;
}
+ analysisStatistics.FirstPassCPUTime.AddDuration(start);
return (new Intro(lhsId, lhsIntro), new Intro(rhsId, rhsIntro));
}