Skip to content

Commit 6ca5cfd

Browse files
committed
Add new comparison features
- Compare with NS regular expression - Save comparison results - Save and compare estimated compile times
1 parent 49c1d8c commit 6ca5cfd

File tree

5 files changed

+257
-142
lines changed

5 files changed

+257
-142
lines changed

Sources/RegexBenchmark/Benchmark.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ struct InputListNSBenchmark: RegexBenchmark {
8989

9090
/// A benchmark meant to be ran across multiple engines
9191
struct CrossBenchmark {
92+
/// Suffix added onto NSRegularExpression benchmarks
93+
static let nsSuffix = "_NS"
94+
9295
/// The base name of the benchmark
9396
var baseName: String
9497

@@ -127,7 +130,7 @@ struct CrossBenchmark {
127130
target: input))
128131
runner.register(
129132
NSBenchmark(
130-
name: baseName + "Whole_NS",
133+
name: baseName + "Whole" + CrossBenchmark.nsSuffix,
131134
regex: nsRegex,
132135
type: .first,
133136
target: input))
@@ -140,7 +143,7 @@ struct CrossBenchmark {
140143
target: input))
141144
runner.register(
142145
NSBenchmark(
143-
name: baseName + "All_NS",
146+
name: baseName + "All" + CrossBenchmark.nsSuffix,
144147
regex: nsRegex,
145148
type: .allMatches,
146149
target: input))
@@ -153,7 +156,7 @@ struct CrossBenchmark {
153156
target: input))
154157
runner.register(
155158
NSBenchmark(
156-
name: baseName + "First_NS",
159+
name: baseName + "First" + CrossBenchmark.nsSuffix,
157160
regex: nsRegex,
158161
type: .first,
159162
target: input))

Sources/RegexBenchmark/BenchmarkChart.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,7 @@ import Charts
1515
import SwiftUI
1616

1717
struct BenchmarkChart: View {
18-
struct Comparison: Identifiable {
19-
var id = UUID()
20-
var name: String
21-
var baseline: BenchmarkResult
22-
var latest: BenchmarkResult
23-
}
24-
25-
var comparisons: [Comparison]
18+
var comparisons: [BenchmarkResult.Comparison]
2619

2720
var body: some View {
2821
VStack(alignment: .leading) {
@@ -94,7 +87,7 @@ struct BenchmarkChart: View {
9487
}
9588

9689
struct BenchmarkResultApp: App {
97-
static var comparisons: [BenchmarkChart.Comparison]?
90+
static var comparisons: [BenchmarkResult.Comparison]?
9891

9992
var body: some Scene {
10093
WindowGroup {
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import Foundation
2+
3+
extension BenchmarkRunner {
4+
func save(to savePath: String) throws {
5+
let url = URL(fileURLWithPath: savePath, isDirectory: false)
6+
let parent = url.deletingLastPathComponent()
7+
if !FileManager.default.fileExists(atPath: parent.path) {
8+
try! FileManager.default.createDirectory(atPath: parent.path, withIntermediateDirectories: true)
9+
}
10+
print("Saving result to \(url.path)")
11+
try results.save(to: url)
12+
}
13+
14+
func compare(against compareFilePath: String, showChart: Bool, saveTo: String?) throws {
15+
let compareFileURL = URL(fileURLWithPath: compareFilePath)
16+
let compareResult = try SuiteResult.load(from: compareFileURL)
17+
let compareFile = compareFileURL.lastPathComponent
18+
19+
let comparisons = results
20+
.compare(with: compareResult)
21+
.filter({!$0.name.contains("_NS")})
22+
.filter({$0.diff != nil})
23+
displayComparisons(comparisons, showChart, against: "saved benchmark result " + compareFile)
24+
if let saveFile = saveTo {
25+
try saveComparisons(comparisons, path: saveFile)
26+
}
27+
28+
let compileTimeComparisons = results
29+
.compareCompileTimes(with: compareResult)
30+
.filter({!$0.name.contains("_NS")})
31+
.filter({$0.diff != nil})
32+
print("Comparing estimated compile times")
33+
displayComparisons(compileTimeComparisons, false, against: "saved benchmark result " + compareFile)
34+
}
35+
36+
func compareWithNS(showChart: Bool, saveTo: String?) throws {
37+
let comparisons = results.compareWithNS().filter({$0.diff != nil})
38+
displayComparisons(comparisons, showChart, against: "NSRegularExpression (via CrossBenchmark)")
39+
if let saveFile = saveTo {
40+
try saveComparisons(comparisons, path: saveFile)
41+
}
42+
}
43+
44+
func displayComparisons(_ comparisons: [BenchmarkResult.Comparison], _ showChart: Bool, against: String) {
45+
let regressions = comparisons.filter({$0.diff!.seconds > 0})
46+
.sorted(by: {(a,b) in a.diff!.seconds > b.diff!.seconds})
47+
let improvements = comparisons.filter({$0.diff!.seconds < 0})
48+
.sorted(by: {(a,b) in a.diff!.seconds < b.diff!.seconds})
49+
50+
print("Comparing against \(against)")
51+
print("=== Regressions ======================================================================")
52+
for item in regressions {
53+
print(item)
54+
}
55+
56+
print("=== Improvements =====================================================================")
57+
for item in improvements {
58+
print(item)
59+
}
60+
61+
#if os(macOS)
62+
if showChart {
63+
print("""
64+
=== Comparison chart =================================================================
65+
Press Control-C to close...
66+
""")
67+
BenchmarkResultApp.comparisons = {
68+
return comparisons.sorted {
69+
let delta0 = Float($0.latest.median.seconds - $0.baseline.median.seconds)
70+
/ Float($0.baseline.median.seconds)
71+
let delta1 = Float($1.latest.median.seconds - $1.baseline.median.seconds)
72+
/ Float($1.baseline.median.seconds)
73+
return delta0 > delta1
74+
}
75+
}()
76+
BenchmarkResultApp.main()
77+
}
78+
#endif
79+
}
80+
81+
func saveComparisons(_ comparisons: [BenchmarkResult.Comparison], path: String) throws {
82+
let url = URL(fileURLWithPath: path, isDirectory: false)
83+
let parent = url.deletingLastPathComponent()
84+
if !FileManager.default.fileExists(atPath: parent.path) {
85+
try! FileManager.default.createDirectory(atPath: parent.path, withIntermediateDirectories: true)
86+
}
87+
88+
var contents = "name,baseline,latest,diff,percentage\n"
89+
for comparison in comparisons {
90+
contents += comparison.asCsv + "\n"
91+
}
92+
print("Saving comparisons as .csv to \(path)")
93+
try contents.write(to: url, atomically: true, encoding: String.Encoding.utf8)
94+
}
95+
}
96+
97+
struct BenchmarkResult: Codable {
98+
let median: Time
99+
let estimatedCompileTime: Time
100+
let stdev: Double
101+
let samples: Int
102+
103+
init(_ initialRunTime: Time, _ median: Time, _ stdev: Double, _ samples: Int) {
104+
self.estimatedCompileTime = initialRunTime - median
105+
self.median = median
106+
self.stdev = stdev
107+
self.samples = samples
108+
}
109+
}
110+
111+
extension BenchmarkResult {
112+
struct Comparison: Identifiable, CustomStringConvertible {
113+
var id = UUID()
114+
var name: String
115+
var baseline: BenchmarkResult
116+
var latest: BenchmarkResult
117+
var diffCompileTimes: Bool = false
118+
119+
var diff: Time? {
120+
if diffCompileTimes {
121+
return latest.estimatedCompileTime - baseline.estimatedCompileTime
122+
}
123+
if Stats.tTest(baseline, latest) {
124+
return latest.median - baseline.median
125+
}
126+
return nil
127+
}
128+
129+
var description: String {
130+
guard let diff = diff else {
131+
return "- \(name) N/A"
132+
}
133+
let oldVal: Time
134+
let newVal: Time
135+
if diffCompileTimes {
136+
oldVal = baseline.estimatedCompileTime
137+
newVal = latest.estimatedCompileTime
138+
} else {
139+
oldVal = baseline.median
140+
newVal = latest.median
141+
}
142+
let percentage = (1000 * diff.seconds / oldVal.seconds).rounded()/10
143+
let len = max(40 - name.count, 1)
144+
let nameSpacing = String(repeating: " ", count: len)
145+
return "- \(name)\(nameSpacing)\(newVal)\t\(oldVal)\t\(diff)\t\t\(percentage)%"
146+
}
147+
148+
var asCsv: String {
149+
guard let diff = diff else {
150+
return "\(name),N/A"
151+
}
152+
let oldVal: Time
153+
let newVal: Time
154+
if diffCompileTimes {
155+
oldVal = baseline.estimatedCompileTime
156+
newVal = latest.estimatedCompileTime
157+
} else {
158+
oldVal = baseline.median
159+
newVal = latest.median
160+
}
161+
let percentage = (1000 * diff.seconds / oldVal.seconds).rounded()/10
162+
return "\"\(name)\",\(newVal.seconds),\(oldVal.seconds),\(diff.seconds),\(percentage)%"
163+
}
164+
}
165+
}
166+
167+
struct SuiteResult {
168+
var results: [String: BenchmarkResult] = [:]
169+
170+
mutating func add(name: String, result: BenchmarkResult) {
171+
results.updateValue(result, forKey: name)
172+
}
173+
174+
/// Compares with the given SuiteResult
175+
func compare(with other: SuiteResult) -> [BenchmarkResult.Comparison] {
176+
var comparisons: [BenchmarkResult.Comparison] = []
177+
for item in results {
178+
if let otherVal = other.results[item.key] {
179+
comparisons.append(
180+
.init(name: item.key, baseline: item.value, latest: otherVal))
181+
}
182+
}
183+
return comparisons
184+
}
185+
186+
/// Compares with the NSRegularExpression benchmarks generated by CrossBenchmark
187+
func compareWithNS() -> [BenchmarkResult.Comparison] {
188+
var comparisons: [BenchmarkResult.Comparison] = []
189+
for item in results {
190+
let key = item.key + CrossBenchmark.nsSuffix
191+
if let nsResult = results[key] {
192+
comparisons.append(
193+
.init(name: item.key, baseline: nsResult, latest: item.value))
194+
}
195+
}
196+
return comparisons
197+
}
198+
199+
/// Compares the estimated compile times
200+
func compareCompileTimes(with other: SuiteResult) -> [BenchmarkResult.Comparison] {
201+
var comparisons: [BenchmarkResult.Comparison] = []
202+
for item in results {
203+
if let otherVal = other.results[item.key] {
204+
comparisons.append(
205+
.init(name: item.key, baseline: item.value, latest: otherVal, diffCompileTimes: true))
206+
}
207+
}
208+
return comparisons
209+
}
210+
}
211+
212+
extension SuiteResult: Codable {
213+
func save(to url: URL) throws {
214+
let encoder = JSONEncoder()
215+
let data = try encoder.encode(self)
216+
try data.write(to: url, options: .atomic)
217+
}
218+
219+
static func load(from url: URL) throws -> SuiteResult {
220+
let decoder = JSONDecoder()
221+
let data = try Data(contentsOf: url)
222+
return try decoder.decode(SuiteResult.self, from: data)
223+
}
224+
}

0 commit comments

Comments
 (0)