Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################

/.vs
/BenchmarkDotNetBigQuery/bin
/BenchmarkDotNetBigQuery/obj
*.user
/BenchmarkDotNetBigQuery/project.lock.json
27 changes: 27 additions & 0 deletions BenchmarkDotNetBigQuery.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BenchmarkDotNetBigQuery", "BenchmarkDotNetBigQuery\BenchmarkDotNetBigQuery.xproj", "{0A4F8F8F-E568-4B5C-B8C2-1B36BDB2BC94}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EA215BB9-D476-40C3-9CE8-4A62B325A10F}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0A4F8F8F-E568-4B5C-B8C2-1B36BDB2BC94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A4F8F8F-E568-4B5C-B8C2-1B36BDB2BC94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A4F8F8F-E568-4B5C-B8C2-1B36BDB2BC94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A4F8F8F-E568-4B5C-B8C2-1B36BDB2BC94}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
21 changes: 21 additions & 0 deletions BenchmarkDotNetBigQuery/BenchmarkDotNetBigQuery.xproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>

<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>0a4f8f8f-e568-4b5c-b8c2-1b36bdb2bc94</ProjectGuid>
<RootNamespace>BenchmarkDotNetBigQuery</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>

<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
256 changes: 256 additions & 0 deletions BenchmarkDotNetBigQuery/BigQueryExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Bigquery.v2.Data;
using Google.Cloud.BigQuery.V2;

namespace BenchmarkDotNetBigQuery
{
/// <summary>
/// A BenchmarkDotNet Exporter that saves benchmark data to Google BigQuery Tables.
/// </summary>
public class BigQueryExporter: IExporter
{
private string CommitId { get; }
private bool OneSummary { get; }
private BigQueryTable SummaryTable { get; }
private BigQueryTable ReportTable { get; }

private TableSchema SummaryTableSchema { get; } = new TableSchemaBuilder
{
{"Id", BigQueryDbType.String},
{"Commit", BigQueryDbType.String},
{"Timestamp", BigQueryDbType.Timestamp},
{"HostName", BigQueryDbType.String},
{"OsVersion", BigQueryDbType.String},
{"ProcessorName", BigQueryDbType.String},
{"ProcessorCount", BigQueryDbType.Int64},
{"RuntimeVersion", BigQueryDbType.String},
{"Architecture", BigQueryDbType.String},
{"JitModules", BigQueryDbType.String},
{"DotNetCoreVersion", BigQueryDbType.String},
{"BenchmarkDotNetVersion", BigQueryDbType.String},
{"ChronometerFrequency", BigQueryDbType.Int64},
{"HardwareTimerKind", BigQueryDbType.String}
}.Build();

private TableSchema ReportTableSchema { get; } = new TableSchemaBuilder
{
{"Id", BigQueryDbType.String },
{"SummaryId", BigQueryDbType.String},
{"Namespace", BigQueryDbType.String},
{"Type", BigQueryDbType.String},
{"FullType", BigQueryDbType.String},
{"MethodName", BigQueryDbType.String},
{"FullMethodName", BigQueryDbType.String},
{"Parameters", BigQueryDbType.String},
{"MethodSignature", BigQueryDbType.String},
{"Min", BigQueryDbType.Float64},
{"Max", BigQueryDbType.Float64},
{"Mean", BigQueryDbType.Float64},
{"Median", BigQueryDbType.Float64},
{"StandardDeviation", BigQueryDbType.Float64},
{"StandardError", BigQueryDbType.Float64},
{"Variance", BigQueryDbType.Float64},
{"Percentile67", BigQueryDbType.Float64},
{"Percentile85", BigQueryDbType.Float64},
{"Percentile95", BigQueryDbType.Float64},
{"Percentile100", BigQueryDbType.Float64}
}.Build();

private string _summaryId;

/// <summary>
/// During construction, the BigQueryExporter will create the dataset and tables if needed.
/// If the dataset and tables already exist, it will validate that the tables contain the necessary fields.
/// </summary>
/// <param name="commitId">Id of the commit e.g. git hash.</param>
/// <param name="googleProjectId">The id of the google project to upload to.</param>
/// <param name="datasetId">
/// The id of the Google BigQuery dataset that contains the target tables.
/// </param>
/// <param name="oneSummary">
/// BenchmarkDotNet provides a separate summary for every class. If this parameter is false, the exporter
/// creates a new summary row for every summary. If true, creates a single summary row for the entire
/// lifetime of the exporter. Defaults to true.
/// </param>
/// <param name="summaryTableId">The id of table to put summary information in.</param>
/// <param name="reportTableId">The id of table to put report information in.</param>
/// <param name="googleCredential">Defaults to application default credentials if unspecified.</param>
public BigQueryExporter(
string commitId,
string googleProjectId,
string datasetId,
bool oneSummary = true,
string summaryTableId = "BenchmarkSummary",
string reportTableId = "BenchmarkReport",
GoogleCredential googleCredential = null)
{
CommitId = commitId;
OneSummary = oneSummary;
Task<BigQueryClient> bqClientTask = BigQueryClient.CreateAsync(googleProjectId, googleCredential);
Tuple<BigQueryTable, BigQueryTable> tables =
GetValidTablesFromDataset(datasetId, summaryTableId, reportTableId, bqClientTask).Result;
SummaryTable = tables.Item1;
ReportTable = tables.Item2;
}

/// <summary>
/// BigQueryExporter does not write to a logger. It sends an error message to the logger.
/// </summary>
public void ExportToLog(Summary summary, ILogger logger)
{
logger.WriteLine(LogKind.Error, $"{nameof(BigQueryExporter)} does not output to a logger.");
}

/// <summary>
/// This is where BigQueryExporter writes benchmark data to the BigQuery tables.
/// </summary>
/// <param name="summary">The summary to upload to Google BigQuery</param>
/// <param name="logger">Unused</param>
/// <returns>A string containing the summary guid and the names of the tables they are stoed in.</returns>
public IEnumerable<string> ExportToFiles(Summary summary, ILogger logger)
{
Task insertSummaryTask = null;
if (!OneSummary || _summaryId == null)
{
_summaryId = Guid.NewGuid().ToString();
var summaryRow = BuildSummaryRow(summary, _summaryId);
insertSummaryTask = SummaryTable.InsertAsync(summaryRow);
}
IEnumerable<BigQueryInsertRow> reportRows = summary.Reports.Select(BuildReportRowCurried(_summaryId));
Task insertReportTask = ReportTable.InsertAsync(reportRows);
insertReportTask.Wait();
if (insertSummaryTask != null)
{
insertSummaryTask.Wait();
yield return $"{_summaryId} in {SummaryTable.FullyQualifiedId} and {ReportTable.FullyQualifiedId}";
}
}

private BigQueryInsertRow BuildSummaryRow(Summary summary, string summaryId)
{
return new BigQueryInsertRow
{
{"Id", summaryId},
{"Commit", CommitId},
{"Timestamp", DateTimeOffset.UtcNow},
{"HostName", Environment.MachineName},
{"OsVersion", summary.HostEnvironmentInfo.OsVersion.Value},
{"ProcessorName", summary.HostEnvironmentInfo.ProcessorName.Value},
{"ProcessorCount", summary.HostEnvironmentInfo.ProcessorCount},
{"RuntimeVersion", summary.HostEnvironmentInfo.RuntimeVersion},
{"Architecture", summary.HostEnvironmentInfo.Architecture},
{"JitModules", summary.HostEnvironmentInfo.JitModules},
{"DotNetCoreVersion", summary.HostEnvironmentInfo.DotNetCliVersion.Value},
{"BenchmarkDotNetVersion", summary.HostEnvironmentInfo.BenchmarkDotNetVersion},
{"ChronometerFrequency", summary.HostEnvironmentInfo.ChronometerFrequency.Hertz},
{"HardwareTimerKind", summary.HostEnvironmentInfo.HardwareTimerKind.ToString()}
};
}

private static Func<BenchmarkReport, BigQueryInsertRow> BuildReportRowCurried(string summaryId)
{
return (report) => BuildReportRow(summaryId, report);
}

private static BigQueryInsertRow BuildReportRow(string summaryId, BenchmarkReport report)
{
var fullMethodName = $"{report.Benchmark.Target.Type.FullName}.{report.Benchmark.Target.Method.Name}";
return new BigQueryInsertRow
{
{"Id", Guid.NewGuid().ToString()},
{"SummaryId", summaryId},
{"Namespace", report.Benchmark.Target.Type.Namespace},
{"Type", report.Benchmark.Target.Type.Name},
{"FullType", report.Benchmark.Target.Type.FullName},
{"MethodName", report.Benchmark.Target.Method.Name},
{"FullMethodName", fullMethodName},
{"Parameters", report.Benchmark.Parameters.PrintInfo},
{"MethodSignature", report.Benchmark.Target.Method.ToString()},
{"Min", report.ResultStatistics.Min},
{"Max", report.ResultStatistics.Max},
{"Median", report.ResultStatistics.Median},
{"Mean", report.ResultStatistics.Mean },
{"StandardDeviation", report.ResultStatistics.StandardDeviation},
{"StandardError", report.ResultStatistics.StandardError},
{"Variance", report.ResultStatistics.Variance},
{"Percentile67", report.ResultStatistics.Percentiles.P67},
{"Percentile85", report.ResultStatistics.Percentiles.P85},
{"Percentile95", report.ResultStatistics.Percentiles.P95},
{"Percentile100", report.ResultStatistics.Percentiles.P100}
};
}

private async Task<Tuple<BigQueryTable, BigQueryTable>> GetValidTablesFromDataset(
string datasetId, string summaryTableId, string reportTableId, Task<BigQueryClient> bigQueryClientTask)
{
BigQueryClient bigQueryClient = await bigQueryClientTask;
BigQueryDataset dataset = await bigQueryClient.GetOrCreateDatasetAsync(datasetId);

Task<BigQueryTable> summaryTableTask = dataset.GetOrCreateTableAsync(summaryTableId, SummaryTableSchema);
Task<BigQueryTable> reportTableTask = dataset.GetOrCreateTableAsync(reportTableId, ReportTableSchema);

Task validateSummaryTableTask = ValidateTableSchemaAsync(SummaryTableSchema, summaryTableTask);
Task validateReportTableTask = ValidateTableSchemaAsync(ReportTableSchema, reportTableTask);

await Task.WhenAll(validateSummaryTableTask, validateReportTableTask);
return Tuple.Create(await summaryTableTask, await reportTableTask);
}

private async Task ValidateTableSchemaAsync(TableSchema testSchema, Task<BigQueryTable> tableTask)
{
BigQueryTable actualTable = await tableTask;
ValidateSchema(testSchema.Fields, actualTable.Schema.Fields, actualTable.Reference.TableId);
}

private void ValidateSchema(
ICollection<TableFieldSchema> testSchema, ICollection<TableFieldSchema> actualSchema, string schemaId)
{
int actualCount = actualSchema?.Count ?? 0;
int testCount = testSchema?.Count ?? 0;
if (actualCount < testCount)
{
throw new InvalidOperationException($"Schema for {schemaId} has too few fields.");
}
if (testSchema != null && actualSchema != null)
{
Func<TableFieldSchema, string> fieldNameSelector = schema => schema.Name;
var fields =
testSchema.GroupJoin(actualSchema, fieldNameSelector, fieldNameSelector, Tuple.Create);
foreach (Tuple<TableFieldSchema, IEnumerable<TableFieldSchema>> fieldTuple in fields)
{
TableFieldSchema testField = fieldTuple.Item1;
IEnumerable<TableFieldSchema> actualFields = fieldTuple.Item2;
ValidateFieldSchema(testField, actualFields, $"{schemaId}.{testField.Name}");
}
}
}

private void ValidateFieldSchema(
TableFieldSchema testField, IEnumerable<TableFieldSchema> actualFields, string fieldSchemaId)
{
TableFieldSchema actualField;
try
{
actualField = actualFields.Single();
}
catch (Exception e)
{
throw new InvalidOperationException(
$"Field {fieldSchemaId} does not exist exactly once on actual table.", e);
}
ValidateSchema(testField.Fields, actualField.Fields, fieldSchemaId);
if (!testField.Type.Equals(actualField.Type))
{
throw new InvalidOperationException(
$"Field {fieldSchemaId} had Type {actualField.Type} but should have {testField.Type}");
}
}
}
}
Loading