perf: optim hangs from DebugMenu

This commit is contained in:
lwouis committed 2026-02-08 10:30:48 +01:00
1 parent 698e5e8e0c
commit 4b830e6ba7
1 file changed
+93 -53
+93 -53
View File
@@ -1,4 +1,5 @@
import Cocoa
import CoreText
final class DebugMenu: NSPanel {
static let width = CGFloat(400)
@@ -54,61 +55,66 @@ final class DebugMenu: NSPanel {
private final class QueueGraphView: NSView {
private var history: [(timestamp: Date, data: [String: Double])] = []
private var latestCounts: [String: Int] = [:]
private var orderedQueues: [String] = []
private var colors: [NSColor] = []
private var axisYLines: [CTLine] = []
private var axisYValues: [Int] = []
private var legendLines: [CTLine] = []
private var cachedMaxY = 1.0
private let maxDuration: TimeInterval = 60
private let padding: CGFloat = 20
private let textFont = NSFont.systemFont(ofSize: 8)
private lazy var axisTextAttributes: [NSAttributedString.Key: Any] = [.font: textFont, .foregroundColor: NSColor.white]
private lazy var legendTextAttributes: [NSAttributedString.Key: Any] = [.font: textFont, .foregroundColor: NSColor.white]
private lazy var axisXLines: [CTLine] = (0...3).map { makeLine("\(Int(Double($0) * 20))s", attributes: axisTextAttributes) }
private let legendRowHeight: CGFloat = 15
func reset() { history.removeAll(); needsDisplay = true }
func reset() {
history.removeAll()
latestCounts.removeAll()
orderedQueues.removeAll()
colors.removeAll()
axisYLines.removeAll()
axisYValues.removeAll()
legendLines.removeAll()
cachedMaxY = 1.0
needsDisplay = true
}
func addData(_ data: [String: Double]) {
let now = Date()
history.append((now, data))
history = history.filter { now.timeIntervalSince($0.timestamp) <= maxDuration }
trimHistory(now)
updateCaches(data)
needsDisplay = true
}
override func draw(_ dirtyRect: NSRect) {
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
ctx.clear(bounds)
ctx.textMatrix = .identity
drawAxes(in: ctx)
drawCurves(in: ctx)
drawLegend(in: ctx)
}
private func drawAxes(in ctx: CGContext) {
updateAxisLines()
let width = bounds.width - 2*padding
let height = DebugMenu.height - 2*padding
let maxY = (history.flatMap { $0.data.values }.max() ?? 1) * 1.1
ctx.setStrokeColor(NSColor.white.cgColor)
ctx.setLineWidth(1)
// Y grid
for i in 0...5 {
for i in 0..<axisYLines.count {
let y = bounds.height - DebugMenu.height + padding + CGFloat(i)/5 * height
// if i == 0 {
// ctx.move(to: CGPoint(x: padding, y: y))
// ctx.addLine(to: CGPoint(x: bounds.width - padding, y: y))
// ctx.strokePath()
// }
let val = Int(Double(i)/5 * maxY)
NSString(string: "\(val)").draw(at: CGPoint(x: DebugMenu.width - padding + 4, y: y - 6),
withAttributes: [.font: NSFont.systemFont(ofSize: 8),
.foregroundColor: NSColor.white])
ctx.textPosition = CGPoint(x: DebugMenu.width - padding + 4, y: y - 6)
CTLineDraw(axisYLines[i], ctx)
}
// X grid reversed
for i in 0...3 {
for i in 0..<axisXLines.count {
let x = bounds.width - padding - CGFloat(i)/3 * width
let yBase = bounds.height - DebugMenu.height + padding
// if i == 0 {
// ctx.move(to: CGPoint(x: x, y: yBase))
// ctx.addLine(to: CGPoint(x: x, y: yBase + height))
// ctx.strokePath()
// }
let sec = Int(Double(i) * 20)
NSString(string: "\(sec)s").draw(at: CGPoint(x: x - 10, y: yBase - 12),
withAttributes: [.font: NSFont.systemFont(ofSize: 8),
.foregroundColor: NSColor.white])
ctx.textPosition = CGPoint(x: x - 10, y: yBase - 12)
CTLineDraw(axisXLines[i], ctx)
}
}
@@ -117,12 +123,9 @@ private final class QueueGraphView: NSView {
let width = bounds.width - 2*padding
let height = DebugMenu.height - 2*padding
let now = Date()
let maxY = max(1.0, (history.flatMap { $0.data.values }.max() ?? 1)) * 1.1
let ordered = orderedQueues()
let colors = distinctColors(count: ordered.count)
for (i, name) in ordered.enumerated() {
let maxY = max(1.0, cachedMaxY)
for (i, name) in orderedQueues.enumerated() {
guard i < colors.count else { break }
ctx.setStrokeColor(colors[i].cgColor)
ctx.setLineWidth(1)
var first = true
@@ -140,40 +143,77 @@ private final class QueueGraphView: NSView {
}
private func drawLegend(in ctx: CGContext) {
guard let latest = history.last?.data else { return }
let ordered = orderedQueues()
let colors = distinctColors(count: ordered.count)
let rowHeight: CGFloat = 15
let rowsPerColumn = Int(floor((DebugMenu.height - 2*padding) / rowHeight))
guard !orderedQueues.isEmpty, orderedQueues.count == legendLines.count else { return }
let rowsPerColumn = Int(floor((DebugMenu.height - 2*padding) / legendRowHeight))
let columnWidth: CGFloat = (bounds.width - 2*padding) /
CGFloat(max(1, (ordered.count + rowsPerColumn - 1) / rowsPerColumn))
CGFloat(max(1, (orderedQueues.count + rowsPerColumn - 1) / rowsPerColumn))
let legendBottom = padding
for (i, name) in ordered.enumerated() {
for (i, _) in orderedQueues.enumerated() {
let col = CGFloat(i / rowsPerColumn)
let row = CGFloat(i % rowsPerColumn)
let x = padding / 2 + col * columnWidth
let y = legendBottom + (DebugMenu.height - 2*padding) - rowHeight*(row + 1)
let y = legendBottom + (DebugMenu.height - 2*padding) - legendRowHeight*(row + 1)
let color = colors[i]
let count = Int(latest[name] ?? 0)
ctx.setFillColor(color.cgColor)
ctx.fill(CGRect(x: x, y: y, width: 10, height: 10))
NSString(string: "\(name): \(count)").draw(at: CGPoint(x: x + 15, y: y - 2),
withAttributes: [.font: NSFont.systemFont(ofSize: 8),
.foregroundColor: NSColor.white])
ctx.textPosition = CGPoint(x: x + 15, y: y - 2)
CTLineDraw(legendLines[i], ctx)
}
}
private func orderedQueues() -> [String] {
guard let keys = history.last?.data.keys else { return [] }
return keys.sorted()
}
private func distinctColors(count: Int) -> [NSColor] {
guard count > 0 else { return [] }
return (0..<count).map {
NSColor(hue: CGFloat($0)/CGFloat(count), saturation: 0.75, brightness: 0.95, alpha: 1)
}
}
private func trimHistory(_ now: Date) {
let cutoff = now.addingTimeInterval(-maxDuration)
history.removeAll { $0.timestamp < cutoff }
}
private func updateCaches(_ data: [String: Double]) {
updateMaxY()
updateQueues(data)
updateLegend(data)
}
private func updateMaxY() {
var maxY = 1.0
for entry in history {
for value in entry.data.values where value > maxY { maxY = value }
}
let next = maxY * 1.1
if next == cachedMaxY { return }
cachedMaxY = next
axisYValues.removeAll()
}
private func updateQueues(_ data: [String: Double]) {
let next = data.keys.sorted()
if next == orderedQueues { return }
orderedQueues = next
colors = distinctColors(count: orderedQueues.count)
legendLines.removeAll()
}
private func updateLegend(_ data: [String: Double]) {
var counts: [String: Int] = [:]
for (key, value) in data { counts[key] = Int(value) }
if counts == latestCounts, !legendLines.isEmpty { return }
latestCounts = counts
legendLines = orderedQueues.map { makeLine("\($0): \(latestCounts[$0] ?? 0)", attributes: legendTextAttributes) }
}
private func updateAxisLines() {
let values = (0...5).map { Int(Double($0)/5 * cachedMaxY) }
if values == axisYValues, !axisYLines.isEmpty { return }
axisYValues = values
axisYLines = values.map { makeLine("\($0)", attributes: axisTextAttributes) }
}
private func makeLine(_ text: String, attributes: [NSAttributedString.Key: Any]) -> CTLine {
CTLineCreateWithAttributedString(NSAttributedString(string: text, attributes: attributes))
}
}