--- /dev/null
+/**
+ * Sampling Profiler Visualization - Scoped CSS
+ */
+
+.sampling-profiler-viz {
+ /* Match docs background colors */
+ --bg-page: #ffffff;
+ --bg-panel: #ffffff;
+ --bg-subtle: #f8f8f8;
+ --bg-code: #f8f8f8;
+
+ /* Match docs border style */
+ --border-color: #e1e4e8;
+ --border-accent: #3776ab;
+
+ /* Match docs text colors */
+ --text-primary: #0d0d0d;
+ --text-secondary: #505050;
+ --text-muted: #6e6e6e;
+ --text-code: #333333;
+
+ /* Accent colors */
+ --color-python-blue: #306998;
+ --color-green: #388e3c;
+ --color-orange: #e65100;
+ --color-purple: #7b1fa2;
+ --color-red: #c62828;
+ --color-teal: #00897b;
+ --color-yellow: #d4a910;
+ --color-highlight: #fff9e6;
+
+ --radius-lg: 8px;
+ --radius-md: 6px;
+ --radius-sm: 4px;
+
+ /* Lighter shadows to match docs style */
+ --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
+
+ --container-height: 520px;
+ --code-panel-width: 320px;
+
+ /* Reset for isolation */
+ font-family: var(--font-ui);
+ line-height: 1.5;
+ font-weight: 400;
+ color: var(--text-primary);
+ background-color: var(--bg-page);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Layout */
+ position: relative;
+ width: 100%;
+ max-width: 920px;
+ height: var(--container-height);
+ display: grid;
+ grid-template-columns: var(--code-panel-width) 1fr;
+ margin: 24px auto;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow-card);
+ border: 1px solid var(--border-color);
+ background: var(--bg-panel);
+ /* Prevent any DOM changes inside from affecting page scroll */
+ contain: strict;
+}
+
+.sampling-profiler-viz * {
+ box-sizing: border-box;
+}
+
+/* Code Panel - Left Column */
+.sampling-profiler-viz #code-panel {
+ background: var(--bg-panel);
+ border-right: 1px solid var(--border-color);
+ overflow-y: auto;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ line-height: 1.6;
+ display: flex;
+ flex-direction: column;
+}
+
+.sampling-profiler-viz #code-panel .code-panel-title {
+ padding: 12px 16px;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-muted);
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-code);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ flex-shrink: 0;
+}
+
+.sampling-profiler-viz #code-panel .code-container {
+ margin: 0;
+ padding: 12px 0;
+ overflow-x: auto;
+ flex: 1;
+}
+
+.sampling-profiler-viz #code-panel .line {
+ display: flex;
+ padding: 1px 0;
+ min-height: 20px;
+ transition: background-color 0.1s ease;
+}
+
+.sampling-profiler-viz #code-panel .line-number {
+ color: var(--text-muted);
+ min-width: 40px;
+ text-align: right;
+ padding-right: 12px;
+ padding-left: 12px;
+ user-select: none;
+ flex-shrink: 0;
+ font-size: 11px;
+}
+
+.sampling-profiler-viz #code-panel .line-content {
+ flex: 1;
+ color: var(--text-code);
+ padding-right: 12px;
+ white-space: pre;
+}
+
+.sampling-profiler-viz #code-panel .line.highlighted {
+ background: var(--color-highlight);
+ border-left: 3px solid var(--color-yellow);
+}
+
+.sampling-profiler-viz #code-panel .line.highlighted .line-number {
+ color: var(--color-yellow);
+ padding-left: 9px;
+}
+
+.sampling-profiler-viz #code-panel .line.highlighted .line-content {
+ font-weight: 600;
+}
+
+/* Python Syntax Highlighting */
+.sampling-profiler-viz #code-panel .keyword {
+ color: var(--color-red);
+ font-weight: 600;
+}
+
+.sampling-profiler-viz #code-panel .function {
+ color: var(--color-purple);
+ font-weight: 600;
+}
+
+.sampling-profiler-viz #code-panel .number {
+ color: var(--color-python-blue);
+}
+
+.sampling-profiler-viz #code-panel .string {
+ color: #032f62;
+}
+
+.sampling-profiler-viz #code-panel .comment {
+ color: #6a737d;
+ font-style: italic;
+}
+
+.sampling-profiler-viz #code-panel .builtin {
+ color: var(--color-python-blue);
+}
+
+/* Visualization Column - Right Side */
+.sampling-profiler-viz .viz-column {
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-subtle);
+ overflow: hidden;
+}
+
+/* Stack Section */
+.sampling-profiler-viz .stack-section {
+ padding: 12px 16px;
+ flex: 1;
+ min-height: 150px;
+ overflow-y: auto;
+}
+
+.sampling-profiler-viz .stack-section-title {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-bottom: 10px;
+}
+
+.sampling-profiler-viz .stack-visualization {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-height: 80px;
+}
+
+/* Stack Frames - Vertical Layout */
+.sampling-profiler-viz .stack-frame {
+ position: relative;
+ width: 100%;
+ height: 32px;
+ cursor: pointer;
+ contain: layout style paint;
+ opacity: 0;
+ transform: translateY(-10px);
+}
+
+.sampling-profiler-viz .stack-frame.visible {
+ opacity: 1;
+ transform: translateY(0);
+ transition:
+ opacity 0.3s ease,
+ transform 0.3s ease;
+}
+
+.sampling-profiler-viz .stack-frame-bg {
+ position: absolute;
+ inset: 0;
+ border-radius: var(--radius-sm);
+ transition: opacity 0.15s;
+}
+
+.sampling-profiler-viz .stack-frame-text {
+ position: absolute;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ font: 500 11px var(--font-mono);
+ color: white;
+ pointer-events: none;
+}
+
+.sampling-profiler-viz .stack-frame-flash {
+ position: absolute;
+ inset: 0;
+ background: white;
+ border-radius: var(--radius-sm);
+ opacity: 0;
+ pointer-events: none;
+}
+
+.sampling-profiler-viz .stack-frame:hover .stack-frame-bg {
+ opacity: 0.85;
+}
+
+/* Flying frames */
+.sampling-profiler-viz .stack-frame.flying {
+ pointer-events: none;
+ z-index: 1000;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: auto;
+ height: 32px;
+ opacity: 1;
+}
+
+/* Flying stack clone */
+.stack-visualization.flying-clone {
+ transform-origin: center center;
+ will-change: transform, opacity;
+}
+
+/* Sampling Panel */
+.sampling-profiler-viz .sampling-panel {
+ margin: 0 16px 12px 16px;
+ background: var(--bg-panel);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+ height: 200px;
+ /* Lock font size to prevent Sphinx responsive scaling */
+ font-size: 12px;
+}
+
+.sampling-profiler-viz .sampling-header {
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border-color);
+ flex-shrink: 0;
+}
+
+.sampling-profiler-viz .sampling-title {
+ font: 600 10px var(--font-mono);
+ color: var(--text-primary);
+ margin: 0 0 3px 0;
+}
+
+.sampling-profiler-viz .sampling-stats {
+ font: 400 9px var(--font-mono);
+ color: var(--text-secondary);
+ display: flex;
+ gap: 12px;
+}
+
+.sampling-profiler-viz .sampling-stats .missed {
+ color: var(--color-red);
+}
+
+.sampling-profiler-viz .sampling-bars {
+ flex: 1;
+ padding: 10px 12px;
+ overflow-y: auto;
+}
+
+.sampling-profiler-viz .sampling-bar-row {
+ display: flex;
+ align-items: center;
+ height: 22px;
+ gap: 8px;
+}
+
+.sampling-profiler-viz .bar-label {
+ font: 500 8px var(--font-mono);
+ color: var(--text-primary);
+ flex-shrink: 0;
+ width: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.sampling-profiler-viz .bar-container {
+ flex: 1;
+ height: 12px;
+ background: var(--border-color);
+ border-radius: 3px;
+ position: relative;
+ overflow: hidden;
+}
+
+.sampling-profiler-viz .bar-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.2s ease;
+ min-width: 2px;
+}
+
+.sampling-profiler-viz .bar-percent {
+ font: 500 8px var(--font-mono);
+ color: var(--text-secondary);
+ width: 36px;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+/* Impact effect circle */
+.impact-circle {
+ position: fixed;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background: var(--color-teal);
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ z-index: 2000;
+}
+
+/* Control Panel - Integrated */
+.sampling-profiler-viz #control-panel {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ background: var(--bg-panel);
+ border-top: 1px solid var(--border-color);
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+.sampling-profiler-viz .control-group {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+}
+
+.sampling-profiler-viz .control-group label {
+ font-size: 10px;
+ color: var(--text-muted);
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.sampling-profiler-viz .control-btn {
+ background: var(--bg-panel);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+ padding: 6px 10px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.15s ease;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.sampling-profiler-viz .control-btn:hover {
+ background: var(--bg-subtle);
+ border-color: var(--text-muted);
+}
+
+.sampling-profiler-viz .control-btn:active {
+ transform: scale(0.98);
+}
+
+.sampling-profiler-viz .control-btn.active {
+ background: var(--color-python-blue);
+ color: white;
+ border-color: var(--color-python-blue);
+}
+
+.sampling-profiler-viz .control-btn.active:hover {
+ background: #2f6493;
+}
+
+/* Timeline Scrubber */
+.sampling-profiler-viz .timeline-scrubber {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 160px;
+}
+
+.sampling-profiler-viz #timeline-scrubber {
+ flex: 1;
+ height: 5px;
+ border-radius: 3px;
+ background: var(--border-color);
+ outline: none;
+ appearance: none;
+ -webkit-appearance: none;
+ cursor: pointer;
+ min-width: 60px;
+}
+
+.sampling-profiler-viz #timeline-scrubber::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--color-python-blue);
+ cursor: pointer;
+ transition: transform 0.15s;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+}
+
+.sampling-profiler-viz #timeline-scrubber::-webkit-slider-thumb:hover {
+ transform: scale(1.15);
+}
+
+.sampling-profiler-viz #timeline-scrubber::-moz-range-thumb {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--color-python-blue);
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+}
+
+.sampling-profiler-viz #time-display {
+ font: 500 10px var(--font-mono);
+ color: var(--text-secondary);
+ min-width: 90px;
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+}
+
+/* Sample Interval Slider */
+.sampling-profiler-viz #sample-interval {
+ width: 80px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--border-color);
+ outline: none;
+ appearance: none;
+ -webkit-appearance: none;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.sampling-profiler-viz #sample-interval::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--color-teal);
+ cursor: pointer;
+ transition: transform 0.15s;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.sampling-profiler-viz #sample-interval::-webkit-slider-thumb:hover {
+ transform: scale(1.15);
+}
+
+.sampling-profiler-viz #sample-interval::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--color-teal);
+ cursor: pointer;
+ border: none;
+}
+
+.sampling-profiler-viz #interval-display {
+ font: 500 9px var(--font-mono);
+ color: var(--text-secondary);
+ min-width: 40px;
+ font-variant-numeric: tabular-nums;
+}
+
+/* Flash overlay */
+.sampling-profiler-viz .flash-overlay {
+ position: absolute;
+ inset: 0;
+ background: white;
+ pointer-events: none;
+ opacity: 0;
+}
+
+/* Performance optimizations */
+.sampling-profiler-viz .stack-frame,
+.sampling-profiler-viz .flying-frame,
+.sampling-profiler-viz .sampling-bar-row {
+ will-change: transform, opacity;
+ contain: layout style paint;
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ .sampling-profiler-viz .stack-frame,
+ .sampling-profiler-viz .flying-frame,
+ .sampling-profiler-viz .sampling-bar-row,
+ .impact-circle {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Responsive adjustments for narrower viewports */
+@media (max-width: 800px) {
+ .sampling-profiler-viz {
+ grid-template-columns: 280px 1fr;
+ --container-height: 550px;
+ }
+
+ .sampling-profiler-viz #code-panel {
+ font-size: 11px;
+ }
+
+ .sampling-profiler-viz .control-btn {
+ padding: 5px 8px;
+ font-size: 10px;
+ }
+}
--- /dev/null
+/**
+ * Sampling Profiler Visualization
+ */
+(function () {
+ "use strict";
+
+ // ============================================================================
+ // Configuration
+ // ============================================================================
+
+ const TIMINGS = {
+ sampleIntervalMin: 100,
+ sampleIntervalMax: 500,
+ sampleIntervalDefault: 200,
+ sampleToFlame: 600,
+ defaultSpeed: 0.05,
+ };
+
+ const LAYOUT = { frameSpacing: 6 };
+
+ // Function name to color mapping
+ const FUNCTION_COLORS = {
+ main: "#306998",
+ fibonacci: "#D4A910",
+ add: "#E65100",
+ multiply: "#7B1FA2",
+ calculate: "#D4A910",
+ };
+ const DEFAULT_FUNCTION_COLOR = "#306998";
+
+ // Easing functions - cubic-bezier approximations
+ const EASING_MAP = {
+ linear: "linear",
+ easeOutQuad: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
+ easeOutCubic: "cubic-bezier(0.215, 0.61, 0.355, 1)",
+ };
+
+ function getFunctionColor(funcName) {
+ return FUNCTION_COLORS[funcName] || DEFAULT_FUNCTION_COLOR;
+ }
+
+ // ============================================================================
+ // Animation Manager
+ // ============================================================================
+
+ class AnimationManager {
+ constructor() {
+ this.activeAnimations = new Set();
+ }
+
+ to(element, props, duration, easing = "easeOutQuad", onComplete = null) {
+ this.killAnimationsOf(element);
+
+ const cssEasing = EASING_MAP[easing] || EASING_MAP.easeOutQuad;
+
+ const transformProps = {};
+ const otherProps = {};
+
+ for (const [key, value] of Object.entries(props)) {
+ if (key === "position") {
+ if (typeof value.x === "number") transformProps.x = value.x;
+ if (typeof value.y === "number") transformProps.y = value.y;
+ } else if (key === "x" || key === "y") {
+ transformProps[key] = value;
+ } else if (key === "scale") {
+ transformProps.scale = value;
+ } else if (key === "alpha" || key === "opacity") {
+ otherProps.opacity = value;
+ } else {
+ otherProps[key] = value;
+ }
+ }
+
+ const computedStyle = getComputedStyle(element);
+ const matrix = new DOMMatrix(computedStyle.transform);
+ const currentScale = Math.sqrt(
+ matrix.m11 * matrix.m11 + matrix.m21 * matrix.m21,
+ );
+
+ transformProps.x ??= matrix.m41;
+ transformProps.y ??= matrix.m42;
+ transformProps.scale ??= currentScale;
+
+ const initialTransform = this._buildTransformString(
+ matrix.m41,
+ matrix.m42,
+ currentScale,
+ );
+
+ const finalTransform = this._buildTransformString(
+ transformProps.x,
+ transformProps.y,
+ transformProps.scale,
+ );
+
+ const initialKeyframe = { transform: initialTransform };
+ const finalKeyframe = { transform: finalTransform };
+
+ for (const [key, value] of Object.entries(otherProps)) {
+ const currentVal =
+ key === "opacity"
+ ? element.style.opacity || computedStyle.opacity
+ : element.style[key];
+ initialKeyframe[key] = currentVal;
+ finalKeyframe[key] = value;
+ }
+
+ const animation = element.animate([initialKeyframe, finalKeyframe], {
+ duration,
+ easing: cssEasing,
+ fill: "forwards",
+ });
+
+ this.activeAnimations.add(animation);
+ animation.onfinish = () => {
+ this.activeAnimations.delete(animation);
+ element.style.transform = finalTransform;
+ for (const [key, value] of Object.entries(finalKeyframe)) {
+ if (key !== "transform") {
+ element.style[key] = typeof value === "number" ? `${value}` : value;
+ }
+ }
+ if (onComplete) onComplete();
+ };
+
+ return animation;
+ }
+
+ killAnimationsOf(element) {
+ element.getAnimations().forEach((animation) => animation.cancel());
+ this.activeAnimations.forEach((animation) => {
+ if (animation.effect && animation.effect.target === element) {
+ animation.cancel();
+ this.activeAnimations.delete(animation);
+ }
+ });
+ }
+
+ _buildTransformString(x, y, scale = 1) {
+ return `translate(${x}px, ${y}px) scale(${scale})`;
+ }
+ }
+
+ const anim = new AnimationManager();
+
+ // ============================================================================
+ // Execution Trace Model
+ // ============================================================================
+
+ class ExecutionEvent {
+ constructor(
+ type,
+ functionName,
+ lineno,
+ timestamp,
+ args = null,
+ value = null,
+ ) {
+ this.type = type;
+ this.functionName = functionName;
+ this.lineno = lineno;
+ this.timestamp = timestamp;
+ this.args = args;
+ this.value = value;
+ }
+ }
+
+ class ExecutionTrace {
+ constructor(source, events) {
+ this.source = source;
+ this.events = events.map(
+ (e) =>
+ new ExecutionEvent(e.type, e.func, e.line, e.ts, e.args, e.value),
+ );
+ this.duration = events.length > 0 ? events[events.length - 1].ts : 0;
+ }
+
+ getEventsUntil(timestamp) {
+ return this.events.filter((e) => e.timestamp <= timestamp);
+ }
+
+ getStackAt(timestamp) {
+ const stack = [];
+ const events = this.getEventsUntil(timestamp);
+
+ for (const event of events) {
+ if (event.type === "call") {
+ stack.push({
+ func: event.functionName,
+ line: event.lineno,
+ args: event.args,
+ });
+ } else if (event.type === "return") {
+ stack.pop();
+ } else if (event.type === "line") {
+ if (stack.length > 0) {
+ stack[stack.length - 1].line = event.lineno;
+ }
+ }
+ }
+ return stack;
+ }
+
+ getNextEvent(timestamp) {
+ return this.events.find((e) => e.timestamp > timestamp);
+ }
+
+ getSourceLines() {
+ return this.source.split("\n");
+ }
+ }
+
+ // ============================================================================
+ // Demo Data
+ // ============================================================================
+
+ // This placeholder is replaced by the profiling_trace Sphinx extension
+ // during the documentation build with dynamically generated trace data.
+ const DEMO_SIMPLE = /* PROFILING_TRACE_DATA */ null;
+
+ // ============================================================================
+ // Code Panel Component
+ // ============================================================================
+
+ class CodePanel {
+ constructor(source) {
+ this.source = source;
+ this.currentLine = null;
+
+ this.element = document.createElement("div");
+ this.element.id = "code-panel";
+
+ const title = document.createElement("div");
+ title.className = "code-panel-title";
+ title.textContent = "source code";
+ this.element.appendChild(title);
+
+ this.codeContainer = document.createElement("pre");
+ this.codeContainer.className = "code-container";
+ this.element.appendChild(this.codeContainer);
+
+ this._renderSource();
+ }
+
+ updateSource(source) {
+ this.source = source;
+ this.codeContainer.innerHTML = "";
+ this._renderSource();
+ this.currentLine = null;
+ }
+
+ _renderSource() {
+ const lines = this.source.split("\n");
+
+ lines.forEach((line, index) => {
+ const lineNumber = index + 1;
+ const lineDiv = document.createElement("div");
+ lineDiv.className = "line";
+ lineDiv.dataset.line = lineNumber;
+
+ const lineNumSpan = document.createElement("span");
+ lineNumSpan.className = "line-number";
+ lineNumSpan.textContent = lineNumber;
+ lineDiv.appendChild(lineNumSpan);
+
+ const codeSpan = document.createElement("span");
+ codeSpan.className = "line-content";
+ codeSpan.innerHTML = this._highlightSyntax(line);
+ lineDiv.appendChild(codeSpan);
+
+ this.codeContainer.appendChild(lineDiv);
+ });
+ }
+
+ _highlightSyntax(line) {
+ return line
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/(f?"[^"]*"|f?'[^']*')/g, '<span class="string">$1</span>')
+ .replace(/(#.*$)/g, '<span class="comment">$1</span>')
+ .replace(
+ /\b(def|if|elif|else|return|for|in|range|print|__name__|__main__)\b/g,
+ '<span class="keyword">$1</span>',
+ )
+ .replace(
+ /<span class="keyword">def<\/span>\s+(\w+)/g,
+ '<span class="keyword">def</span> <span class="function">$1</span>',
+ )
+ .replace(/\b(\d+)\b/g, '<span class="number">$1</span>');
+ }
+
+ highlightLine(lineNumber) {
+ if (this.currentLine === lineNumber) return;
+
+ if (this.currentLine !== null) {
+ const prevLine = this.codeContainer.querySelector(
+ `[data-line="${this.currentLine}"]`,
+ );
+ if (prevLine) prevLine.classList.remove("highlighted");
+ }
+
+ if (lineNumber === null || lineNumber === undefined) {
+ this.currentLine = null;
+ return;
+ }
+
+ this.currentLine = lineNumber;
+ const newLine = this.codeContainer.querySelector(
+ `[data-line="${lineNumber}"]`,
+ );
+ if (newLine) {
+ newLine.classList.add("highlighted");
+ }
+ }
+
+ reset() {
+ this.highlightLine(null);
+ this.codeContainer.scrollTop = 0;
+ }
+
+ destroy() {
+ this.element.remove();
+ }
+ }
+
+ // ============================================================================
+ // Stack Frame Component
+ // ============================================================================
+
+ class DOMStackFrame {
+ constructor(functionName, lineno, args = null) {
+ this.functionName = functionName;
+ this.lineno = lineno;
+ this.args = args;
+ this.isActive = false;
+ this.color = getFunctionColor(functionName);
+
+ this.element = document.createElement("div");
+ this.element.className = "stack-frame";
+ this.element.dataset.function = functionName;
+
+ this.bgElement = document.createElement("div");
+ this.bgElement.className = "stack-frame-bg";
+ this.bgElement.style.backgroundColor = this.color;
+ this.element.appendChild(this.bgElement);
+
+ this.textElement = document.createElement("span");
+ this.textElement.className = "stack-frame-text";
+ this.textElement.textContent = functionName;
+ this.element.appendChild(this.textElement);
+
+ this.flashElement = document.createElement("div");
+ this.flashElement.className = "stack-frame-flash";
+ this.element.appendChild(this.flashElement);
+
+ this.element.addEventListener("pointerover", this._onHover.bind(this));
+ this.element.addEventListener("pointerout", this._onHoverOut.bind(this));
+ }
+
+ destroy() {
+ this.element.parentNode?.removeChild(this.element);
+ }
+
+ updateLine(lineno) {
+ this.lineno = lineno;
+ this.textElement.textContent = this.functionName;
+ }
+
+ setActive(isActive) {
+ if (this.isActive === isActive) return;
+ this.isActive = isActive;
+ this.bgElement.style.opacity = isActive ? "1.0" : "0.9";
+ }
+
+ _onHover() {
+ this.bgElement.style.opacity = "0.8";
+ }
+
+ _onHoverOut() {
+ this.bgElement.style.opacity = this.isActive ? "1.0" : "0.9";
+ }
+
+ flash(duration = 150) {
+ this.flashElement.animate([{ opacity: 1 }, { opacity: 0 }], {
+ duration,
+ easing: "ease-out",
+ });
+ }
+
+ getPosition() {
+ const rect = this.element.getBoundingClientRect();
+ return { x: rect.left, y: rect.top };
+ }
+ }
+
+ // ============================================================================
+ // Stack Visualization Component
+ // ============================================================================
+
+ class DOMStackVisualization {
+ constructor() {
+ this.frames = [];
+ this.frameSpacing = LAYOUT.frameSpacing;
+
+ this.element = document.createElement("div");
+ this.element.className = "stack-visualization";
+ }
+
+ processEvent(event) {
+ if (event.type === "call") {
+ this.pushFrame(event.functionName, event.lineno, event.args);
+ } else if (event.type === "return") {
+ this.popFrame();
+ } else if (event.type === "line") {
+ this.updateTopFrameLine(event.lineno);
+ }
+ }
+
+ updateTopFrameLine(lineno) {
+ if (this.frames.length > 0) {
+ this.frames[this.frames.length - 1].updateLine(lineno);
+ }
+ }
+
+ pushFrame(functionName, lineno, args = null) {
+ if (this.frames.length > 0) {
+ this.frames[this.frames.length - 1].setActive(false);
+ }
+
+ const frame = new DOMStackFrame(functionName, lineno, args);
+ frame.setActive(true);
+ this.element.appendChild(frame.element);
+ this.frames.push(frame);
+
+ requestAnimationFrame(() => {
+ frame.element.classList.add("visible");
+ });
+ }
+
+ popFrame() {
+ if (this.frames.length === 0) return;
+
+ const frame = this.frames.pop();
+ frame.element.classList.remove("visible");
+ setTimeout(() => frame.destroy(), 300);
+
+ if (this.frames.length > 0) {
+ this.frames[this.frames.length - 1].setActive(true);
+ }
+ }
+
+ clear() {
+ this.frames.forEach((frame) => frame.destroy());
+ this.frames = [];
+ this.element.innerHTML = "";
+ }
+
+ flashAll() {
+ this.frames.forEach((frame) => frame.flash());
+ }
+
+ createStackClone(container) {
+ const clone = this.element.cloneNode(false);
+ clone.className = "stack-visualization flying-clone";
+
+ const elementRect = this.element.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+
+ // Position relative to container since contain: strict makes position:fixed relative to container
+ clone.style.position = "absolute";
+ clone.style.left = elementRect.left - containerRect.left + "px";
+ clone.style.top = elementRect.top - containerRect.top + "px";
+ clone.style.width = elementRect.width + "px";
+ clone.style.pointerEvents = "none";
+ clone.style.zIndex = "1000";
+
+ this.frames.forEach((frame) => {
+ const frameClone = frame.element.cloneNode(true);
+ frameClone.classList.add("visible");
+ frameClone.style.opacity = "1";
+ frameClone.style.transform = "translateY(0)";
+ frameClone.style.transition = "none";
+ clone.appendChild(frameClone);
+ });
+
+ container.appendChild(clone);
+ return clone;
+ }
+
+ updateToMatch(targetStack) {
+ while (this.frames.length > targetStack.length) {
+ this.popFrame();
+ }
+
+ targetStack.forEach(({ func, line, args }, index) => {
+ if (index < this.frames.length) {
+ const frame = this.frames[index];
+ if (frame.functionName !== func) {
+ frame.updateLine(line);
+ }
+ frame.setActive(index === targetStack.length - 1);
+ } else {
+ this.pushFrame(func, line, args);
+ }
+ });
+
+ if (this.frames.length > 0) {
+ this.frames[this.frames.length - 1].setActive(true);
+ }
+ }
+ }
+
+ // ============================================================================
+ // Sampling Panel Component
+ // ============================================================================
+
+ class DOMSamplingPanel {
+ constructor() {
+ this.samples = [];
+ this.functionCounts = {};
+ this.totalSamples = 0;
+ this.sampleInterval = TIMINGS.sampleIntervalDefault;
+ this.groundTruthFunctions = new Set();
+ this.bars = {};
+
+ this.element = document.createElement("div");
+ this.element.className = "sampling-panel";
+
+ const header = document.createElement("div");
+ header.className = "sampling-header";
+
+ const title = document.createElement("h3");
+ title.className = "sampling-title";
+ title.textContent = "Sampling Profiler";
+ header.appendChild(title);
+
+ const stats = document.createElement("div");
+ stats.className = "sampling-stats";
+
+ this.sampleCountEl = document.createElement("span");
+ this.sampleCountEl.textContent = "Samples: 0";
+ stats.appendChild(this.sampleCountEl);
+
+ this.intervalEl = document.createElement("span");
+ this.intervalEl.textContent = `Interval: ${this.sampleInterval}ms`;
+ stats.appendChild(this.intervalEl);
+
+ this.missedFunctionsEl = document.createElement("span");
+ this.missedFunctionsEl.className = "missed";
+ stats.appendChild(this.missedFunctionsEl);
+
+ header.appendChild(stats);
+ this.element.appendChild(header);
+
+ this.barsContainer = document.createElement("div");
+ this.barsContainer.className = "sampling-bars";
+ this.element.appendChild(this.barsContainer);
+ }
+
+ setSampleInterval(interval) {
+ this.sampleInterval = interval;
+ this.intervalEl.textContent = `Interval: ${interval}ms`;
+ }
+
+ setGroundTruth(allFunctions) {
+ this.groundTruthFunctions = new Set(allFunctions);
+ this._updateMissedCount();
+ }
+
+ addSample(stack) {
+ this.totalSamples++;
+ this.sampleCountEl.textContent = `Samples: ${this.totalSamples}`;
+
+ stack.forEach((frame) => {
+ const funcName = frame.func;
+ this.functionCounts[funcName] =
+ (this.functionCounts[funcName] || 0) + 1;
+ });
+
+ this._updateBars();
+ this._updateMissedCount();
+ }
+
+ reset() {
+ this.samples = [];
+ this.functionCounts = {};
+ this.totalSamples = 0;
+ this.sampleCountEl.textContent = "Samples: 0";
+ this.missedFunctionsEl.textContent = "";
+ this.barsContainer.innerHTML = "";
+ this.bars = {};
+ }
+
+ _updateMissedCount() {
+ if (this.groundTruthFunctions.size === 0) return;
+
+ const capturedFunctions = new Set(Object.keys(this.functionCounts));
+ const notYetSeen = [...this.groundTruthFunctions].filter(
+ (f) => !capturedFunctions.has(f),
+ );
+
+ if (notYetSeen.length > 0) {
+ this.missedFunctionsEl.textContent = `Not yet seen: ${notYetSeen.length}`;
+ this.missedFunctionsEl.classList.add("missed");
+ this.missedFunctionsEl.style.color = "";
+ } else if (this.totalSamples > 0) {
+ this.missedFunctionsEl.textContent = "All captured!";
+ this.missedFunctionsEl.classList.remove("missed");
+ this.missedFunctionsEl.style.color = "var(--color-green)";
+ } else {
+ this.missedFunctionsEl.textContent = "";
+ }
+ }
+
+ _updateBars() {
+ const sorted = Object.entries(this.functionCounts).sort(
+ (a, b) => b[1] - a[1],
+ );
+
+ sorted.forEach(([funcName, count], index) => {
+ const percentage =
+ this.totalSamples > 0 ? count / this.totalSamples : 0;
+
+ if (!this.bars[funcName]) {
+ const row = this._createBarRow(funcName);
+ this.barsContainer.appendChild(row);
+ this.bars[funcName] = row;
+ }
+
+ const row = this.bars[funcName];
+ const barFill = row.querySelector(".bar-fill");
+ barFill.style.width = `${percentage * 100}%`;
+
+ const percentEl = row.querySelector(".bar-percent");
+ percentEl.textContent = `${(percentage * 100).toFixed(0)}%`;
+
+ const currentIndex = Array.from(this.barsContainer.children).indexOf(
+ row,
+ );
+ if (currentIndex !== index) {
+ this.barsContainer.insertBefore(
+ row,
+ this.barsContainer.children[index],
+ );
+ }
+ });
+ }
+
+ _createBarRow(funcName) {
+ const row = document.createElement("div");
+ row.className = "sampling-bar-row";
+ row.dataset.function = funcName;
+
+ const label = document.createElement("span");
+ label.className = "bar-label";
+ label.textContent = funcName;
+ row.appendChild(label);
+
+ const barContainer = document.createElement("div");
+ barContainer.className = "bar-container";
+
+ const barFill = document.createElement("div");
+ barFill.className = "bar-fill";
+ barFill.style.backgroundColor = getFunctionColor(funcName);
+ barContainer.appendChild(barFill);
+
+ row.appendChild(barContainer);
+
+ const percent = document.createElement("span");
+ percent.className = "bar-percent";
+ percent.textContent = "0%";
+ row.appendChild(percent);
+
+ return row;
+ }
+
+ getTargetPosition() {
+ const rect = this.barsContainer.getBoundingClientRect();
+ return { x: rect.left + rect.width / 2, y: rect.top + 50 };
+ }
+
+ showImpactEffect(position) {
+ const impact = document.createElement("div");
+ impact.className = "impact-circle";
+ impact.style.position = "fixed";
+ impact.style.left = `${position.x}px`;
+ impact.style.top = `${position.y}px`;
+
+ // Append to barsContainer parent to avoid triggering scroll
+ this.element.appendChild(impact);
+
+ impact.animate(
+ [
+ { transform: "translate(-50%, -50%) scale(1)", opacity: 0.6 },
+ { transform: "translate(-50%, -50%) scale(4)", opacity: 0 },
+ ],
+ {
+ duration: 300,
+ easing: "ease-out",
+ },
+ ).onfinish = () => impact.remove();
+ }
+ }
+
+ // ============================================================================
+ // Control Panel Component
+ // ============================================================================
+
+ class ControlPanel {
+ constructor(
+ container,
+ onPlay,
+ onPause,
+ onReset,
+ onSpeedChange,
+ onSeek,
+ onStep,
+ onSampleIntervalChange = null,
+ ) {
+ this.container = container;
+ this.onPlay = onPlay;
+ this.onPause = onPause;
+ this.onReset = onReset;
+ this.onSpeedChange = onSpeedChange;
+ this.onSeek = onSeek;
+ this.onStep = onStep;
+ this.onSampleIntervalChange = onSampleIntervalChange;
+
+ this.isPlaying = false;
+ this.speed = TIMINGS.defaultSpeed;
+
+ this._createControls();
+ }
+
+ _createControls() {
+ const panel = document.createElement("div");
+ panel.id = "control-panel";
+
+ const sampleIntervalHtml = this.onSampleIntervalChange
+ ? `
+ <div class="control-group">
+ <label>Sample interval:</label>
+ <input type="range" id="sample-interval"
+ min="${TIMINGS.sampleIntervalMin}"
+ max="${TIMINGS.sampleIntervalMax}"
+ value="${TIMINGS.sampleIntervalDefault}"
+ step="100"
+ aria-label="Sample interval in milliseconds">
+ <span id="interval-display">${TIMINGS.sampleIntervalDefault}ms</span>
+ </div>
+ `
+ : "";
+
+ panel.innerHTML = `
+ <div class="control-group">
+ <button id="play-pause-btn" class="control-btn" aria-label="Play animation">▶ Play</button>
+ <button id="reset-btn" class="control-btn" aria-label="Reset visualization to beginning">↻ Reset</button>
+ <button id="step-btn" class="control-btn" aria-label="Step to next event">→ Step</button>
+ </div>
+
+ ${sampleIntervalHtml}
+
+ <div class="control-group timeline-scrubber">
+ <input type="range" id="timeline-scrubber" min="0" max="100" value="0" step="0.1" aria-label="Timeline position">
+ <span id="time-display">0ms</span>
+ </div>
+ `;
+
+ this.container.appendChild(panel);
+
+ this.playPauseBtn = panel.querySelector("#play-pause-btn");
+ this.resetBtn = panel.querySelector("#reset-btn");
+ this.stepBtn = panel.querySelector("#step-btn");
+ this.scrubber = panel.querySelector("#timeline-scrubber");
+ this.timeDisplay = panel.querySelector("#time-display");
+
+ this.playPauseBtn.addEventListener("click", () =>
+ this._togglePlayPause(),
+ );
+ this.resetBtn.addEventListener("click", () => this._handleReset());
+ this.stepBtn.addEventListener("click", () => this._handleStep());
+ this.scrubber.addEventListener("input", (e) => this._handleSeek(e));
+
+ if (this.onSampleIntervalChange) {
+ this.sampleIntervalSlider = panel.querySelector("#sample-interval");
+ this.intervalDisplay = panel.querySelector("#interval-display");
+ this.sampleIntervalSlider.addEventListener("input", (e) =>
+ this._handleSampleIntervalChange(e),
+ );
+ }
+ }
+
+ _handleSampleIntervalChange(e) {
+ const interval = parseInt(e.target.value);
+ this.intervalDisplay.textContent = `${interval}ms`;
+ this.onSampleIntervalChange(interval);
+ }
+
+ _togglePlayPause() {
+ this.isPlaying = !this.isPlaying;
+
+ if (this.isPlaying) {
+ this.playPauseBtn.textContent = "⏸ Pause";
+ this.playPauseBtn.classList.add("active");
+ this.onPlay();
+ } else {
+ this.playPauseBtn.textContent = "▶ Play";
+ this.playPauseBtn.classList.remove("active");
+ this.onPause();
+ }
+ }
+
+ _handleReset() {
+ this.isPlaying = false;
+ this.playPauseBtn.textContent = "▶ Play";
+ this.playPauseBtn.classList.remove("active");
+ this.scrubber.value = 0;
+ this.timeDisplay.textContent = "0ms";
+ this.onReset();
+ }
+
+ _handleStep() {
+ if (this.onStep) this.onStep();
+ }
+
+ _handleSeek(e) {
+ const percentage = parseFloat(e.target.value);
+ this.onSeek(percentage / 100);
+ }
+
+ updateTimeDisplay(currentTime, totalTime) {
+ this.timeDisplay.textContent = `${Math.floor(currentTime)}ms / ${Math.floor(totalTime)}ms`;
+ const percentage = (currentTime / totalTime) * 100;
+ this.scrubber.value = percentage;
+ }
+
+ setDuration(duration) {
+ this.duration = duration;
+ }
+
+ pause() {
+ if (this.isPlaying) this._togglePlayPause();
+ }
+
+ destroy() {
+ const panel = this.container.querySelector("#control-panel");
+ if (panel) panel.remove();
+ }
+ }
+
+ // ============================================================================
+ // Visual Effects Manager
+ // ============================================================================
+
+ class VisualEffectsManager {
+ constructor(container) {
+ this.container = container;
+ this.flyingAnimationInProgress = false;
+
+ this.flashOverlay = document.createElement("div");
+ this.flashOverlay.className = "flash-overlay";
+ this.container.appendChild(this.flashOverlay);
+ }
+
+ triggerSamplingEffect(stackViz, samplingPanel, currentTime, trace) {
+ if (this.flyingAnimationInProgress) return;
+
+ const stack = trace.getStackAt(currentTime);
+
+ if (stack.length === 0) {
+ samplingPanel.addSample(stack);
+ return;
+ }
+
+ this.flyingAnimationInProgress = true;
+ stackViz.flashAll();
+
+ const clone = stackViz.createStackClone(this.container);
+ const targetPosition = samplingPanel.getTargetPosition();
+
+ this._animateFlash();
+ this._animateFlyingStack(clone, targetPosition, () => {
+ samplingPanel.showImpactEffect(targetPosition);
+ clone.remove();
+
+ const currentStack = trace.getStackAt(currentTime);
+ samplingPanel.addSample(currentStack);
+ this.flyingAnimationInProgress = false;
+ });
+ }
+
+ _animateFlash() {
+ anim.to(this.flashOverlay, { opacity: 0.1 }, 0).onfinish = () => {
+ anim.to(this.flashOverlay, { opacity: 0 }, 150, "easeOutQuad");
+ };
+ }
+
+ _animateFlyingStack(clone, targetPosition, onComplete) {
+ const containerRect = this.container.getBoundingClientRect();
+ const cloneRect = clone.getBoundingClientRect();
+
+ // Convert viewport coordinates to container-relative
+ const startX = cloneRect.left - containerRect.left + cloneRect.width / 2;
+ const startY = cloneRect.top - containerRect.top + cloneRect.height / 2;
+ const targetX = targetPosition.x - containerRect.left;
+ const targetY = targetPosition.y - containerRect.top;
+
+ const deltaX = targetX - startX;
+ const deltaY = targetY - startY;
+
+ anim.to(
+ clone,
+ {
+ x: deltaX,
+ y: deltaY,
+ scale: 0.3,
+ opacity: 0.6,
+ },
+ TIMINGS.sampleToFlame,
+ "easeOutCubic",
+ onComplete,
+ );
+ }
+ }
+
+ // ============================================================================
+ // Main Visualization Class
+ // ============================================================================
+
+ class SamplingVisualization {
+ constructor(container) {
+ this.container = container;
+
+ this.trace = new ExecutionTrace(DEMO_SIMPLE.source, DEMO_SIMPLE.trace);
+
+ this.currentTime = 0;
+ this.isPlaying = false;
+ this.playbackSpeed = TIMINGS.defaultSpeed;
+ this.eventIndex = 0;
+
+ this.sampleInterval = TIMINGS.sampleIntervalDefault;
+ this.lastSampleTime = 0;
+
+ this._createLayout();
+
+ this.effectsManager = new VisualEffectsManager(this.vizColumn);
+
+ this.lastTime = performance.now();
+ this._animate();
+ }
+
+ _createLayout() {
+ this.codePanel = new CodePanel(this.trace.source);
+ this.container.appendChild(this.codePanel.element);
+
+ this.vizColumn = document.createElement("div");
+ this.vizColumn.className = "viz-column";
+ this.container.appendChild(this.vizColumn);
+
+ const stackSection = document.createElement("div");
+ stackSection.className = "stack-section";
+
+ const stackTitle = document.createElement("div");
+ stackTitle.className = "stack-section-title";
+ stackTitle.textContent = "Call Stack";
+ stackSection.appendChild(stackTitle);
+
+ this.stackViz = new DOMStackVisualization();
+ stackSection.appendChild(this.stackViz.element);
+ this.vizColumn.appendChild(stackSection);
+
+ this.samplingPanel = new DOMSamplingPanel();
+ this.samplingPanel.setGroundTruth(this._getGroundTruthFunctions());
+ this.vizColumn.appendChild(this.samplingPanel.element);
+
+ this.controls = new ControlPanel(
+ this.vizColumn,
+ () => this.play(),
+ () => this.pause(),
+ () => this.reset(),
+ (speed) => this.setSpeed(speed),
+ (progress) => this.seek(progress),
+ () => this.step(),
+ (interval) => this.setSampleInterval(interval),
+ );
+ this.controls.setDuration(this.trace.duration);
+ }
+
+ _getGroundTruthFunctions() {
+ const functions = new Set();
+ this.trace.events.forEach((event) => {
+ if (event.type === "call") {
+ functions.add(event.functionName);
+ }
+ });
+ return [...functions];
+ }
+
+ play() {
+ this.isPlaying = true;
+ }
+
+ pause() {
+ this.isPlaying = false;
+ }
+
+ reset() {
+ this.currentTime = 0;
+ this.eventIndex = 0;
+ this.isPlaying = false;
+ this.lastSampleTime = 0;
+ this.stackViz.clear();
+ this.codePanel.reset();
+ this.samplingPanel.reset();
+ this.controls.updateTimeDisplay(0, this.trace.duration);
+ }
+
+ setSpeed(speed) {
+ this.playbackSpeed = speed;
+ }
+
+ setSampleInterval(interval) {
+ this.sampleInterval = interval;
+ this.samplingPanel.setSampleInterval(interval);
+ }
+
+ seek(progress) {
+ this.currentTime = progress * this.trace.duration;
+ this.eventIndex = 0;
+ this.lastSampleTime = 0;
+ this._rebuildState();
+ }
+
+ step() {
+ this.pause();
+
+ const nextEvent = this.trace.getNextEvent(this.currentTime);
+
+ if (nextEvent) {
+ // Calculate delta to reach next event + epsilon
+ const targetTime = nextEvent.timestamp + 0.1;
+ const delta = targetTime - this.currentTime;
+ if (delta > 0) {
+ this._advanceTime(delta);
+ }
+ }
+ }
+
+ _animate(currentTime = performance.now()) {
+ const deltaTime = currentTime - this.lastTime;
+ this.lastTime = currentTime;
+
+ this.update(deltaTime);
+ requestAnimationFrame((t) => this._animate(t));
+ }
+
+ update(deltaTime) {
+ if (!this.isPlaying) {
+ this.controls.updateTimeDisplay(this.currentTime, this.trace.duration);
+ return;
+ }
+
+ const virtualDelta = deltaTime * this.playbackSpeed;
+ this._advanceTime(virtualDelta);
+ }
+
+ _advanceTime(virtualDelta) {
+ this.currentTime += virtualDelta;
+
+ if (this.currentTime >= this.trace.duration) {
+ this.currentTime = this.trace.duration;
+ this.isPlaying = false;
+ this.controls.pause();
+ }
+
+ while (this.eventIndex < this.trace.events.length) {
+ const event = this.trace.events[this.eventIndex];
+
+ if (event.timestamp > this.currentTime) break;
+
+ this._processEvent(event);
+ this.eventIndex++;
+ }
+
+ this.controls.updateTimeDisplay(this.currentTime, this.trace.duration);
+
+ if (this.currentTime - this.lastSampleTime >= this.sampleInterval) {
+ this._takeSample();
+ this.lastSampleTime = this.currentTime;
+ }
+ }
+
+ _processEvent(event) {
+ this.stackViz.processEvent(event);
+
+ if (event.type === "call") {
+ this.codePanel.highlightLine(event.lineno);
+ } else if (event.type === "return") {
+ const currentStack = this.trace.getStackAt(this.currentTime);
+ if (currentStack.length > 0) {
+ this.codePanel.highlightLine(
+ currentStack[currentStack.length - 1].line,
+ );
+ } else {
+ this.codePanel.highlightLine(null);
+ }
+ } else if (event.type === "line") {
+ this.codePanel.highlightLine(event.lineno);
+ }
+ }
+
+ _takeSample() {
+ this.effectsManager.triggerSamplingEffect(
+ this.stackViz,
+ this.samplingPanel,
+ this.currentTime,
+ this.trace,
+ );
+ }
+
+ _rebuildState() {
+ this.stackViz.clear();
+ this.codePanel.reset();
+ this.samplingPanel.reset();
+
+ for (let t = 0; t < this.currentTime; t += this.sampleInterval) {
+ const stack = this.trace.getStackAt(t);
+ this.samplingPanel.addSample(stack);
+ this.lastSampleTime = t;
+ }
+
+ const stack = this.trace.getStackAt(this.currentTime);
+ this.stackViz.updateToMatch(stack);
+
+ if (stack.length > 0) {
+ this.codePanel.highlightLine(stack[stack.length - 1].line);
+ }
+
+ this.eventIndex = this.trace.getEventsUntil(this.currentTime).length;
+ }
+ }
+
+ // ============================================================================
+ // Initialize
+ // ============================================================================
+
+ function init() {
+ // If trace data hasn't been injected yet (local dev), don't initialize
+ if (!DEMO_SIMPLE) return;
+
+ const appContainer = document.getElementById("sampling-profiler-viz");
+ if (appContainer) {
+ new SamplingVisualization(appContainer);
+ }
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+})();