# Navigation Framework Tests — WebKit Web Content Process Cleanup tags: #webkit #testing #navigation #webcontentprocess date: 2026-07-10 --- ## Context Investigating how the WebKit test suite handles web content process lifetime between tests, specifically to prevent processes from piling up in memory and causing test runner hangs. --- ## How WebKit Tests Handle Web Content Process Lifetime ### The dominant approach: reuse, not kill LayoutTests (via `WebKitTestRunner`) **deliberately reuse the same web content process** across tests for speed. Between tests, `resetStateToConsistentValues()` performs a soft reset: 1. Navigate to `about:blank` 2. Call `WKPageResetStateBetweenTests` (soft in-process reset — process stays alive) 3. Clear back/forward list and cache 4. Only fall back to `WKPageTerminate` if loading `about:blank` fails If the process becomes fully unresponsive, `TestInvocation.cpp` terminates it and calls `reattachPageToWebProcess()` to get a fresh one: ```cpp if (TestController::singleton().resetStateToConsistentValues(...)) return; // The process is unresponsive, so let's start a new one. TestController::singleton().terminateWebContentProcess(); TestController::singleton().reattachPageToWebProcess(); ``` --- ## Explicit Termination APIs | API | Granularity | Notes | |-----|------------|-------| | `_killWebContentProcessAndResetState` (ObjC) | Single `WKWebView` | Graceful via `requestTermination`, also kills provisional process | | `_killWebContentProcess` (ObjC) | Single `WKWebView` | Hard kill via `AuxiliaryProcessProxy::terminate()` | | `_terminateAllWebContentProcesses` (ObjC) | Entire `WKProcessPool` | Calls `requestTermination` on every process in pool | | `WKPageTerminate` (C API) | Single page | Used internally by `WebKitTestRunner` | | `webkit_web_view_terminate_web_process` (GLib) | Single view | GTK/WPE equivalent | | `terminateWebContentProcess()` (Swift `@_spi(Testing)`) | `WebPage` | Wraps `_killWebContentProcess` | | `Internals::terminateWebContentProcess()` | From JS inside page | Calls `exit(0)` in web process | --- ## LRU-Based Automatic Eviction (`_setWebProcessCountLimit`) `[WKProcessPool _setWebProcessCountLimit:N]` sets a hard cap. When a new process would exceed it, the **least-recently-used** process is terminated: ```cpp // WebProcessProxy::create() if (liveProcessesLRU().computeSize() >= s_maxProcessCount) { for (auto& processPool : WebProcessPool::allProcessPools()) processPool->webProcessCache().clear(); if (liveProcessesLRU().computeSize() >= s_maxProcessCount) protect(liveProcessesLRU().first())->requestTermination( ProcessTerminationReason::ExceededProcessCountLimit); } ASSERT(liveProcessesLRU().computeSize() < s_maxProcessCount); liveProcessesLRU().add(proxy.get()); proxy->connect(); ``` Default limit is **400**. Test `WebProcessLimit` in `WebContentProcessDidTerminate.mm` exercises this. --- ## Will `_setWebProcessCountLimit` Resolve Test Runner Hangs? **Short answer: No — and it can introduce a new kind of hang.** ### What happens at the limit Eviction is **fully synchronous** on the main thread — `requestTermination` immediately calls `processDidTerminateOrFailedToLaunch`, which removes the LRU from the tracking set before the new process is even added. No blocking wait at the limit. ### Why it can cause hangs instead `processDidTerminateOrFailedToLaunch` fires `dispatchProcessDidTerminate` on every page owned by the evicted process: ```cpp for (auto& page : pages) page->resetStateAfterProcessTermination(reason); for (auto& page : pages) page->dispatchProcessDidTerminate(*this, reason); // fires delegate callback ``` If a **test is blocked in a run loop waiting for a response from the now-killed process** (navigation completion, JS evaluation, policy decision), that callback never fires and the test hangs indefinitely. ### `ExceededProcessCountLimit` does NOT trigger auto-reload Unlike crashes or memory-limit kills, LRU eviction is explicitly excluded from the auto-reload path: ```cpp static bool shouldReloadAfterProcessTermination(ProcessTerminationReason reason) { switch (reason) { case ProcessTerminationReason::Crash: case ProcessTerminationReason::ExceededMemoryLimit: return true; // ← auto-reload case ProcessTerminationReason::ExceededProcessCountLimit: case ProcessTerminationReason::RequestedByClient: break; // ← no auto-reload } return false; } ``` So the evicted pages are left in a dead-process state with no recovery. --- ## Correct Approaches for Test Runner Process Accumulation | Approach | Notes | |----------|-------| | Explicitly `nil` / release `WKWebView` after each test | Processes are released by their owners | | `[pool _terminateAllWebContentProcesses]` in `tearDown` / `afterEach` | Controlled bulk eviction at a safe point | | Single shared `WKWebView` per suite, reset with `WKPageResetStateBetweenTests` + `about:blank` | The LayoutTests model — fastest | | `nonPersistentDataStore` per test | Data isolation without spawning extra processes | | `[WKWebsiteDataStore removeDataOfTypes:...]` | Storage cleanup without touching processes | Using the process count cap as the **primary** mechanism is fragile: the cap is applied globally and may evict a process that the currently-running test is actively waiting on, trading a memory problem for a deadlock.