Skip to content

Commit 205f15f

Browse files
emerybergerclaude
andauthored
Experimental timeline view for stitched Python+native stacks (#1040)
* Experimental timeline view for stitched Python+native stacks Adds a new GUI section beneath the existing flame chart that renders the captured stitched stacks chronologically. x-axis is wallclock time, y-axis is stack depth (icicle, outermost on top). Two summary tracks above the main panel show GC and I/O activity, classified by frame patterns inspired by Chrome / Firefox profiler conventions. Backend: - ScaleneStatistics gains combined_stacks_timeline: a run-length-encoded list of (timestamp, stack_key, count). Consecutive identical stacks coalesce, so a tight loop firing the same stack thousands of times in a row is one entry. Soft cap at 100K runs. - combined_stacks keys move from tagged tuples to frozen dataclasses (PyFrameKey, NativeFrameKey) so the stack-key shape is type-checked end to end. - Native-frame helpers (_is_scalene_handler_frame, _is_cpython_runtime_frame, _trim_native_stack) now operate on a frozen _ResolvedNativeFrame dataclass. The on-wire [module, symbol, ip, offset] list shape only materializes at the JSON output boundary, preserving compatibility with consumers. - Pydantic CombinedStackTimelineEvent and CombinedStackFrame describe the wire shape. GUI: - renderCombinedStacksTimeline() draws the timeline. Each run becomes a vertical strip of stacked colored frames; clicking a [py] frame jumps to its source line. - GC and I/O classifier patterns use (?:\b|_)...(?:_|\b) tokenization so symbols like select_kqueue_control_impl, os_read, _io_FileIO_read, accept4 match correctly. The previous \b...\b patterns missed them because '_' is a regex word character. Async-aware reconstruction: - New ScaleneTracing.should_trace_for_stitched_stack: looser variant that lets asyncio/* and selectors.py through. Used only by the timeline path; line-level CPU/memory accounting still uses the strict should_trace. - process_cpu_sample now starts the f_back walk from the original innermost frame (not the already-walked-back filtered frame), so the full asyncio call chain becomes visible on the timeline. - When a sample lands inside the event loop with coroutines suspended, the timeline emits one synthetic stitched run per suspended task at its await point - mirroring the existing per-line "await %" attribution. The synthetic display name "[await] <func> (<task>)" is recognized by the I/O classifier. Tests: - tests/test_native_stacks.py: end-to-end smoke tests covering the GC/IO classifier and async-blocking attribution. Pure-Python tests for the trim helpers updated to construct _ResolvedNativeFrame; combined_stack tests updated to construct PyFrameKey / NativeFrameKey. - tests/test_async_profiling.py: SuspendedTaskInfo unpacking test updated for the new func_name field. - test/timeline_view_demo.py: standalone demo workload that exercises CPU, sync I/O, forced GC, and asyncio.sleep so all three timeline classifications are visible at once. Full pytest suite passes (397 + 13 skipped); mypy and ruff clean on the scalene package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * GUI: render disclosure triangles as plain text, matching CSrankings The toggle triangles (▶/▼) inherit Bootstrap 5's font-family stack which includes "Apple Color Emoji". On macOS/iOS, Safari can pick up U+25BA (►) from the emoji font and render it as a colored "play button" emoji instead of a plain text triangle, especially in the collapsed (right-pointing) state. The expanded triangle (U+25BC, ▼) doesn't have this problem because no emoji presentation exists for it. The result was a visually mismatched pair when collapsed. Match what CSrankings does: route every toggle through a single .disclosure-triangle CSS class that pins font-family to Helvetica / Arial / sans-serif and sets font-variant-emoji: text. Same effective styling (cursor: pointer; color: blue) as the previous inline styles, but emoji presentation is now suppressed. - gui-elements.ts: add the missing trailing semicolons to RightTriangle / DownTriangle entity strings (matches CSrankings exactly; HTML5 parsers tolerate omission but it's not strictly compliant). - index.html.template: add the .disclosure-triangle CSS rule to the global <style> block. - scalene-gui.ts: replace inline cursor/color styles on the three toggle spans (button-combined-stacks, button-combined-timeline, per-file button-${id}) with class="disclosure-triangle". Verified via Selenium that the rendered toggle now reports fontFamily="Helvetica, Arial, sans-serif" and fontVariantEmoji="text", matching CSrankings exactly (15.84px × 18px). All 397 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop should_trace_for_stitched_stack: synthetic await frames cover it The looser stitched-stack-only filter was added to surface asyncio / selectors frames in the timeline call chain. The subsequent async-aware reconstruction (add_async_await_run + suspended-task snapshot) replaced those frames with one synthetic "[await] <func> (<task>)" entry per suspended task, mirroring the existing "await %" attribution. In default mode the looser filter no longer contributes any frames to the timeline (verified on test/timeline_view_demo.py: 11 synthetic frames, 0 raw asyncio path frames). It only had effect under --no-async, where the user has explicitly opted out of async-aware attribution; serving that minority with truncated <module>+native_leaf stacks is consistent with the flag's intent. Removes: - ScaleneTracing.should_trace_for_stitched_stack and the _is_async_io_stdlib_module helper. - Scalene._should_trace_for_stitched_stack static delegate. - The should_trace_for_stitched_stack kwarg on ScaleneCPUProfiler.process_cpu_sample. - The orig_frame plumbing in process_cpu_sample (revert to main_thread_frame for consistency with add_stack — strict should_trace produces the same Python chain from either start frame). Net -71 lines across 3 files. Full test suite still 397 + 13 skipped; mypy and ruff clean. Demo produces the same 11 [await] synthetic frames as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Multi-frame stitched stacks for synthetic [await] runs Each suspended task's synthetic timeline run was previously a single [await] frame at the innermost suspension point. The user-visible call chain that led up to that await — including any intermediate user-async functions — was missing. Wire the existing ScaleneAsync.walk_await_chain through to add_async_await_run so the synthetic stack mirrors the actual nested coroutine chain at suspend time. Backend - SuspendedTaskInfo gains an optional ``chain`` field — a tuple of (filename, lineno, func_name) tuples, outermost-first (matches the combined_stacks convention). - _poll_suspended_tasks (Strategy A) populates chain via walk_await_chain(coro). - _on_yield (Strategy B / sys.monitoring) populates chain via walk_await_chain(task.get_coro()) at the moment of yield. - add_async_await_run renders chain entries as PyFrameKey frames in the stitched stack, with the [await] marker (and the (Task-N) suffix) applied only to the innermost frame the timeline keeps. Filtering is done first so the marker stays on the deepest user-code frame even when the chain ends in stdlib asyncio frames that should_trace rejects. Falls back to the original single-frame stack when chain is empty. Tests / demo - tests/test_async_profiling.py — SuspendedTaskInfo unpacking test now unpacks the new ``chain`` field (defaults to ``()``). - test/timeline_view_demo.py — async phase now exercises a three-deep user-async chain (_slow_io -> _fetch_one -> _wait_for_data) so the multi-frame synthetic stack is visible in the rendered timeline. End-to-end on the demo: each of the 10 concurrent tasks now produces a 3-frame stack ``_slow_io -> _fetch_one -> [await] _wait_for_data (Task-N)`` with 70 hits each, instead of the prior single-frame collapsed run. Bug fix while in here: is-leaf detection in add_async_await_run was checked against the unfiltered chain. When the deepest frame got filtered out (e.g. an asyncio.sleep frame at the bottom of the chain), the [await] marker and the (Task-N) suffix were lost on the next-deepest frame, and concurrent tasks awaiting at the same line collapsed into one undifferentiated row. Filter first, then index. Full test suite still 397 + 13 skipped; mypy and ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1fd91c4 commit 205f15f

13 files changed

Lines changed: 1533 additions & 157 deletions

scalene/scalene-gui/gui-elements.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export const Lightning = "&#9889;"; // lightning bolt (for optimizing a line)
44
export const Explosion = "&#128165;"; // explosion (for optimizing a region)
55
export const WhiteLightning = `<span style="opacity:0">${Lightning}</span>`; // invisible but same width as lightning bolt
66
export const WhiteExplosion = `<span style="opacity:0">${Explosion}</span>`; // invisible but same width as explosion
7-
export const RightTriangle = "&#9658"; // right-facing triangle symbol (collapsed view)
8-
export const DownTriangle = "&#9660"; // downward-facing triangle symbol (expanded view)
7+
export const RightTriangle = "&#9658;"; // right-facing triangle symbol (collapsed view)
8+
export const DownTriangle = "&#9660;"; // downward-facing triangle symbol (expanded view)
99

1010
// Type for chart parameters
1111
export interface ChartParams {

scalene/scalene-gui/index.html.template

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,22 @@
2222
#vg-tooltip-element {
2323
z-index: 2000;
2424
}
25-
body {
25+
body {
2626
padding: 0 0 90px 0;
27+
}
28+
/* Disclosure-triangle toggle buttons (matches CSrankings .hovertip).
29+
* The default Bootstrap 5 font stack includes Apple Color Emoji, which
30+
* on macOS/iOS will hijack characters like U+25BA (▶) and render them as
31+
* a colored "play button" emoji glyph. Force a plain text font here so
32+
* the triangle renders as a normal text glyph in both collapsed and
33+
* expanded states, matching the look CSrankings uses. */
34+
.disclosure-triangle {
35+
font-family: Helvetica, Arial, sans-serif;
36+
font-variant-emoji: text;
37+
cursor: pointer;
38+
color: blue;
39+
user-select: none;
40+
-webkit-user-select: none;
2741
}
2842
</style>
2943
{% if standalone %}

scalene/scalene-gui/scalene-gui-bundle.js

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2457,8 +2457,10 @@ var ScaleneGUI = (() => {
24572457
refreshGeminiModels: () => refreshGeminiModels,
24582458
refreshOpenAIModels: () => refreshOpenAIModels,
24592459
renderCombinedStacks: () => renderCombinedStacks,
2460+
renderCombinedStacksTimeline: () => renderCombinedStacksTimeline,
24602461
toggleAdvanced: () => toggleAdvanced,
24612462
toggleCombinedStacks: () => toggleCombinedStacks,
2463+
toggleCombinedStacksTimeline: () => toggleCombinedStacksTimeline,
24622464
toggleDisplay: () => toggleDisplay,
24632465
togglePassword: () => togglePassword,
24642466
toggleReduced: () => toggleReduced,
@@ -69969,8 +69971,8 @@ ${toHex(hashedRequest)}`;
6996969971
var Explosion = "&#128165;";
6997069972
var WhiteLightning = `<span style="opacity:0">${Lightning}</span>`;
6997169973
var WhiteExplosion = `<span style="opacity:0">${Explosion}</span>`;
69972-
var RightTriangle = "&#9658";
69973-
var DownTriangle = "&#9660";
69974+
var RightTriangle = "&#9658;";
69975+
var DownTriangle = "&#9660;";
6997469976
function makeTooltip(title2, value3) {
6997569977
const secs = value3 / 100 * globalThis.profile.elapsed_time_sec;
6997669978
return `(${title2}) ` + value3.toFixed(1) + "% [" + time_consumed_str(secs * 1e3) + "]";
@@ -71795,7 +71797,7 @@ ${node.totalHits} hits (${pct}%)`;
7179571797
const containerHeight = depth * FLAME_ROW_HEIGHT;
7179671798
let s2 = `<hr><div class="container-fluid combined-stacks-section">`;
7179771799
s2 += `<p style="margin-bottom: 4px;">`;
71798-
s2 += `<span id="button-combined-stacks" title="Click to show or hide stitched Python+native call stacks." style="cursor: pointer; color: blue;" onClick="toggleCombinedStacks()">${RightTriangle}</span>`;
71800+
s2 += `<span id="button-combined-stacks" class="disclosure-triangle" title="Click to show or hide stitched Python+native call stacks." onClick="toggleCombinedStacks()">${RightTriangle}</span>`;
7179971801
s2 += ` <strong>Combined Python + native call stacks</strong> `;
7180071802
s2 += `<span class="text-muted" style="font-size: 80%;">${stacks.length} stitched stacks, ${totalHits} samples \u2014 hover for details, click a [py] frame to jump to its source line</span>`;
7180171803
s2 += `</p>`;
@@ -71817,6 +71819,223 @@ ${node.totalHits} hits (${pct}%)`;
7181771819
btn.innerHTML = RightTriangle;
7181871820
}
7181971821
}
71822+
var TIMELINE_ROW_HEIGHT = 14;
71823+
var TIMELINE_MIN_LABEL_WIDTH_PX = 40;
71824+
var TIMELINE_TRACK_HEIGHT = 10;
71825+
var TIMELINE_TRACK_GAP = 2;
71826+
var TIMELINE_BUCKETS = 600;
71827+
var GC_NAME_PATTERNS = [
71828+
/(?:\b|_)gc[._]collect(?:_|\b)/i,
71829+
/(?:\b|_)PyGC(?:_|\b)/,
71830+
/(?:\b|_)_PyGC_/,
71831+
/(?:\b|_)gc_collect_main(?:_|\b)/,
71832+
/(?:\b|_)deallocate(?:_|\b)/
71833+
];
71834+
var IO_NAME_PATTERNS = [
71835+
// Synthetic await frame emitted by add_async_await_run when a sample
71836+
// lands inside the event loop with coroutines suspended. Kept first
71837+
// since the literal "[await]" prefix is the cheapest possible match.
71838+
/^\[await\]/,
71839+
/(?:\b|_)read(?:v)?(?:_|\b)/,
71840+
/(?:\b|_)pread(?:v)?(?:_|\b)/,
71841+
/(?:\b|_)write(?:v)?(?:_|\b)/,
71842+
/(?:\b|_)pwrite(?:v)?(?:_|\b)/,
71843+
/(?:\b|_)recv(?:from|msg)?(?:_|\b)/,
71844+
/(?:\b|_)send(?:to|msg)?(?:_|\b)/,
71845+
/(?:\b|_)select(?:_|\b)/,
71846+
/(?:\b|_)epoll(?:_|\b)/,
71847+
/(?:\b|_)kevent(?:_|\b)/,
71848+
/(?:\b|_)kqueue(?:_|\b)/,
71849+
/(?:\b|_)poll(?:_|\b)/,
71850+
/(?:\b|_)accept[0-9]*(?:_|\b)/,
71851+
/(?:\b|_)connect(?:_|\b)/,
71852+
/(?:\b|_)open(?:at|dir)?(?:_|\b)/,
71853+
/(?:\b|_)close(?:_|\b)/,
71854+
/(?:\b|_)fsync(?:_|\b)/,
71855+
/(?:\b|_)fread(?:_|\b)/,
71856+
/(?:\b|_)fwrite(?:_|\b)/,
71857+
/(?:\b|_)lseek(?:_|\b)/
71858+
];
71859+
var IO_FILE_PATTERNS = [
71860+
/\b_io\b/,
71861+
/\bsocket\.py$/,
71862+
/\bsocket\b/,
71863+
/\bselectors\.py$/,
71864+
/\bselector_events\.py$/,
71865+
/\basyncio\b/,
71866+
/\bsubprocess\.py$/
71867+
];
71868+
function classifyFrame(f2) {
71869+
const name4 = f2.display_name ?? "";
71870+
for (const re3 of GC_NAME_PATTERNS) {
71871+
if (re3.test(name4)) return "gc";
71872+
}
71873+
for (const re3 of IO_NAME_PATTERNS) {
71874+
if (re3.test(name4)) return "io";
71875+
}
71876+
if (f2.kind === "py") {
71877+
for (const re3 of IO_FILE_PATTERNS) {
71878+
if (re3.test(f2.filename_or_module ?? "")) return "io";
71879+
}
71880+
}
71881+
return "other";
71882+
}
71883+
function classifyStack(stack2) {
71884+
let gc = false;
71885+
let io = false;
71886+
for (const f2 of stack2) {
71887+
const c4 = classifyFrame(f2);
71888+
if (c4 === "gc") gc = true;
71889+
else if (c4 === "io") io = true;
71890+
if (gc && io) break;
71891+
}
71892+
return { gc, io };
71893+
}
71894+
function timelineColor(name4, kind) {
71895+
let h3 = 0;
71896+
for (let i2 = 0; i2 < name4.length; i2++) {
71897+
h3 = (h3 << 5) - h3 + name4.charCodeAt(i2) | 0;
71898+
}
71899+
const hue2 = Math.abs(h3) % 360;
71900+
if (kind === "py") return `hsl(${hue2}, 45%, 78%)`;
71901+
return `hsl(${(hue2 + 30) % 360}, 70%, 65%)`;
71902+
}
71903+
function buildTimelineRuns(events3, totalElapsedSec) {
71904+
if (events3.length === 0) return { runs: [], totalSec: 0 };
71905+
const runs = [];
71906+
for (let i2 = 0; i2 < events3.length; i2++) {
71907+
const ev = events3[i2];
71908+
const next = events3[i2 + 1];
71909+
let end;
71910+
if (next) {
71911+
end = next.t_sec;
71912+
} else {
71913+
const synthetic = ev.t_sec + ev.count * 1e-3;
71914+
end = Math.max(ev.t_sec, totalElapsedSec || synthetic, synthetic);
71915+
}
71916+
if (end <= ev.t_sec) end = ev.t_sec;
71917+
runs.push({
71918+
startSec: ev.t_sec,
71919+
endSec: end,
71920+
stackIndex: ev.stack_index,
71921+
hits: ev.count
71922+
});
71923+
}
71924+
const totalSec = Math.max(totalElapsedSec, runs[runs.length - 1].endSec) - runs[0].startSec;
71925+
return { runs, totalSec };
71926+
}
71927+
function renderTimelineFrames(runs, stacks, totalSec, startSec) {
71928+
let s2 = "";
71929+
for (const run2 of runs) {
71930+
const stackEntry = stacks[run2.stackIndex];
71931+
if (!stackEntry) continue;
71932+
const frames = stackEntry[0];
71933+
const leftPct = (run2.startSec - startSec) / totalSec * 100;
71934+
const widthPct = (run2.endSec - run2.startSec) / totalSec * 100;
71935+
if (widthPct <= 0) continue;
71936+
const showLabels = widthPct / 100 * TIMELINE_BUCKETS >= TIMELINE_MIN_LABEL_WIDTH_PX / 2;
71937+
for (let depth = 0; depth < frames.length; depth++) {
71938+
const f2 = frames[depth];
71939+
const color5 = timelineColor(f2.display_name, f2.kind);
71940+
const top = depth * TIMELINE_ROW_HEIGHT;
71941+
const tooltip2 = f2.kind === "py" ? `[py] ${f2.display_name}
71942+
${f2.filename_or_module}:${f2.line}
71943+
${run2.startSec.toFixed(3)}s \u2014 ${run2.endSec.toFixed(3)}s (${run2.hits} samples)` : `[native] ${f2.display_name}
71944+
${f2.filename_or_module}
71945+
${run2.startSec.toFixed(3)}s \u2014 ${run2.endSec.toFixed(3)}s (${run2.hits} samples)`;
71946+
const label = showLabels ? escapeHtml(f2.display_name) : "";
71947+
const cursor3 = f2.kind === "py" && f2.line !== null ? "pointer" : "default";
71948+
const clickAttr = f2.kind === "py" && f2.line !== null ? ` onclick="vsNavigate('${escape(f2.filename_or_module)}',${f2.line})"` : "";
71949+
s2 += `<div style="position:absolute;top:${top}px;left:${leftPct.toFixed(4)}%;width:${widthPct.toFixed(4)}%;height:${TIMELINE_ROW_HEIGHT - 1}px;background:${color5};border:1px solid rgba(0,0,0,0.10);overflow:hidden;font-family:monospace;font-size:10px;line-height:${TIMELINE_ROW_HEIGHT - 1}px;padding:0 2px;white-space:nowrap;text-overflow:ellipsis;cursor:${cursor3};box-sizing:border-box;" title="${escapeHtml(tooltip2)}"${clickAttr}>${label}</div>`;
71950+
}
71951+
}
71952+
return s2;
71953+
}
71954+
function renderTimelineTrack(label, color5, runs, classifiedRuns, totalSec, startSec, topPx) {
71955+
let s2 = "";
71956+
s2 += `<div style="position:absolute;left:0;top:${topPx}px;width:60px;height:${TIMELINE_TRACK_HEIGHT}px;font-family:monospace;font-size:10px;line-height:${TIMELINE_TRACK_HEIGHT}px;color:#444;">${label}</div>`;
71957+
s2 += `<div style="position:absolute;left:60px;right:0;top:${topPx}px;height:${TIMELINE_TRACK_HEIGHT}px;background:#f7f7f7;border:1px solid #ddd;box-sizing:border-box;">`;
71958+
for (let i2 = 0; i2 < runs.length; i2++) {
71959+
if (!classifiedRuns[i2]) continue;
71960+
const run2 = runs[i2];
71961+
const leftPct = (run2.startSec - startSec) / totalSec * 100;
71962+
const widthPct = (run2.endSec - run2.startSec) / totalSec * 100;
71963+
if (widthPct <= 0) continue;
71964+
s2 += `<div style="position:absolute;left:${leftPct.toFixed(4)}%;width:${widthPct.toFixed(4)}%;top:0;height:100%;background:${color5};box-sizing:border-box;" title="${escapeHtml(label)} during ${run2.startSec.toFixed(3)}s \u2014 ${run2.endSec.toFixed(3)}s"></div>`;
71965+
}
71966+
s2 += `</div>`;
71967+
return s2;
71968+
}
71969+
function renderCombinedStacksTimeline(prof) {
71970+
const events3 = prof.combined_stacks_timeline ?? [];
71971+
const stacks = prof.combined_stacks ?? [];
71972+
if (events3.length === 0 || stacks.length === 0) return "";
71973+
const elapsed = prof.elapsed_time_sec ?? 0;
71974+
const { runs, totalSec } = buildTimelineRuns(events3, elapsed);
71975+
if (runs.length === 0 || totalSec <= 0) return "";
71976+
const startSec = runs[0].startSec;
71977+
let maxDepth2 = 0;
71978+
const isGcRun = new Array(runs.length).fill(false);
71979+
const isIoRun = new Array(runs.length).fill(false);
71980+
for (let i2 = 0; i2 < runs.length; i2++) {
71981+
const stackEntry = stacks[runs[i2].stackIndex];
71982+
if (!stackEntry) continue;
71983+
const frames = stackEntry[0];
71984+
if (frames.length > maxDepth2) maxDepth2 = frames.length;
71985+
const c4 = classifyStack(frames);
71986+
isGcRun[i2] = c4.gc;
71987+
isIoRun[i2] = c4.io;
71988+
}
71989+
const trackGcTop = 0;
71990+
const trackIoTop = trackGcTop + TIMELINE_TRACK_HEIGHT + TIMELINE_TRACK_GAP;
71991+
const mainTop = trackIoTop + TIMELINE_TRACK_HEIGHT + TIMELINE_TRACK_GAP * 2;
71992+
const mainHeight = Math.max(maxDepth2, 1) * TIMELINE_ROW_HEIGHT;
71993+
const containerHeight = mainTop + mainHeight + 4;
71994+
let s2 = `<hr><div class="container-fluid combined-stacks-timeline-section">`;
71995+
s2 += `<p style="margin-bottom: 4px;">`;
71996+
s2 += `<span id="button-combined-timeline" class="disclosure-triangle" title="Click to show or hide the experimental timeline view." onClick="toggleCombinedStacksTimeline()">${RightTriangle}</span>`;
71997+
s2 += ` <strong>Stitched stack timeline</strong> `;
71998+
s2 += `<span class="badge bg-warning text-dark" style="font-size: 70%; vertical-align: middle;">experimental</span> `;
71999+
s2 += `<span class="text-muted" style="font-size: 80%;">${runs.length} runs over ${totalSec.toFixed(2)}s \u2014 x: time, y: stack depth (outermost on top); GC and I/O tracks shown above</span>`;
72000+
s2 += `</p>`;
72001+
s2 += `<div id="combined-timeline-body" style="display: none;">`;
72002+
s2 += `<div class="combined-stacks-timeline" style="position:relative;width:100%;height:${containerHeight}px;border:1px solid #ccc;background:#f0f0f0;overflow-x:auto;padding:0;">`;
72003+
s2 += renderTimelineTrack(
72004+
"GC",
72005+
"#d62728",
72006+
runs,
72007+
isGcRun,
72008+
totalSec,
72009+
startSec,
72010+
trackGcTop
72011+
);
72012+
s2 += renderTimelineTrack(
72013+
"I/O",
72014+
"#1f77b4",
72015+
runs,
72016+
isIoRun,
72017+
totalSec,
72018+
startSec,
72019+
trackIoTop
72020+
);
72021+
s2 += `<div style="position:absolute;left:60px;right:0;top:${mainTop}px;height:${mainHeight}px;background:#fafafa;border:1px solid #ddd;box-sizing:border-box;">`;
72022+
s2 += renderTimelineFrames(runs, stacks, totalSec, startSec);
72023+
s2 += `</div>`;
72024+
s2 += `</div></div></div>`;
72025+
return s2;
72026+
}
72027+
function toggleCombinedStacksTimeline() {
72028+
const body = document.getElementById("combined-timeline-body");
72029+
const btn = document.getElementById("button-combined-timeline");
72030+
if (!body || !btn) return;
72031+
if (body.style.display === "none") {
72032+
body.style.display = "block";
72033+
btn.innerHTML = DownTriangle;
72034+
} else {
72035+
body.style.display = "none";
72036+
btn.innerHTML = RightTriangle;
72037+
}
72038+
}
7182072039
function toggleDisplay(id2) {
7182172040
const d2 = document.getElementById(`profile-${id2}`);
7182272041
if (d2) {
@@ -72087,7 +72306,7 @@ ${node.totalHits} hits (${pct}%)`;
7208772306
).padWithNonBreakingSpaces(8)})`;
7208872307
}
7208972308
s2 += `</font>`;
72090-
s2 += `<br /><span id="button-${id2}" title="Click to show or hide profile." style="cursor: pointer; color: blue;" onClick="toggleDisplay('${id2}')">`;
72309+
s2 += `<br /><span id="button-${id2}" class="disclosure-triangle" title="Click to show or hide profile." onClick="toggleDisplay('${id2}')">`;
7209172310
s2 += `${triangle}`;
7209272311
s2 += "</span>";
7209372312
s2 += `<code> ${ff[0]}</code>`;
@@ -72205,6 +72424,7 @@ ${node.totalHits} hits (${pct}%)`;
7220572424
files4 = files4.filter((x5) => !excludedFiles.has(x5));
7220672425
s2 += "</div>";
7220772426
s2 += renderCombinedStacks(prof);
72427+
s2 += renderCombinedStacksTimeline(prof);
7220872428
const p2 = document.getElementById("profile");
7220972429
if (p2) {
7221072430
p2.innerHTML = s2;
@@ -72530,6 +72750,7 @@ ${node.totalHits} hits (${pct}%)`;
7253072750
window.expandAll = expandAll;
7253172751
window.toggleDisplay = toggleDisplay;
7253272752
window.toggleCombinedStacks = toggleCombinedStacks;
72753+
window.toggleCombinedStacksTimeline = toggleCombinedStacksTimeline;
7253372754
window.toggleReduced = toggleReduced;
7253472755
window.onFileDisplayModeChange = onFileDisplayModeChange;
7253572756
window.load = load3;

0 commit comments

Comments
 (0)