% js-engines-performance.tex — JS engine pipelines (V8, JSC, Hermes), tiered JIT and
% deoptimisation, hidden classes + inline caches, elements kinds, tracing GC vs ARC,
% leaks, weak references, and how to measure. Senior-interview level.
% Source: own knowledge (no repo sources). Versions only where sure.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/runtime/js-engines-performance.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=runtime kind=concept level=deep platform=web new=no round=typescript-2026-09-24 topic=performance,memory
% @tags: v8, javascriptcore, hermes, jit-tiers, turbofan, deoptimization, hidden-classes, inline-caches, elements-kinds, generational-gc, weakref, heap-snapshot
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{TSSheet}{
  morekeywords={import,from,export,default,const,let,type,interface,extends,function,
    return,async,await,if,else,switch,case,new,true,false,null,undefined,typeof,keyof,
    as,satisfies,never,string,number,boolean,void,declare,namespace,readonly,delete,this,for},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/},
  morestring=[b]", morestring=[b]', morestring=[b]`}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm, minimum width=21mm},
  hot2/.style={sb, draw=sheetOrange, fill=sheetOrange!10},
  top/.style={sb, draw=sheetGreen!70!black, fill=sheetGreen!10},
  mp/.style={box, font=\tiny\ttfamily, inner sep=1.5pt, minimum height=4mm, minimum width=11mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  pt/.style={font=\bfseries\small, anchor=west},
  deo/.style={->, thick, draw=sheetRed, dashed},
}

\begin{document}

\sheettitle{JS engines — JIT tiers, shapes, inline caches, GC}{runtime · memo}

\oneliner{An engine parses to \textbf{bytecode}, \textbf{interprets} it while collecting
\textbf{type feedback}, and JIT-compiles hot code in \textbf{tiers} that \textbf{speculate} on
the shapes it saw — a failed guard \textbf{deoptimises} back to the interpreter. Memory is a
\textbf{tracing, generational GC}: reachability, not reference counts.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  \draw[sheetGrey!40] (9.35,3.0) -- (9.35,-0.75);
  \node[pt] at (0,2.95) {\textcolor{sheetBlue}{1} the tier ladder (up = hot + stable feedback)};
  % V8
  \node[lbl, font=\scriptsize\bfseries] at (1.55,2.55) {V8 (Chrome, Node, Deno)};
  \node[sb] (v0) at (1.55,0.0) {\textbf{Ignition} interpreter};
  \node[sb] (v1) at (1.55,0.65) {\textbf{Sparkplug} baseline};
  \node[hot2] (v2) at (1.55,1.3) {\textbf{Maglev} mid-tier opt};
  \node[top] (v3) at (1.55,1.95) {\textbf{TurboFan} top opt};
  \draw[flow] (v0) -- (v1); \draw[flow] (v1) -- (v2); \draw[flow] (v2) -- (v3);
  \draw[deo] (v3.west) to[out=190, in=170] node[lbl, left, text=sheetRed]{deopt} (v0.west);
  \draw[deo] (v2.west) to[out=195, in=165] (v0.west);
  % JSC
  \node[lbl, font=\scriptsize\bfseries] at (4.6,2.55) {JavaScriptCore (Safari, Bun)};
  \node[sb] (j0) at (4.6,0.0) {\textbf{LLInt} interpreter};
  \node[sb] (j1) at (4.6,0.65) {\textbf{Baseline} JIT};
  \node[hot2] (j2) at (4.6,1.3) {\textbf{DFG} optimising};
  \node[top] (j3) at (4.6,1.95) {\textbf{FTL} (B3 backend)};
  \draw[flow] (j0) -- (j1); \draw[flow] (j1) -- (j2); \draw[flow] (j2) -- (j3);
  \draw[deo] (j3.east) to[out=-10, in=10] node[lbl, right, text=sheetRed]{OSR exit} (j0.east);
  % Hermes
  \node[lbl, font=\scriptsize\bfseries] at (7.75,2.55) {Hermes (React Native)};
  \node[sb, draw=sheetGrey, fill=white, dashed] (h1) at (7.75,1.8) {\texttt{hermesc} at \textbf{build}\\JS $\to$ \texttt{.hbc} bytecode};
  \node[sb] (h0) at (7.75,0.45) {interpreter runs \texttt{.hbc}\\\textbf{no JIT}, no parse at start};
  \draw[flow] (h1) -- node[lbl, right]{ship in app} (h0);
  \node[lbl, text=sheetGreen!50!black] at (7.75,-0.2) {fast start, small heap};
  % base
  \node[lbl, anchor=west] at (0,-0.6) {all: source $\to$ (lazy pre-parse) $\to$ AST $\to$ bytecode + feedback vectors};
  % ── right: shapes + ICs ──
  \node[pt] at (9.45,2.95) {\textcolor{sheetBlue}{2} shapes (V8 ``maps'') + inline caches};
  \node[mp] (m0) at (10.1,2.25) {M0 \{\}};
  \node[mp] (m1) at (11.85,2.25) {M1 \{x\}};
  \node[mp] (m2) at (13.75,2.25) {M2 \{x,y\}};
  \node[mp] (m3) at (11.85,1.5) {M3 \{y\}};
  \node[mp] (m4) at (13.75,1.5) {M4 \{y,x\}};
  \draw[flow] (m0) -- node[lbl, above]{+x} (m1); \draw[flow] (m1) -- node[lbl, above]{+y} (m2);
  \draw[flow] (m0.south) |- node[lbl, pos=0.75, above]{+y} (m3); \draw[flow] (m3) -- node[lbl, above]{+x} (m4);
  \node[lbl, text=sheetRed, anchor=west] at (14.45,1.88) {same keys,\\different\\shape};
  \node[mp, draw=sheetRed, fill=sheetRed!7, minimum width=16mm] (dc) at (15.5,2.6) {dictionary};
  \draw[deo] (m2.north) |- node[lbl, pos=0.7, above]{\texttt{delete}} (dc.west);
  \node[lbl, anchor=west] at (9.45,0.95) {IC at \texttt{o.x} remembers (shape $\to$ offset):};
  \node[top, minimum width=19mm] (i1) at (10.55,0.35) {mono: 1 shape};
  \node[hot2, minimum width=19mm] (i2) at (12.7,0.35) {poly: 2--4};
  \node[sb, minimum width=21mm, draw=sheetRed, fill=sheetRed!7] (i3) at (14.95,0.35) {mega: $>$4, hash};
  \draw[flow] (i1) -- (i2); \draw[flow] (i2) -- (i3);
  \node[lbl, anchor=west] at (9.45,-0.25) {mono = one compare + load at a fixed offset;};
  \node[lbl, anchor=west] at (9.45,-0.5) {mega = global stub-cache lookup, little inlining};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works — the pipeline}
\begin{itemize}\raggedright
  \item \textbf{Lazy parsing}: inner functions only pre-parsed until first call. Each call
        site / property access records \textbf{type feedback} in a feedback vector.
  \item \textbf{Tiers} trade compile time for speed: Sparkplug (Chrome 91) copies bytecode to
        machine code without optimising; Maglev (Chrome 117) is a fast SSA optimiser; TurboFan
        inlines, removes checks, unboxes numbers — \emph{speculatively}. Long loops tier up
        mid-run via \textbf{OSR} (on-stack replacement).
  \item \textbf{Deopt}: optimised code guards its assumptions (map check, ``is Smi'', bounds);
        a failure rebuilds the interpreter frame and continues there; repeated deopts $\to$
        no more optimising. Spot: \texttt{node --trace-opt --trace-deopt} (\emph{wrong map},
        \emph{not a Smi}).
  \item \textbf{Hermes}: best TTI and memory on phones, lower peak CPU than a JIT. An in-app
        JSC on iOS gets \textbf{no JIT} either (no writable+executable pages without Apple's
        entitlement); Safari/WKWebView JIT in a separate process.
  \item \textbf{Shapes}: same keys in the same \textbf{order} $\Rightarrow$ one hidden class;
        a key added later = a transition; \texttt{delete} (non-last key) usually $\to$
        \textbf{dictionary mode}. Field types generalise too (Smi $\to$ double $\to$ tagged).
  \item \textbf{Elements kinds} (one-way lattice): \texttt{PACKED\_SMI} $\to$
        \texttt{PACKED\_DOUBLE} $\to$ \texttt{PACKED\_ELEMENTS}, each can turn
        \texttt{HOLEY\_*}. \texttt{new Array(n)}, writing past the end, \texttt{-0}/\texttt{NaN}
        into ints degrade it, for good. Smi = 31-bit int (pointer compression). Hot
        numeric data: typed arrays.
\end{itemize}

\section{Example — shape-friendly vs not}
\begin{lstlisting}[language=TSSheet]
class P { constructor(x, y) { this.x = x; this.y = y; } } // 1
const a = { x: 1, y: 2 }, b = { y: 2, x: 1 }; // 2 shapes
const getX = (o) => o.x;          // IC here
getX(a); getX(b);                 // polymorphic now
a.z = 3;                          // new transition M2 -> +z
delete a.x;                       // dictionary: slow props
const arr = [1, 2, 3];            // PACKED_SMI
arr.push(1.5);                    // PACKED_DOUBLE (for good)
arr[10] = 0;                      // HOLEY_DOUBLE
\end{lstlisting}

\section{Garbage collection}
\begin{itemize}\raggedright
  \item \textbf{Tracing}: live = reachable from roots (stack, globals, handles). Cycles die
        together — no \texttt{weak} needed, unlike \textbf{ARC} (deterministic, cycles leak).
        Price: no \texttt{deinit} timing, pauses, headroom.
  \item \textbf{Generational} (V8 Orinoco): most objects die young. Young gen =
        \textbf{scavenger} (semi-space copy, parallel; survivors promoted); old gen =
        \textbf{mark-sweep-compact}, marking \textbf{incremental + concurrent} (write barriers),
        sweeping concurrent/lazy. Hermes: \textbf{Hades}, mostly concurrent old gen.
  \item \textbf{JS leaks = unwanted reachability}: listeners/subscriptions never removed
        (RN: no \texttt{useEffect} cleanup), \texttt{setInterval}, unbounded \texttt{Map}
        caches, globals, \textbf{detached DOM} held by JS, closures — closures of one scope
        share \emph{one} context, so a tiny callback can pin a huge sibling capture.
  \item \textbf{Weak} (ES2015/2021): \texttt{WeakMap}/\texttt{WeakSet} — keys held weakly
        (ephemeron), not iterable: per-object side tables. \texttt{WeakRef.deref()} may be
        \texttt{undefined};
        \texttt{FinalizationRegistry} callbacks may \emph{never} run — cleanup hints, not logic.
\end{itemize}

\section{Measuring}
\begin{itemize}\raggedright
  \item \textbf{Heap snapshot} (DevTools Memory / \texttt{node --inspect} /
        \texttt{v8.writeHeapSnapshot()}): shallow vs \textbf{retained} size, the
        \textbf{retainers} path (who keeps it alive) $\approx$ Xcode Memory Graph. Leak hunt:
        snapshot, repeat the action, snapshot, \textbf{Comparison} view; filter
        ``Detached''.
  \item \textbf{Allocation timeline / sampling}: who allocates, what survives.
        \texttt{process.memoryUsage()}: \texttt{heapUsed} vs \texttt{rss} vs
        \texttt{external} (Buffers). CPU: \texttt{--cpu-prof}, Performance panel $\approx$
        Time Profiler; time with \texttt{performance.now()}.
\end{itemize}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{Microbenchmarks lie: dead code eliminated, no warm-up (timing the interpreter),
        monomorphic in the bench but not in prod, GC in one run.}
  \trap{``\texttt{try/catch} / \texttt{arguments} kill optimisation'' — Crankshaft-era folklore;
        TurboFan handles them. Measure before believing.}
  \trap{\texttt{delete obj.k} frees nothing and slows the object; set \texttt{undefined} or
        use a \texttt{Map} for dynamic keys.}
  \trap{``JS can't leak, it has a GC'' — it leaks by reachability.}
\end{itemize}

\section{Remember}
\emph{Same keys, same order, same types — and let go of what you subscribed to.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item What is a deopt? — a failed speculation guard; execution resumes in the interpreter.
  \item Why is Hermes fast without a JIT? — no parse/compile at startup, small heap, AOT bytecode.
  \item Mono vs mega? — fixed-offset load vs hashed lookup.
  \item Find a leak? — 3 snapshots, comparison view, follow retainers to the root.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} js-event-loop · node-runtime ·
rn-architecture (Hermes, JSI) · rn-performance · arc-ownership · Instruments}

\end{document}
