uttrflow / uttrflow/uttrflow-swift
Opening the clipboard panel reads every picture file whole just to check that it still exists
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
On every ⇧⌘V, `toggleQuickPanel` asks which picture clips have lost their file (`Sources/Uttrflow/AppDelegate.swift:773`). The helper's comment promises "one `stat` each":
```swift
// Sources/Uttrflow/AppDelegate.swift:800-808
/// B8 — the picture clips whose files are gone, at one `stat` each and none for text.
private func missingPictures(among clips: [Clip]) async -> Set {
...
if await clipboard.imageData(for: image) == nil { missing.insert(clip.id) }
```
But `ClipboardStore.imageData(for:)` is `try? Data(contentsOf:)` (`Sources/UttrflowClipboard/ClipboardStore.swift:220-223`). It reads the whole PNG into memory, only to compare the result with `nil`. This happens for every picture in history, sequentially, before the panel is shown.
## Measured
Throwaway Release harness against a real `ClipboardStore` in a temporary folder. 150 picture clips of 1.5 MB each (a Retina screenshot is often larger):
| | time per panel open | bytes read |
|---|---|---|
| today (`imageData`) | 16 ms with a warm cache | 225 MB |
| `FileManager.fileExists` for each | 0.6 ms | 0 |
With a cold cache, for example after the Mac was under memory pressure, those 225 MB are real disk reads. They also push other pages out of memory on an 8 GB machine, on the path the user is waiting on. Pictures are kept for 7 days, with up to 500 records and a 1 GB disk budget (`ClipboardBudget.standard`).
## Acceptance criteria
- Checking for a missing picture does not read the picture: for example a `ClipboardStore.hasImage(for:)` that checks existence.
- `imageData(for:)` is used only where the bytes are needed (the paste).
- A test in `Tests/UttrflowClipboardTests/ClipImageTests.swift` shows the new check answers true for a kept picture and false after its file is deleted.
- The comment on `missingPictures` is true.
## Where to start
- `Sources/UttrflowClipboard/ClipboardStore.swift:219-223`, and the call at `Sources/Uttrflow/AppDelegate.swift:805`
- Test to extend: `Tests/UttrflowClipboardTests/ClipImageTests.swift`
- Before pushing, run `make verify` (export `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` first). It is the same command CI runs, and it enforces the 95% coverage floor per module.
- Read [CONTRIBUTING.md](https://github.com/uttrflow/uttrflow-swift/blob/main/CONTRIBUTING.md) first, and say on this issue that you are taking it.
**Size:** XS, about 1 hour.
Contributor guide
Assessment
This issue has not been assessed yet.