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:
- Navigate to
about:blank - Call
WKPageResetStateBetweenTests(soft in-process reset — process stays alive) - Clear back/forward list and cache
- Only fall back to
WKPageTerminateif loadingabout:blankfails
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.