The example illustrates a "clock-time" benchmark for assessing detection speed.Using a YAML formatted evidence file - "20000 Evidence Records.yml" - supplied with the distribution or can be obtained from the data repository on Github.
It's important to understand the trade-offs between performance, memory usage and accuracy, that the 51Degrees pipeline configuration makes available, and this example shows a range of different configurations to illustrate the difference in performance.
Requesting properties from a single component reduces detection time compared with requesting properties from multiple components. If you don't specify any properties to detect, then all properties are detected.
This example requires a local data file. The free 'Lite' data file can be acquired by
pulling the git submodules under this repository (run `git submodule update --recursive`)
or from the device-detection-data
GitHub repository.
The Lite data file is only used for illustration, and has limited accuracy and capabilities.
Find out about the more capable data files that are available on our
pricing page
using FiftyOne.Pipeline.Core.FlowElements;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
{
public class Program
{
public class BenchmarkResult
{
public long Count { get; set; }
public Stopwatch Timer { get; } = new Stopwatch();
public long MobileCount { get; set; }
public long NotMobileCount { get; set; }
}
private static readonly PerformanceConfiguration[] _configs = new PerformanceConfiguration[]
{
new PerformanceConfiguration(true, PerformanceProfiles.MaxPerformance, false, true, false),
new PerformanceConfiguration(true, PerformanceProfiles.MaxPerformance, true, true, false),
new PerformanceConfiguration(false, PerformanceProfiles.LowMemory, false, true, false),
new PerformanceConfiguration(false, PerformanceProfiles.LowMemory, true, true, false)
};
private const ushort DEFAULT_THREAD_COUNT = 4;
private static float GetMsPerDetection(
IList<BenchmarkResult> results,
int threadCount)
{
var detections = results.Sum(r => r.Count);
var milliseconds = results.Sum(r => r.Timer.ElapsedMilliseconds);
return (float)(milliseconds) / (detections * threadCount);
}
public class Example : ExampleBase
{
private IPipeline _pipeline;
public Example(IPipeline pipeline)
{
_pipeline = pipeline;
}
private List<BenchmarkResult> Run(
TextReader evidenceReader,
TextWriter output,
int threadCount,
bool enableGC = true,
long noGcRegionSize = 64 * 1024 * 1024)
{
output.WriteLine($"Loaded {evidence.Count} evidence records");
evidence = Utf8EvidencePreprocessor.ConvertToUtf8(evidence);
output.WriteLine("Warming up");
var warmup = Benchmark(evidence, threadCount);
var warmupTime = warmup.Sum(r => r.Timer.ElapsedMilliseconds);
GC.Collect();
Task.Delay(500).Wait();
output.WriteLine("Running");
bool gcWasDisabled = false;
if (!enableGC)
{
try
{
gcWasDisabled = GC.TryStartNoGCRegion(noGcRegionSize);
if (gcWasDisabled)
{
output.WriteLine($"GC disabled for performance test (NoGC region: {noGcRegionSize / (1024 * 1024)}MB)");
}
else
{
output.WriteLine("Could not disable GC - running with GC enabled");
}
}
catch (ArgumentOutOfRangeException)
{
output.WriteLine($"NoGC region size too large ({noGcRegionSize / (1024 * 1024)}MB) - running with GC enabled");
}
}
var execution = Benchmark(evidence, threadCount);
var executionTime = execution.Sum(r => r.Timer.ElapsedMilliseconds);
if (gcWasDisabled)
{
GC.EndNoGCRegion();
output.WriteLine("GC re-enabled");
}
output.WriteLine($"Finished - Execution time was {executionTime} ms, " +
$"adjustment from warm-up {executionTime - warmupTime} ms");
Report(execution, threadCount, output);
return execution;
}
private void Report(List<BenchmarkResult> results,
int threadCount,
TextWriter output)
{
var msPerDetection = GetMsPerDetection(results, threadCount);
var detectionsPerSecond = 1000 / msPerDetection;
output.WriteLine($"Overall: {results.Sum(i => i.Count)} detections, Average millisecs per " +
$"detection: {msPerDetection}, Detections per second: {detectionsPerSecond}");
output.WriteLine($"Overall: Concurrent threads: {threadCount}");
}
private List<BenchmarkResult> Benchmark(
List<Dictionary<string, object>> allEvidence,
int threadCount)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
List<BenchmarkResult> results = new List<BenchmarkResult>();
var processing = Parallel.ForEach(allEvidence,
new ParallelOptions()
{
MaxDegreeOfParallelism = threadCount,
CancellationToken = cancellationTokenSource.Token
},
() => new BenchmarkResult(),
(evidence, loopState, result) =>
{
result.Timer.Start();
using (var data = _pipeline.CreateFlowData())
{
data.AddEvidence(evidence).Process();
var device = data.Get<IDeviceData>();
result.Count++;
if(device.IsMobile.HasValue && device.IsMobile.Value)
{
result.MobileCount++;
}
else
{
result.NotMobileCount++;
}
}
result.Timer.Stop();
return result;
},
(result) =>
{
lock (results)
{
results.Add(result);
}
});
return results;
}
public static List<BenchmarkResult> Run(string dataFile, string evidenceFile,
PerformanceConfiguration config, TextWriter output, ushort threadCount, bool enableGC = true, long noGcRegionSize = 64 * 1024 * 1024)
{
using (var serviceProvider = new ServiceCollection()
.AddLogging(l => l.AddConsole())
.AddTransient<PipelineBuilder>()
.AddTransient<DeviceDetectionHashEngineBuilder>()
.AddSingleton((x) =>
{
var builder = x.GetRequiredService<DeviceDetectionHashEngineBuilder>()
.SetDataFileSystemWatcher(false)
.SetAutoUpdate(false)
.SetDataUpdateOnStartup(false)
.SetPerformanceProfile(config.Profile)
.SetUsePerformanceGraph(config.PerformanceGraph)
.SetUsePredictiveGraph(config.PredictiveGraph)
.SetConcurrency(threadCount);
if (config.AllProperties == false)
{
builder.SetProperty("IsMobile");
}
DeviceDetectionHashEngine engine = null;
if (config.LoadFromDisk)
{
engine = builder.Build(dataFile, false);
}
else
{
using (MemoryStream stream = new MemoryStream(File.ReadAllBytes(dataFile)))
{
engine = builder.Build(stream);
}
}
return engine;
})
.AddSingleton((x) => {
return x.GetRequiredService<PipelineBuilder>()
.AddFlowElement(x.GetRequiredService<DeviceDetectionHashEngine>())
.Build();
})
.AddTransient<Example>()
.BuildServiceProvider())
using (var evidenceReader = new StreamReader(File.OpenRead(evidenceFile)))
{
if (string.IsNullOrWhiteSpace(dataFile))
{
serviceProvider.GetRequiredService<ILogger<Program>>().LogError(
"Failed to find a device detection data file. Make sure the " +
"device-detection-data submodule has been updated by running " +
"`git submodule update --recursive`.");
return null;
}
else
{
ExampleUtils.CheckDataFile(
serviceProvider.GetRequiredService<IPipeline>(),
serviceProvider.GetRequiredService<ILogger<Program>>());
output.WriteLine($"Processing evidence from '{evidenceFile}'");
output.WriteLine($"Data loaded from '{(config.LoadFromDisk ? "disk" : "memory")}'");
output.WriteLine($"Benchmarking with profile '{config.Profile}', " +
$"AllProperties {config.AllProperties}, " +
$"PerformanceGraph {config.PerformanceGraph}, " +
$"PredictiveGraph {config.PredictiveGraph}");
return serviceProvider.GetRequiredService<Example>().Run(evidenceReader, output, threadCount, enableGC, noGcRegionSize);
}
}
}
}
static void Main(string[] args)
{
var options = ExampleUtils.ParseOptions(args);
if (options != null) {
var dataFile = options.DataFilePath != null ? options.DataFilePath :
ExampleUtils.FindFile("TAC-HashV41.hash") ??
ExampleUtils.FindFile(Constants.LITE_HASH_DATA_FILE_NAME);
var evidenceFile = options.EvidenceFile != null ? options.EvidenceFile :
ExampleUtils.FindFile(Constants.YAML_EVIDENCE_FILE_NAME);
Console.WriteLine("\n==== Running benchmarks WITH Garbage Collection ====\n");
var resultsWithGC = new Dictionary<PerformanceConfiguration, IList<BenchmarkResult>>();
foreach (var config in _configs)
{
var result = Example.Run(dataFile, evidenceFile, config, Console.Out, DEFAULT_THREAD_COUNT, true);
resultsWithGC[config] = result;
}
var optimalNoGcSize = 128 * 1024 * 1024;
Console.WriteLine($"\n==== Running benchmarks WITHOUT Garbage Collection ({optimalNoGcSize / (1024 * 1024)}MB NoGC Region) ====\n");
var resultsWithoutGC = new Dictionary<PerformanceConfiguration, IList<BenchmarkResult>>();
foreach (var config in _configs)
{
var result = Example.Run(dataFile, evidenceFile, config, Console.Out, DEFAULT_THREAD_COUNT, false, optimalNoGcSize);
resultsWithoutGC[config] = result;
}
Console.WriteLine("\n==== Performance Comparison Summary ====\n");
Console.WriteLine("Configuration\t\t\t\tWith GC (ms/det)\tWithout GC (ms/det)\tImprovement %\tDet/Sec (GC)\tDet/Sec (No GC)");
Console.WriteLine(new string('-', 120));
foreach (var config in _configs)
{
var msWithGC = GetMsPerDetection(resultsWithGC[config], DEFAULT_THREAD_COUNT);
var msWithoutGC = GetMsPerDetection(resultsWithoutGC[config], DEFAULT_THREAD_COUNT);
var improvement = ((msWithGC - msWithoutGC) / msWithGC) * 100;
var detectionsPerSecWithGC = 1000 / msWithGC;
var detectionsPerSecWithoutGC = 1000 / msWithoutGC;
var configName = $"{config.Profile}{(config.AllProperties ? " All" : " Specific")}";
Console.WriteLine($"{configName,-35}\t{msWithGC:F4}\t\t{msWithoutGC:F4}\t\t{improvement:F1}%\t\t{detectionsPerSecWithGC:F0}\t\t{detectionsPerSecWithoutGC:F0}");
}
if (string.IsNullOrEmpty(options.JsonOutput) == false)
{
using (var jsonOutput = File.CreateText(options.JsonOutput))
{
var jsonResults = resultsWithoutGC.ToDictionary(
k => $"{Enum.GetName(k.Key.Profile)}{(k.Key.AllProperties ? "_All" : "")}",
v => new Dictionary<string, float>()
{
{"DetectionsPerSecond", 1000 / GetMsPerDetection(v.Value, DEFAULT_THREAD_COUNT) },
{"DetectionsPerSecondPerThread", 1000 / (GetMsPerDetection(v.Value, DEFAULT_THREAD_COUNT) * DEFAULT_THREAD_COUNT) },
{"MsPerDetection", GetMsPerDetection(v.Value, DEFAULT_THREAD_COUNT) }
});
jsonOutput.Write(JsonSerializer.Serialize(jsonResults));
}
}
}
}
}
}