blob: 1377b3c614c81c6031300a1a0767267e6ffdcd85 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Serving of pprof-like profiles.
package traceviewer
import (
"bufio"
"fmt"
"internal/profile"
"internal/trace"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
)
type ProfileFunc func(r *http.Request) ([]ProfileRecord, error)
// SVGProfileHandlerFunc serves pprof-like profile generated by prof as svg.
func SVGProfileHandlerFunc(f ProfileFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.FormValue("raw") != "" {
w.Header().Set("Content-Type", "application/octet-stream")
failf := func(s string, args ...any) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Go-Pprof", "1")
http.Error(w, fmt.Sprintf(s, args...), http.StatusInternalServerError)
}
records, err := f(r)
if err != nil {
failf("failed to get records: %v", err)
return
}
if err := BuildProfile(records).Write(w); err != nil {
failf("failed to write profile: %v", err)
return
}
return
}
blockf, err := os.CreateTemp("", "block")
if err != nil {
http.Error(w, fmt.Sprintf("failed to create temp file: %v", err), http.StatusInternalServerError)
return
}
defer func() {
blockf.Close()
os.Remove(blockf.Name())
}()
records, err := f(r)
if err != nil {
http.Error(w, fmt.Sprintf("failed to generate profile: %v", err), http.StatusInternalServerError)
}
blockb := bufio.NewWriter(blockf)
if err := BuildProfile(records).Write(blockb); err != nil {
http.Error(w, fmt.Sprintf("failed to write profile: %v", err), http.StatusInternalServerError)
return
}
if err := blockb.Flush(); err != nil {
http.Error(w, fmt.Sprintf("failed to flush temp file: %v", err), http.StatusInternalServerError)
return
}
if err := blockf.Close(); err != nil {
http.Error(w, fmt.Sprintf("failed to close temp file: %v", err), http.StatusInternalServerError)
return
}
svgFilename := blockf.Name() + ".svg"
if output, err := exec.Command(goCmd(), "tool", "pprof", "-svg", "-output", svgFilename, blockf.Name()).CombinedOutput(); err != nil {
http.Error(w, fmt.Sprintf("failed to execute go tool pprof: %v\n%s", err, output), http.StatusInternalServerError)
return
}
defer os.Remove(svgFilename)
w.Header().Set("Content-Type", "image/svg+xml")
http.ServeFile(w, r, svgFilename)
}
}
type ProfileRecord struct {
Stack []*trace.Frame
Count uint64
Time time.Duration
}
func BuildProfile(prof []ProfileRecord) *profile.Profile {
p := &profile.Profile{
PeriodType: &profile.ValueType{Type: "trace", Unit: "count"},
Period: 1,
SampleType: []*profile.ValueType{
{Type: "contentions", Unit: "count"},
{Type: "delay", Unit: "nanoseconds"},
},
}
locs := make(map[uint64]*profile.Location)
funcs := make(map[string]*profile.Function)
for _, rec := range prof {
var sloc []*profile.Location
for _, frame := range rec.Stack {
loc := locs[frame.PC]
if loc == nil {
fn := funcs[frame.File+frame.Fn]
if fn == nil {
fn = &profile.Function{
ID: uint64(len(p.Function) + 1),
Name: frame.Fn,
SystemName: frame.Fn,
Filename: frame.File,
}
p.Function = append(p.Function, fn)
funcs[frame.File+frame.Fn] = fn
}
loc = &profile.Location{
ID: uint64(len(p.Location) + 1),
Address: frame.PC,
Line: []profile.Line{
{
Function: fn,
Line: int64(frame.Line),
},
},
}
p.Location = append(p.Location, loc)
locs[frame.PC] = loc
}
sloc = append(sloc, loc)
}
p.Sample = append(p.Sample, &profile.Sample{
Value: []int64{int64(rec.Count), int64(rec.Time)},
Location: sloc,
})
}
return p
}
func goCmd() string {
var exeSuffix string
if runtime.GOOS == "windows" {
exeSuffix = ".exe"
}
path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix)
if _, err := os.Stat(path); err == nil {
return path
}
return "go"
}