V8pedia

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 → MaglevTurboFan, 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