kikuta@dev:~$cat ./notes/clipboard-polling-benchmark.md
·8 min·kikuta

NSPasteboard.changeCount監視のポーリング間隔を実測する

20〜250ms間隔のsynthetic writeに対し25/50/100ms pollingを比較し、取りこぼしとidle CPUを測定した結果。

changeCountは差分検知であって履歴ではない

macOSのNSPasteboard.generalはchangeCountを持つため、前回値と比較すればclipboardが変更されたことを検知できます。ただしchangeCountが2以上進んでいても、中間payloadは保持されません。監視側が読む前に次のcopyで上書きされれば、その内容は復元不能です。

したがってpolling intervalの設計では、CPU負荷だけでなく「どの程度の連続copyを観測できるか」を測る必要があります。今回はgeneral pasteboardを汚さないようprivate named NSPasteboardを使い、synthetic writerとwatcherを分離して計測しました。

100ms pollingでは50ms書き込みを約半分落とした

100ms pollingに対して20ms間隔で50回writeした条件ではpersistable snapshotが13、lost payloadが37でした。50ms間隔で40回writeした条件でも22件取得、18件lossでした。100ms間隔writeでは30件中29件、250ms間隔writeでは20件中20件を取得できました。

pollerが遅い場合、changeCountのjump自体は観測できます。しかし「何件失ったか」は分かっても、そのpayloadを回収するAPIはありません。履歴DBを高速化しても解決せず、観測頻度そのものが上限になります。

50ms pollingでは100ms以上のwriteを取りこぼさなかった

50ms pollingでは20ms writeが24/50、50ms writeが39/40、100ms writeが30/30、250ms writeが20/20でした。さらに25ms pollingでは50ms writeを40/40取得しました。

この結果だけから25ms固定を採用するのではなく、実利用で必要なburst耐性と消費電力を見て決めるべきです。今回の実装では50msを候補とし、毎回payload全体を読むのではなくchangeCountだけを安価に確認する構成にしました。

50ms idle pollingのCPU実測

3秒間のidle runで、50msごとにchangeCountを確認し、変化時だけsnapshot extractionを行うwatcherはuser CPU約0.01秒、system CPU約0.00秒でした。短時間の単一環境計測なので一般化はできませんが、少なくともpolling frequencyとpayload extraction costを分離する効果は確認できました。

監視ループ内でUTType列挙、hash、永続化、metadata解析まで毎回実行すると、この結果にはなりません。hot pathはchangeCount比較だけにし、重い処理をchange時へ限定することが前提です。

Universal Clipboard由来情報は補助metadataに留める

実機を含む観測ではcom.apple.is-remote-clipboardが見えるケースがありましたが、公開API契約として依存できる情報ではありません。またiPhone側source applicationのbundle IDはpayloadから取得できませんでした。

そのためcapture triggerはremote markerではなく、すべてのsupported pasteboard changeです。remote markerは取得できたときだけoriginDeviceEvidenceを補強するmetadataとして扱い、消えても履歴保存自体が壊れない構成にしています。