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
7package traceviewer
8
9import (
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
22type ProfileFunc func(r *http.Request) ([]ProfileRecord, error)
23
24// SVGProfileHandlerFunc serves pprof-like profile generated by prof as svg.
25func 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
84type ProfileRecord struct {
85	Stack []*trace.Frame
86	Count uint64
87	Time  time.Duration
88}
89
90func 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
140func 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