diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-06 15:35:24 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-06 15:35:24 +0200 |
| commit | 99b02bf8c389a793df5d5986db05eed7e459f7b1 (patch) | |
| tree | bc4e36cfcd3c9ef9b067beed2eb5b68a75a45aa2 | |
| parent | 4ff17c30120d657b966f8a55188ba167dc875e64 (diff) | |
refactor: remove web flamegrapher and keep TUI-only
31 files changed, 46 insertions, 4586 deletions
@@ -46,7 +46,7 @@ Generator source code: - **Entry point**: `cmd/ior/main.go` - Linux-only BPF-based I/O syscall tracer - **Core packages**: `/internal/event/` (BPF event handling), `/internal/flamegraph/` (FlameGraph generation), `/internal/c/` (BPF programs) -- **Output**: Compressed `.ior.zst` trace data and native SVG flamegraphs (served via embedded web server in `-ior` mode) +- **Output**: TUI dashboard and TUI flamegraphs (no embedded web flamegraph server mode) - **TUI package**: `/internal/tui/` contains top-level Bubble Tea orchestration (`tui.go`), shared key map (`keys.go`), and styles (`styles.go`). - **Dashboard tabs**: `/internal/tui/dashboard/` contains tab renderers (overview/syscalls/files/processes/latency/gaps) and tab framework model. - **Export modal**: `/internal/tui/export/model.go` implements the centered modal used for CSV export flow in TUI mode. @@ -61,60 +61,11 @@ make sudo cp -v ./libelf/libelf.a /usr/lib64/ ``` -## Native Flamegraph Generation +## TUI Flamegraphs -Flamegraphs are generated natively by `ior` from `.ior.zst` data files; no external flamegraph tool is required. -When `-fields` is omitted, the default stack order is `comm,path,tracepoint` (bottom to top). -To change grouping order, pass `-fields` explicitly in the desired order. - -```sh -./ior -ior=trace.ior.zst -fields=comm,path,tracepoint -count=count -``` - -This generates an SVG and starts an embedded web server. The terminal prints a URL like: - -```text -Flamegraph available at http://HOSTNAME:PORT/abs/path/to.svg -``` - -For experimental WebAssembly frontends, you can also emit a flamegraph JSON tree: - -```sh -./ior -ior=trace.ior.zst -flamegraphJson -``` - -This writes `<trace>.<fields>-by-<count>.json` next to the SVG. - -To keep the served flamegraph changing as the `.ior.zst` file is updated, enable watch mode: - -```sh -./ior -ior=trace.ior.zst -iorWatchInterval=2s -``` - -This polls the input file for modifications, regenerates SVG/JSON outputs, and serves an auto-reloading viewer at `/`. - -## Live Flamegraph Mode - -Run live mode (requires root privileges): - -```sh -sudo ./ior -live -pid <PID> -live-interval 200ms -duration 300 -``` - -The terminal prints a URL like: - -```text -Live flamegraph available at http://HOSTNAME:PORT/ -``` - -Live controls: - -- `Space`: pause/resume incoming updates. -- `/`: search frame labels. -- `Escape`: reset zoom and search highlighting. -- `r`: reset baseline (clears all live aggregated stats on the server and restarts from zero). -- `Reset Baseline` button: same behavior as `r`. -- `Order: ...` toggle button: cycles stack order presets on the fly and re-baselines live aggregation for the new order. +Flamegraphs are available only inside the TUI dashboard. +Use `-fields` to change the stack order and `-count` to choose the metric. +The default stack order is `comm,path,tracepoint` (bottom to top). ## TUI Hotkeys diff --git a/docs/tui-flamegraph-plan.md b/docs/tui-flamegraph-plan.md index 67e8653..261f0fb 100644 --- a/docs/tui-flamegraph-plan.md +++ b/docs/tui-flamegraph-plan.md @@ -5,8 +5,8 @@ Add a **7th dashboard tab** (`7:Flame`) that renders a live, interactive flamegraph directly in the terminal using lipgloss for layout/styling and **Charm Harmonica** for smooth spring-based animations on both zoom transitions and live data refresh. -The tab consumes data from an embedded `LiveTrie` (same as the web live mode) and -provides full feature parity with the browser version. +The tab consumes data from an embedded `LiveTrie` and +provides interactive flamegraph navigation directly in-terminal. ## Architecture @@ -145,8 +145,7 @@ Terminal flamegraphs use a **cell-based layout** rather than pixel coordinates: - Use lipgloss background color fill with the existing `frameColor()` warm palette - Frame text = truncated function/path name that fits within the frame width - Selected frame gets a distinct border/highlight style (e.g., bold + inverted) - - Search-matched frames get a different highlight color (e.g., red background like - the web version's `matchColor`) + - Search-matched frames get a different highlight color (e.g., red background) 3. **Compositing**: Use `lipgloss.Place()` or the new lipgloss v2 compositor/canvas to layer frames at their (col, row) positions. Each row of the flamegraph is @@ -282,7 +281,7 @@ distinct colors for uncategorized frames. ### 10. Field Order Cycling -Same preset cycle as the web version: +Preset cycle: ```go fieldPresets = [][]string{ {"comm", "path", "tracepoint"}, @@ -330,8 +329,7 @@ sys_read (1,234 calls, 45.2%) - /usr/bin/myapp > /dev/sda > sys_enter_read ### 14. Risks and Mitigations 1. **Performance at high event rates**: The `LiveTrie.Ingest()` call adds overhead - to the hot path. Mitigation: already designed for production rates (used in - `-live` mode). TUI render is decoupled via version polling. + to the hot path. Mitigation: TUI render is decoupled via version polling. 2. **Terminal width too narrow**: Flamegraphs with many shallow frames may not render meaningfully in 80-column terminals. Mitigation: cull frames below 1 cell, diff --git a/internal/eventloop.go b/internal/eventloop.go index 7d33f87..6f14325 100644 --- a/internal/eventloop.go +++ b/internal/eventloop.go @@ -13,7 +13,6 @@ import ( "ior/internal/event" "ior/internal/file" - "ior/internal/flamegraph" "ior/internal/types" . "ior/internal/types" ) @@ -21,18 +20,13 @@ import ( const sysEnterNameToHandleAtName = "name_to_handle_at" type eventLoopConfig struct { - pidFilter int - commFilter string - pathFilter string - liveFlamegraph bool - liveInterval time.Duration - liveOpenCommand string - collapsedFields []string - countField string - flamegraphName string - flamegraphEnable bool - pprofEnable bool - plainMode bool + pidFilter int + commFilter string + pathFilter string + collapsedFields []string + countField string + pprofEnable bool + plainMode bool } type fdTracker struct { @@ -174,8 +168,6 @@ type eventLoop struct { commResolver *commResolver prevPairTimes map[uint32]uint64 // Previous event's time (to calculate time differences between two events) rawHandlers map[EventType]rawEventHandler - flamegraph flamegraph.IorDataCollector // Storing all paths in a map structure for analysis - liveTrie *flamegraph.LiveTrie printCb func(ep *event.Pair) // Callback to print the event warningCb func(message string) // Optional callback for non-fatal event processing warnings cfg eventLoopConfig @@ -205,14 +197,10 @@ func newEventLoop(cfg eventLoopConfig) *eventLoop { prevPairTimes: make(map[uint32]uint64), rawHandlers: make(map[EventType]rawEventHandler), printCb: func(ep *event.Pair) { fmt.Println(ep); ep.Recycle() }, - flamegraph: flamegraph.New(cfg.flamegraphName), cfg: cfg, done: make(chan struct{}), } el.initRawHandlers() - if cfg.liveFlamegraph { - el.liveTrie = flamegraph.NewLiveTrie(cfg.collapsedFields, cfg.countField) - } el.configureOutputCallback() el.seedTrackedPidComm() return el @@ -244,14 +232,6 @@ func (e *eventLoop) commState() *commResolver { func (e *eventLoop) configureOutputCallback() { switch { - case e.cfg.flamegraphEnable: - e.printCb = func(ep *event.Pair) { - e.flamegraph.Ch <- ep - } - case e.liveTrie != nil: - e.printCb = func(ep *event.Pair) { - e.liveTrie.Ingest(ep) - } case e.cfg.pprofEnable: e.printCb = func(ep *event.Pair) { ep.Recycle() @@ -282,29 +262,10 @@ func (e *eventLoop) stats() string { func (e *eventLoop) run(ctx context.Context, rawCh <-chan []byte) { defer close(e.done) - if e.liveTrie != nil { - fmt.Println("Starting live flamegraph server") - go func() { - liveOptions := flamegraph.LiveServerOptions{ - OpenCommand: e.cfg.liveOpenCommand, - } - if e.warningCb != nil { - liveOptions.WarningCb = e.notifyWarning - } - if err := flamegraph.ServeLiveWithOptions(ctx, e.liveTrie, e.cfg.liveInterval, liveOptions); err != nil && ctx.Err() == nil { - fmt.Println("Live flamegraph server error:", err) - } - }() - } - - if e.cfg.flamegraphEnable { - fmt.Println("Collecting flame graph stats, press Ctrl+C to stop") - e.flamegraph.Start(ctx) - } if e.cfg.pprofEnable { fmt.Println("Profiling, press Ctrl+C to stop") } - if e.cfg.plainMode && !e.cfg.flamegraphEnable && !e.cfg.pprofEnable { + if e.cfg.plainMode && !e.cfg.pprofEnable { fmt.Println(event.EventStreamHeader) } @@ -316,16 +277,6 @@ func (e *eventLoop) run(ctx context.Context, rawCh <-chan []byte) { e.printCb(ep) e.numSyscallsAfterFilter++ } - - if e.cfg.flamegraphEnable { - fmt.Println("Waiting for flamegraph") - if err := <-e.flamegraph.Done; err != nil { - e.notifyWarning(fmt.Sprintf("Flamegraph generation failed: %v", err)) - if e.warningCb == nil { - fmt.Println("Flamegraph generation failed:", err) - } - } - } } func (e *eventLoop) events(ctx context.Context, rawCh <-chan []byte) <-chan *event.Pair { diff --git a/internal/eventloop_filter_test.go b/internal/eventloop_filter_test.go index e6e3b45..ddd6451 100644 --- a/internal/eventloop_filter_test.go +++ b/internal/eventloop_filter_test.go @@ -8,7 +8,6 @@ import ( "ior/internal/event" "ior/internal/file" - "ior/internal/flamegraph" "ior/internal/types" ) @@ -443,7 +442,6 @@ func TestCommFilterToggle(t *testing.T) { comms: make(map[uint32]string), prevPairTimes: make(map[uint32]uint64), printCb: func(ep *event.Pair) { outCh <- ep }, - flamegraph: flamegraph.New(), done: make(chan struct{}), } go el.run(ctx, inCh) @@ -483,7 +481,6 @@ func TestCommFilterToggle(t *testing.T) { comms: make(map[uint32]string), prevPairTimes: make(map[uint32]uint64), printCb: func(ep *event.Pair) { outCh <- ep }, - flamegraph: flamegraph.New(), done: make(chan struct{}), } go el.run(ctx, inCh) @@ -518,7 +515,6 @@ func newEventLoopWithFilter(commFilter, pathFilter string) *eventLoop { comms: make(map[uint32]string), prevPairTimes: make(map[uint32]uint64), printCb: func(ep *event.Pair) { fmt.Println(ep); ep.Recycle() }, - flamegraph: flamegraph.New(), done: make(chan struct{}), } return el diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 503aefb..cc6e70a 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -54,23 +54,14 @@ type Flags struct { TracepointsToAttach []*regexp.Regexp TracepointsToExclude []*regexp.Regexp - // Flamegraph flags - PlainMode bool - FlamegraphEnable bool - LiveFlamegraph bool - TestFlames bool - TestLiveFlames bool - LiveInterval time.Duration - OpenCommand string - FlamegraphName string - FlamegraphJSON bool - TUIExportEnable bool - - // To convert ior data into native SVG format - IorDataFile string - IorWatchInterval time.Duration - CollapsedFields []string - CountField string + // Output/runtime flags + PlainMode bool + TestFlames bool + TestLiveFlames bool + LiveInterval time.Duration + TUIExportEnable bool + CollapsedFields []string + CountField string } // NewFlags returns a configuration instance initialized with project defaults. @@ -81,7 +72,6 @@ func NewFlags() Flags { EventMapSize: 4096 * 16, Duration: 900, LiveInterval: 200 * time.Millisecond, - FlamegraphName: "default", TUIExportEnable: true, CollapsedFields: []string{"comm", "tracepoint", "path"}, CountField: "count", @@ -184,19 +174,10 @@ func parse() error { tracepointsToExclude := flag.String("tpsExclude", "", "Comma separated list regexes for tracepoints to exclude") flag.BoolVar(&cfg.PlainMode, "plain", false, "Enable plain CSV output mode (disable TUI)") - flag.BoolVar(&cfg.FlamegraphEnable, "flamegraph", false, "Enable flamegraph builder") - flag.BoolVar(&cfg.LiveFlamegraph, "live", false, "Enable live flamegraph mode") flag.BoolVar(&cfg.TestFlames, "testflames", false, "Run TUI with static synthetic flamegraph data for keyboard-navigation testing") flag.BoolVar(&cfg.TestLiveFlames, "testliveflames", false, "Run TUI with continuously-updating synthetic flamegraph data for live keyboard-navigation testing") - flag.DurationVar(&cfg.LiveInterval, "live-interval", cfg.LiveInterval, "Live flamegraph refresh interval") - flag.StringVar(&cfg.OpenCommand, "open", "", "Command to open live flamegraph URL (used with -live); use {url} placeholder or URL is appended") - flag.StringVar(&cfg.FlamegraphName, "name", cfg.FlamegraphName, "Name of the flamegraph, used to generate the SVG file") - flag.BoolVar(&cfg.FlamegraphJSON, "flamegraphJson", false, "Also export flamegraph tree as JSON in -ior mode (experimental WASM-ready output)") + flag.DurationVar(&cfg.LiveInterval, "live-interval", cfg.LiveInterval, "Synthetic live flamegraph refresh interval for --testliveflames") flag.BoolVar(&cfg.TUIExportEnable, "tuiExport", cfg.TUIExportEnable, "Enable writing TUI snapshot export files") - - flag.StringVar(&cfg.IorDataFile, "ior", "", "IOR data file to convert into native SVG flamegraph") - flag.DurationVar(&cfg.IorWatchInterval, "iorWatchInterval", 0, - "In -ior mode, poll input file for changes and regenerate outputs; also enables auto-reloading viewer") fields := flag.String("fields", "", fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validCollapsedFields)) flag.StringVar(&cfg.CountField, "count", cfg.CountField, diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 7323438..08dd6a2 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -38,15 +38,12 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) { return cfg, err } -func TestParseLiveFlagsAndInterval(t *testing.T) { - cfg, err := parseForTest(t, "-live", "-live-interval", "200ms", "-pid", "1234") +func TestParseLiveIntervalAndPID(t *testing.T) { + cfg, err := parseForTest(t, "-live-interval", "200ms", "-pid", "1234") if err != nil { t.Fatalf("parse returned error: %v", err) } - if !cfg.LiveFlamegraph { - t.Fatalf("expected -live to enable live mode") - } if cfg.LiveInterval != 200*time.Millisecond { t.Fatalf("live interval = %v, want %v", cfg.LiveInterval, 200*time.Millisecond) } @@ -56,9 +53,6 @@ func TestParseLiveFlagsAndInterval(t *testing.T) { if got := Get().GetPidFilter(); got != 1234 { t.Fatalf("Get().GetPidFilter() = %d, want 1234", got) } - if cfg.OpenCommand != "" { - t.Fatalf("expected empty open command by default") - } } func TestNewFlagsDefaultsAndGetters(t *testing.T) { @@ -83,48 +77,9 @@ func TestParseLiveDefaults(t *testing.T) { t.Fatalf("parse returned error: %v", err) } - if cfg.LiveFlamegraph { - t.Fatalf("expected live mode disabled by default") - } if cfg.LiveInterval != 200*time.Millisecond { t.Fatalf("default live interval = %v, want %v", cfg.LiveInterval, 200*time.Millisecond) } - if cfg.OpenCommand != "" { - t.Fatalf("expected empty open command by default") - } -} - -func TestParseOpenFlags(t *testing.T) { - cfg, err := parseForTest(t, "-live", "-open", "chromium --new-window") - if err != nil { - t.Fatalf("parse returned error: %v", err) - } - if !cfg.LiveFlamegraph { - t.Fatalf("expected live mode enabled") - } - if cfg.OpenCommand != "chromium --new-window" { - t.Fatalf("open command = %q, want %q", cfg.OpenCommand, "chromium --new-window") - } -} - -func TestParseFlamegraphJSONFlag(t *testing.T) { - cfg, err := parseForTest(t, "-flamegraphJson") - if err != nil { - t.Fatalf("parse returned error: %v", err) - } - if !cfg.FlamegraphJSON { - t.Fatalf("expected -flamegraphJson to enable JSON export") - } -} - -func TestParseIorWatchIntervalFlag(t *testing.T) { - cfg, err := parseForTest(t, "-iorWatchInterval", "2s") - if err != nil { - t.Fatalf("parse returned error: %v", err) - } - if cfg.IorWatchInterval != 2*time.Second { - t.Fatalf("ior watch interval = %v, want %v", cfg.IorWatchInterval, 2*time.Second) - } } func TestParseTestFlamesFlag(t *testing.T) { diff --git a/internal/flamegraph/doc.go b/internal/flamegraph/doc.go index 8ff27d2..02429d3 100644 --- a/internal/flamegraph/doc.go +++ b/internal/flamegraph/doc.go @@ -1,2 +1,2 @@ -// Package flamegraph builds aggregated call trees and rendering inputs for I/O flamegraphs. +// Package flamegraph provides TUI flamegraph aggregation primitives. package flamegraph diff --git a/internal/flamegraph/iordatacollector.go b/internal/flamegraph/iordatacollector.go deleted file mode 100644 index a2ae731..0000000 --- a/internal/flamegraph/iordatacollector.go +++ /dev/null @@ -1,65 +0,0 @@ -package flamegraph - -import ( - "context" - "fmt" - "runtime" - "sync" - - "ior/internal/event" -) - -type IorDataCollector struct { - flamegraphName string - Ch chan *event.Pair - Done chan error - workers []worker -} - -func New(flamegraphName ...string) IorDataCollector { - name := "default" - if len(flamegraphName) > 0 && flamegraphName[0] != "" { - name = flamegraphName[0] - } - - f := IorDataCollector{ - flamegraphName: name, - Ch: make(chan *event.Pair, 4096), - Done: make(chan error, 1), - } - numWorkers := runtime.NumCPU() / 4 - if numWorkers == 0 { - numWorkers = 1 - } - for range numWorkers { - f.workers = append(f.workers, newWorker()) - } - return f -} - -func (f IorDataCollector) Start(ctx context.Context) { - go func() { - defer close(f.Done) - var wg sync.WaitGroup - wg.Add(len(f.workers)) - - for i, worker := range f.workers { - fmt.Println("Starting flamegraph worker", i) - go worker.run(ctx, &wg, f.Ch) - } - wg.Wait() - - iod := f.workers[0].iod - if len(f.workers) > 1 { - for i, w := range f.workers[1:] { - iod = iod.merge(w.iod) - fmt.Println("Worker", i+1, "merged") - } - } - if err := iod.serializeToFile(f.flamegraphName); err != nil { - f.Done <- err - return - } - f.Done <- nil - }() -} diff --git a/internal/flamegraph/layout.go b/internal/flamegraph/layout.go deleted file mode 100644 index c319800..0000000 --- a/internal/flamegraph/layout.go +++ /dev/null @@ -1,78 +0,0 @@ -package flamegraph - -import "fmt" - -// FrameLayout captures renderer-agnostic flamegraph geometry for a single frame. -// -// The layout is reusable by non-SVG renderers (for example SDL or WASM UIs) so -// they can render the same hierarchy without depending on SVG internals. -type FrameLayout struct { - Name string - Title string - Fill string - X float64 - Y float64 - Width float64 - Height float64 - Depth int - Total uint64 - Percent float64 -} - -func sanitizeSVGConfig(cfg SVGConfig) SVGConfig { - if cfg.Width <= 0 || cfg.FrameHeight <= 0 || cfg.FontSize <= 0 || cfg.MinWidthPx <= 0 { - return defaultSVGConfig() - } - if cfg.Title == "" { - cfg.Title = defaultSVGConfig().Title - } - return cfg -} - -func canvasHeightFor(cfg SVGConfig, t *trie) int { - return cfg.FrameHeight*(t.maxDepth+1) + 80 -} - -// BuildFrameLayout builds renderer-agnostic frame coordinates from a flamegraph trie. -func BuildFrameLayout(t *trie, cfg SVGConfig) []FrameLayout { - if t == nil || t.root == nil || t.root.total == 0 { - return nil - } - cfg = sanitizeSVGConfig(cfg) - canvasHeight := canvasHeightFor(cfg, t) - out := make([]FrameLayout, 0, len(t.root.children)) - collectFrameLayout(&out, t.root, t.root.total, cfg, 0, 0, canvasHeight, true) - return out -} - -func collectFrameLayout(out *[]FrameLayout, node *trieNode, rootTotal uint64, - cfg SVGConfig, x float64, depth int, canvasHeight int, isRoot bool) { - - if !isRoot { - w := float64(cfg.Width) * (float64(node.total) / float64(rootTotal)) - if w < cfg.MinWidthPx { - return - } - y := float64(canvasHeight - (depth+1)*cfg.FrameHeight) - pct := 100 * float64(node.total) / float64(rootTotal) - *out = append(*out, FrameLayout{ - Name: node.name, - Title: fmt.Sprintf("%s (%d, %.2f%%)", node.name, node.total, pct), - Fill: frameColor(node.name), - X: x, - Y: y, - Width: w, - Height: float64(cfg.FrameHeight - 1), - Depth: depth, - Total: node.total, - Percent: pct, - }) - } - - cursor := x - for _, child := range node.children { - cw := float64(cfg.Width) * (float64(child.total) / float64(rootTotal)) - collectFrameLayout(out, child, rootTotal, cfg, cursor, depth+1, canvasHeight, false) - cursor += cw - } -} diff --git a/internal/flamegraph/layout_test.go b/internal/flamegraph/layout_test.go deleted file mode 100644 index 8fa7398..0000000 --- a/internal/flamegraph/layout_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package flamegraph - -import ( - "math" - "testing" -) - -func almostEqual(a, b float64) bool { - return math.Abs(a-b) < 1e-6 -} - -func TestBuildFrameLayoutBasicGeometry(t *testing.T) { - tr := newTrie() - tr.add([]string{"A"}, 4) - tr.add([]string{"B"}, 1) - tr.computeTotals() - - cfg := defaultSVGConfig() - cfg.Width = 100 - cfg.FrameHeight = 10 - cfg.FontSize = 10 - cfg.MinWidthPx = 1 - - frames := BuildFrameLayout(tr, cfg) - if len(frames) != 2 { - t.Fatalf("frames len = %d, want 2", len(frames)) - } - - a := frames[0] - if a.Name != "A" { - t.Fatalf("first frame name = %q, want %q", a.Name, "A") - } - if !almostEqual(a.X, 0) { - t.Fatalf("A x = %f, want 0", a.X) - } - if !almostEqual(a.Width, 80) { - t.Fatalf("A width = %f, want 80", a.Width) - } - if !almostEqual(a.Percent, 80) { - t.Fatalf("A percent = %f, want 80", a.Percent) - } - if a.Depth != 1 { - t.Fatalf("A depth = %d, want 1", a.Depth) - } - - b := frames[1] - if b.Name != "B" { - t.Fatalf("second frame name = %q, want %q", b.Name, "B") - } - if !almostEqual(b.X, 80) { - t.Fatalf("B x = %f, want 80", b.X) - } - if !almostEqual(b.Width, 20) { - t.Fatalf("B width = %f, want 20", b.Width) - } -} - -func TestBuildFrameLayoutSkipsFramesBelowMinWidth(t *testing.T) { - tr := newTrie() - tr.add([]string{"A"}, 999) - tr.add([]string{"B"}, 1) - tr.computeTotals() - - cfg := defaultSVGConfig() - cfg.Width = 100 - cfg.FrameHeight = 10 - cfg.FontSize = 10 - cfg.MinWidthPx = 1 - - frames := BuildFrameLayout(tr, cfg) - if len(frames) != 1 { - t.Fatalf("frames len = %d, want 1", len(frames)) - } - if frames[0].Name != "A" { - t.Fatalf("remaining frame name = %q, want %q", frames[0].Name, "A") - } -} diff --git a/internal/flamegraph/livehtml.go b/internal/flamegraph/livehtml.go deleted file mode 100644 index 71b955e..0000000 --- a/internal/flamegraph/livehtml.go +++ /dev/null @@ -1,842 +0,0 @@ -package flamegraph - -const liveHTML = `<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>I/O Flame Graph (Live)</title> - <style> - :root { - --fg-bg: #f6f1ea; - --fg-panel: #fbf7f1; - --fg-border: #d8cdc0; - --fg-text: #232323; - --fg-muted: #5f5f5f; - --fg-accent: #7b2d1f; - --fg-btn: #efe2d2; - --fg-btn-hover: #e6d5c1; - --fg-paused: #b02222; - } - - * { box-sizing: border-box; } - - body { - margin: 0; - min-height: 100vh; - background: linear-gradient(180deg, #f8f2ea 0%, #f2e9dc 100%); - color: var(--fg-text); - font-family: monospace; - } - - #controls { - position: sticky; - top: 0; - z-index: 1; - display: flex; - gap: 8px; - align-items: center; - flex-wrap: wrap; - padding: 10px 12px; - background: var(--fg-panel); - border-bottom: 1px solid var(--fg-border); - } - - #controls button { - border: 1px solid var(--fg-border); - background: var(--fg-btn); - color: var(--fg-text); - font: inherit; - font-size: 12px; - line-height: 1.2; - padding: 6px 10px; - cursor: pointer; - } - - #controls button:hover { - background: var(--fg-btn-hover); - } - - #controls .order-toggle { - min-width: 220px; - text-align: left; - } - - #status { - margin-left: 8px; - font-size: 12px; - color: var(--fg-muted); - } - - .paused #status { - color: var(--fg-paused); - font-weight: 700; - letter-spacing: 0.03em; - text-transform: uppercase; - } - - #flamegraph { - display: block; - width: 100%; - height: calc(100vh - 56px); - min-height: calc(100vh - 56px); - background: transparent; - } - - .title { - font-size: 14px; - font-family: monospace; - } - - .controls text { - font-size: 12px; - font-family: monospace; - cursor: pointer; - fill: #444; - } - - .frame text { - font-size: 11px; - font-family: monospace; - pointer-events: none; - fill: #111; - } - - .frame rect { - stroke: rgba(0, 0, 0, 0.18); - stroke-width: 0.5; - } - </style> -</head> -<body> - <div id="controls"> - <button id="btn-pause" type="button">Pause</button> - <button id="btn-search" type="button">Search</button> - <button id="btn-reset-search" type="button">Reset Search</button> - <button id="btn-undo-zoom" type="button">Undo Zoom</button> - <button id="btn-reset-zoom" type="button">Reset Zoom</button> - <button id="btn-reset-baseline" type="button">Reset Baseline</button> - <button id="btn-toggle-order" class="order-toggle" type="button">Order: comm > tracepoint > path</button> - <span id="status">LIVE</span> - </div> - - <svg id="flamegraph" xmlns="http://www.w3.org/2000/svg"></svg> - - <script> - (function () { - var fg = { - paused: false, - resetting: false, - lastTreeData: null, - pendingData: null, - searchQuery: '', - zoomStack: [], - zoomRange: null, - frames: [], - rootWidth: 0, - matchColor: 'rgb(220,30,70)', - eventSource: null, - svg: document.getElementById('flamegraph'), - status: document.getElementById('status'), - pauseBtn: document.getElementById('btn-pause'), - searchBtn: document.getElementById('btn-search'), - resetSearchBtn: document.getElementById('btn-reset-search'), - undoZoomBtn: document.getElementById('btn-undo-zoom'), - resetZoomBtn: document.getElementById('btn-reset-zoom'), - |
