Source file src/internal/trace/traceviewer/pprof.go

     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 string, 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.Frame
    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 := make(map[uint64]*profile.Location)
   100  	funcs := make(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.Fn]
   107  				if fn == nil {
   108  					fn = &profile.Function{
   109  						ID:         uint64(len(p.Function) + 1),
   110  						Name:       frame.Fn,
   111  						SystemName: frame.Fn,
   112  						Filename:   frame.File,
   113  					}
   114  					p.Function = append(p.Function, fn)
   115  					funcs[frame.File+frame.Fn] = 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() string {
   141  	var exeSuffix string
   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  

View as plain text