Skip to content

Commit 0839f93

Browse files
committed
[JSC] Introduce JSC WarmUpThread for MarkedBlocks
https://bugs.webkit.org/show_bug.cgi?id=322774 rdar://186036234 Reviewed by Marcus Plutowski and Dan Hecht. System page size is 16KB (on Darwin at least) and MarkedBlock size is also 16KB. We end up having a page-fault for each MarkedBlock allocation and it is really costly when it is executed synchronously. On M5 device, each page-fault cost is 700ns. This patch introduces WarmUpBlockProvider. This holds its own helper thread via AutomaticThread, and offering a pre-page-faulted warmed-up MarkedBlocks if possible. This manages a buffer of pre-page-faulted pages and provides them. If it is exhausted, thread is launched / acked and start allocating a new pages and perform page-fault concurrently to the mutator. Memory scores (Membuster7 / PLUM4) are both neutral. * JSTests/stress/warm-up-marked-blocks-state-machine.js: Added. * JSTests/stress/warm-up-marked-blocks.js: Added. (shouldBe): * Source/JavaScriptCore/heap/FastMallocAlignedMemoryAllocator.cpp: (JSC::warmUpMarkedBlockStateForTesting): (JSC::setWarmUpMarkedBlockAllocationShouldFailForTesting): (JSC::FastMallocAlignedMemoryAllocator::tryAllocateAlignedMemory): * Source/JavaScriptCore/heap/FastMallocAlignedMemoryAllocator.h: * Source/JavaScriptCore/runtime/OptionsList.h: * Source/JavaScriptCore/tools/JSDollarVM.cpp: (JSC::JSC_DEFINE_HOST_FUNCTION): (JSC::JSDollarVM::finishCreation): Canonical link: https://commits.webkit.org/320112@main
1 parent df289ce commit 0839f93

6 files changed

Lines changed: 352 additions & 2 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//@ runDefault("--useDollarVM=1", "--warmUpMarkedBlockCount=8", "--warmUpMarkedBlockIdleTimeout=0.2")
2+
3+
// Drives the warm-up supply through its whole state machine: fill, release on idle, restart,
4+
// stand-down on allocation failure, and recovery once allocation works again. Every assertion is
5+
// "reaches this state eventually", since a helper thread drives the transitions.
6+
7+
const pollSeconds = 0.05;
8+
const timeoutSeconds = 20;
9+
10+
const retained = [];
11+
12+
function allocateBlocks() {
13+
// Retaining these is the point: a heap that can recycle stops asking the allocator for blocks,
14+
// and sustained block demand is what the stand-down and restart paths are driven by. The count
15+
// is kept small so that a run which ends up timing out still reports rather than exhausting
16+
// memory first.
17+
for (let i = 0; i < 2000; ++i)
18+
retained.push({ a: i, b: i, c: i });
19+
}
20+
21+
function waitFor(description, predicate, betweenAttempts = () => { }) {
22+
for (let attempt = 0; attempt < timeoutSeconds / pollSeconds; ++attempt) {
23+
if (predicate($vm.warmUpMarkedBlockState()))
24+
return;
25+
betweenAttempts();
26+
sleepSeconds(pollSeconds);
27+
}
28+
const state = $vm.warmUpMarkedBlockState();
29+
throw new Error(`Timed out waiting for ${description}; blocks=${state.blocks} phase=${state.phase}`);
30+
}
31+
32+
const isFilled = state => state.phase === "armed" && state.blocks > 0;
33+
34+
// Demand starts the helper, which arms to the configured depth and fills.
35+
allocateBlocks();
36+
waitFor("the supply to fill", isFilled, allocateBlocks);
37+
38+
// With no demand at all, the helper hands everything back and shuts down.
39+
waitFor("the supply to be released when idle", state => state.phase === "stopped");
40+
41+
// Fresh demand brings it back.
42+
allocateBlocks();
43+
waitFor("the helper to restart", isFilled, allocateBlocks);
44+
45+
// An allocation failure makes it stand down rather than spin against an exhausted heap.
46+
$vm.setWarmUpMarkedBlockAllocationShouldFail(true);
47+
waitFor("the helper to stand down", state => state.phase === "standingDown", allocateBlocks);
48+
49+
// Standing down must not be permanent: the idle timeout still fires while demand continues, and
50+
// lifts it once allocation works again.
51+
$vm.setWarmUpMarkedBlockAllocationShouldFail(false);
52+
waitFor("the stand-down to lift", isFilled, allocateBlocks);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@ runDefault("--useWarmUpMarkedBlocks=1", "--warmUpMarkedBlockCount=64")
2+
//@ runDefault("--useWarmUpMarkedBlocks=1", "--warmUpMarkedBlockCount=1")
3+
//@ runDefault("--useWarmUpMarkedBlocks=1", "--warmUpMarkedBlockCount=0")
4+
//@ runDefault("--useWarmUpMarkedBlocks=0")
5+
//@ runDefault("--useWarmUpMarkedBlocks=1", "--warmUpMarkedBlockCount=8", "--scribbleFreeCells=1")
6+
//@ runDefault("--useWarmUpMarkedBlocks=1", "--forceMiniVMMode=1")
7+
8+
function shouldBe(actual, expected) {
9+
if (actual !== expected)
10+
throw new Error(`Expected ${expected} but got ${actual}`);
11+
}
12+
13+
let chain = null;
14+
let expectedLength = 0;
15+
for (let i = 0; i < 300000; ++i) {
16+
const garbage = { index: i, payload: [i, i + 1, i + 2] };
17+
shouldBe(garbage.payload[2], i + 2);
18+
if (!(i % 1000)) {
19+
chain = { index: i, next: chain };
20+
++expectedLength;
21+
}
22+
}
23+
24+
let length = 0;
25+
for (let node = chain; node; node = node.next) {
26+
++length;
27+
shouldBe(node.index, (expectedLength - length) * 1000);
28+
}
29+
shouldBe(length, expectedLength);

Source/JavaScriptCore/heap/FastMallocAlignedMemoryAllocator.cpp

Lines changed: 223 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2017 Apple Inc. All rights reserved.
2+
* Copyright (C) 2017-2026 Apple Inc. All rights reserved.
33
*
44
* Redistribution and use in source and binary forms, with or without
55
* modification, are permitted provided that the following conditions
@@ -20,16 +20,231 @@
2020
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
2121
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2222
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23-
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2424
*/
2525

2626
#include "config.h"
2727
#include "FastMallocAlignedMemoryAllocator.h"
2828

29+
#include "MarkedBlock.h"
30+
#include "Options.h"
31+
#include "VM.h"
32+
#include <wtf/AutomaticThread.h>
33+
#include <wtf/Box.h>
2934
#include <wtf/FastMalloc.h>
35+
#include <wtf/Lock.h>
36+
#include <wtf/NeverDestroyed.h>
37+
#include <wtf/PageBlock.h>
38+
#include <wtf/StdLibExtras.h>
39+
#include <wtf/TZoneMallocInlines.h>
40+
#include <wtf/Vector.h>
3041

3142
namespace JSC {
3243

44+
#if !ENABLE(MALLOC_HEAP_BREAKDOWN)
45+
46+
namespace {
47+
48+
// Lets a test drive the exhaustion path without running the machine out of memory.
49+
std::atomic<bool> s_allocationFailsForTesting { false };
50+
51+
void* tryAllocateBlock()
52+
{
53+
if (s_allocationFailsForTesting.load(std::memory_order_relaxed)) [[unlikely]]
54+
return nullptr;
55+
return tryFastCompactAlignedMalloc(MarkedBlock::blockSize, MarkedBlock::blockSize);
56+
}
57+
58+
// The first store into a freshly allocated MarkedBlock takes a write fault, and a heap that is
59+
// ramping up pays that fault thousands of times on the mutator thread. WarmUpBlockProvider keeps a
60+
// supply of blocks whose pages a helper thread has already made resident, so the fault lands on the
61+
// helper instead.
62+
//
63+
// The supply has to be deep from the very first request, because ramp demand runs at tens of blocks
64+
// per millisecond and a depth that grows in proportion to observed demand arrives too late to help.
65+
// Warming a page is not free even when it is handed back promptly, so the supply is given up once an
66+
// interval passes with no demand at all.
67+
class WarmUpBlockProvider {
68+
public:
69+
using Phase = WarmUpMarkedBlockPhase;
70+
71+
WarmUpBlockProvider()
72+
: m_lock(Box<Lock>::create())
73+
, m_condition(AutomaticThreadCondition::create())
74+
, m_thread(adoptRef(*new WarmUpThread(Locker { *m_lock }, *this)))
75+
{
76+
}
77+
78+
static bool isEnabled()
79+
{
80+
// Mini mode trades throughput for footprint, which is the opposite of the bargain here.
81+
return Options::useWarmUpMarkedBlocks() && Options::warmUpMarkedBlockCount() && !VM::isInMiniMode();
82+
}
83+
84+
static WarmUpBlockProvider& singleton()
85+
{
86+
static LazyNeverDestroyed<WarmUpBlockProvider> provider;
87+
static std::once_flag flag;
88+
std::call_once(flag, [] {
89+
provider.construct();
90+
});
91+
return provider;
92+
}
93+
94+
void* tryTake()
95+
{
96+
Locker locker { *m_lock };
97+
void* result = m_blocks.isEmpty() ? nullptr : m_blocks.takeLast();
98+
m_demandSinceRefill = true;
99+
m_demandSinceIdleCheck = true;
100+
// A miss means the mutator is about to take the very fault this exists to avoid, and it is
101+
// also the only thing that brings the helper back once it has shut itself down. While it is
102+
// standing down, a notify would only postpone the timeout that lifts the stand-down.
103+
if ((!result || isRunningLow()) && m_phase != Phase::StandingDown)
104+
m_condition->notifyOne(locker);
105+
return result;
106+
}
107+
108+
WarmUpMarkedBlockState stateForTesting()
109+
{
110+
Locker locker { *m_lock };
111+
return { m_blocks.size(), m_phase };
112+
}
113+
114+
private:
115+
// AutomaticThread has no voluntary temporary stop: PollResult::Stop is permanent, and start()
116+
// release-asserts that the thread is still running. So giving up after an allocation failure has
117+
// to be a wait that suppresses notifies, which is what leaves the idle timeout free to lift it.
118+
class WarmUpThread final : public AutomaticThread {
119+
WTF_MAKE_TZONE_ALLOCATED_INLINE(WarmUpThread);
120+
WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(WarmUpThread);
121+
public:
122+
WarmUpThread(const AbstractLocker& locker, WarmUpBlockProvider& provider)
123+
: AutomaticThread(locker, provider.m_lock, provider.m_condition.copyRef(), Seconds(Options::warmUpMarkedBlockIdleTimeout()))
124+
, m_provider(provider)
125+
{
126+
}
127+
128+
ASCIILiteral name() const final { return "JSCWarmUp"_s; }
129+
130+
protected:
131+
void threadDidStart() final
132+
{
133+
Locker locker { *m_provider.m_lock };
134+
m_provider.m_phase = Phase::Armed;
135+
}
136+
137+
PollResult poll(const AbstractLocker&) final
138+
{
139+
assertIsHeld(*m_provider.m_lock);
140+
if (m_provider.m_phase != Phase::Armed)
141+
return PollResult::Wait;
142+
// Topping the supply back up while demand is still arriving keeps it near its full depth
143+
// on a ramp, rather than letting it drain to the watermark first.
144+
if (m_provider.m_demandSinceRefill || m_provider.isRunningLow())
145+
return PollResult::Work;
146+
return PollResult::Wait;
147+
}
148+
149+
WorkResult work() final
150+
{
151+
m_provider.refill();
152+
return WorkResult::Continue;
153+
}
154+
155+
bool shouldSleep(const AbstractLocker&) final
156+
{
157+
assertIsHeld(*m_provider.m_lock);
158+
if (!std::exchange(m_provider.m_demandSinceIdleCheck, false))
159+
return true;
160+
m_provider.m_phase = Phase::Armed;
161+
return false;
162+
}
163+
164+
void threadIsStopping(const AbstractLocker&) final
165+
{
166+
assertIsHeld(*m_provider.m_lock);
167+
// fastFree does not reach back into this lock, so it is safe to call with it held.
168+
m_provider.m_phase = Phase::Stopped;
169+
for (void* block : std::exchange(m_provider.m_blocks, { }))
170+
fastFree(block);
171+
}
172+
173+
private:
174+
WarmUpBlockProvider& m_provider;
175+
};
176+
177+
size_t targetDepth() WTF_REQUIRES_LOCK(*m_lock) { return m_phase == Phase::Armed ? Options::warmUpMarkedBlockCount() : 0; }
178+
179+
bool isRunningLow() WTF_REQUIRES_LOCK(*m_lock) { return m_blocks.size() * 4 < targetDepth(); }
180+
181+
static void makeResident(void* block)
182+
{
183+
// One store per page is what takes the fault. Striding by the real page size rather than by
184+
// the smallest a supported system could have avoids repeating the store within a page.
185+
size_t pageSize = WTF::pageSize();
186+
ASSERT(!(MarkedBlock::blockSize % pageSize));
187+
auto bytes = unsafeMakeSpan(static_cast<volatile char*>(block), MarkedBlock::blockSize);
188+
for (size_t offset = 0; offset < bytes.size(); offset += pageSize)
189+
bytes[offset] = 0;
190+
}
191+
192+
void refill()
193+
{
194+
size_t want;
195+
{
196+
Locker locker { *m_lock };
197+
m_demandSinceRefill = false;
198+
size_t target = targetDepth();
199+
want = target > m_blocks.size() ? target - m_blocks.size() : 0;
200+
}
201+
202+
Vector<void*, 32> staging;
203+
bool exhausted = false;
204+
for (size_t i = 0; i < want; ++i) {
205+
void* block = tryAllocateBlock();
206+
if (!block) {
207+
exhausted = true;
208+
break;
209+
}
210+
makeResident(block);
211+
staging.append(block);
212+
}
213+
214+
Locker locker { *m_lock };
215+
m_blocks.appendVector(WTF::move(staging));
216+
if (exhausted)
217+
m_phase = Phase::StandingDown;
218+
}
219+
220+
const Box<Lock> m_lock;
221+
const Ref<AutomaticThreadCondition> m_condition;
222+
const Ref<WarmUpThread> m_thread;
223+
Vector<void*, 32> m_blocks WTF_GUARDED_BY_LOCK(*m_lock);
224+
Phase m_phase WTF_GUARDED_BY_LOCK(*m_lock) { Phase::Stopped };
225+
bool m_demandSinceRefill WTF_GUARDED_BY_LOCK(*m_lock) { false };
226+
bool m_demandSinceIdleCheck WTF_GUARDED_BY_LOCK(*m_lock) { false };
227+
};
228+
229+
} // anonymous namespace
230+
231+
WarmUpMarkedBlockState warmUpMarkedBlockStateForTesting()
232+
{
233+
return WarmUpBlockProvider::singleton().stateForTesting();
234+
}
235+
236+
void setWarmUpMarkedBlockAllocationShouldFailForTesting(bool shouldFail)
237+
{
238+
s_allocationFailsForTesting.store(shouldFail, std::memory_order_relaxed);
239+
}
240+
241+
#else // ENABLE(MALLOC_HEAP_BREAKDOWN)
242+
243+
WarmUpMarkedBlockState warmUpMarkedBlockStateForTesting() { return { }; }
244+
void setWarmUpMarkedBlockAllocationShouldFailForTesting(bool) { }
245+
246+
#endif
247+
33248
FastMallocAlignedMemoryAllocator::FastMallocAlignedMemoryAllocator()
34249
#if ENABLE(MALLOC_HEAP_BREAKDOWN)
35250
: m_heap("WebKit FastMallocAlignedMemoryAllocator")
@@ -44,6 +259,12 @@ void* FastMallocAlignedMemoryAllocator::tryAllocateAlignedMemory(size_t alignmen
44259
#if ENABLE(MALLOC_HEAP_BREAKDOWN)
45260
return m_heap.memalign(alignment, size, true);
46261
#else
262+
// MarkedBlock::tryCreate is the only caller today and always asks for a block-shaped region.
263+
// The guard keeps a future caller of some other size from being handed a block.
264+
if (alignment == MarkedBlock::blockSize && size == MarkedBlock::blockSize && WarmUpBlockProvider::isEnabled()) {
265+
if (void* block = WarmUpBlockProvider::singleton().tryTake())
266+
return block;
267+
}
47268
return tryFastCompactAlignedMalloc(alignment, size);
48269
#endif
49270

Source/JavaScriptCore/heap/FastMallocAlignedMemoryAllocator.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,15 @@ class FastMallocAlignedMemoryAllocator final : public AlignedMemoryAllocator {
5353
#endif
5454
};
5555

56+
// The supply of pre-warmed MarkedBlocks is a process-wide singleton with no other observer, so $vm
57+
// reaches its state through these rather than through any VM.
58+
enum class WarmUpMarkedBlockPhase : uint8_t { Stopped, Armed, StandingDown };
59+
struct WarmUpMarkedBlockState {
60+
size_t blockCount { 0 };
61+
WarmUpMarkedBlockPhase phase { WarmUpMarkedBlockPhase::Stopped };
62+
};
63+
WarmUpMarkedBlockState warmUpMarkedBlockStateForTesting();
64+
void setWarmUpMarkedBlockAllocationShouldFailForTesting(bool);
65+
5666
} // namespace JSC
5767

Source/JavaScriptCore/runtime/OptionsList.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ bool hasCapacityToUseLargeGigacage();
239239
v(Double, gcIncrementBytes, 10000, Normal, nullptr) \
240240
v(Double, gcIncrementMaxBytes, 100000, Normal, nullptr) \
241241
v(Double, gcIncrementScale, 0, Normal, nullptr) \
242+
v(Bool, useWarmUpMarkedBlocks, true, Normal, "hand MarkedBlock allocation pages that a helper thread already made resident"_s) \
243+
v(Unsigned, warmUpMarkedBlockCount, 32, Normal, "how many MarkedBlocks the helper thread keeps ready with their pages already resident; 0 turns it off"_s) \
244+
v(Double, warmUpMarkedBlockIdleTimeout, 10, Normal, "seconds without a MarkedBlock request before the helper thread releases what it is holding and shuts down"_s) \
242245
v(Bool, scribbleFreeCells, false, Normal, nullptr) \
243246
v(Double, sizeClassProgression, 1.4, Normal, nullptr) \
244247
v(Unsigned, preciseAllocationCutoff, 100000, Normal, nullptr) \

0 commit comments

Comments
 (0)