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

128 lines
5.3 KiB
Markdown

# 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.