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
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
3142namespace 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+
33248FastMallocAlignedMemoryAllocator::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
0 commit comments