summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/flags/flags.go2
-rw-r--r--internal/flamegraph/nativesvg.go74
-rw-r--r--internal/flamegraph/svgwriter_test.go109
-rw-r--r--internal/ior.go31
4 files changed, 203 insertions, 13 deletions
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 87ece5d..5bde9c6 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -21,7 +21,7 @@ var (
pidFilter atomic.Int64
)
-const flamegraphToolDefault = "$HOME/git/FlameGraph/flamegraph.pl"
+const flamegraphToolDefault = ""
var (
validCollapsedFields = []string{
diff --git a/internal/flamegraph/nativesvg.go b/internal/flamegraph/nativesvg.go
new file mode 100644
index 0000000..2c76a7d
--- /dev/null
+++ b/internal/flamegraph/nativesvg.go
@@ -0,0 +1,74 @@
+package flamegraph
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "iter"
+)
+
+type NativeSVG struct {
+ fields []string
+ countField string
+ config SVGConfig
+}
+
+func NewNativeSVG(fields []string, countField string) NativeSVG {
+ return NativeSVG{
+ fields: fields,
+ countField: countField,
+ config: defaultSVGConfig(),
+ }
+}
+
+func (n NativeSVG) WriteSVGFromFile(iorDataFile string) error {
+ outFile := fmt.Sprintf("%s.%s-by-%s.svg",
+ strings.TrimSuffix(iorDataFile, ".ior.zst"),
+ strings.Join(n.fields, ":"),
+ n.countField,
+ )
+
+ iod, err := newIorDataFromFile(iorDataFile)
+ if err != nil {
+ return fmt.Errorf("read ior data: %w", err)
+ }
+
+ fd, err := os.Create(outFile)
+ if err != nil {
+ return fmt.Errorf("create output %s: %w", outFile, err)
+ }
+ defer fd.Close()
+
+ return n.WriteSVGFromIter(iod.iter(), fd)
+}
+
+func (n NativeSVG) WriteSVGFromIter(records iter.Seq[IterRecord], w io.Writer) error {
+ tr := newTrie()
+ for record := range records {
+ frames, err := n.recordFrames(record)
+ if err != nil {
+ return err
+ }
+ tr.add(frames, record.Cnt.ValueByName(n.countField))
+ }
+ tr.computeTotals()
+ return WriteSVG(w, tr, n.config)
+}
+
+func (n NativeSVG) recordFrames(record IterRecord) ([]string, error) {
+ var frames []string
+ for _, fieldName := range n.fields {
+ value, err := record.StringByName(fieldName)
+ if err != nil {
+ return nil, fmt.Errorf("field %s: %w", fieldName, err)
+ }
+ for _, part := range strings.Split(value, ";") {
+ if part != "" {
+ frames = append(frames, part)
+ }
+ }
+ }
+ return frames, nil
+}
diff --git a/internal/flamegraph/svgwriter_test.go b/internal/flamegraph/svgwriter_test.go
new file mode 100644
index 0000000..6fdbe3d
--- /dev/null
+++ b/internal/flamegraph/svgwriter_test.go
@@ -0,0 +1,109 @@
+package flamegraph
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func renderSVGForTest(t *testing.T, tr *trie, cfg SVGConfig) string {
+ t.Helper()
+ var buf bytes.Buffer
+ if err := WriteSVG(&buf, tr, cfg); err != nil {
+ t.Fatalf("WriteSVG failed: %v", err)
+ }
+ return buf.String()
+}
+
+func TestWriteSVGBasic(t *testing.T) {
+ tr := newTrie()
+ tr.add([]string{"a", "b"}, 3)
+ tr.add([]string{"a", "c"}, 2)
+ tr.computeTotals()
+
+ svg := renderSVGForTest(t, tr, defaultSVGConfig())
+ if !strings.Contains(svg, "<svg") || !strings.Contains(svg, "</svg>") {
+ t.Fatalf("expected valid svg wrapper, got: %s", svg)
+ }
+ if !strings.Contains(svg, "data-name=\"a\"") || !strings.Contains(svg, "data-name=\"b\"") {
+ t.Fatalf("expected rendered frame names, got: %s", svg)
+ }
+}
+
+func TestWriteSVGEmptyTrie(t *testing.T) {
+ tr := newTrie()
+ tr.computeTotals()
+
+ svg := renderSVGForTest(t, tr, defaultSVGConfig())
+ if !strings.Contains(svg, "<svg") || !strings.Contains(svg, "</svg>") {
+ t.Fatalf("expected valid svg wrapper, got: %s", svg)
+ }
+ if strings.Contains(svg, "class=\"frame\"") {
+ t.Fatalf("expected no rendered frames for empty trie, got: %s", svg)
+ }
+}
+
+func TestWriteSVGMinWidth(t *testing.T) {
+ tr := newTrie()
+ tr.add([]string{"wide"}, 100)
+ tr.add([]string{"tiny"}, 1)
+ tr.computeTotals()
+
+ cfg := defaultSVGConfig()
+ cfg.Width = 120
+ cfg.MinWidthPx = 2.0
+ svg := renderSVGForTest(t, tr, cfg)
+
+ if !strings.Contains(svg, "data-name=\"wide\"") {
+ t.Fatalf("expected wide frame to be rendered, got: %s", svg)
+ }
+ if strings.Contains(svg, "data-name=\"tiny\"") {
+ t.Fatalf("expected tiny frame to be skipped by min width, got: %s", svg)
+ }
+}
+
+func TestWriteSVGTitle(t *testing.T) {
+ tr := newTrie()
+ tr.add([]string{"a"}, 1)
+ tr.computeTotals()
+
+ cfg := defaultSVGConfig()
+ cfg.Title = "Custom Flamegraph"
+ svg := renderSVGForTest(t, tr, cfg)
+
+ if !strings.Contains(svg, "Custom Flamegraph") {
+ t.Fatalf("expected custom title in output, got: %s", svg)
+ }
+}
+
+func TestFrameColor(t *testing.T) {
+ colorA1 := frameColor("read")
+ colorA2 := frameColor("read")
+ colorB := frameColor("write")
+
+ if colorA1 != colorA2 {
+ t.Fatalf("expected deterministic color for identical names, got %q vs %q", colorA1, colorA2)
+ }
+ if !strings.HasPrefix(colorA1, "rgb(") || !strings.HasSuffix(colorA1, ")") {
+ t.Fatalf("expected rgb() format, got %q", colorA1)
+ }
+ if colorA1 == colorB {
+ t.Fatalf("expected different colors for different names, got %q", colorA1)
+ }
+}
+
+func TestWriteSVGInvalidConfigFallsBack(t *testing.T) {
+ tr := newTrie()
+ tr.add([]string{"a"}, 1)
+ tr.computeTotals()
+
+ cfg := SVGConfig{Title: "x", Width: 0, FrameHeight: 0, FontSize: 0, MinWidthPx: 0}
+ svg := renderSVGForTest(t, tr, cfg)
+
+ if !strings.Contains(svg, `width="1200"`) {
+ t.Fatalf("expected fallback width, got: %s", svg)
+ }
+ if !strings.Contains(svg, "I/O Flame Graph") {
+ t.Fatalf("expected fallback title, got: %s", svg)
+ }
+}
diff --git a/internal/ior.go b/internal/ior.go
index e46796b..ee1f988 100644
--- a/internal/ior.go
+++ b/internal/ior.go
@@ -99,18 +99,25 @@ func Run() error {
if iorFile != "" {
noTraceRun = true
- 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
+ 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
+ }
+ } else {
+ native := flamegraph.NewNativeSVG(cfg.CollapsedFields, cfg.CountField)
+ if err := native.WriteSVGFromFile(iorFile); err != nil {
+ return err
+ }
}
}