in Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Results+Compare.swift [79:228]
func run() throws {
let first = try BenchmarkResults.load(from: self.first)
let second = try BenchmarkResults.load(from: self.second)
if chartLabels.count != 2 {
throw Benchmark.Error("--chart-labels requires exactly two values")
}
if chartLabels[0] == chartLabels[1] {
throw Benchmark.Error("--chart-labels requires two different values")
}
// Get list of all known tasks.
let firstKnownTasks = first.alltaskIDs().filter { $0.label == firstLabel }
let secondKnownTasks = second.alltaskIDs().filter { $0.label == secondLabel }
let allKnownTasks = Set(
firstKnownTasks.map { TaskID(title: $0.title) }
+ secondKnownTasks.map { TaskID(title: $0.title) }
)
let tasks = try self.tasks.resolve(
allKnownTasks: allKnownTasks,
ignoreLabels: true)
let maxCharts = self.maxCharts ?? Int.max
var missingFirst: [TaskID] = []
var missingSecond: [TaskID] = []
var missingData: [String] = []
var common: [(score: Score, first: TaskResults, second: TaskResults)] = []
for id in tasks {
let beforeID = TaskID(label: firstLabel, title: id.title)
let afterID = TaskID(label: secondLabel, title: id.title)
let before = first[id: beforeID]
guard before.sampleCount > 0 else {
missingFirst.append(beforeID)
continue
}
let after = second[id: afterID]
guard after.sampleCount > 0 else {
missingSecond.append(afterID)
continue
}
guard let score = Score(before: before, after: after) else {
missingData.append(beforeID.title)
continue
}
common.append((score, before, after))
}
if !missingFirst.isEmpty {
complain("Tasks missing from first file:")
missingFirst.forEach { complain(" \($0)") }
}
if !missingSecond.isEmpty {
complain("Tasks missing from second file:")
missingSecond.forEach { complain(" \($0)") }
}
if !missingData.isEmpty {
complain("Tasks with no overlapping measurements:")
missingData.forEach { complain(" \($0)") }
}
guard !common.isEmpty else {
throw Benchmark.Error("There is not enough data available to compare results")
}
let renderer = Graphics.bestAvailableRenderer
let theme = try chartOptions.themeSpec.resolve(with: renderer)
common.sort(by: { $0.score > $1.score })
print("Tasks with difference scores larger than \(listCutoff):")
print(" \(Score.header) Name")
for item in common {
guard item.score.score > listCutoff else { break }
let mark = item.score.score > chartCutoff ? " (*)" : ""
print(" \(item.score) \(item.first.taskID.title)\(mark)")
}
guard self.output != nil else { return }
let (output, format, multifile) = try ImageFormat.resolve(
stem: "diff",
output: self.output,
format: self.format,
multifile: (self.multifile ? true
: self.singlefile ? false
: nil))
// Generate charts.
typealias Image = (title: String, score: Score, graphics: Graphics)
var images: [Image] = []
for (score, before, after) in common.prefix(maxCharts) {
guard score.score > chartCutoff else { break }
var results = BenchmarkResults()
results.add(before.withLabel(chartLabels[0]))
results.add(after.withLabel(chartLabels[1]))
let chart = Chart(
taskIDs: results.alltaskIDs(),
in: results,
options: try chartOptions.chartOptions())
let graphics = chart.draw(
bounds: chartOptions._bounds,
theme: theme,
renderer: renderer)
images.append((before.taskID.title, score, graphics))
}
if multifile {
guard output._isDirectory else {
throw Benchmark.Error("Multifile output must be a directory: \(output)")
}
let dir = URL(output, isDirectory: true)
for (i, (title, _, graphics)) in images.enumerated() {
let data = try renderer.render(
graphics,
format: format.rawValue,
bitmapScale: chartOptions.scale)
let filename = self.filename(title: title, index: i, format: format)
let url = dir.appendingPathComponent(filename)
try data.write(to: url, options: .atomic)
}
print("\(images.count) image\(images.count == 1 ? "" : "s") generated in \(output).")
} else {
guard format.supportsSinglefileRendering else {
throw Benchmark.Error("Format '\(format)' does not support multiple charts in a single file")
}
var doc = try renderer.documentRenderer(
title: "Benchmark differentials",
format: format,
style: .flat)
for (title, score, graphics) in images {
try doc.item(
title: "\(title) (score: \(score.typesetDescription))",
graphics: graphics,
collapsed: false)
}
for (score, a, _) in common.dropFirst(images.count) {
guard score.score > listCutoff else { break }
try doc.item(
title: "\(a.taskID.title) (score: \(score.typesetDescription))",
graphics: nil,
collapsed: false)
}
let url = URL(output, isDirectory: false)
try doc.render().write(to: url)
print("\(images.count) images written to \(url.relativePath)")
}
}