diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-03 13:10:18 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-03 13:10:18 +0200 |
| commit | 6907c5db1125cc385694f1c4283144f7d03b020e (patch) | |
| tree | 304347f8eac5adad6c1ad76ade503fe37113d774 /internal/flamegraph/webserver.go | |
| parent | d80acf0c92ad4b436c23ac881ec24485297a80d8 (diff) | |
Add watch mode for dynamic flamegraph updates
Diffstat (limited to 'internal/flamegraph/webserver.go')
| -rw-r--r-- | internal/flamegraph/webserver.go | 64 |
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 { |
