def<\/span>\s+(\w+)/g,
+ 'def $1',
+ )
+ .replace(/\b(\d+)\b/g, '$1');
+ }
+
+ 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
+ ? `
+
+
+
+ ${TIMINGS.sampleIntervalDefault}ms
+
+ `
+ : "";
+
+ panel.innerHTML = `
+
+
+
+
+
+
+ ${sampleIntervalHtml}
+
+
+
+ 0ms
+
+ `;
+
+ 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();
+ }
+})();
diff --git a/Doc/conf.py b/Doc/conf.py
index a4275835059efa..5b2dba423ae8fd 100644
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -33,6 +33,7 @@
'issue_role',
'lexers',
'misc_news',
+ 'profiling_trace',
'pydoc_topics',
'pyspecific',
'sphinx.ext.coverage',
diff --git a/Doc/library/profiling-sampling-visualization.html b/Doc/library/profiling-sampling-visualization.html
new file mode 100644
index 00000000000000..0cbd0f2374deaa
--- /dev/null
+++ b/Doc/library/profiling-sampling-visualization.html
@@ -0,0 +1 @@
+
diff --git a/Doc/library/profiling.sampling.rst b/Doc/library/profiling.sampling.rst
index a05adf8c3da20e..d179069cfed5f9 100644
--- a/Doc/library/profiling.sampling.rst
+++ b/Doc/library/profiling.sampling.rst
@@ -44,6 +44,23 @@ of samples over a profiling session, Tachyon constructs an accurate statistical
estimate of where time is spent. The more samples collected, the
more precise this estimate becomes.
+.. only:: html
+
+ The following interactive visualization demonstrates how sampling profiling
+ works. Press **Play** to watch a Python program execute, and observe how the
+ profiler periodically captures snapshots of the call stack. Adjust the
+ **sample interval** to see how sampling frequency affects the results.
+
+ .. raw:: html
+ :file: profiling-sampling-visualization.html
+
+.. only:: not html
+
+ .. note::
+
+ An interactive visualization of sampling profiling is available in the
+ HTML version of this documentation.
+
How time is estimated
---------------------
diff --git a/Doc/tools/extensions/profiling_trace.py b/Doc/tools/extensions/profiling_trace.py
new file mode 100644
index 00000000000000..7185ef351ddc7f
--- /dev/null
+++ b/Doc/tools/extensions/profiling_trace.py
@@ -0,0 +1,166 @@
+"""
+Sphinx extension to generate profiler trace data during docs build.
+
+This extension executes a demo Python program with sys.settrace() to capture
+the execution trace and injects it into the profiling visualization JS file.
+"""
+
+import json
+import re
+import sys
+from io import StringIO
+from pathlib import Path
+
+from sphinx.errors import ExtensionError
+
+DEMO_SOURCE = """\
+def add(a, b):
+ return a + b
+
+def multiply(x, y):
+ result = 0
+ for i in range(y):
+ result = add(result, x)
+ return result
+
+def calculate(a, b):
+ sum_val = add(a, b)
+ product = multiply(a, b)
+ return sum_val + product
+
+def main():
+ result = calculate(3, 4)
+ print(f"Result: {result}")
+
+main()
+"""
+
+PLACEHOLDER = "/* PROFILING_TRACE_DATA */null"
+
+
+def generate_trace(source: str) -> list[dict]:
+ """
+ Execute the source code with tracing enabled and capture execution events.
+ """
+ trace_events = []
+ timestamp = [0]
+ timestamp_step = 50
+ tracing_active = [False]
+ pending_line = [None]
+
+ def tracer(frame, event, arg):
+ if frame.f_code.co_filename != '':
+ return tracer
+
+ func_name = frame.f_code.co_name
+ lineno = frame.f_lineno
+
+ if event == 'line' and not tracing_active[0]:
+ pending_line[0] = {'type': 'line', 'line': lineno}
+ return tracer
+
+ # Start tracing only once main() is called
+ if event == 'call' and func_name == 'main':
+ tracing_active[0] = True
+ # Emit the buffered line event (the main() call line) at ts=0
+ if pending_line[0]:
+ pending_line[0]['ts'] = 0
+ trace_events.append(pending_line[0])
+ pending_line[0] = None
+ timestamp[0] = timestamp_step
+
+ # Skip events until we've entered main()
+ if not tracing_active[0]:
+ return tracer
+
+ if event == 'call':
+ trace_events.append({
+ 'type': 'call',
+ 'func': func_name,
+ 'line': lineno,
+ 'ts': timestamp[0],
+ })
+ elif event == 'line':
+ trace_events.append({
+ 'type': 'line',
+ 'line': lineno,
+ 'ts': timestamp[0],
+ })
+ elif event == 'return':
+ try:
+ value = arg if arg is None else repr(arg)
+ except Exception:
+ value = ''
+ trace_events.append({
+ 'type': 'return',
+ 'func': func_name,
+ 'ts': timestamp[0],
+ 'value': value,
+ })
+
+ if func_name == 'main':
+ tracing_active[0] = False
+
+ timestamp[0] += timestamp_step
+ return tracer
+
+ # Suppress print output during tracing
+ old_stdout = sys.stdout
+ sys.stdout = StringIO()
+
+ old_trace = sys.gettrace()
+ sys.settrace(tracer)
+ try:
+ code = compile(source, '', 'exec')
+ exec(code, {'__name__': '__main__'})
+ finally:
+ sys.settrace(old_trace)
+ sys.stdout = old_stdout
+
+ return trace_events
+
+
+def inject_trace(app, exception):
+ if exception:
+ return
+
+ js_path = (
+ Path(app.outdir) / '_static' / 'profiling-sampling-visualization.js'
+ )
+
+ if not js_path.exists():
+ return
+
+ trace = generate_trace(DEMO_SOURCE)
+
+ demo_data = {'source': DEMO_SOURCE.rstrip(), 'trace': trace, 'samples': []}
+
+ demo_json = json.dumps(demo_data, indent=2)
+ content = js_path.read_text(encoding='utf-8')
+
+ pattern = r"(const DEMO_SIMPLE\s*=\s*/\* PROFILING_TRACE_DATA \*/)[^;]+;"
+
+ if re.search(pattern, content):
+ content = re.sub(
+ pattern, lambda m: f"{m.group(1)} {demo_json};", content
+ )
+ js_path.write_text(content, encoding='utf-8')
+ print(
+ f"profiling_trace: Injected {len(trace)} trace events into {js_path.name}"
+ )
+ else:
+ raise ExtensionError(
+ f"profiling_trace: Placeholder pattern not found in {js_path.name}"
+ )
+
+
+def setup(app):
+ app.connect('build-finished', inject_trace)
+ app.add_js_file('profiling-sampling-visualization.js')
+ app.add_css_file('profiling-sampling-visualization.css')
+
+ return {
+ 'version': '1.0',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }