On-stack replacement (OSR)
Tiering up normally takes effect on the next call to a function. But what about a function that is currently running a long loop and may not return for seconds? Waiting for the next call is useless. On-stack replacement (OSR) solves this: it transfers a running execution into freshly-optimized code, in the middle of a loop.
::: info Ubiquitous language OSR: replacing the code backing an active stack frame while it runs, typically at a loop back-edge. OSR urgency: a counter that escalates how aggressively V8 wants to OSR a hot loop. :::
The motivating case
function compute() {
let total = 0;
for (let i = 0; i < 1e9; i++) total += heavy(i); // never returns for a while
return total;
}
compute();
This loop is white-hot, but compute is called once. Pure
call-triggered tiering would run the whole billion
iterations in the interpreter/baseline before the optimized compute ever gets a
chance. OSR exists precisely for this shape.
How it triggers: urgency, not immediacy
OSR does not compile-and-jump the instant a loop is hot. Instead, V8 raises an urgency level stored on the FeedbackVector:
void TrySetOsrUrgency(Isolate* isolate, Tagged<JSFunction> function,
int osr_urgency) {
…
DCHECK_GE(osr_urgency, fv->osr_urgency()); // never lower urgency here
fv->set_osr_urgency(osr_urgency);
}
— src/execution/tiering-manager.cc#L254-L298
The JumpLoop bytecode at each loop back-edge checks this urgency. When it is
high enough, the loop requests OSR compilation; once the optimized OSR code is
ready, the next back-edge enters it directly, carrying the loop's live state
across. There are separate budgets for OSR into Maglev (invocation_count_for_maglev_osr,
default 100) and into TurboFan (invocation_count_for_osr, default 500).
The hard part: entering mid-function
Normal optimized code is entered at the top, with a fresh frame. OSR code is entered in the middle, and must adopt the current frame's live values (locals, the loop counter, accumulator). The compiler builds a special entry that maps the interpreter/baseline frame slots into the optimized frame:
class OsrHelper {
public:
void SetupFrame(Frame* frame); // prepare frame w.r.t. OSR
size_t UnoptimizedFrameSlots(); // slots to import from the live frame
static int FirstStackSlotIndex(int parameter_count) {
return 1 + parameter_count; // receiver + params
}
};
In effect, OSR is deoptimization run in reverse: deopt rebuilds an unoptimized frame from an optimized one; OSR builds an optimized frame from an unoptimized one. They share the same notion of mapping between frame layouts.
::: details Why "PrepareForOnStackReplacement" is a deopt reason
You may have seen PrepareForOnStackReplacement in the
deopt reasons. The compiler can insert a controlled bailout at
the OSR boundary to set up the transition cleanly — so OSR and deopt are wired
through overlapping machinery, which is why they appear together.
:::
Why it matters
Without OSR, a single hot loop in a top-level script (think: a benchmark, a parser, a number-crunching routine that runs once but for a long time) would never benefit from optimization. OSR closes that gap, making "hot loop in a cold function" — an extremely common real-world shape — fast.
See also
Feedback & tiering decisions — where urgency is raised.
Deoptimization — the same frame-mapping machinery, reversed.