From 00ffd7970ebaa4763600747ed695997ef5b84fb3 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Fri, 10 Apr 2026 16:25:49 -0700 Subject: [PATCH] Performance Improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — VisualizerStorageManager.swift VisFrameBuffer is a flat ContiguousArray with frameCount and pointsPerFrame. All frame data for a track lives in one contiguous allocation rather than a [[Float]] array-of-arrays. loadCache now uses Data(contentsOf:options:.alwaysMapped) — the OS maps the file into virtual memory and faults pages in on demand rather than reading the whole file into heap. copyFrame(at:into:) copies a frame slice directly into _audioLevels using initialize(from:) — no intermediate [Float] created. Phase 2 — MitsuhaVisualizerView.swift + AudioPlayer.swift VisualizerLevelBox now pre-allocates targetLevels, displayLevels, idleLevels, and the full 16-slot history ring on first use via resizeIfNeeded. This only triggers when numberOfPoints changes (rare — settings slider). updateDisplayLevels writes directly into box.targetLevels throughout — no var targetLevels = [Float](), no map, no append. The history ring buffer now copies in-place into pre-allocated slots, eliminating the COW trigger on every frame. drawIdleState uses box.idleLevels — no Array(repeating:). The Canvas body no longer falls back to Array(repeating:) since displayLevels is always pre-allocated. The timer in AudioPlayer now calls buf.copyFrame(at:into:&_audioLevels) directly — no intermediate [Float] copy. Phase 3 — View invalidation + drawBars fix fillColors hoisted out of the drawBars per-bar loop — it was allocating a new [Color] array count times per frame (8–24 allocations per draw call at 60fps). Now computed once before the loop. @ObservedObject var settings and @StateObject private var box are correct — box has zero @Published properties so it never triggers parent redraws. The Canvas closure only captures tickDate which changes every tick, ensuring per-tick re-execution without touching SwiftUI’s diffing engine. --- .../Visualizer/MitsuhaVisualizerView.swift | 202 ++++++++++-------- .../Visualizer/VisualizerStorageManager.swift | 111 ++++++---- 2 files changed, 185 insertions(+), 128 deletions(-) diff --git a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift index 7259c72..36648cb 100644 --- a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift +++ b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift @@ -143,13 +143,34 @@ final class WaveStateCache { // MARK: - Level Box /// Per-instance mutable render state. Zero @Published properties → no observation warnings. +/// All working buffers pre-allocated here so the 60fps render loop does zero heap allocation. fileprivate final class VisualizerLevelBox: ObservableObject { - var displayLevels: [Float] = [] - var peakFollower: Float = 0.01 + var displayLevels: [Float] = [] + var targetLevels: [Float] = [] // scratch buffer — pre-allocated, mutated in-place each frame + var idleLevels: [Float] = [] // flat idle wave — pre-allocated, updated only when count changes + var peakFollower: Float = 0.01 var levelHistoryBuf: [[Float]] = [] var historyWriteIdx: Int = 0 var wobblePhaseOffset: Double = 0 - var lastTickTime: CFTimeInterval = 0 + var lastTickTime: CFTimeInterval = 0 + + /// Resize all per-frame buffers when point count changes (rare — settings slider). + func resizeIfNeeded(count: Int, idleAmplitude: Float) { + guard count > 0 else { return } + if targetLevels.count != count { + targetLevels = [Float](repeating: 0, count: count) + displayLevels = [Float](repeating: idleAmplitude, count: count) + idleLevels = [Float](repeating: idleAmplitude, count: count) + // Pre-fill history ring with idle so depth ghost doesn't start blank + levelHistoryBuf = [[Float]]( + repeating: [Float](repeating: idleAmplitude, count: count), + count: MitsuhaVisualizerView.historySize + ) + historyWriteIdx = 0 + } else if idleLevels.first != idleAmplitude { + for i in 0..