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;
}
}
}