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("") 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("") } let trackedInteractionMetricNames = [ "active_line_lookup", "selection_update", "dirty_line_invalidation_click", "typing_state_update", "dirty_line_invalidation_typing", "render_update_typing_dirty" ] func format(_ value: Double) -> String { String(format: "%.3f", value) }