Files
obsidian-vault/work/projects/navigation-framework-tests-webkit-process-cleanup.md
T

5.3 KiB

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:

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:

// 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:

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:

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.