Feedback & tiering decisions
The tier ladder only works if something decides when to
climb. That something is the TieringManager, driven by an interrupt
budget and the contents of each function's FeedbackVector.
This page is the control-system view: the counters, the thresholds, and the
decision logic.
The interrupt budget
Each function carries a budget that ticks down as it runs (at function entry and at loop back-edges). When it reaches zero, V8 takes an interrupt and asks the tiering manager what to do. The budget is proportional to bytecode length:
int TieringManager::InterruptBudgetFor(Isolate*, Tagged<JSFunction> function, …) {
const int bytecode_length =
function->shared()->GetBytecodeArray(isolate)->length();
if (FirstTimeTierUpToSparkplug(isolate, function)) {
return bytecode_length * v8_flags.invocation_count_for_feedback_allocation;
}
…
return ::i::InterruptBudgetFor(isolate, …, bytecode_length);
}
— src/execution/tiering-manager.cc#L180-L251
Scaling by length is a deliberate heuristic: a tiny hot function reaches its
budget after few invocations (optimize it eagerly — it's cheap and pays back
fast), while a large function needs proportionally more (optimizing it is
expensive, so demand more evidence). Functions already over
max_optimized_bytecode_size are effectively never optimized.
The thresholds
DEFINE_INT(invocation_count_for_feedback_allocation, 8, …)
DEFINE_INT(invocation_count_for_maglev, 400, …) // 1000 on Android
DEFINE_INT(invocation_count_for_turbofan, 3000, …)
DEFINE_INT(invocation_count_for_osr, 500, …)
DEFINE_INT(invocation_count_for_maglev_osr, 100, …)
DEFINE_INT(minimum_invocations_after_ic_update, 500, …)
— src/flags/flag-definitions.h#L1143-L1176
::: warning Heuristic, not contract
Every number here is a tuning knob that varies by platform and release, and that
modes like --jit-fuzzing rewrite wholesale (it drops Maglev's threshold to 10).
The interesting part is the shape of the policy, not the digits.
:::
Note minimum_invocations_after_ic_update: after an inline cache changes (the
function saw a new shape), V8 waits before optimizing. Optimizing on top of
still-changing feedback would just produce code that immediately deoptimizes, so
it lets the feedback settle first.
The decision
When the budget fires, OnInterruptTick runs. It lazily allocates the feedback
vector on the way to Sparkplug, then for higher tiers consults ShouldOptimize:
OptimizationDecision TieringManager::ShouldOptimize(
Tagged<FeedbackVector> feedback_vector, CodeKind current_code_kind) {
if (current_code_kind == CodeKind::TURBOFAN_JS)
return OptimizationDecision::DoNotOptimize(); // already top tier
if (TiersUpToMaglev(current_code_kind) && /* maglev enabled & allowed */)
return OptimizationDecision::Maglev(); // prefer Maglev next
…
if (bytecode->length() > v8_flags.max_optimized_bytecode_size)
return OptimizationDecision::DoNotOptimize();
return OptimizationDecision::TurbofanHotAndStable();
}
— src/execution/tiering-manager.cc#L395-L435
So the normal path is Ignition → Sparkplug → Maglev → TurboFan, with
filters (--maglev-filter, --turbo-filter), size caps, and efficiency/battery
modes able to short-circuit or delay each step. Optimization is requested
concurrently where possible — the compile happens on a background thread while
the current tier keeps running.
Where the counts live
invocation_count, OSR urgency, and tiering flags are stored right on the
FeedbackVector:
inline int32_t invocation_count() const;
inline uint8_t osr_state() const;
// flags: TieringInProgress, OsrTieringInProgress, WasOnceDeoptimized, …
— src/objects/feedback-vector.h#L308-L556
This co-location is intentional: the same object that records what types were seen also records how hot the function is and whether it has been deoptimized before. Tiering decisions can therefore consider both heat and stability from one place. A function that "was once deoptimized" is treated more cautiously — V8 learns from a failed bet.
The feedback loop, in one picture
run in Ignition/Sparkplug
│ ICs record Maps/targets/types ──► FeedbackVector
│ budget ticks down (∝ 1/bytecode_length)
▼
budget == 0 ──► TieringManager.OnInterruptTick
│ │
│ ShouldOptimize? (heat + stability + filters)
│ ├─ Maglev ─► compile (often concurrent)
│ └─ TurboFan ─► compile (concurrent, via heap broker)
▼
install optimized code; reset/raise budget
See also
Overview: the tier ladder — the tiers this orchestrates.
Inline caches & feedback — what fills the FeedbackVector.
OSR — tiering a function that is stuck in a loop.
Deoptimization — the downward direction of this control loop.