using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; namespace Ariadne.PerformanceTests { /// /// Times ticking arcs through an ArcScheduler against ticking every arc each frame. /// public static class Program { public const string QuickOption = "--quick"; public const string MinimumSleepOption = "--min-sleep="; public const float BusyMachinePercent = 20; public const int BusySampleMilliseconds = 500; public const string Usage = "Usage: Ariadne.PerformanceTests [--quick] [--min-sleep=frames] [text in the scenario names to run]"; [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetSystemTimes(out long idleTime, out long kernelTime, out long userTime); public static bool IsJitOptimizerDisabled(Assembly assembly) { var debuggable = assembly.GetCustomAttribute(); return debuggable != null && debuggable.IsJITOptimizerDisabled; } /// /// Why timings taken in this process wouldn't mean anything, or null when they would. /// public static string ReasonTimingsAreMeaningless() { if (Debugger.IsAttached) { return "a debugger is attached"; } foreach (Assembly assembly in new[] { typeof(Program).Assembly, typeof(ArcScheduler<>).Assembly }) { if (IsJitOptimizerDisabled(assembly)) { return assembly.GetName().Name + " is a Debug build"; } } return null; } /// /// One core at high priority, so the scheduler neither migrates the timing loop nor preempts it for background /// work. /// public static void ClaimOneCore() { Process self = Process.GetCurrentProcess(); self.PriorityClass = ProcessPriorityClass.High; self.ProcessorAffinity = (IntPtr)(1L << (Environment.ProcessorCount - 1)); } /// /// How busy every processor was over a short sample, or null where Windows won't say. /// public static float? SystemBusyPercent() { if (!GetSystemTimes(out long idleBefore, out long kernelBefore, out long userBefore)) { return null; } Thread.Sleep(BusySampleMilliseconds); if (!GetSystemTimes(out long idleAfter, out long kernelAfter, out long userAfter)) { return null; } // Kernel time includes idle time. long total = kernelAfter - kernelBefore + userAfter - userBefore; long idle = idleAfter - idleBefore; if (total <= 0) { return null; } return 100f * (total - idle) / total; } private static string Microseconds(double nanoseconds) { return (nanoseconds / 1000).ToString("0.00", CultureInfo.InvariantCulture); } private static string Ratio(double value, double baseline) { return (value / baseline).ToString("0.00", CultureInfo.InvariantCulture) + "x"; } private static void RunScheduler(string filter, int rounds, int minimumSleepFrames) { Console.WriteLine("Ticking arcs: the time to run one frame, warm. The speedup is the every-arc time " + "over the scheduler's."); Console.WriteLine($"{"scenario",-34} {"every arc us",13} {"scheduler us",13} {"speedup",9} " + $"{"bytes",8} {"bytes",8}"); foreach (SchedulerScenario scenario in Scenarios.BuildScheduler()) { if (scenario.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } scenario.CheckSameWork(minimumSleepFrames); Host everyArc = new EveryArcHost(scenario.MakeArcs()); Host scheduled = new SchedulerHost(scenario.MakeArcs(), minimumSleepFrames); Timing[] timings = Measurement.TimeTakingTurns( new Action[] { everyArc.Step, scheduled.Step }, rounds); Console.WriteLine($"{scenario.Name,-34} {Microseconds(timings[0].MedianNanoseconds),13} " + $"{Microseconds(timings[1].MedianNanoseconds),13} " + $"{Ratio(timings[0].MedianNanoseconds, timings[1].MedianNanoseconds),9} " + $"{timings[0].BytesPerCall,8:0} {timings[1].BytesPerCall,8:0}"); } Console.WriteLine(); } private static void RunHerd(string filter, int rounds) { Console.WriteLine("Waking a herd: the time to run one frame, warm. The arcs poll a flag every frame, " + "or wait on a trigger raised on a period. ns per wake is the trigger's time over the arcs it woke."); Console.WriteLine($"{"scenario",-34} {"polling us",13} {"trigger us",13} {"speedup",9} " + $"{"bytes",8} {"bytes",8} {"ns per wake",12}"); foreach (HerdScenario scenario in Scenarios.BuildHerd()) { if (scenario.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) { continue; } scenario.CheckSameWork(); var polling = new PollingHerdHost(scenario.ArcCount, scenario.PeriodFrames); var triggered = new TriggerHerdHost(scenario.ArcCount, scenario.PeriodFrames); // Each timed call is a whole period, since a batch of a few frames would often miss the raise. int framesPerCall = Math.Max(1, scenario.PeriodFrames); Timing[] timings = Measurement.TimeTakingTurns(new Action[] { () => polling.Step(framesPerCall), () => triggered.Step(framesPerCall), }, rounds); double polledNanoseconds = timings[0].MedianNanoseconds / framesPerCall; double triggeredNanoseconds = timings[1].MedianNanoseconds / framesPerCall; string perWake = ""; if (scenario.WakesPerFrame > 0) { perWake = (triggeredNanoseconds / scenario.WakesPerFrame).ToString("0.0", CultureInfo.InvariantCulture); } Console.WriteLine($"{scenario.Name,-34} {Microseconds(polledNanoseconds),13} " + $"{Microseconds(triggeredNanoseconds),13} {Ratio(polledNanoseconds, triggeredNanoseconds),9} " + $"{timings[0].BytesPerCall / framesPerCall,8:0} {timings[1].BytesPerCall / framesPerCall,8:0} " + $"{perWake,12}"); } Console.WriteLine(); } public static int Main(string[] args) { bool quick = false; int minimumSleepFrames = new ArcScheduler().MinimumSleepFrames; string filter = ""; foreach (string arg in args) { if (arg == QuickOption) { quick = true; } else if (arg.StartsWith(MinimumSleepOption, StringComparison.Ordinal)) { if (!int.TryParse(arg.Substring(MinimumSleepOption.Length), NumberStyles.None, CultureInfo.InvariantCulture, out minimumSleepFrames)) { Console.WriteLine(Usage); return 2; } } else if (arg.StartsWith("--", StringComparison.Ordinal) || filter != "") { Console.WriteLine(Usage); return 2; } else { filter = arg; } } string meaningless = ReasonTimingsAreMeaningless(); if (meaningless != null) { Console.WriteLine("Timings here would mean nothing, because " + meaningless + "."); return 1; } float? busy = SystemBusyPercent(); ClaimOneCore(); Console.WriteLine($".NET {Environment.Version}, {IntPtr.Size * 8}-bit, {Environment.ProcessorCount} " + $"logical processors, timer at {Stopwatch.Frequency / 1e6:0.##} MHz, pinned to the last processor at " + "high priority."); if (busy > BusyMachinePercent) { Console.WriteLine($"WARNING: the machine was {busy:0}% busy before starting. Absolute times will be " + "inflated and noisy; the two sides of each row take turns, so ratios still compare."); } Console.WriteLine($"The scheduler sleeps an arc whose wait is over {minimumSleepFrames} frames."); Console.WriteLine(); int rounds = quick ? Measurement.QuickRounds : Measurement.Rounds; RunScheduler(filter, rounds, minimumSleepFrames); RunHerd(filter, rounds); return 0; } } }