blob: 5ca71ab8be6d716444e027c3ff14ae4ba64dd204 [file] [log] [blame]
// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package symbolizer provides a routine to populate a profile with
// symbol, file and line number information. It relies on the
// addr2liner and demangle packages to do the actual work.
package symbolizer
import (
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/google/pprof/internal/binutils"
"github.com/google/pprof/internal/plugin"
"github.com/google/pprof/internal/symbolz"
"github.com/google/pprof/profile"
"github.com/ianlancetaylor/demangle"
)
// Symbolizer implements the plugin.Symbolize interface.
type Symbolizer struct {
Obj plugin.ObjTool
UI plugin.UI
Transport http.RoundTripper
}
// test taps for dependency injection
var symbolzSymbolize = symbolz.Symbolize
var localSymbolize = doLocalSymbolize
var demangleFunction = Demangle
// Symbolize attempts to symbolize profile p. First uses binutils on
// local binaries; if the source is a URL it attempts to get any
// missed entries using symbolz.
func (s *Symbolizer) Symbolize(mode string, sources plugin.MappingSources, p *profile.Profile) error {
remote, local, fast, force, demanglerMode := true, true, false, false, ""
for _, o := range strings.Split(strings.ToLower(mode), ":") {
switch o {
case "":
continue
case "none", "no":
return nil
case "local":
remote, local = false, true
case "fastlocal":
remote, local, fast = false, true, true
case "remote":
remote, local = true, false
case "force":
force = true
default:
switch d := strings.TrimPrefix(o, "demangle="); d {
case "full", "none", "templates":
demanglerMode = d
force = true
continue
case "default":
continue
}
s.UI.PrintErr("ignoring unrecognized symbolization option: " + mode)
s.UI.PrintErr("expecting -symbolize=[local|fastlocal|remote|none][:force][:demangle=[none|full|templates|default]")
}
}
var err error
if local {
// Symbolize locally using binutils.
if err = localSymbolize(p, fast, force, s.Obj, s.UI); err != nil {
s.UI.PrintErr("local symbolization: " + err.Error())
}
}
if remote {
post := func(source, post string) ([]byte, error) {
return postURL(source, post, s.Transport)
}
if err = symbolzSymbolize(p, force, sources, post, s.UI); err != nil {
return err // Ran out of options.
}
}
demangleFunction(p, force, demanglerMode)
return nil
}
// postURL issues a POST to a URL over HTTP.
func postURL(source, post string, tr http.RoundTripper) ([]byte, error) {
client := &http.Client{
Transport: tr,
}
resp, err := client.Post(source, "application/octet-stream", strings.NewReader(post))
if err != nil {
return nil, fmt.Errorf("http post %s: %v", source, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http post %s: %v", source, statusCodeError(resp))
}
return io.ReadAll(resp.Body)
}
func statusCodeError(resp *http.Response) error {
if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
// error is from pprof endpoint
if body, err := io.ReadAll(resp.Body); err == nil {
return fmt.Errorf("server response: %s - %s", resp.Status, body)
}
}
return fmt.Errorf("server response: %s", resp.Status)
}
// doLocalSymbolize adds symbol and line number information to all locations
// in a profile. mode enables some options to control
// symbolization.
func doLocalSymbolize(prof *profile.Profile, fast, force bool, obj plugin.ObjTool, ui plugin.UI) error {
if fast {
if bu, ok := obj.(*binutils.Binutils); ok {
bu.SetFastSymbolization(true)
}
}
mt, err := newMapping(prof, obj, ui, force)
if err != nil {
return err
}
defer mt.close()
functions := make(map[profile.Function]*profile.Function)
for _, l := range mt.prof.Location {
m := l.Mapping
segment := mt.segments[m]
if segment == nil {
// Nothing to do.
continue
}
stack, err := segment.SourceLine(l.Address)
if err != nil || len(stack) == 0 {
// No answers from addr2line.
continue
}
l.Line = make([]profile.Line, len(stack))
l.IsFolded = false
for i, frame := range stack {
if frame.Func != "" {
m.HasFunctions = true
}
if frame.File != "" {
m.HasFilenames = true
}
if frame.Line != 0 {
m.HasLineNumbers = true
}
f := &profile.Function{
Name: frame.Func,
SystemName: frame.Func,
Filename: frame.File,
}
if fp := functions[*f]; fp != nil {
f = fp
} else {
functions[*f] = f
f.ID = uint64(len(mt.prof.Function)) + 1
mt.prof.Function = append(mt.prof.Function, f)
}
l.Line[i] = profile.Line{
Function: f,
Line: int64(frame.Line),
Column: int64(frame.Column),
}
}
if len(stack) > 0 {
m.HasInlineFrames = true
}
}
return nil
}
// Demangle updates the function names in a profile with demangled C++
// names, simplified according to demanglerMode. If force is set,
// overwrite any names that appear already demangled.
func Demangle(prof *profile.Profile, force bool, demanglerMode string) {
if force {
// Remove the current demangled names to force demangling
for _, f := range prof.Function {
if f.Name != "" && f.SystemName != "" {
f.Name = f.SystemName
}
}
}
options := demanglerModeToOptions(demanglerMode)
for _, fn := range prof.Function {
demangleSingleFunction(fn, options)
}
}
func demanglerModeToOptions(demanglerMode string) []demangle.Option {
switch demanglerMode {
case "": // demangled, simplified: no parameters, no templates, no return type
return []demangle.Option{demangle.NoParams, demangle.NoEnclosingParams, demangle.NoTemplateParams}
case "templates": // demangled, simplified: no parameters, no return type
return []demangle.Option{demangle.NoParams, demangle.NoEnclosingParams}
case "full":
return []demangle.Option{demangle.NoClones}
case "none": // no demangling
return []demangle.Option{}
}
panic(fmt.Sprintf("unknown demanglerMode %s", demanglerMode))
}
func demangleSingleFunction(fn *profile.Function, options []demangle.Option) {
if fn.Name != "" && fn.SystemName != fn.Name {
return // Already demangled.
}
// Copy the options because they may be updated by the call.
o := make([]demangle.Option, len(options))
copy(o, options)
if demangled := demangle.Filter(fn.SystemName, o...); demangled != fn.SystemName {
fn.Name = demangled
return
}
// Could not demangle. Apply heuristics in case the name is
// already demangled.
name := fn.SystemName
if looksLikeDemangledCPlusPlus(name) {
for _, o := range options {
switch o {
case demangle.NoParams:
name = removeMatching(name, '(', ')')
case demangle.NoTemplateParams:
name = removeMatching(name, '<', '>')
}
}
}
fn.Name = name
}
// looksLikeDemangledCPlusPlus is a heuristic to decide if a name is
// the result of demangling C++. If so, further heuristics will be
// applied to simplify the name.
func looksLikeDemangledCPlusPlus(demangled string) bool {
// Skip java names of the form "class.<init>".
if strings.Contains(demangled, ".<") {
return false
}
// Skip Go names of the form "foo.(*Bar[...]).Method".
if strings.Contains(demangled, "]).") {
return false
}
return strings.ContainsAny(demangled, "<>[]") || strings.Contains(demangled, "::")
}
// removeMatching removes nested instances of start..end from name.
func removeMatching(name string, start, end byte) string {
s := string(start) + string(end)
var nesting, first, current int
for index := strings.IndexAny(name[current:], s); index != -1; index = strings.IndexAny(name[current:], s) {
switch current += index; name[current] {
case start:
nesting++
if nesting == 1 {
first = current
}
case end:
nesting--
switch {
case nesting < 0:
return name // Mismatch, abort
case nesting == 0:
name = name[:first] + name[current+1:]
current = first - 1
}
}
current++
}
return name
}
// newMapping creates a mappingTable for a profile.
func newMapping(prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI, force bool) (*mappingTable, error) {
mt := &mappingTable{
prof: prof,
segments: make(map[*profile.Mapping]plugin.ObjFile),
}
// Identify used mappings
mappings := make(map[*profile.Mapping]bool)
for _, l := range prof.Location {
mappings[l.Mapping] = true
}
missingBinaries := false
for midx, m := range prof.Mapping {
if !mappings[m] {
continue
}
// Do not attempt to re-symbolize a mapping that has already been symbolized.
if !force && (m.HasFunctions || m.HasFilenames || m.HasLineNumbers) {
continue
}
if m.File == "" {
if midx == 0 {
ui.PrintErr("Main binary filename not available.")
continue
}
missingBinaries = true
continue
}
// Skip well-known system mappings
if m.Unsymbolizable() {
continue
}
// Skip mappings pointing to a source URL
if m.BuildID == "" {
if u, err := url.Parse(m.File); err == nil && u.IsAbs() && strings.Contains(strings.ToLower(u.Scheme), "http") {
continue
}
}
name := filepath.Base(m.File)
if m.BuildID != "" {
name += fmt.Sprintf(" (build ID %s)", m.BuildID)
}
f, err := obj.Open(m.File, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol)
if err != nil {
ui.PrintErr("Local symbolization failed for ", name, ": ", err)
missingBinaries = true
continue
}
if fid := f.BuildID(); m.BuildID != "" && fid != "" && fid != m.BuildID {
ui.PrintErr("Local symbolization failed for ", name, ": build ID mismatch")
f.Close()
continue
}
mt.segments[m] = f
}
if missingBinaries {
ui.PrintErr("Some binary filenames not available. Symbolization may be incomplete.\n" +
"Try setting PPROF_BINARY_PATH to the search path for local binaries.")
}
return mt, nil
}
// mappingTable contains the mechanisms for symbolization of a
// profile.
type mappingTable struct {
prof *profile.Profile
segments map[*profile.Mapping]plugin.ObjFile
}
// close releases any external processes being used for the mapping.
func (mt *mappingTable) close() {
for _, segment := range mt.segments {
segment.Close()
}
}