src/Elastic.CommonSchema.BenchmarkDotNetExporter/ElasticsearchBenchmarkExporter.cs (225 lines of code) (raw):
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using Elastic.Channels;
using Elastic.CommonSchema.BenchmarkDotNetExporter.Domain;
using Elastic.Ingest.Elasticsearch.CommonSchema;
using Elastic.Ingest.Elasticsearch.DataStreams;
using Elastic.Transport;
using Elastic.Transport.Products.Elasticsearch;
namespace Elastic.CommonSchema.BenchmarkDotNetExporter
{
/// <summary> Exports benchmark results to Elasticsearch </summary>
public class ElasticsearchBenchmarkExporter : ExporterBase
{
// ReSharper disable once UnusedMember.Global
/// <summary> Exports benchmark results to Elasticsearch </summary>
public ElasticsearchBenchmarkExporter(string cloudId, string apiKey)
: this(new ElasticsearchBenchmarkExporterOptions(cloudId) { ApiKey = apiKey}) { }
/// <summary> Exports benchmark results to Elasticsearch </summary>
public ElasticsearchBenchmarkExporter(ElasticsearchBenchmarkExporterOptions options)
{
Options = options;
var config = Options.CreateTransportConfiguration();
Transport = new DistributedTransport<ITransportConfiguration>(config);
}
// ReSharper disable once UnusedMember.Global
/// <summary> Exports benchmark results to Elasticsearch </summary>
public ElasticsearchBenchmarkExporter(ElasticsearchBenchmarkExporterOptions options, Func<ElasticsearchBenchmarkExporterOptions, TransportConfiguration> configure)
{
Options = options;
Transport = new DistributedTransport<ITransportConfiguration>(configure(Options));
}
private ITransport<ITransportConfiguration> Transport { get; }
private ElasticsearchBenchmarkExporterOptions Options { get; }
// We only log when we cannot write to Elasticsearch
/// <inheritdoc cref="ExporterBase.FileExtension"/>
protected override string FileExtension => "log";
/// <inheritdoc cref="ExporterBase.FileNameSuffix"/>
protected override string FileNameSuffix => "-elasticsearch-error";
/// <inheritdoc cref="ExporterBase.ExportToLog"/>
public override void ExportToLog(Summary summary, ILogger logger)
{
var waitHandle = new CountdownEvent(1);
var benchmarksCount = summary.Reports.Length;
Exception observedException = null;
var options = new DataStreamChannelOptions<BenchmarkDocument>(Transport)
{
DataStream = new DataStreamName("benchmarks", "dotnet", Options.DataStreamNamespace),
BufferOptions = new BufferOptions
{
WaitHandle = waitHandle,
OutboundBufferMaxSize = benchmarksCount,
OutboundBufferMaxLifetime = TimeSpan.FromSeconds(5)
},
ExportExceptionCallback = e => observedException ??= e,
ExportResponseCallback = (response, _) =>
{
var errorItems = response.Items.Where(i => i.Status >= 300).ToList();
if (response.TryGetElasticsearchServerError(out var error))
logger.WriteError(error.ToString());
else if (errorItems.Count == 0)
logger.WriteLine("Successfully indexed benchmark results");
foreach (var errorItem in errorItems)
logger.WriteError($"Failed to {errorItem.Action} document status: ${errorItem.Status}, error: ${errorItem.Error}");
}
};
Options.ChannelOptionsCallback?.Invoke(options);
var channel = new EcsDataStreamChannel<BenchmarkDocument>(options);
if (channel.DiagnosticsListener != null)
Options.ChannelDiagnosticsCallback?.Invoke(channel.DiagnosticsListener);
if (!channel.BootstrapElasticsearch(Options.BootstrapMethod)) return;
var benchmarks = CreateBenchmarkDocuments(summary);
var writeResult = benchmarks.Select(b => channel.TryWrite(b)).All(b => b);
var completedOnTime = waitHandle.Wait(TimeSpan.FromSeconds(20));
if (!completedOnTime)
{
logger.WriteError($"No flush in 20 seconds, published: {writeResult}, possible error: {observedException?.Message}");
if (observedException != null)
logger.WriteError(observedException.ToString());
}
}
private List<BenchmarkDocument> CreateBenchmarkDocuments(Summary summary)
{
var benchmarks = summary.Reports.Select(r =>
{
var gc = r.BenchmarkCase.Job.Environment.Gc;
var run = r.BenchmarkCase.Job.Run;
var jobConfig = new BenchmarkJobConfig
{
Platform = Enum.GetName(typeof(Platform), r.BenchmarkCase.Job.Environment.Platform),
Launch = new BenchmarkLaunchInformation
{
RunStrategy = Enum.GetName(typeof(RunStrategy), run.RunStrategy),
LaunchCount = run.LaunchCount,
WarmCount = run.WarmupCount,
UnrollFactor = run.UnrollFactor,
IterationCount = run.IterationCount,
InvocationCount = run.InvocationCount,
MaxIterationCount = run.MaxIterationCount,
MinIterationCount = run.MinIterationCount,
MaxWarmupIterationCount = run.MaxWarmupIterationCount,
MinWarmupIterationCount = run.MinWarmupIterationCount,
IterationTimeInMilliseconds = run.IterationTime.ToMilliseconds(),
},
RunTime = r.BenchmarkCase.Job.Environment.Runtime?.Name,
Jit = Enum.GetName(typeof(Jit), r.BenchmarkCase.Job.Environment.Jit),
Gc = new BenchmarkGcInfo
{
Force = gc.Force,
Server = gc.Server,
Concurrent = gc.Concurrent,
RetainVm = gc.RetainVm,
CpuGroups = gc.CpuGroups,
HeapCount = gc.HeapCount,
NoAffinitize = gc.NoAffinitize,
HeapAffinitizeMask = gc.HeapAffinitizeMask,
AllowVeryLargeObjects = gc.AllowVeryLargeObjects,
},
Id = r.BenchmarkCase.Job.Environment.Id,
};
var host = CreateHostEnvironmentInformation(summary);
var git = new BenchmarkGit
{
Sha = Options.GitCommitSha,
BranchName = Options.GitBranch,
CommitMessage = Options.GitCommitMessage,
Repository = Options.GitRepositoryIdentifier
};
var runtimeVersion = new BenchmarkLanguage
{
Version = summary.HostEnvironmentInfo.RuntimeVersion,
DotNetSdkVersion = summary.HostEnvironmentInfo.DotNetSdkVersion.Value,
HasRyuJit = summary.HostEnvironmentInfo.HasRyuJit,
//JitModules = summary.HostEnvironmentInfo.,
JitInfo = summary.HostEnvironmentInfo.JitInfo,
BuildConfiguration = summary.HostEnvironmentInfo.Configuration,
BenchmarkDotNetCaption = HostEnvironmentInfo.BenchmarkDotNetCaption,
BenchmarkDotNetVersion = summary.HostEnvironmentInfo.BenchmarkDotNetVersion,
};
var agent = new BenchmarkAgent { Git = git, Language = runtimeVersion, };
var @event = new BenchmarkEvent
{
Description = r.BenchmarkCase.Descriptor.WorkloadMethodDisplayInfo,
Action = r.BenchmarkCase.Descriptor.WorkloadMethod.Name,
Module = r.BenchmarkCase.Descriptor.Type.Namespace,
Category = new [] { summary.Title },
Type = new [] { FullNameProvider.GetTypeName(r.BenchmarkCase.Descriptor.Type) },
Duration = summary.TotalTime.Ticks * 100,
Original = r.BenchmarkCase.DisplayInfo,
JobConfig = jobConfig,
Method = FullNameProvider.GetBenchmarkName(r.BenchmarkCase),
Parameters = r.BenchmarkCase.Parameters.PrintInfo,
};
var data = new BenchmarkDocument
{
Timestamp = DateTime.UtcNow,
Host = host,
Agent = agent,
Event = @event,
Benchmark = new BenchmarkData(r.ResultStatistics, r.Success),
};
if (summary.BenchmarksCases.Any(c => c.Config.HasMemoryDiagnoser()))
data.Benchmark.Memory = new BenchmarkGcStats(r.GcStats, r.BenchmarkCase);
var grouped = r.AllMeasurements
.GroupBy(m => $"{m.IterationStage.ToString()}-{m.IterationMode.ToString()}")
.Where(g => g.Any())
.ToList();
@event.MeasurementStages = grouped
.Select(g => new BenchmarkMeasurementStage
{
IterationMode = g.First().IterationMode.ToString(),
IterationStage = g.First().IterationStage.ToString(),
Operations = g.First().Operations,
});
var warmupCount = grouped.Select(g => g.First())
.FirstOrDefault(s => s.IterationStage == IterationStage.Warmup && s.IterationMode == IterationMode.Workload)
.Operations;
var measuredCount = grouped.Select(g => g.First())
.FirstOrDefault(s => s.IterationStage == IterationStage.Result && s.IterationMode == IterationMode.Workload)
.Operations;
@event.Repetitions = new BenchmarkSimplifiedWorkloadCounts { Warmup = warmupCount, Measured = measuredCount };
return data;
})
.ToList();
return benchmarks;
}
private static string OsName() =>
Environment.OSVersion.Platform switch
{
PlatformID.MacOSX => "Max OS X",
PlatformID.Unix => "Linux",
PlatformID.Win32NT => "Windows",
PlatformID.Win32S => "Windows",
PlatformID.Win32Windows => "Windows",
PlatformID.WinCE => "Windows",
PlatformID.Xbox => "XBox",
_ => "Unknown"
};
private static string OsPlatform() =>
Environment.OSVersion.Platform switch
{
PlatformID.MacOSX => "darwin",
PlatformID.Unix => "unix",
_ => OsName().ToLowerInvariant()
};
private static BenchmarkHost CreateHostEnvironmentInformation(Summary summary)
{
var environmentInfo = new BenchmarkHost
{
ProcessorName = summary.HostEnvironmentInfo.CpuInfo.Value.ProcessorName,
PhysicalProcessorCount = summary.HostEnvironmentInfo.CpuInfo.Value?.PhysicalProcessorCount,
PhysicalCoreCount = summary.HostEnvironmentInfo.CpuInfo.Value?.PhysicalCoreCount,
LogicalCoreCount = summary.HostEnvironmentInfo.CpuInfo.Value?.LogicalCoreCount,
Architecture = summary.HostEnvironmentInfo.Architecture,
VirtualMachineHypervisor = summary.HostEnvironmentInfo.VirtualMachineHypervisor.Value?.Name,
InDocker = summary.HostEnvironmentInfo.InDocker,
HasAttachedDebugger = summary.HostEnvironmentInfo.HasAttachedDebugger,
ChronometerFrequencyHertz = summary.HostEnvironmentInfo.ChronometerFrequency.Hertz,
HardwareTimerKind = summary.HostEnvironmentInfo.HardwareTimerKind.ToString(),
Os = new Os
{
Version = summary.HostEnvironmentInfo.OsVersion.Value,
Name = OsName(),
Platform = OsPlatform()
}
};
return environmentInfo;
}
}
}