summaryrefslogtreecommitdiff
path: root/internal/flamegraph/webserver.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-03 13:10:18 +0200
committerPaul Buetow <paul@buetow.org>2026-03-03 13:10:18 +0200
commit6907c5db1125cc385694f1c4283144f7d03b020e (patch)
tree304347f8eac5adad6c1ad76ade503fe37113d774 /internal/flamegraph/webserver.go
parentd80acf0c92ad4b436c23ac881ec24485297a80d8 (diff)
Add watch mode for dynamic flamegraph updates
Diffstat (limited to 'internal/flamegraph/webserver.go')
-rw-r--r--internal/flamegraph/webserver.go64
1 files changed, 64 insertions, 0 deletions
diff --git a/internal/flamegraph/webserver.go b/internal/flamegraph/webserver.go
index 2bf6286..c472dfb 100644
--- a/internal/flamegraph/webserver.go
+++ b/internal/flamegraph/webserver.go
@@ -43,6 +43,30 @@ func ServeSVG(svgFile string) error {
})
}
+// ServeSVGAutoReload serves an SVG viewer page that periodically reloads the SVG.
+//
+// The SVG file itself is still served directly at its absolute URL path, while "/"
+// serves a small HTML wrapper that appends a cache-busting query parameter on each
+// refresh interval to pick up newly written SVG content.
+func ServeSVGAutoReload(svgFile string, refreshInterval time.Duration) error {
+ if refreshInterval <= 0 {
+ return fmt.Errorf("refresh interval must be > 0")
+ }
+
+ absPath, err := filepath.Abs(svgFile)
+ if err != nil {
+ return fmt.Errorf("resolve svg path: %w", err)
+ }
+ urlPath := buildURLPath(absPath)
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer stop()
+
+ mux := buildSVGAutoReloadHandler(absPath, urlPath, refreshInterval)
+ return runServer(ctx, mux, defaultServerTimeouts, func(hostname string, port int) {
+ printServerURL(hostname, port, "/")
+ })
+}
+
func buildURLPath(absPath string) string {
urlPath := filepath.ToSlash(absPath)
if !strings.HasPrefix(urlPath, "/") {
@@ -62,6 +86,46 @@ func buildSVGHandler(absPath, urlPath string) *http.ServeMux {
return mux
}
+func buildSVGAutoReloadHandler(absPath, urlPath string, refreshInterval time.Duration) *http.ServeMux {
+ intervalMs := refreshInterval.Milliseconds()
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = fmt.Fprintf(w, `<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
+ <title>I/O Flamegraph (Auto-Reload)</title>
+ <style>
+ body { margin: 0; font-family: monospace; }
+ .bar { padding: 8px 12px; border-bottom: 1px solid #ddd; }
+ .viewer { width: 100%%; height: calc(100vh - 42px); border: 0; display: block; }
+ </style>
+</head>
+<body>
+ <div class="bar">
+ Auto-refresh every %d ms.
+ <button type="button" onclick="refreshNow()">Refresh now</button>
+ </div>
+ <iframe id="fg" class="viewer" src="%s"></iframe>
+ <script>
+ const base = %q;
+ function refreshNow() {
+ document.getElementById("fg").src = base + "?t=" + Date.now();
+ }
+ setInterval(refreshNow, %d);
+ </script>
+</body>
+</html>
+`, intervalMs, urlPath, urlPath, intervalMs)
+ })
+ 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 {