perf: optim hangs from DebugMenu
This commit is contained in:
1 parent
698e5e8e0c
commit
4b830e6ba7
1 file changed
+93
-53
+93
-53
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user