{"title":"Emptying Caches When macOS Reports Memory Pressure","html_url":"https://deallocatedobjects.com/posts/emptying-caches-when-macos-reports-memory-pressure","source_url":"","markdown_content":"`NSCache` evicts entries on its own when memory gets tight, but a plain Swift dictionary keeps everything until you clear it. [Postage](https://postageformac.com/?ref=deallocated-post), my Mac email client, has several dictionaries for things like normalized subjects and cleaned-up message text, so it listens for memory pressure itself:\n\n```swift title=\"SharedEmailStore.swift\"\nlet pressure = DispatchSource.makeMemoryPressureSource(eventMask: [.warning, .critical], queue: .main)\npressure.setEventHandler {\n    MainActor.assumeIsolated { Self.purgeCaches() }\n}\npressure.activate()\nmemoryPressureSource = pressure\n```\n\nKeep that reference in a property. If the source deallocates, your handler stops firing.\n\nThe purge itself is one short function:\n\n```swift\nstatic func purgeCaches() {\n    ThreadFilter.purgeCaches()\n    SubjectInfoCache.purge()\n    HTMLMail.purgeCache()\n    MessageText.purgeCache()\n    URLCache.shared.removeAllCachedResponses()\n}\n```\n\n### What Makes It Safe\n\nPurging at a random moment only works if nothing depends on an entry being there. Each of these caches computes on a miss, so an empty cache costs some recomputing and returns the same answers. Here's the subject cache:\n\n```swift title=\"Email.swift\"\nstatic func info(for subject: String) -> (normalized: String, isReply: Bool) {\n    lock.lock()\n    if let hit = entries[subject] { lock.unlock(); return hit }\n    lock.unlock()\n    let info = (normalized: EmailThread.normalizeSubject(subject).lowercased(),\n                isReply: EmailThread.isReplySubject(subject))\n    lock.lock()\n    if entries.count >= 50_000 { entries.removeAll(keepingCapacity: true) }\n    entries[subject] = info\n    lock.unlock()\n    return info\n}\n```\n\nThe lock matters here because the handler runs on the main queue, while lookups can come from elsewhere.\n\nAnything that can't be rebuilt doesn't belong in this list, like unsaved drafts or state that only lives in memory. Postage has a test, `purgingCachesChangesNoResults`, that checks filtering and threading return the same results after a purge.\n\n### Trying It\n\nmacOS ships a tool that puts the system at a given pressure level, so you don't have to fill your RAM to see the handler run:\n\n```bash\nsudo memory_pressure -S -l warn\n```\n\n`-S` simulates the level, and `-l` takes `warn` or `critical`. The whole system sees it, so other apps will react too. Put a breakpoint in your handler the first time to confirm it fires.","html_content":"<p><code>NSCache</code> evicts entries on its own when memory gets tight, but a plain Swift dictionary keeps everything until you clear it. <a href=\"https://postageformac.com/?ref=deallocated-post\">Postage</a>, my Mac email client, has several dictionaries for things like normalized subjects and cleaned-up message text, so it listens for memory pressure itself:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"file\">SharedEmailStore.swift</span><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> pressure </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> DispatchSource.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">makeMemoryPressureSource</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">eventMask</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: [.warning, .critical], </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">queue</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: .main)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">pressure.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">setEventHandler</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    MainActor.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">assumeIsolated</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">Self</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">purgeCaches</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">() }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">pressure.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">activate</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">memoryPressureSource </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> pressure</span></span></code></pre></figure><p>Keep that reference in a property. If the source deallocates, your handler stops firing.</p>\n<p>The purge itself is one short function:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">static</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> func</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> purgeCaches</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    ThreadFilter.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">purgeCaches</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    SubjectInfoCache.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">purge</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    HTMLMail.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">purgeCache</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    MessageText.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">purgeCache</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    URLCache.shared.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">removeAllCachedResponses</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><h2 id=\"what-makes-it-safe\" data-level=\"3\">What Makes It Safe</h2>\n<p>Purging at a random moment only works if nothing depends on an entry being there. Each of these caches computes on a miss, so an empty cache costs some recomputing and returns the same answers. Here’s the subject cache:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"file\">Email.swift</span><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">static</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> func</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> info</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">for</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> subject: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">String</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">-></span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (normalized: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">String</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, isReply: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">Bool</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    lock.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">lock</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> hit </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> entries[subject] { lock.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">unlock</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(); </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> hit }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    lock.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">unlock</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> info </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">normalized</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: EmailThread.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">normalizeSubject</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(subject).</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">lowercased</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">                isReply</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: EmailThread.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">isReplySubject</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(subject))</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    lock.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">lock</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> entries.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">count</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> >=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 50_000</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { entries.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">removeAll</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">keepingCapacity</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">true</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    entries[subject] </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> info</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    lock.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">unlock</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> info</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><p>The lock matters here because the handler runs on the main queue, while lookups can come from elsewhere.</p>\n<p>Anything that can’t be rebuilt doesn’t belong in this list, like unsaved drafts or state that only lives in memory. Postage has a test, <code>purgingCachesChangesNoResults</code>, that checks filtering and threading return the same results after a purge.</p>\n<h2 id=\"trying-it\" data-level=\"3\">Trying It</h2>\n<p>macOS ships a tool that puts the system at a given pressure level, so you don’t have to fill your RAM to see the handler run:</p>\n<figure class=\"code\" data-lang=\"bash\"><figcaption><span class=\"lang\">Shell</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">sudo</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> memory_pressure</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -S</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> -l</span><span style=\"--shiki-light:#032F62;--shiki-dark:#9ECBFF\"> warn</span></span></code></pre></figure><p><code>-S</code> simulates the level, and <code>-l</code> takes <code>warn</code> or <code>critical</code>. The whole system sees it, so other apps will react too. Put a breakpoint in your handler the first time to confirm it fires.</p>","created_at":"2026-11-11T14:00:00.000Z","updated_at":"2026-11-11T14:00:00.000Z"}