summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/c/generated_tracepoints.c46
-rw-r--r--internal/flamegraph/nativesvg.go11
-rw-r--r--internal/flamegraph/svgwriter.go16
-rw-r--r--internal/flamegraph/svgwriter_js.go155
-rw-r--r--internal/flamegraph/svgwriter_jscode.go155
-rw-r--r--internal/flamegraph/tool.go4
-rw-r--r--internal/flamegraph/webserver.go108
-rw-r--r--internal/ior.go10
8 files changed, 453 insertions, 52 deletions
diff --git a/internal/c/generated_tracepoints.c b/internal/c/generated_tracepoints.c
index 06f8c39..b4d4e0f 100644
--- a/internal/c/generated_tracepoints.c
+++ b/internal/c/generated_tracepoints.c
@@ -1,7 +1,7 @@
// Code generated - don't change manually!
-/// Ignoring sys_enter_accept sys_exit_accept as possibly not file I/O related
/// Ignoring sys_enter_accept4 sys_exit_accept4 as possibly not file I/O related
+/// Ignoring sys_enter_accept sys_exit_accept as possibly not file I/O related
/// Ignoring sys_enter_acct sys_exit_acct as possibly not file I/O related
/// Ignoring sys_enter_add_key sys_exit_add_key as possibly not file I/O related
/// Ignoring sys_enter_adjtimex sys_exit_adjtimex as possibly not file I/O related
@@ -17,20 +17,20 @@
/// Ignoring sys_enter_clock_gettime sys_exit_clock_gettime as possibly not file I/O related
/// Ignoring sys_enter_clock_nanosleep sys_exit_clock_nanosleep as possibly not file I/O related
/// Ignoring sys_enter_clock_settime sys_exit_clock_settime as possibly not file I/O related
-/// Ignoring sys_enter_clone sys_exit_clone as possibly not file I/O related
/// Ignoring sys_enter_clone3 sys_exit_clone3 as possibly not file I/O related
+/// Ignoring sys_enter_clone sys_exit_clone as possibly not file I/O related
/// Ignoring sys_enter_connect sys_exit_connect as possibly not file I/O related
/// Ignoring sys_enter_delete_module sys_exit_delete_module as possibly not file I/O related
-/// Ignoring sys_enter_epoll_create sys_exit_epoll_create as possibly not file I/O related
/// Ignoring sys_enter_epoll_create1 sys_exit_epoll_create1 as possibly not file I/O related
+/// Ignoring sys_enter_epoll_create sys_exit_epoll_create as possibly not file I/O related
/// Ignoring sys_enter_epoll_ctl sys_exit_epoll_ctl as possibly not file I/O related
-/// Ignoring sys_enter_epoll_pwait sys_exit_epoll_pwait as possibly not file I/O related
/// Ignoring sys_enter_epoll_pwait2 sys_exit_epoll_pwait2 as possibly not file I/O related
+/// Ignoring sys_enter_epoll_pwait sys_exit_epoll_pwait as possibly not file I/O related
/// Ignoring sys_enter_epoll_wait sys_exit_epoll_wait as possibly not file I/O related
-/// Ignoring sys_enter_eventfd sys_exit_eventfd as possibly not file I/O related
/// Ignoring sys_enter_eventfd2 sys_exit_eventfd2 as possibly not file I/O related
-/// Ignoring sys_enter_execve sys_exit_execve as possibly not file I/O related
+/// Ignoring sys_enter_eventfd sys_exit_eventfd as possibly not file I/O related
/// Ignoring sys_enter_execveat sys_exit_execveat as possibly not file I/O related
+/// Ignoring sys_enter_execve sys_exit_execve as possibly not file I/O related
/// Ignoring sys_enter_exit sys_exit_exit as possibly not file I/O related
/// Ignoring sys_enter_exit_group sys_exit_exit_group as possibly not file I/O related
/// Ignoring sys_enter_fanotify_init sys_exit_fanotify_init as possibly not file I/O related
@@ -42,14 +42,13 @@
/// Ignoring sys_enter_futex_wait sys_exit_futex_wait as possibly not file I/O related
/// Ignoring sys_enter_futex_waitv sys_exit_futex_waitv as possibly not file I/O related
/// Ignoring sys_enter_futex_wake sys_exit_futex_wake as possibly not file I/O related
-/// Ignoring sys_enter_get_mempolicy sys_exit_get_mempolicy as possibly not file I/O related
-/// Ignoring sys_enter_get_robust_list sys_exit_get_robust_list as possibly not file I/O related
/// Ignoring sys_enter_getcpu sys_exit_getcpu as possibly not file I/O related
/// Ignoring sys_enter_getegid sys_exit_getegid as possibly not file I/O related
/// Ignoring sys_enter_geteuid sys_exit_geteuid as possibly not file I/O related
/// Ignoring sys_enter_getgid sys_exit_getgid as possibly not file I/O related
/// Ignoring sys_enter_getgroups sys_exit_getgroups as possibly not file I/O related
/// Ignoring sys_enter_getitimer sys_exit_getitimer as possibly not file I/O related
+/// Ignoring sys_enter_get_mempolicy sys_exit_get_mempolicy as possibly not file I/O related
/// Ignoring sys_enter_getpeername sys_exit_getpeername as possibly not file I/O related
/// Ignoring sys_enter_getpgid sys_exit_getpgid as possibly not file I/O related
/// Ignoring sys_enter_getpgrp sys_exit_getpgrp as possibly not file I/O related
@@ -60,6 +59,7 @@
/// Ignoring sys_enter_getresgid sys_exit_getresgid as possibly not file I/O related
/// Ignoring sys_enter_getresuid sys_exit_getresuid as possibly not file I/O related
/// Ignoring sys_enter_getrlimit sys_exit_getrlimit as possibly not file I/O related
+/// Ignoring sys_enter_get_robust_list sys_exit_get_robust_list as possibly not file I/O related
/// Ignoring sys_enter_getrusage sys_exit_getrusage as possibly not file I/O related
/// Ignoring sys_enter_getsid sys_exit_getsid as possibly not file I/O related
/// Ignoring sys_enter_getsockname sys_exit_getsockname as possibly not file I/O related
@@ -69,8 +69,8 @@
/// Ignoring sys_enter_getuid sys_exit_getuid as possibly not file I/O related
/// Ignoring sys_enter_init_module sys_exit_init_module as possibly not file I/O related
/// Ignoring sys_enter_inotify_add_watch sys_exit_inotify_add_watch as possibly not file I/O related
-/// Ignoring sys_enter_inotify_init sys_exit_inotify_init as possibly not file I/O related
/// Ignoring sys_enter_inotify_init1 sys_exit_inotify_init1 as possibly not file I/O related
+/// Ignoring sys_enter_inotify_init sys_exit_inotify_init as possibly not file I/O related
/// Ignoring sys_enter_inotify_rm_watch sys_exit_inotify_rm_watch as possibly not file I/O related
/// Ignoring sys_enter_ioperm sys_exit_ioperm as possibly not file I/O related
/// Ignoring sys_enter_iopl sys_exit_iopl as possibly not file I/O related
@@ -97,11 +97,11 @@
/// Ignoring sys_enter_memfd_secret sys_exit_memfd_secret as possibly not file I/O related
/// Ignoring sys_enter_migrate_pages sys_exit_migrate_pages as possibly not file I/O related
/// Ignoring sys_enter_mincore sys_exit_mincore as possibly not file I/O related
-/// Ignoring sys_enter_mknod sys_exit_mknod as possibly not file I/O related
/// Ignoring sys_enter_mknodat sys_exit_mknodat as possibly not file I/O related
-/// Ignoring sys_enter_mlock sys_exit_mlock as possibly not file I/O related
+/// Ignoring sys_enter_mknod sys_exit_mknod as possibly not file I/O related
/// Ignoring sys_enter_mlock2 sys_exit_mlock2 as possibly not file I/O related
/// Ignoring sys_enter_mlockall sys_exit_mlockall as possibly not file I/O related
+/// Ignoring sys_enter_mlock sys_exit_mlock as possibly not file I/O related
/// Ignoring sys_enter_modify_ldt sys_exit_modify_ldt as possibly not file I/O related
/// Ignoring sys_enter_mount sys_exit_mount as possibly not file I/O related
/// Ignoring sys_enter_move_mount sys_exit_move_mount as possibly not file I/O related
@@ -119,8 +119,8 @@
/// Ignoring sys_enter_msgget sys_exit_msgget as possibly not file I/O related
/// Ignoring sys_enter_msgrcv sys_exit_msgrcv as possibly not file I/O related
/// Ignoring sys_enter_msgsnd sys_exit_msgsnd as possibly not file I/O related
-/// Ignoring sys_enter_munlock sys_exit_munlock as possibly not file I/O related
/// Ignoring sys_enter_munlockall sys_exit_munlockall as possibly not file I/O related
+/// Ignoring sys_enter_munlock sys_exit_munlock as possibly not file I/O related
/// Ignoring sys_enter_munmap sys_exit_munmap as possibly not file I/O related
/// Ignoring sys_enter_nanosleep sys_exit_nanosleep as possibly not file I/O related
/// Ignoring sys_enter_newuname sys_exit_newuname as possibly not file I/O related
@@ -129,8 +129,8 @@
/// Ignoring sys_enter_personality sys_exit_personality as possibly not file I/O related
/// Ignoring sys_enter_pidfd_open sys_exit_pidfd_open as possibly not file I/O related
/// Ignoring sys_enter_pidfd_send_signal sys_exit_pidfd_send_signal as possibly not file I/O related
-/// Ignoring sys_enter_pipe sys_exit_pipe as possibly not file I/O related
/// Ignoring sys_enter_pipe2 sys_exit_pipe2 as possibly not file I/O related
+/// Ignoring sys_enter_pipe sys_exit_pipe as possibly not file I/O related
/// Ignoring sys_enter_pivot_root sys_exit_pivot_root as possibly not file I/O related
/// Ignoring sys_enter_pkey_alloc sys_exit_pkey_alloc as possibly not file I/O related
/// Ignoring sys_enter_pkey_free sys_exit_pkey_free as possibly not file I/O related
@@ -162,11 +162,11 @@
/// Ignoring sys_enter_rt_sigsuspend sys_exit_rt_sigsuspend as possibly not file I/O related
/// Ignoring sys_enter_rt_sigtimedwait sys_exit_rt_sigtimedwait as possibly not file I/O related
/// Ignoring sys_enter_rt_tgsigqueueinfo sys_exit_rt_tgsigqueueinfo as possibly not file I/O related
-/// Ignoring sys_enter_sched_get_priority_max sys_exit_sched_get_priority_max as possibly not file I/O related
-/// Ignoring sys_enter_sched_get_priority_min sys_exit_sched_get_priority_min as possibly not file I/O related
/// Ignoring sys_enter_sched_getaffinity sys_exit_sched_getaffinity as possibly not file I/O related
/// Ignoring sys_enter_sched_getattr sys_exit_sched_getattr as possibly not file I/O related
/// Ignoring sys_enter_sched_getparam sys_exit_sched_getparam as possibly not file I/O related
+/// Ignoring sys_enter_sched_get_priority_max sys_exit_sched_get_priority_max as possibly not file I/O related
+/// Ignoring sys_enter_sched_get_priority_min sys_exit_sched_get_priority_min as possibly not file I/O related
/// Ignoring sys_enter_sched_getscheduler sys_exit_sched_getscheduler as possibly not file I/O related
/// Ignoring sys_enter_sched_rr_get_interval sys_exit_sched_rr_get_interval as possibly not file I/O related
/// Ignoring sys_enter_sched_setaffinity sys_exit_sched_setaffinity as possibly not file I/O related
@@ -184,10 +184,6 @@
/// Ignoring sys_enter_sendmmsg sys_exit_sendmmsg as possibly not file I/O related
/// Ignoring sys_enter_sendmsg sys_exit_sendmsg as possibly not file I/O related
/// Ignoring sys_enter_sendto sys_exit_sendto as possibly not file I/O related
-/// Ignoring sys_enter_set_mempolicy sys_exit_set_mempolicy as possibly not file I/O related
-/// Ignoring sys_enter_set_mempolicy_home_node sys_exit_set_mempolicy_home_node as possibly not file I/O related
-/// Ignoring sys_enter_set_robust_list sys_exit_set_robust_list as possibly not file I/O related
-/// Ignoring sys_enter_set_tid_address sys_exit_set_tid_address as possibly not file I/O related
/// Ignoring sys_enter_setdomainname sys_exit_setdomainname as possibly not file I/O related
/// Ignoring sys_enter_setfsgid sys_exit_setfsgid as possibly not file I/O related
/// Ignoring sys_enter_setfsuid sys_exit_setfsuid as possibly not file I/O related
@@ -195,6 +191,8 @@
/// Ignoring sys_enter_setgroups sys_exit_setgroups as possibly not file I/O related
/// Ignoring sys_enter_sethostname sys_exit_sethostname as possibly not file I/O related
/// Ignoring sys_enter_setitimer sys_exit_setitimer as possibly not file I/O related
+/// Ignoring sys_enter_set_mempolicy sys_exit_set_mempolicy as possibly not file I/O related
+/// Ignoring sys_enter_set_mempolicy_home_node sys_exit_set_mempolicy_home_node as possibly not file I/O related
/// Ignoring sys_enter_setns sys_exit_setns as possibly not file I/O related
/// Ignoring sys_enter_setpgid sys_exit_setpgid as possibly not file I/O related
/// Ignoring sys_enter_setpriority sys_exit_setpriority as possibly not file I/O related
@@ -203,8 +201,10 @@
/// Ignoring sys_enter_setresuid sys_exit_setresuid as possibly not file I/O related
/// Ignoring sys_enter_setreuid sys_exit_setreuid as possibly not file I/O related
/// Ignoring sys_enter_setrlimit sys_exit_setrlimit as possibly not file I/O related
+/// Ignoring sys_enter_set_robust_list sys_exit_set_robust_list as possibly not file I/O related
/// Ignoring sys_enter_setsid sys_exit_setsid as possibly not file I/O related
/// Ignoring sys_enter_setsockopt sys_exit_setsockopt as possibly not file I/O related
+/// Ignoring sys_enter_set_tid_address sys_exit_set_tid_address as possibly not file I/O related
/// Ignoring sys_enter_settimeofday sys_exit_settimeofday as possibly not file I/O related
/// Ignoring sys_enter_setuid sys_exit_setuid as possibly not file I/O related
/// Ignoring sys_enter_shmat sys_exit_shmat as possibly not file I/O related
@@ -213,8 +213,8 @@
/// Ignoring sys_enter_shmget sys_exit_shmget as possibly not file I/O related
/// Ignoring sys_enter_shutdown sys_exit_shutdown as possibly not file I/O related
/// Ignoring sys_enter_sigaltstack sys_exit_sigaltstack as possibly not file I/O related
-/// Ignoring sys_enter_signalfd sys_exit_signalfd as possibly not file I/O related
/// Ignoring sys_enter_signalfd4 sys_exit_signalfd4 as possibly not file I/O related
+/// Ignoring sys_enter_signalfd sys_exit_signalfd as possibly not file I/O related
/// Ignoring sys_enter_socket sys_exit_socket as possibly not file I/O related
/// Ignoring sys_enter_socketpair sys_exit_socketpair as possibly not file I/O related
/// Ignoring sys_enter_splice sys_exit_splice as possibly not file I/O related
@@ -228,12 +228,12 @@
/// Ignoring sys_enter_time sys_exit_time as possibly not file I/O related
/// Ignoring sys_enter_timer_create sys_exit_timer_create as possibly not file I/O related
/// Ignoring sys_enter_timer_delete sys_exit_timer_delete as possibly not file I/O related
-/// Ignoring sys_enter_timer_getoverrun sys_exit_timer_getoverrun as possibly not file I/O related
-/// Ignoring sys_enter_timer_gettime sys_exit_timer_gettime as possibly not file I/O related
-/// Ignoring sys_enter_timer_settime sys_exit_timer_settime as possibly not file I/O related
/// Ignoring sys_enter_timerfd_create sys_exit_timerfd_create as possibly not file I/O related
/// Ignoring sys_enter_timerfd_gettime sys_exit_timerfd_gettime as possibly not file I/O related
/// Ignoring sys_enter_timerfd_settime sys_exit_timerfd_settime as possibly not file I/O related
+/// Ignoring sys_enter_timer_getoverrun sys_exit_timer_getoverrun as possibly not file I/O related
+/// Ignoring sys_enter_timer_gettime sys_exit_timer_gettime as possibly not file I/O related
+/// Ignoring sys_enter_timer_settime sys_exit_timer_settime as possibly not file I/O related
/// Ignoring sys_enter_times sys_exit_times as possibly not file I/O related
/// Ignoring sys_enter_tkill sys_exit_tkill as possibly not file I/O related
/// Ignoring sys_enter_umask sys_exit_umask as possibly not file I/O related
diff --git a/internal/flamegraph/nativesvg.go b/internal/flamegraph/nativesvg.go
index 2c76a7d..de0364b 100644
--- a/internal/flamegraph/nativesvg.go
+++ b/internal/flamegraph/nativesvg.go
@@ -23,7 +23,7 @@ func NewNativeSVG(fields []string, countField string) NativeSVG {
}
}
-func (n NativeSVG) WriteSVGFromFile(iorDataFile string) error {
+func (n NativeSVG) WriteSVGFromFile(iorDataFile string) (string, error) {
outFile := fmt.Sprintf("%s.%s-by-%s.svg",
strings.TrimSuffix(iorDataFile, ".ior.zst"),
strings.Join(n.fields, ":"),
@@ -32,16 +32,19 @@ func (n NativeSVG) WriteSVGFromFile(iorDataFile string) error {
iod, err := newIorDataFromFile(iorDataFile)
if err != nil {
- return fmt.Errorf("read ior data: %w", err)
+ return outFile, fmt.Errorf("read ior data: %w", err)
}
fd, err := os.Create(outFile)
if err != nil {
- return fmt.Errorf("create output %s: %w", outFile, err)
+ return outFile, fmt.Errorf("create output %s: %w", outFile, err)
}
defer fd.Close()
- return n.WriteSVGFromIter(iod.iter(), fd)
+ if err := n.WriteSVGFromIter(iod.iter(), fd); err != nil {
+ return outFile, err
+ }
+ return outFile, nil
}
func (n NativeSVG) WriteSVGFromIter(records iter.Seq[IterRecord], w io.Writer) error {
diff --git a/internal/flamegraph/svgwriter.go b/internal/flamegraph/svgwriter.go
index 26e203e..5ab8e50 100644
--- a/internal/flamegraph/svgwriter.go
+++ b/internal/flamegraph/svgwriter.go
@@ -68,7 +68,7 @@ func writeSVGHeader(bw *bufio.Writer, cfg SVGConfig, height int) error {
if err != nil {
return err
}
- _, err = fmt.Fprintf(bw, `<g class="controls"><text x="10" y="42" onclick="fgSearch()">Search</text><text x="80" y="42" onclick="fgResetSearch()">Reset Search</text><text x="190" y="42" onclick="fgResetZoom()">Reset Zoom</text><text id="fg-info" x="320" y="42"></text></g>`+"\n")
+ _, err = fmt.Fprintf(bw, `<g class="controls"><text x="10" y="42" onclick="fgSearch()">Search</text><text x="80" y="42" onclick="fgResetSearch()">Reset Search</text><text x="190" y="42" onclick="fgUndoZoom()">Undo Zoom</text><text x="280" y="42" onclick="fgResetZoom()">Reset Zoom</text><text id="fg-info" x="390" y="42"></text></g>`+"\n")
return err
}
@@ -113,11 +113,14 @@ func writeFrame(bw *bufio.Writer, name, title, fill string, x, y, w, h float64,
svgEscape(title), x, y, w, h, fill); err != nil {
return err
}
- if w > float64(fontSize*2) {
- _, err = fmt.Fprintf(bw, `<text x="%.3f" y="%.3f">%s</text>`+"\n", x+3, y+float64(fontSize), svgEscape(name))
- if err != nil {
- return err
- }
+ labelStyle := ""
+ if w <= float64(fontSize*2) {
+ labelStyle = ` style="display:none"`
+ }
+ _, err = fmt.Fprintf(bw, `<text x="%.3f" y="%.3f"%s>%s</text>`+"\n",
+ x+3, y+float64(fontSize), labelStyle, svgEscape(name))
+ if err != nil {
+ return err
}
_, err = fmt.Fprintln(bw, "</g>")
return err
@@ -139,6 +142,7 @@ func flamegraphCSS(cfg SVGConfig) string {
.controls text { font-size: %dpx; font-family: monospace; cursor: pointer; fill: #444; }
.frame text { font-size: %dpx; font-family: monospace; pointer-events: none; fill: #111; }
.frame rect { stroke: rgba(0,0,0,0.18); stroke-width: 0.5; }
+.title, .controls text, .frame text { user-select: none; -webkit-user-select: none; }
`, cfg.FontSize+2, cfg.FontSize, cfg.FontSize-1)
}
diff --git a/internal/flamegraph/svgwriter_js.go b/internal/flamegraph/svgwriter_js.go
index 7b9183f..bf8bfd2 100644
--- a/internal/flamegraph/svgwriter_js.go
+++ b/internal/flamegraph/svgwriter_js.go
@@ -5,16 +5,33 @@ const fg = {
frames: [],
info: null,
matchColor: "rgb(220, 30, 70)",
+ zoomStack: [],
+ zoomRange: null,
+ rootWidth: 0,
};
function fgInit() {
fg.frames = Array.from(document.querySelectorAll("g.frame"));
fg.info = document.getElementById("fg-info");
+ fg.rootWidth = fgDetectRootWidth();
fg.frames.forEach((frame) => {
- frame.addEventListener("click", (ev) => fgZoom(ev.currentTarget));
+ fgSnapshotOriginalGeometry(frame);
+ frame.addEventListener("click", (ev) => {
+ if (ev.detail > 1) return;
+ ev.stopPropagation();
+ fgZoom(ev.currentTarget);
+ });
+ frame.addEventListener("dblclick", (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ fgResetZoom();
+ });
frame.addEventListener("mouseenter", (ev) => fgHover(ev.currentTarget));
});
- document.addEventListener("dblclick", () => fgResetZoom());
+ document.addEventListener("dblclick", (ev) => {
+ ev.preventDefault();
+ fgResetZoom();
+ });
}
function fgHover(frame) {
@@ -24,15 +41,42 @@ function fgHover(frame) {
}
function fgZoom(frame) {
- const x = Number(frame.dataset.x || "0");
- const w = Number(frame.dataset.w || "0");
+ const x = fgOriginalX(frame);
+ const w = fgOriginalW(frame);
if (w <= 0) return;
- const end = x + w;
+ if (fg.zoomRange) {
+ fg.zoomStack.push(fg.zoomRange);
+ }
+ fg.zoomRange = { x: x, w: w, depth: Number(frame.dataset.depth || "0") };
+ fgApplyZoom();
+}
+
+function fgApplyZoom() {
+ if (!fg.zoomRange) {
+ fg.frames.forEach((frame) => {
+ frame.style.display = "";
+ });
+ return;
+ }
+ const x = fg.zoomRange.x;
+ const end = x + fg.zoomRange.w;
+ const width = fg.zoomRange.w;
+ const minDepth = fg.zoomRange.depth;
+ const eps = 1e-6;
+ const scale = fg.rootWidth / width;
fg.frames.forEach((other) => {
- const ox = Number(other.dataset.x || "0");
- const ow = Number(other.dataset.w || "0");
- const sameBand = Number(other.dataset.depth || "0") >= Number(frame.dataset.depth || "0");
- if (sameBand && ox >= x && ox + ow <= end) {
+ const ox = fgOriginalX(other);
+ const ow = fgOriginalW(other);
+ const depth = Number(other.dataset.depth || "0");
+ const inSelectedRange = ox >= x-eps && ox+ow <= end+eps;
+ const isAncestor = depth < minDepth && ox <= x+eps && ox+ow >= end-eps;
+
+ if (isAncestor || (depth >= minDepth && inSelectedRange)) {
+ if (isAncestor) {
+ fgSetFrameGeometry(other, 0, fg.rootWidth);
+ } else {
+ fgSetFrameGeometry(other, (ox-x)*scale, ow*scale);
+ }
other.style.display = "";
} else {
other.style.display = "none";
@@ -40,8 +84,20 @@ function fgZoom(frame) {
});
}
+function fgUndoZoom() {
+ if (fg.zoomStack.length === 0) {
+ fgResetZoom();
+ return;
+ }
+ fg.zoomRange = fg.zoomStack.pop();
+ fgApplyZoom();
+}
+
function fgResetZoom() {
+ fg.zoomStack = [];
+ fg.zoomRange = null;
fg.frames.forEach((frame) => {
+ fgRestoreFrameGeometry(frame);
frame.style.display = "";
});
}
@@ -71,5 +127,86 @@ function fgResetSearch() {
});
}
+function fgDetectRootWidth() {
+ let maxEnd = 0;
+ fg.frames.forEach((frame) => {
+ const x = Number(frame.dataset.x || "0");
+ const w = Number(frame.dataset.w || "0");
+ maxEnd = Math.max(maxEnd, x + w);
+ });
+ return maxEnd;
+}
+
+function fgSnapshotOriginalGeometry(frame) {
+ const rect = frame.querySelector("rect");
+ const text = frame.querySelector("text");
+ frame.dataset.ox = frame.dataset.x || "0";
+ frame.dataset.ow = frame.dataset.w || "0";
+ if (rect) {
+ rect.dataset.ox = rect.getAttribute("x") || "0";
+ rect.dataset.ow = rect.getAttribute("width") || "0";
+ }
+ if (text) {
+ text.dataset.ox = text.getAttribute("x") || "0";
+ text.dataset.hidden = text.style.display === "none" ? "1" : "0";
+ text.dataset.full = text.textContent || frame.dataset.name || "";
+ }
+}
+
+function fgOriginalX(frame) {
+ return Number(frame.dataset.ox || frame.dataset.x || "0");
+}
+
+function fgOriginalW(frame) {
+ return Number(frame.dataset.ow || frame.dataset.w || "0");
+}
+
+function fgSetFrameGeometry(frame, x, w) {
+ const rect = frame.querySelector("rect");
+ const text = frame.querySelector("text");
+ if (rect) {
+ rect.setAttribute("x", String(x));
+ rect.setAttribute("width", String(w));
+ }
+ if (text) {
+ text.setAttribute("x", String(x + 3));
+ fgFitLabel(text, w);
+ }
+}
+
+function fgRestoreFrameGeometry(frame) {
+ const rect = frame.querySelector("rect");
+ const text = frame.querySelector("text");
+ if (rect) {
+ rect.setAttribute("x", rect.dataset.ox || "0");
+ rect.setAttribute("width", rect.dataset.ow || "0");
+ }
+ if (text) {
+ text.setAttribute("x", text.dataset.ox || "0");
+ if (text.dataset.hidden === "1") {
+ text.style.display = "none";
+ text.textContent = text.dataset.full || "";
+ } else {
+ fgFitLabel(text, Number(rect ? (rect.dataset.ow || "0") : "0"));
+ }
+ }
+}
+
+function fgFitLabel(text, width) {
+ const full = text.dataset.full || text.textContent || "";
+ const maxChars = Math.floor((width - 6) / 7);
+ if (maxChars < 3) {
+ text.style.display = "none";
+ text.textContent = full;
+ return;
+ }
+ text.style.display = "";
+ if (full.length <= maxChars) {
+ text.textContent = full;
+ return;
+ }
+ text.textContent = full.slice(0, maxChars - 1) + "…";
+}
+
window.addEventListener("DOMContentLoaded", fgInit);
`
diff --git a/internal/flamegraph/svgwriter_jscode.go b/internal/flamegraph/svgwriter_jscode.go
index 52f8818..3ac00fd 100644
--- a/internal/flamegraph/svgwriter_jscode.go
+++ b/internal/flamegraph/svgwriter_jscode.go
@@ -7,16 +7,33 @@ const fg = {
frames: [],
info: null,
matchColor: "rgb(220, 30, 70)",
+ zoomStack: [],
+ zoomRange: null,
+ rootWidth: 0,
};
function fgInit() {
fg.frames = Array.from(document.querySelectorAll("g.frame"));
fg.info = document.getElementById("fg-info");
+ fg.rootWidth = fgDetectRootWidth();
fg.frames.forEach((frame) => {
- frame.addEventListener("click", (ev) => fgZoom(ev.currentTarget));
+ fgSnapshotOriginalGeometry(frame);
+ frame.addEventListener("click", (ev) => {
+ if (ev.detail > 1) return;
+ ev.stopPropagation();
+ fgZoom(ev.currentTarget);
+ });
+ frame.addEventListener("dblclick", (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ fgResetZoom();
+ });
frame.addEventListener("mouseenter", (ev) => fgHover(ev.currentTarget));
});
- document.addEventListener("dblclick", () => fgResetZoom());
+ document.addEventListener("dblclick", (ev) => {
+ ev.preventDefault();
+ fgResetZoom();
+ });
}
function fgHover(frame) {
@@ -26,15 +43,42 @@ function fgHover(frame) {
}
function fgZoom(frame) {
- const x = Number(frame.dataset.x || "0");
- const w = Number(frame.dataset.w || "0");
+ const x = fgOriginalX(frame);
+ const w = fgOriginalW(frame);
if (w <= 0) return;
- const end = x + w;
+ if (fg.zoomRange) {
+ fg.zoomStack.push(fg.zoomRange);
+ }
+ fg.zoomRange = { x: x, w: w, depth: Number(frame.dataset.depth || "0") };
+ fgApplyZoom();
+}
+
+function fgApplyZoom() {
+ if (!fg.zoomRange) {
+ fg.frames.forEach((frame) => {
+ frame.style.display = "";
+ });
+ return;
+ }
+ const x = fg.zoomRange.x;
+ const end = x + fg.zoomRange.w;
+ const width = fg.zoomRange.w;
+ const minDepth = fg.zoomRange.depth;
+ const eps = 1e-6;
+ const scale = fg.rootWidth / width;
fg.frames.forEach((other) => {
- const ox = Number(other.dataset.x || "0");
- const ow = Number(other.dataset.w || "0");
- const sameBand = Number(other.dataset.depth || "0") >= Number(frame.dataset.depth || "0");
- if (sameBand && ox >= x && ox + ow <= end) {
+ const ox = fgOriginalX(other);
+ const ow = fgOriginalW(other);
+ const depth = Number(other.dataset.depth || "0");
+ const inSelectedRange = ox >= x-eps && ox+ow <= end+eps;
+ const isAncestor = depth < minDepth && ox <= x+eps && ox+ow >= end-eps;
+
+ if (isAncestor || (depth >= minDepth && inSelectedRange)) {
+ if (isAncestor) {
+ fgSetFrameGeometry(other, 0, fg.rootWidth);
+ } else {
+ fgSetFrameGeometry(other, (ox-x)*scale, ow*scale);
+ }
other.style.display = "";
} else {
other.style.display = "none";
@@ -42,8 +86,20 @@ function fgZoom(frame) {
});
}
+function fgUndoZoom() {
+ if (fg.zoomStack.length === 0) {
+ fgResetZoom();
+ return;
+ }
+ fg.zoomRange = fg.zoomStack.pop();
+ fgApplyZoom();
+}
+
function fgResetZoom() {
+ fg.zoomStack = [];
+ fg.zoomRange = null;
fg.frames.forEach((frame) => {
+ fgRestoreFrameGeometry(frame);
frame.style.display = "";
});
}
@@ -73,5 +129,86 @@ function fgResetSearch() {
});
}
+function fgDetectRootWidth() {
+ let maxEnd = 0;
+ fg.frames.forEach((frame) => {
+ const x = Number(frame.dataset.x || "0");
+ const w = Number(frame.dataset.w || "0");
+ maxEnd = Math.max(maxEnd, x + w);
+ });
+ return maxEnd;
+}
+
+function fgSnapshotOriginalGeometry(frame) {
+ const rect = frame.querySelector("rect");
+ const text = frame.querySelector("text");
+ frame.dataset.ox = frame.dataset.x || "0";
+ frame.dataset.ow = frame.dataset.w || "0";
+ if (rect) {
+ rect.dataset.ox = rect.getAttribute("x") || "0";
+ rect.dataset.ow = rect.getAttribute("width") || "0";
+ }
+ if (text) {
+ text.dataset.ox = text.getAttribute("x") || "0";
+ text.dataset.hidden = text.style.display === "none" ? "1" : "0";
+ text.dataset.full = text.textContent || frame.dataset.name || "";
+ }
+}
+
+function fgOriginalX(frame) {
+ return Number(frame.dataset.ox || frame.dataset.x || "0");
+}
+
+function fgOriginalW(frame) {
+ return Number(frame.dataset.ow || frame.dataset.w || "0");
+}
+
+function fgSetFrameGeometry(frame, x, w) {
+ const rect = frame.querySelector("rect");
+ const text = frame.querySelector("text");
+ if (rect) {
+ rect.setAttribute("x", String(x));
+ rect.setAttribute("width", String(w));
+ }
+ if (text) {
+ text.setAttribute("x", String(x + 3));
+ fgFitLabel(text, w);
+ }
+}
+
+function fgRestoreFrameGeometry(frame) {
+ const rect = frame.querySelector("rect");
+ const text = frame.querySelector("text");
+ if (rect) {
+ rect.setAttribute("x", rect.dataset.ox || "0");
+ rect.setAttribute("width", rect.dataset.ow || "0");
+ }
+ if (text) {
+ text.setAttribute("x", text.dataset.ox || "0");
+ if (text.dataset.hidden === "1") {
+ text.style.display = "none";
+ text.textContent = text.dataset.full || "";
+ } else {
+ fgFitLabel(text, Number(rect ? (rect.dataset.ow || "0") : "0"));
+ }
+ }
+}
+
+function fgFitLabel(text, width) {
+ const full = text.dataset.full || text.textContent || "";
+ const maxChars = Math.floor((width - 6) / 7);
+ if (maxChars < 3) {
+ text.style.display = "none";
+ text.textContent = full;
+ return;
+ }
+ text.style.display = "";
+ if (full.length <= maxChars) {
+ text.textContent = full;
+ return;
+ }
+ text.textContent = full.slice(0, maxChars - 1) + "…";
+}
+
window.addEventListener("DOMContentLoaded", fgInit);
`
diff --git a/internal/flamegraph/tool.go b/internal/flamegraph/tool.go
index 3719b0a..a83c44f 100644
--- a/internal/flamegraph/tool.go
+++ b/internal/flamegraph/tool.go
@@ -69,6 +69,10 @@ func (t Tool) WriteSVG() error {
return nil
}
+func (t Tool) OutFile() string {
+ return t.outFile
+}
+
func decompress(compressedFile string) (string, error) {
decompressedFile := strings.TrimSuffix(compressedFile, ".zst")
diff --git a/internal/flamegraph/webserver.go b/internal/flamegraph/webserver.go
new file mode 100644
index 0000000..7925011
--- /dev/null
+++ b/internal/flamegraph/webserver.go
@@ -0,0 +1,108 @@
+package flamegraph
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+)
+
+func ServeSVG(svgFile string) error {
+ absPath, err := filepath.Abs(svgFile)
+ if err != nil {
+ return fmt.Errorf("resolve svg path: %w", err)
+ }
+ urlPath := buildURLPath(absPath)
+ srv := &http.Server{Handler: buildSVGHandler(absPath, urlPath)}
+
+ listener, err := listenRandomPort()
+ if err != nil {
+ return err
+ }
+ defer listener.Close()
+
+ hostname, port := serverHostPort(listener)
+ printServerURL(hostname, port, urlPath)
+
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- srv.Serve(listener)
+ }()
+
+ return waitForStop(srv, errCh)
+}
+
+func buildURLPath(absPath string) string {
+ urlPath := filepath.ToSlash(absPath)
+ if !strings.HasPrefix(urlPath, "/") {
+ return "/" + urlPath
+ }
+ return urlPath
+}
+
+func buildSVGHandler(absPath, urlPath string) http.Handler {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, urlPath, http.StatusFound)
+ })
+ mux.HandleFunc(urlPath, func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, absPath)
+ })
+ return mux
+}
+
+func listenRandomPort() (net.Listener, error) {
+ listener, err := net.Listen("tcp", ":0")
+ if err != nil {
+ return nil, fmt.Errorf("start web server: %w", err)
+ }
+ return listener, nil
+}
+
+func serverHostPort(listener net.Listener) (string, int) {
+ hostname, err := os.Hostname()
+ if err != nil {
+ hostname = "localhost"
+ }
+ port := listener.Addr().(*net.TCPAddr).Port
+ return hostname, port
+}
+
+func printServerURL(hostname string, port int, urlPath string) {
+ fmt.Printf("Flamegraph available at http://%s:%d%s\n", hostname, port, urlPath)
+ fmt.Println("Press Ctrl+C to stop the web server.")
+}
+
+func waitForStop(srv *http.Server, errCh <-chan error) error {
+ stopCh := make(chan os.Signal, 1)
+ signal.Notify(stopCh, os.Interrupt, syscall.SIGTERM)
+ defer signal.Stop(stopCh)
+
+ select {
+ case sig := <-stopCh:
+ _ = sig
+ case serveErr := <-errCh:
+ if serveErr != nil && serveErr != http.ErrServerClosed {
+ return serveErr
+ }
+ return nil
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ if err := srv.Shutdown(ctx); err != nil {
+ return fmt.Errorf("shutdown web server: %w", err)
+ }
+
+ serveErr := <-errCh
+ if serveErr != nil && serveErr != http.ErrServerClosed {
+ return serveErr
+ }
+ return nil
+}
diff --git a/internal/ior.go b/internal/ior.go
index ee1f988..599736d 100644
--- a/internal/ior.go
+++ b/internal/ior.go
@@ -99,6 +99,7 @@ func Run() error {
if iorFile != "" {
noTraceRun = true
+ var svgFile string
if cfg.FlamegraphTool != "" {
collapsed := flamegraph.NewCollapsed(iorFile, cfg.CollapsedFields, cfg.CountField)
collapsedFile, err := collapsed.Write(iorFile)
@@ -113,12 +114,19 @@ func Run() error {
if err := tool.WriteSVG(); err != nil {
return err
}
+ svgFile = tool.OutFile()
} else {
native := flamegraph.NewNativeSVG(cfg.CollapsedFields, cfg.CountField)
- if err := native.WriteSVGFromFile(iorFile); err != nil {
+ var err error
+ svgFile, err = native.WriteSVGFromFile(iorFile)
+ if err != nil {
return err
}
}
+
+ if err := flamegraph.ServeSVG(svgFile); err != nil {
+ return err
+ }
}
if noTraceRun {