Sapling/Sources/SaplingEditorBenchmark/main.swift

113 lines
4.4 KiB
Swift

import Foundation
import SaplingCore
import SaplingEditor
struct BenchmarkScenario {
var name: String
var url: URL
}
let trackedInteractionMetricNames = [
"active_line_lookup",
"selection_update",
"dirty_line_invalidation_click",
"typing_state_update",
"dirty_line_invalidation_typing",
"render_update_typing_dirty"
]
let arguments = Array(CommandLine.arguments.dropFirst())
let repositoryRoot = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true)
do {
let scenarios = try benchmarkScenarios(arguments: arguments, repositoryRoot: repositoryRoot)
print("# Sapling Editor Benchmark")
print("")
print("Generated: \(ISO8601DateFormatter().string(from: Date()))")
print("")
for scenario in scenarios {
let result = try EditorBenchmarkProfiler.profileDocument(at: scenario.url)
printScenario(name: scenario.name, result: result)
}
} catch {
fputs("SaplingEditorBenchmark failed: \(error)\n", stderr)
exit(1)
}
func benchmarkScenarios(arguments: [String], repositoryRoot: URL) throws -> [BenchmarkScenario] {
if !arguments.isEmpty {
return arguments.map { path in
let url = URL(fileURLWithPath: path)
return BenchmarkScenario(name: url.deletingPathExtension().lastPathComponent, url: url)
}
}
return try [
sampleScenario(),
BenchmarkScenario(
name: "hybrid-large-2100",
url: repositoryRoot.appendingPathComponent("Docs/EditorPrototypes/hybrid-large-2100.md")
),
BenchmarkScenario(
name: "5mb",
url: repositoryRoot.appendingPathComponent("Docs/Benchmarks/5mb.md")
)
]
}
func sampleScenario() throws -> BenchmarkScenario {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("SaplingEditorBenchmark-sample-document.md")
try SaplingSampleData.document.content.write(to: url, atomically: true, encoding: .utf8)
return BenchmarkScenario(name: "sample-document", url: url)
}
func printScenario(name: String, result: EditorBenchmarkResult) {
print("## \(name)")
print("")
print("| Property | Value |")
print("| --- | ---: |")
print("| File | `\(result.document.fileName)` |")
print("| Bytes | \(result.document.byteCount) |")
print("| Characters | \(result.document.characterCount) |")
print("| UTF-16 units | \(result.document.utf16Count) |")
print("| Lines | \(result.document.lineCount) |")
print("| Max line UTF-16 length | \(result.document.maxLineUTF16Length) |")
print("| Average line UTF-16 length | \(format(result.document.averageLineUTF16Length)) |")
print("")
print("Markdown structure: \(result.document.markdownStructureSummary)")
print("")
print("Rendered mode trace: \(result.renderedModeTrace.renderedLineCount) rendered lines, "
+ "\(result.renderedModeTrace.sourceLineCount) source lines, "
+ "\(result.renderedModeTrace.headingPlanCount) heading plans, "
+ "\(result.renderedModeTrace.paragraphPlanCount) paragraph plans, "
+ "\(result.renderedModeTrace.inlineSpanCount) inline spans, "
+ "\(result.renderedModeTrace.unsupportedReferenceLinkLikeCount) reference-style markers.")
print("")
print("| Rank | Operation | Category | Time | Percent | Notes |")
print("| ---: | --- | --- | ---: | ---: | --- |")
for (index, measurement) in result.hottestMeasurements.prefix(20).enumerated() {
let percentage = result.measuredTotalMilliseconds == 0
? 0
: measurement.durationMilliseconds / result.measuredTotalMilliseconds * 100
print("| \(index + 1) | `\(measurement.name)` | \(measurement.category.rawValue) | "
+ "\(format(measurement.durationMilliseconds)) ms | \(format(percentage))% | \(measurement.notes) |")
}
print("")
print("Tracked interaction metrics:")
print("")
print("| Operation | Time | Notes |")
print("| --- | ---: | --- |")
for name in trackedInteractionMetricNames {
guard let measurement = result.measurements.first(where: { $0.name == name }) else { continue }
print("| `\(measurement.name)` | \(format(measurement.durationMilliseconds)) ms | \(measurement.notes) |")
}
print("")
print("Measured total: \(format(result.measuredTotalMilliseconds)) ms")
print("")
}
func format(_ value: Double) -> String {
String(format: "%.3f", value)
}