Skip to content

perf: share preprocessing for async palette graphics - #25

Open
2dubu wants to merge 1 commit into
mainfrom
perf/share-extraction-preprocessing
Open

2dubu wants to merge 1 commit into
mainfrom
perf/share-extraction-preprocessing

Conversation

@2dubu

@2dubu 2dubu commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Summary

Reuse decoding, sampling, and forward OKLCH conversion when async palette graphics request both a palette and semantic swatches with the automatic or CPU quantizer.

Background

A cache miss in AsyncPaletteGraphicLoader currently extracts a palette and then calls swatches, which repeats the image pipeline with at least 16 colors. With the default 10-color palette, the two quantizations are necessary to preserve their different results, but preprocessing the same source twice is redundant.

Solution

Add an internal paired extraction path with request-local prepared pixels. It preserves both quantizations at K and max(K, 16), including when K is already 16 or greater. The loader uses the pair and retains its cancellation check before caching or publishing success.

Custom and Metal quantizers keep the existing two-call path. Public API signatures, filtering, fallback, source color-space selection, and the first palette's timing boundary are preserved. A second extraction failure still produces a successful palette with nil swatches. The internal cancellation method returns the canceled task so tests can await completion deterministically.

Related Issue

N/A.

Verification

  • Added ordered palette RGB/population/proportion and role-by-role swatch equivalence checks over K=2/10/16/32, all output color spaces, sRGB/P3 sources, and automatic/CPU quantizers. Also cover encoded input, resizing/sampling/filter options, fallback, invalid options, pre-cancellation, custom call order, and second-call failure.
  • Added UIKit loader tests for failure/cache behavior and cancellation during a suspended second custom quantization. The optimized CPU path's cancellation boundaries are additionally source-reviewed; the suspension test intentionally uses a controllable custom quantizer.
  • Commands run:
    • swift package resolve
    • swift build -c debug
    • swift test --filter PaletteKitTests --filter PaletteKitInsightsTests — 67 tests passed.
    • swift test -c release --filter SharedExtractionTests — 8 tests passed.
    • xcodebuild test -scheme PaletteKit-Package -only-testing:PaletteKitTests -only-testing:PaletteKitInsightsTests -destination 'id=5F0334CA-5AD2-47FE-B837-F802AFFB20FD' — 164 tests passed on iOS 26.5 Simulator (155 core and 9 Insights).
    • Release SharedExtractionBenchmarks/testSeparatePaletteAndSwatches and testSharedPaletteAndSwatches, three process runs each with order alternated.
    • git diff --check
  • An additional temporary integration of all three independent PRs passed 71 macOS tests and 168 iOS Simulator tests.
  • Environment: Apple M5 Max, macOS 26.6.2, Swift 6.3.2. iOS compilation reports existing existential/CIContext warnings in unchanged renderer files and generated App Intents metadata warnings; they are outside this change.

Compatibility and Impact

  • Public API: None.
  • Behavioral impact: The paired CPU path reuses one source's preprocessing while preserving the palette and richer swatch results. Independent public calls and custom/Metal extraction retain their existing selection and error semantics.
  • Performance impact: Removes duplicate preprocessing in the paired CPU path. Prepared buffers remain alive through the pair; peak memory and iPhone performance have not been measured. No memory-reduction claim is made.
  • Affected platforms: Shared extraction logic on iOS/macOS; the async graphic loader is UIKit-backed.

Additional Context

Release benchmark: a reused 1024×1024 deterministic noise CGImage, K=10, OKLCH, CPU, default downsampling. The same full output checksum consumes both results; input generation is outside measurement. Fifteen clock samples per path from three process runs:

Palette plus swatches Median
Separate extraction 15.955 ms
Shared preprocessing 8.555 ms

This is 46.4% lower latency for this Mac fixture, not an iPhone or all-image improvement claim. Both paths are measured from this branch; a separate base-versus-head standalone API control uses three JPEG fixtures, gradient, noise, and transparent fallback, with collectTimings: true and 45 samples per input. Ordered output signatures match, and median changes range from -0.5% to +1.1%; these measurements do not establish zero overhead or peak-memory equivalence.

The benchmark target also provides testStandalonePalette. This branch is based directly on main and is independent of the LUT and MMCQ cache PRs.

Checklist

  • This pull request is focused and contains no unrelated changes.
  • Tests were added or updated where behavior changed, or I explained why they are not needed.
  • Regression tests cover the intended behavior or root cause rather than only a reported sample.
  • Public API changes are documented and reflected in the changelog, or this pull request has none.
  • I considered source compatibility, behavioral changes, and performance impact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant