mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
Change-Id: I8b224ae48a2f8acd5a64c9ff283e97821479a9a8 Reviewed-on: https://go-review.googlesource.com/c/145457 Run-TryBot: Peter Weinberger <pjw@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
390 lines
11 KiB
Go
390 lines
11 KiB
Go
// Copyright 2014 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 main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
trace "internal/traceparser"
|
|
|
|
"github.com/google/pprof/profile"
|
|
)
|
|
|
|
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"
|
|
}
|
|
|
|
func init() {
|
|
http.HandleFunc("/io", serveSVGProfile(pprofByGoroutine(computePprofIO)))
|
|
http.HandleFunc("/block", serveSVGProfile(pprofByGoroutine(computePprofBlock)))
|
|
http.HandleFunc("/syscall", serveSVGProfile(pprofByGoroutine(computePprofSyscall)))
|
|
http.HandleFunc("/sched", serveSVGProfile(pprofByGoroutine(computePprofSched)))
|
|
|
|
http.HandleFunc("/regionio", serveSVGProfile(pprofByRegion(computePprofIO)))
|
|
http.HandleFunc("/regionblock", serveSVGProfile(pprofByRegion(computePprofBlock)))
|
|
http.HandleFunc("/regionsyscall", serveSVGProfile(pprofByRegion(computePprofSyscall)))
|
|
http.HandleFunc("/regionsched", serveSVGProfile(pprofByRegion(computePprofSched)))
|
|
}
|
|
|
|
// Record represents one entry in pprof-like profiles.
|
|
type Record struct {
|
|
stk []*trace.Frame
|
|
n uint64
|
|
time int64
|
|
}
|
|
|
|
// interval represents a time interval in the trace.
|
|
type interval struct {
|
|
begin, end int64 // nanoseconds.
|
|
}
|
|
|
|
func pprofByGoroutine(compute func(io.Writer, map[uint64][]interval, *trace.Parsed) error) func(w io.Writer, r *http.Request) error {
|
|
return func(w io.Writer, r *http.Request) error {
|
|
id := r.FormValue("id")
|
|
res, err := parseTrace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gToIntervals, err := pprofMatchingGoroutines(id, res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return compute(w, gToIntervals, res)
|
|
}
|
|
}
|
|
|
|
func pprofByRegion(compute func(io.Writer, map[uint64][]interval, *trace.Parsed) error) func(w io.Writer, r *http.Request) error {
|
|
return func(w io.Writer, r *http.Request) error {
|
|
filter, err := newRegionFilter(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gToIntervals, err := pprofMatchingRegions(filter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
events, _ := parseTrace()
|
|
|
|
return compute(w, gToIntervals, events)
|
|
}
|
|
}
|
|
|
|
// pprofMatchingGoroutines parses the goroutine type id string (i.e. pc)
|
|
// and returns the ids of goroutines of the matching type and its interval.
|
|
// If the id string is empty, returns nil without an error.
|
|
func pprofMatchingGoroutines(id string, p *trace.Parsed) (map[uint64][]interval, error) {
|
|
if id == "" {
|
|
return nil, nil
|
|
}
|
|
pc, err := strconv.ParseUint(id, 10, 64) // id is string
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid goroutine type: %v", id)
|
|
}
|
|
analyzeGoroutines(p)
|
|
var res map[uint64][]interval
|
|
for _, g := range gs {
|
|
if g.PC != pc {
|
|
continue
|
|
}
|
|
if res == nil {
|
|
res = make(map[uint64][]interval)
|
|
}
|
|
endTime := g.EndTime
|
|
if g.EndTime == 0 {
|
|
endTime = lastTimestamp() // the trace doesn't include the goroutine end event. Use the trace end time.
|
|
}
|
|
res[g.ID] = []interval{{begin: g.StartTime, end: endTime}}
|
|
}
|
|
if len(res) == 0 && id != "" {
|
|
return nil, fmt.Errorf("failed to find matching goroutines for id: %s", id)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// pprofMatchingRegions returns the time intervals of matching regions
|
|
// grouped by the goroutine id. If the filter is nil, returns nil without an error.
|
|
func pprofMatchingRegions(filter *regionFilter) (map[uint64][]interval, error) {
|
|
res, err := analyzeAnnotations()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filter == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
gToIntervals := make(map[uint64][]interval)
|
|
for id, regions := range res.regions {
|
|
for _, s := range regions {
|
|
if filter.match(id, s) {
|
|
gToIntervals[s.G] = append(gToIntervals[s.G], interval{begin: s.firstTimestamp(), end: s.lastTimestamp()})
|
|
}
|
|
}
|
|
}
|
|
|
|
for g, intervals := range gToIntervals {
|
|
// in order to remove nested regions and
|
|
// consider only the outermost regions,
|
|
// first, we sort based on the start time
|
|
// and then scan through to select only the outermost regions.
|
|
sort.Slice(intervals, func(i, j int) bool {
|
|
x := intervals[i].begin
|
|
y := intervals[j].begin
|
|
if x == y {
|
|
return intervals[i].end < intervals[j].end
|
|
}
|
|
return x < y
|
|
})
|
|
var lastTimestamp int64
|
|
var n int
|
|
// select only the outermost regions.
|
|
for _, i := range intervals {
|
|
if lastTimestamp <= i.begin {
|
|
intervals[n] = i // new non-overlapping region starts.
|
|
lastTimestamp = i.end
|
|
n++
|
|
} // otherwise, skip because this region overlaps with a previous region.
|
|
}
|
|
gToIntervals[g] = intervals[:n]
|
|
}
|
|
return gToIntervals, nil
|
|
}
|
|
|
|
func stklen(p *trace.Parsed, ev *trace.Event) int {
|
|
if ev.StkID == 0 {
|
|
return 0
|
|
}
|
|
return len(p.Stacks[ev.StkID])
|
|
}
|
|
|
|
// computePprofIO generates IO pprof-like profile (time spent in IO wait, currently only network blocking event).
|
|
func computePprofIO(w io.Writer, gToIntervals map[uint64][]interval, res *trace.Parsed) error {
|
|
events := res.Events
|
|
prof := make(map[uint32]Record)
|
|
for _, ev := range events {
|
|
if ev.Type != trace.EvGoBlockNet || ev.Link == nil || ev.StkID == 0 || stklen(res, ev) == 0 {
|
|
continue
|
|
}
|
|
overlapping := pprofOverlappingDuration(gToIntervals, ev)
|
|
if overlapping > 0 {
|
|
rec := prof[ev.StkID]
|
|
rec.stk = res.Stacks[ev.StkID]
|
|
rec.n++
|
|
rec.time += overlapping.Nanoseconds()
|
|
prof[ev.StkID] = rec
|
|
}
|
|
}
|
|
return buildProfile(prof).Write(w)
|
|
}
|
|
|
|
// computePprofBlock generates blocking pprof-like profile (time spent blocked on synchronization primitives).
|
|
func computePprofBlock(w io.Writer, gToIntervals map[uint64][]interval, res *trace.Parsed) error {
|
|
events := res.Events
|
|
prof := make(map[uint32]Record)
|
|
for _, ev := range events {
|
|
switch ev.Type {
|
|
case trace.EvGoBlockSend, trace.EvGoBlockRecv, trace.EvGoBlockSelect,
|
|
trace.EvGoBlockSync, trace.EvGoBlockCond, trace.EvGoBlockGC:
|
|
// TODO(hyangah): figure out why EvGoBlockGC should be here.
|
|
// EvGoBlockGC indicates the goroutine blocks on GC assist, not
|
|
// on synchronization primitives.
|
|
default:
|
|
continue
|
|
}
|
|
if ev.Link == nil || ev.StkID == 0 || stklen(res, ev) == 0 {
|
|
continue
|
|
}
|
|
overlapping := pprofOverlappingDuration(gToIntervals, ev)
|
|
if overlapping > 0 {
|
|
rec := prof[ev.StkID]
|
|
rec.stk = res.Stacks[ev.StkID]
|
|
rec.n++
|
|
rec.time += overlapping.Nanoseconds()
|
|
prof[ev.StkID] = rec
|
|
}
|
|
}
|
|
return buildProfile(prof).Write(w)
|
|
}
|
|
|
|
// computePprofSyscall generates syscall pprof-like profile (time spent blocked in syscalls).
|
|
func computePprofSyscall(w io.Writer, gToIntervals map[uint64][]interval, res *trace.Parsed) error {
|
|
events := res.Events
|
|
prof := make(map[uint32]Record)
|
|
for _, ev := range events {
|
|
if ev.Type != trace.EvGoSysCall || ev.Link == nil || ev.StkID == 0 || stklen(res, ev) == 0 {
|
|
continue
|
|
}
|
|
overlapping := pprofOverlappingDuration(gToIntervals, ev)
|
|
if overlapping > 0 {
|
|
rec := prof[ev.StkID]
|
|
rec.stk = res.Stacks[ev.StkID]
|
|
rec.n++
|
|
rec.time += overlapping.Nanoseconds()
|
|
prof[ev.StkID] = rec
|
|
}
|
|
}
|
|
return buildProfile(prof).Write(w)
|
|
}
|
|
|
|
// computePprofSched generates scheduler latency pprof-like profile
|
|
// (time between a goroutine become runnable and actually scheduled for execution).
|
|
func computePprofSched(w io.Writer, gToIntervals map[uint64][]interval, res *trace.Parsed) error {
|
|
events := res.Events
|
|
prof := make(map[uint32]Record)
|
|
for _, ev := range events {
|
|
if (ev.Type != trace.EvGoUnblock && ev.Type != trace.EvGoCreate) ||
|
|
ev.Link == nil || ev.StkID == 0 || stklen(res, ev) == 0 {
|
|
continue
|
|
}
|
|
overlapping := pprofOverlappingDuration(gToIntervals, ev)
|
|
if overlapping > 0 {
|
|
rec := prof[ev.StkID]
|
|
rec.stk = res.Stacks[ev.StkID]
|
|
rec.n++
|
|
rec.time += overlapping.Nanoseconds()
|
|
prof[ev.StkID] = rec
|
|
}
|
|
}
|
|
return buildProfile(prof).Write(w)
|
|
}
|
|
|
|
// pprofOverlappingDuration returns the overlapping duration between
|
|
// the time intervals in gToIntervals and the specified event.
|
|
// If gToIntervals is nil, this simply returns the event's duration.
|
|
func pprofOverlappingDuration(gToIntervals map[uint64][]interval, ev *trace.Event) time.Duration {
|
|
if gToIntervals == nil { // No filtering.
|
|
return time.Duration(ev.Link.Ts-ev.Ts) * time.Nanosecond
|
|
}
|
|
intervals := gToIntervals[ev.G]
|
|
if len(intervals) == 0 {
|
|
return 0
|
|
}
|
|
|
|
var overlapping time.Duration
|
|
for _, i := range intervals {
|
|
if o := overlappingDuration(i.begin, i.end, ev.Ts, ev.Link.Ts); o > 0 {
|
|
overlapping += o
|
|
}
|
|
}
|
|
return overlapping
|
|
}
|
|
|
|
// serveSVGProfile serves pprof-like profile generated by prof as svg.
|
|
func serveSVGProfile(prof func(w io.Writer, r *http.Request) error) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.FormValue("raw") != "" {
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
if err := prof(w, r); err != nil {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("X-Go-Pprof", "1")
|
|
http.Error(w, fmt.Sprintf("failed to get profile: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
blockf, err := ioutil.TempFile("", "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())
|
|
}()
|
|
blockb := bufio.NewWriter(blockf)
|
|
if err := prof(blockb, r); err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to generate 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)
|
|
}
|
|
}
|
|
|
|
func buildProfile(prof map[uint32]Record) *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.stk {
|
|
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{
|
|
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.n), rec.time},
|
|
Location: sloc,
|
|
})
|
|
}
|
|
return p
|
|
}
|