summaryrefslogtreecommitdiff
path: root/internal/flamegraph
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-02 08:39:35 +0200
committerPaul Buetow <paul@buetow.org>2026-03-02 08:39:35 +0200
commitb3327d17f9f5c7080b05d6d5457cf25550d40ad9 (patch)
tree090e66047c14c9183058bd68a8d4afc12b48f399 /internal/flamegraph
parent38620359537d77ab10acb888d266d3c6eb16fc9b (diff)
Add --open support for live flamegraph browser launch
Diffstat (limited to 'internal/flamegraph')
-rw-r--r--internal/flamegraph/liveserver.go79
-rw-r--r--internal/flamegraph/liveserver_open_test.go130
2 files changed, 208 insertions, 1 deletions
diff --git a/internal/flamegraph/liveserver.go b/internal/flamegraph/liveserver.go
index 801b841..dda1af1 100644
--- a/internal/flamegraph/liveserver.go
+++ b/internal/flamegraph/liveserver.go
@@ -3,8 +3,12 @@ package flamegraph
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"net/http"
+ "os/exec"
+ "runtime"
+ "strings"
"time"
)
@@ -14,18 +18,91 @@ var liveServerTimeouts = serverTimeouts{
idleTimeout: 60 * time.Second,
}
+type LiveServerOptions struct {
+ AutoOpenBrowser bool
+ OpenCommand string
+}
+
+var openBrowserURLFn = openBrowserURL
+
// ServeLive starts the live flamegraph HTTP server and blocks until ctx is canceled.
func ServeLive(ctx context.Context, lt *LiveTrie, interval time.Duration) error {
+ return ServeLiveWithOptions(ctx, lt, interval, LiveServerOptions{})
+}
+
+// ServeLiveWithOptions starts the live flamegraph server with runtime options.
+func ServeLiveWithOptions(ctx context.Context, lt *LiveTrie, interval time.Duration, options LiveServerOptions) error {
mux := http.NewServeMux()
mux.HandleFunc("/", handleLivePage())
mux.HandleFunc("/events", handleSSE(lt, interval))
mux.HandleFunc("/reset", handleReset(lt))
mux.HandleFunc("/order", handleOrder(lt))
return runServer(ctx, mux, liveServerTimeouts, func(hostname string, port int) {
- fmt.Printf("Live flamegraph available at http://%s:%d/\n", hostname, port)
+ url := fmt.Sprintf("http://%s:%d/", hostname, port)
+ fmt.Printf("Live flamegraph available at %s\n", url)
+ if err := maybeOpenLiveBrowser(url, options); err != nil {
+ fmt.Printf("Live flamegraph browser auto-open failed: %v\n", err)
+ }
})
}
+func maybeOpenLiveBrowser(url string, options LiveServerOptions) error {
+ if !options.AutoOpenBrowser {
+ return nil
+ }
+ return openBrowserURLFn(url, options.OpenCommand)
+}
+
+func openBrowserURL(url, openCommand string) error {
+ parts, err := browserOpenCommandParts(runtime.GOOS, openCommand, url)
+ if err != nil {
+ return err
+ }
+ cmd := exec.Command(parts[0], parts[1:]...)
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ go func() { _ = cmd.Wait() }()
+ return nil
+}
+
+func browserOpenCommandParts(goos, openCommand, url string) ([]string, error) {
+ var parts []string
+ if trimmed := strings.TrimSpace(openCommand); trimmed != "" {
+ parts = strings.Fields(trimmed)
+ } else {
+ parts = defaultBrowserCommand(goos)
+ }
+ if len(parts) == 0 {
+ return nil, errors.New("empty browser open command")
+ }
+
+ containsURL := false
+ for i := range parts {
+ if strings.Contains(parts[i], "{url}") {
+ parts[i] = strings.ReplaceAll(parts[i], "{url}", url)
+ containsURL = true
+ }
+ }
+ if !containsURL {
+ parts = append(parts, url)
+ }
+ return parts, nil
+}
+
+func defaultBrowserCommand(goos string) []string {
+ switch goos {
+ case "darwin":
+ return []string{"open", "-a", "Firefox"}
+ case "linux":
+ return []string{"firefox"}
+ case "windows":
+ return []string{"cmd", "/c", "start"}
+ default:
+ return []string{"firefox"}
+ }
+}
+
func handleLivePage() http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
diff --git a/internal/flamegraph/liveserver_open_test.go b/internal/flamegraph/liveserver_open_test.go
new file mode 100644
index 0000000..9d280b9
--- /dev/null
+++ b/internal/flamegraph/liveserver_open_test.go
@@ -0,0 +1,130 @@
+package flamegraph
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestBrowserOpenCommandPartsUsesLinuxDefault(t *testing.T) {
+ got, err := browserOpenCommandParts("linux", "", "http://localhost:1234/")
+ if err != nil {
+ t.Fatalf("browserOpenCommandParts returned error: %v", err)
+ }
+ want := []string{"firefox", "http://localhost:1234/"}
+ if len(got) != len(want) {
+ t.Fatalf("len(parts) = %d, want %d", len(got), len(want))
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i])
+ }
+ }
+}
+
+func TestBrowserOpenCommandPartsUsesDarwinDefault(t *testing.T) {
+ got, err := browserOpenCommandParts("darwin", "", "http://localhost:1234/")
+ if err != nil {
+ t.Fatalf("browserOpenCommandParts returned error: %v", err)
+ }
+ want := []string{"open", "-a", "Firefox", "http://localhost:1234/"}
+ if len(got) != len(want) {
+ t.Fatalf("len(parts) = %d, want %d", len(got), len(want))
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i])
+ }
+ }
+}
+
+func TestBrowserOpenCommandPartsOverrideAppendsURL(t *testing.T) {
+ got, err := browserOpenCommandParts("linux", "chromium --new-window", "http://localhost:1234/")
+ if err != nil {
+ t.Fatalf("browserOpenCommandParts returned error: %v", err)
+ }
+ want := []string{"chromium", "--new-window", "http://localhost:1234/"}
+ if len(got) != len(want) {
+ t.Fatalf("len(parts) = %d, want %d", len(got), len(want))
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i])
+ }
+ }
+}
+
+func TestBrowserOpenCommandPartsOverrideReplacesPlaceholder(t *testing.T) {
+ got, err := browserOpenCommandParts("linux", "open-browser --target={url}", "http://localhost:1234/")
+ if err != nil {
+ t.Fatalf("browserOpenCommandParts returned error: %v", err)
+ }
+ want := []string{"open-browser", "--target=http://localhost:1234/"}
+ if len(got) != len(want) {
+ t.Fatalf("len(parts) = %d, want %d", len(got), len(want))
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("parts[%d] = %q, want %q", i, got[i], want[i])
+ }
+ }
+}
+
+func TestMaybeOpenLiveBrowserDisabledSkipsOpen(t *testing.T) {
+ called := false
+ orig := openBrowserURLFn
+ openBrowserURLFn = func(url, openCommand string) error {
+ called = true
+ return nil
+ }
+ t.Cleanup(func() { openBrowserURLFn = orig })
+
+ err := maybeOpenLiveBrowser("http://localhost:1234/", LiveServerOptions{AutoOpenBrowser: false})
+ if err != nil {
+ t.Fatalf("maybeOpenLiveBrowser returned error: %v", err)
+ }
+ if called {
+ t.Fatalf("expected browser opener not to be called when disabled")
+ }
+}
+
+func TestMaybeOpenLiveBrowserEnabledCallsOpen(t *testing.T) {
+ called := false
+ orig := openBrowserURLFn
+ openBrowserURLFn = func(url, openCommand string) error {
+ called = true
+ if url != "http://localhost:1234/" {
+ t.Fatalf("url = %q, want %q", url, "http://localhost:1234/")
+ }
+ if openCommand != "chromium" {
+ t.Fatalf("openCommand = %q, want %q", openCommand, "chromium")
+ }
+ return nil
+ }
+ t.Cleanup(func() { openBrowserURLFn = orig })
+
+ err := maybeOpenLiveBrowser("http://localhost:1234/", LiveServerOptions{
+ AutoOpenBrowser: true,
+ OpenCommand: "chromium",
+ })
+ if err != nil {
+ t.Fatalf("maybeOpenLiveBrowser returned error: %v", err)
+ }
+ if !called {
+ t.Fatalf("expected browser opener to be called")
+ }
+}
+
+func TestMaybeOpenLiveBrowserPropagatesOpenError(t *testing.T) {
+ orig := openBrowserURLFn
+ openBrowserURLFn = func(url, openCommand string) error {
+ return errors.New("launch failed")
+ }
+ t.Cleanup(func() { openBrowserURLFn = orig })
+
+ err := maybeOpenLiveBrowser("http://localhost:1234/", LiveServerOptions{
+ AutoOpenBrowser: true,
+ })
+ if err == nil || err.Error() != "launch failed" {
+ t.Fatalf("expected launch failed error, got %v", err)
+ }
+}