Tests/OrgEditorAppKitTests/GateTests.swift
116 lines · 5360 bytes
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 /// Entries with property and LOGBOOK drawers, all folded when the file opens.
19 static let drawers: String = {
20 var text = ""
21 for n in 0..<3_000 {
22 text += "* TODO Entry \(n) :tag:\n:PROPERTIES:\n:ID: \(n)-uuid\n:CREATED: [2026-10-04 Sun]\n:END:\n"
23 text += ":LOGBOOK:\n- State \"DONE\" from \"TODO\" [2026-10-04 Sun 10:00]\nCLOCK: [2026-10-04 Sun 09:00]--[2026-10-04 Sun 10:00] => 1:00\n:END:\n"
24 text += "Body of entry \(n), a paragraph of text.\n\n"
25 }
26 return text
27 }()
28
29 static let singleSection: String = {
30 var text = "* One big section\n"
31 var n = 0
32 while text.utf8.count < 1_000_000 {
33 text += "Paragraph \(n) with *bold*, /italic/, a [[https://example.com/\(n)][link]] and <2026-10-04 Sun>.\n"
34 text += "It continues on a second line with =code= and ~more~.\n\n"
35 text += "- item \(n)\n - nested [ ] task\n\n"
36 n += 1
37 }
38 return text
39 }()
40
41 /// A 5,000-row table.
42 static let bigTable: String = {
43 var text = "* Table\n| id | name | note |\n|----+------+------|\n"
44 for n in 0..<5_000 { text += "| \(n) | row \(n) | *bold* and [[https://example.com][link]] |\n" }
45 return text
46 }()
47
48 struct Result: CustomStringConvertible {
49 var name: String
50 var bytes: Int
51 var open: Duration
52 var typingP95: Duration
53 var newlineP95: Duration
54 var memoryRatio: Double
55
56 var description: String {
57 "\(name) (\(bytes / 1000) kB): open \(open), typing p95 \(typingP95), blank line p95 \(newlineP95), memory \(String(format: "%.1f", memoryRatio))x"
58 }
59 }
60
61 /// Heap bytes in use, which unlike resident size isn't hidden by memory freed earlier.
62 static func heapBytes() -> Int {
63 var stats = malloc_statistics_t()
64 malloc_zone_statistics(nil, &stats)
65 return Int(stats.size_in_use)
66 }
67
68 func measure(_ name: String, _ text: String) -> Result {
69 let clock = ContinuousClock()
70 let bytes = Array(text.utf8)
71 let before = Self.heapBytes()
72 var harness: Harness!
73 let open = clock.measure {
74 harness = Harness(DocumentState(bytes: bytes), layout: false)
75 }
76 harness.drainStyling()
77 let memoryRatio = Double(Self.heapBytes() - before) / Double(bytes.count)
78
79 var timings: [Duration] = []
80 harness.editor.onEditTiming = { timings.append($0) }
81 let paragraphs = harness.editor.document.tree.root.descendants().filter { $0.kind == .paragraph || $0.kind == .tableCell }
82 harness.caret(at: paragraphs[paragraphs.count / 2].range.lowerBound + 1)
83 for _ in 0..<100 { harness.textView.insertText("x", replacementRange: harness.textView.selectedRange()) }
84 let typing = timings.sorted()[95]
85
86 timings = []
87 for _ in 0..<20 {
88 harness.textView.insertNewline(nil)
89 harness.textView.insertNewline(nil)
90 harness.textView.deleteBackward(nil)
91 harness.textView.deleteBackward(nil)
92 }
93 let sorted = timings.sorted()
94 return Result(name: name, bytes: bytes.count, open: open, typingP95: typing, newlineP95: sorted[sorted.count * 95 / 100], memoryRatio: memoryRatio)
95 }
96
97 @Test func gates() throws {
98 // The first text view, and the first text loaded into one, set up the text system once
99 // per process; an app pays that at launch, not per file.
100 NSTextView(usingTextLayoutManager: true).textStorage?.setAttributedString(NSAttributedString(string: "warm up"))
101 // Memory limits: worst-case dense markup for the synthetic files, typical for real ones.
102 var inputs = [("single 1 MB section", Self.singleSection, 45.0), ("5,000-row table", Self.bigTable, 45.0), ("3,000 entries with drawers", Self.drawers, 45.0)]
103 if let path = ProcessInfo.processInfo.environment["ORGSTAR_GATE_FILE"] {
104 inputs.append((URL(fileURLWithPath: path).lastPathComponent, try String(contentsOfFile: path, encoding: .utf8), 10.0))
105 }
106 for (name, text, memoryLimit) in inputs {
107 let result = measure(name, text)
108 print("GATE \(result)")
109 let perMegabyte = Double(result.bytes) / 1_000_000
110 #expect(result.open < .milliseconds(100) * max(1, perMegabyte), "\(name): open")
111 #expect(result.typingP95 < .milliseconds(16), "\(name): typing")
112 #expect(result.newlineP95 < .milliseconds(16), "\(name): blank line")
113 #expect(result.memoryRatio < memoryLimit, "\(name): memory")
114 }
115 }
116}