Skip to content

Commit 166ba68

Browse files
committed
⚡ perf: cut the work each redraw repeats
The status bar parsed its template character by character once per cell and once more to decide whether a countdown was needed, for each of the five tiers the adaptive ladder tries. That is two dozen passes over the same string per rebuild, and a rebuild happens every second while a template references {reset}. Parsing now happens once per build. The log buffer re-encoded its whole 500-entry array and wrote it atomically for every line, which with detailed logging is several per HTTP request. Writes are coalesced to one every five seconds, with a flush on shutdown and on clear; every line still reaches os_log as it happens, so that is all a crash could lose. LinkifiedText compiled a Regex per banner per body evaluation, the window selection list recomputed the default selection twice per row, and the hover cards cancelled and allocated a Task for every mouse-move point over a card that was already showing. Claude-Session: https://claude.ai/code/session_01CGckDFXvufWVmRH4uyE4z8
1 parent a49091d commit 166ba68

8 files changed

Lines changed: 75 additions & 11 deletions

File tree

Sources/TokenMenuBarCore/Logging/LogBuffer.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ public final class LogBuffer: @unchecked Sendable {
5252
public static let retention: TimeInterval = 7 * 86400
5353

5454
private let lock = NSLock()
55+
/// How long a line may sit in memory before it reaches disk. Every line goes to `os_log` as it happens, so this
56+
/// only sets how much of the in-app log a crash could lose.
57+
public static let flushInterval: TimeInterval = 5
5558
private var entries: [LogEntry] = []
5659
private var lastPrune: Date?
60+
private var lastPersist: Date?
61+
private var dirty = false
5762
private let fileURL: URL?
5863
private let clock: Clock
5964
private let osLog = Logger(subsystem: "dev.tox.token-menu-bar", category: "app")
@@ -102,11 +107,21 @@ public final class LogBuffer: @unchecked Sendable {
102107
entries.removeAll { $0.timestamp < cutoff }
103108
lastPrune = now
104109
}
105-
persist()
110+
dirty = true
111+
// Persisting re-encodes the whole buffer and writes it atomically. With detailed logging on that is several
112+
// hundred entries per HTTP request, so the writes are coalesced instead.
113+
if lastPersist.map({ now.timeIntervalSince($0) >= Self.flushInterval }) ?? true { persist(now: now) }
106114
}
107115
}
108116

109-
private func persist() {
117+
/// Writes anything still held in memory. Call before the app goes away or before reading the file back.
118+
public func flush() {
119+
lock.withLock { persist(now: clock.now()) }
120+
}
121+
122+
private func persist(now: Date) {
123+
lastPersist = now
124+
dirty = false
110125
guard let fileURL, let data = try? JSONEncoder().encode(entries) else { return }
111126
try? FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
112127
try? data.write(to: fileURL, options: .atomic)
@@ -123,7 +138,7 @@ public final class LogBuffer: @unchecked Sendable {
123138
public func clear() {
124139
lock.withLock {
125140
entries.removeAll()
126-
persist()
141+
persist(now: clock.now())
127142
}
128143
}
129144

Sources/TokenMenuBarCore/StatusBar/StatusItemModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ public enum StatusItemBuilder {
142142
return StatusItemModel(cells: [], iconTone: tone, showsIcon: true, countdownActive: false)
143143
}
144144
let format = input.effectiveFormat
145-
let template = format.template ?? input.customTemplate
146-
let countdown = format != .miniBars && StatusTemplate.referencesCountdown(template)
145+
let template = StatusTemplate.compile(format.template ?? input.customTemplate)
146+
let countdown = format != .miniBars && template.referencesCountdown
147147
var entries = input.selectedKeys.compactMap { key -> (WindowKey, ProviderSnapshot, QuotaWindow)? in
148148
guard let snapshot = input.snapshots[key.provider], let window = snapshot.window(key.windowID) else { return nil }
149149
return (key, snapshot, window)

Sources/TokenMenuBarCore/StatusBar/StatusTemplate.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,33 @@ public enum StatusTemplate {
7979
case newline
8080
}
8181

82+
/// A template parsed once. The status bar renders the same template for every cell, and again for every tier the
83+
/// adaptive ladder tries, so parsing per render walked the string two dozen times per rebuild.
84+
public struct Compiled: Sendable {
85+
let tokens: [Token]
86+
public let referencesCountdown: Bool
87+
88+
init(_ template: String) {
89+
tokens = StatusTemplate.parse(template)
90+
referencesCountdown = tokens.contains { $0 == .placeholder("reset") }
91+
}
92+
}
93+
94+
public static func compile(_ template: String) -> Compiled {
95+
Compiled(template)
96+
}
97+
8298
public static func referencesCountdown(_ template: String) -> Bool {
83-
parse(template).contains { $0 == .placeholder("reset") }
99+
Compiled(template).referencesCountdown
84100
}
85101

86102
public static func render(_ template: String, context: StatusCellContext) -> [[StatusRun]] {
103+
render(Compiled(template), context: context)
104+
}
105+
106+
public static func render(_ compiled: Compiled, context: StatusCellContext) -> [[StatusRun]] {
87107
var lines: [[StatusRun]] = [[]]
88-
for token in parse(template) {
108+
for token in compiled.tokens {
89109
switch token {
90110
case .newline: lines.append([])
91111
case .text(let text): lines[lines.count - 1].append(StatusRun(text: text, kind: .label))

Sources/TokenMenuBarUI/AppController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ public final class AppController {
228228
for observer in workspaceObservers { NSWorkspace.shared.notificationCenter.removeObserver(observer) }
229229
workspaceObservers.removeAll()
230230
dependencies.log.log("stopped")
231+
dependencies.log.flush()
231232
}
232233

233234
func installObservers() {

Sources/TokenMenuBarUI/Views/Components.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,30 @@ public struct LinkifiedText: View {
7474

7575
public static func attributed(_ text: String) -> AttributedString {
7676
var result = AttributedString(text)
77-
let pattern = try! Regex("https?://[^\\s)]+")
78-
for match in text.matches(of: pattern) {
79-
if let range = Range(match.range, in: result), let url = URL(string: String(text[match.range])) {
77+
// Compiling a Regex costs more than scanning the string, and every banner does this on every body evaluation.
78+
for span in links(in: text) {
79+
if let range = Range(span, in: result), let url = URL(string: String(text[span])) {
8080
result[range].link = url
8181
result[range].underlineStyle = .single
8282
}
8383
}
8484
return result
8585
}
86+
87+
/// The `http://` and `https://` runs in `text`, each ending at the first space or closing bracket.
88+
static func links(in text: String) -> [Range<String.Index>] {
89+
var spans: [Range<String.Index>] = []
90+
var cursor = text.startIndex
91+
while let scheme = text.range(of: "http", range: cursor..<text.endIndex) {
92+
cursor = scheme.upperBound
93+
let rest = text[scheme.upperBound...]
94+
guard rest.hasPrefix("://") || rest.hasPrefix("s://") else { continue }
95+
let end = text[scheme.lowerBound...].firstIndex { $0.isWhitespace || $0 == ")" } ?? text.endIndex
96+
spans.append(scheme.lowerBound..<end)
97+
cursor = end
98+
}
99+
return spans
100+
}
86101
}
87102

88103
public struct ChipView: View {

Sources/TokenMenuBarUI/Views/HoverHelp.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ public final class HoverState {
1111
public init() {}
1212

1313
public func hover(_ active: Bool) {
14+
// `onContinuousHover` reports every mouse-move point, so without these each one cancelled and allocated a task
15+
// for a card that is already showing, or already on its way.
16+
if active, presented || task != nil { return }
17+
if !active, !presented, task == nil { return }
1418
task?.cancel()
1519
guard active else {
20+
task = nil
1621
presented = false
1722
return
1823
}

Sources/TokenMenuBarUI/Views/SettingsTab.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ public struct WindowSelectionList: View {
266266
}
267267

268268
public var body: some View {
269+
// Without the binding, every row recomputed the default selection twice: once for the toggle and once to decide
270+
// whether it is the last one standing.
271+
let selection = selection
272+
let showsLabelField = settings.activeTemplate.contains("{label}")
269273
VStack(alignment: .leading, spacing: 4) {
270274
if rows.isEmpty {
271275
Text("Windows appear after the first successful refresh.").foregroundStyle(.secondary)
@@ -282,7 +286,7 @@ public struct WindowSelectionList: View {
282286
}
283287
.disabled(selection == [row.key])
284288
Text(Format.percent(row.window.usedPercent)).monospacedDigit().foregroundStyle(.secondary)
285-
if settings.activeTemplate.contains("{label}") {
289+
if showsLabelField {
286290
TextField(
287291
"Label", text: Binding(get: { settings.shortLabels[row.key] ?? "" }, set: { setLabel(row.key, $0) })
288292
).frame(width: 56)

Tests/TokenMenuBarUITests/ViewTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ func findScrollView(_ view: NSView) -> NSScrollView? {
258258
#expect(inkFraction(Banner("plain"), width: 300, height: 60) > 0)
259259
let attributed = LinkifiedText.attributed("a https://x.y/z) b")
260260
#expect(attributed.runs.contains { $0.link != nil })
261+
#expect(attributed.runs.compactMap(\.link?.absoluteString) == ["https://x.y/z"])
262+
#expect(LinkifiedText.attributed("no link, http not a scheme").runs.allSatisfy { $0.link == nil })
263+
let both = LinkifiedText.attributed("http://a.b and https://c.d/e")
264+
#expect(both.runs.compactMap(\.link?.absoluteString) == ["http://a.b", "https://c.d/e"])
261265
var copied: [String] = []
262266
var opened: [URL] = []
263267
let chip = ChipView(

0 commit comments

Comments
 (0)