diff options
Diffstat (limited to '')
-rw-r--r-- | src/cmd/trace/pprof.go | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/src/cmd/trace/pprof.go b/src/cmd/trace/pprof.go new file mode 100644 index 0000000..c4d3742 --- /dev/null +++ b/src/cmd/trace/pprof.go @@ -0,0 +1,377 @@ +// 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" + exec "internal/execabs" + "internal/trace" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "time" + + "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.Event) error) func(w io.Writer, r *http.Request) error { + return func(w io.Writer, r *http.Request) error { + id := r.FormValue("id") + events, err := parseEvents() + if err != nil { + return err + } + gToIntervals, err := pprofMatchingGoroutines(id, events) + if err != nil { + return err + } + return compute(w, gToIntervals, events) + } +} + +func pprofByRegion(compute func(io.Writer, map[uint64][]interval, []*trace.Event) 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, _ := parseEvents() + + 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, events []*trace.Event) (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(events) + 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 +} + +// 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, events []*trace.Event) error { + prof := make(map[uint64]Record) + for _, ev := range events { + if ev.Type != trace.EvGoBlockNet || ev.Link == nil || ev.StkID == 0 || len(ev.Stk) == 0 { + continue + } + overlapping := pprofOverlappingDuration(gToIntervals, ev) + if overlapping > 0 { + rec := prof[ev.StkID] + rec.stk = ev.Stk + 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, events []*trace.Event) error { + prof := make(map[uint64]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 || len(ev.Stk) == 0 { + continue + } + overlapping := pprofOverlappingDuration(gToIntervals, ev) + if overlapping > 0 { + rec := prof[ev.StkID] + rec.stk = ev.Stk + 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, events []*trace.Event) error { + prof := make(map[uint64]Record) + for _, ev := range events { + if ev.Type != trace.EvGoSysCall || ev.Link == nil || ev.StkID == 0 || len(ev.Stk) == 0 { + continue + } + overlapping := pprofOverlappingDuration(gToIntervals, ev) + if overlapping > 0 { + rec := prof[ev.StkID] + rec.stk = ev.Stk + 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, events []*trace.Event) error { + prof := make(map[uint64]Record) + for _, ev := range events { + if (ev.Type != trace.EvGoUnblock && ev.Type != trace.EvGoCreate) || + ev.Link == nil || ev.StkID == 0 || len(ev.Stk) == 0 { + continue + } + overlapping := pprofOverlappingDuration(gToIntervals, ev) + if overlapping > 0 { + rec := prof[ev.StkID] + rec.stk = ev.Stk + 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 := 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()) + }() + 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[uint64]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{ + { + 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 +} |