package flamegraph import ( "bufio" "fmt" "hash" "hash/fnv" "io" "strings" "sync" ) var svgEscaper = strings.NewReplacer( "&", "&", "<", "<", ">", ">", `"`, """, "'", "'", ) var fnv32aPool = sync.Pool{ New: func() any { return fnv.New32a() }, } // SVGConfig controls the layout and styling of generated flamegraph SVGs. // // Width is the virtual canvas width in pixels, FrameHeight is the height of each // stack frame row, FontSize is the base font size, and MinWidthPx controls the // minimum rendered width for a frame (smaller frames are skipped to avoid noise). type SVGConfig struct { Title string Width int FrameHeight int FontSize int MinWidthPx float64 } func defaultSVGConfig() SVGConfig { return SVGConfig{ Title: "I/O Flame Graph", Width: 1200, FrameHeight: 16, FontSize: 12, MinWidthPx: 1.0, } } // DefaultSVGConfig returns the default SVG configuration values. func DefaultSVGConfig() SVGConfig { return defaultSVGConfig() } // WriteSVG renders a flamegraph trie into an interactive SVG document. // // The output is a self-contained SVG that includes embedded CSS and JavaScript // for zoom, search, and highlighting, and is designed to be served directly to // a browser (for example via ServeSVG) without any external assets. func WriteSVG(w io.Writer, t *trie, cfg SVGConfig) error { cfg = sanitizeSVGConfig(cfg) canvasHeight := canvasHeightFor(cfg, t) bw := bufio.NewWriter(w) if err := writeSVGHeader(bw, cfg, canvasHeight); err != nil { return err } for _, frame := range BuildFrameLayout(t, cfg) { if err := writeFrame(bw, frame.Name, frame.Title, frame.Fill, frame.X, frame.Y, frame.Width, frame.Height, frame.Depth, cfg.FontSize); err != nil { return err } } if err := writeSVGFooter(bw); err != nil { return err } return bw.Flush() } func writeSVGHeader(bw *bufio.Writer, cfg SVGConfig, height int) error { _, err := fmt.Fprintf(bw, ``+"\n", height, cfg.Width, height) if err != nil { return err } _, err = fmt.Fprintf(bw, "\n", flamegraphCSS(cfg)) if err != nil { return err } _, err = fmt.Fprintf(bw, "\n", flamegraphJS) if err != nil { return err } _, err = fmt.Fprintf(bw, `%s`+"\n", svgEscape(cfg.Title)) if err != nil { return err } _, err = fmt.Fprintf(bw, `SearchReset SearchUndo ZoomReset Zoom`+"\n") return err } func writeSVGFooter(bw *bufio.Writer) error { _, err := fmt.Fprintln(bw, "") return err } func writeFrame(bw *bufio.Writer, name, title, fill string, x, y, w, h float64, depth, fontSize int) error { textStyle := "" labelStyle := "" if w <= float64(fontSize*2) { labelStyle = ` style="display:none"` } if labelStyle != "" { textStyle = labelStyle } _, err := fmt.Fprintf(bw, ` %s %s `, svgEscape(name), x, w, depth, fill, svgEscape(title), x, y, w, h, fill, x+3, y+float64(fontSize), textStyle, svgEscape(name)) return err } func frameColor(name string) string { hasher := fnv32aPool.Get().(hash.Hash32) hasher.Reset() _, _ = io.WriteString(hasher, name) h := hasher.Sum32() fnv32aPool.Put(hasher) r := 200 + int(h%35) g := 80 + int((h>>8)%120) b := 40 + int((h>>16)%90) return fmt.Sprintf("rgb(%d,%d,%d)", r, g, b) } func flamegraphCSS(cfg SVGConfig) string { return fmt.Sprintf(` .title { font-size: %dpx; font-family: monospace; } .controls text { font-size: %dpx; font-family: monospace; cursor: pointer; fill: #444; } .frame text { font-size: %dpx; font-family: monospace; pointer-events: none; fill: #111; } .frame rect { stroke: rgba(0,0,0,0.18); stroke-width: 0.5; } .title, .controls text, .frame text { user-select: none; -webkit-user-select: none; } `, cfg.FontSize+2, cfg.FontSize, cfg.FontSize-1) } func svgEscape(s string) string { return svgEscaper.Replace(s) }