2026-05-30 17:57:37 +02:00
|
|
|
import Foundation
|
|
|
|
|
import SaplingCore
|
|
|
|
|
import SaplingEditor
|
|
|
|
|
|
|
|
|
|
struct BenchmarkScenario {
|
|
|
|
|
var name: String
|
|
|
|
|
var url: URL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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("")
|
2026-05-30 19:25:41 +02:00
|
|
|
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("")
|
2026-05-30 17:57:37 +02:00
|
|
|
print("Measured total: \(format(result.measuredTotalMilliseconds)) ms")
|
|
|
|
|
print("")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 19:25:41 +02:00
|
|
|
let trackedInteractionMetricNames = [
|
|
|
|
|
"active_line_lookup",
|
|
|
|
|
"selection_update",
|
|
|
|
|
"dirty_line_invalidation_click",
|
|
|
|
|
"typing_state_update",
|
|
|
|
|
"dirty_line_invalidation_typing",
|
|
|
|
|
"render_update_typing_dirty"
|
|
|
|
|
]
|
|
|
|
|
|
2026-05-30 17:57:37 +02:00
|
|
|
func format(_ value: Double) -> String {
|
|
|
|
|
String(format: "%.3f", value)
|
|
|
|
|
}
|