Performance Optimization Tips for Palmier Pro: 5 Architecture Patterns from the Source Code
Palmier Pro achieves buttery-smooth video editing on macOS by combining disk-backed asset caching, semaphore-gated concurrency, and zero-allocation drawing loops—patterns you can replicate by following the MediaVisualCache, AsyncSemaphore, and TimelineView implementations in the Swift codebase.
Palmier Pro is a native macOS video editor built with Swift 6.2, SwiftUI, AppKit, and AVFoundation. Keeping the timeline responsive during heavy multimedia workloads requires strict architectural discipline to avoid blocking the main thread. These performance optimization tips for Palmier Pro are extracted directly from the palmier-io/palmier-pro repository, showing exactly how the codebase minimizes CPU and memory pressure through deterministic caching and throttled background work.
Cache-First Resource Handling
The foundation of Palmier Pro’s speed is its refusal to recompute expensive assets. Instead of generating waveforms, thumbnails, or encoded images on every UI frame, the app stores them in ~/Library/Caches/PalmierPro and references them via lightweight in-memory dictionaries.
Cache Audio Waveforms on Disk
When MediaVisualCache.generateWaveform(for:) receives an audio asset, it first checks the waveformSamples dictionary and waveformInFlight set to prevent duplicate work. If the waveform is absent, it computes a disk cache key using file size and modification time (lines 209–216 in Sources/PalmierPro/Timeline/MediaVisualCache.swift), then attempts to load a cached .waveform file via loadWaveform(key:). Only when the disk cache misses does it invoke WaveformAnalyzer to compute fresh samples.
let cacheKey = diskCacheKey(for: url)
if let cacheKey, let cached = loadWaveform(key: cacheKey) {
return cached
}
// … compute and then saveWaveform(samples, key: cacheKey)
Performance tip: Always include file metadata (size and mtime) in your cache key logic so that edited source files automatically invalidate stale entries.
Generate Video Thumbnails Once
The generateVideoThumbnails(for:) method in MediaVisualCache.swift (lines 364–395) implements a sprite-sheet strategy. It first attempts loadThumbnails(key:) to retrieve an existing JPEG grid; if none exists, it builds a new sprite sheet and JSON sidecar describing tile layouts. This sprite is created once per asset and reused for every subsequent draw operation, eliminating repeated AVAssetImageGenerator calls during timeline scrolling.
Encode Images with Dual-Tier Caching
ImageEncoder.encode(url:) (found in Sources/PalmierPro/Utilities/ImageEncoder.swift) implements a pass-through check (lines 70–78) that returns the original file if it already fits the 1568-pixel longest-edge and 3.5 MiI byte budget. When downsampling is required, it JPEG-compresses with progressively lower quality until the size constraint is met, then stores the result in an LRU-style dictionary (maxCacheEntries = 32). This prevents redundant encoding of frequently accessed still images.
Use the Centralized DiskCache Utility
All persistent caches route through DiskCache (Sources/PalmierPro/Utilities/DiskCache.swift), which provides size reporting (bytes(at:)) and bulk clearing (clear()). The settings UI surfaces total cache sizes via StoragePane.swift (lines 140–141), allowing users to reclaim space without deleting active project files.
Throttle Background Work with AsyncSemaphore
Unbounded concurrency destroys responsiveness. Palmier Pro caps simultaneous background tasks using AsyncSemaphore, a thin wrapper around DispatchSemaphore located in Sources/PalmierPro/Utilities/AsyncSemaphore.swift.
The MediaVisualCache class instantiates two gates:
waveformGate– initialized with value2(line 16), allowing at most two concurrent waveform analysesimageThumbnailGate– initialized with value4(line 27), limiting concurrent image thumbnail generation
Each heavy operation begins with await waveformGate.wait() and ends with waveformGate.signal(), ensuring that CPU-intensive work never starves the system or blocks AVFoundation’s render pipeline.
Performance tip: When adding new background pipelines (e.g., neural-network pre-filtering), create a dedicated semaphore sized to the device’s core count, and launch work via Task.detached(priority: .utility) to match urgency to system load.
private static let exportGate = AsyncSemaphore(value: 3)
func exportProject(_ project: MediaProject) async throws -> URL {
try await Self.exportGate.wait()
defer { Task { await Self.exportGate.signal() } }
// Heavy export work using AVAssetExportSession
return try await performExport(project)
}
Eliminate Per-Frame Allocations in the Render Loop
The TimelineView (Sources/PalmierPro/Timeline/TimelineView.swift) draws directly into an NSView canvas. A comment on line 68 explicitly warns: “Cached for draw performance — avoid per‑frame allocations.” The view stores reusable resources—such as static trackBg colors and pre-computed TimelineGeometry—in static or class-level containers. The draw loop references these cached assets from EditorViewModel.mediaVisualCache rather than instantiating temporary objects, preventing GC-style pauses that would degrade smooth scrolling.
Performance tip: When extending UI components (e.g., adding a ruler overlay), store fonts, colors, and gradients statically. Pre-compute any CGContext transforms outside the hot draw path.
Compress Large Video Assets on Import
Memory pressure from oversized reference footage is mitigated by VideoCompressor.compressIfNeeded(url:maxLongSide:) in Sources/PalmierPro/Generation/VideoCompressor.swift. When importing video exceeding the default 1100-pixel longest edge, the function asynchronously transcodes it to a 960 × 540 preset. This reduces memory footprint and accelerates subsequent thumbnail generation.
The function runs asynchronously (async throws) and logs lifecycle events via Log.generation.notice, allowing the UI to remain responsive during the compression pass.
Optimize Drag-and-Drop with Native AppKit
SwiftUI’s .onDrop modifier can shadow nested drop targets on macOS 26, causing expensive layout thrashing. Palmier Pro avoids this by using the native AppKit method registerForDraggedTypes inside TimelineView. This lightweight approach computes only the drop target and snap state (see applyExternalSnap(at:geo:)), deferring heavy asset loading to background tasks and preserving frame rate during drag operations.
Summary
- Cache first, compute once: Store waveforms, thumbnails, and encoded images in
DiskCachewith deterministic keys based on file metadata. - Gate concurrency: Use
AsyncSemaphoreto limit simultaneous background tasks—two for waveforms, four for image thumbnails—to prevent system starvation. - Zero-allocation drawing: Keep the render loop free of object creation by storing reusable assets in static containers within
TimelineView. - Pre-compress imports: Automatically down-scale large reference videos to 960 × 540 via
VideoCompressorto save memory and I/O. - Native drag handling: Prefer AppKit’s
registerForDraggedTypesover SwiftUI drop modifiers to avoid layout thrashing during drag-and-drop.
Frequently Asked Questions
How does Palmier Pro keep the timeline smooth while generating audio waveforms?
Palmier Pro uses MediaVisualCache to store computed waveforms in ~/Library/Caches/PalmierPro and references them via the waveformSamples dictionary. The waveformGate semaphore limits concurrent analysis to two tasks, ensuring the CPU remains available for UI interaction and playback according to the source code in Sources/PalmierPro/Timeline/MediaVisualCache.swift.
What is the maximum cache size for encoded images in Palmier Pro?
The ImageEncoder class maintains an in-memory LRU cache with a hard limit of 32 entries (maxCacheEntries = 32). Images that exceed the 1568-pixel longest edge or 3.5 MiB size limit are down-sampled and JPEG-compressed before storage, while smaller files are passed through without modification.
Why does Palmier Pro use semaphores instead of unstructured tasks for background work?
The codebase uses AsyncSemaphore (a wrapper around DispatchSemaphore) to cap simultaneous background operations. This prevents the “thundering herd” problem where too many concurrent Task.detached calls would exhaust CPU cores or AVFoundation resources, causing frame drops during timeline scrolling or video playback.
How can I extend Palmier Pro with a custom asset cache?
Follow the pattern in MediaVisualCache.swift: compute a deterministic cache key using file path, size, and modification time; check DiskCache for an existing file; and only generate the asset if the cache misses. Store the result back to disk using the cache’s directory URL, then reference it via a static cache instance to ensure thread-safe reuse across views.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →