diff options
| -rw-r--r-- | AGENTS.md | 2 | ||||
| -rw-r--r-- | Magefile.go | 2 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | integrationtests/cleanup_test.go | 4 | ||||
| -rw-r--r-- | internal/flags/flags.go | 19 | ||||
| -rw-r--r-- | internal/flamegraph/collapsed.go | 76 | ||||
| -rw-r--r-- | internal/flamegraph/tool.go | 114 | ||||
| -rw-r--r-- | internal/ior.go | 27 |
8 files changed, 20 insertions, 238 deletions
@@ -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 @@ -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 { |
