Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions JSTests/stress/codeblock-aging-ftl-idle-off.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//@ skip if !$isFTLPlatform
//@ runDefault("--useEagerCodeBlockJettisonTiming=1", "--useConcurrentJIT=0", "--optimizedCodeAgingQuietAllocationMB=0", "--optimizedCodeAgingQuietSeconds=0.02")

// With optimizedCodeAgingQuietAllocationMB=0, FTL code never ages out (the previous behaviour), even across two
// collections further apart than optimizedCodeAgingQuietSeconds with nothing allocated in between.

function shouldBe(actual, expected, msg) {
if (actual !== expected)
throw new Error((msg || "") + " expected " + expected + " but got " + actual);
}

var f = new Function("o", "var s = 0; for (var i = 0; i < 8; i++) s += o.a + o.b * i; return [s, isFinalTier()];");
var o = { a: 1, b: 2 }, reached = false;
for (var i = 0; i < 1e6 && !reached; i++)
reached = f(o)[1];
if (!reached)
throw new Error("test needs f to reach FTL");
fullGC();
var start = preciseTime();
while (preciseTime() - start < 0.04) { }
fullGC();
shouldBe(f(o)[1], true, "with the option off an FTL block never ages out;");
70 changes: 70 additions & 0 deletions JSTests/stress/codeblock-aging-ftl-idle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//@ skip if !$isFTLPlatform
//@ runDefault("--useEagerCodeBlockJettisonTiming=1", "--useConcurrentJIT=0", "--optimizedCodeAgingQuietSeconds=0.02")

// FTL code has no execution counter of its own, so it ages against the mutator as a whole
// (Options::optimizedCodeAgingQuietAllocationMB): once past its TTL it is jettisoned by a full
// collection that finds (almost) nothing was allocated since the previous one looked at it, and
// kept - together with the baseline code it pins - whenever the mutator has been allocating.

function shouldBe(actual, expected, msg) {
if (actual !== expected)
throw new Error((msg || "") + " expected " + expected + " but got " + actual);
}

function makeHot() {
return new Function("o", "var s = 0; for (var i = 0; i < 8; i++) s += o.a + o.b * i; return [s, isFinalTier()];");
}

function warmToFTL(f) {
var o = { a: 1, b: 2 };
for (var i = 0; i < 1e6; i++) {
if (f(o)[1])
return true;
}
return false;
}

function idleFor(seconds) {
var start = preciseTime();
while (preciseTime() - start < seconds) { }
}

function allocateMB(n) {
var keep = [];
for (var i = 0; i < n * 16; i++)
keep.push(new Float64Array(8192)); // 64 KB each
return keep.length;
}

// optimizedCodeAgingQuietSeconds above; leave some slack for ApproximateTime.
var quiet = 0.04;
var o = { a: 1, b: 2 };

// 1. Nothing is allocated between two full collections a TTL apart: the FTL block goes.
var f = makeHot();
if (!warmToFTL(f))
throw new Error("test needs f to reach FTL");
fullGC(); // records the allocation total for f's FTL block
idleFor(quiet);
fullGC(); // nothing allocated since, past TTL: aged out
shouldBe(f(o)[1], false, "an FTL block should age out across an idle stretch;");
shouldBe(f(o)[0], 64, "and the function should still work;");

// 2. The mutator allocates between the collections: the block stays, however old.
var g = makeHot();
if (!warmToFTL(g))
throw new Error("test needs g to reach FTL");
fullGC();
for (var round = 0; round < 3; round++) {
idleFor(quiet);
allocateMB(4);
fullGC();
shouldBe(g(o)[1], true, "an FTL block should survive while the mutator is allocating (round " + round + ");");
}

// 3. And once the mutator goes quiet it goes too.
idleFor(quiet);
fullGC();
idleFor(quiet);
fullGC();
Comment thread
claude[bot] marked this conversation as resolved.
shouldBe(g(o)[1], false, "the FTL block should age out once the mutator goes quiet;");
62 changes: 51 additions & 11 deletions Source/JavaScriptCore/bytecode/CodeBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,22 @@ Seconds CodeBlock::timeToLive(JITType jitType)
}
}

#if USE(BUN_JSC_ADDITIONS)
bool CodeBlock::agesByMutatorQuietness()
{
switch (jitType()) {
case JITType::FTLJIT:
return true;
#if ENABLE(DFG_JIT)
case JITType::DFGJIT:
return !Options::useExecutionCountForCodeBlockAging() || !dfgJITData() || baselineVersion()->m_didFailFTLCompilation;
#endif
default:
return false;
}
}
#endif

template<typename Visitor>
ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker&, Visitor& visitor)
{
Expand All @@ -1354,19 +1370,36 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker
#if USE(BUN_JSC_ADDITIONS)
JITType type = jitType();
Seconds ttl = timeToLive(type);
if (timeSinceCreation() < ttl)
return false;

// Optimizing tiers: an FTL block is small and very expensive to rebuild, so it never ages out (it stays alive for as
// long as the structures it speculated on do, see determineLiveness()). A DFG block ages like a baseline one, using
// the tier-up counter its code already decrements at returns and loop back-edges as the sign of life; a DFG block
// compiled without tier-up checks has no such signal and is kept.
if (type == JITType::FTLJIT)
return false;
#if ENABLE(DFG_JIT)
if (type == JITType::DFGJIT && (!Options::useExecutionCountForCodeBlockAging() || !dfgJITData() || baselineVersion()->m_didFailFTLCompilation))
// Optimizing tiers: a DFG block ages like a baseline one, using the tier-up counter its code already decrements at
// returns and loop back-edges as the sign of life. FTL code, and DFG code compiled without tier-up checks, has no
// counter of its own. It is expensive to rebuild, but it also pins its baseline alternative and every baseline
// CodeBlock it inlined along with their metadata, so rather than never ageing it ages against the mutator as a
// whole: a full collection lets it go once it is past its TTL and (almost) nothing has been allocated since the
// previous full collection looked at it - the process has been sitting idle - and renews it whenever the mutator
// has been allocating, exactly as before. Collections are themselves allocation-driven, so a busy mutator never
// sees two such quiet collections in a row; an embedder's idle-time collections do.
if (agesByMutatorQuietness()) {
unsigned quietMB = Options::optimizedCodeAgingQuietAllocationMB();
if (!quietMB)
return false;
// Whole megabytes, wrapping: only the distance from the lease's start matters. The first check of a block (field
// still 0) and a block that just moved into this regime both see a large distance and renew.
uint32_t allocatedMB = static_cast<uint32_t>(vm().heap.totalBytesAllocated() >> 20);
if (static_cast<uint32_t>(allocatedMB - m_leaseStartAllocatedMB) > quietMB) {
// The mutator has been allocating since this block's lease started: renew it from here.
m_leaseStartAllocatedMB = allocatedMB;
m_creationTime = ApproximateTime::now();
return false;
}
Seconds quietFor = Seconds(Options::optimizedCodeAgingQuietSeconds());
if (Options::useEagerCodeBlockJettisonTiming()) [[unlikely]]
quietFor = std::min(quietFor, ttl);
return timeSinceCreation() >= quietFor;
}

if (timeSinceCreation() < ttl)
return false;
#endif

if (Options::useExecutionCountForCodeBlockAging()) {
// LLInt and Baseline CodeBlocks already tick an execution counter on
Expand Down Expand Up @@ -1925,6 +1958,13 @@ void CodeBlock::reconcileWeakReferencesAtGCEnd(VM& vm, CollectionScope)
auto updateActivity = [&] {
if (!VM::useUnlinkedCodeBlockJettisoning())
return;
#if USE(BUN_JSC_ADDITIONS)
if (agesByMutatorQuietness()) {
if (!m_visitChildrenSkippedDueToOldAge)
m_unlinkedCode->resetAge();
return;
}
#endif
auto* jitCode = m_jitCode.get();
float count = 0;
bool alwaysActive = false;
Expand Down
10 changes: 9 additions & 1 deletion Source/JavaScriptCore/bytecode/CodeBlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ class CodeBlock : public JSCell {
static constexpr ptrdiff_t offsetOfShouldAlwaysBeInlined() { return OBJECT_OFFSETOF(CodeBlock, m_shouldAlwaysBeInlined); }

#if ENABLE(JIT)
unsigned m_capabilityLevelState : 2; // DFG::CapabilityLevel
uint8_t m_capabilityLevelState : 2; // DFG::CapabilityLevel. Byte-typed so MSVC packs it with the bools below.
#endif

bool m_didFailJITCompilation : 1;
Expand Down Expand Up @@ -931,6 +931,10 @@ class CodeBlock : public JSCell {
static Seconds timeToLive(JITType);
// Start the execution-count aging lease from the counter's current value (call when a tier's code is installed).
void snapshotExecutionCounterForAging(float count) { m_previousCounter = count; }
#if USE(BUN_JSC_ADDITIONS)
// Optimizing code with no execution counter to read liveness off (FTL; DFG without tier-up checks): it ages once the mutator goes quiet instead.
bool agesByMutatorQuietness();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#endif
private:

template<typename Visitor> void propagateTransitions(const ConcurrentJSLocker&, Visitor&);
Expand Down Expand Up @@ -998,6 +1002,10 @@ class CodeBlock : public JSCell {
uint16_t m_optimizationDelayCounter { 0 };
uint16_t m_reoptimizationRetryCounter { 0 };
float m_previousCounter { 0 };
#if USE(BUN_JSC_ADDITIONS)
// For agesByMutatorQuietness() code: the heap's allocation total, in whole MB modulo 2^32, when the current lease started.
uint32_t m_leaseStartAllocatedMB { 0 };
#endif
StructureWatchpointMap m_llintGetByIdWatchpointMap;
RefPtr<JSC::JITCode> m_jitCode;
#if ENABLE(JIT)
Expand Down
3 changes: 3 additions & 0 deletions Source/JavaScriptCore/heap/Heap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,9 @@ void Heap::setGarbageCollectionTimerEnabled(bool enable)
constexpr size_t oversizedAllocationThreshold = 64 * KB;
void Heap::didAllocate(size_t bytes)
{
#if USE(BUN_JSC_ADDITIONS)
m_totalBytesAllocated += bytes;
#endif
if (bytes >= oversizedAllocationThreshold) {
m_oversizedBytesAllocatedThisCycle += bytes;
m_lastOversidedAllocationThisCycle = bytes;
Expand Down
7 changes: 7 additions & 0 deletions Source/JavaScriptCore/heap/Heap.h
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,10 @@ class Heap {
JS_EXPORT_PRIVATE void deprecatedReportExtraMemorySlowCase(size_t);

size_t totalBytesAllocatedThisCycle() { return m_nonOversizedBytesAllocatedThisCycle + m_oversizedBytesAllocatedThisCycle; }
#if USE(BUN_JSC_ADDITIONS)
// Monotonic; lets a full collection tell whether the mutator did anything since an earlier one (CodeBlock::shouldJettisonDueToOldAge).
uint64_t totalBytesAllocated() const { return m_totalBytesAllocated; }
#endif

bool shouldCollectInCollectorThread(const AbstractLocker&);
void collectInCollectorThread();
Expand Down Expand Up @@ -877,6 +881,9 @@ class Heap {
const size_t m_ramSize;
const size_t m_minBytesPerCycle;
size_t m_bytesAllocatedBeforeLastEdenCollect { 0 };
#if USE(BUN_JSC_ADDITIONS)
uint64_t m_totalBytesAllocated { 0 };
#endif
size_t m_sizeAfterLastCollect { 0 };
size_t m_sizeAfterLastFullCollect { 0 };
size_t m_sizeBeforeLastFullCollect { 0 };
Expand Down
2 changes: 2 additions & 0 deletions Source/JavaScriptCore/runtime/OptionsList.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ bool hasCapacityToUseLargeGigacage();
v(Bool, verboseFFI, false, Normal, "dataLog on FFI thunk/stub/signature creation"_s)
#define FOR_EACH_JSC_CODEBLOCK_AGING_OPTION(v) \
v(Bool, useExecutionCountForCodeBlockAging, true, Normal, "If true, an LLInt/Baseline CodeBlock whose execution counter has advanced since the last old-age check is treated as still in use and its TTL is renewed instead of being jettisoned."_s) \
v(Unsigned, optimizedCodeAgingQuietAllocationMB, 1, Normal, "FTL code (and DFG code without a tier-up counter) past its TTL is jettisoned for old age by a full collection that finds at most this much was allocated since the previous one looked at it, i.e. the mutator has been idle. 0 = such code never ages out."_s) \
v(Double, optimizedCodeAgingQuietSeconds, 30, Normal, "How long the mutator must have been that quiet, measured from the last collection that saw it allocating, before such code is let go (capped at the tier's TTL under useEagerCodeBlockJettisonTiming)."_s) \
v(Double, codeBlockAgingLeaseMultiplier, 3.0, Normal, "When useExecutionCountForCodeBlockAging proves a CodeBlock is still active, renew its old-age TTL to this many multiples of timeToLive for its tier."_s)
#define FOR_EACH_JSC_BYTECODE_CACHE_DECODER_OPTION(v) \
v(Bool, useLeanBytecodeCacheDecoder, true, Normal, "If true, the bytecode cache Decoder skips bookkeeping that is only needed for decoded objects shared by multiple references."_s) \
Expand Down
Loading