Skip to content
C.W.K.
Lesson 03 of 05 · published

Measuring the Reason Dad Left Safari

~15 min · WebKit, memory, measurement, probes

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

An old reason, asked again

Dad left Safari about three years before Voyager, and the reason he gave was pages dying under memory limits. That is a serious charge against an engine you are about to build a browser on. When Dad asked for WebKit to be compared again after the fork discussion, this was the claim that had to be settled first, and settled by measurement, because a memory from 2023 and a forum thread are both weak grades.

The method had three parts: read the engine's own source for the rule, write a probe that pushes a real page against the rule on a real Mac, and write down what the probe could and could not tell.

What the source says

WebKit kills a web content process whose memory footprint passes a threshold, and the thresholds are plain code in Source/WTF/wtf/MemoryPressureHandler.cpp. On a 64-bit Mac, an active process may reach 15 GB if the Mac has more than 16 GB of RAM (7 GB otherwise), plus 1 GB for every page in the process. An inactive one gets 3 GB plus 1 GB per page, never more than 90 percent of the Mac's RAM, a cap that only bites on a Mac with little memory. The evaluation graded this Verified here: the source was opened and read in that session.

Two numbers like that raise the real question at once. What makes a page "active" or "inactive" in a browser? A tab in the background is the obvious guess, and guessing is exactly what the probe exists to replace.

The probe

scripts/probes/wk-memory-probe.swift is a small AppKit program with one WKWebView and a page whose only job is to grow. Its grow(gb) function allocates 256 MB Uint8Array chunks and writes one byte in every 4096-byte page of each chunk, so the memory is really touched and counts toward the footprint. The program listens for webViewWebContentProcessDidTerminate, the public delegate call WebKit makes when a page's process dies, and logs when it came.

It has four modes. visible grows a page in a window step by step and holds each step for 45 seconds, because WebKit polls a process's footprint about every 30 seconds. hidden does the same with a web view in no window. background imitates a background tab: the page grows while shown, then its web view is removed from the window and another web view takes its place, and the probe checks every minute. covered is the control: the page stays in the window, underneath the one in front.

What it measured

ModeWhat happened
visibleHeld 8 GB and 14 GB for 45 s each; the process died during the step to 18 GB, which fits a line at 16 GB (15 plus 1 for its one page)
background, 6 GBAlive at minute 8, gone at minute 9: ended 567 s after its web view left the window
background, 3 GBLived the full 14 minutes
covered, 6 GBLived the full 13 minutes

The first probe held each step for 45 seconds and saw no lower limit at all, because that limit only takes hold about eight minutes after a page leaves the window. A probe that is too short does not fail loudly. It just reports that everything survived. The commit that recorded Dad's ruling said, honestly, that the background limit "was not measured cleanly". The longer runs landed the same day in the first-brick commit, and the evaluation now carries them as gate case W10. (The repo's CLAUDE.md still has one bullet saying it was not measured cleanly, a few lines above a bullet giving the measurement. The measurement is the current fact.)

The covered run is the one that changed the design. The evaluation states the result in one line: the two limits turn on whether the web view is in a window, not on whether Dad can see it. A heavy page kept in the window underneath the front tab keeps the visible page's limit.

What it could not say

The evaluation is careful about the edges of its own evidence. There is no public way to raise or disable the thresholds; that is graded Research, from Apple developer forum threads with no answer from Apple. Why Dad's pages died in 2023 is "not established". The foreground threshold was already that high in the source of that time, so the cause could have been the background limit, an energy limit or plain site incompatibility. And what it costs to keep a heavy page in the window (it keeps running timers and drawing like a visible page) was not measured either.

So the conclusion is deliberately narrow: the old reason no longer holds for an ordinary heavy page. A visible page gets about 16 GB on a Mac with plenty of memory, and the background limit is known, timed and avoidable through public API.

The ruling

With the memory measured, the family's own frontends searched (next lesson) and a precedent found for extensions on WebKit, Dad ruled:

웹키트로 가자.

"Let's go with WebKit." Voyager runs on WebKit on the Mac, the iPhone and the iPad. The measured facts did not stay in the evaluation, either. The sleep policy and the way Pippa herself is kept in the window under the front tab are both built on the covered result; the track on tabs that sleep and wake shows how.

Principle: When a remembered failure is the reason against a tool, measure the failure on today's version before you decide, and write down what the measurement cannot explain.

Code

cwkVoyager, scripts/probes/wk-memory-probe.swift: the probe's four modes (verbatim, lines 1-18)·swift
// Probe: at what footprint does this Mac's WebKit kill a WKWebView page?
// (gate case W10 and the memory measurement in ENGINE-EVALUATION.md §2)
//
//   wk-memory-probe visible <GB> <GB> ...
//       A page in a visible window grows to each step and holds 45 s (WebKit
//       polls a process's footprint about every 30 s).
//   wk-memory-probe hidden <GB> <GB> ...
//       The same with a web view that is in no window at all.
//   wk-memory-probe background <GB> <minutes>
//       What a background tab is in a browser: the page is shown, grows to
//       <GB> while visible, then its web view is taken out of the window
//       (another web view stays in front) and held for <minutes>. Reports
//       when, counted from leaving the window, the process is terminated.
//   wk-memory-probe covered <GB> <minutes>
//       The same, except the page's web view stays in the window underneath
//       the one in front. Does the engine still count it as a visible page?
//
// It reports whether and when the web content process was terminated.
cwkVoyager, scripts/probes/wk-memory-probe.swift: background versus covered, the one-line difference (verbatim, lines 138-148)·swift
    func leaveWindow() {
        if staysInWindow {
            log("holding \(heldGB) GB; another web view now covers the page, which stays in the window")
        } else {
            log("holding \(heldGB) GB; the page leaves the window now and another web view takes its place")
            page.removeFromSuperview()
        }
        container.addSubview(front)
        leftWindowAt = Date()
        tick(minute: 1)
    }
WebKit, Source/WTF/wtf/MemoryPressureHandler.cpp: the kill thresholds (verbatim, lines 111-130 of main as read on 2026-10-27; C++)·text
static size_t thresholdForMemoryKillOfActiveProcess(unsigned tabCount)
{
#if CPU(ADDRESS64)
    size_t baseThreshold = ramSize() > 16 * GB ? 15 * GB : 7 * GB;
    return baseThreshold + tabCount * GB;
#else
    UNUSED_PARAM(tabCount);
    return std::min(3 * GB, static_cast<size_t>(truncateDoubleToUint64(ramSize() * 0.9)));
#endif
}

static size_t thresholdForMemoryKillOfInactiveProcess(unsigned tabCount)
{
#if CPU(ADDRESS64)
    size_t baseThreshold = 3 * GB + tabCount * GB;
#else
    size_t baseThreshold = tabCount > 1 ? 3 * GB : 2 * GB;
#endif
    return std::min(baseThreshold, static_cast<size_t>(truncateDoubleToUint64(ramSize() * 0.9)));
}

External links

Exercise

Write a small probe for a resource limit in a platform you build on: memory per web view, a background-execution budget, a file-descriptor ceiling, a request timeout. First find the rule in the platform's own source or documentation and write down the number it predicts. Then build the probe with at least one control case that differs from the test case in exactly one thing (as covered differs from background only in whether the view leaves the window). Run it long enough for the slowest plausible failure, record when each case ended, and write a short note listing what the result does not explain.
Hint
Make the probe report a positive event (a termination callback, an error, a timestamp) rather than inferring failure from silence. Hold each step longer than the platform's polling interval, and log time since the change you made, not time since launch.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.