docs/runtime/function-architecture.md
To execute JavaScript functions efficiently, V8 splits function data into context-independent and context-dependent parts. This separation allows sharing of heavy assets (like bytecode) while allowing specific instances (closures) to maintain their own state and feedback.
This document describes this architecture and how functions are chained together both statically and dynamically.
SharedFunctionInfoData that does not change regardless of where or how many times a function is instantiated is stored in the SharedFunctionInfo (SFI).
NativeContexts.ScopeInfo: The lexical structure of the scope (see Scopes and ScopeInfos).FeedbackMetadata: Describes the shape and size of the FeedbackVector that needs to be allocated for this function.SharedFunctionInfo objects do not form a direct linked list. Instead, they form a tree structure embedded within the bytecode.
When a function contains a nested function definition, the outer function's BytecodeArray is responsible for creating the closure for the inner function.
BytecodeArray.CreateClosure Bytecode: The outer function's bytecode contains a CreateClosure instruction at the point where the inner function is defined/created.CreateClosure bytecode takes an operand that is the index of the inner SFI in the constant pool.Data that depends on the specific execution context or function instance is stored in objects linked from the JSFunction.
JSFunctionA JSFunction represents an actual JavaScript function object available to the user code (a closure).
SharedFunctionInfo: Points to the shared data.Context: Points to the execution context (lexical environment) where the function was created. This is what makes it a closure.FeedbackCell: Points to a cell that holds the FeedbackVector.JSDispatchHandle: In modern V8, this is a 32-bit index into the JSDispatchTable, used to find the executable code, enabling efficient tiering.FeedbackCellThe FeedbackCell acts as a level of indirection between the JSFunction and the FeedbackVector.
interrupt_budget, which is decremented on function entry and backward branches. When it reaches zero, it triggers a tiering request.FeedbackVector until the function becomes "hot".FeedbackCell if they are guaranteed to have the same feedback behavior.FeedbackVectorThe FeedbackVector holds the runtime feedback (Inline Caches) used by the optimizing compilers.
ClosureFeedbackCellArray: Holds FeedbackCells for inner closures.CreateClosure SlotsThe dynamic chaining happens through the feedback system, specifically to allow inner functions to have their own feedback vectors when they become hot.
ClosureFeedbackCellArray: Every FeedbackVector (and also uncompiled FeedbackCells that are preparing for vector allocation) contains a ClosureFeedbackCellArray.CreateClosure bytecode in a function, there is a corresponding slot in its ClosureFeedbackCellArray.FeedbackCells before the closures are actually created.CreateClosure is executed, it looks up the pre-allocated FeedbackCell from the outer function's ClosureFeedbackCellArray and links it to the new JSFunction.This creates a runtime chain:
Outer JSFunction -> Outer FeedbackVector -> ClosureFeedbackCellArray -> Inner FeedbackCell -> Inner FeedbackVector (once allocated).
JSDispatchHandleFeedbackCell in ClosureFeedbackCellArray is allocated a JSDispatchHandle (initially pointing to CompileLazy).CreateClosure copies the JSDispatchHandle from the FeedbackCell into the new JSFunction.JSDispatchTable entry.FeedbackCell State TransitionsV8 tracks the number of closures created from a site using the Map of the FeedbackCell:
NoClosuresCellMap -> OneClosureCellMap -> ManyClosuresCellMap
These help decide when to allocate a FeedbackVector and manage specialized code.Consider the following code:
// Top-level script
function outer() {
function inner() {
return 42;
}
return inner;
}
outer(); // Call to outer
Here is what the object graph looks like after the top-level script has been executed and outer() has been called, but before inner() is called.
digraph G {
rankdir=TB;
newrank=true;
node [shape=record];
edge [];
subgraph cluster_static {
label="Static Structure (SFIs)";
TL_SFI [label="Top-level Script (SFI)"];
Outer_SFI [label="outer (SFI)"];
Inner_SFI [label="inner (SFI)"];
TL_SFI -> Outer_SFI [label="via Bytecode's\nConstant Pool"];
Outer_SFI -> Inner_SFI [label="via Bytecode's\nConstant Pool"];
}
subgraph cluster_dynamic {
label="Dynamic Instances (Runtime)";
TL_JSF [label="Top-level Script (JSFunction)"];
TL_FC [label="FeedbackCell"];
CC_TL [label="CompileScript"];
TL_FV [label="FeedbackVector"];
Outer_FC [label="FeedbackCell\n(for outer)"];
CC_Outer [label="CreateClosure\n(Outer)"];
Outer_JSF [label="outer (JSFunction)"];
Outer_FV [label="FeedbackVector"];
Inner_FC [label="FeedbackCell\n(for inner)"];
CC_Inner [label="CreateClosure\n(Inner)"];
Inner_JSF [label="inner (JSFunction)"];
Inner_FV [label="FeedbackVector\nor Undefined"];
TL_FC -> TL_JSF [style=invis];
TL_FC -> CC_TL [label="consumed by", style=dashed];
CC_TL -> TL_JSF [label="creates", style=dashed];
TL_JSF -> TL_FC;
TL_FC -> TL_FV;
TL_FV -> Outer_FC [label="contains"];
Outer_FC -> Outer_JSF [style=invis];
Outer_FC -> CC_Outer [label="consumed by", style=dashed];
CC_Outer -> Outer_JSF [label="creates", style=dashed];
Outer_JSF -> Outer_FC;
Outer_FC -> Outer_FV;
Outer_FV -> Inner_FC [label="contains"];
Inner_FC -> Inner_JSF [style=invis];
Inner_FC -> CC_Inner [label="consumed by", style=dashed];
CC_Inner -> Inner_JSF [label="creates", style=dashed];
Inner_JSF -> Inner_FC;
Inner_FC -> Inner_FV;
}
TL_SFI -> CC_TL [label="consumed by", style=dashed];
Outer_SFI -> CC_Outer [label="consumed by", style=dashed];
Inner_SFI -> CC_Inner [label="consumed by", style=dashed];
TL_JSF -> TL_SFI;
Outer_JSF -> Outer_SFI;
Inner_JSF -> Inner_SFI;
TL_JSF -> Outer_JSF [style=invis];
Outer_JSF -> Inner_JSF [style=invis];
{ rank="same"; TL_SFI; TL_FC; }
{ rank="same"; Outer_SFI; Outer_FC; }
{ rank="same"; Inner_SFI; Inner_FC; }
}
This graph shows how ScopeInfo (static) and Context (dynamic) are linked.
digraph G {
rankdir=TB;
newrank=true;
node [shape=record];
edge [];
subgraph cluster_static {
label="Static Structure (Scopes)";
TL_SFI [label="Top-level Script (SFI)"];
TL_SI [label="Top-level ScopeInfo"];
Outer_SFI [label="outer (SFI)"];
Outer_SI [label="outer ScopeInfo"];
Inner_SFI [label="inner (SFI)"];
Inner_SI [label="inner ScopeInfo"];
TL_SFI -> TL_SI;
Outer_SFI -> Outer_SI [constraint=false];
Inner_SFI -> Inner_SI [constraint=false];
Inner_SI -> Outer_SI -> TL_SI [label="parent"];
}
subgraph cluster_dynamic {
label="Dynamic Instances (Contexts)";
TL_JSF [label="Top-level Script (JSFunction)"];
TL_Ctx [label="Top-level Context"];
Outer_JSF [label="outer (JSFunction)"];
Outer_Ctx [label="outer Context"];
Inner_JSF [label="inner (JSFunction)"];
Inner_Ctx [label="inner Context"];
TL_JSF -> TL_Ctx;
Outer_JSF -> Outer_Ctx [constraint=false];
Inner_JSF -> Inner_Ctx [constraint=false];
Inner_Ctx -> Outer_Ctx -> TL_Ctx [label="parent"];
}
TL_Ctx -> TL_SI [constraint=false];
Outer_Ctx -> Outer_SI [constraint=false];
Inner_Ctx -> Inner_SI [constraint=false];
TL_JSF -> TL_SFI [constraint=false];
Outer_JSF -> Outer_SFI [constraint=false];
Inner_JSF -> Inner_SFI [constraint=false];
TL_SFI -> Outer_SFI -> Inner_SFI [style=invis];
TL_JSF -> Outer_JSF -> Inner_JSF [style=invis];
TL_SI -> Outer_SI -> Inner_SI [style=invis];
TL_Ctx -> Outer_Ctx -> Inner_Ctx [style=invis];
}
JSFunction::CreateAndAttachFeedbackVectorJSFunction::EnsureClosureFeedbackCellArrayFastNewClosure (Builtin)