% swiftui-instrument-hitches-hangs.tex — the Xcode 26 SwiftUI instrument (lanes, the
% Cause & Effect Graph), Xcode 27 additions (Summary of Updates, layout-not-cached
% reasons), Animation Hitches (redesigned in 26), commit vs render hitches, Hangs
% thresholds, hang vs hitch, the Organizer Hitches metric (27).
% Not repeated: the layer/render-server pipeline (swiftui/rendering-pipeline.tex),
% identity + dependency rules (swiftui/swiftui-performance-identity.tex), tool choice
% (ios-swift/instruments-performance.tex).
% Sources (primary, checked 2026-09-25): WWDC25 306 "Optimize SwiftUI performance with
% Instruments" (lanes, colours, node kinds, "Show Cause & Effect Graph", template
% contents, example); Xcode 26 + 27 release notes (Instruments, Organizer); WWDC23 10248
% "Analyze hangs with Instruments" (100 / 250 / 500 ms); Apple doc "Understanding user
% interface responsiveness" (hang vs hitch); research-swift-xcode-2026-09-25.md.
% Not printed (could not confirm): the "Severe Hang" cut-off.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/debugging/swiftui-instrument-hitches-hangs.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=debugging kind=tooling level=deep platform=apple new=yes round=market-2026-09-25 topic=performance,ui,debugging
% @tags: swiftui-instrument, cause-and-effect-graph, attributegraph, long-view-body, hitches, commit-hitch, animation-hitches, hangs, micro-hang, frame-deadline, xcode-organizer, observable
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}

\lstdefinelanguage{SwiftSheet}{
  morekeywords={func,let,var,final,class,private,return,@Observable,@ObservationIgnored},
  alsoletter={@}, sensitive=true, morecomment=[l]{//}, morestring=[b]",
  literate={->}{{\hbox{-}\hbox{>}}}2 {==}{{\hbox{=}\hbox{=}}}2 {??}{{\hbox{?}\hbox{?}}}2}

\tikzset{
  lbl/.style={font=\scriptsize, text=black!75, inner sep=1pt},
  tr/.style={font=\scriptsize\bfseries, anchor=east, inner sep=1pt, align=right},
  ug/.style={draw=none, fill=black!30},
  lg/.style={draw=sheetRed, fill=sheetRed!25, thick},
  sl/.style={draw=sheetGrey, fill=black!4, minimum height=3.4mm, minimum width=6.8mm,
             inner sep=0pt, font=\scriptsize},
  nd/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  mine/.style={nd, draw=sheetBlue, fill=sheetBlue!18},
  sys/.style={nd, draw=sheetGrey, fill=black!6},
  bd/.style={nd, draw=sheetOrange, fill=sheetOrange!14},
  dim/.style={nd, draw=black!25, fill=white, text=black!35, dashed},
  ed/.style={->, thick, draw=sheetGrey},
}

\begin{document}

\sheettitle{SwiftUI instrument · Cause \& Effect · hitches \& hangs}{debugging · memo}

\oneliner{The Xcode 26 \textbf{SwiftUI} instrument times \emph{every} SwiftUI update and
flags the long ones (\textbf{orange / red} = likely to cause a \textbf{hitch} or
\textbf{hang}); its \textbf{Cause \& Effect Graph} shows \emph{why} each body ran, from
the gesture or state change on the left to the view bodies on the right. A body that
overruns the \textbf{frame deadline} = a hitch; a main thread that cannot answer a
\emph{discrete} input for \textbf{250~ms+} = a hang.}

\medskip
\noindent\begin{tikzpicture}[sheet]
  % ── trace mock, 120 Hz: 1 frame = 8.33 ms = 0.83 cm ──
  \def\f{0.83}
  \foreach \k in {0,...,10} \draw[sheetGrey!45, densely dashed] ({2.7+\k*\f},-0.05) -- ({2.7+\k*\f},2.75);
  \node[lbl, text=sheetGrey, anchor=west] at (2.7,2.95) {vsync every 8.33~ms (120~Hz) — each frame's update must finish before its deadline};
  \node[tr] at (2.6,2.45) {Update Groups};
  \node[tr] at (2.6,1.95) {Long View Body};
  \node[tr] at (2.6,1.45) {Time Profiler};
  \node[tr] at (2.6,0.95) {Hitches};
  \node[tr] at (2.6,0.35) {on glass};
  \foreach \k in {0,1,2,6,7,8,9} \fill[ug] ({2.72+\k*\f},2.35) rectangle ({2.95+\k*\f},2.55);
  \fill[lg] ({2.7+3*\f},2.35) rectangle ({2.7+3*\f+2.2},2.55);
  \draw[lg] ({2.7+3*\f},1.85) rectangle ({2.7+3*\f+2.2},2.05);
  \node[lbl, text=sheetRed] at ({2.7+3*\f+1.1},1.95) {\texttt{RowView.body} 22~ms};
  \fill[sheetOrange!45] ({2.7+3*\f},1.35) rectangle ({2.7+3*\f+2.2},1.55);
  \node[lbl] at ({2.7+3*\f+1.1},1.45) {formatter \texttt{init} ×40};
  \foreach \k/\n in {0/1,1/2,2/3,3/4,4/4,5/4,6/5,7/6,8/7,9/8} {
    \node[sl] at ({2.7+\k*\f+\f/2},0.35) {\n};
  }
  \node[sl, draw=sheetRed, fill=sheetRed!20] at ({2.7+4*\f+\f/2},0.35) {4};
  \node[sl, draw=sheetRed, fill=sheetRed!20] at ({2.7+5*\f+\f/2},0.35) {4};
  \fill[sheetRed] ({2.7+4*\f},0.85) rectangle ({2.7+6*\f},1.05);
  \node[lbl, text=sheetRed, anchor=west] at ({2.7+6*\f+0.05},0.95) {hitch: frame 4 shown 3× — 5 is late};
  \draw[sheetRed, very thick] ({2.7+4*\f},1.8) -- ({2.7+4*\f},2.62);
  \node[lbl, text=sheetRed, anchor=west] at ({2.7+4*\f+0.03},2.72) {deadline};
  % ── hang thresholds ruler ──
  \begin{scope}[shift={(11.6,0)}]
    \node[font=\bfseries\scriptsize, anchor=west] at (0,2.45) {Hangs: main run loop unresponsive};
    \fill[sheetGreen!35]  (0,1.3) rectangle (0.8,1.8);
    \fill[sheetOrange!20] (0.8,1.3) rectangle (2.0,1.8);
    \fill[sheetOrange!55] (2.0,1.3) rectangle (4.0,1.8);
    \fill[sheetRed!55]    (4.0,1.3) rectangle (4.95,1.8);
    \node[lbl, align=center] at (0.4,1.55) {instant};
    \node[lbl, align=center] at (1.4,1.55) {felt};
    \node[lbl, align=center] at (3.0,1.55) {\textbf{micro hang}};
    \node[lbl, align=center, text=white] at (4.47,1.55) {\textbf{hang}};
    \foreach \x/\t in {0/0,0.8/100,2.0/250,4.0/500} {
      \draw[sheetGrey] (\x,1.3) -- (\x,1.18); \node[lbl, anchor=north] at (\x,1.17) {\t};
    }
    \node[lbl, anchor=north] at (4.75,1.17) {ms};
    \draw[->, thick, sheetBlue] (2.0,2.2) -- (2.0,1.85);
    \node[lbl, text=sheetBlue, anchor=west] at (2.08,2.05) {tools report from here};
    \node[lbl, align=left, anchor=north west, text width=5.0cm] at (0,0.75)
      {a \textbf{hitch} needs only \emph{one} late frame (8.3 / 16.7~ms, no input needed);
       a \textbf{hang} needs a user \emph{waiting} on a tap.};
  \end{scope}
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}
  \item \textbf{SwiftUI template} (26) = SwiftUI instrument + Time Profiler + Hangs +
        Hitches. Lanes: \textbf{Update Groups} (when SwiftUI is working — CPU busy
        while it is empty $\Rightarrow$ the cost is \emph{outside} SwiftUI) ·
        \textbf{Long View Body Updates} · \textbf{Long Representable Updates}
        (\texttt{UIViewRepresentable} / \texttt{…ControllerRepresentable}) ·
        \textbf{Other Long Updates}. Expanded: View Body / Representable / Other Updates.
  \item \textbf{Drill down}: right-click a red update $\to$ \emph{Set Inspection Range
        and Zoom} $\to$ select the Time Profiler track $\to$ the call tree of \emph{that}
        body only.
  \item \textbf{Detail pane}: every body that ran, grouped by module and view type,
        with counts and durations. 26: \emph{Show View Hierarchy}. 27: a
        \textbf{Summary of Updates} focus action in the View Hierarchy view, and
        \textbf{layout passes} + the reason a layout \emph{was not cached}.
  \item \textbf{Why bodies run} (AttributeGraph): a state change opens a transaction
        and marks its attribute \emph{outdated}; dependents are marked in turn; next
        frame SwiftUI updates only outdated attributes, in order, and a view
        \emph{checks} its inputs before running \texttt{body}.
  \item \textbf{Cause \& Effect Graph}: hover a view name $\to$ arrow $\to$ \emph{Show
        Cause \& Effect Graph}. Reads \textbf{left $\to$ right}; \textbf{blue} = your
        code or your actions. Nodes: \emph{Gesture}, \emph{State Change} (variable +
        host view type), \emph{External Environment} (e.g.\ colour scheme),
        \emph{EnvironmentWriter} (\texttt{.environment}), \emph{View Body Update};
        a \textbf{dimmed} icon = checked, body \emph{not} run. Edges: \emph{update},
        \emph{Creation}.
  \item \textbf{Environment}: a view reading \texttt{@Environment} depends on
        \emph{all} of \texttt{EnvironmentValues} — any change notifies it. Never put
        geometry or timers there.
\end{itemize}

\section{Picture — a Cause \& Effect Graph}
\begin{tikzpicture}[sheet]
  \node[mine, text width=14mm, align=center] (g) at (0,1.2) {Gesture\\tap Favorite};
  \node[mine, text width=18mm, align=center] (s) at (2.55,1.2) {State Change\\\texttt{favorites}};
  \node[sys] (e) at (0,0) {External Env.};
  \node[sys] (w) at (2.55,0) {color scheme};
  \foreach \i/\y in {1/2.1,2/1.55,3/1.0} \node[bd] (b\i) at (5.4,\y) {\texttt{LandmarkRow} body};
  \node[lbl, text=sheetRed] at (5.4,0.62) {… ×40 rows};
  \node[dim] (d) at (5.4,0.1) {\texttt{DetailView} (skipped)};
  \draw[ed, sheetBlue] (g) -- (s);
  \foreach \i in {1,2,3} \draw[ed, sheetOrange] (s.east) -- (b\i.west);
  \node[lbl, text=sheetOrange] at (4.05,1.95) {update};
  \draw[ed] (e) -- (w); \draw[ed, dashed] (w.east) -- (d.west);
  \node[note, anchor=north west, text width=7.6cm] at (-0.9,-0.35)
    {Select the body that ran too often, walk \textbf{left} to its cause. One state change
     fanning out to every row = a dependency on the \emph{whole array}. After the fix the
     same tap reaches \textbf{one} \texttt{LandmarkRow}.};
\end{tikzpicture}

\columnbreak

\section{Hitch vs hang, precisely}
\begin{itemize}
  \item \textbf{Hitch} = a frame on screen too long because the next was not ready.
        \emph{Commit} hitch: your process late (body, layout, decode on main);
        \emph{render} hitch: the render server late\unverified. \textbf{Animation Hitches} (26,
        redesigned): multiple displays, app update intervals, more reliable, less data.
  \item \textbf{Hang} = delay between a \emph{discrete} input and the screen update.
        \textless100~ms feels instant; tools report from \textbf{250~ms} (micro hang);
        \textbf{\textgreater500~ms} is a hang. Lower the Hangs threshold for shorter ones.
  \item \textbf{Organizer} (27): \textbf{Hitches} metric replaces \emph{Scrolling} —
        every animation, not just scroll views.
\end{itemize}

\section{Example — narrow the dependency}
\begin{lstlisting}[language=SwiftSheet]
// BEFORE: every row reads the whole array -> 1 tap, 40 bodies
func isFavorite(_ l: Landmark) -> Bool {
  favorites.landmarks.contains(l) }
// AFTER: each row observes only its own tiny model
@Observable final class RowModel { var isFavorite = false }
@ObservationIgnored private var rows: [Landmark.ID: RowModel] = [:]
func isFavorite(_ l: Landmark) -> Bool { row(for: l).isFavorite }
func toggle(_ l: Landmark) {
  row(for: l).isFavorite.toggle()   // 1 change -> 1 body
  /* …and update favorites.landmarks as before */ }
\end{lstlisting}

\section{Interview traps}
\begin{itemize}
  \trap{\texttt{Self.\_printChanges()} names the \emph{property}, not who changed it
        — the graph shows the chain back to the tap.}
  \trap{Many short bodies can miss the deadline too: the lanes flag \emph{long}
        updates; also read the counts in the summary.}
  \trap{Representable updates are \emph{your} UIKit code in SwiftUI's frame budget.}
  \trap{Hitch $\neq$ hang: no user waiting, still a hitch; a hang with no animation
        running shows no hitch.}
  \trap{Profile on device, Release: Debug SwiftUI is far slower.}
\end{itemize}

\section{Remember}
\textbf{Lane = \emph{which} update was long · Time Profiler = \emph{what} it did ·
graph = \emph{why} it ran.}

\section{Likely questions}
\begin{enumerate}
  \item The four lanes? — Update Groups, Long View Body, Long Representable, Other Long.
  \item Why did this body run? — Cause \& Effect Graph, walk left.
  \item Dimmed node? — checked, inputs unchanged, body skipped.
  \item When is it a hang? — 250~ms reported (micro), 500~ms+ hang.
  \item Commit vs render hitch? — app late vs render server late.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} rendering-pipeline (commit
/ render, frame budget) · swiftui-performance-identity (dependencies, identity) ·
instruments-performance · time-profiler-cpu-deep · concurrency-debugging (blocked
main actor)}

\end{document}
