krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 7af09a499f

7af09a499f682c82ef5bbbdf1b0a5d35343dbc3f

parent: 0271e942ff

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-05 02:34 UTC

Add performance gate tests

Layout: unified · split

Tests/OrgEditorAppKitTests/GateTests.swift added +105
@@ -0,0 +1,105 @@
1import AppKit
2import Foundation
3import OrgDocument
4import Testing
5@testable import OrgEditorAppKit
6
7/// The phase 1 performance gates (design, "Phases"). Skipped unless `ORGSTAR_GATES` is set;
8/// run in a release build on the reference machine:
9///
10/// ORGSTAR_GATES=1 swift test -c release --filter GateTests
11///
12/// `ORGSTAR_GATE_FILE` adds a real file to the synthetic ones. Memory limits follow the design:
13/// 10x the file size for typical documents, 45x for the synthetic worst cases.
14@MainActor
15@Suite(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_GATES"] != nil))
16struct GateTests {
17 /// About 1 MB of body text under a single top-level heading.
18 static let singleSection: String = {
19 var text = "* One big section\n"
20 var n = 0
21 while text.utf8.count < 1_000_000 {
22 text += "Paragraph \(n) with *bold*, /italic/, a [[https://example.com/\(n)][link]] and <2026-10-04 Sun>.\n"
23 text += "It continues on a second line with =code= and ~more~.\n\n"
24 text += "- item \(n)\n - nested [ ] task\n\n"
25 n += 1
26 }
27 return text
28 }()
29
30 /// A 5,000-row table.
31 static let bigTable: String = {
32 var text = "* Table\n| id | name | note |\n|----+------+------|\n"
33 for n in 0..<5_000 { text += "| \(n) | row \(n) | *bold* and [[https://example.com][link]] |\n" }
34 return text
35 }()
36
37 struct Result: CustomStringConvertible {
38 var name: String
39 var bytes: Int
40 var open: Duration
41 var typingP95: Duration
42 var newlineP95: Duration
43 var memoryRatio: Double
44
45 var description: String {
46 "\(name) (\(bytes / 1000) kB): open \(open), typing p95 \(typingP95), blank line p95 \(newlineP95), memory \(String(format: "%.1f", memoryRatio))x"
47 }
48 }
49
50 /// Heap bytes in use, which unlike resident size isn't hidden by memory freed earlier.
51 static func heapBytes() -> Int {
52 var stats = malloc_statistics_t()
53 malloc_zone_statistics(nil, &stats)
54 return Int(stats.size_in_use)
55 }
56
57 func measure(_ name: String, _ text: String) -> Result {
58 let clock = ContinuousClock()
59 let bytes = Array(text.utf8)
60 let before = Self.heapBytes()
61 var harness: Harness!
62 let open = clock.measure {
63 harness = Harness(DocumentState(bytes: bytes), layout: false)
64 }
65 harness.drainStyling()
66 let memoryRatio = Double(Self.heapBytes() - before) / Double(bytes.count)
67
68 var timings: [Duration] = []
69 harness.editor.onEditTiming = { timings.append($0) }
70 let paragraphs = harness.editor.document.tree.root.descendants().filter { $0.kind == .paragraph || $0.kind == .tableCell }
71 harness.caret(at: paragraphs[paragraphs.count / 2].range.lowerBound + 1)
72 for _ in 0..<100 { harness.textView.insertText("x", replacementRange: harness.textView.selectedRange()) }
73 let typing = timings.sorted()[95]
74
75 timings = []
76 for _ in 0..<20 {
77 harness.textView.insertNewline(nil)
78 harness.textView.insertNewline(nil)
79 harness.textView.deleteBackward(nil)
80 harness.textView.deleteBackward(nil)
81 }
82 let sorted = timings.sorted()
83 return Result(name: name, bytes: bytes.count, open: open, typingP95: typing, newlineP95: sorted[sorted.count * 95 / 100], memoryRatio: memoryRatio)
84 }
85
86 @Test func gates() throws {
87 // The first text view, and the first text loaded into one, set up the text system once
88 // per process; an app pays that at launch, not per file.
89 NSTextView(usingTextLayoutManager: true).textStorage?.setAttributedString(NSAttributedString(string: "warm up"))
90 // Memory limits: worst-case dense markup for the synthetic files, typical for real ones.
91 var inputs = [("single 1 MB section", Self.singleSection, 45.0), ("5,000-row table", Self.bigTable, 45.0)]
92 if let path = ProcessInfo.processInfo.environment["ORGSTAR_GATE_FILE"] {
93 inputs.append((URL(fileURLWithPath: path).lastPathComponent, try String(contentsOfFile: path, encoding: .utf8), 10.0))
94 }
95 for (name, text, memoryLimit) in inputs {
96 let result = measure(name, text)
97 print("GATE \(result)")
98 let perMegabyte = Double(result.bytes) / 1_000_000
99 #expect(result.open < .milliseconds(100) * max(1, perMegabyte), "\(name): open")
100 #expect(result.typingP95 < .milliseconds(16), "\(name): typing")
101 #expect(result.newlineP95 < .milliseconds(16), "\(name): blank line")
102 #expect(result.memoryRatio < memoryLimit, "\(name): memory")
103 }
104 }
105}