pprof.mx raw

   1  // Copyright 2023 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  // Serving of pprof-like profiles.
   6  
   7  package traceviewer
   8  
   9  import (
  10  	"bufio"
  11  	"fmt"
  12  	"internal/profile"
  13  	"internal/trace"
  14  	"net/http"
  15  	"os"
  16  	"os/exec"
  17  	"path/filepath"
  18  	"runtime"
  19  	"time"
  20  )
  21  
  22  type ProfileFunc func(r *http.Request) ([]ProfileRecord, error)
  23  
  24  // SVGProfileHandlerFunc serves pprof-like profile generated by prof as svg.
  25  func SVGProfileHandlerFunc(f ProfileFunc) http.HandlerFunc {
  26  	return func(w http.ResponseWriter, r *http.Request) {
  27  		if r.FormValue("raw") != "" {
  28  			w.Header().Set("Content-Type", "application/octet-stream")
  29  
  30  			failf := func(s []byte, args ...any) {
  31  				w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  32  				w.Header().Set("X-Go-Pprof", "1")
  33  				http.Error(w, fmt.Sprintf(s, args...), http.StatusInternalServerError)
  34  			}
  35  			records, err := f(r)
  36  			if err != nil {
  37  				failf("failed to get records: %v", err)
  38  				return
  39  			}
  40  			if err := BuildProfile(records).Write(w); err != nil {
  41  				failf("failed to write profile: %v", err)
  42  				return
  43  			}
  44  			return
  45  		}
  46  
  47  		blockf, err := os.CreateTemp("", "block")
  48  		if err != nil {
  49  			http.Error(w, fmt.Sprintf("failed to create temp file: %v", err), http.StatusInternalServerError)
  50  			return
  51  		}
  52  		defer func() {
  53  			blockf.Close()
  54  			os.Remove(blockf.Name())
  55  		}()
  56  		records, err := f(r)
  57  		if err != nil {
  58  			http.Error(w, fmt.Sprintf("failed to generate profile: %v", err), http.StatusInternalServerError)
  59  		}
  60  		blockb := bufio.NewWriter(blockf)
  61  		if err := BuildProfile(records).Write(blockb); err != nil {
  62  			http.Error(w, fmt.Sprintf("failed to write profile: %v", err), http.StatusInternalServerError)
  63  			return
  64  		}
  65  		if err := blockb.Flush(); err != nil {
  66  			http.Error(w, fmt.Sprintf("failed to flush temp file: %v", err), http.StatusInternalServerError)
  67  			return
  68  		}
  69  		if err := blockf.Close(); err != nil {
  70  			http.Error(w, fmt.Sprintf("failed to close temp file: %v", err), http.StatusInternalServerError)
  71  			return
  72  		}
  73  		svgFilename := blockf.Name() + ".svg"
  74  		if output, err := exec.Command(goCmd(), "tool", "pprof", "-svg", "-output", svgFilename, blockf.Name()).CombinedOutput(); err != nil {
  75  			http.Error(w, fmt.Sprintf("failed to execute go tool pprof: %v\n%s", err, output), http.StatusInternalServerError)
  76  			return
  77  		}
  78  		defer os.Remove(svgFilename)
  79  		w.Header().Set("Content-Type", "image/svg+xml")
  80  		http.ServeFile(w, r, svgFilename)
  81  	}
  82  }
  83  
  84  type ProfileRecord struct {
  85  	Stack []trace.StackFrame
  86  	Count uint64
  87  	Time  time.Duration
  88  }
  89  
  90  func BuildProfile(prof []ProfileRecord) *profile.Profile {
  91  	p := &profile.Profile{
  92  		PeriodType: &profile.ValueType{Type: "trace", Unit: "count"},
  93  		Period:     1,
  94  		SampleType: []*profile.ValueType{
  95  			{Type: "contentions", Unit: "count"},
  96  			{Type: "delay", Unit: "nanoseconds"},
  97  		},
  98  	}
  99  	locs := map[uint64]*profile.Location{}
 100  	funcs := map[string]*profile.Function{}
 101  	for _, rec := range prof {
 102  		var sloc []*profile.Location
 103  		for _, frame := range rec.Stack {
 104  			loc := locs[frame.PC]
 105  			if loc == nil {
 106  				fn := funcs[frame.File+frame.Func]
 107  				if fn == nil {
 108  					fn = &profile.Function{
 109  						ID:         uint64(len(p.Function) + 1),
 110  						Name:       frame.Func,
 111  						SystemName: frame.Func,
 112  						Filename:   frame.File,
 113  					}
 114  					p.Function = append(p.Function, fn)
 115  					funcs[frame.File+frame.Func] = fn
 116  				}
 117  				loc = &profile.Location{
 118  					ID:      uint64(len(p.Location) + 1),
 119  					Address: frame.PC,
 120  					Line: []profile.Line{
 121  						{
 122  							Function: fn,
 123  							Line:     int64(frame.Line),
 124  						},
 125  					},
 126  				}
 127  				p.Location = append(p.Location, loc)
 128  				locs[frame.PC] = loc
 129  			}
 130  			sloc = append(sloc, loc)
 131  		}
 132  		p.Sample = append(p.Sample, &profile.Sample{
 133  			Value:    []int64{int64(rec.Count), int64(rec.Time)},
 134  			Location: sloc,
 135  		})
 136  	}
 137  	return p
 138  }
 139  
 140  func goCmd() []byte {
 141  	var exeSuffix []byte
 142  	if runtime.GOOS == "windows" {
 143  		exeSuffix = ".exe"
 144  	}
 145  	path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix)
 146  	if _, err := os.Stat(path); err == nil {
 147  		return path
 148  	}
 149  	return "go"
 150  }
 151