From 4d91e83ee8c0f820768023d7e5aaa01bf11a0985 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 10 Jul 2026 12:32:55 +0600 Subject: [PATCH] [2026-07-10] eagle: work/projects/navigation-framework-tests-webkit-process-cleanup.md --- ...-framework-tests-webkit-process-cleanup.md | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 work/projects/navigation-framework-tests-webkit-process-cleanup.md diff --git a/work/projects/navigation-framework-tests-webkit-process-cleanup.md b/work/projects/navigation-framework-tests-webkit-process-cleanup.md new file mode 100644 index 00000000..f383f910 --- /dev/null +++ b/work/projects/navigation-framework-tests-webkit-process-cleanup.md @@ -0,0 +1,127 @@ +# 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.