using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ariadne.PerformanceTests
{
public struct Timing
{
public double MedianNanoseconds;
public double MinimumNanoseconds;
public double BytesPerCall;
}
public static class Measurement
{
public const double WarmUpMilliseconds = 100;
public const double BatchMilliseconds = 5;
///
/// Odd, so the median is a sample rather than an average of two.
///
public const int Rounds = 31;
public const int QuickRounds = 7;
public static double NanosecondsFor(long ticks)
{
return ticks * 1e9 / Stopwatch.Frequency;
}
public static double Median(List samples)
{
var sorted = new List(samples);
sorted.Sort();
return sorted[sorted.Count / 2];
}
public static double Minimum(List samples)
{
double returnMe = double.MaxValue;
foreach (double sample in samples)
{
returnMe = Math.Min(returnMe, sample);
}
return returnMe;
}
///
/// Runs the call long enough to JIT it and settle the caches, and returns how many calls fill one batch.
///
public static int WarmUp(Action call)
{
long budget = (long)(WarmUpMilliseconds * Stopwatch.Frequency / 1000);
long start = Stopwatch.GetTimestamp();
long calls = 0;
long elapsed;
do
{
call();
++calls;
elapsed = Stopwatch.GetTimestamp() - start;
}
while (elapsed < budget);
double millisecondsPerCall = elapsed * 1000.0 / Stopwatch.Frequency / calls;
return Math.Max(1, (int)(BatchMilliseconds / millisecondsPerCall));
}
public static double NanosecondsPerCall(Action call, int calls)
{
long start = Stopwatch.GetTimestamp();
for (int i = 0; i < calls; ++i)
{
call();
}
return NanosecondsFor(Stopwatch.GetTimestamp() - start) / calls;
}
public static double BytesPerCall(Action call, int calls)
{
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < calls; ++i)
{
call();
}
return (double)(GC.GetAllocatedBytesForCurrentThread() - before) / calls;
}
///
/// Times each call once warm, one batch at a time, taking turns.
///
public static Timing[] TimeTakingTurns(IReadOnlyList calls, int rounds)
{
var callsPerBatch = new int[calls.Count];
var samples = new List[calls.Count];
for (int call = 0; call < calls.Count; ++call)
{
callsPerBatch[call] = WarmUp(calls[call]);
samples[call] = new List(rounds);
}
// Each round starts from a different call, so drift in clock speed as the CPU heats up lands on every call
// alike rather than on whichever ran last.
for (int round = 0; round < rounds; ++round)
{
for (int turn = 0; turn < calls.Count; ++turn)
{
int call = (round + turn) % calls.Count;
samples[call].Add(NanosecondsPerCall(calls[call], callsPerBatch[call]));
}
}
var returnMe = new Timing[calls.Count];
for (int call = 0; call < calls.Count; ++call)
{
returnMe[call] = new Timing
{
MedianNanoseconds = Median(samples[call]),
MinimumNanoseconds = Minimum(samples[call]),
BytesPerCall = BytesPerCall(calls[call], callsPerBatch[call]),
};
}
return returnMe;
}
}
}