Historical Rolling Trend Charts (Swift Charts Engine) #23

Closed
opened 2026-09-08 10:29:43 +01:00 by gronod · 1 comment
Owner

Resolved in Milestone 4 via commit 7d2dd30 on develop.

  • Implemented thread-safe generic RollingRingBuffer<T> with capacity bounds and oldest sample eviction.
  • Implemented TelemetryHistoryStore to buffer rolling historical samples across CPU load, memory, network I/O, disk I/O, thermals, and power.
  • Implemented TelemetryTrendChartView leveraging Swift Charts (LineMark and AreaMark with gradient fills and live stat tags).
  • Integrated into ContentView.swift under the "Historical Trends" tab.
  • Added unit tests in MMHistoryTests.swift.
Resolved in Milestone 4 via commit `7d2dd30` on `develop`. - Implemented thread-safe generic `RollingRingBuffer<T>` with capacity bounds and oldest sample eviction. - Implemented `TelemetryHistoryStore` to buffer rolling historical samples across CPU load, memory, network I/O, disk I/O, thermals, and power. - Implemented `TelemetryTrendChartView` leveraging Swift Charts (`LineMark` and `AreaMark` with gradient fills and live stat tags). - Integrated into `ContentView.swift` under the "Historical Trends" tab. - Added unit tests in `MMHistoryTests.swift`.
gronod added the Kind/Feature
Priority
Medium
3
Project/AntigravityFeature/UI
labels 2026-09-08 10:29:43 +01:00
Author
Owner

Technical Implementation Plan & Code Solution

1. Rolling Time-Series Data Architecture

Visualizing 60-second to 1-hour timelines without continuous heap allocation requires a high-performance circular ring buffer:

  • Constant time O(1) appending.
  • Zero memory reallocations during telemetry loops.
  • Swift Charts integration using LineMark with gradient AreaMark.

2. Ring Buffer Implementation (RingBuffer.swift)

import Foundation

public struct TimeSeriesSample: Identifiable, Sendable {
    public let id = UUID()
    public let timestamp: Date
    public let value: Double
}

public final class RingBuffer<T: Sendable>: @unchecked Sendable {
    private var buffer: [T?]
    private var writeIndex = 0
    private var count = 0
    private let capacity: Int
    private let lock = NSLock()

    public init(capacity: Int) {
        self.capacity = max(capacity, 10)
        self.buffer = Array(repeating: nil, count: self.capacity)
    }

    public func append(_ element: T) {
        lock.lock()
        defer { lock.unlock() }

        buffer[writeIndex] = element
        writeIndex = (writeIndex + 1) % capacity
        if count < capacity {
            count += 1
        }
    }

    public func allElements() -> [T] {
        lock.lock()
        defer { lock.unlock() }

        if count < capacity {
            return buffer[0..<count].compactMap { $0 }
        }

        var result = [T]()
        result.reserveCapacity(capacity)
        for i in 0..<capacity {
            let idx = (writeIndex + i) % capacity
            if let val = buffer[idx] {
                result.append(val)
            }
        }
        return result
    }
}

3. Swift Charts Component (TelemetryTrendChart.swift)

import SwiftUI
import Charts

public struct TelemetryTrendChart: View {
    public let title: String
    public let samples: [TimeSeriesSample]
    public let unit: String
    public let color: Color
    public let yMax: Double?

    public var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.headline)

            Chart(samples) { sample in
                AreaMark(
                    x: .value("Time", sample.timestamp),
                    y: .value("Value", sample.value)
                )
                .foregroundStyle(
                    LinearGradient(
                        colors: [color.opacity(0.4), color.opacity(0.0)],
                        startPoint: .top,
                        endPoint: .bottom
                    )
                )

                LineMark(
                    x: .value("Time", sample.timestamp),
                    y: .value("Value", sample.value)
                )
                .foregroundStyle(color)
                .lineStyle(StrokeStyle(lineWidth: 2))
            }
            .chartYScale(domain: 0...(yMax ?? (samples.map(\.value).max() ?? 100) * 1.1))
            .chartXAxis {
                AxisMarks(values: .automatic(desiredCount: 5)) { _ in
                    AxisGridLine()
                    AxisTick()
                    AxisValueLabel(format: .dateTime.minute().second())
                }
            }
            .frame(height: 120)
        }
        .padding()
        .background(RoundedRectangle(cornerRadius: 10).fill(.background.secondary))
    }
}

4. GPU Rendering Performance

By rendering immutable arrays extracted from the ring buffer on @MainActor, Swift Charts utilizes Metal-backed rasterization without blocking background telemetry sampling threads.

## Technical Implementation Plan & Code Solution ### 1. Rolling Time-Series Data Architecture Visualizing 60-second to 1-hour timelines without continuous heap allocation requires a high-performance circular ring buffer: - Constant time `O(1)` appending. - Zero memory reallocations during telemetry loops. - Swift Charts integration using `LineMark` with gradient `AreaMark`. --- ### 2. Ring Buffer Implementation (`RingBuffer.swift`) ```swift import Foundation public struct TimeSeriesSample: Identifiable, Sendable { public let id = UUID() public let timestamp: Date public let value: Double } public final class RingBuffer<T: Sendable>: @unchecked Sendable { private var buffer: [T?] private var writeIndex = 0 private var count = 0 private let capacity: Int private let lock = NSLock() public init(capacity: Int) { self.capacity = max(capacity, 10) self.buffer = Array(repeating: nil, count: self.capacity) } public func append(_ element: T) { lock.lock() defer { lock.unlock() } buffer[writeIndex] = element writeIndex = (writeIndex + 1) % capacity if count < capacity { count += 1 } } public func allElements() -> [T] { lock.lock() defer { lock.unlock() } if count < capacity { return buffer[0..<count].compactMap { $0 } } var result = [T]() result.reserveCapacity(capacity) for i in 0..<capacity { let idx = (writeIndex + i) % capacity if let val = buffer[idx] { result.append(val) } } return result } } ``` --- ### 3. Swift Charts Component (`TelemetryTrendChart.swift`) ```swift import SwiftUI import Charts public struct TelemetryTrendChart: View { public let title: String public let samples: [TimeSeriesSample] public let unit: String public let color: Color public let yMax: Double? public var body: some View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.headline) Chart(samples) { sample in AreaMark( x: .value("Time", sample.timestamp), y: .value("Value", sample.value) ) .foregroundStyle( LinearGradient( colors: [color.opacity(0.4), color.opacity(0.0)], startPoint: .top, endPoint: .bottom ) ) LineMark( x: .value("Time", sample.timestamp), y: .value("Value", sample.value) ) .foregroundStyle(color) .lineStyle(StrokeStyle(lineWidth: 2)) } .chartYScale(domain: 0...(yMax ?? (samples.map(\.value).max() ?? 100) * 1.1)) .chartXAxis { AxisMarks(values: .automatic(desiredCount: 5)) { _ in AxisGridLine() AxisTick() AxisValueLabel(format: .dateTime.minute().second()) } } .frame(height: 120) } .padding() .background(RoundedRectangle(cornerRadius: 10).fill(.background.secondary)) } } ``` --- ### 4. GPU Rendering Performance By rendering immutable arrays extracted from the ring buffer on `@MainActor`, Swift Charts utilizes Metal-backed rasterization without blocking background telemetry sampling threads.
gronod added this to the M4: Presentation, Visuals & Menu Bar Integration milestone 2026-09-08 10:42:50 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#23