package flamegraph import ( "context" "fmt" "net" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" ) type serverTimeouts struct { readTimeout time.Duration writeTimeout time.Duration idleTimeout time.Duration } var defaultServerTimeouts = serverTimeouts{ readTimeout: 10 * time.Second, writeTimeout: 30 * time.Second, idleTimeout: 60 * time.Second, } // ServeSVG starts a small HTTP server that serves a single flamegraph SVG. // // It prints a URL of the form http://HOSTNAME:PORT/abs/path/to.svg and blocks until // the user presses Ctrl+C or the process receives SIGTERM, at which point the server // is shut down gracefully. func ServeSVG(svgFile string) error { 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() return runServer(ctx, buildSVGHandler(absPath, urlPath), defaultServerTimeouts, func(hostname string, port int) { printServerURL(hostname, port, urlPath) }) } // 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, "/") { return "/" + urlPath } return urlPath } func buildSVGHandler(absPath, urlPath string) *http.ServeMux { 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 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, `