summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-24 21:19:09 +0200
committerPaul Buetow <paul@buetow.org>2026-02-24 21:19:09 +0200
commit6ea8920dac3b7e3868707a84e58a5d7e10ebbbf3 (patch)
tree63138d32b6197522855a0b69e3c09068a0e1df41
parent93d587a6f5ae453907de3d5556866b60bac405cb (diff)
flamegraph: remove external tool path and document native generation
-rw-r--r--AGENTS.md2
-rw-r--r--Magefile.go2
-rw-r--r--README.md14
-rw-r--r--integrationtests/cleanup_test.go4
-rw-r--r--internal/flags/flags.go19
-rw-r--r--internal/flamegraph/collapsed.go76
-rw-r--r--internal/flamegraph/tool.go114
-rw-r--r--internal/ior.go27
8 files changed, 20 insertions, 238 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 71fec53..36d2bb1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,7 +45,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 zstd files, collapsed stack format compatible with Inferno FlameGraphs
+- **Output**: Compressed `.ior.zst` trace data and native SVG flamegraphs (served via embedded web server in `-ior` 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.
diff --git a/Magefile.go b/Magefile.go
index 57ce61c..09b219c 100644
--- a/Magefile.go
+++ b/Magefile.go
@@ -238,7 +238,7 @@ func Clean() error {
// Mrproper removes build artifacts and generated output files.
func Mrproper() error {
mg.SerialDeps(Clean)
- patterns := []string{"*.zst", "*.collapsed", "*.svg", "*profile", "*.pdf", "*.tmp", "palette.map"}
+ patterns := []string{"*.zst", "*.svg", "*profile", "*.pdf", "*.tmp", "palette.map"}
for _, pattern := range patterns {
if err := removeFilesByGlob(pattern); err != nil {
return err
diff --git a/README.md b/README.md
index dce3849..e5c41bb 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Important details:
- `durationToPrevNs` is tracked per `tid` (thread), not globally across all threads.
- The first observed syscall pair for a thread has `durationToPrevNs = 0` because there is no prior exit timestamp.
- `durationToPrevNs` is attributed to the current syscall pair (the one whose `enter` closes the gap).
-- There is no separate "idle" pseudo-event bucket; use the `durationToPrev` count field when collapsed/aggregated output should emphasize inter-syscall time.
+- There is no separate "idle" pseudo-event bucket; use the `durationToPrev` count field when aggregated flamegraph output should emphasize inter-syscall time.
## Fedora
@@ -57,10 +57,16 @@ make
sudo cp -v ./libelf/libelf.a /usr/lib64/
```
-## Inferno Flamegraphs
+## Native Flamegraph Generation
-We are using Inferno Flamegraphs: https://github.com/jonhoo/inferno
+Flamegraphs are generated natively by `ior` from `.ior.zst` data files; no external flamegraph tool is required.
```sh
-cargo install inferno
+./ior -ior=trace.ior.zst -fields=pid,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
```
diff --git a/integrationtests/cleanup_test.go b/integrationtests/cleanup_test.go
index 6fecd10..18a0531 100644
--- a/integrationtests/cleanup_test.go
+++ b/integrationtests/cleanup_test.go
@@ -55,7 +55,7 @@ func TestCleanupOutputDirContainsOnlyExpectedFiles(t *testing.T) {
workloadBin := writeScript(t, tmpDir, "workload", `echo $$`)
iorBin := writeScript(t, tmpDir, "ior",
- `touch test.ior.zst test.collapsed.zst test.svg`)
+ `touch test.ior.zst test.svg`)
h := TestHarness{
IorBinary: iorBin,
@@ -80,7 +80,6 @@ func TestCleanupOutputDirContainsOnlyExpectedFiles(t *testing.T) {
for _, e := range entries {
name := e.Name()
validSuffix := strings.HasSuffix(name, ".ior.zst") ||
- strings.HasSuffix(name, ".collapsed.zst") ||
strings.HasSuffix(name, ".svg") ||
name == "ior.bpf.o" // symlink created by startIor
if !validSuffix {
@@ -204,7 +203,6 @@ func TestCleanupNoArtifactsOutsideOutputDir(t *testing.T) {
}
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".ior.zst") ||
- strings.HasSuffix(e.Name(), ".collapsed.zst") ||
strings.HasSuffix(e.Name(), ".svg") {
t.Errorf("artifact leaked to script dir: %s", e.Name())
}
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 5bde9c6..9354bc3 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -21,8 +21,6 @@ var (
pidFilter atomic.Int64
)
-const flamegraphToolDefault = ""
-
var (
validCollapsedFields = []string{
"path",
@@ -60,13 +58,10 @@ type Flags struct {
FlamegraphName string
TUIExportEnable bool
- // To convert ior data into collapsed format
+ // To convert ior data into native SVG format
IorDataFile string
CollapsedFields []string
CountField string
-
- // To generate the Flamegraph SVGs
- FlamegraphTool string
}
func Get() Flags {
@@ -110,22 +105,14 @@ func parse() {
flag.StringVar(&singleton.FlamegraphName, "name", "default", "Name of the flamegraph, used to generate the SVG file")
flag.BoolVar(&singleton.TUIExportEnable, "tuiExport", true, "Enable writing TUI snapshot export files")
- flag.StringVar(&singleton.IorDataFile, "ior", "", "IOR data file to convert into collapsed format")
+ flag.StringVar(&singleton.IorDataFile, "ior", "", "IOR data file to convert into native SVG flamegraph")
fields := flag.String("fields", "",
fmt.Sprintf("Comma separated list of fields to collapse, valid are: %v", validCollapsedFields))
flag.StringVar(&singleton.CountField, "count", "count",
- fmt.Sprintf("Count field to collaps, valid are: %v", validCollapsedCounts))
-
- // https://github.com/brendangregg/FlameGraph
- flag.StringVar(&singleton.FlamegraphTool, "flamegraphTool",
- "", "Path to the flamegraph tool (e.g. flamegraph.pl or inferno-flamegraph)")
+ fmt.Sprintf("Count field to collapse, valid are: %v", validCollapsedCounts))
flag.Parse()
pidFilter.Store(int64(singleton.PidFilter))
- if singleton.FlamegraphTool == "" {
- singleton.FlamegraphTool = flamegraphToolDefault
- }
-
singleton.TracepointsToAttach = extractTracepointFlags(*tracepointsToAttach)
singleton.TracepointsToExclude = extractTracepointFlags(*tracepointsToExclude)
diff --git a/internal/flamegraph/collapsed.go b/internal/flamegraph/collapsed.go
deleted file mode 100644
index f04a38d..0000000
--- a/internal/flamegraph/collapsed.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package flamegraph
-
-import (
- "fmt"
- "os"
- "strings"
-
- "github.com/DataDog/zstd"
-)
-
-// Collapsed represents a structure used to process and store information
-// related to a collapsed flamegraph. It includes the following fields:
-// - iorFile: The path to the input/output report file.
-// - fields: A list of field names used in the flamegraph processing.
-// - countField: The name of the field that represents the count or weight
-// in the flamegraph data.
-type Collapsed struct {
- iorFile string // Path to the input/output report file.
- fields []string // List of field names used in processing.
- countField string // Field name representing the count or weight.
-}
-
-func NewCollapsed(iorFile string, fields []string, countField string) Collapsed {
- return Collapsed{iorFile: iorFile, fields: fields, countField: countField}
-}
-
-func (c Collapsed) Write(iorDataFile string) (string, error) {
- outFile := fmt.Sprintf("%s.%s-by-%s.collapsed.zst",
- strings.TrimSuffix(iorDataFile, ".ior.zst"),
- strings.Join(c.fields, ":"),
- c.countField,
- )
-
- if _, err := os.Stat(outFile); err == nil {
- fmt.Println(outFile, "already exists!")
- return outFile, nil
- }
-
- // outFD should be zstd compressed
- outFd, err := os.Create(outFile)
- if err != nil {
- return outFile, err
- }
- defer outFd.Close()
-
- fmt.Println("Reading", iorDataFile)
- iod, err := newIorDataFromFile(iorDataFile)
- if err != nil {
- return outFile, err
- }
-
- fmt.Println("Writing", outFile)
- writer := zstd.NewWriter(outFd)
- if err != nil {
- return outFile, err
- }
- defer writer.Close()
-
- for record := range iod.iter() {
- var fieldValues []string
- for _, fieldName := range c.fields {
- v, err := record.StringByName(fieldName)
- if err != nil {
- return outFile, fmt.Errorf("field %s: %w", fieldName, err)
- }
- fieldValues = append(fieldValues, v)
- }
- writer.Write([]byte(fmt.Sprintf("%s %d\n",
- strings.Join(fieldValues, ";"),
- record.Cnt.ValueByName(c.countField),
- )))
- }
- writer.Flush()
-
- return outFile, nil
-}
diff --git a/internal/flamegraph/tool.go b/internal/flamegraph/tool.go
deleted file mode 100644
index a83c44f..0000000
--- a/internal/flamegraph/tool.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package flamegraph
-
-import (
- "fmt"
- "io"
- "ior/internal/flags"
- "os"
- "os/exec"
- "strings"
-
- "github.com/DataDog/zstd"
-)
-
-// Tool represents a utility for generating flamegraphs.
-// It contains the path to the flamegraph tool, the arguments to be passed to it,
-// and the output file where the generated flamegraph will be stored.
-type Tool struct {
- flamegraphTool string // Path to the flamegraph tool executable.
- args []string // Arguments to be passed to the flamegraph tool.
- outFile string // Path to the output file where the flamegraph will be saved.
-}
-
-func NewTool(collapsedFile string) (Tool, error) {
- if strings.HasSuffix(collapsedFile, ".zst") {
- var err error
- collapsedFile, err = decompress(collapsedFile)
- if err != nil {
- return Tool{}, err
- }
- }
-
- t := Tool{
- flamegraphTool: flags.Get().FlamegraphTool,
- args: []string{collapsedFile, "--hash"},
- outFile: strings.TrimSuffix(collapsedFile, ".collapsed") + ".svg",
- }
-
- t.args = append(t.args, "--title")
- t.args = append(t.args, fmt.Sprintf("I/O Traces (%s by %s)",
- strings.Join(flags.Get().CollapsedFields, ","), flags.Get().CountField,
- ))
-
- return t, nil
-}
-
-func (t Tool) WriteSVG() error {
- defer deleteFileIfEmpty(t.outFile)
-
- if _, err := os.Stat(t.outFile); err == nil {
- fmt.Println(t.outFile, "already exists!")
- return nil
- }
- cmd := exec.Command(t.flamegraphTool, t.args...)
- fmt.Println("Running", cmd)
-
- outFd, err := os.Create(t.outFile)
- if err != nil {
- return err
- }
- defer outFd.Close()
-
- cmd.Stdout = outFd
- cmd.Stderr = os.Stderr
-
- if err := cmd.Run(); err != nil {
- return err
- }
-
- return nil
-}
-
-func (t Tool) OutFile() string {
- return t.outFile
-}
-
-func decompress(compressedFile string) (string, error) {
- decompressedFile := strings.TrimSuffix(compressedFile, ".zst")
-
- file, err := os.Open(compressedFile)
- if err != nil {
- return decompressedFile, err
- }
- defer file.Close()
-
- decoder := zstd.NewReader(file)
- defer decoder.Close()
-
- decompressedFd, err := os.Create(decompressedFile)
- if err != nil {
- return decompressedFile, err
- }
- defer decompressedFd.Close()
-
- _, err = io.Copy(decompressedFd, decoder)
- if err != nil {
- return decompressedFile, err
- }
-
- return decompressedFile, nil
-}
-
-func deleteFileIfEmpty(file string) error {
- if _, err := os.Stat(file); err == nil {
- fileInfo, err := os.Stat(file)
- if err != nil {
- return err
- }
- if fileInfo.Size() == 0 {
- fmt.Println("Deleting", file, "as it is empty")
- return os.Remove(file)
- }
- }
- return nil
-}
diff --git a/internal/ior.go b/internal/ior.go
index 599736d..b136931 100644
--- a/internal/ior.go
+++ b/internal/ior.go
@@ -99,29 +99,10 @@ func Run() error {
if iorFile != "" {
noTraceRun = true
- var svgFile string
- if cfg.FlamegraphTool != "" {
- collapsed := flamegraph.NewCollapsed(iorFile, cfg.CollapsedFields, cfg.CountField)
- collapsedFile, err := collapsed.Write(iorFile)
- if err != nil {
- return err
- }
-
- tool, err := flamegraph.NewTool(collapsedFile)
- if err != nil {
- return err
- }
- if err := tool.WriteSVG(); err != nil {
- return err
- }
- svgFile = tool.OutFile()
- } else {
- native := flamegraph.NewNativeSVG(cfg.CollapsedFields, cfg.CountField)
- var err error
- svgFile, err = native.WriteSVGFromFile(iorFile)
- if err != nil {
- return err
- }
+ native := flamegraph.NewNativeSVG(cfg.CollapsedFields, cfg.CountField)
+ svgFile, err := native.WriteSVGFromFile(iorFile)
+ if err != nil {
+ return err
}
if err := flamegraph.ServeSVG(svgFile); err != nil {