feat: add pprof

This commit is contained in:
langhuihui
2024-12-16 20:06:39 +08:00
parent c1616740ec
commit b3a3e37429
220 changed files with 36494 additions and 56 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: monibuca
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

96
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
name: Go
on:
push:
branches:
- v5
env:
dest: bin
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Set up Env
run: echo "version=${GITHUB_REF:11}" >> $GITHUB_ENV
- name: Set beta
if: contains(env.version, 'beta')
run: echo "dest=beta" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.23.4
- name: Cache Go modules
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: runner.osgo{ { hashFiles('**/go.sum') } }
restore-keys: ${{ runner.os }}-go-
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
if: success() && startsWith(github.ref, 'refs/tags/')
with:
version: v1.8.3
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Checkout m7s-import
# uses: actions/checkout@v3
# with:
# repository: langhuihui/m7s-import
# path: m7s-import
# persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
# fetch-depth: 0
# - name: Add bin to m7s-import
# if: success() && startsWith(github.ref, 'refs/tags/')
# run: |
# cd m7s-import
# mkdir -p apps/m7s-website/src/public/bin
# cp ../dist/m7s_${{ env.version }}_windows_amd64.tar.gz apps/m7s-website/src/public/bin/m7s_windows_amd64.tar.gz
# cp ../dist/m7s_${{ env.version }}_darwin_amd64.tar.gz apps/m7s-website/src/public/bin/m7s_darwin_amd64.tar.gz
# cp ../dist/m7s_${{ env.version }}_darwin_arm64.tar.gz apps/m7s-website/src/public/bin/m7s_darwin_arm64.tar.gz
# cp ../dist/m7s_${{ env.version }}_linux_amd64.tar.gz apps/m7s-website/src/public/bin/m7s_linux_amd64.tar.gz
# cp ../dist/m7s_${{ env.version }}_linux_arm64.tar.gz apps/m7s-website/src/public/bin/m7s_linux_arm64.tar.gz
# ls apps/m7s-website/src/public/bin
- name: copy
if: success() && startsWith(github.ref, 'refs/tags/')
run: |
mkdir -p bin
cp dist/m7s_${{ env.version }}_windows_amd64.tar.gz bin/m7s_windows_amd64.tar.gz
cp dist/m7s_${{ env.version }}_darwin_amd64.tar.gz bin/m7s_darwin_amd64.tar.gz
cp dist/m7s_${{ env.version }}_darwin_arm64.tar.gz bin/m7s_darwin_arm64.tar.gz
cp dist/m7s_${{ env.version }}_linux_amd64.tar.gz bin/m7s_linux_amd64.tar.gz
cp dist/m7s_${{ env.version }}_linux_arm64.tar.gz bin/m7s_linux_arm64.tar.gz
ls bin
- uses: jakejarvis/s3-sync-action@master
# with:
# args: --acl public-read --follow-symlinks --delete
env:
AWS_S3_ENDPOINT: https://${{ secrets.R2_DOMAIN }}
AWS_ACCESS_KEY_ID: ${{ secrets.R2_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET }}
AWS_S3_BUCKET: monibuca
SOURCE_DIR: 'bin'
DEST_DIR: ${{ env.dest }}
- name: docker build
if: success() && startsWith(github.ref, 'refs/tags/')
run: |
tar -zxvf bin/m7s_linux_amd64.tar.gz
mv m7s monibuca_linux
docker login -u langhuihui -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t langhuihui/monibuca:v5 .
docker push langhuihui/monibuca:v5
- name: docker push
if: success() && !contains(env.version, 'beta')
run: |
docker tag langhuihui/monibuca:latest langhuihui/monibuca:${{ env.version }}
docker push langhuihui/monibuca:${{ env.version }}

113
api.go
View File

@@ -740,6 +740,28 @@ func (s *Server) AddPullProxy(ctx context.Context, req *pb.PullProxyInfo) (res *
Description: req.Description,
StreamPath: req.StreamPath,
}
if device.Type == "" {
var u *url.URL
u, err = url.Parse(req.PullURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
device.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
device.Type = "hls"
case ".flv":
device.Type = "flv"
case ".mp4":
device.Type = "mp4"
}
}
}
defaults.SetDefaults(&device.Pull)
defaults.SetDefaults(&device.Record)
device.URL = req.PullURL
@@ -763,11 +785,36 @@ func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (re
return
}
target := &PullProxy{}
s.DB.First(target, req.ID)
err = s.DB.First(target, req.ID).Error
if err != nil {
return
}
target.Name = req.Name
target.URL = req.PullURL
target.ParentID = uint(req.ParentID)
target.Type = req.Type
if target.Type == "" {
var u *url.URL
u, err = url.Parse(req.PullURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
target.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
target.Type = "hls"
case ".flv":
target.Type = "flv"
case ".mp4":
target.Type = "mp4"
}
}
}
target.PullOnStart = req.PullOnStart
target.StopOnIdle = req.StopOnIdle
target.Audio = req.Audio
@@ -777,6 +824,24 @@ func (s *Server) UpdatePullProxy(ctx context.Context, req *pb.PullProxyInfo) (re
target.RTT = time.Duration(int(req.Rtt)) * time.Millisecond
target.StreamPath = req.StreamPath
s.DB.Save(target)
s.PullProxies.Call(func() error {
if device, ok := s.PullProxies.Get(uint(req.ID)); ok {
if target.URL != device.URL || device.Audio != target.Audio || device.StreamPath != target.StreamPath || device.Record.FilePath != target.Record.FilePath || device.Record.Fragment != target.Record.Fragment {
device.Stop(task.ErrStopByUser)
device.WaitStopped()
s.PullProxies.Add(target)
return nil
}
if device.PullOnStart != target.PullOnStart && target.PullOnStart && device.Handler != nil && device.Status == PullProxyStatusOnline {
device.Handler.Pull()
}
device.Name = target.Name
device.PullOnStart = target.PullOnStart
device.StopOnIdle = target.StopOnIdle
device.Description = target.Description
}
return nil
})
res = &pb.SuccessResponse{}
return
}
@@ -951,6 +1016,30 @@ func (s *Server) AddPushProxy(ctx context.Context, req *pb.PushProxyInfo) (res *
Description: req.Description,
StreamPath: req.StreamPath,
}
if device.Type == "" {
var u *url.URL
u, err = url.Parse(req.PushURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
device.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
device.Type = "hls"
case ".flv":
device.Type = "flv"
case ".mp4":
device.Type = "mp4"
}
}
}
defaults.SetDefaults(&device.Push)
device.URL = req.PushURL
device.Audio = req.Audio
@@ -975,6 +1064,28 @@ func (s *Server) UpdatePushProxy(ctx context.Context, req *pb.PushProxyInfo) (re
target.URL = req.PushURL
target.ParentID = uint(req.ParentID)
target.Type = req.Type
if target.Type == "" {
var u *url.URL
u, err = url.Parse(req.PushURL)
if err != nil {
s.Error("parse pull url failed", "error", err)
return
}
switch u.Scheme {
case "srt", "rtsp", "rtmp":
target.Type = u.Scheme
default:
ext := filepath.Ext(u.Path)
switch ext {
case ".m3u8":
target.Type = "hls"
case ".flv":
target.Type = "flv"
case ".mp4":
target.Type = "mp4"
}
}
}
target.PushOnStart = req.PushOnStart
target.Audio = req.Audio
target.Description = req.Description

1
go.mod
View File

@@ -129,6 +129,7 @@ require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/gorilla/websocket v1.5.1
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/phsym/console-slog v0.3.1
github.com/prometheus/client_golang v1.20.4

2
go.sum
View File

@@ -128,6 +128,8 @@ github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 h1:4Jk58quTZmzJcTrLlbB5L1Q6qXu49EIjCReWxcBFWKo=
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8/go.mod h1:medl9/CfYoQlqAXtAARmMW5dAX2UOdwwkhaszYPk0AM=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM=
github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

24
goreleaser.yml Normal file
View File

@@ -0,0 +1,24 @@
project_name: m7s
archives:
-
files:
- favicon.ico
builds:
- id: "all"
main: ./example/default/main.go
env:
- CGO_ENABLED=0
tags:
- sqlite
ldflags:
- -s -w -X main.version={{.Tag}}
goos:
- linux
- windows
- darwin
goarch:
- arm64
- amd64
hooks:
pre:
- go mod tidy

View File

@@ -1,23 +1,33 @@
package plugin_debug
import (
myproc "github.com/cloudwego/goref/pkg/proc"
"github.com/go-delve/delve/pkg/config"
"github.com/go-delve/delve/service/debugger"
"context"
"fmt"
"io"
"m7s.live/v5"
"net/http"
"net/http/pprof"
"os"
"runtime"
runtimePPROF "runtime/pprof"
"sort"
"strings"
"time"
myproc "github.com/cloudwego/goref/pkg/proc"
"github.com/go-delve/delve/pkg/config"
"github.com/go-delve/delve/service/debugger"
"google.golang.org/protobuf/types/known/emptypb"
"m7s.live/v5"
"m7s.live/v5/plugin/debug/pb"
debug "m7s.live/v5/plugin/debug/pkg"
"m7s.live/v5/plugin/debug/pkg/profile"
)
var _ = m7s.InstallPlugin[DebugPlugin]()
var _ = m7s.InstallPlugin[DebugPlugin](&pb.Api_ServiceDesc, pb.RegisterApiHandler)
var conf, _ = config.LoadConfig()
type DebugPlugin struct {
pb.UnimplementedApiServer
m7s.Plugin
ProfileDuration time.Duration `default:"10s" desc:"profile持续时间"`
Profile string `desc:"采集profile存储文件"`
@@ -114,3 +124,170 @@ func (p *DebugPlugin) Grf(w http.ResponseWriter, r *http.Request) {
}
w.Write([]byte("ok"))
}
func (p *DebugPlugin) GetHeap(ctx context.Context, empty *emptypb.Empty) (*pb.HeapResponse, error) {
// 创建临时文件用于存储堆信息
f, err := os.CreateTemp("", "heap")
if err != nil {
return nil, err
}
defer os.Remove(f.Name())
defer f.Close()
// 获取堆信息
runtime.GC()
if err := runtimePPROF.WriteHeapProfile(f); err != nil {
return nil, err
}
// 读取堆信息
f.Seek(0, 0)
prof, err := profile.Parse(f)
if err != nil {
return nil, err
}
// 准备响应数据
resp := &pb.HeapResponse{
Data: &pb.HeapData{
Stats: &pb.HeapStats{},
Objects: make([]*pb.HeapObject, 0),
Edges: make([]*pb.HeapEdge, 0),
},
}
// 创建类型映射用于聚合统计
typeMap := make(map[string]*pb.HeapObject)
var totalSize int64
// 处理每个样本
for _, sample := range prof.Sample {
size := sample.Value[1] // 内存大小
if size == 0 {
continue
}
// 获取分配类型信息
var typeName string
if len(sample.Location) > 0 && len(sample.Location[0].Line) > 0 {
if fn := sample.Location[0].Line[0].Function; fn != nil {
typeName = fn.Name
}
}
// 创建或更新堆对象
obj, exists := typeMap[typeName]
if !exists {
obj = &pb.HeapObject{
Type: typeName,
Address: fmt.Sprintf("%p", sample),
Refs: make([]string, 0),
}
typeMap[typeName] = obj
resp.Data.Objects = append(resp.Data.Objects, obj)
}
obj.Count++
obj.Size += size
totalSize += size
// 构建引用关系
for i := 1; i < len(sample.Location); i++ {
loc := sample.Location[i]
if len(loc.Line) == 0 || loc.Line[0].Function == nil {
continue
}
callerName := loc.Line[0].Function.Name
// 跳过系统函数
if callerName == "" || strings.HasPrefix(callerName, "runtime.") {
continue
}
// 添加边
edge := &pb.HeapEdge{
From: callerName,
To: typeName,
FieldName: callerName,
}
resp.Data.Edges = append(resp.Data.Edges, edge)
// 将调用者添加到引用列表
if !contains(obj.Refs, callerName) {
obj.Refs = append(obj.Refs, callerName)
}
}
}
// 计算百分比
for _, obj := range resp.Data.Objects {
if totalSize > 0 {
obj.SizePerc = float64(obj.Size) / float64(totalSize) * 100
}
}
// 按大小排序
sort.Slice(resp.Data.Objects, func(i, j int) bool {
return resp.Data.Objects[i].Size > resp.Data.Objects[j].Size
})
// 获取运行时内存统计
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
// 填充内存统计信息
resp.Data.Stats.Alloc = ms.Alloc
resp.Data.Stats.TotalAlloc = ms.TotalAlloc
resp.Data.Stats.Sys = ms.Sys
resp.Data.Stats.NumGC = ms.NumGC
resp.Data.Stats.HeapAlloc = ms.HeapAlloc
resp.Data.Stats.HeapSys = ms.HeapSys
resp.Data.Stats.HeapIdle = ms.HeapIdle
resp.Data.Stats.HeapInuse = ms.HeapInuse
resp.Data.Stats.HeapReleased = ms.HeapReleased
resp.Data.Stats.HeapObjects = ms.HeapObjects
resp.Data.Stats.GcCPUFraction = ms.GCCPUFraction
return resp, nil
}
// 辅助函数:检查字符串切片是否包含特定字符串
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
func (p *DebugPlugin) GetHeapGraph(ctx context.Context, empty *emptypb.Empty) (*pb.HeapGraphResponse, error) {
// 创建临时文件用于存储堆信息
f, err := os.CreateTemp("", "heap")
if err != nil {
return nil, err
}
defer os.Remove(f.Name())
defer f.Close()
// 获取堆信息
runtime.GC()
if err := runtimePPROF.WriteHeapProfile(f); err != nil {
return nil, err
}
// 读取堆信息
f.Seek(0, 0)
profile, err := profile.Parse(f)
if err != nil {
return nil, err
}
// Generate dot graph.
dot, err := debug.GetDotGraph(profile)
if err != nil {
return nil, err
}
return &pb.HeapGraphResponse{
Data: dot,
}, nil
}

709
plugin/debug/pb/debug.pb.go Normal file
View File

@@ -0,0 +1,709 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.19.1
// source: debug.proto
package pb
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
_ "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HeapObject struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"`
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
SizePerc float64 `protobuf:"fixed64,4,opt,name=sizePerc,proto3" json:"sizePerc,omitempty"`
Address string `protobuf:"bytes,5,opt,name=address,proto3" json:"address,omitempty"`
Refs []string `protobuf:"bytes,6,rep,name=refs,proto3" json:"refs,omitempty"`
}
func (x *HeapObject) Reset() {
*x = HeapObject{}
if protoimpl.UnsafeEnabled {
mi := &file_debug_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeapObject) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeapObject) ProtoMessage() {}
func (x *HeapObject) ProtoReflect() protoreflect.Message {
mi := &file_debug_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeapObject.ProtoReflect.Descriptor instead.
func (*HeapObject) Descriptor() ([]byte, []int) {
return file_debug_proto_rawDescGZIP(), []int{0}
}
func (x *HeapObject) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *HeapObject) GetCount() int64 {
if x != nil {
return x.Count
}
return 0
}
func (x *HeapObject) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *HeapObject) GetSizePerc() float64 {
if x != nil {
return x.SizePerc
}
return 0
}
func (x *HeapObject) GetAddress() string {
if x != nil {
return x.Address
}
return ""
}
func (x *HeapObject) GetRefs() []string {
if x != nil {
return x.Refs
}
return nil
}
type HeapStats struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Alloc uint64 `protobuf:"varint,1,opt,name=alloc,proto3" json:"alloc,omitempty"`
TotalAlloc uint64 `protobuf:"varint,2,opt,name=totalAlloc,proto3" json:"totalAlloc,omitempty"`
Sys uint64 `protobuf:"varint,3,opt,name=sys,proto3" json:"sys,omitempty"`
NumGC uint32 `protobuf:"varint,4,opt,name=numGC,proto3" json:"numGC,omitempty"`
HeapAlloc uint64 `protobuf:"varint,5,opt,name=heapAlloc,proto3" json:"heapAlloc,omitempty"`
HeapSys uint64 `protobuf:"varint,6,opt,name=heapSys,proto3" json:"heapSys,omitempty"`
HeapIdle uint64 `protobuf:"varint,7,opt,name=heapIdle,proto3" json:"heapIdle,omitempty"`
HeapInuse uint64 `protobuf:"varint,8,opt,name=heapInuse,proto3" json:"heapInuse,omitempty"`
HeapReleased uint64 `protobuf:"varint,9,opt,name=heapReleased,proto3" json:"heapReleased,omitempty"`
HeapObjects uint64 `protobuf:"varint,10,opt,name=heapObjects,proto3" json:"heapObjects,omitempty"`
GcCPUFraction float64 `protobuf:"fixed64,11,opt,name=gcCPUFraction,proto3" json:"gcCPUFraction,omitempty"`
}
func (x *HeapStats) Reset() {
*x = HeapStats{}
if protoimpl.UnsafeEnabled {
mi := &file_debug_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeapStats) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeapStats) ProtoMessage() {}
func (x *HeapStats) ProtoReflect() protoreflect.Message {
mi := &file_debug_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeapStats.ProtoReflect.Descriptor instead.
func (*HeapStats) Descriptor() ([]byte, []int) {
return file_debug_proto_rawDescGZIP(), []int{1}
}
func (x *HeapStats) GetAlloc() uint64 {
if x != nil {
return x.Alloc
}
return 0
}
func (x *HeapStats) GetTotalAlloc() uint64 {
if x != nil {
return x.TotalAlloc
}
return 0
}
func (x *HeapStats) GetSys() uint64 {
if x != nil {
return x.Sys
}
return 0
}
func (x *HeapStats) GetNumGC() uint32 {
if x != nil {
return x.NumGC
}
return 0
}
func (x *HeapStats) GetHeapAlloc() uint64 {
if x != nil {
return x.HeapAlloc
}
return 0
}
func (x *HeapStats) GetHeapSys() uint64 {
if x != nil {
return x.HeapSys
}
return 0
}
func (x *HeapStats) GetHeapIdle() uint64 {
if x != nil {
return x.HeapIdle
}
return 0
}
func (x *HeapStats) GetHeapInuse() uint64 {
if x != nil {
return x.HeapInuse
}
return 0
}
func (x *HeapStats) GetHeapReleased() uint64 {
if x != nil {
return x.HeapReleased
}
return 0
}
func (x *HeapStats) GetHeapObjects() uint64 {
if x != nil {
return x.HeapObjects
}
return 0
}
func (x *HeapStats) GetGcCPUFraction() float64 {
if x != nil {
return x.GcCPUFraction
}
return 0
}
type HeapData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Stats *HeapStats `protobuf:"bytes,1,opt,name=stats,proto3" json:"stats,omitempty"`
Objects []*HeapObject `protobuf:"bytes,2,rep,name=objects,proto3" json:"objects,omitempty"`
Edges []*HeapEdge `protobuf:"bytes,3,rep,name=edges,proto3" json:"edges,omitempty"`
}
func (x *HeapData) Reset() {
*x = HeapData{}
if protoimpl.UnsafeEnabled {
mi := &file_debug_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeapData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeapData) ProtoMessage() {}
func (x *HeapData) ProtoReflect() protoreflect.Message {
mi := &file_debug_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeapData.ProtoReflect.Descriptor instead.
func (*HeapData) Descriptor() ([]byte, []int) {
return file_debug_proto_rawDescGZIP(), []int{2}
}
func (x *HeapData) GetStats() *HeapStats {
if x != nil {
return x.Stats
}
return nil
}
func (x *HeapData) GetObjects() []*HeapObject {
if x != nil {
return x.Objects
}
return nil
}
func (x *HeapData) GetEdges() []*HeapEdge {
if x != nil {
return x.Edges
}
return nil
}
type HeapEdge struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
From string `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"`
To string `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"`
FieldName string `protobuf:"bytes,3,opt,name=fieldName,proto3" json:"fieldName,omitempty"`
}
func (x *HeapEdge) Reset() {
*x = HeapEdge{}
if protoimpl.UnsafeEnabled {
mi := &file_debug_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeapEdge) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeapEdge) ProtoMessage() {}
func (x *HeapEdge) ProtoReflect() protoreflect.Message {
mi := &file_debug_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeapEdge.ProtoReflect.Descriptor instead.
func (*HeapEdge) Descriptor() ([]byte, []int) {
return file_debug_proto_rawDescGZIP(), []int{3}
}
func (x *HeapEdge) GetFrom() string {
if x != nil {
return x.From
}
return ""
}
func (x *HeapEdge) GetTo() string {
if x != nil {
return x.To
}
return ""
}
func (x *HeapEdge) GetFieldName() string {
if x != nil {
return x.FieldName
}
return ""
}
type HeapResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Data *HeapData `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
}
func (x *HeapResponse) Reset() {
*x = HeapResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_debug_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeapResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeapResponse) ProtoMessage() {}
func (x *HeapResponse) ProtoReflect() protoreflect.Message {
mi := &file_debug_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeapResponse.ProtoReflect.Descriptor instead.
func (*HeapResponse) Descriptor() ([]byte, []int) {
return file_debug_proto_rawDescGZIP(), []int{4}
}
func (x *HeapResponse) GetCode() uint32 {
if x != nil {
return x.Code
}
return 0
}
func (x *HeapResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *HeapResponse) GetData() *HeapData {
if x != nil {
return x.Data
}
return nil
}
type HeapGraphResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
}
func (x *HeapGraphResponse) Reset() {
*x = HeapGraphResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_debug_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HeapGraphResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeapGraphResponse) ProtoMessage() {}
func (x *HeapGraphResponse) ProtoReflect() protoreflect.Message {
mi := &file_debug_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeapGraphResponse.ProtoReflect.Descriptor instead.
func (*HeapGraphResponse) Descriptor() ([]byte, []int) {
return file_debug_proto_rawDescGZIP(), []int{5}
}
func (x *HeapGraphResponse) GetCode() uint32 {
if x != nil {
return x.Code
}
return 0
}
func (x *HeapGraphResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *HeapGraphResponse) GetData() string {
if x != nil {
return x.Data
}
return ""
}
var File_debug_proto protoreflect.FileDescriptor
var file_debug_proto_rawDesc = []byte{
0x0a, 0x0b, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x64,
0x65, 0x62, 0x75, 0x67, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a,
0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x22, 0x94, 0x01, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12,
0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74,
0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1a, 0x0a,
0x08, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x65, 0x72, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52,
0x08, 0x73, 0x69, 0x7a, 0x65, 0x50, 0x65, 0x72, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72,
0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x66, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28,
0x09, 0x52, 0x04, 0x72, 0x65, 0x66, 0x73, 0x22, 0xc7, 0x02, 0x0a, 0x09, 0x48, 0x65, 0x61, 0x70,
0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x01,
0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x74,
0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52,
0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x73,
0x79, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x73, 0x79, 0x73, 0x12, 0x14, 0x0a,
0x05, 0x6e, 0x75, 0x6d, 0x47, 0x43, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x6e, 0x75,
0x6d, 0x47, 0x43, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x70, 0x41, 0x6c, 0x6c, 0x6f, 0x63,
0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x68, 0x65, 0x61, 0x70, 0x41, 0x6c, 0x6c, 0x6f,
0x63, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x70, 0x53, 0x79, 0x73, 0x18, 0x06, 0x20, 0x01,
0x28, 0x04, 0x52, 0x07, 0x68, 0x65, 0x61, 0x70, 0x53, 0x79, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x68,
0x65, 0x61, 0x70, 0x49, 0x64, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x68,
0x65, 0x61, 0x70, 0x49, 0x64, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x70, 0x49,
0x6e, 0x75, 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x68, 0x65, 0x61, 0x70,
0x49, 0x6e, 0x75, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x68, 0x65, 0x61, 0x70, 0x52, 0x65, 0x6c,
0x65, 0x61, 0x73, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x68, 0x65, 0x61,
0x70, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x68, 0x65, 0x61,
0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b,
0x68, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67,
0x63, 0x43, 0x50, 0x55, 0x46, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01,
0x28, 0x01, 0x52, 0x0d, 0x67, 0x63, 0x43, 0x50, 0x55, 0x46, 0x72, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x48, 0x65, 0x61, 0x70, 0x44, 0x61, 0x74, 0x61, 0x12, 0x26,
0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e,
0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x12, 0x2b, 0x0a, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e,
0x48, 0x65, 0x61, 0x70, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x45,
0x64, 0x67, 0x65, 0x52, 0x05, 0x65, 0x64, 0x67, 0x65, 0x73, 0x22, 0x4c, 0x0a, 0x08, 0x48, 0x65,
0x61, 0x70, 0x45, 0x64, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x69,
0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66,
0x69, 0x65, 0x6c, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x61, 0x0a, 0x0c, 0x48, 0x65, 0x61, 0x70,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61,
0x70, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x55, 0x0a, 0x11, 0x48,
0x65, 0x61, 0x70, 0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04,
0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61,
0x74, 0x61, 0x32, 0xb7, 0x01, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x12, 0x4f, 0x0a, 0x07, 0x47, 0x65,
0x74, 0x48, 0x65, 0x61, 0x70, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x13, 0x2e,
0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x64, 0x65, 0x62,
0x75, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x61, 0x70, 0x12, 0x5f, 0x0a, 0x0c, 0x47,
0x65, 0x74, 0x48, 0x65, 0x61, 0x70, 0x47, 0x72, 0x61, 0x70, 0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x18, 0x2e, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2e, 0x48, 0x65, 0x61, 0x70,
0x47, 0x72, 0x61, 0x70, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1d, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x68, 0x65, 0x61, 0x70, 0x2f, 0x67, 0x72, 0x61, 0x70, 0x68, 0x42, 0x1d, 0x5a, 0x1b,
0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67,
0x69, 0x6e, 0x2f, 0x64, 0x65, 0x62, 0x75, 0x67, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
file_debug_proto_rawDescOnce sync.Once
file_debug_proto_rawDescData = file_debug_proto_rawDesc
)
func file_debug_proto_rawDescGZIP() []byte {
file_debug_proto_rawDescOnce.Do(func() {
file_debug_proto_rawDescData = protoimpl.X.CompressGZIP(file_debug_proto_rawDescData)
})
return file_debug_proto_rawDescData
}
var file_debug_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_debug_proto_goTypes = []interface{}{
(*HeapObject)(nil), // 0: debug.HeapObject
(*HeapStats)(nil), // 1: debug.HeapStats
(*HeapData)(nil), // 2: debug.HeapData
(*HeapEdge)(nil), // 3: debug.HeapEdge
(*HeapResponse)(nil), // 4: debug.HeapResponse
(*HeapGraphResponse)(nil), // 5: debug.HeapGraphResponse
(*emptypb.Empty)(nil), // 6: google.protobuf.Empty
}
var file_debug_proto_depIdxs = []int32{
1, // 0: debug.HeapData.stats:type_name -> debug.HeapStats
0, // 1: debug.HeapData.objects:type_name -> debug.HeapObject
3, // 2: debug.HeapData.edges:type_name -> debug.HeapEdge
2, // 3: debug.HeapResponse.data:type_name -> debug.HeapData
6, // 4: debug.api.GetHeap:input_type -> google.protobuf.Empty
6, // 5: debug.api.GetHeapGraph:input_type -> google.protobuf.Empty
4, // 6: debug.api.GetHeap:output_type -> debug.HeapResponse
5, // 7: debug.api.GetHeapGraph:output_type -> debug.HeapGraphResponse
6, // [6:8] is the sub-list for method output_type
4, // [4:6] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_debug_proto_init() }
func file_debug_proto_init() {
if File_debug_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_debug_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeapObject); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_debug_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeapStats); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_debug_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeapData); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_debug_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeapEdge); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_debug_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeapResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_debug_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HeapGraphResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_debug_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_debug_proto_goTypes,
DependencyIndexes: file_debug_proto_depIdxs,
MessageInfos: file_debug_proto_msgTypes,
}.Build()
File_debug_proto = out.File
file_debug_proto_rawDesc = nil
file_debug_proto_goTypes = nil
file_debug_proto_depIdxs = nil
}

View File

@@ -0,0 +1,225 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: debug.proto
/*
Package pb is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package pb
import (
"context"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
func request_Api_GetHeap_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := client.GetHeap(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetHeap_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := server.GetHeap(ctx, &protoReq)
return msg, metadata, err
}
func request_Api_GetHeapGraph_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := client.GetHeapGraph(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Api_GetHeapGraph_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq emptypb.Empty
var metadata runtime.ServerMetadata
msg, err := server.GetHeapGraph(ctx, &protoReq)
return msg, metadata, err
}
// RegisterApiHandlerServer registers the http handlers for service Api to "mux".
// UnaryRPC :call ApiServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead.
func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error {
mux.Handle("GET", pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetHeap_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Api_GetHeapGraph_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeapGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.DialContext(ctx, endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterApiHandler(ctx, mux, conn)
}
// RegisterApiHandler registers the http handlers for service Api to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterApiHandlerClient(ctx, mux, NewApiClient(conn))
}
// RegisterApiHandlerClient registers the http handlers for service Api
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "ApiClient" to call the correct interceptors.
func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error {
mux.Handle("GET", pattern_Api_GetHeap_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeap", runtime.WithHTTPPathPattern("/debug/api/heap"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetHeap_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeap_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Api_GetHeapGraph_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/debug.Api/GetHeapGraph", runtime.WithHTTPPathPattern("/debug/api/heap/graph"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Api_GetHeapGraph_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Api_GetHeapGraph_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Api_GetHeap_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"debug", "api", "heap"}, ""))
pattern_Api_GetHeapGraph_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"debug", "api", "heap", "graph"}, ""))
)
var (
forward_Api_GetHeap_0 = runtime.ForwardResponseMessage
forward_Api_GetHeapGraph_0 = runtime.ForwardResponseMessage
)

View File

@@ -0,0 +1,66 @@
syntax = "proto3";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
package debug;
option go_package="m7s.live/v5/plugin/debug/pb";
service api {
rpc GetHeap (google.protobuf.Empty) returns (HeapResponse) {
option (google.api.http) = {
get: "/debug/api/heap"
};
}
rpc GetHeapGraph (google.protobuf.Empty) returns (HeapGraphResponse) {
option (google.api.http) = {
get: "/debug/api/heap/graph"
};
}
}
message HeapObject {
string type = 1;
int64 count = 2;
int64 size = 3;
double sizePerc = 4;
string address = 5;
repeated string refs = 6;
}
message HeapStats {
uint64 alloc = 1;
uint64 totalAlloc = 2;
uint64 sys = 3;
uint32 numGC = 4;
uint64 heapAlloc = 5;
uint64 heapSys = 6;
uint64 heapIdle = 7;
uint64 heapInuse = 8;
uint64 heapReleased = 9;
uint64 heapObjects = 10;
double gcCPUFraction = 11;
}
message HeapData {
HeapStats stats = 1;
repeated HeapObject objects = 2;
repeated HeapEdge edges = 3;
}
message HeapEdge {
string from = 1;
string to = 2;
string fieldName = 3;
}
message HeapResponse {
uint32 code = 1;
string message = 2;
HeapData data = 3;
}
message HeapGraphResponse {
uint32 code = 1;
string message = 2;
string data = 3;
}

View File

@@ -0,0 +1,142 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.19.1
// source: debug.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// ApiClient is the client API for Api service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ApiClient interface {
GetHeap(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HeapResponse, error)
GetHeapGraph(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HeapGraphResponse, error)
}
type apiClient struct {
cc grpc.ClientConnInterface
}
func NewApiClient(cc grpc.ClientConnInterface) ApiClient {
return &apiClient{cc}
}
func (c *apiClient) GetHeap(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HeapResponse, error) {
out := new(HeapResponse)
err := c.cc.Invoke(ctx, "/debug.api/GetHeap", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *apiClient) GetHeapGraph(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HeapGraphResponse, error) {
out := new(HeapGraphResponse)
err := c.cc.Invoke(ctx, "/debug.api/GetHeapGraph", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ApiServer is the server API for Api service.
// All implementations must embed UnimplementedApiServer
// for forward compatibility
type ApiServer interface {
GetHeap(context.Context, *emptypb.Empty) (*HeapResponse, error)
GetHeapGraph(context.Context, *emptypb.Empty) (*HeapGraphResponse, error)
mustEmbedUnimplementedApiServer()
}
// UnimplementedApiServer must be embedded to have forward compatible implementations.
type UnimplementedApiServer struct {
}
func (UnimplementedApiServer) GetHeap(context.Context, *emptypb.Empty) (*HeapResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetHeap not implemented")
}
func (UnimplementedApiServer) GetHeapGraph(context.Context, *emptypb.Empty) (*HeapGraphResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetHeapGraph not implemented")
}
func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {}
// UnsafeApiServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ApiServer will
// result in compilation errors.
type UnsafeApiServer interface {
mustEmbedUnimplementedApiServer()
}
func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) {
s.RegisterService(&Api_ServiceDesc, srv)
}
func _Api_GetHeap_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).GetHeap(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/debug.api/GetHeap",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetHeap(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Api_GetHeapGraph_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ApiServer).GetHeapGraph(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/debug.api/GetHeapGraph",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ApiServer).GetHeapGraph(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// Api_ServiceDesc is the grpc.ServiceDesc for Api service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Api_ServiceDesc = grpc.ServiceDesc{
ServiceName: "debug.api",
HandlerType: (*ApiServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetHeap",
Handler: _Api_GetHeap_Handler,
},
{
MethodName: "GetHeapGraph",
Handler: _Api_GetHeapGraph_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "debug.proto",
}

17
plugin/debug/pkg/index.go Normal file
View File

@@ -0,0 +1,17 @@
package debug
import (
"bytes"
"m7s.live/v5/plugin/debug/pkg/internal/graph"
"m7s.live/v5/plugin/debug/pkg/internal/report"
"m7s.live/v5/plugin/debug/pkg/profile"
)
func GetDotGraph(profile *profile.Profile) (string, error) {
rpt := report.NewDefault(profile, report.Options{})
g, config := report.GetDOT(rpt)
dot := &bytes.Buffer{}
graph.ComposeDot(dot, g, &graph.DotAttributes{}, config)
return dot.String(), nil
}

View File

@@ -0,0 +1,238 @@
// 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 binutils
import (
"bufio"
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
const (
defaultAddr2line = "addr2line"
// addr2line may produce multiple lines of output. We
// use this sentinel to identify the end of the output.
sentinel = ^uint64(0)
)
// addr2Liner is a connection to an addr2line command for obtaining
// address and line number information from a binary.
type addr2Liner struct {
mu sync.Mutex
rw lineReaderWriter
base uint64
// nm holds an addr2Liner using nm tool. Certain versions of addr2line
// produce incomplete names due to
// https://sourceware.org/bugzilla/show_bug.cgi?id=17541. As a workaround,
// the names from nm are used when they look more complete. See addrInfo()
// code below for the exact heuristic.
nm *addr2LinerNM
}
// lineReaderWriter is an interface to abstract the I/O to an addr2line
// process. It writes a line of input to the job, and reads its output
// one line at a time.
type lineReaderWriter interface {
write(string) error
readLine() (string, error)
close()
}
type addr2LinerJob struct {
cmd *exec.Cmd
in io.WriteCloser
out *bufio.Reader
}
func (a *addr2LinerJob) write(s string) error {
_, err := fmt.Fprint(a.in, s+"\n")
return err
}
func (a *addr2LinerJob) readLine() (string, error) {
s, err := a.out.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(s), nil
}
// close releases any resources used by the addr2liner object.
func (a *addr2LinerJob) close() {
a.in.Close()
a.cmd.Wait()
}
// newAddr2Liner starts the given addr2liner command reporting
// information about the given executable file. If file is a shared
// library, base should be the address at which it was mapped in the
// program under consideration.
func newAddr2Liner(cmd, file string, base uint64) (*addr2Liner, error) {
if cmd == "" {
cmd = defaultAddr2line
}
j := &addr2LinerJob{
cmd: exec.Command(cmd, "-aif", "-e", file),
}
var err error
if j.in, err = j.cmd.StdinPipe(); err != nil {
return nil, err
}
outPipe, err := j.cmd.StdoutPipe()
if err != nil {
return nil, err
}
j.out = bufio.NewReader(outPipe)
if err := j.cmd.Start(); err != nil {
return nil, err
}
a := &addr2Liner{
rw: j,
base: base,
}
return a, nil
}
// readFrame parses the addr2line output for a single address. It
// returns a populated plugin.Frame and whether it has reached the end of the
// data.
func (d *addr2Liner) readFrame() (plugin.Frame, bool) {
funcname, err := d.rw.readLine()
if err != nil {
return plugin.Frame{}, true
}
if strings.HasPrefix(funcname, "0x") {
// If addr2line returns a hex address we can assume it is the
// sentinel. Read and ignore next two lines of output from
// addr2line
d.rw.readLine()
d.rw.readLine()
return plugin.Frame{}, true
}
fileline, err := d.rw.readLine()
if err != nil {
return plugin.Frame{}, true
}
linenumber := 0
if funcname == "??" {
funcname = ""
}
if fileline == "??:0" {
fileline = ""
} else {
if i := strings.LastIndex(fileline, ":"); i >= 0 {
// Remove discriminator, if present
if disc := strings.Index(fileline, " (discriminator"); disc > 0 {
fileline = fileline[:disc]
}
// If we cannot parse a number after the last ":", keep it as
// part of the filename.
if line, err := strconv.Atoi(fileline[i+1:]); err == nil {
linenumber = line
fileline = fileline[:i]
}
}
}
return plugin.Frame{
Func: funcname,
File: fileline,
Line: linenumber}, false
}
func (d *addr2Liner) rawAddrInfo(addr uint64) ([]plugin.Frame, error) {
d.mu.Lock()
defer d.mu.Unlock()
if err := d.rw.write(fmt.Sprintf("%x", addr-d.base)); err != nil {
return nil, err
}
if err := d.rw.write(fmt.Sprintf("%x", sentinel)); err != nil {
return nil, err
}
resp, err := d.rw.readLine()
if err != nil {
return nil, err
}
if !strings.HasPrefix(resp, "0x") {
return nil, fmt.Errorf("unexpected addr2line output: %s", resp)
}
var stack []plugin.Frame
for {
frame, end := d.readFrame()
if end {
break
}
if frame != (plugin.Frame{}) {
stack = append(stack, frame)
}
}
return stack, err
}
// addrInfo returns the stack frame information for a specific program
// address. It returns nil if the address could not be identified.
func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) {
stack, err := d.rawAddrInfo(addr)
if err != nil {
return nil, err
}
// Certain versions of addr2line produce incomplete names due to
// https://sourceware.org/bugzilla/show_bug.cgi?id=17541. Attempt to replace
// the name with a better one from nm.
if len(stack) > 0 && d.nm != nil {
nm, err := d.nm.addrInfo(addr)
if err == nil && len(nm) > 0 {
// Last entry in frame list should match since it is non-inlined. As a
// simple heuristic, we only switch to the nm-based name if it is longer
// by 2 or more characters. We consider nm names that are longer by 1
// character insignificant to avoid replacing foo with _foo on MacOS (for
// unknown reasons read2line produces the former and nm produces the
// latter on MacOS even though both tools are asked to produce mangled
// names).
nmName := nm[len(nm)-1].Func
a2lName := stack[len(stack)-1].Func
if len(nmName) > len(a2lName)+1 {
stack[len(stack)-1].Func = nmName
}
}
}
return stack, nil
}

View File

@@ -0,0 +1,184 @@
// 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 binutils
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
const (
defaultLLVMSymbolizer = "llvm-symbolizer"
)
// llvmSymbolizer is a connection to an llvm-symbolizer command for
// obtaining address and line number information from a binary.
type llvmSymbolizer struct {
sync.Mutex
filename string
rw lineReaderWriter
base uint64
isData bool
}
type llvmSymbolizerJob struct {
cmd *exec.Cmd
in io.WriteCloser
out *bufio.Reader
// llvm-symbolizer requires the symbol type, CODE or DATA, for symbolization.
symType string
}
func (a *llvmSymbolizerJob) write(s string) error {
_, err := fmt.Fprintln(a.in, a.symType, s)
return err
}
func (a *llvmSymbolizerJob) readLine() (string, error) {
s, err := a.out.ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(s), nil
}
// close releases any resources used by the llvmSymbolizer object.
func (a *llvmSymbolizerJob) close() {
a.in.Close()
a.cmd.Wait()
}
// newLLVMSymbolizer starts the given llvmSymbolizer command reporting
// information about the given executable file. If file is a shared
// library, base should be the address at which it was mapped in the
// program under consideration.
func newLLVMSymbolizer(cmd, file string, base uint64, isData bool) (*llvmSymbolizer, error) {
if cmd == "" {
cmd = defaultLLVMSymbolizer
}
j := &llvmSymbolizerJob{
cmd: exec.Command(cmd, "--inlining", "-demangle=false", "--output-style=JSON"),
symType: "CODE",
}
if isData {
j.symType = "DATA"
}
var err error
if j.in, err = j.cmd.StdinPipe(); err != nil {
return nil, err
}
outPipe, err := j.cmd.StdoutPipe()
if err != nil {
return nil, err
}
j.out = bufio.NewReader(outPipe)
if err := j.cmd.Start(); err != nil {
return nil, err
}
a := &llvmSymbolizer{
filename: file,
rw: j,
base: base,
isData: isData,
}
return a, nil
}
// readDataFrames parses the llvm-symbolizer DATA output for a single address. It
// returns a populated plugin.Frame array with a single entry.
func (d *llvmSymbolizer) readDataFrames() ([]plugin.Frame, error) {
line, err := d.rw.readLine()
if err != nil {
return nil, err
}
var frame struct {
Address string `json:"Address"`
ModuleName string `json:"ModuleName"`
Data struct {
Start string `json:"Start"`
Size string `json:"Size"`
Name string `json:"Name"`
} `json:"Data"`
}
if err := json.Unmarshal([]byte(line), &frame); err != nil {
return nil, err
}
// Match non-JSON output behaviour of stuffing the start/size into the filename of a single frame,
// with the size being a decimal value.
size, err := strconv.ParseInt(frame.Data.Size, 0, 0)
if err != nil {
return nil, err
}
var stack []plugin.Frame
stack = append(stack, plugin.Frame{Func: frame.Data.Name, File: fmt.Sprintf("%s %d", frame.Data.Start, size)})
return stack, nil
}
// readCodeFrames parses the llvm-symbolizer CODE output for a single address. It
// returns a populated plugin.Frame array.
func (d *llvmSymbolizer) readCodeFrames() ([]plugin.Frame, error) {
line, err := d.rw.readLine()
if err != nil {
return nil, err
}
var frame struct {
Address string `json:"Address"`
ModuleName string `json:"ModuleName"`
Symbol []struct {
Line int `json:"Line"`
Column int `json:"Column"`
FunctionName string `json:"FunctionName"`
FileName string `json:"FileName"`
StartLine int `json:"StartLine"`
} `json:"Symbol"`
}
if err := json.Unmarshal([]byte(line), &frame); err != nil {
return nil, err
}
var stack []plugin.Frame
for _, s := range frame.Symbol {
stack = append(stack, plugin.Frame{Func: s.FunctionName, File: s.FileName, Line: s.Line, Column: s.Column, StartLine: s.StartLine})
}
return stack, nil
}
// addrInfo returns the stack frame information for a specific program
// address. It returns nil if the address could not be identified.
func (d *llvmSymbolizer) addrInfo(addr uint64) ([]plugin.Frame, error) {
d.Lock()
defer d.Unlock()
if err := d.rw.write(fmt.Sprintf("%s 0x%x", d.filename, addr-d.base)); err != nil {
return nil, err
}
if d.isData {
return d.readDataFrames()
}
return d.readCodeFrames()
}

View File

@@ -0,0 +1,144 @@
// 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 binutils
import (
"bufio"
"bytes"
"io"
"os/exec"
"strconv"
"strings"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
const (
defaultNM = "nm"
)
// addr2LinerNM is a connection to an nm command for obtaining symbol
// information from a binary.
type addr2LinerNM struct {
m []symbolInfo // Sorted list of symbol addresses from binary.
}
type symbolInfo struct {
address uint64
size uint64
name string
symType string
}
// isData returns if the symbol has a known data object symbol type.
func (s *symbolInfo) isData() bool {
// The following symbol types are taken from https://linux.die.net/man/1/nm:
// Lowercase letter means local symbol, uppercase denotes a global symbol.
// - b or B: the symbol is in the uninitialized data section, e.g. .bss;
// - d or D: the symbol is in the initialized data section;
// - r or R: the symbol is in a read only data section;
// - v or V: the symbol is a weak object;
// - W: the symbol is a weak symbol that has not been specifically tagged as a
// weak object symbol. Experiments with some binaries, showed these to be
// mostly data objects.
return strings.ContainsAny(s.symType, "bBdDrRvVW")
}
// newAddr2LinerNM starts the given nm command reporting information about the
// given executable file. If file is a shared library, base should be the
// address at which it was mapped in the program under consideration.
func newAddr2LinerNM(cmd, file string, base uint64) (*addr2LinerNM, error) {
if cmd == "" {
cmd = defaultNM
}
var b bytes.Buffer
c := exec.Command(cmd, "--numeric-sort", "--print-size", "--format=posix", file)
c.Stdout = &b
if err := c.Run(); err != nil {
return nil, err
}
return parseAddr2LinerNM(base, &b)
}
func parseAddr2LinerNM(base uint64, nm io.Reader) (*addr2LinerNM, error) {
a := &addr2LinerNM{
m: []symbolInfo{},
}
// Parse nm output and populate symbol map.
// Skip lines we fail to parse.
buf := bufio.NewReader(nm)
for {
line, err := buf.ReadString('\n')
if line == "" && err != nil {
if err == io.EOF {
break
}
return nil, err
}
line = strings.TrimSpace(line)
fields := strings.Split(line, " ")
if len(fields) != 4 {
continue
}
address, err := strconv.ParseUint(fields[2], 16, 64)
if err != nil {
continue
}
size, err := strconv.ParseUint(fields[3], 16, 64)
if err != nil {
continue
}
a.m = append(a.m, symbolInfo{
address: address + base,
size: size,
name: fields[0],
symType: fields[1],
})
}
return a, nil
}
// addrInfo returns the stack frame information for a specific program
// address. It returns nil if the address could not be identified.
func (a *addr2LinerNM) addrInfo(addr uint64) ([]plugin.Frame, error) {
if len(a.m) == 0 || addr < a.m[0].address || addr >= (a.m[len(a.m)-1].address+a.m[len(a.m)-1].size) {
return nil, nil
}
// Binary search. Search until low, high are separated by 1.
low, high := 0, len(a.m)
for low+1 < high {
mid := (low + high) / 2
v := a.m[mid].address
if addr == v {
low = mid
break
} else if addr > v {
low = mid
} else {
high = mid
}
}
// Address is between a.m[low] and a.m[high]. Pick low, as it represents
// [low, high). For data symbols, we use a strict check that the address is in
// the [start, start + size) range of a.m[low].
if a.m[low].isData() && addr >= (a.m[low].address+a.m[low].size) {
return nil, nil
}
return []plugin.Frame{{Func: a.m[low].name}}, nil
}

View File

@@ -0,0 +1,736 @@
// 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 binutils provides access to the GNU binutils.
package binutils
import (
"debug/elf"
"debug/macho"
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"m7s.live/v5/plugin/debug/pkg/internal/elfexec"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
// A Binutils implements plugin.ObjTool by invoking the GNU binutils.
type Binutils struct {
mu sync.Mutex
rep *binrep
}
var (
objdumpLLVMVerRE = regexp.MustCompile(`LLVM version (?:(\d*)\.(\d*)\.(\d*)|.*(trunk).*)`)
// Defined for testing
elfOpen = elf.Open
)
// binrep is an immutable representation for Binutils. It is atomically
// replaced on every mutation to provide thread-safe access.
type binrep struct {
// Commands to invoke.
llvmSymbolizer string
llvmSymbolizerFound bool
addr2line string
addr2lineFound bool
nm string
nmFound bool
objdump string
objdumpFound bool
isLLVMObjdump bool
// if fast, perform symbolization using nm (symbol names only),
// instead of file-line detail from the slower addr2line.
fast bool
}
// get returns the current representation for bu, initializing it if necessary.
func (bu *Binutils) get() *binrep {
bu.mu.Lock()
r := bu.rep
if r == nil {
r = &binrep{}
initTools(r, "")
bu.rep = r
}
bu.mu.Unlock()
return r
}
// update modifies the rep for bu via the supplied function.
func (bu *Binutils) update(fn func(r *binrep)) {
r := &binrep{}
bu.mu.Lock()
defer bu.mu.Unlock()
if bu.rep == nil {
initTools(r, "")
} else {
*r = *bu.rep
}
fn(r)
bu.rep = r
}
// String returns string representation of the binutils state for debug logging.
func (bu *Binutils) String() string {
r := bu.get()
var llvmSymbolizer, addr2line, nm, objdump string
if r.llvmSymbolizerFound {
llvmSymbolizer = r.llvmSymbolizer
}
if r.addr2lineFound {
addr2line = r.addr2line
}
if r.nmFound {
nm = r.nm
}
if r.objdumpFound {
objdump = r.objdump
}
return fmt.Sprintf("llvm-symbolizer=%q addr2line=%q nm=%q objdump=%q fast=%t",
llvmSymbolizer, addr2line, nm, objdump, r.fast)
}
// SetFastSymbolization sets a toggle that makes binutils use fast
// symbolization (using nm), which is much faster than addr2line but
// provides only symbol name information (no file/line).
func (bu *Binutils) SetFastSymbolization(fast bool) {
bu.update(func(r *binrep) { r.fast = fast })
}
// SetTools processes the contents of the tools option. It
// expects a set of entries separated by commas; each entry is a pair
// of the form t:path, where cmd will be used to look only for the
// tool named t. If t is not specified, the path is searched for all
// tools.
func (bu *Binutils) SetTools(config string) {
bu.update(func(r *binrep) { initTools(r, config) })
}
func initTools(b *binrep, config string) {
// paths collect paths per tool; Key "" contains the default.
paths := make(map[string][]string)
for _, t := range strings.Split(config, ",") {
name, path := "", t
if ct := strings.SplitN(t, ":", 2); len(ct) == 2 {
name, path = ct[0], ct[1]
}
paths[name] = append(paths[name], path)
}
defaultPath := paths[""]
b.llvmSymbolizer, b.llvmSymbolizerFound = chooseExe([]string{"llvm-symbolizer"}, []string{}, append(paths["llvm-symbolizer"], defaultPath...))
b.addr2line, b.addr2lineFound = chooseExe([]string{"addr2line"}, []string{"gaddr2line"}, append(paths["addr2line"], defaultPath...))
// The "-n" option is supported by LLVM since 2011. The output of llvm-nm
// and GNU nm with "-n" option is interchangeable for our purposes, so we do
// not need to differrentiate them.
b.nm, b.nmFound = chooseExe([]string{"llvm-nm", "nm"}, []string{"gnm"}, append(paths["nm"], defaultPath...))
b.objdump, b.objdumpFound, b.isLLVMObjdump = findObjdump(append(paths["objdump"], defaultPath...))
}
// findObjdump finds and returns path to preferred objdump binary.
// Order of preference is: llvm-objdump, objdump.
// On MacOS only, also looks for gobjdump with least preference.
// Accepts a list of paths and returns:
// a string with path to the preferred objdump binary if found,
// or an empty string if not found;
// a boolean if any acceptable objdump was found;
// a boolean indicating if it is an LLVM objdump.
func findObjdump(paths []string) (string, bool, bool) {
objdumpNames := []string{"llvm-objdump", "objdump"}
if runtime.GOOS == "darwin" {
objdumpNames = append(objdumpNames, "gobjdump")
}
for _, objdumpName := range objdumpNames {
if objdump, objdumpFound := findExe(objdumpName, paths); objdumpFound {
cmdOut, err := exec.Command(objdump, "--version").Output()
if err != nil {
continue
}
if isLLVMObjdump(string(cmdOut)) {
return objdump, true, true
}
if isBuObjdump(string(cmdOut)) {
return objdump, true, false
}
}
}
return "", false, false
}
// chooseExe finds and returns path to preferred binary. names is a list of
// names to search on both Linux and OSX. osxNames is a list of names specific
// to OSX. names always has a higher priority than osxNames. The order of
// the name within each list decides its priority (e.g. the first name has a
// higher priority than the second name in the list).
//
// It returns a string with path to the binary and a boolean indicating if any
// acceptable binary was found.
func chooseExe(names, osxNames []string, paths []string) (string, bool) {
if runtime.GOOS == "darwin" {
names = append(names, osxNames...)
}
for _, name := range names {
if binary, found := findExe(name, paths); found {
return binary, true
}
}
return "", false
}
// isLLVMObjdump accepts a string with path to an objdump binary,
// and returns a boolean indicating if the given binary is an LLVM
// objdump binary of an acceptable version.
func isLLVMObjdump(output string) bool {
fields := objdumpLLVMVerRE.FindStringSubmatch(output)
if len(fields) != 5 {
return false
}
if fields[4] == "trunk" {
return true
}
verMajor, err := strconv.Atoi(fields[1])
if err != nil {
return false
}
verPatch, err := strconv.Atoi(fields[3])
if err != nil {
return false
}
if runtime.GOOS == "linux" && verMajor >= 8 {
// Ensure LLVM objdump is at least version 8.0 on Linux.
// Some flags, like --demangle, and double dashes for options are
// not supported by previous versions.
return true
}
if runtime.GOOS == "darwin" {
// Ensure LLVM objdump is at least version 10.0.1 on MacOS.
return verMajor > 10 || (verMajor == 10 && verPatch >= 1)
}
return false
}
// isBuObjdump accepts a string with path to an objdump binary,
// and returns a boolean indicating if the given binary is a GNU
// binutils objdump binary. No version check is performed.
func isBuObjdump(output string) bool {
return strings.Contains(output, "GNU objdump")
}
// findExe looks for an executable command on a set of paths.
// If it cannot find it, returns cmd.
func findExe(cmd string, paths []string) (string, bool) {
for _, p := range paths {
cp := filepath.Join(p, cmd)
if c, err := exec.LookPath(cp); err == nil {
return c, true
}
}
return cmd, false
}
// Disasm returns the assembly instructions for the specified address range
// of a binary.
func (bu *Binutils) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) {
b := bu.get()
if !b.objdumpFound {
return nil, errors.New("cannot disasm: no objdump tool available")
}
args := []string{"--disassemble", "--demangle", "--no-show-raw-insn",
"--line-numbers", fmt.Sprintf("--start-address=%#x", start),
fmt.Sprintf("--stop-address=%#x", end)}
if intelSyntax {
if b.isLLVMObjdump {
args = append(args, "--x86-asm-syntax=intel")
} else {
args = append(args, "-M", "intel")
}
}
args = append(args, file)
cmd := exec.Command(b.objdump, args...)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("%v: %v", cmd.Args, err)
}
return disassemble(out)
}
// Open satisfies the plugin.ObjTool interface.
func (bu *Binutils) Open(name string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
b := bu.get()
// Make sure file is a supported executable.
// This uses magic numbers, mainly to provide better error messages but
// it should also help speed.
if _, err := os.Stat(name); err != nil {
// For testing, do not require file name to exist.
if strings.Contains(b.addr2line, "testdata/") {
return &fileAddr2Line{file: file{b: b, name: name}}, nil
}
return nil, err
}
// Read the first 4 bytes of the file.
f, err := os.Open(name)
if err != nil {
return nil, fmt.Errorf("error opening %s: %v", name, err)
}
defer f.Close()
var header [4]byte
if _, err = io.ReadFull(f, header[:]); err != nil {
return nil, fmt.Errorf("error reading magic number from %s: %v", name, err)
}
elfMagic := string(header[:])
// Match against supported file types.
if elfMagic == elf.ELFMAG {
f, err := b.openELF(name, start, limit, offset, relocationSymbol)
if err != nil {
return nil, fmt.Errorf("error reading ELF file %s: %v", name, err)
}
return f, nil
}
// Mach-O magic numbers can be big or little endian.
machoMagicLittle := binary.LittleEndian.Uint32(header[:])
machoMagicBig := binary.BigEndian.Uint32(header[:])
if machoMagicLittle == macho.Magic32 || machoMagicLittle == macho.Magic64 ||
machoMagicBig == macho.Magic32 || machoMagicBig == macho.Magic64 {
f, err := b.openMachO(name, start, limit, offset)
if err != nil {
return nil, fmt.Errorf("error reading Mach-O file %s: %v", name, err)
}
return f, nil
}
if machoMagicLittle == macho.MagicFat || machoMagicBig == macho.MagicFat {
f, err := b.openFatMachO(name, start, limit, offset)
if err != nil {
return nil, fmt.Errorf("error reading fat Mach-O file %s: %v", name, err)
}
return f, nil
}
peMagic := string(header[:2])
if peMagic == "MZ" {
f, err := b.openPE(name, start, limit, offset)
if err != nil {
return nil, fmt.Errorf("error reading PE file %s: %v", name, err)
}
return f, nil
}
return nil, fmt.Errorf("unrecognized binary format: %s", name)
}
func (b *binrep) openMachOCommon(name string, of *macho.File, start, limit, offset uint64) (plugin.ObjFile, error) {
// Subtract the load address of the __TEXT section. Usually 0 for shared
// libraries or 0x100000000 for executables. You can check this value by
// running `objdump -private-headers <file>`.
textSegment := of.Segment("__TEXT")
if textSegment == nil {
return nil, fmt.Errorf("could not identify base for %s: no __TEXT segment", name)
}
if textSegment.Addr > start {
return nil, fmt.Errorf("could not identify base for %s: __TEXT segment address (0x%x) > mapping start address (0x%x)",
name, textSegment.Addr, start)
}
base := start - textSegment.Addr
if b.fast || (!b.addr2lineFound && !b.llvmSymbolizerFound) {
return &fileNM{file: file{b: b, name: name, base: base}}, nil
}
return &fileAddr2Line{file: file{b: b, name: name, base: base}}, nil
}
func (b *binrep) openFatMachO(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
of, err := macho.OpenFat(name)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", name, err)
}
defer of.Close()
if len(of.Arches) == 0 {
return nil, fmt.Errorf("empty fat Mach-O file: %s", name)
}
var arch macho.Cpu
// Use the host architecture.
// TODO: This is not ideal because the host architecture may not be the one
// that was profiled. E.g. an amd64 host can profile a 386 program.
switch runtime.GOARCH {
case "386":
arch = macho.Cpu386
case "amd64", "amd64p32":
arch = macho.CpuAmd64
case "arm", "armbe", "arm64", "arm64be":
arch = macho.CpuArm
case "ppc":
arch = macho.CpuPpc
case "ppc64", "ppc64le":
arch = macho.CpuPpc64
default:
return nil, fmt.Errorf("unsupported host architecture for %s: %s", name, runtime.GOARCH)
}
for i := range of.Arches {
if of.Arches[i].Cpu == arch {
return b.openMachOCommon(name, of.Arches[i].File, start, limit, offset)
}
}
return nil, fmt.Errorf("architecture not found in %s: %s", name, runtime.GOARCH)
}
func (b *binrep) openMachO(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
of, err := macho.Open(name)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", name, err)
}
defer of.Close()
return b.openMachOCommon(name, of, start, limit, offset)
}
func (b *binrep) openELF(name string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
ef, err := elfOpen(name)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", name, err)
}
defer ef.Close()
buildID := ""
if id, err := elfexec.GetBuildID(ef); err == nil {
buildID = fmt.Sprintf("%x", id)
}
var (
kernelOffset *uint64
pageAligned = func(addr uint64) bool { return addr%4096 == 0 }
)
if strings.Contains(name, "vmlinux") || !pageAligned(start) || !pageAligned(limit) || !pageAligned(offset) {
// Reading all Symbols is expensive, and we only rarely need it so
// we don't want to do it every time. But if _stext happens to be
// page-aligned but isn't the same as Vaddr, we would symbolize
// wrong. So if the name the addresses aren't page aligned, or if
// the name is "vmlinux" we read _stext. We can be wrong if: (1)
// someone passes a kernel path that doesn't contain "vmlinux" AND
// (2) _stext is page-aligned AND (3) _stext is not at Vaddr
symbols, err := ef.Symbols()
if err != nil && err != elf.ErrNoSymbols {
return nil, err
}
// The kernel relocation symbol (the mapping start address) can be either
// _text or _stext. When profiles are generated by `perf`, which one was used is
// distinguished by the mapping name for the kernel image:
// '[kernel.kallsyms]_text' or '[kernel.kallsyms]_stext', respectively. If we haven't
// been able to parse it from the mapping, we default to _stext.
if relocationSymbol == "" {
relocationSymbol = "_stext"
}
for _, s := range symbols {
if s.Name == relocationSymbol {
kernelOffset = &s.Value
break
}
}
}
// Check that we can compute a base for the binary. This may not be the
// correct base value, so we don't save it. We delay computing the actual base
// value until we have a sample address for this mapping, so that we can
// correctly identify the associated program segment that is needed to compute
// the base.
if _, err := elfexec.GetBase(&ef.FileHeader, elfexec.FindTextProgHeader(ef), kernelOffset, start, limit, offset); err != nil {
return nil, fmt.Errorf("could not identify base for %s: %v", name, err)
}
if b.fast || (!b.addr2lineFound && !b.llvmSymbolizerFound) {
return &fileNM{file: file{
b: b,
name: name,
buildID: buildID,
m: &elfMapping{start: start, limit: limit, offset: offset, kernelOffset: kernelOffset},
}}, nil
}
return &fileAddr2Line{file: file{
b: b,
name: name,
buildID: buildID,
m: &elfMapping{start: start, limit: limit, offset: offset, kernelOffset: kernelOffset},
}}, nil
}
func (b *binrep) openPE(name string, start, limit, offset uint64) (plugin.ObjFile, error) {
pf, err := pe.Open(name)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %v", name, err)
}
defer pf.Close()
var imageBase uint64
switch h := pf.OptionalHeader.(type) {
case *pe.OptionalHeader32:
imageBase = uint64(h.ImageBase)
case *pe.OptionalHeader64:
imageBase = uint64(h.ImageBase)
default:
return nil, fmt.Errorf("unknown OptionalHeader %T", pf.OptionalHeader)
}
var base uint64
if start > 0 {
base = start - imageBase
}
if b.fast || (!b.addr2lineFound && !b.llvmSymbolizerFound) {
return &fileNM{file: file{b: b, name: name, base: base}}, nil
}
return &fileAddr2Line{file: file{b: b, name: name, base: base}}, nil
}
// elfMapping stores the parameters of a runtime mapping that are needed to
// identify the ELF segment associated with a mapping.
type elfMapping struct {
// Runtime mapping parameters.
start, limit, offset uint64
// Offset of kernel relocation symbol. Only defined for kernel images, nil otherwise.
kernelOffset *uint64
}
// findProgramHeader returns the program segment that matches the current
// mapping and the given address, or an error if it cannot find a unique program
// header.
func (m *elfMapping) findProgramHeader(ef *elf.File, addr uint64) (*elf.ProgHeader, error) {
// For user space executables, we try to find the actual program segment that
// is associated with the given mapping. Skip this search if limit <= start.
// We cannot use just a check on the start address of the mapping to tell if
// it's a kernel / .ko module mapping, because with quipper address remapping
// enabled, the address would be in the lower half of the address space.
if m.kernelOffset != nil || m.start >= m.limit || m.limit >= (uint64(1)<<63) {
// For the kernel, find the program segment that includes the .text section.
return elfexec.FindTextProgHeader(ef), nil
}
// Fetch all the loadable segments.
var phdrs []elf.ProgHeader
for i := range ef.Progs {
if ef.Progs[i].Type == elf.PT_LOAD {
phdrs = append(phdrs, ef.Progs[i].ProgHeader)
}
}
// Some ELF files don't contain any loadable program segments, e.g. .ko
// kernel modules. It's not an error to have no header in such cases.
if len(phdrs) == 0 {
return nil, nil
}
// Get all program headers associated with the mapping.
headers := elfexec.ProgramHeadersForMapping(phdrs, m.offset, m.limit-m.start)
if len(headers) == 0 {
return nil, errors.New("no program header matches mapping info")
}
if len(headers) == 1 {
return headers[0], nil
}
// Use the file offset corresponding to the address to symbolize, to narrow
// down the header.
return elfexec.HeaderForFileOffset(headers, addr-m.start+m.offset)
}
// file implements the binutils.ObjFile interface.
type file struct {
b *binrep
name string
buildID string
baseOnce sync.Once // Ensures the base, baseErr and isData are computed once.
base uint64
baseErr error // Any eventual error while computing the base.
isData bool
// Mapping information. Relevant only for ELF files, nil otherwise.
m *elfMapping
}
// computeBase computes the relocation base for the given binary file only if
// the elfMapping field is set. It populates the base and isData fields and
// returns an error.
func (f *file) computeBase(addr uint64) error {
if f == nil || f.m == nil {
return nil
}
if addr < f.m.start || addr >= f.m.limit {
return fmt.Errorf("specified address %x is outside the mapping range [%x, %x] for file %q", addr, f.m.start, f.m.limit, f.name)
}
ef, err := elfOpen(f.name)
if err != nil {
return fmt.Errorf("error parsing %s: %v", f.name, err)
}
defer ef.Close()
ph, err := f.m.findProgramHeader(ef, addr)
if err != nil {
return fmt.Errorf("failed to find program header for file %q, ELF mapping %#v, address %x: %v", f.name, *f.m, addr, err)
}
base, err := elfexec.GetBase(&ef.FileHeader, ph, f.m.kernelOffset, f.m.start, f.m.limit, f.m.offset)
if err != nil {
return err
}
f.base = base
f.isData = ph != nil && ph.Flags&elf.PF_X == 0
return nil
}
func (f *file) Name() string {
return f.name
}
func (f *file) ObjAddr(addr uint64) (uint64, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return 0, f.baseErr
}
return addr - f.base, nil
}
func (f *file) BuildID() string {
return f.buildID
}
func (f *file) SourceLine(addr uint64) ([]plugin.Frame, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return nil, f.baseErr
}
return nil, nil
}
func (f *file) Close() error {
return nil
}
func (f *file) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) {
// Get from nm a list of symbols sorted by address.
cmd := exec.Command(f.b.nm, "-n", f.name)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("%v: %v", cmd.Args, err)
}
return findSymbols(out, f.name, r, addr)
}
// fileNM implements the binutils.ObjFile interface, using 'nm' to map
// addresses to symbols (without file/line number information). It is
// faster than fileAddr2Line.
type fileNM struct {
file
addr2linernm *addr2LinerNM
}
func (f *fileNM) SourceLine(addr uint64) ([]plugin.Frame, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return nil, f.baseErr
}
if f.addr2linernm == nil {
addr2liner, err := newAddr2LinerNM(f.b.nm, f.name, f.base)
if err != nil {
return nil, err
}
f.addr2linernm = addr2liner
}
return f.addr2linernm.addrInfo(addr)
}
// fileAddr2Line implements the binutils.ObjFile interface, using
// llvm-symbolizer, if that's available, or addr2line to map addresses to
// symbols (with file/line number information). It can be slow for large
// binaries with debug information.
type fileAddr2Line struct {
once sync.Once
file
addr2liner *addr2Liner
llvmSymbolizer *llvmSymbolizer
isData bool
}
func (f *fileAddr2Line) SourceLine(addr uint64) ([]plugin.Frame, error) {
f.baseOnce.Do(func() { f.baseErr = f.computeBase(addr) })
if f.baseErr != nil {
return nil, f.baseErr
}
f.once.Do(f.init)
if f.llvmSymbolizer != nil {
return f.llvmSymbolizer.addrInfo(addr)
}
if f.addr2liner != nil {
return f.addr2liner.addrInfo(addr)
}
return nil, fmt.Errorf("could not find local addr2liner")
}
func (f *fileAddr2Line) init() {
if llvmSymbolizer, err := newLLVMSymbolizer(f.b.llvmSymbolizer, f.name, f.base, f.isData); err == nil {
f.llvmSymbolizer = llvmSymbolizer
return
}
if addr2liner, err := newAddr2Liner(f.b.addr2line, f.name, f.base); err == nil {
f.addr2liner = addr2liner
// When addr2line encounters some gcc compiled binaries, it
// drops interesting parts of names in anonymous namespaces.
// Fallback to NM for better function names.
if nm, err := newAddr2LinerNM(f.b.nm, f.name, f.base); err == nil {
f.addr2liner.nm = nm
}
}
}
func (f *fileAddr2Line) Close() error {
if f.llvmSymbolizer != nil {
f.llvmSymbolizer.rw.close()
f.llvmSymbolizer = nil
}
if f.addr2liner != nil {
f.addr2liner.rw.close()
f.addr2liner = nil
}
return nil
}

View File

@@ -0,0 +1,978 @@
// 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 binutils
import (
"bytes"
"debug/elf"
"encoding/binary"
"errors"
"fmt"
"math"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"testing"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
var testAddrMap = map[int]string{
1000: "_Z3fooid.clone2",
2000: "_ZNSaIiEC1Ev.clone18",
3000: "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm",
}
func functionName(level int) (name string) {
if name = testAddrMap[level]; name != "" {
return name
}
return fmt.Sprintf("fun%d", level)
}
func TestAddr2Liner(t *testing.T) {
const offset = 0x500
a := addr2Liner{rw: &mockAddr2liner{}, base: offset}
for i := 1; i < 8; i++ {
addr := i*0x1000 + offset
s, err := a.addrInfo(uint64(addr))
if err != nil {
t.Fatalf("addrInfo(%#x): %v", addr, err)
}
if len(s) != i {
t.Fatalf("addrInfo(%#x): got len==%d, want %d", addr, len(s), i)
}
for l, f := range s {
level := (len(s) - l) * 1000
want := plugin.Frame{Func: functionName(level), File: fmt.Sprintf("file%d", level), Line: level}
if f != want {
t.Errorf("AddrInfo(%#x)[%d]: = %+v, want %+v", addr, l, f, want)
}
}
}
s, err := a.addrInfo(0xFFFF)
if err != nil {
t.Fatalf("addrInfo(0xFFFF): %v", err)
}
if len(s) != 0 {
t.Fatalf("AddrInfo(0xFFFF): got len==%d, want 0", len(s))
}
a.rw.close()
}
type mockAddr2liner struct {
output []string
}
func (a *mockAddr2liner) write(s string) error {
var lines []string
switch s {
case "1000":
lines = []string{"_Z3fooid.clone2", "file1000:1000"}
case "2000":
lines = []string{"_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "3000":
lines = []string{"_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "4000":
lines = []string{"fun4000", "file4000:4000", "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "5000":
lines = []string{"fun5000", "file5000:5000", "fun4000", "file4000:4000", "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "6000":
lines = []string{"fun6000", "file6000:6000", "fun5000", "file5000:5000", "fun4000", "file4000:4000", "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "7000":
lines = []string{"fun7000", "file7000:7000", "fun6000", "file6000:6000", "fun5000", "file5000:5000", "fun4000", "file4000:4000", "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "8000":
lines = []string{"fun8000", "file8000:8000", "fun7000", "file7000:7000", "fun6000", "file6000:6000", "fun5000", "file5000:5000", "fun4000", "file4000:4000", "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
case "9000":
lines = []string{"fun9000", "file9000:9000", "fun8000", "file8000:8000", "fun7000", "file7000:7000", "fun6000", "file6000:6000", "fun5000", "file5000:5000", "fun4000", "file4000:4000", "_ZNSt6vectorIS_IS_IiSaIiEESaIS1_EESaIS3_EEixEm", "file3000:3000", "_ZNSaIiEC1Ev.clone18", "file2000:2000", "_Z3fooid.clone2", "file1000:1000"}
default:
lines = []string{"??", "??:0"}
}
a.output = append(a.output, "0x"+s)
a.output = append(a.output, lines...)
return nil
}
func (a *mockAddr2liner) readLine() (string, error) {
if len(a.output) == 0 {
return "", fmt.Errorf("end of file")
}
next := a.output[0]
a.output = a.output[1:]
return next, nil
}
func (a *mockAddr2liner) close() {
}
func TestAddr2LinerLookup(t *testing.T) {
for _, tc := range []struct {
desc string
nmOutput string
wantSymbolized map[uint64]string
wantUnsymbolized []uint64
}{
{
desc: "odd symbol count",
nmOutput: `
0x1000 T 1000 100
0x2000 T 2000 120
0x3000 T 3000 130
`,
wantSymbolized: map[uint64]string{
0x1000: "0x1000",
0x1001: "0x1000",
0x1FFF: "0x1000",
0x2000: "0x2000",
0x2001: "0x2000",
0x3000: "0x3000",
0x312f: "0x3000",
},
wantUnsymbolized: []uint64{0x0fff, 0x3130},
},
{
desc: "even symbol count",
nmOutput: `
0x1000 T 1000 100
0x2000 T 2000 120
0x3000 T 3000 130
0x4000 T 4000 140
`,
wantSymbolized: map[uint64]string{
0x1000: "0x1000",
0x1001: "0x1000",
0x1FFF: "0x1000",
0x2000: "0x2000",
0x2fff: "0x2000",
0x3000: "0x3000",
0x3fff: "0x3000",
0x4000: "0x4000",
0x413f: "0x4000",
},
wantUnsymbolized: []uint64{0x0fff, 0x4140},
},
{
desc: "different symbol types",
nmOutput: `
absolute_0x100 a 100
absolute_0x200 A 200
text_0x1000 t 1000 100
bss_0x2000 b 2000 120
data_0x3000 d 3000 130
rodata_0x4000 r 4000 140
weak_0x5000 v 5000 150
text_0x6000 T 6000 160
bss_0x7000 B 7000 170
data_0x8000 D 8000 180
rodata_0x9000 R 9000 190
weak_0xa000 V a000 1a0
weak_0xb000 W b000 1b0
`,
wantSymbolized: map[uint64]string{
0x1000: "text_0x1000",
0x1FFF: "text_0x1000",
0x2000: "bss_0x2000",
0x211f: "bss_0x2000",
0x3000: "data_0x3000",
0x312f: "data_0x3000",
0x4000: "rodata_0x4000",
0x413f: "rodata_0x4000",
0x5000: "weak_0x5000",
0x514f: "weak_0x5000",
0x6000: "text_0x6000",
0x6fff: "text_0x6000",
0x7000: "bss_0x7000",
0x716f: "bss_0x7000",
0x8000: "data_0x8000",
0x817f: "data_0x8000",
0x9000: "rodata_0x9000",
0x918f: "rodata_0x9000",
0xa000: "weak_0xa000",
0xa19f: "weak_0xa000",
0xb000: "weak_0xb000",
0xb1af: "weak_0xb000",
},
wantUnsymbolized: []uint64{0x100, 0x200, 0x0fff, 0x2120, 0x3130, 0x4140, 0x5150, 0x7170, 0x8180, 0x9190, 0xa1a0, 0xb1b0},
},
} {
t.Run(tc.desc, func(t *testing.T) {
a, err := parseAddr2LinerNM(0, bytes.NewBufferString(tc.nmOutput))
if err != nil {
t.Fatalf("nm parse error: %v", err)
}
for address, want := range tc.wantSymbolized {
if got, _ := a.addrInfo(address); !checkAddress(got, address, want) {
t.Errorf("%x: got %v, want %s", address, got, want)
}
}
for _, unknown := range tc.wantUnsymbolized {
if got, _ := a.addrInfo(unknown); got != nil {
t.Errorf("%x: got %v, want nil", unknown, got)
}
}
})
}
}
func checkAddress(got []plugin.Frame, address uint64, want string) bool {
if len(got) != 1 {
return false
}
return got[0].Func == want
}
func TestSetTools(t *testing.T) {
// Test that multiple calls work.
bu := &Binutils{}
bu.SetTools("")
bu.SetTools("")
}
func TestSetFastSymbolization(t *testing.T) {
// Test that multiple calls work.
bu := &Binutils{}
bu.SetFastSymbolization(true)
bu.SetFastSymbolization(false)
}
func skipUnlessLinuxAmd64(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
t.Skip("This test only works on x86-64 Linux")
}
}
func skipUnlessDarwinAmd64(t *testing.T) {
if runtime.GOOS != "darwin" || runtime.GOARCH != "amd64" {
t.Skip("This test only works on x86-64 macOS")
}
}
func skipUnlessWindowsAmd64(t *testing.T) {
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
t.Skip("This test only works on x86-64 Windows")
}
}
func testDisasm(t *testing.T, intelSyntax bool) {
_, llvmObjdump, buObjdump := findObjdump([]string{""})
if !(llvmObjdump || buObjdump) {
t.Skip("cannot disasm: no objdump tool available")
}
bu := &Binutils{}
var testexe string
switch runtime.GOOS {
case "linux":
testexe = "exe_linux_64"
case "darwin":
testexe = "exe_mac_64"
case "windows":
testexe = "exe_windows_64.exe"
default:
t.Skipf("unsupported OS %q", runtime.GOOS)
}
insts, err := bu.Disasm(filepath.Join("testdata", testexe), 0, math.MaxUint64, intelSyntax)
if err != nil {
t.Fatalf("Disasm: unexpected error %v", err)
}
mainCount := 0
for _, x := range insts {
// macOS symbols have a leading underscore.
if x.Function == "main" || x.Function == "_main" {
mainCount++
}
}
if mainCount == 0 {
t.Error("Disasm: found no main instructions")
}
}
func TestDisasm(t *testing.T) {
if (runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "windows") || runtime.GOARCH != "amd64" {
t.Skip("This test only works on x86-64 Linux, macOS or Windows")
}
testDisasm(t, false)
}
func TestDisasmIntelSyntax(t *testing.T) {
if (runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "windows") || runtime.GOARCH != "amd64" {
t.Skip("This test only works on x86_64 Linux, macOS or Windows as it tests Intel asm syntax")
}
testDisasm(t, true)
}
func findSymbol(syms []*plugin.Sym, name string) *plugin.Sym {
for _, s := range syms {
for _, n := range s.Name {
if n == name {
return s
}
}
}
return nil
}
func TestObjFile(t *testing.T) {
// If this test fails, check the address for main function in testdata/exe_linux_64
// using the command 'nm -n '. Update the hardcoded addresses below to match
// the addresses from the output.
skipUnlessLinuxAmd64(t)
for _, tc := range []struct {
desc string
start, limit, offset uint64
addr uint64
}{
{"fixed load address", 0x400000, 0x4006fc, 0, 0x40052d},
// True user-mode ASLR binaries are ET_DYN rather than ET_EXEC so this case
// is a bit artificial except that it approximates the
// vmlinux-with-kernel-ASLR case where the binary *is* ET_EXEC.
{"simulated ASLR address", 0x500000, 0x5006fc, 0, 0x50052d},
} {
t.Run(tc.desc, func(t *testing.T) {
bu := &Binutils{}
f, err := bu.Open(filepath.Join("testdata", "exe_linux_64"), tc.start, tc.limit, tc.offset, "")
if err != nil {
t.Fatalf("Open: unexpected error %v", err)
}
defer f.Close()
syms, err := f.Symbols(regexp.MustCompile("main"), 0)
if err != nil {
t.Fatalf("Symbols: unexpected error %v", err)
}
m := findSymbol(syms, "main")
if m == nil {
t.Fatalf("Symbols: did not find main")
}
addr, err := f.ObjAddr(tc.addr)
if err != nil {
t.Fatalf("ObjAddr(%x) failed: %v", tc.addr, err)
}
if addr != m.Start {
t.Errorf("ObjAddr(%x) got %x, want %x", tc.addr, addr, m.Start)
}
gotFrames, err := f.SourceLine(tc.addr)
if err != nil {
t.Fatalf("SourceLine: unexpected error %v", err)
}
wantFrames := []plugin.Frame{
{Func: "main", File: "/tmp/hello.c", Line: 3, StartLine: 3},
}
if !reflect.DeepEqual(gotFrames, wantFrames) {
t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames)
}
})
}
}
func TestMachoFiles(t *testing.T) {
// If this test fails, check the address for main function in testdata/exe_mac_64
// and testdata/lib_mac_64 using addr2line or gaddr2line. Update the
// hardcoded addresses below to match the addresses from the output.
skipUnlessDarwinAmd64(t)
// Load `file`, pretending it was mapped at `start`. Then get the symbol
// table. Check that it contains the symbol `sym` and that the address
// `addr` gives the `expected` stack trace.
for _, tc := range []struct {
desc string
file string
start, limit, offset uint64
addr uint64
sym string
expected []plugin.Frame
}{
{"normal mapping", "exe_mac_64", 0x100000000, math.MaxUint64, 0,
0x100000f50, "_main",
[]plugin.Frame{
{Func: "main", File: "/tmp/hello.c", Line: 3, StartLine: 3},
}},
{"other mapping", "exe_mac_64", 0x200000000, math.MaxUint64, 0,
0x200000f50, "_main",
[]plugin.Frame{
{Func: "main", File: "/tmp/hello.c", Line: 3, StartLine: 3},
}},
{"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0,
0xfa0, "_bar",
[]plugin.Frame{
{Func: "bar", File: "/tmp/lib.c", Line: 5, StartLine: 5},
}},
} {
t.Run(tc.desc, func(t *testing.T) {
bu := &Binutils{}
f, err := bu.Open(filepath.Join("testdata", tc.file), tc.start, tc.limit, tc.offset, "")
if err != nil {
t.Fatalf("Open: unexpected error %v", err)
}
t.Logf("binutils: %v", bu)
if runtime.GOOS == "darwin" && !bu.rep.addr2lineFound && !bu.rep.llvmSymbolizerFound {
// On macOS, user needs to install gaddr2line or llvm-symbolizer with
// Homebrew, skip the test when the environment doesn't have it
// installed.
t.Skip("couldn't find addr2line or gaddr2line")
}
defer f.Close()
syms, err := f.Symbols(nil, 0)
if err != nil {
t.Fatalf("Symbols: unexpected error %v", err)
}
m := findSymbol(syms, tc.sym)
if m == nil {
t.Fatalf("Symbols: could not find symbol %v", tc.sym)
}
gotFrames, err := f.SourceLine(tc.addr)
if err != nil {
t.Fatalf("SourceLine: unexpected error %v", err)
}
if !reflect.DeepEqual(gotFrames, tc.expected) {
t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, tc.expected)
}
})
}
}
func TestLLVMSymbolizer(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("testtdata/llvm-symbolizer has only been tested on linux")
}
cmd := filepath.Join("testdata", "fake-llvm-symbolizer")
for _, c := range []struct {
addr uint64
isData bool
frames []plugin.Frame
}{
{0x10, false, []plugin.Frame{
{Func: "Inlined_0x10", File: "foo.h", Line: 0, Column: 0, StartLine: 0},
{Func: "Func_0x10", File: "foo.c", Line: 2, Column: 1, StartLine: 2},
}},
{0x20, true, []plugin.Frame{
{Func: "foo_0x20", File: "0x20 8"},
}},
} {
desc := fmt.Sprintf("Code %x", c.addr)
if c.isData {
desc = fmt.Sprintf("Data %x", c.addr)
}
t.Run(desc, func(t *testing.T) {
symbolizer, err := newLLVMSymbolizer(cmd, "foo", 0, c.isData)
if err != nil {
t.Fatalf("newLLVMSymbolizer: unexpected error %v", err)
}
defer symbolizer.rw.close()
frames, err := symbolizer.addrInfo(c.addr)
if err != nil {
t.Fatalf("LLVM: unexpected error %v", err)
}
if !reflect.DeepEqual(frames, c.frames) {
t.Errorf("LLVM: expect %v; got %v\n", c.frames, frames)
}
})
}
}
func TestPEFile(t *testing.T) {
// If this test fails, check the address for main function in testdata/exe_windows_64.exe
// using the command 'nm -n '. Update the hardcoded addresses below to match
// the addresses from the output.
skipUnlessWindowsAmd64(t)
for _, tc := range []struct {
desc string
start, limit, offset uint64
addr uint64
}{
{"fake mapping", 0, math.MaxUint64, 0, 0x140001594},
{"fixed load address", 0x140000000, 0x140002000, 0, 0x140001594},
{"simulated ASLR address", 0x150000000, 0x150002000, 0, 0x150001594},
} {
t.Run(tc.desc, func(t *testing.T) {
bu := &Binutils{}
f, err := bu.Open(filepath.Join("testdata", "exe_windows_64.exe"), tc.start, tc.limit, tc.offset, "")
if err != nil {
t.Fatalf("Open: unexpected error %v", err)
}
defer f.Close()
syms, err := f.Symbols(regexp.MustCompile("main"), 0)
if err != nil {
t.Fatalf("Symbols: unexpected error %v", err)
}
m := findSymbol(syms, "main")
if m == nil {
t.Fatalf("Symbols: did not find main")
}
addr, err := f.ObjAddr(tc.addr)
if err != nil {
t.Fatalf("ObjAddr(%x) failed: %v", tc.addr, err)
}
if addr != m.Start {
t.Errorf("ObjAddr(%x) got %x, want %x", tc.addr, addr, m.Start)
}
gotFrames, err := f.SourceLine(tc.addr)
if err != nil {
t.Fatalf("SourceLine: unexpected error %v", err)
}
wantFrames := []plugin.Frame{
{Func: "main", File: "hello.c", Line: 3, Column: 12, StartLine: 3},
}
if !reflect.DeepEqual(gotFrames, wantFrames) {
t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames)
}
})
}
}
func TestOpenMalformedELF(t *testing.T) {
// Test that opening a malformed ELF file will report an error containing
// the word "ELF".
bu := &Binutils{}
_, err := bu.Open(filepath.Join("testdata", "malformed_elf"), 0, 0, 0, "")
if err == nil {
t.Fatalf("Open: unexpected success")
}
if !strings.Contains(err.Error(), "ELF") {
t.Errorf("Open: got %v, want error containing 'ELF'", err)
}
}
func TestOpenMalformedMachO(t *testing.T) {
// Test that opening a malformed Mach-O file will report an error containing
// the word "Mach-O".
bu := &Binutils{}
_, err := bu.Open(filepath.Join("testdata", "malformed_macho"), 0, 0, 0, "")
if err == nil {
t.Fatalf("Open: unexpected success")
}
if !strings.Contains(err.Error(), "Mach-O") {
t.Errorf("Open: got %v, want error containing 'Mach-O'", err)
}
}
func TestObjdumpVersionChecks(t *testing.T) {
// Test that the objdump version strings are parsed properly.
type testcase struct {
desc string
os string
ver string
want bool
}
for _, tc := range []testcase{
{
desc: "Valid Apple LLVM version string with usable version",
os: "darwin",
ver: "Apple LLVM version 11.0.3 (clang-1103.0.32.62)\nOptimized build.",
want: true,
},
{
desc: "Valid Apple LLVM version string with unusable version",
os: "darwin",
ver: "Apple LLVM version 10.0.0 (clang-1000.11.45.5)\nOptimized build.",
want: false,
},
{
desc: "Invalid Apple LLVM version string with usable version",
os: "darwin",
ver: "Apple LLVM versions 11.0.3 (clang-1103.0.32.62)\nOptimized build.",
want: false,
},
{
desc: "Valid LLVM version string with usable version",
os: "linux",
ver: "LLVM (http://llvm.org/):\nLLVM version 9.0.1\n\nOptimized build.",
want: true,
},
{
desc: "Valid LLVM version string with unusable version",
os: "linux",
ver: "LLVM (http://llvm.org/):\nLLVM version 6.0.1\n\nOptimized build.",
want: false,
},
{
desc: "Invalid LLVM version string with usable version",
os: "linux",
ver: "LLVM (http://llvm.org/):\nLLVM versions 9.0.1\n\nOptimized build.",
want: false,
},
{
desc: "Valid LLVM objdump version string with trunk",
os: runtime.GOOS,
ver: "LLVM (http://llvm.org/):\nLLVM version custom-trunk 124ffeb592a00bfe\nOptimized build.",
want: true,
},
{
desc: "Invalid LLVM objdump version string with trunk",
os: runtime.GOOS,
ver: "LLVM (http://llvm.org/):\nLLVM version custom-trank 124ffeb592a00bfe\nOptimized build.",
want: false,
},
{
desc: "Invalid LLVM objdump version string with trunk",
os: runtime.GOOS,
ver: "LLVM (http://llvm.org/):\nllvm version custom-trunk 124ffeb592a00bfe\nOptimized build.",
want: false,
},
} {
if runtime.GOOS == tc.os {
if got := isLLVMObjdump(tc.ver); got != tc.want {
t.Errorf("%v: got %v, want %v", tc.desc, got, tc.want)
}
}
}
for _, tc := range []testcase{
{
desc: "Valid GNU objdump version string",
ver: "GNU objdump (GNU Binutils) 2.34\nCopyright (C) 2020 Free Software Foundation, Inc.",
want: true,
},
{
desc: "Invalid GNU objdump version string",
ver: "GNU nm (GNU Binutils) 2.34\nCopyright (C) 2020 Free Software Foundation, Inc.",
want: false,
},
} {
if got := isBuObjdump(tc.ver); got != tc.want {
t.Errorf("%v: got %v, want %v", tc.desc, got, tc.want)
}
}
}
func TestComputeBase(t *testing.T) {
realELFOpen := elfOpen
defer func() {
elfOpen = realELFOpen
}()
tinyExecFile := &elf.File{
FileHeader: elf.FileHeader{Type: elf.ET_EXEC},
Progs: []*elf.Prog{
{ProgHeader: elf.ProgHeader{Type: elf.PT_PHDR, Flags: elf.PF_R | elf.PF_X, Off: 0x40, Vaddr: 0x400040, Paddr: 0x400040, Filesz: 0x1f8, Memsz: 0x1f8, Align: 8}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_INTERP, Flags: elf.PF_R, Off: 0x238, Vaddr: 0x400238, Paddr: 0x400238, Filesz: 0x1c, Memsz: 0x1c, Align: 1}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_LOAD, Flags: elf.PF_R | elf.PF_X, Off: 0, Vaddr: 0, Paddr: 0, Filesz: 0xc80, Memsz: 0xc80, Align: 0x200000}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_LOAD, Flags: elf.PF_R | elf.PF_W, Off: 0xc80, Vaddr: 0x200c80, Paddr: 0x200c80, Filesz: 0x1f0, Memsz: 0x1f0, Align: 0x200000}},
},
}
tinyBadBSSExecFile := &elf.File{
FileHeader: elf.FileHeader{Type: elf.ET_EXEC},
Progs: []*elf.Prog{
{ProgHeader: elf.ProgHeader{Type: elf.PT_PHDR, Flags: elf.PF_R | elf.PF_X, Off: 0x40, Vaddr: 0x400040, Paddr: 0x400040, Filesz: 0x1f8, Memsz: 0x1f8, Align: 8}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_INTERP, Flags: elf.PF_R, Off: 0x238, Vaddr: 0x400238, Paddr: 0x400238, Filesz: 0x1c, Memsz: 0x1c, Align: 1}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_LOAD, Flags: elf.PF_R | elf.PF_X, Off: 0, Vaddr: 0, Paddr: 0, Filesz: 0xc80, Memsz: 0xc80, Align: 0x200000}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_LOAD, Flags: elf.PF_R | elf.PF_W, Off: 0xc80, Vaddr: 0x200c80, Paddr: 0x200c80, Filesz: 0x100, Memsz: 0x1f0, Align: 0x200000}},
{ProgHeader: elf.ProgHeader{Type: elf.PT_LOAD, Flags: elf.PF_R | elf.PF_W, Off: 0xd80, Vaddr: 0x400d80, Paddr: 0x400d80, Filesz: 0x90, Memsz: 0x90, Align: 0x200000}},
},
}
for _, tc := range []struct {
desc string
file *elf.File
openErr error
mapping *elfMapping
addr uint64
wantError bool
wantBase uint64
wantIsData bool
}{
{
desc: "no elf mapping, no error",
mapping: nil,
addr: 0x1000,
wantBase: 0,
wantIsData: false,
},
{
desc: "address outside mapping bounds means error",
file: &elf.File{},
mapping: &elfMapping{start: 0x2000, limit: 0x5000, offset: 0x1000},
addr: 0x1000,
wantError: true,
},
{
desc: "elf.Open failing means error",
file: &elf.File{FileHeader: elf.FileHeader{Type: elf.ET_EXEC}},
openErr: errors.New("elf.Open failed"),
mapping: &elfMapping{start: 0x2000, limit: 0x5000, offset: 0x1000},
addr: 0x4000,
wantError: true,
},
{
desc: "no loadable segments, no error",
file: &elf.File{FileHeader: elf.FileHeader{Type: elf.ET_EXEC}},
mapping: &elfMapping{start: 0x2000, limit: 0x5000, offset: 0x1000},
addr: 0x4000,
wantBase: 0,
wantIsData: false,
},
{
desc: "unsupported executable type, Get Base returns error",
file: &elf.File{FileHeader: elf.FileHeader{Type: elf.ET_NONE}},
mapping: &elfMapping{start: 0x2000, limit: 0x5000, offset: 0x1000},
addr: 0x4000,
wantError: true,
},
{
desc: "tiny file select executable segment by offset",
file: tinyExecFile,
mapping: &elfMapping{start: 0x5000000, limit: 0x5001000, offset: 0x0},
addr: 0x5000c00,
wantBase: 0x5000000,
wantIsData: false,
},
{
desc: "tiny file select data segment by offset",
file: tinyExecFile,
mapping: &elfMapping{start: 0x5200000, limit: 0x5201000, offset: 0x0},
addr: 0x5200c80,
wantBase: 0x5000000,
wantIsData: true,
},
{
desc: "tiny file offset outside any segment means error",
file: tinyExecFile,
mapping: &elfMapping{start: 0x5200000, limit: 0x5201000, offset: 0x0},
addr: 0x5200e70,
wantError: true,
},
{
desc: "tiny file with bad BSS segment selects data segment by offset in initialized section",
file: tinyBadBSSExecFile,
mapping: &elfMapping{start: 0x5200000, limit: 0x5201000, offset: 0x0},
addr: 0x5200d79,
wantBase: 0x5000000,
wantIsData: true,
},
{
desc: "tiny file with bad BSS segment with offset in uninitialized section means error",
file: tinyBadBSSExecFile,
mapping: &elfMapping{start: 0x5200000, limit: 0x5201000, offset: 0x0},
addr: 0x5200d80,
wantError: true,
},
} {
t.Run(tc.desc, func(t *testing.T) {
elfOpen = func(_ string) (*elf.File, error) {
return tc.file, tc.openErr
}
f := file{m: tc.mapping}
err := f.computeBase(tc.addr)
if (err != nil) != tc.wantError {
t.Errorf("got error %v, want any error=%v", err, tc.wantError)
}
if err != nil {
return
}
if f.base != tc.wantBase {
t.Errorf("got base %x, want %x", f.base, tc.wantBase)
}
if f.isData != tc.wantIsData {
t.Errorf("got isData %v, want %v", f.isData, tc.wantIsData)
}
})
}
}
func TestELFObjAddr(t *testing.T) {
// The exe_linux_64 has two loadable program headers:
// LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
// 0x00000000000006fc 0x00000000000006fc R E 0x200000
// LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
// 0x0000000000000230 0x0000000000000238 RW 0x200000
name := filepath.Join("testdata", "exe_linux_64")
for _, tc := range []struct {
desc string
start, limit, offset uint64
wantOpenError bool
addr uint64
wantObjAddr uint64
wantAddrError bool
}{
{"exec mapping, good address", 0x5400000, 0x5401000, 0, false, 0x5400400, 0x400400, false},
{"exec mapping, address outside segment", 0x5400000, 0x5401000, 0, false, 0x5400800, 0, true},
{"short data mapping, good address", 0x5600e00, 0x5602000, 0xe00, false, 0x5600e10, 0x600e10, false},
{"short data mapping, address outside segment", 0x5600e00, 0x5602000, 0xe00, false, 0x5600e00, 0x600e00, false},
{"page aligned data mapping, good address", 0x5600000, 0x5602000, 0, false, 0x5601000, 0x601000, false},
{"page aligned data mapping, address outside segment", 0x5600000, 0x5602000, 0, false, 0x5601048, 0, true},
{"bad file offset, no matching segment", 0x5600000, 0x5602000, 0x2000, false, 0x5600e10, 0, true},
{"large mapping size, match by sample offset", 0x5600000, 0x5603000, 0, false, 0x5600e10, 0x600e10, false},
} {
t.Run(tc.desc, func(t *testing.T) {
b := binrep{}
o, err := b.openELF(name, tc.start, tc.limit, tc.offset, "")
if (err != nil) != tc.wantOpenError {
t.Errorf("openELF got error %v, want any error=%v", err, tc.wantOpenError)
}
if err != nil {
return
}
got, err := o.ObjAddr(tc.addr)
if (err != nil) != tc.wantAddrError {
t.Errorf("ObjAddr got error %v, want any error=%v", err, tc.wantAddrError)
}
if err != nil {
return
}
if got != tc.wantObjAddr {
t.Errorf("got ObjAddr %x; want %x\n", got, tc.wantObjAddr)
}
})
}
}
type buf struct {
data []byte
}
// write appends a null-terminated string and returns its starting index.
func (b *buf) write(s string) uint32 {
res := uint32(len(b.data))
b.data = append(b.data, s...)
b.data = append(b.data, '\x00')
return res
}
// fakeELFFile generates a minimal valid ELF file, with fake .head.text and
// .text sections, and their corresponding _text and _stext start symbols,
// mimicking a kernel vmlinux image.
func fakeELFFile(t *testing.T) *elf.File {
var (
sizeHeader64 = binary.Size(elf.Header64{})
sizeProg64 = binary.Size(elf.Prog64{})
sizeSection64 = binary.Size(elf.Section64{})
)
const (
textAddr = 0xffff000010080000
stextAddr = 0xffff000010081000
)
// Generate magic to identify as an ELF file.
var ident [16]uint8
ident[0] = '\x7f'
ident[1] = 'E'
ident[2] = 'L'
ident[3] = 'F'
ident[elf.EI_CLASS] = uint8(elf.ELFCLASS64)
ident[elf.EI_DATA] = uint8(elf.ELFDATA2LSB)
ident[elf.EI_VERSION] = uint8(elf.EV_CURRENT)
ident[elf.EI_OSABI] = uint8(elf.ELFOSABI_NONE)
// A single program header, containing code and starting at the _text address.
progs := []elf.Prog64{{
Type: uint32(elf.PT_LOAD), Flags: uint32(elf.PF_R | elf.PF_X), Off: 0x10000, Vaddr: textAddr, Paddr: textAddr, Filesz: 0x1234567, Memsz: 0x1234567, Align: 0x10000}}
symNames := buf{}
syms := []elf.Sym64{
{}, // first symbol empty by convention
{Name: symNames.write("_text"), Info: 0, Other: 0, Shndx: 0, Value: textAddr, Size: 0},
{Name: symNames.write("_stext"), Info: 0, Other: 0, Shndx: 0, Value: stextAddr, Size: 0},
}
const numSections = 5
// We'll write `textSize` zero bytes as contents of the .head.text and .text sections.
const textSize = 16
// Offset of section contents in the byte stream -- after header, program headers, and section headers.
sectionsStart := uint64(sizeHeader64 + len(progs)*sizeProg64 + numSections*sizeSection64)
secNames := buf{}
sections := [numSections]elf.Section64{
{Name: secNames.write(".head.text"), Type: uint32(elf.SHT_PROGBITS), Flags: uint64(elf.SHF_ALLOC | elf.SHF_EXECINSTR), Addr: textAddr, Off: sectionsStart, Size: textSize, Link: 0, Info: 0, Addralign: 2048, Entsize: 0},
{Name: secNames.write(".text"), Type: uint32(elf.SHT_PROGBITS), Flags: uint64(elf.SHF_ALLOC | elf.SHF_EXECINSTR), Addr: stextAddr, Off: sectionsStart + textSize, Size: textSize, Link: 0, Info: 0, Addralign: 2048, Entsize: 0},
{Name: secNames.write(".symtab"), Type: uint32(elf.SHT_SYMTAB), Flags: 0, Addr: 0, Off: sectionsStart + 2*textSize, Size: uint64(len(syms) * elf.Sym64Size), Link: 3 /*index of .strtab*/, Info: 0, Addralign: 8, Entsize: elf.Sym64Size},
{Name: secNames.write(".strtab"), Type: uint32(elf.SHT_STRTAB), Flags: 0, Addr: 0, Off: sectionsStart + 2*textSize + uint64(len(syms)*elf.Sym64Size), Size: uint64(len(symNames.data)), Link: 0, Info: 0, Addralign: 1, Entsize: 0},
{Name: secNames.write(".shstrtab"), Type: uint32(elf.SHT_STRTAB), Flags: 0, Addr: 0, Off: sectionsStart + 2*textSize + uint64(len(syms)*elf.Sym64Size+len(symNames.data)), Size: uint64(len(secNames.data)), Link: 0, Info: 0, Addralign: 1, Entsize: 0},
}
hdr := elf.Header64{
Ident: ident,
Type: uint16(elf.ET_DYN),
Machine: uint16(elf.EM_AARCH64),
Version: uint32(elf.EV_CURRENT),
Entry: textAddr,
Phoff: uint64(sizeHeader64),
Shoff: uint64(sizeHeader64 + len(progs)*sizeProg64),
Flags: 0,
Ehsize: uint16(sizeHeader64),
Phentsize: uint16(sizeProg64),
Phnum: uint16(len(progs)),
Shentsize: uint16(sizeSection64),
Shnum: uint16(len(sections)),
Shstrndx: 4, // index of .shstrtab
}
// Serialize all headers and sections into a single binary stream.
var data bytes.Buffer
for i, b := range []interface{}{hdr, progs, sections, [textSize]byte{}, [textSize]byte{}, syms, symNames.data, secNames.data} {
err := binary.Write(&data, binary.LittleEndian, b)
if err != nil {
t.Fatalf("Write(%v) got err %v, want nil", i, err)
}
}
// ... and parse it as and ELF file.
ef, err := elf.NewFile(bytes.NewReader(data.Bytes()))
if err != nil {
t.Fatalf("elf.NewFile got err %v, want nil", err)
}
return ef
}
func TestELFKernelOffset(t *testing.T) {
realELFOpen := elfOpen
defer func() {
elfOpen = realELFOpen
}()
wantAddr := uint64(0xffff000010082000)
elfOpen = func(_ string) (*elf.File, error) {
return fakeELFFile(t), nil
}
for _, tc := range []struct {
name string
relocationSymbol string
start uint64
}{
{"text", "_text", 0xffff000020080000},
{"stext", "_stext", 0xffff000020081000},
} {
b := binrep{}
o, err := b.openELF("vmlinux", tc.start, 0xffffffffffffffff, tc.start, tc.relocationSymbol)
if err != nil {
t.Errorf("%v: openELF got error %v, want nil", tc.name, err)
continue
}
addr, err := o.ObjAddr(0xffff000020082000)
if err != nil {
t.Errorf("%v: ObjAddr got err %v, want nil", tc.name, err)
continue
}
if addr != wantAddr {
t.Errorf("%v: ObjAddr got %x, want %x", tc.name, addr, wantAddr)
}
}
}

View File

@@ -0,0 +1,180 @@
// 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 binutils
import (
"bytes"
"io"
"regexp"
"strconv"
"strings"
"github.com/ianlancetaylor/demangle"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
var (
nmOutputRE = regexp.MustCompile(`^\s*([[:xdigit:]]+)\s+(.)\s+(.*)`)
objdumpAsmOutputRE = regexp.MustCompile(`^\s*([[:xdigit:]]+):\s+(.*)`)
objdumpOutputFileLine = regexp.MustCompile(`^;?\s?(.*):([0-9]+)`)
objdumpOutputFunction = regexp.MustCompile(`^;?\s?(\S.*)\(\):`)
objdumpOutputFunctionLLVM = regexp.MustCompile(`^([[:xdigit:]]+)?\s?(.*):`)
)
func findSymbols(syms []byte, file string, r *regexp.Regexp, address uint64) ([]*plugin.Sym, error) {
// Collect all symbols from the nm output, grouping names mapped to
// the same address into a single symbol.
// The symbols to return.
var symbols []*plugin.Sym
// The current group of symbol names, and the address they are all at.
names, start := []string{}, uint64(0)
buf := bytes.NewBuffer(syms)
for {
symAddr, name, err := nextSymbol(buf)
if err == io.EOF {
// Done. If there was an unfinished group, append it.
if len(names) != 0 {
if match := matchSymbol(names, start, symAddr-1, r, address); match != nil {
symbols = append(symbols, &plugin.Sym{Name: match, File: file, Start: start, End: symAddr - 1})
}
}
// And return the symbols.
return symbols, nil
}
if err != nil {
// There was some kind of serious error reading nm's output.
return nil, err
}
// If this symbol is at the same address as the current group, add it to the group.
if symAddr == start {
names = append(names, name)
continue
}
// Otherwise append the current group to the list of symbols.
if match := matchSymbol(names, start, symAddr-1, r, address); match != nil {
symbols = append(symbols, &plugin.Sym{Name: match, File: file, Start: start, End: symAddr - 1})
}
// And start a new group.
names, start = []string{name}, symAddr
}
}
// matchSymbol checks if a symbol is to be selected by checking its
// name to the regexp and optionally its address. It returns the name(s)
// to be used for the matched symbol, or nil if no match
func matchSymbol(names []string, start, end uint64, r *regexp.Regexp, address uint64) []string {
if address != 0 && address >= start && address <= end {
return names
}
for _, name := range names {
if r == nil || r.MatchString(name) {
return []string{name}
}
// Match all possible demangled versions of the name.
for _, o := range [][]demangle.Option{
{demangle.NoClones},
{demangle.NoParams, demangle.NoEnclosingParams},
{demangle.NoParams, demangle.NoEnclosingParams, demangle.NoTemplateParams},
} {
if demangled, err := demangle.ToString(name, o...); err == nil && r.MatchString(demangled) {
return []string{demangled}
}
}
}
return nil
}
// disassemble parses the output of the objdump command and returns
// the assembly instructions in a slice.
func disassemble(asm []byte) ([]plugin.Inst, error) {
buf := bytes.NewBuffer(asm)
function, file, line := "", "", 0
var assembly []plugin.Inst
for {
input, err := buf.ReadString('\n')
if err != nil {
if err != io.EOF {
return nil, err
}
if input == "" {
break
}
}
input = strings.TrimSpace(input)
if fields := objdumpAsmOutputRE.FindStringSubmatch(input); len(fields) == 3 {
if address, err := strconv.ParseUint(fields[1], 16, 64); err == nil {
assembly = append(assembly,
plugin.Inst{
Addr: address,
Text: fields[2],
Function: function,
File: file,
Line: line,
})
continue
}
}
if fields := objdumpOutputFileLine.FindStringSubmatch(input); len(fields) == 3 {
if l, err := strconv.ParseUint(fields[2], 10, 32); err == nil {
file, line = fields[1], int(l)
}
continue
}
if fields := objdumpOutputFunction.FindStringSubmatch(input); len(fields) == 2 {
function = fields[1]
continue
} else {
if fields := objdumpOutputFunctionLLVM.FindStringSubmatch(input); len(fields) == 3 {
function = fields[2]
continue
}
}
// Reset on unrecognized lines.
function, file, line = "", "", 0
}
return assembly, nil
}
// nextSymbol parses the nm output to find the next symbol listed.
// Skips over any output it cannot recognize.
func nextSymbol(buf *bytes.Buffer) (uint64, string, error) {
for {
line, err := buf.ReadString('\n')
if err != nil {
if err != io.EOF || line == "" {
return 0, "", err
}
}
line = strings.TrimSpace(line)
if fields := nmOutputRE.FindStringSubmatch(line); len(fields) == 4 {
if address, err := strconv.ParseUint(fields[1], 16, 64); err == nil {
return address, fields[3], nil
}
}
}
}

View File

@@ -0,0 +1,160 @@
// 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 binutils
import (
"fmt"
"regexp"
"testing"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
// TestFindSymbols tests the FindSymbols routine using a hardcoded nm output.
func TestFindSymbols(t *testing.T) {
type testcase struct {
query, syms string
want []plugin.Sym
}
testsyms := `0000000000001000 t lineA001
0000000000001000 t lineA002
0000000000001000 t line1000
0000000000002000 t line200A
0000000000002000 t line2000
0000000000002000 t line200B
0000000000003000 t line3000
0000000000003000 t _ZNK4DumbclEPKc
0000000000003000 t lineB00C
0000000000003000 t line300D
0000000000004000 t _the_end
`
testcases := []testcase{
{
"line.*[AC]",
testsyms,
[]plugin.Sym{
{Name: []string{"lineA001"}, File: "object.o", Start: 0x1000, End: 0x1FFF},
{Name: []string{"line200A"}, File: "object.o", Start: 0x2000, End: 0x2FFF},
{Name: []string{"lineB00C"}, File: "object.o", Start: 0x3000, End: 0x3FFF},
},
},
{
"Dumb::operator",
testsyms,
[]plugin.Sym{
{Name: []string{"Dumb::operator()(char const*) const"}, File: "object.o", Start: 0x3000, End: 0x3FFF},
},
},
}
for _, tc := range testcases {
syms, err := findSymbols([]byte(tc.syms), "object.o", regexp.MustCompile(tc.query), 0)
if err != nil {
t.Fatalf("%q: findSymbols: %v", tc.query, err)
}
if err := checkSymbol(syms, tc.want); err != nil {
t.Errorf("%q: %v", tc.query, err)
}
}
}
func checkSymbol(got []*plugin.Sym, want []plugin.Sym) error {
if len(got) != len(want) {
return fmt.Errorf("unexpected number of symbols %d (want %d)", len(got), len(want))
}
for i, g := range got {
w := want[i]
if len(g.Name) != len(w.Name) {
return fmt.Errorf("names, got %d, want %d", len(g.Name), len(w.Name))
}
for n := range g.Name {
if g.Name[n] != w.Name[n] {
return fmt.Errorf("name %d, got %q, want %q", n, g.Name[n], w.Name[n])
}
}
if g.File != w.File {
return fmt.Errorf("filename, got %q, want %q", g.File, w.File)
}
if g.Start != w.Start {
return fmt.Errorf("start address, got %#x, want %#x", g.Start, w.Start)
}
if g.End != w.End {
return fmt.Errorf("end address, got %#x, want %#x", g.End, w.End)
}
}
return nil
}
// TestFunctionAssembly tests the FunctionAssembly routine by using a
// fake objdump script.
func TestFunctionAssembly(t *testing.T) {
type testcase struct {
s plugin.Sym
asm string
want []plugin.Inst
}
testcases := []testcase{
{
plugin.Sym{Name: []string{"symbol1"}, Start: 0x1000, End: 0x1FFF},
" 1000: instruction one\n 1001: instruction two\n 1002: instruction three\n 1003: instruction four",
[]plugin.Inst{
{Addr: 0x1000, Text: "instruction one"},
{Addr: 0x1001, Text: "instruction two"},
{Addr: 0x1002, Text: "instruction three"},
{Addr: 0x1003, Text: "instruction four"},
},
},
{
plugin.Sym{Name: []string{"symbol2"}, Start: 0x2000, End: 0x2FFF},
" 2000: instruction one\n 2001: instruction two",
[]plugin.Inst{
{Addr: 0x2000, Text: "instruction one"},
{Addr: 0x2001, Text: "instruction two"},
},
},
{
plugin.Sym{Name: []string{"_main"}, Start: 0x30000, End: 0x3FFF},
"_main:\n; /tmp/hello.c:3\n30001: push %rbp",
[]plugin.Inst{
{Addr: 0x30001, Text: "push %rbp", Function: "_main", File: "/tmp/hello.c", Line: 3},
},
},
{
plugin.Sym{Name: []string{"main"}, Start: 0x4000, End: 0x4FFF},
"000000000040052d <main>:\nmain():\n/tmp/hello.c:3\n40001: push %rbp",
[]plugin.Inst{
{Addr: 0x40001, Text: "push %rbp", Function: "main", File: "/tmp/hello.c", Line: 3},
},
},
}
for _, tc := range testcases {
insts, err := disassemble([]byte(tc.asm))
if err != nil {
t.Fatalf("FunctionAssembly: %v", err)
}
if len(insts) != len(tc.want) {
t.Errorf("Unexpected number of assembly instructions %d (want %d)\n", len(insts), len(tc.want))
}
for i := range insts {
if insts[i] != tc.want[i] {
t.Errorf("Expected symbol %v, got %v\n", tc.want[i], insts[i])
}
}
}
}

View File

@@ -0,0 +1,94 @@
// Copyright 2019 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.
// This is a script that generates the test executables for MacOS and Linux
// in this directory. It should be needed very rarely to run this script.
// It is mostly provided as a future reference on how the original binary
// set was created.
// When a new executable is generated, hardcoded addresses in the
// functions TestObjFile, TestMachoFiles, TestPEFile in binutils_test.go must be updated.
package main
import (
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
)
func main() {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
switch runtime.GOOS {
case "linux":
if err := removeGlob("exe_linux_64*"); err != nil {
log.Fatal(err)
}
out, err := exec.Command("cc", "-g", "-ffile-prefix-map="+wd+"="+"/tmp", "-o", "exe_linux_64", "hello.c").CombinedOutput()
log.Println(string(out))
if err != nil {
log.Fatal(err)
}
case "darwin":
if err := removeGlob("exe_mac_64*", "lib_mac_64"); err != nil {
log.Fatal(err)
}
out, err := exec.Command("clang", "-g", "-ffile-prefix-map="+wd+"="+"/tmp", "-o", "exe_mac_64", "hello.c").CombinedOutput()
log.Println(string(out))
if err != nil {
log.Fatal(err)
}
out, err = exec.Command("clang", "-g", "-ffile-prefix-map="+wd+"="+"/tmp", "-o", "lib_mac_64", "-dynamiclib", "lib.c").CombinedOutput()
log.Println(string(out))
if err != nil {
log.Fatal(err)
}
case "windows":
// Many gcc environments may create binaries that trigger false-positives
// in antiviruses. MSYS2 with gcc 10.2.0 is a working environment for
// compiling. To setup the environment follow the guide at
// https://www.msys2.org/ and install gcc with `pacman -S gcc`.
out, err := exec.Command("gcc", "-g", "-ffile-prefix-map="+wd+"=", "-o", "exe_windows_64.exe", "hello.c").CombinedOutput()
log.Println(string(out))
if err != nil {
log.Fatal(err)
}
log.Println("Please verify that exe_windows_64.exe does not trigger any antivirus on `virustotal.com`.")
default:
log.Fatalf("Unsupported OS %q", runtime.GOOS)
}
}
func removeGlob(globs ...string) error {
for _, glob := range globs {
matches, err := filepath.Glob(glob)
if err != nil {
return err
}
for _, p := range matches {
os.Remove(p)
}
}
return nil
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.exe_mac_64</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

Binary file not shown.

View File

@@ -0,0 +1,39 @@
#!/bin/sh
#
# 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.
#
# Fake llvm-symbolizer to use in tests
set -f
IFS=" "
while read line; do
# line has form:
# filename 0xaddr
# Emit dummy output that matches llvm-symbolizer JSON output format.
set -- ${line}
kind=$1
fname=$2
addr=$3
case ${kind} in
CODE)
echo "{\"Address\":\"${addr}\",\"ModuleName\":\"${fname}\",\"Symbol\":[{\"Column\":0,\"FileName\":\"${fname}.h\",\"FunctionName\":\"Inlined_${addr}\",\"Line\":0,\"StartLine\":0},{\"Column\":1,\"FileName\":\"${fname}.c\",\"FunctionName\":\"Func_${addr}\",\"Line\":2,\"StartLine\":2}]}"
;;
DATA)
echo "{\"Address\":\"${addr}\",\"ModuleName\":\"${fname}\",\"Data\":{\"Name\":\"${fname}_${addr}\",\"Size\":\"0x8\",\"Start\":\"${addr}\"}}"
;;
*) exit 1;;
esac
done

View File

@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}

View File

@@ -0,0 +1,7 @@
int foo() {
return 1;
}
int bar() {
return 2;
}

Binary file not shown.

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.lib_mac_64</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
ELF<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

View File

@@ -0,0 +1,360 @@
// 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 driver
import (
"errors"
"fmt"
"os"
"m7s.live/v5/plugin/debug/pkg/internal/binutils"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
)
type source struct {
Sources []string
ExecName string
BuildID string
Base []string
DiffBase bool
Normalize bool
Seconds int
Timeout int
Symbolize string
HTTPHostport string
HTTPDisableBrowser bool
Comment string
}
// parseFlags parses the command lines through the specified flags package
// and returns the source of the profile and optionally the command
// for the kind of report to generate (nil for interactive use).
func parseFlags(o *plugin.Options) (*source, []string, error) {
flag := o.Flagset
// Comparisons.
flagDiffBase := flag.StringList("diff_base", "", "Source of base profile for comparison")
flagBase := flag.StringList("base", "", "Source of base profile for profile subtraction")
// Source options.
flagSymbolize := flag.String("symbolize", "", "Options for profile symbolization")
flagBuildID := flag.String("buildid", "", "Override build id for first mapping")
flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile")
flagAddComment := flag.String("add_comment", "", "Annotation string to record in the profile")
// CPU profile options
flagSeconds := flag.Int("seconds", -1, "Length of time for dynamic profiles")
// Heap profile options
flagInUseSpace := flag.Bool("inuse_space", false, "Display in-use memory size")
flagInUseObjects := flag.Bool("inuse_objects", false, "Display in-use object counts")
flagAllocSpace := flag.Bool("alloc_space", false, "Display allocated memory size")
flagAllocObjects := flag.Bool("alloc_objects", false, "Display allocated object counts")
// Contention profile options
flagTotalDelay := flag.Bool("total_delay", false, "Display total delay at each region")
flagContentions := flag.Bool("contentions", false, "Display number of delays at each region")
flagMeanDelay := flag.Bool("mean_delay", false, "Display mean delay at each region")
flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames")
flagHTTP := flag.String("http", "", "Present interactive web UI at the specified http host:port")
flagNoBrowser := flag.Bool("no_browser", false, "Skip opening a browser for the interactive web UI")
// Flags that set configuration properties.
cfg := currentConfig()
configFlagSetter := installConfigFlags(flag, &cfg)
flagCommands := make(map[string]*bool)
flagParamCommands := make(map[string]*string)
for name, cmd := range pprofCommands {
if cmd.hasParam {
flagParamCommands[name] = flag.String(name, "", "Generate a report in "+name+" format, matching regexp")
} else {
flagCommands[name] = flag.Bool(name, false, "Generate a report in "+name+" format")
}
}
args := flag.Parse(func() {
o.UI.Print(usageMsgHdr +
usage(true) +
usageMsgSrc +
flag.ExtraUsage() +
usageMsgVars)
})
if len(args) == 0 {
return nil, nil, errors.New("no profile source specified")
}
var execName string
// Recognize first argument as an executable or buildid override.
if len(args) > 1 {
arg0 := args[0]
if file, err := o.Obj.Open(arg0, 0, ^uint64(0), 0, ""); err == nil {
file.Close()
execName = arg0
args = args[1:]
}
}
// Apply any specified flags to cfg.
if err := configFlagSetter(); err != nil {
return nil, nil, err
}
cmd, err := outputFormat(flagCommands, flagParamCommands)
if err != nil {
return nil, nil, err
}
if cmd != nil && *flagHTTP != "" {
return nil, nil, errors.New("-http is not compatible with an output format on the command line")
}
if *flagNoBrowser && *flagHTTP == "" {
return nil, nil, errors.New("-no_browser only makes sense with -http")
}
si := cfg.SampleIndex
si = sampleIndex(flagTotalDelay, si, "delay", "-total_delay", o.UI)
si = sampleIndex(flagMeanDelay, si, "delay", "-mean_delay", o.UI)
si = sampleIndex(flagContentions, si, "contentions", "-contentions", o.UI)
si = sampleIndex(flagInUseSpace, si, "inuse_space", "-inuse_space", o.UI)
si = sampleIndex(flagInUseObjects, si, "inuse_objects", "-inuse_objects", o.UI)
si = sampleIndex(flagAllocSpace, si, "alloc_space", "-alloc_space", o.UI)
si = sampleIndex(flagAllocObjects, si, "alloc_objects", "-alloc_objects", o.UI)
cfg.SampleIndex = si
if *flagMeanDelay {
cfg.Mean = true
}
source := &source{
Sources: args,
ExecName: execName,
BuildID: *flagBuildID,
Seconds: *flagSeconds,
Timeout: *flagTimeout,
Symbolize: *flagSymbolize,
HTTPHostport: *flagHTTP,
HTTPDisableBrowser: *flagNoBrowser,
Comment: *flagAddComment,
}
if err := source.addBaseProfiles(*flagBase, *flagDiffBase); err != nil {
return nil, nil, err
}
normalize := cfg.Normalize
if normalize && len(source.Base) == 0 {
return nil, nil, errors.New("must have base profile to normalize by")
}
source.Normalize = normalize
if bu, ok := o.Obj.(*binutils.Binutils); ok {
bu.SetTools(*flagTools)
}
setCurrentConfig(cfg)
return source, cmd, nil
}
// addBaseProfiles adds the list of base profiles or diff base profiles to
// the source. This function will return an error if both base and diff base
// profiles are specified.
func (source *source) addBaseProfiles(flagBase, flagDiffBase []*string) error {
base, diffBase := dropEmpty(flagBase), dropEmpty(flagDiffBase)
if len(base) > 0 && len(diffBase) > 0 {
return errors.New("-base and -diff_base flags cannot both be specified")
}
source.Base = base
if len(diffBase) > 0 {
source.Base, source.DiffBase = diffBase, true
}
return nil
}
// dropEmpty list takes a slice of string pointers, and outputs a slice of
// non-empty strings associated with the flag.
func dropEmpty(list []*string) []string {
var l []string
for _, s := range list {
if *s != "" {
l = append(l, *s)
}
}
return l
}
// installConfigFlags creates command line flags for configuration
// fields and returns a function which can be called after flags have
// been parsed to copy any flags specified on the command line to
// *cfg.
func installConfigFlags(flag plugin.FlagSet, cfg *config) func() error {
// List of functions for setting the different parts of a config.
var setters []func()
var err error // Holds any errors encountered while running setters.
for _, field := range configFields {
n := field.name
help := configHelp[n]
var setter func()
switch ptr := cfg.fieldPtr(field).(type) {
case *bool:
f := flag.Bool(n, *ptr, help)
setter = func() { *ptr = *f }
case *int:
f := flag.Int(n, *ptr, help)
setter = func() { *ptr = *f }
case *float64:
f := flag.Float64(n, *ptr, help)
setter = func() { *ptr = *f }
case *string:
if len(field.choices) == 0 {
f := flag.String(n, *ptr, help)
setter = func() { *ptr = *f }
} else {
// Make a separate flag per possible choice.
// Set all flags to initially false so we can
// identify conflicts.
bools := make(map[string]*bool)
for _, choice := range field.choices {
bools[choice] = flag.Bool(choice, false, configHelp[choice])
}
setter = func() {
var set []string
for k, v := range bools {
if *v {
set = append(set, k)
}
}
switch len(set) {
case 0:
// Leave as default value.
case 1:
*ptr = set[0]
default:
err = fmt.Errorf("conflicting options set: %v", set)
}
}
}
}
setters = append(setters, setter)
}
return func() error {
// Apply the setter for every flag.
for _, setter := range setters {
setter()
if err != nil {
return err
}
}
return nil
}
}
func sampleIndex(flag *bool, si string, sampleType, option string, ui plugin.UI) string {
if *flag {
if si == "" {
return sampleType
}
ui.PrintErr("Multiple value selections, ignoring ", option)
}
return si
}
func outputFormat(bcmd map[string]*bool, acmd map[string]*string) (cmd []string, err error) {
for n, b := range bcmd {
if *b {
if cmd != nil {
return nil, errors.New("must set at most one output format")
}
cmd = []string{n}
}
}
for n, s := range acmd {
if *s != "" {
if cmd != nil {
return nil, errors.New("must set at most one output format")
}
cmd = []string{n, *s}
}
}
return cmd, nil
}
var usageMsgHdr = `usage:
Produce output in the specified format.
pprof <format> [options] [binary] <source> ...
Omit the format to get an interactive shell whose commands can be used
to generate various views of a profile
pprof [options] [binary] <source> ...
Omit the format and provide the "-http" flag to get an interactive web
interface at the specified host:port that can be used to navigate through
various views of a profile.
pprof -http [host]:[port] [options] [binary] <source> ...
Details:
`
var usageMsgSrc = "\n\n" +
" Source options:\n" +
" -seconds Duration for time-based profile collection\n" +
" -timeout Timeout in seconds for profile collection\n" +
" -buildid Override build id for main binary\n" +
" -add_comment Free-form annotation to add to the profile\n" +
" Displayed on some reports or with pprof -comments\n" +
" -diff_base source Source of base profile for comparison\n" +
" -base source Source of base profile for profile subtraction\n" +
" profile.pb.gz Profile in compressed protobuf format\n" +
" legacy_profile Profile in legacy pprof format\n" +
" http://host/profile URL for profile handler to retrieve\n" +
" -symbolize= Controls source of symbol information\n" +
" none Do not attempt symbolization\n" +
" local Examine only local binaries\n" +
" fastlocal Only get function names from local binaries\n" +
" remote Do not examine local binaries\n" +
" force Force re-symbolization\n" +
" Binary Local path or build id of binary for symbolization\n"
var usageMsgVars = "\n\n" +
" Misc options:\n" +
" -http Provide web interface at host:port.\n" +
" Host is optional and 'localhost' by default.\n" +
" Port is optional and a randomly available port by default.\n" +
" -no_browser Skip opening a browser for the interactive web UI.\n" +
" -tools Search path for object tools\n" +
"\n" +
" Legacy convenience options:\n" +
" -inuse_space Same as -sample_index=inuse_space\n" +
" -inuse_objects Same as -sample_index=inuse_objects\n" +
" -alloc_space Same as -sample_index=alloc_space\n" +
" -alloc_objects Same as -sample_index=alloc_objects\n" +
" -total_delay Same as -sample_index=delay\n" +
" -contentions Same as -sample_index=contentions\n" +
" -mean_delay Same as -mean -sample_index=delay\n" +
"\n" +
" Environment Variables:\n" +
" PPROF_TMPDIR Location for saved profiles (default $HOME/pprof)\n" +
" PPROF_TOOLS Search path for object-level tools\n" +
" PPROF_BINARY_PATH Search path for local binary files\n" +
" default: $HOME/pprof/binaries\n" +
" searches $buildid/$name, $buildid/*, $path/$buildid,\n" +
" ${buildid:0:2}/${buildid:2}.debug, $name, $path,\n" +
" ${name}.debug, $dir/.debug/${name}.debug,\n" +
" usr/lib/debug/$dir/${name}.debug\n" +
" * On Windows, %USERPROFILE% is used instead of $HOME"

View File

@@ -0,0 +1,461 @@
// 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 driver
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"runtime"
"sort"
"strings"
"time"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/internal/report"
)
// commands describes the commands accepted by pprof.
type commands map[string]*command
// command describes the actions for a pprof command. Includes a
// function for command-line completion, the report format to use
// during report generation, any postprocessing functions, and whether
// the command expects a regexp parameter (typically a function name).
type command struct {
format int // report format to generate
postProcess PostProcessor // postprocessing to run on report
visualizer PostProcessor // display output using some callback
hasParam bool // collect a parameter from the CLI
description string // single-line description text saying what the command does
usage string // multi-line help text saying how the command is used
}
// help returns a help string for a command.
func (c *command) help(name string) string {
message := c.description + "\n"
if c.usage != "" {
message += " Usage:\n"
lines := strings.Split(c.usage, "\n")
for _, line := range lines {
message += fmt.Sprintf(" %s\n", line)
}
}
return message + "\n"
}
// AddCommand adds an additional command to the set of commands
// accepted by pprof. This enables extensions to add new commands for
// specialized visualization formats. If the command specified already
// exists, it is overwritten.
func AddCommand(cmd string, format int, post PostProcessor, desc, usage string) {
pprofCommands[cmd] = &command{format, post, nil, false, desc, usage}
}
// SetVariableDefault sets the default value for a pprof
// variable. This enables extensions to set their own defaults.
func SetVariableDefault(variable, value string) {
configure(variable, value)
}
// PostProcessor is a function that applies post-processing to the report output
type PostProcessor func(input io.Reader, output io.Writer, ui plugin.UI) error
// interactiveMode is true if pprof is running on interactive mode, reading
// commands from its shell.
var interactiveMode = false
// pprofCommands are the report generation commands recognized by pprof.
var pprofCommands = commands{
// Commands that require no post-processing.
"comments": {report.Comments, nil, nil, false, "Output all profile comments", ""},
"disasm": {report.Dis, nil, nil, true, "Output assembly listings annotated with samples", listHelp("disasm", true)},
"dot": {report.Dot, nil, nil, false, "Outputs a graph in DOT format", reportHelp("dot", false, true)},
"list": {report.List, nil, nil, true, "Output annotated source for functions matching regexp", listHelp("list", false)},
"peek": {report.Tree, nil, nil, true, "Output callers/callees of functions matching regexp", "peek func_regex\nDisplay callers and callees of functions matching func_regex."},
"raw": {report.Raw, nil, nil, false, "Outputs a text representation of the raw profile", ""},
"tags": {report.Tags, nil, nil, false, "Outputs all tags in the profile", "tags [tag_regex]* [-ignore_regex]* [>file]\nList tags with key:value matching tag_regex and exclude ignore_regex."},
"text": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("text", true, true)},
"top": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("top", true, true)},
"traces": {report.Traces, nil, nil, false, "Outputs all profile samples in text form", ""},
"tree": {report.Tree, nil, nil, false, "Outputs a text rendering of call graph", reportHelp("tree", true, true)},
// Save binary formats to a file
"callgrind": {report.Callgrind, nil, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format", reportHelp("callgrind", false, true)},
"proto": {report.Proto, nil, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format", ""},
"topproto": {report.TopProto, nil, awayFromTTY("pb.gz"), false, "Outputs top entries in compressed protobuf format", ""},
// Generate report in DOT format and postprocess with dot
"gif": {report.Dot, invokeDot("gif"), awayFromTTY("gif"), false, "Outputs a graph image in GIF format", reportHelp("gif", false, true)},
"pdf": {report.Dot, invokeDot("pdf"), awayFromTTY("pdf"), false, "Outputs a graph in PDF format", reportHelp("pdf", false, true)},
"png": {report.Dot, invokeDot("png"), awayFromTTY("png"), false, "Outputs a graph image in PNG format", reportHelp("png", false, true)},
"ps": {report.Dot, invokeDot("ps"), awayFromTTY("ps"), false, "Outputs a graph in PS format", reportHelp("ps", false, true)},
// Save SVG output into a file
"svg": {report.Dot, massageDotSVG(), awayFromTTY("svg"), false, "Outputs a graph in SVG format", reportHelp("svg", false, true)},
// Visualize postprocessed dot output
"eog": {report.Dot, invokeDot("svg"), invokeVisualizer("svg", []string{"eog"}), false, "Visualize graph through eog", reportHelp("eog", false, false)},
"evince": {report.Dot, invokeDot("pdf"), invokeVisualizer("pdf", []string{"evince"}), false, "Visualize graph through evince", reportHelp("evince", false, false)},
"gv": {report.Dot, invokeDot("ps"), invokeVisualizer("ps", []string{"gv --noantialias"}), false, "Visualize graph through gv", reportHelp("gv", false, false)},
"web": {report.Dot, massageDotSVG(), invokeVisualizer("svg", browsers()), false, "Visualize graph through web browser", reportHelp("web", false, false)},
// Visualize callgrind output
"kcachegrind": {report.Callgrind, nil, invokeVisualizer("grind", kcachegrind), false, "Visualize report in KCachegrind", reportHelp("kcachegrind", false, false)},
// Visualize HTML directly generated by report.
"weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)},
}
// configHelp contains help text per configuration parameter.
var configHelp = map[string]string{
// Filename for file-based output formats, stdout by default.
"output": helpText("Output filename for file-based outputs"),
// Comparisons.
"drop_negative": helpText(
"Ignore negative differences",
"Do not show any locations with values <0."),
// Graph handling options.
"call_tree": helpText(
"Create a context-sensitive call tree",
"Treat locations reached through different paths as separate."),
// Display options.
"relative_percentages": helpText(
"Show percentages relative to focused subgraph",
"If unset, percentages are relative to full graph before focusing",
"to facilitate comparison with original graph."),
"unit": helpText(
"Measurement units to display",
"Scale the sample values to this unit.",
"For time-based profiles, use seconds, milliseconds, nanoseconds, etc.",
"For memory profiles, use megabytes, kilobytes, bytes, etc.",
"Using auto will scale each value independently to the most natural unit."),
"compact_labels": "Show minimal headers",
"source_path": "Search path for source files",
"trim_path": "Path to trim from source paths before search",
"intel_syntax": helpText(
"Show assembly in Intel syntax",
"Only applicable to commands `disasm` and `weblist`"),
// Filtering options
"nodecount": helpText(
"Max number of nodes to show",
"Uses heuristics to limit the number of locations to be displayed.",
"On graphs, dotted edges represent paths through nodes that have been removed."),
"nodefraction": "Hide nodes below <f>*total",
"edgefraction": "Hide edges below <f>*total",
"trim": helpText(
"Honor nodefraction/edgefraction/nodecount defaults",
"Set to false to get the full profile, without any trimming."),
"focus": helpText(
"Restricts to samples going through a node matching regexp",
"Discard samples that do not include a node matching this regexp.",
"Matching includes the function name, filename or object name."),
"ignore": helpText(
"Skips paths going through any nodes matching regexp",
"If set, discard samples that include a node matching this regexp.",
"Matching includes the function name, filename or object name."),
"prune_from": helpText(
"Drops any functions below the matched frame.",
"If set, any frames matching the specified regexp and any frames",
"below it will be dropped from each sample."),
"hide": helpText(
"Skips nodes matching regexp",
"Discard nodes that match this location.",
"Other nodes from samples that include this location will be shown.",
"Matching includes the function name, filename or object name."),
"show": helpText(
"Only show nodes matching regexp",
"If set, only show nodes that match this location.",
"Matching includes the function name, filename or object name."),
"show_from": helpText(
"Drops functions above the highest matched frame.",
"If set, all frames above the highest match are dropped from every sample.",
"Matching includes the function name, filename or object name."),
"tagroot": helpText(
"Adds pseudo stack frames for labels key/value pairs at the callstack root.",
"A comma-separated list of label keys.",
"The first key creates frames at the new root."),
"tagleaf": helpText(
"Adds pseudo stack frames for labels key/value pairs at the callstack leaf.",
"A comma-separated list of label keys.",
"The last key creates frames at the new leaf."),
"tagfocus": helpText(
"Restricts to samples with tags in range or matched by regexp",
"Use name=value syntax to limit the matching to a specific tag.",
"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
"String tag filter examples: foo, foo.*bar, mytag=foo.*bar"),
"tagignore": helpText(
"Discard samples with tags in range or matched by regexp",
"Use name=value syntax to limit the matching to a specific tag.",
"Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:",
"String tag filter examples: foo, foo.*bar, mytag=foo.*bar"),
"tagshow": helpText(
"Only consider tags matching this regexp",
"Discard tags that do not match this regexp"),
"taghide": helpText(
"Skip tags matching this regexp",
"Discard tags that match this regexp"),
// Heap profile options
"divide_by": helpText(
"Ratio to divide all samples before visualization",
"Divide all samples values by a constant, eg the number of processors or jobs."),
"mean": helpText(
"Average sample value over first value (count)",
"For memory profiles, report average memory per allocation.",
"For time-based profiles, report average time per event."),
"sample_index": helpText(
"Sample value to report (0-based index or name)",
"Profiles contain multiple values per sample.",
"Use sample_index=i to select the ith value (starting at 0)."),
"normalize": helpText(
"Scales profile based on the base profile."),
// Data sorting criteria
"flat": helpText("Sort entries based on own weight"),
"cum": helpText("Sort entries based on cumulative weight"),
// Output granularity
"functions": helpText(
"Aggregate at the function level.",
"Ignores the filename where the function was defined."),
"filefunctions": helpText(
"Aggregate at the function level.",
"Takes into account the filename where the function was defined."),
"files": "Aggregate at the file level.",
"lines": "Aggregate at the source code line level.",
"addresses": helpText(
"Aggregate at the address level.",
"Includes functions' addresses in the output."),
"noinlines": helpText(
"Ignore inlines.",
"Attributes inlined functions to their first out-of-line caller."),
"showcolumns": helpText(
"Show column numbers at the source code line level."),
}
func helpText(s ...string) string {
return strings.Join(s, "\n") + "\n"
}
// usage returns a string describing the pprof commands and configuration
// options. if commandLine is set, the output reflect cli usage.
func usage(commandLine bool) string {
var prefix string
if commandLine {
prefix = "-"
}
fmtHelp := func(c, d string) string {
return fmt.Sprintf(" %-16s %s", c, strings.SplitN(d, "\n", 2)[0])
}
var commands []string
for name, cmd := range pprofCommands {
commands = append(commands, fmtHelp(prefix+name, cmd.description))
}
sort.Strings(commands)
var help string
if commandLine {
help = " Output formats (select at most one):\n"
} else {
help = " Commands:\n"
commands = append(commands, fmtHelp("o/options", "List options and their current values"))
commands = append(commands, fmtHelp("q/quit/exit/^D", "Exit pprof"))
}
help = help + strings.Join(commands, "\n") + "\n\n" +
" Options:\n"
// Print help for configuration options after sorting them.
// Collect choices for multi-choice options print them together.
var variables []string
var radioStrings []string
for _, f := range configFields {
if len(f.choices) == 0 {
variables = append(variables, fmtHelp(prefix+f.name, configHelp[f.name]))
continue
}
// Format help for for this group.
s := []string{fmtHelp(f.name, "")}
for _, choice := range f.choices {
s = append(s, " "+fmtHelp(prefix+choice, configHelp[choice]))
}
radioStrings = append(radioStrings, strings.Join(s, "\n"))
}
sort.Strings(variables)
sort.Strings(radioStrings)
return help + strings.Join(variables, "\n") + "\n\n" +
" Option groups (only set one per group):\n" +
strings.Join(radioStrings, "\n")
}
func reportHelp(c string, cum, redirect bool) string {
h := []string{
c + " [n] [focus_regex]* [-ignore_regex]*",
"Include up to n samples",
"Include samples matching focus_regex, and exclude ignore_regex.",
}
if cum {
h[0] += " [-cum]"
h = append(h, "-cum sorts the output by cumulative weight")
}
if redirect {
h[0] += " >f"
h = append(h, "Optionally save the report on the file f")
}
return strings.Join(h, "\n")
}
func listHelp(c string, redirect bool) string {
h := []string{
c + "<func_regex|address> [-focus_regex]* [-ignore_regex]*",
"Include functions matching func_regex, or including the address specified.",
"Include samples matching focus_regex, and exclude ignore_regex.",
}
if redirect {
h[0] += " >f"
h = append(h, "Optionally save the report on the file f")
}
return strings.Join(h, "\n")
}
// browsers returns a list of commands to attempt for web visualization.
func browsers() []string {
var cmds []string
if userBrowser := os.Getenv("BROWSER"); userBrowser != "" {
cmds = append(cmds, userBrowser)
}
switch runtime.GOOS {
case "darwin":
cmds = append(cmds, "/usr/bin/open")
case "windows":
cmds = append(cmds, "cmd /c start")
default:
// Commands opening browsers are prioritized over xdg-open, so browser()
// command can be used on linux to open the .svg file generated by the -web
// command (the .svg file includes embedded javascript so is best viewed in
// a browser).
cmds = append(cmds, []string{"chrome", "google-chrome", "chromium", "firefox", "sensible-browser"}...)
if os.Getenv("DISPLAY") != "" {
// xdg-open is only for use in a desktop environment.
cmds = append(cmds, "xdg-open")
}
}
return cmds
}
var kcachegrind = []string{"kcachegrind"}
// awayFromTTY saves the output in a file if it would otherwise go to
// the terminal screen. This is used to avoid dumping binary data on
// the screen.
func awayFromTTY(format string) PostProcessor {
return func(input io.Reader, output io.Writer, ui plugin.UI) error {
if output == os.Stdout && (ui.IsTerminal() || interactiveMode) {
tempFile, err := newTempFile("", "profile", "."+format)
if err != nil {
return err
}
ui.PrintErr("Generating report in ", tempFile.Name())
output = tempFile
}
_, err := io.Copy(output, input)
return err
}
}
func invokeDot(format string) PostProcessor {
return func(input io.Reader, output io.Writer, ui plugin.UI) error {
cmd := exec.Command("dot", "-T"+format)
cmd.Stdin, cmd.Stdout, cmd.Stderr = input, output, os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to execute dot. Is Graphviz installed? Error: %v", err)
}
return nil
}
}
// massageDotSVG invokes the dot tool to generate an SVG image and alters
// the image to have panning capabilities when viewed in a browser.
func massageDotSVG() PostProcessor {
generateSVG := invokeDot("svg")
return func(input io.Reader, output io.Writer, ui plugin.UI) error {
baseSVG := new(bytes.Buffer)
if err := generateSVG(input, baseSVG, ui); err != nil {
return err
}
_, err := output.Write([]byte(massageSVG(baseSVG.String())))
return err
}
}
func invokeVisualizer(suffix string, visualizers []string) PostProcessor {
return func(input io.Reader, output io.Writer, ui plugin.UI) error {
tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix)
if err != nil {
return err
}
deferDeleteTempFile(tempFile.Name())
if _, err := io.Copy(tempFile, input); err != nil {
return err
}
tempFile.Close()
// Try visualizers until one is successful
for _, v := range visualizers {
// Separate command and arguments for exec.Command.
args := strings.Split(v, " ")
if len(args) == 0 {
continue
}
viewer := exec.Command(args[0], append(args[1:], tempFile.Name())...)
viewer.Stderr = os.Stderr
if err = viewer.Start(); err == nil {
// Wait for a second so that the visualizer has a chance to
// open the input file. This needs to be done even if we're
// waiting for the visualizer as it can be just a wrapper that
// spawns a browser tab and returns right away.
defer func(t <-chan time.Time) {
<-t
}(time.After(time.Second))
// On interactive mode, let the visualizer run in the background
// so other commands can be issued.
if !interactiveMode {
return viewer.Wait()
}
return nil
}
}
return err
}
}
// stringToBool is a custom parser for bools. We avoid using strconv.ParseBool
// to remain compatible with old pprof behavior (e.g., treating "" as true).
func stringToBool(s string) (bool, error) {
switch strings.ToLower(s) {
case "true", "t", "yes", "y", "1", "":
return true, nil
case "false", "f", "no", "n", "0":
return false, nil
default:
return false, fmt.Errorf(`illegal value "%s" for bool variable`, s)
}
}

View File

@@ -0,0 +1,373 @@
package driver
import (
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
)
// config holds settings for a single named config.
// The JSON tag name for a field is used both for JSON encoding and as
// a named variable.
type config struct {
// Filename for file-based output formats, stdout by default.
Output string `json:"-"`
// Display options.
CallTree bool `json:"call_tree,omitempty"`
RelativePercentages bool `json:"relative_percentages,omitempty"`
Unit string `json:"unit,omitempty"`
CompactLabels bool `json:"compact_labels,omitempty"`
SourcePath string `json:"-"`
TrimPath string `json:"-"`
IntelSyntax bool `json:"intel_syntax,omitempty"`
Mean bool `json:"mean,omitempty"`
SampleIndex string `json:"-"`
DivideBy float64 `json:"-"`
Normalize bool `json:"normalize,omitempty"`
Sort string `json:"sort,omitempty"`
// Label pseudo stack frame generation options
TagRoot string `json:"tagroot,omitempty"`
TagLeaf string `json:"tagleaf,omitempty"`
// Filtering options
DropNegative bool `json:"drop_negative,omitempty"`
NodeCount int `json:"nodecount,omitempty"`
NodeFraction float64 `json:"nodefraction,omitempty"`
EdgeFraction float64 `json:"edgefraction,omitempty"`
Trim bool `json:"trim,omitempty"`
Focus string `json:"focus,omitempty"`
Ignore string `json:"ignore,omitempty"`
PruneFrom string `json:"prune_from,omitempty"`
Hide string `json:"hide,omitempty"`
Show string `json:"show,omitempty"`
ShowFrom string `json:"show_from,omitempty"`
TagFocus string `json:"tagfocus,omitempty"`
TagIgnore string `json:"tagignore,omitempty"`
TagShow string `json:"tagshow,omitempty"`
TagHide string `json:"taghide,omitempty"`
NoInlines bool `json:"noinlines,omitempty"`
ShowColumns bool `json:"showcolumns,omitempty"`
// Output granularity
Granularity string `json:"granularity,omitempty"`
}
// defaultConfig returns the default configuration values; it is unaffected by
// flags and interactive assignments.
func defaultConfig() config {
return config{
Unit: "minimum",
NodeCount: -1,
NodeFraction: 0.005,
EdgeFraction: 0.001,
Trim: true,
DivideBy: 1.0,
Sort: "flat",
Granularity: "", // Default depends on the display format
}
}
// currentConfig holds the current configuration values; it is affected by
// flags and interactive assignments.
var currentCfg = defaultConfig()
var currentMu sync.Mutex
func currentConfig() config {
currentMu.Lock()
defer currentMu.Unlock()
return currentCfg
}
func setCurrentConfig(cfg config) {
currentMu.Lock()
defer currentMu.Unlock()
currentCfg = cfg
}
// configField contains metadata for a single configuration field.
type configField struct {
name string // JSON field name/key in variables
urlparam string // URL parameter name
saved bool // Is field saved in settings?
field reflect.StructField // Field in config
choices []string // Name Of variables in group
defaultValue string // Default value for this field.
}
var (
configFields []configField // Precomputed metadata per config field
// configFieldMap holds an entry for every config field as well as an
// entry for every valid choice for a multi-choice field.
configFieldMap map[string]configField
)
func init() {
// Config names for fields that are not saved in settings and therefore
// do not have a JSON name.
notSaved := map[string]string{
// Not saved in settings, but present in URLs.
"SampleIndex": "sample_index",
// Following fields are also not placed in URLs.
"Output": "output",
"SourcePath": "source_path",
"TrimPath": "trim_path",
"DivideBy": "divide_by",
}
// choices holds the list of allowed values for config fields that can
// take on one of a bounded set of values.
choices := map[string][]string{
"sort": {"cum", "flat"},
"granularity": {"functions", "filefunctions", "files", "lines", "addresses"},
}
// urlparam holds the mapping from a config field name to the URL
// parameter used to hold that config field. If no entry is present for
// a name, the corresponding field is not saved in URLs.
urlparam := map[string]string{
"drop_negative": "dropneg",
"call_tree": "calltree",
"relative_percentages": "rel",
"unit": "unit",
"compact_labels": "compact",
"intel_syntax": "intel",
"nodecount": "n",
"nodefraction": "nf",
"edgefraction": "ef",
"trim": "trim",
"focus": "f",
"ignore": "i",
"prune_from": "prunefrom",
"hide": "h",
"show": "s",
"show_from": "sf",
"tagfocus": "tf",
"tagignore": "ti",
"tagshow": "ts",
"taghide": "th",
"mean": "mean",
"sample_index": "si",
"normalize": "norm",
"sort": "sort",
"granularity": "g",
"noinlines": "noinlines",
"showcolumns": "showcolumns",
}
def := defaultConfig()
configFieldMap = map[string]configField{}
t := reflect.TypeOf(config{})
for i, n := 0, t.NumField(); i < n; i++ {
field := t.Field(i)
js := strings.Split(field.Tag.Get("json"), ",")
if len(js) == 0 {
continue
}
// Get the configuration name for this field.
name := js[0]
if name == "-" {
name = notSaved[field.Name]
if name == "" {
// Not a configurable field.
continue
}
}
f := configField{
name: name,
urlparam: urlparam[name],
saved: (name == js[0]),
field: field,
choices: choices[name],
}
f.defaultValue = def.get(f)
configFields = append(configFields, f)
configFieldMap[f.name] = f
for _, choice := range f.choices {
configFieldMap[choice] = f
}
}
}
// fieldPtr returns a pointer to the field identified by f in *cfg.
func (cfg *config) fieldPtr(f configField) interface{} {
// reflect.ValueOf: converts to reflect.Value
// Elem: dereferences cfg to make *cfg
// FieldByIndex: fetches the field
// Addr: takes address of field
// Interface: converts back from reflect.Value to a regular value
return reflect.ValueOf(cfg).Elem().FieldByIndex(f.field.Index).Addr().Interface()
}
// get returns the value of field f in cfg.
func (cfg *config) get(f configField) string {
switch ptr := cfg.fieldPtr(f).(type) {
case *string:
return *ptr
case *int:
return fmt.Sprint(*ptr)
case *float64:
return fmt.Sprint(*ptr)
case *bool:
return fmt.Sprint(*ptr)
}
panic(fmt.Sprintf("unsupported config field type %v", f.field.Type))
}
// set sets the value of field f in cfg to value.
func (cfg *config) set(f configField, value string) error {
switch ptr := cfg.fieldPtr(f).(type) {
case *string:
if len(f.choices) > 0 {
// Verify that value is one of the allowed choices.
for _, choice := range f.choices {
if choice == value {
*ptr = value
return nil
}
}
return fmt.Errorf("invalid %q value %q", f.name, value)
}
*ptr = value
case *int:
v, err := strconv.Atoi(value)
if err != nil {
return err
}
*ptr = v
case *float64:
v, err := strconv.ParseFloat(value, 64)
if err != nil {
return err
}
*ptr = v
case *bool:
v, err := stringToBool(value)
if err != nil {
return err
}
*ptr = v
default:
panic(fmt.Sprintf("unsupported config field type %v", f.field.Type))
}
return nil
}
// isConfigurable returns true if name is either the name of a config field, or
// a valid value for a multi-choice config field.
func isConfigurable(name string) bool {
_, ok := configFieldMap[name]
return ok
}
// isBoolConfig returns true if name is either name of a boolean config field,
// or a valid value for a multi-choice config field.
func isBoolConfig(name string) bool {
f, ok := configFieldMap[name]
if !ok {
return false
}
if name != f.name {
return true // name must be one possible value for the field
}
var cfg config
_, ok = cfg.fieldPtr(f).(*bool)
return ok
}
// completeConfig returns the list of configurable names starting with prefix.
func completeConfig(prefix string) []string {
var result []string
for v := range configFieldMap {
if strings.HasPrefix(v, prefix) {
result = append(result, v)
}
}
return result
}
// configure stores the name=value mapping into the current config, correctly
// handling the case when name identifies a particular choice in a field.
func configure(name, value string) error {
currentMu.Lock()
defer currentMu.Unlock()
f, ok := configFieldMap[name]
if !ok {
return fmt.Errorf("unknown config field %q", name)
}
if f.name == name {
return currentCfg.set(f, value)
}
// name must be one of the choices. If value is true, set field-value
// to name.
if v, err := strconv.ParseBool(value); v && err == nil {
return currentCfg.set(f, name)
}
return fmt.Errorf("unknown config field %q", name)
}
// resetTransient sets all transient fields in *cfg to their currently
// configured values.
func (cfg *config) resetTransient() {
current := currentConfig()
cfg.Output = current.Output
cfg.SourcePath = current.SourcePath
cfg.TrimPath = current.TrimPath
cfg.DivideBy = current.DivideBy
cfg.SampleIndex = current.SampleIndex
}
// applyURL updates *cfg based on params.
func (cfg *config) applyURL(params url.Values) error {
for _, f := range configFields {
var value string
if f.urlparam != "" {
value = params.Get(f.urlparam)
}
if value == "" {
continue
}
if err := cfg.set(f, value); err != nil {
return fmt.Errorf("error setting config field %s: %v", f.name, err)
}
}
return nil
}
// makeURL returns a URL based on initialURL that contains the config contents
// as parameters. The second result is true iff a parameter value was changed.
func (cfg *config) makeURL(initialURL url.URL) (url.URL, bool) {
q := initialURL.Query()
changed := false
for _, f := range configFields {
if f.urlparam == "" || !f.saved {
continue
}
v := cfg.get(f)
if v == f.defaultValue {
v = "" // URL for of default value is the empty string.
} else if f.field.Type.Kind() == reflect.Bool {
// Shorten bool values to "f" or "t"
v = v[:1]
}
if q.Get(f.urlparam) == v {
continue
}
changed = true
if v == "" {
q.Del(f.urlparam)
} else {
q.Set(f.urlparam, v)
}
}
if changed {
initialURL.RawQuery = q.Encode()
}
return initialURL, changed
}

View File

@@ -0,0 +1,408 @@
// 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 driver implements the core pprof functionality. It can be
// parameterized with a flag implementation, fetch and symbolize
// mechanisms.
package driver
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/internal/report"
"m7s.live/v5/plugin/debug/pkg/profile"
)
// PProf acquires a profile, and symbolizes it using a profile
// manager. Then it generates a report formatted according to the
// options selected through the flags package.
func PProf(eo *plugin.Options) error {
// Remove any temporary files created during pprof processing.
defer cleanupTempFiles()
o := setDefaults(eo)
src, cmd, err := parseFlags(o)
if err != nil {
return err
}
p, err := fetchProfiles(src, o)
if err != nil {
return err
}
if cmd != nil {
return generateReport(p, cmd, currentConfig(), o)
}
if src.HTTPHostport != "" {
return serveWebInterface(src.HTTPHostport, p, o, src.HTTPDisableBrowser)
}
return interactive(p, o)
}
// generateRawReport is allowed to modify p.
func generateRawReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) (*command, *report.Report, error) {
// Identify units of numeric tags in profile.
numLabelUnits := identifyNumLabelUnits(p, o.UI)
// Get report output format
c := pprofCommands[cmd[0]]
if c == nil {
panic("unexpected nil command")
}
cfg = applyCommandOverrides(cmd[0], c.format, cfg)
// Create label pseudo nodes before filtering, in case the filters use
// the generated nodes.
generateTagRootsLeaves(p, cfg, o.UI)
// Delay focus after configuring report to get percentages on all samples.
relative := cfg.RelativePercentages
if relative {
if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil {
return nil, nil, err
}
}
ropt, err := reportOptions(p, numLabelUnits, cfg)
if err != nil {
return nil, nil, err
}
ropt.OutputFormat = c.format
if len(cmd) == 2 {
s, err := regexp.Compile(cmd[1])
if err != nil {
return nil, nil, fmt.Errorf("parsing argument regexp %s: %v", cmd[1], err)
}
ropt.Symbol = s
}
rpt := report.New(p, ropt)
if !relative {
if err := applyFocus(p, numLabelUnits, cfg, o.UI); err != nil {
return nil, nil, err
}
}
if err := aggregate(p, cfg); err != nil {
return nil, nil, err
}
return c, rpt, nil
}
// generateReport is allowed to modify p.
func generateReport(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
c, rpt, err := generateRawReport(p, cmd, cfg, o)
if err != nil {
return err
}
// Generate the report.
dst := new(bytes.Buffer)
switch rpt.OutputFormat() {
case report.WebList:
// We need template expansion, so generate here instead of in report.
err = printWebList(dst, rpt, o.Obj)
default:
err = report.Generate(dst, rpt, o.Obj)
}
if err != nil {
return err
}
src := dst
// If necessary, perform any data post-processing.
if c.postProcess != nil {
dst = new(bytes.Buffer)
if err := c.postProcess(src, dst, o.UI); err != nil {
return err
}
src = dst
}
// If no output is specified, use default visualizer.
output := cfg.Output
if output == "" {
if c.visualizer != nil {
return c.visualizer(src, os.Stdout, o.UI)
}
_, err := src.WriteTo(os.Stdout)
return err
}
// Output to specified file.
o.UI.PrintErr("Generating report in ", output)
out, err := o.Writer.Open(output)
if err != nil {
return err
}
if _, err := src.WriteTo(out); err != nil {
out.Close()
return err
}
return out.Close()
}
func printWebList(dst io.Writer, rpt *report.Report, obj plugin.ObjTool) error {
listing, err := report.MakeWebList(rpt, obj, -1)
if err != nil {
return err
}
legend := report.ProfileLabels(rpt)
return renderHTML(dst, "sourcelisting", rpt, nil, legend, webArgs{
Standalone: true,
Listing: listing,
})
}
func applyCommandOverrides(cmd string, outputFormat int, cfg config) config {
// Some report types override the trim flag to false below. This is to make
// sure the default heuristics of excluding insignificant nodes and edges
// from the call graph do not apply. One example where it is important is
// annotated source or disassembly listing. Those reports run on a specific
// function (or functions), but the trimming is applied before the function
// data is selected. So, with trimming enabled, the report could end up
// showing no data if the specified function is "uninteresting" as far as the
// trimming is concerned.
trim := cfg.Trim
switch cmd {
case "disasm":
trim = false
cfg.Granularity = "addresses"
// Force the 'noinlines' mode so that source locations for a given address
// collapse and there is only one for the given address. Without this
// cumulative metrics would be double-counted when annotating the assembly.
// This is because the merge is done by address and in case of an inlined
// stack each of the inlined entries is a separate callgraph node.
cfg.NoInlines = true
case "weblist":
trim = false
cfg.Granularity = "addresses"
cfg.NoInlines = false // Need inline info to support call expansion
case "peek":
trim = false
case "list":
trim = false
cfg.Granularity = "lines"
// Do not force 'noinlines' to be false so that specifying
// "-list foo -noinlines" is supported and works as expected.
case "text", "top", "topproto":
if cfg.NodeCount == -1 {
cfg.NodeCount = 0
}
default:
if cfg.NodeCount == -1 {
cfg.NodeCount = 80
}
}
switch outputFormat {
case report.Proto, report.Raw, report.Callgrind:
trim = false
cfg.Granularity = "addresses"
}
if !trim {
cfg.NodeCount = 0
cfg.NodeFraction = 0
cfg.EdgeFraction = 0
}
return cfg
}
// generateTagRootsLeaves generates extra nodes from the tagroot and tagleaf options.
func generateTagRootsLeaves(prof *profile.Profile, cfg config, ui plugin.UI) {
tagRootLabelKeys := dropEmptyStrings(strings.Split(cfg.TagRoot, ","))
tagLeafLabelKeys := dropEmptyStrings(strings.Split(cfg.TagLeaf, ","))
rootm, leafm := addLabelNodes(prof, tagRootLabelKeys, tagLeafLabelKeys, cfg.Unit)
warnNoMatches(cfg.TagRoot == "" || rootm, "TagRoot", ui)
warnNoMatches(cfg.TagLeaf == "" || leafm, "TagLeaf", ui)
}
// dropEmptyStrings filters a slice to only non-empty strings
func dropEmptyStrings(in []string) (out []string) {
for _, s := range in {
if s != "" {
out = append(out, s)
}
}
return
}
func aggregate(prof *profile.Profile, cfg config) error {
var function, filename, linenumber, address bool
inlines := !cfg.NoInlines
switch cfg.Granularity {
case "":
function = true // Default granularity is "functions"
case "addresses":
if inlines {
return nil
}
function = true
filename = true
linenumber = true
address = true
case "lines":
function = true
filename = true
linenumber = true
case "files":
filename = true
case "functions":
function = true
case "filefunctions":
function = true
filename = true
default:
return fmt.Errorf("unexpected granularity")
}
return prof.Aggregate(inlines, function, filename, linenumber, cfg.ShowColumns, address)
}
func reportOptions(p *profile.Profile, numLabelUnits map[string]string, cfg config) (*report.Options, error) {
si, mean := cfg.SampleIndex, cfg.Mean
value, meanDiv, sample, err := sampleFormat(p, si, mean)
if err != nil {
return nil, err
}
stype := sample.Type
if mean {
stype = "mean_" + stype
}
if cfg.DivideBy == 0 {
return nil, fmt.Errorf("zero divisor specified")
}
var filters []string
addFilter := func(k string, v string) {
if v != "" {
filters = append(filters, k+"="+v)
}
}
addFilter("focus", cfg.Focus)
addFilter("ignore", cfg.Ignore)
addFilter("hide", cfg.Hide)
addFilter("show", cfg.Show)
addFilter("show_from", cfg.ShowFrom)
addFilter("tagfocus", cfg.TagFocus)
addFilter("tagignore", cfg.TagIgnore)
addFilter("tagshow", cfg.TagShow)
addFilter("taghide", cfg.TagHide)
ropt := &report.Options{
CumSort: cfg.Sort == "cum",
CallTree: cfg.CallTree,
DropNegative: cfg.DropNegative,
CompactLabels: cfg.CompactLabels,
Ratio: 1 / cfg.DivideBy,
NodeCount: cfg.NodeCount,
NodeFraction: cfg.NodeFraction,
EdgeFraction: cfg.EdgeFraction,
ActiveFilters: filters,
NumLabelUnits: numLabelUnits,
SampleValue: value,
SampleMeanDivisor: meanDiv,
SampleType: stype,
SampleUnit: sample.Unit,
OutputUnit: cfg.Unit,
SourcePath: cfg.SourcePath,
TrimPath: cfg.TrimPath,
IntelSyntax: cfg.IntelSyntax,
}
if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
ropt.Title = filepath.Base(p.Mapping[0].File)
}
return ropt, nil
}
// identifyNumLabelUnits returns a map of numeric label keys to the units
// associated with those keys.
func identifyNumLabelUnits(p *profile.Profile, ui plugin.UI) map[string]string {
numLabelUnits, ignoredUnits := p.NumLabelUnits()
// Print errors for tags with multiple units associated with
// a single key.
for k, units := range ignoredUnits {
ui.PrintErr(fmt.Sprintf("For tag %s used unit %s, also encountered unit(s) %s", k, numLabelUnits[k], strings.Join(units, ", ")))
}
return numLabelUnits
}
type sampleValueFunc func([]int64) int64
// sampleFormat returns a function to extract values out of a profile.Sample,
// and the type/units of those values.
func sampleFormat(p *profile.Profile, sampleIndex string, mean bool) (value, meanDiv sampleValueFunc, v *profile.ValueType, err error) {
if len(p.SampleType) == 0 {
return nil, nil, nil, fmt.Errorf("profile has no samples")
}
index, err := p.SampleIndexByName(sampleIndex)
if err != nil {
return nil, nil, nil, err
}
value = valueExtractor(index)
if mean {
meanDiv = valueExtractor(0)
}
v = p.SampleType[index]
return
}
func valueExtractor(ix int) sampleValueFunc {
return func(v []int64) int64 {
return v[ix]
}
}
// profileCopier can be used to obtain a fresh copy of a profile.
// It is useful since reporting code may mutate the profile handed to it.
type profileCopier []byte
func makeProfileCopier(src *profile.Profile) profileCopier {
// Pre-serialize the profile. We will deserialize every time a fresh copy is needed.
var buf bytes.Buffer
src.WriteUncompressed(&buf)
return profileCopier(buf.Bytes())
}
// newCopy returns a new copy of the profile.
func (c profileCopier) newCopy() *profile.Profile {
p, err := profile.ParseUncompressed([]byte(c))
if err != nil {
panic(err)
}
return p
}

View File

@@ -0,0 +1,219 @@
// 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 driver
import (
"fmt"
"regexp"
"strconv"
"strings"
"m7s.live/v5/plugin/debug/pkg/internal/measurement"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/profile"
)
var tagFilterRangeRx = regexp.MustCompile("([+-]?[[:digit:]]+)([[:alpha:]]+)?")
// applyFocus filters samples based on the focus/ignore options
func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, cfg config, ui plugin.UI) error {
focus, err := compileRegexOption("focus", cfg.Focus, nil)
ignore, err := compileRegexOption("ignore", cfg.Ignore, err)
hide, err := compileRegexOption("hide", cfg.Hide, err)
show, err := compileRegexOption("show", cfg.Show, err)
showfrom, err := compileRegexOption("show_from", cfg.ShowFrom, err)
tagfocus, err := compileTagFilter("tagfocus", cfg.TagFocus, numLabelUnits, ui, err)
tagignore, err := compileTagFilter("tagignore", cfg.TagIgnore, numLabelUnits, ui, err)
prunefrom, err := compileRegexOption("prune_from", cfg.PruneFrom, err)
if err != nil {
return err
}
fm, im, hm, hnm := prof.FilterSamplesByName(focus, ignore, hide, show)
warnNoMatches(focus == nil || fm, "Focus", ui)
warnNoMatches(ignore == nil || im, "Ignore", ui)
warnNoMatches(hide == nil || hm, "Hide", ui)
warnNoMatches(show == nil || hnm, "Show", ui)
sfm := prof.ShowFrom(showfrom)
warnNoMatches(showfrom == nil || sfm, "ShowFrom", ui)
tfm, tim := prof.FilterSamplesByTag(tagfocus, tagignore)
warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)
tagshow, err := compileRegexOption("tagshow", cfg.TagShow, err)
taghide, err := compileRegexOption("taghide", cfg.TagHide, err)
tns, tnh := prof.FilterTagsByName(tagshow, taghide)
warnNoMatches(tagshow == nil || tns, "TagShow", ui)
warnNoMatches(taghide == nil || tnh, "TagHide", ui)
if prunefrom != nil {
prof.PruneFrom(prunefrom)
}
return err
}
func compileRegexOption(name, value string, err error) (*regexp.Regexp, error) {
if value == "" || err != nil {
return nil, err
}
rx, err := regexp.Compile(value)
if err != nil {
return nil, fmt.Errorf("parsing %s regexp: %v", name, err)
}
return rx, nil
}
func compileTagFilter(name, value string, numLabelUnits map[string]string, ui plugin.UI, err error) (func(*profile.Sample) bool, error) {
if value == "" || err != nil {
return nil, err
}
tagValuePair := strings.SplitN(value, "=", 2)
var wantKey string
if len(tagValuePair) == 2 {
wantKey = tagValuePair[0]
value = tagValuePair[1]
}
if numFilter := parseTagFilterRange(value); numFilter != nil {
ui.PrintErr(name, ":Interpreted '", value, "' as range, not regexp")
labelFilter := func(vals []int64, unit string) bool {
for _, val := range vals {
if numFilter(val, unit) {
return true
}
}
return false
}
numLabelUnit := func(key string) string {
return numLabelUnits[key]
}
if wantKey == "" {
return func(s *profile.Sample) bool {
for key, vals := range s.NumLabel {
if labelFilter(vals, numLabelUnit(key)) {
return true
}
}
return false
}, nil
}
return func(s *profile.Sample) bool {
if vals, ok := s.NumLabel[wantKey]; ok {
return labelFilter(vals, numLabelUnit(wantKey))
}
return false
}, nil
}
var rfx []*regexp.Regexp
for _, tagf := range strings.Split(value, ",") {
fx, err := regexp.Compile(tagf)
if err != nil {
return nil, fmt.Errorf("parsing %s regexp: %v", name, err)
}
rfx = append(rfx, fx)
}
if wantKey == "" {
return func(s *profile.Sample) bool {
matchedrx:
for _, rx := range rfx {
for key, vals := range s.Label {
for _, val := range vals {
// TODO: Match against val, not key:val in future
if rx.MatchString(key + ":" + val) {
continue matchedrx
}
}
}
return false
}
return true
}, nil
}
return func(s *profile.Sample) bool {
if vals, ok := s.Label[wantKey]; ok {
for _, rx := range rfx {
for _, val := range vals {
if rx.MatchString(val) {
return true
}
}
}
}
return false
}, nil
}
// parseTagFilterRange returns a function to checks if a value is
// contained on the range described by a string. It can recognize
// strings of the form:
// "32kb" -- matches values == 32kb
// ":64kb" -- matches values <= 64kb
// "4mb:" -- matches values >= 4mb
// "12kb:64mb" -- matches values between 12kb and 64mb (both included).
func parseTagFilterRange(filter string) func(int64, string) bool {
ranges := tagFilterRangeRx.FindAllStringSubmatch(filter, 2)
if len(ranges) == 0 {
return nil // No ranges were identified
}
v, err := strconv.ParseInt(ranges[0][1], 10, 64)
if err != nil {
panic(fmt.Errorf("failed to parse int %s: %v", ranges[0][1], err))
}
scaledValue, unit := measurement.Scale(v, ranges[0][2], ranges[0][2])
if len(ranges) == 1 {
switch match := ranges[0][0]; filter {
case match:
return func(v int64, u string) bool {
sv, su := measurement.Scale(v, u, unit)
return su == unit && sv == scaledValue
}
case match + ":":
return func(v int64, u string) bool {
sv, su := measurement.Scale(v, u, unit)
return su == unit && sv >= scaledValue
}
case ":" + match:
return func(v int64, u string) bool {
sv, su := measurement.Scale(v, u, unit)
return su == unit && sv <= scaledValue
}
}
return nil
}
if filter != ranges[0][0]+":"+ranges[1][0] {
return nil
}
if v, err = strconv.ParseInt(ranges[1][1], 10, 64); err != nil {
panic(fmt.Errorf("failed to parse int %s: %v", ranges[1][1], err))
}
scaledValue2, unit2 := measurement.Scale(v, ranges[1][2], unit)
if unit != unit2 {
return nil
}
return func(v int64, u string) bool {
sv, su := measurement.Scale(v, u, unit)
return su == unit && sv >= scaledValue && sv <= scaledValue2
}
}
func warnNoMatches(match bool, option string, ui plugin.UI) {
if !match {
ui.PrintErr(option + " expression matched no samples")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,622 @@
// 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 driver
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"m7s.live/v5/plugin/debug/pkg/internal/measurement"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/profile"
)
// fetchProfiles fetches and symbolizes the profiles specified by s.
// It will merge all the profiles it is able to retrieve, even if
// there are some failures. It will return an error if it is unable to
// fetch any profiles.
func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
sources := make([]profileSource, 0, len(s.Sources))
for _, src := range s.Sources {
sources = append(sources, profileSource{
addr: src,
source: s,
})
}
bases := make([]profileSource, 0, len(s.Base))
for _, src := range s.Base {
bases = append(bases, profileSource{
addr: src,
source: s,
})
}
p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport)
if err != nil {
return nil, err
}
if pbase != nil {
if s.DiffBase {
pbase.SetLabel("pprof::base", []string{"true"})
}
if s.Normalize {
err := p.Normalize(pbase)
if err != nil {
return nil, err
}
}
pbase.Scale(-1)
p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase})
if err != nil {
return nil, err
}
}
// Symbolize the merged profile.
if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil {
return nil, err
}
p.RemoveUninteresting()
unsourceMappings(p)
if s.Comment != "" {
p.Comments = append(p.Comments, s.Comment)
}
// Save a copy of the merged profile if there is at least one remote source.
if save {
dir, err := setTmpDir(o.UI)
if err != nil {
return nil, err
}
prefix := "pprof."
if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
prefix += filepath.Base(p.Mapping[0].File) + "."
}
for _, s := range p.SampleType {
prefix += s.Type + "."
}
tempFile, err := newTempFile(dir, prefix, ".pb.gz")
if err == nil {
if err = p.Write(tempFile); err == nil {
o.UI.PrintErr("Saved profile in ", tempFile.Name())
}
}
if err != nil {
o.UI.PrintErr("Could not save profile: ", err)
}
}
if err := p.CheckValid(); err != nil {
return nil, err
}
return p, nil
}
func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, *profile.Profile, plugin.MappingSources, plugin.MappingSources, bool, error) {
wg := sync.WaitGroup{}
wg.Add(2)
var psrc, pbase *profile.Profile
var msrc, mbase plugin.MappingSources
var savesrc, savebase bool
var errsrc, errbase error
var countsrc, countbase int
go func() {
defer wg.Done()
psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr)
}()
go func() {
defer wg.Done()
pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr)
}()
wg.Wait()
save := savesrc || savebase
if errsrc != nil {
return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc)
}
if errbase != nil {
return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase)
}
if countsrc == 0 {
return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles")
}
if countbase == 0 && len(bases) > 0 {
return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles")
}
if want, got := len(sources), countsrc; want != got {
ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want))
}
if want, got := len(bases), countbase; want != got {
ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want))
}
return psrc, pbase, msrc, mbase, save, nil
}
// chunkedGrab fetches the profiles described in source and merges them into
// a single profile. It fetches a chunk of profiles concurrently, with a maximum
// chunk size to limit its memory usage.
func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
const chunkSize = 128
var p *profile.Profile
var msrc plugin.MappingSources
var save bool
var count int
for start := 0; start < len(sources); start += chunkSize {
end := start + chunkSize
if end > len(sources) {
end = len(sources)
}
chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr)
switch {
case chunkErr != nil:
return nil, nil, false, 0, chunkErr
case chunkP == nil:
continue
case p == nil:
p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
default:
p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
if chunkErr != nil {
return nil, nil, false, 0, chunkErr
}
if chunkSave {
save = true
}
count += chunkCount
}
}
return p, msrc, save, count, nil
}
// concurrentGrab fetches multiple profiles concurrently
func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
wg := sync.WaitGroup{}
wg.Add(len(sources))
for i := range sources {
go func(s *profileSource) {
defer wg.Done()
s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr)
}(&sources[i])
}
wg.Wait()
var save bool
profiles := make([]*profile.Profile, 0, len(sources))
msrcs := make([]plugin.MappingSources, 0, len(sources))
for i := range sources {
s := &sources[i]
if err := s.err; err != nil {
ui.PrintErr(s.addr + ": " + err.Error())
continue
}
save = save || s.remote
profiles = append(profiles, s.p)
msrcs = append(msrcs, s.msrc)
*s = profileSource{}
}
if len(profiles) == 0 {
return nil, nil, false, 0, nil
}
p, msrc, err := combineProfiles(profiles, msrcs)
if err != nil {
return nil, nil, false, 0, err
}
return p, msrc, save, len(profiles), nil
}
func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
// Merge profiles.
//
// The merge call below only treats exactly matching sample type lists as
// compatible and will fail otherwise. Make the profiles' sample types
// compatible for the merge, see CompatibilizeSampleTypes() doc for details.
if err := profile.CompatibilizeSampleTypes(profiles); err != nil {
return nil, nil, err
}
if err := measurement.ScaleProfiles(profiles); err != nil {
return nil, nil, err
}
// Avoid expensive work for the common case of a single profile/src.
if len(profiles) == 1 && len(msrcs) == 1 {
return profiles[0], msrcs[0], nil
}
p, err := profile.Merge(profiles)
if err != nil {
return nil, nil, err
}
// Combine mapping sources.
msrc := make(plugin.MappingSources)
for _, ms := range msrcs {
for m, s := range ms {
msrc[m] = append(msrc[m], s...)
}
}
return p, msrc, nil
}
type profileSource struct {
addr string
source *source
p *profile.Profile
msrc plugin.MappingSources
remote bool
err error
}
func homeEnv() string {
switch runtime.GOOS {
case "windows":
return "USERPROFILE"
case "plan9":
return "home"
default:
return "HOME"
}
}
// setTmpDir prepares the directory to use to save profiles retrieved
// remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof, and, if
// $HOME is not set, falls back to os.TempDir().
func setTmpDir(ui plugin.UI) (string, error) {
var dirs []string
if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
dirs = append(dirs, profileDir)
}
if homeDir := os.Getenv(homeEnv()); homeDir != "" {
dirs = append(dirs, filepath.Join(homeDir, "pprof"))
}
dirs = append(dirs, os.TempDir())
for _, tmpDir := range dirs {
if err := os.MkdirAll(tmpDir, 0755); err != nil {
ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
continue
}
return tmpDir, nil
}
return "", fmt.Errorf("failed to identify temp dir")
}
const testSourceAddress = "pproftest.local"
// grabProfile fetches a profile. Returns the profile, sources for the
// profile mappings, a bool indicating if the profile was fetched
// remotely, and an error.
func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
var src string
duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
if fetcher != nil {
p, src, err = fetcher.Fetch(source, duration, timeout)
if err != nil {
return
}
}
if err != nil || p == nil {
// Fetch the profile over HTTP or from a file.
p, src, err = fetch(source, duration, timeout, ui, tr)
if err != nil {
return
}
}
if err = p.CheckValid(); err != nil {
return
}
// Update the binary locations from command line and paths.
locateBinaries(p, s, obj, ui)
// Collect the source URL for all mappings.
if src != "" {
msrc = collectMappingSources(p, src)
remote = true
if strings.HasPrefix(src, "http://"+testSourceAddress) {
// Treat test inputs as local to avoid saving
// testcase profiles during driver testing.
remote = false
}
}
return
}
// collectMappingSources saves the mapping sources of a profile.
func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources {
ms := plugin.MappingSources{}
for _, m := range p.Mapping {
src := struct {
Source string
Start uint64
}{
source, m.Start,
}
key := m.BuildID
if key == "" {
key = m.File
}
if key == "" {
// If there is no build id or source file, use the source as the
// mapping file. This will enable remote symbolization for this
// mapping, in particular for Go profiles on the legacy format.
// The source is reset back to empty string by unsourceMapping
// which is called after symbolization is finished.
m.File = source
key = source
}
ms[key] = append(ms[key], src)
}
return ms
}
// unsourceMappings iterates over the mappings in a profile and replaces file
// set to the remote source URL by collectMappingSources back to empty string.
func unsourceMappings(p *profile.Profile) {
for _, m := range p.Mapping {
if m.BuildID == "" && filepath.VolumeName(m.File) == "" {
if u, err := url.Parse(m.File); err == nil && u.IsAbs() {
m.File = ""
}
}
}
}
// locateBinaries searches for binary files listed in the profile and, if found,
// updates the profile accordingly.
func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) {
// Construct search path to examine
searchPath := os.Getenv("PPROF_BINARY_PATH")
if searchPath == "" {
// Use $HOME/pprof/binaries as default directory for local symbolization binaries
searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries")
}
mapping:
for _, m := range p.Mapping {
var noVolumeFile string
var baseName string
var dirName string
if m.File != "" {
noVolumeFile = strings.TrimPrefix(m.File, filepath.VolumeName(m.File))
baseName = filepath.Base(m.File)
dirName = filepath.Dir(noVolumeFile)
}
for _, path := range filepath.SplitList(searchPath) {
var fileNames []string
if m.BuildID != "" {
fileNames = []string{filepath.Join(path, m.BuildID, baseName)}
if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
fileNames = append(fileNames, matches...)
}
fileNames = append(fileNames, filepath.Join(path, noVolumeFile, m.BuildID)) // perf path format
// Llvm buildid protocol: the first two characters of the build id
// are used as directory, and the remaining part is in the filename.
// e.g. `/ab/cdef0123456.debug`
fileNames = append(fileNames, filepath.Join(path, m.BuildID[:2], m.BuildID[2:]+".debug"))
}
if m.File != "" {
// Try both the basename and the full path, to support the same directory
// structure as the perf symfs option.
fileNames = append(fileNames, filepath.Join(path, baseName))
fileNames = append(fileNames, filepath.Join(path, noVolumeFile))
// Other locations: use the same search paths as GDB, according to
// https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
fileNames = append(fileNames, filepath.Join(path, noVolumeFile+".debug"))
fileNames = append(fileNames, filepath.Join(path, dirName, ".debug", baseName+".debug"))
fileNames = append(fileNames, filepath.Join(path, "usr", "lib", "debug", dirName, baseName+".debug"))
}
for _, name := range fileNames {
if f, err := obj.Open(name, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol); err == nil {
defer f.Close()
fileBuildID := f.BuildID()
if m.BuildID != "" && m.BuildID != fileBuildID {
ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")")
} else {
// Explicitly do not update KernelRelocationSymbol --
// the new local file name is most likely missing it.
m.File = name
continue mapping
}
}
}
}
}
if len(p.Mapping) == 0 {
// If there are no mappings, add a fake mapping to attempt symbolization.
// This is useful for some profiles generated by the golang runtime, which
// do not include any mappings. Symbolization with a fake mapping will only
// be successful against a non-PIE binary.
m := &profile.Mapping{ID: 1}
p.Mapping = []*profile.Mapping{m}
for _, l := range p.Location {
l.Mapping = m
}
}
// If configured, apply executable filename override and (maybe, see below)
// build ID override from source. Assume the executable is the first mapping.
if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" {
m := p.Mapping[0]
if execName != "" {
// Explicitly do not update KernelRelocationSymbol --
// the source override is most likely missing it.
m.File = execName
}
// Only apply the build ID override if the build ID in the main mapping is
// missing. Overwriting the build ID in case it's present is very likely a
// wrong thing to do so we refuse to do that.
if buildID != "" && m.BuildID == "" {
m.BuildID = buildID
}
}
}
// fetch fetches a profile from source, within the timeout specified,
// producing messages through the ui. It returns the profile and the
// url of the actual source of the profile for remote profiles.
func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) {
var f io.ReadCloser
// First determine whether the source is a file, if not, it will be treated as a URL.
if _, err = os.Stat(source); err == nil {
if isPerfFile(source) {
f, err = convertPerfData(source, ui)
} else {
f, err = os.Open(source)
}
} else {
sourceURL, timeout := adjustURL(source, duration, timeout)
if sourceURL != "" {
ui.Print("Fetching profile over HTTP from " + sourceURL)
if duration > 0 {
ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
}
f, err = fetchURL(sourceURL, timeout, tr)
src = sourceURL
}
}
if err == nil {
defer f.Close()
p, err = profile.Parse(f)
}
return
}
// fetchURL fetches a profile from a URL using HTTP.
func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.ReadCloser, error) {
client := &http.Client{
Transport: tr,
Timeout: timeout + 5*time.Second,
}
resp, err := client.Get(source)
if err != nil {
return nil, fmt.Errorf("http fetch: %v", err)
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
return nil, statusCodeError(resp)
}
return resp.Body, nil
}
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)
}
// isPerfFile checks if a file is in perf.data format. It also returns false
// if it encounters an error during the check.
func isPerfFile(path string) bool {
sourceFile, openErr := os.Open(path)
if openErr != nil {
return false
}
defer sourceFile.Close()
// If the file is the output of a perf record command, it should begin
// with the string PERFILE2.
perfHeader := []byte("PERFILE2")
actualHeader := make([]byte, len(perfHeader))
if _, readErr := sourceFile.Read(actualHeader); readErr != nil {
return false
}
return bytes.Equal(actualHeader, perfHeader)
}
// convertPerfData converts the file at path which should be in perf.data format
// using the perf_to_profile tool and returns the file containing the
// profile.proto formatted data.
func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
ui.Print(fmt.Sprintf(
"Converting %s to a profile.proto... (May take a few minutes)",
perfPath))
profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz")
if err != nil {
return nil, err
}
deferDeleteTempFile(profile.Name())
cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f")
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
profile.Close()
return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
}
return profile, nil
}
// adjustURL validates if a profile source is a URL and returns an
// cleaned up URL and the timeout to use for retrieval over HTTP.
// If the source cannot be recognized as a URL it returns an empty string.
func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
u, err := url.Parse(source)
if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
// Try adding http:// to catch sources of the form hostname:port/path.
// url.Parse treats "hostname" as the scheme.
u, err = url.Parse("http://" + source)
}
if err != nil || u.Host == "" {
return "", 0
}
// Apply duration/timeout overrides to URL.
values := u.Query()
if duration > 0 {
values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
} else {
if urlSeconds := values.Get("seconds"); urlSeconds != "" {
if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
duration = time.Duration(us) * time.Second
}
}
}
if timeout <= 0 {
if duration > 0 {
timeout = duration + duration/2
} else {
timeout = 60 * time.Second
}
}
u.RawQuery = values.Encode()
return u.String(), timeout
}

View File

@@ -0,0 +1,836 @@
// 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 driver
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"testing"
"time"
"m7s.live/v5/plugin/debug/pkg/internal/binutils"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/internal/proftest"
"m7s.live/v5/plugin/debug/pkg/internal/symbolizer"
"m7s.live/v5/plugin/debug/pkg/internal/transport"
"m7s.live/v5/plugin/debug/pkg/profile"
)
func TestSymbolizationPath(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test assumes Unix paths")
}
// Save environment variables to restore after test
saveHome := os.Getenv(homeEnv())
savePath := os.Getenv("PPROF_BINARY_PATH")
tempdir, err := os.MkdirTemp("", "home")
if err != nil {
t.Fatal("creating temp dir: ", err)
}
defer os.RemoveAll(tempdir)
os.MkdirAll(filepath.Join(tempdir, "pprof", "binaries", "abcde10001"), 0700)
os.Create(filepath.Join(tempdir, "pprof", "binaries", "abcde10001", "binary"))
os.MkdirAll(filepath.Join(tempdir, "pprof", "binaries", "fg"), 0700)
os.Create(filepath.Join(tempdir, "pprof", "binaries", "fg", "hij10001.debug"))
obj := testObj{tempdir}
os.Setenv(homeEnv(), tempdir)
for _, tc := range []struct {
env, file, buildID, want string
msgCount int
}{
{"", "/usr/bin/binary", "", "/usr/bin/binary", 0},
{"", "/usr/bin/binary", "fedcb10000", "/usr/bin/binary", 0},
{"/usr", "/bin/binary", "", "/usr/bin/binary", 0},
{"", "/prod/path/binary", "abcde10001", filepath.Join(tempdir, "pprof/binaries/abcde10001/binary"), 0},
{"/alternate/architecture", "/usr/bin/binary", "", "/alternate/architecture/binary", 0},
{"/alternate/architecture", "/usr/bin/binary", "abcde10001", "/alternate/architecture/binary", 0},
{"", "", "fghij10001", filepath.Join(tempdir, "pprof/binaries/fg/hij10001.debug"), 0},
{"/nowhere:/alternate/architecture", "/usr/bin/binary", "fedcb10000", "/usr/bin/binary", 1},
{"/nowhere:/alternate/architecture", "/usr/bin/binary", "abcde10002", "/usr/bin/binary", 1},
} {
os.Setenv("PPROF_BINARY_PATH", tc.env)
p := &profile.Profile{
Mapping: []*profile.Mapping{
{
File: tc.file,
BuildID: tc.buildID,
},
},
}
s := &source{}
locateBinaries(p, s, obj, &proftest.TestUI{T: t, Ignore: tc.msgCount})
if file := p.Mapping[0].File; file != tc.want {
t.Errorf("%s:%s:%s, want %s, got %s", tc.env, tc.file, tc.buildID, tc.want, file)
}
}
os.Setenv(homeEnv(), saveHome)
os.Setenv("PPROF_BINARY_PATH", savePath)
}
func TestCollectMappingSources(t *testing.T) {
const startAddress uint64 = 0x40000
const url = "http://example.com"
for _, tc := range []struct {
file, buildID string
want plugin.MappingSources
}{
{"/usr/bin/binary", "buildId", mappingSources("buildId", url, startAddress)},
{"/usr/bin/binary", "", mappingSources("/usr/bin/binary", url, startAddress)},
{"", "", mappingSources(url, url, startAddress)},
} {
p := &profile.Profile{
Mapping: []*profile.Mapping{
{
File: tc.file,
BuildID: tc.buildID,
Start: startAddress,
},
},
}
got := collectMappingSources(p, url)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("%s:%s, want %v, got %v", tc.file, tc.buildID, tc.want, got)
}
}
}
func TestUnsourceMappings(t *testing.T) {
for _, tc := range []struct {
os, file, buildID, want string
}{
{"any", "/usr/bin/binary", "buildId", "/usr/bin/binary"},
{"any", "http://example.com", "", ""},
{"windows", `C:\example.exe`, "", `C:\example.exe`},
{"windows", `c:/example.exe`, "", `c:/example.exe`},
} {
t.Run(tc.file+"-"+tc.os, func(t *testing.T) {
if tc.os != "any" && tc.os != runtime.GOOS {
t.Skipf("%s only test", tc.os)
}
p := &profile.Profile{
Mapping: []*profile.Mapping{
{
File: tc.file,
BuildID: tc.buildID,
},
},
}
unsourceMappings(p)
if got := p.Mapping[0].File; got != tc.want {
t.Errorf("%s:%s, want %s, got %s", tc.file, tc.buildID, tc.want, got)
}
})
}
}
type testObj struct {
home string
}
func (o testObj) Open(file string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
switch file {
case "/alternate/architecture/binary":
return testFile{file, "abcde10001"}, nil
case "/usr/bin/binary":
return testFile{file, "fedcb10000"}, nil
case filepath.Join(o.home, "pprof/binaries/abcde10001/binary"):
return testFile{file, "abcde10001"}, nil
case filepath.Join(o.home, "pprof/binaries/fg/hij10001.debug"):
return testFile{file, "fghij10001"}, nil
}
return nil, fmt.Errorf("not found: %s", file)
}
func (testObj) Demangler(_ string) func(names []string) (map[string]string, error) {
return func(names []string) (map[string]string, error) { return nil, nil }
}
func (testObj) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) {
return nil, nil
}
type testFile struct{ name, buildID string }
func (f testFile) Name() string { return f.name }
func (testFile) ObjAddr(addr uint64) (uint64, error) { return addr, nil }
func (f testFile) BuildID() string { return f.buildID }
func (testFile) SourceLine(addr uint64) ([]plugin.Frame, error) { return nil, nil }
func (testFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { return nil, nil }
func (testFile) Close() error { return nil }
func TestFetch(t *testing.T) {
const path = "testdata/"
type testcase struct {
source, execName string
wantErr bool
}
ts := []testcase{
{path + "go.crc32.cpu", "", false},
{path + "go.nomappings.crash", "/bin/gotest.exe", false},
{"http://localhost/profile?file=cppbench.cpu", "", false},
{"./missing", "", true},
}
// Test that paths with a colon character are recognized as file paths
// if the file exists, rather than as a URL. We have to skip this test
// on Windows since the colon char is not allowed in Windows paths.
if runtime.GOOS != "windows" {
src := filepath.Join(path, "go.crc32.cpu")
dst := filepath.Join(t.TempDir(), "go.crc32.cpu_2023-11-11_01:02:03")
data, err := os.ReadFile(src)
if err != nil {
t.Fatalf("read src file %s failed: %#v", src, err)
}
err = os.WriteFile(dst, data, 0644)
if err != nil {
t.Fatalf("create dst file %s failed: %#v", dst, err)
}
ts = append(ts, testcase{dst, "", false})
}
for _, tc := range ts {
t.Run(tc.source, func(t *testing.T) {
p, _, _, err := grabProfile(&source{ExecName: tc.execName}, tc.source, nil, testObj{}, &proftest.TestUI{T: t}, &httpTransport{})
if tc.wantErr {
if err == nil {
t.Fatal("got no error, want an error")
}
return
}
if err != nil {
t.Fatalf("got error %v, want no error", err)
}
if len(p.Sample) == 0 {
t.Error("got zero samples, want non-zero")
}
if e := tc.execName; e != "" {
switch {
case len(p.Mapping) == 0 || p.Mapping[0] == nil:
t.Errorf("got no mappings, want mapping[0].execName == %s", e)
case p.Mapping[0].File != e:
t.Errorf("got mapping[0].execName == %s, want %s", p.Mapping[0].File, e)
}
}
})
}
}
func TestFetchWithBase(t *testing.T) {
baseConfig := currentConfig()
defer setCurrentConfig(baseConfig)
type WantSample struct {
values []int64
labels map[string][]string
}
const path = "testdata/"
type testcase struct {
desc string
sources []string
bases []string
diffBases []string
normalize bool
wantSamples []WantSample
wantParseErrorMsg string
wantFetchErrorMsg string
}
testcases := []testcase{
{
"not normalized base is same as source",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention"},
nil,
false,
nil,
"",
"",
},
{
"not normalized base is same as source",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention"},
nil,
false,
nil,
"",
"",
},
{
"not normalized single source, multiple base (all profiles same)",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention", path + "cppbench.contention"},
nil,
false,
[]WantSample{
{
values: []int64{-2700, -608881724},
labels: map[string][]string{},
},
{
values: []int64{-100, -23992},
labels: map[string][]string{},
},
{
values: []int64{-200, -179943},
labels: map[string][]string{},
},
{
values: []int64{-100, -17778444},
labels: map[string][]string{},
},
{
values: []int64{-100, -75976},
labels: map[string][]string{},
},
{
values: []int64{-300, -63568134},
labels: map[string][]string{},
},
},
"",
"",
},
{
"not normalized, different base and source",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.small.contention"},
nil,
false,
[]WantSample{
{
values: []int64{1700, 608878600},
labels: map[string][]string{},
},
{
values: []int64{100, 23992},
labels: map[string][]string{},
},
{
values: []int64{200, 179943},
labels: map[string][]string{},
},
{
values: []int64{100, 17778444},
labels: map[string][]string{},
},
{
values: []int64{100, 75976},
labels: map[string][]string{},
},
{
values: []int64{300, 63568134},
labels: map[string][]string{},
},
},
"",
"",
},
{
"normalized base is same as source",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention"},
nil,
true,
nil,
"",
"",
},
{
"normalized single source, multiple base (all profiles same)",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention", path + "cppbench.contention"},
nil,
true,
nil,
"",
"",
},
{
"normalized different base and source",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.small.contention"},
nil,
true,
[]WantSample{
{
values: []int64{-229, -369},
labels: map[string][]string{},
},
{
values: []int64{29, 0},
labels: map[string][]string{},
},
{
values: []int64{57, 1},
labels: map[string][]string{},
},
{
values: []int64{29, 80},
labels: map[string][]string{},
},
{
values: []int64{29, 0},
labels: map[string][]string{},
},
{
values: []int64{86, 288},
labels: map[string][]string{},
},
},
"",
"",
},
{
"not normalized diff base is same as source",
[]string{path + "cppbench.contention"},
nil,
[]string{path + "cppbench.contention"},
false,
[]WantSample{
{
values: []int64{2700, 608881724},
labels: map[string][]string{},
},
{
values: []int64{100, 23992},
labels: map[string][]string{},
},
{
values: []int64{200, 179943},
labels: map[string][]string{},
},
{
values: []int64{100, 17778444},
labels: map[string][]string{},
},
{
values: []int64{100, 75976},
labels: map[string][]string{},
},
{
values: []int64{300, 63568134},
labels: map[string][]string{},
},
{
values: []int64{-2700, -608881724},
labels: map[string][]string{"pprof::base": {"true"}},
},
{
values: []int64{-100, -23992},
labels: map[string][]string{"pprof::base": {"true"}},
},
{
values: []int64{-200, -179943},
labels: map[string][]string{"pprof::base": {"true"}},
},
{
values: []int64{-100, -17778444},
labels: map[string][]string{"pprof::base": {"true"}},
},
{
values: []int64{-100, -75976},
labels: map[string][]string{"pprof::base": {"true"}},
},
{
values: []int64{-300, -63568134},
labels: map[string][]string{"pprof::base": {"true"}},
},
},
"",
"",
},
{
"diff_base and base both specified",
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention"},
[]string{path + "cppbench.contention"},
false,
nil,
"-base and -diff_base flags cannot both be specified",
"",
},
{
"input profiles with different sample types (non empty intersection)",
[]string{path + "cppbench.cpu", path + "cppbench.cpu_no_samples_type"},
[]string{path + "cppbench.cpu", path + "cppbench.cpu_no_samples_type"},
nil,
false,
nil,
"",
"",
},
{
"input profiles with different sample types (empty intersection)",
[]string{path + "cppbench.cpu", path + "cppbench.contention"},
[]string{path + "cppbench.cpu", path + "cppbench.contention"},
nil,
false,
nil,
"",
"problem fetching source profiles: profiles have empty common sample type list",
},
}
for _, tc := range testcases {
t.Run(tc.desc, func(t *testing.T) {
setCurrentConfig(baseConfig)
f := testFlags{
stringLists: map[string][]string{
"base": tc.bases,
"diff_base": tc.diffBases,
},
bools: map[string]bool{
"normalize": tc.normalize,
},
}
f.args = tc.sources
o := setDefaults(&plugin.Options{
UI: &proftest.TestUI{T: t, AllowRx: "Local symbolization failed|Some binary filenames not available"},
Flagset: f,
HTTPTransport: transport.New(nil),
})
src, _, err := parseFlags(o)
if tc.wantParseErrorMsg != "" {
if err == nil {
t.Fatalf("got nil, want error %q", tc.wantParseErrorMsg)
}
if gotErrMsg := err.Error(); gotErrMsg != tc.wantParseErrorMsg {
t.Fatalf("got error %q, want error %q", gotErrMsg, tc.wantParseErrorMsg)
}
return
}
if err != nil {
t.Fatalf("got error %q, want no error", err)
}
p, err := fetchProfiles(src, o)
if tc.wantFetchErrorMsg != "" {
if err == nil {
t.Fatalf("got nil, want error %q", tc.wantFetchErrorMsg)
}
if gotErrMsg := err.Error(); gotErrMsg != tc.wantFetchErrorMsg {
t.Fatalf("got error %q, want error %q", gotErrMsg, tc.wantFetchErrorMsg)
}
return
}
if err != nil {
t.Fatalf("got error %q, want no error", err)
}
if got, want := len(p.Sample), len(tc.wantSamples); got != want {
t.Fatalf("got %d samples want %d", got, want)
}
for i, sample := range p.Sample {
if !reflect.DeepEqual(tc.wantSamples[i].values, sample.Value) {
t.Errorf("for sample %d got values %v, want %v", i, sample.Value, tc.wantSamples[i])
}
if !reflect.DeepEqual(tc.wantSamples[i].labels, sample.Label) {
t.Errorf("for sample %d got labels %v, want %v", i, sample.Label, tc.wantSamples[i].labels)
}
}
})
}
}
// mappingSources creates MappingSources map with a single item.
func mappingSources(key, source string, start uint64) plugin.MappingSources {
return plugin.MappingSources{
key: []struct {
Source string
Start uint64
}{
{Source: source, Start: start},
},
}
}
type httpTransport struct{}
func (tr *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
values := req.URL.Query()
file := values.Get("file")
if file == "" {
return nil, fmt.Errorf("want .../file?profile, got %s", req.URL.String())
}
t := &http.Transport{}
t.RegisterProtocol("file", http.NewFileTransport(http.Dir("testdata/")))
c := &http.Client{Transport: t}
return c.Get("file:///" + file)
}
func closedError() string {
if runtime.GOOS == "plan9" {
return "listen hungup"
}
return "use of closed"
}
func TestHTTPSInsecure(t *testing.T) {
if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
t.Skip("test assumes tcp available")
}
saveHome := os.Getenv(homeEnv())
tempdir, err := os.MkdirTemp("", "home")
if err != nil {
t.Fatal("creating temp dir: ", err)
}
defer os.RemoveAll(tempdir)
// pprof writes to $HOME/pprof by default which is not necessarily
// writeable (e.g. on a Debian buildd) so set $HOME to something we
// know we can write to for the duration of the test.
os.Setenv(homeEnv(), tempdir)
defer os.Setenv(homeEnv(), saveHome)
baseConfig := currentConfig()
defer setCurrentConfig(baseConfig)
tlsCert, _, _ := selfSignedCert(t, "")
tlsConfig := &tls.Config{Certificates: []tls.Certificate{tlsCert}}
l, err := tls.Listen("tcp", "localhost:0", tlsConfig)
if err != nil {
t.Fatalf("net.Listen: got error %v, want no error", err)
}
donec := make(chan error, 1)
go func(donec chan<- error) {
donec <- http.Serve(l, nil)
}(donec)
defer func() {
if got, want := <-donec, closedError(); !strings.Contains(got.Error(), want) {
t.Fatalf("Serve got error %v, want %q", got, want)
}
}()
defer l.Close()
outputTempFile, err := os.CreateTemp("", "profile_output")
if err != nil {
t.Fatalf("Failed to create tempfile: %v", err)
}
defer os.Remove(outputTempFile.Name())
defer outputTempFile.Close()
address := "https+insecure://" + l.Addr().String() + "/debug/pprof/goroutine"
s := &source{
Sources: []string{address},
Timeout: 10,
Symbolize: "remote",
}
o := &plugin.Options{
Obj: &binutils.Binutils{},
UI: &proftest.TestUI{T: t, AllowRx: "Saved profile in"},
HTTPTransport: transport.New(nil),
}
o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI}
p, err := fetchProfiles(s, o)
if err != nil {
t.Fatal(err)
}
if len(p.SampleType) == 0 {
t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
}
if len(p.Function) == 0 {
t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
}
if err := checkProfileHasFunction(p, "TestHTTPSInsecure"); err != nil {
t.Fatalf("fetchProfiles(%s) %v", address, err)
}
}
func TestHTTPSWithServerCertFetch(t *testing.T) {
if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
t.Skip("test assumes tcp available")
}
saveHome := os.Getenv(homeEnv())
tempdir, err := os.MkdirTemp("", "home")
if err != nil {
t.Fatal("creating temp dir: ", err)
}
defer os.RemoveAll(tempdir)
// pprof writes to $HOME/pprof by default which is not necessarily
// writeable (e.g. on a Debian buildd) so set $HOME to something we
// know we can write to for the duration of the test.
os.Setenv(homeEnv(), tempdir)
defer os.Setenv(homeEnv(), saveHome)
baseConfig := currentConfig()
defer setCurrentConfig(baseConfig)
cert, certBytes, keyBytes := selfSignedCert(t, "localhost")
cas := x509.NewCertPool()
cas.AppendCertsFromPEM(certBytes)
tlsConfig := &tls.Config{
RootCAs: cas,
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: cas,
}
l, err := tls.Listen("tcp", "localhost:0", tlsConfig)
if err != nil {
t.Fatalf("net.Listen: got error %v, want no error", err)
}
donec := make(chan error, 1)
go func(donec chan<- error) {
donec <- http.Serve(l, nil)
}(donec)
defer func() {
if got, want := <-donec, closedError(); !strings.Contains(got.Error(), want) {
t.Fatalf("Serve got error %v, want %q", got, want)
}
}()
defer l.Close()
outputTempFile, err := os.CreateTemp("", "profile_output")
if err != nil {
t.Fatalf("Failed to create tempfile: %v", err)
}
defer os.Remove(outputTempFile.Name())
defer outputTempFile.Close()
// Get port from the address, so request to the server can be made using
// the host name specified in certificates.
_, portStr, err := net.SplitHostPort(l.Addr().String())
if err != nil {
t.Fatalf("cannot get port from URL: %v", err)
}
address := "https://" + "localhost:" + portStr + "/debug/pprof/goroutine"
s := &source{
Sources: []string{address},
Timeout: 10,
Symbolize: "remote",
}
certTempFile, err := os.CreateTemp("", "cert_output")
if err != nil {
t.Errorf("cannot create cert tempfile: %v", err)
}
defer os.Remove(certTempFile.Name())
defer certTempFile.Close()
certTempFile.Write(certBytes)
keyTempFile, err := os.CreateTemp("", "key_output")
if err != nil {
t.Errorf("cannot create key tempfile: %v", err)
}
defer os.Remove(keyTempFile.Name())
defer keyTempFile.Close()
keyTempFile.Write(keyBytes)
f := &testFlags{
strings: map[string]string{
"tls_cert": certTempFile.Name(),
"tls_key": keyTempFile.Name(),
"tls_ca": certTempFile.Name(),
},
}
o := &plugin.Options{
Obj: &binutils.Binutils{},
UI: &proftest.TestUI{T: t, AllowRx: "Saved profile in"},
Flagset: f,
HTTPTransport: transport.New(f),
}
o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI, Transport: o.HTTPTransport}
p, err := fetchProfiles(s, o)
if err != nil {
t.Fatal(err)
}
if len(p.SampleType) == 0 {
t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
}
if len(p.Function) == 0 {
t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
}
if err := checkProfileHasFunction(p, "TestHTTPSWithServerCertFetch"); err != nil {
t.Fatalf("fetchProfiles(%s) %v", address, err)
}
}
func checkProfileHasFunction(p *profile.Profile, fname string) error {
for _, f := range p.Function {
if strings.Contains(f.Name, fname) {
return nil
}
}
return fmt.Errorf("got %s, want function %q", p.String(), fname)
}
// selfSignedCert generates a self-signed certificate, and returns the
// generated certificate, and byte arrays containing the certificate and
// key associated with the certificate.
func selfSignedCert(t *testing.T, host string) (tls.Certificate, []byte, []byte) {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("failed to generate private key: %v", err)
}
b, err := x509.MarshalECPrivateKey(privKey)
if err != nil {
t.Fatalf("failed to marshal private key: %v", err)
}
bk := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
tmpl := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * time.Minute),
IsCA: true,
DNSNames: []string{host},
}
b, err = x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, privKey.Public(), privKey)
if err != nil {
t.Fatalf("failed to create cert: %v", err)
}
bc := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: b})
cert, err := tls.X509KeyPair(bc, bk)
if err != nil {
t.Fatalf("failed to create TLS key pair: %v", err)
}
return cert, bc, bk
}

View File

@@ -0,0 +1,71 @@
// Copyright 2018 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 driver
import (
"flag"
"strings"
)
// GoFlags implements the plugin.FlagSet interface.
type GoFlags struct {
UsageMsgs []string
}
// Bool implements the plugin.FlagSet interface.
func (*GoFlags) Bool(o string, d bool, c string) *bool {
return flag.Bool(o, d, c)
}
// Int implements the plugin.FlagSet interface.
func (*GoFlags) Int(o string, d int, c string) *int {
return flag.Int(o, d, c)
}
// Float64 implements the plugin.FlagSet interface.
func (*GoFlags) Float64(o string, d float64, c string) *float64 {
return flag.Float64(o, d, c)
}
// String implements the plugin.FlagSet interface.
func (*GoFlags) String(o, d, c string) *string {
return flag.String(o, d, c)
}
// StringList implements the plugin.FlagSet interface.
func (*GoFlags) StringList(o, d, c string) *[]*string {
return &[]*string{flag.String(o, d, c)}
}
// ExtraUsage implements the plugin.FlagSet interface.
func (f *GoFlags) ExtraUsage() string {
return strings.Join(f.UsageMsgs, "\n")
}
// AddExtraUsage implements the plugin.FlagSet interface.
func (f *GoFlags) AddExtraUsage(eu string) {
f.UsageMsgs = append(f.UsageMsgs, eu)
}
// Parse implements the plugin.FlagSet interface.
func (*GoFlags) Parse(usage func()) []string {
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) == 0 {
usage()
}
return args
}

View File

@@ -0,0 +1,279 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-size: 13px;
line-height: 1.4;
display: flex;
flex-direction: column;
}
a {
color: #2a66d9;
}
.header {
display: flex;
align-items: center;
height: 44px;
min-height: 44px;
background-color: #eee;
color: #212121;
padding: 0 1rem;
}
.header > div {
margin: 0 0.125em;
}
.header .title h1 {
font-size: 1.75em;
margin-right: 1rem;
margin-bottom: 4px;
}
.header .title a {
color: #212121;
text-decoration: none;
}
.header .title a:hover {
text-decoration: underline;
}
.header .description {
width: 100%;
text-align: right;
white-space: nowrap;
}
@media screen and (max-width: 799px) {
.header input {
display: none;
}
}
#detailsbox {
display: none;
position: fixed;
top: 40px;
right: 20px;
background-color: #ffffff;
box-shadow: 0 1px 5px rgba(0,0,0,.3);
line-height: 24px;
padding: 1em;
text-align: left;
}
.header input {
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' style='pointer-events:none;display:block;width:100%25;height:100%25;fill:%23757575'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61.0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 4px center/20px 20px;
border: 1px solid #d1d2d3;
border-radius: 2px 0 0 2px;
padding: 0.25em;
padding-left: 28px;
margin-left: 1em;
font-family: 'Roboto', 'Noto', sans-serif;
font-size: 1em;
line-height: 24px;
color: #212121;
}
.downArrow {
border-top: .36em solid #ccc;
border-left: .36em solid transparent;
border-right: .36em solid transparent;
margin-bottom: .05em;
margin-left: .5em;
transition: border-top-color 200ms;
}
.menu-item {
height: 100%;
text-transform: uppercase;
font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
.menu-item .menu-name:hover {
opacity: 0.75;
}
.menu-item .menu-name:hover .downArrow {
border-top-color: #666;
}
.menu-name {
height: 100%;
padding: 0 0.5em;
display: flex;
align-items: center;
justify-content: center;
}
.menu-name a {
text-decoration: none;
color: #212121;
}
.submenu {
display: none;
margin-top: -4px;
min-width: 10em;
position: absolute;
left: 0px;
background-color: white;
box-shadow: 0 1px 5px rgba(0,0,0,.3);
font-size: 100%;
text-transform: none;
white-space: nowrap;
}
.menu-item, .submenu {
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.submenu hr {
border: 0;
border-top: 2px solid #eee;
}
.submenu a {
display: block;
padding: .5em 1em;
text-decoration: none;
}
.submenu a:hover, .submenu a.active {
color: white;
background-color: #6b82d6;
}
.submenu a.disabled {
color: gray;
pointer-events: none;
}
.menu-check-mark {
position: absolute;
left: 2px;
}
.menu-delete-btn {
position: absolute;
right: 2px;
}
.help {
padding-left: 1em;
}
{{/* Used to disable events when a modal dialog is displayed */}}
#dialog-overlay {
display: none;
position: fixed;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background-color: rgba(1,1,1,0.1);
}
.dialog {
{{/* Displayed centered horizontally near the top */}}
display: none;
position: fixed;
margin: 0px;
top: 60px;
left: 50%;
transform: translateX(-50%);
font-size: 125%;
background-color: #ffffff;
box-shadow: 0 1px 5px rgba(0,0,0,.3);
}
.dialog-header {
font-size: 120%;
border-bottom: 1px solid #CCCCCC;
width: 100%;
text-align: center;
background: #EEEEEE;
user-select: none;
}
.dialog-footer {
border-top: 1px solid #CCCCCC;
width: 100%;
text-align: right;
padding: 10px;
}
.dialog-error {
margin: 10px;
color: red;
}
.dialog input {
margin: 10px;
font-size: inherit;
}
.dialog button {
margin-left: 10px;
font-size: inherit;
}
#save-dialog, #delete-dialog {
width: 50%;
max-width: 20em;
}
#delete-prompt {
padding: 10px;
}
#content {
overflow-y: scroll;
padding: 1em;
}
#top {
overflow-y: scroll;
}
#graph {
overflow: hidden;
}
#graph svg {
width: 100%;
height: auto;
padding: 10px;
}
#content.source .filename {
margin-top: 0;
margin-bottom: 1em;
font-size: 120%;
}
#content.source pre {
margin-bottom: 3em;
}
table {
border-spacing: 0px;
width: 100%;
padding-bottom: 1em;
white-space: nowrap;
}
table thead {
font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
table tr th {
position: sticky;
top: 0;
background-color: #ddd;
text-align: right;
padding: .3em .5em;
}
table tr td {
padding: .3em .5em;
text-align: right;
}
#top table tr th:nth-child(6),
#top table tr th:nth-child(7),
#top table tr td:nth-child(6),
#top table tr td:nth-child(7) {
text-align: left;
}
#top table tr td:nth-child(6) {
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
cursor: ns-resize;
}
.hilite {
background-color: #ebf5fb;
font-weight: bold;
}
/* stacking order */
.boxtext { z-index: 2; } /* flame graph box text */
#current-details { z-index: 2; } /* flame graph current box info */
#detailsbox { z-index: 3; } /* profile details */
.submenu { z-index: 4; }
.dialog { z-index: 5; }

View File

@@ -0,0 +1,714 @@
// Make svg pannable and zoomable.
// Call clickHandler(t) if a click event is caught by the pan event handlers.
function initPanAndZoom(svg, clickHandler) {
'use strict';
// Current mouse/touch handling mode
const IDLE = 0;
const MOUSEPAN = 1;
const TOUCHPAN = 2;
const TOUCHZOOM = 3;
let mode = IDLE;
// State needed to implement zooming.
let currentScale = 1.0;
const initWidth = svg.viewBox.baseVal.width;
const initHeight = svg.viewBox.baseVal.height;
// State needed to implement panning.
let panLastX = 0; // Last event X coordinate
let panLastY = 0; // Last event Y coordinate
let moved = false; // Have we seen significant movement
let touchid = null; // Current touch identifier
// State needed for pinch zooming
let touchid2 = null; // Second id for pinch zooming
let initGap = 1.0; // Starting gap between two touches
let initScale = 1.0; // currentScale when pinch zoom started
let centerPoint = null; // Center point for scaling
// Convert event coordinates to svg coordinates.
function toSvg(x, y) {
const p = svg.createSVGPoint();
p.x = x;
p.y = y;
let m = svg.getCTM();
if (m == null) m = svg.getScreenCTM(); // Firefox workaround.
return p.matrixTransform(m.inverse());
}
// Change the scaling for the svg to s, keeping the point denoted
// by u (in svg coordinates]) fixed at the same screen location.
function rescale(s, u) {
// Limit to a good range.
if (s < 0.2) s = 0.2;
if (s > 10.0) s = 10.0;
currentScale = s;
// svg.viewBox defines the visible portion of the user coordinate
// system. So to magnify by s, divide the visible portion by s,
// which will then be stretched to fit the viewport.
const vb = svg.viewBox;
const w1 = vb.baseVal.width;
const w2 = initWidth / s;
const h1 = vb.baseVal.height;
const h2 = initHeight / s;
vb.baseVal.width = w2;
vb.baseVal.height = h2;
// We also want to adjust vb.baseVal.x so that u.x remains at same
// screen X coordinate. In other words, want to change it from x1 to x2
// so that:
// (u.x - x1) / w1 = (u.x - x2) / w2
// Simplifying that, we get
// (u.x - x1) * (w2 / w1) = u.x - x2
// x2 = u.x - (u.x - x1) * (w2 / w1)
vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1);
vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1);
}
function handleWheel(e) {
if (e.deltaY == 0) return;
// Change scale factor by 1.1 or 1/1.1
rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
toSvg(e.offsetX, e.offsetY));
}
function setMode(m) {
mode = m;
touchid = null;
touchid2 = null;
}
function panStart(x, y) {
moved = false;
panLastX = x;
panLastY = y;
}
function panMove(x, y) {
let dx = x - panLastX;
let dy = y - panLastY;
if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return; // Ignore tiny moves
moved = true;
panLastX = x;
panLastY = y;
// Firefox workaround: get dimensions from parentNode.
const swidth = svg.clientWidth || svg.parentNode.clientWidth;
const sheight = svg.clientHeight || svg.parentNode.clientHeight;
// Convert deltas from screen space to svg space.
dx *= (svg.viewBox.baseVal.width / swidth);
dy *= (svg.viewBox.baseVal.height / sheight);
svg.viewBox.baseVal.x -= dx;
svg.viewBox.baseVal.y -= dy;
}
function handleScanStart(e) {
if (e.button != 0) return; // Do not catch right-clicks etc.
setMode(MOUSEPAN);
panStart(e.clientX, e.clientY);
e.preventDefault();
svg.addEventListener('mousemove', handleScanMove);
}
function handleScanMove(e) {
if (e.buttons == 0) {
// Missed an end event, perhaps because mouse moved outside window.
setMode(IDLE);
svg.removeEventListener('mousemove', handleScanMove);
return;
}
if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
}
function handleScanEnd(e) {
if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
setMode(IDLE);
svg.removeEventListener('mousemove', handleScanMove);
if (!moved) clickHandler(e.target);
}
// Find touch object with specified identifier.
function findTouch(tlist, id) {
for (const t of tlist) {
if (t.identifier == id) return t;
}
return null;
}
// Return distance between two touch points
function touchGap(t1, t2) {
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.hypot(dx, dy);
}
function handleTouchStart(e) {
if (mode == IDLE && e.changedTouches.length == 1) {
// Start touch based panning
const t = e.changedTouches[0];
setMode(TOUCHPAN);
touchid = t.identifier;
panStart(t.clientX, t.clientY);
e.preventDefault();
} else if (mode == TOUCHPAN && e.touches.length == 2) {
// Start pinch zooming
setMode(TOUCHZOOM);
const t1 = e.touches[0];
const t2 = e.touches[1];
touchid = t1.identifier;
touchid2 = t2.identifier;
initScale = currentScale;
initGap = touchGap(t1, t2);
centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
(t1.clientY + t2.clientY) / 2);
e.preventDefault();
}
}
function handleTouchMove(e) {
if (mode == TOUCHPAN) {
const t = findTouch(e.changedTouches, touchid);
if (t == null) return;
if (e.touches.length != 1) {
setMode(IDLE);
return;
}
panMove(t.clientX, t.clientY);
e.preventDefault();
} else if (mode == TOUCHZOOM) {
// Get two touches; new gap; rescale to ratio.
const t1 = findTouch(e.touches, touchid);
const t2 = findTouch(e.touches, touchid2);
if (t1 == null || t2 == null) return;
const gap = touchGap(t1, t2);
rescale(initScale * gap / initGap, centerPoint);
e.preventDefault();
}
}
function handleTouchEnd(e) {
if (mode == TOUCHPAN) {
const t = findTouch(e.changedTouches, touchid);
if (t == null) return;
panMove(t.clientX, t.clientY);
setMode(IDLE);
e.preventDefault();
if (!moved) clickHandler(t.target);
} else if (mode == TOUCHZOOM) {
setMode(IDLE);
e.preventDefault();
}
}
svg.addEventListener('mousedown', handleScanStart);
svg.addEventListener('mouseup', handleScanEnd);
svg.addEventListener('touchstart', handleTouchStart);
svg.addEventListener('touchmove', handleTouchMove);
svg.addEventListener('touchend', handleTouchEnd);
svg.addEventListener('wheel', handleWheel, true);
}
function initMenus() {
'use strict';
let activeMenu = null;
let activeMenuHdr = null;
function cancelActiveMenu() {
if (activeMenu == null) return;
activeMenu.style.display = 'none';
activeMenu = null;
activeMenuHdr = null;
}
// Set click handlers on every menu header.
for (const menu of document.getElementsByClassName('submenu')) {
const hdr = menu.parentElement;
if (hdr == null) return;
if (hdr.classList.contains('disabled')) return;
function showMenu(e) {
// menu is a child of hdr, so this event can fire for clicks
// inside menu. Ignore such clicks.
if (e.target.parentElement != hdr) return;
activeMenu = menu;
activeMenuHdr = hdr;
menu.style.display = 'block';
}
hdr.addEventListener('mousedown', showMenu);
hdr.addEventListener('touchstart', showMenu);
}
// If there is an active menu and a down event outside, retract the menu.
for (const t of ['mousedown', 'touchstart']) {
document.addEventListener(t, (e) => {
// Note: to avoid unnecessary flicker, if the down event is inside
// the active menu header, do not retract the menu.
if (activeMenuHdr != e.target.closest('.menu-item')) {
cancelActiveMenu();
}
}, { passive: true, capture: true });
}
// If there is an active menu and an up event inside, retract the menu.
document.addEventListener('mouseup', (e) => {
if (activeMenu == e.target.closest('.submenu')) {
cancelActiveMenu();
}
}, { passive: true, capture: true });
}
function sendURL(method, url, done) {
fetch(url.toString(), {method: method})
.then((response) => { done(response.ok); })
.catch((error) => { done(false); });
}
// Initialize handlers for saving/loading configurations.
function initConfigManager() {
'use strict';
// Initialize various elements.
function elem(id) {
const result = document.getElementById(id);
if (!result) console.warn('element ' + id + ' not found');
return result;
}
const overlay = elem('dialog-overlay');
const saveDialog = elem('save-dialog');
const saveInput = elem('save-name');
const saveError = elem('save-error');
const delDialog = elem('delete-dialog');
const delPrompt = elem('delete-prompt');
const delError = elem('delete-error');
let currentDialog = null;
let currentDeleteTarget = null;
function showDialog(dialog) {
if (currentDialog != null) {
overlay.style.display = 'none';
currentDialog.style.display = 'none';
}
currentDialog = dialog;
if (dialog != null) {
overlay.style.display = 'block';
dialog.style.display = 'block';
}
}
function cancelDialog(e) {
showDialog(null);
}
// Show dialog for saving the current config.
function showSaveDialog(e) {
saveError.innerText = '';
showDialog(saveDialog);
saveInput.focus();
}
// Commit save config.
function commitSave(e) {
const name = saveInput.value;
const url = new URL(document.URL);
// Set path relative to existing path.
url.pathname = new URL('./saveconfig', document.URL).pathname;
url.searchParams.set('config', name);
saveError.innerText = '';
sendURL('POST', url, (ok) => {
if (!ok) {
saveError.innerText = 'Save failed';
} else {
showDialog(null);
location.reload(); // Reload to show updated config menu
}
});
}
function handleSaveInputKey(e) {
if (e.key === 'Enter') commitSave(e);
}
function deleteConfig(e, elem) {
e.preventDefault();
const config = elem.dataset.config;
delPrompt.innerText = 'Delete ' + config + '?';
currentDeleteTarget = elem;
showDialog(delDialog);
}
function commitDelete(e, elem) {
if (!currentDeleteTarget) return;
const config = currentDeleteTarget.dataset.config;
const url = new URL('./deleteconfig', document.URL);
url.searchParams.set('config', config);
delError.innerText = '';
sendURL('DELETE', url, (ok) => {
if (!ok) {
delError.innerText = 'Delete failed';
return;
}
showDialog(null);
// Remove menu entry for this config.
if (currentDeleteTarget && currentDeleteTarget.parentElement) {
currentDeleteTarget.parentElement.remove();
}
});
}
// Bind event on elem to fn.
function bind(event, elem, fn) {
if (elem == null) return;
elem.addEventListener(event, fn);
if (event == 'click') {
// Also enable via touch.
elem.addEventListener('touchstart', fn);
}
}
bind('click', elem('save-config'), showSaveDialog);
bind('click', elem('save-cancel'), cancelDialog);
bind('click', elem('save-confirm'), commitSave);
bind('keydown', saveInput, handleSaveInputKey);
bind('click', elem('delete-cancel'), cancelDialog);
bind('click', elem('delete-confirm'), commitDelete);
// Activate deletion button for all config entries in menu.
for (const del of Array.from(document.getElementsByClassName('menu-delete-btn'))) {
bind('click', del, (e) => {
deleteConfig(e, del);
});
}
}
// options if present can contain:
// hiliter: function(Number, Boolean): Boolean
// Overridable mechanism for highlighting/unhighlighting specified node.
// current: function() Map[Number,Boolean]
// Overridable mechanism for fetching set of currently selected nodes.
function viewer(baseUrl, nodes, options) {
'use strict';
// Elements
const search = document.getElementById('search');
const graph0 = document.getElementById('graph0');
const svg = (graph0 == null ? null : graph0.parentElement);
const toptable = document.getElementById('toptable');
let regexpActive = false;
let selected = new Map();
let origFill = new Map();
let searchAlarm = null;
let buttonsEnabled = true;
// Return current selection.
function getSelection() {
if (selected.size > 0) {
return selected;
} else if (options && options.current) {
return options.current();
}
return new Map();
}
function handleDetails(e) {
e.preventDefault();
const detailsText = document.getElementById('detailsbox');
if (detailsText != null) {
if (detailsText.style.display === 'block') {
detailsText.style.display = 'none';
} else {
detailsText.style.display = 'block';
}
}
}
function handleKey(e) {
if (e.keyCode != 13) return;
setHrefParams(window.location, function (params) {
params.set('f', search.value);
});
e.preventDefault();
}
function handleSearch() {
// Delay expensive processing so a flurry of key strokes is handled once.
if (searchAlarm != null) {
clearTimeout(searchAlarm);
}
searchAlarm = setTimeout(selectMatching, 300);
regexpActive = true;
updateButtons();
}
function selectMatching() {
searchAlarm = null;
let re = null;
if (search.value != '') {
try {
re = new RegExp(search.value);
} catch (e) {
// TODO: Display error state in search box
return;
}
}
function match(text) {
return re != null && re.test(text);
}
// drop currently selected items that do not match re.
selected.forEach(function(v, n) {
if (!match(nodes[n])) {
unselect(n);
}
})
// add matching items that are not currently selected.
if (nodes) {
for (let n = 0; n < nodes.length; n++) {
if (!selected.has(n) && match(nodes[n])) {
select(n);
}
}
}
updateButtons();
}
function toggleSvgSelect(elem) {
// Walk up to immediate child of graph0
while (elem != null && elem.parentElement != graph0) {
elem = elem.parentElement;
}
if (!elem) return;
// Disable regexp mode.
regexpActive = false;
const n = nodeId(elem);
if (n < 0) return;
if (selected.has(n)) {
unselect(n);
} else {
select(n);
}
updateButtons();
}
function unselect(n) {
if (setNodeHighlight(n, false)) selected.delete(n);
}
function select(n, elem) {
if (setNodeHighlight(n, true)) selected.set(n, true);
}
function nodeId(elem) {
const id = elem.id;
if (!id) return -1;
if (!id.startsWith('node')) return -1;
const n = parseInt(id.slice(4), 10);
if (isNaN(n)) return -1;
if (n < 0 || n >= nodes.length) return -1;
return n;
}
// Change highlighting of node (returns true if node was found).
function setNodeHighlight(n, set) {
if (options && options.hiliter) return options.hiliter(n, set);
const elem = document.getElementById('node' + n);
if (!elem) return false;
// Handle table row highlighting.
if (elem.nodeName == 'TR') {
elem.classList.toggle('hilite', set);
return true;
}
// Handle svg element highlighting.
const p = findPolygon(elem);
if (p != null) {
if (set) {
origFill.set(p, p.style.fill);
p.style.fill = '#ccccff';
} else if (origFill.has(p)) {
p.style.fill = origFill.get(p);
}
}
return true;
}
function findPolygon(elem) {
if (elem.localName == 'polygon') return elem;
for (const c of elem.children) {
const p = findPolygon(c);
if (p != null) return p;
}
return null;
}
function setSampleIndexLink(si) {
const elem = document.getElementById('sampletype-' + si);
if (elem != null) {
setHrefParams(elem, function (params) {
params.set("si", si);
});
}
}
// Update id's href to reflect current selection whenever it is
// liable to be followed.
function makeSearchLinkDynamic(id) {
const elem = document.getElementById(id);
if (elem == null) return;
// Most links copy current selection into the 'f' parameter,
// but Refine menu links are different.
let param = 'f';
if (id == 'ignore') param = 'i';
if (id == 'hide') param = 'h';
if (id == 'show') param = 's';
if (id == 'show-from') param = 'sf';
// We update on mouseenter so middle-click/right-click work properly.
elem.addEventListener('mouseenter', updater);
elem.addEventListener('touchstart', updater);
function updater() {
// The selection can be in one of two modes: regexp-based or
// list-based. Construct regular expression depending on mode.
let re = regexpActive
? search.value
: Array.from(getSelection().keys()).map(key => pprofQuoteMeta(nodes[key])).join('|');
setHrefParams(elem, function (params) {
if (re != '') {
// For focus/show/show-from, forget old parameter. For others, add to re.
if (param != 'f' && param != 's' && param != 'sf' && params.has(param)) {
const old = params.get(param);
if (old != '') {
re += '|' + old;
}
}
params.set(param, re);
} else {
params.delete(param);
}
});
}
}
function setHrefParams(elem, paramSetter) {
let url = new URL(elem.href);
url.hash = '';
// Copy params from this page's URL.
const params = url.searchParams;
for (const p of new URLSearchParams(window.location.search)) {
params.set(p[0], p[1]);
}
// Give the params to the setter to modify.
paramSetter(params);
elem.href = url.toString();
}
function handleTopClick(e) {
// Walk back until we find TR and then get the Name column (index 5)
let elem = e.target;
while (elem != null && elem.nodeName != 'TR') {
elem = elem.parentElement;
}
if (elem == null || elem.children.length < 6) return;
e.preventDefault();
const tr = elem;
const td = elem.children[5];
if (td.nodeName != 'TD') return;
const name = td.innerText;
const index = nodes.indexOf(name);
if (index < 0) return;
// Disable regexp mode.
regexpActive = false;
if (selected.has(index)) {
unselect(index, elem);
} else {
select(index, elem);
}
updateButtons();
}
function updateButtons() {
const enable = (search.value != '' || getSelection().size != 0);
if (buttonsEnabled == enable) return;
buttonsEnabled = enable;
for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) {
const link = document.getElementById(id);
if (link != null) {
link.classList.toggle('disabled', !enable);
}
}
}
// Initialize button states
updateButtons();
// Setup event handlers
initMenus();
if (svg != null) {
initPanAndZoom(svg, toggleSvgSelect);
}
if (toptable != null) {
toptable.addEventListener('mousedown', handleTopClick);
toptable.addEventListener('touchstart', handleTopClick);
}
const ids = ['topbtn', 'graphbtn',
'flamegraph',
'peek', 'list',
'disasm', 'focus', 'ignore', 'hide', 'show', 'show-from'];
ids.forEach(makeSearchLinkDynamic);
const sampleIDs = [{{range .SampleTypes}}'{{.}}', {{end}}];
sampleIDs.forEach(setSampleIndexLink);
// Bind action to button with specified id.
function addAction(id, action) {
const btn = document.getElementById(id);
if (btn != null) {
btn.addEventListener('click', action);
btn.addEventListener('touchstart', action);
}
}
addAction('details', handleDetails);
initConfigManager();
search.addEventListener('input', handleSearch);
search.addEventListener('keydown', handleKey);
// Give initial focus to main container so it can be scrolled using keys.
const main = document.getElementById('bodycontainer');
if (main) {
main.focus();
}
}
// convert a string to a regexp that matches exactly that string.
function pprofQuoteMeta(str) {
return '^' + str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1') + '$';
}

View File

@@ -0,0 +1,7 @@
#graph {
cursor: grab;
}
#graph:active {
cursor: grabbing;
}

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
{{template "graph_css" .}}
</head>
<body>
{{template "header" .}}
<div id="graph">
{{.HTMLBody}}
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
</body>
</html>

View File

@@ -0,0 +1,119 @@
<div class="header">
<div class="title">
<h1><a href="./">pprof</a></h1>
</div>
<div id="view" class="menu-item">
<div class="menu-name">
View
<i class="downArrow"></i>
</div>
<div class="submenu">
<a title="{{.Help.top}}" href="./top" id="topbtn">Top</a>
<a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a>
<a title="{{.Help.flamegraph}}" href="./flamegraph" id="flamegraph">Flame Graph</a>
<a title="{{.Help.peek}}" href="./peek" id="peek">Peek</a>
<a title="{{.Help.list}}" href="./source" id="list">Source</a>
<a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a>
</div>
</div>
{{$sampleLen := len .SampleTypes}}
{{if gt $sampleLen 1}}
<div id="sample" class="menu-item">
<div class="menu-name">
Sample
<i class="downArrow"></i>
</div>
<div class="submenu">
{{range .SampleTypes}}
<a href="?si={{.}}" id="sampletype-{{.}}">{{.}}</a>
{{end}}
</div>
</div>
{{end}}
<div id="refine" class="menu-item">
<div class="menu-name">
Refine
<i class="downArrow"></i>
</div>
<div class="submenu">
<a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
<a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
<a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
<a title="{{.Help.show}}" href="?" id="show">Show</a>
<a title="{{.Help.show_from}}" href="?" id="show-from">Show from</a>
<hr>
<a title="{{.Help.reset}}" href="?">Reset</a>
</div>
</div>
<div id="config" class="menu-item">
<div class="menu-name">
Config
<i class="downArrow"></i>
</div>
<div class="submenu">
<a title="{{.Help.save_config}}" id="save-config">Save as ...</a>
<hr>
{{range .Configs}}
<a href="{{.URL}}">
{{if .Current}}<span class="menu-check-mark"></span>{{end}}
{{.Name}}
{{if .UserConfig}}<span class="menu-delete-btn" data-config={{.Name}}>🗙</span>{{end}}
</a>
{{end}}
</div>
</div>
<div id="download" class="menu-item">
<div class="menu-name">
<a href="./download">Download</a>
</div>
</div>
<div>
<input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
</div>
<div class="description">
<a title="{{.Help.details}}" href="#" id="details">{{.Title}}</a>
<div id="detailsbox">
{{range .Legend}}<div>{{.}}</div>{{end}}
</div>
</div>
{{if .DocURL}}
<div class="menu-item">
<div class="help menu-name"><a title="Profile documentation" href="{{.DocURL}}" target="_blank">Help&nbsp;</a></div>
</div>
{{end}}
</div>
<div id="dialog-overlay"></div>
<div class="dialog" id="save-dialog">
<div class="dialog-header">Save options as</div>
<datalist id="config-list">
{{range .Configs}}{{if .UserConfig}}<option value="{{.Name}}" />{{end}}{{end}}
</datalist>
<input id="save-name" type="text" list="config-list" placeholder="New config" />
<div class="dialog-footer">
<span class="dialog-error" id="save-error"></span>
<button id="save-cancel">Cancel</button>
<button id="save-confirm">Save</button>
</div>
</div>
<div class="dialog" id="delete-dialog">
<div class="dialog-header" id="delete-dialog-title">Delete config</div>
<div id="delete-prompt"></div>
<div class="dialog-footer">
<span class="dialog-error" id="delete-error"></span>
<button id="delete-cancel">Cancel</button>
<button id="delete-confirm">Delete</button>
</div>
</div>
<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
</head>
<body>
{{template "header" .}}
<div id="content">
<pre>
{{.TextBody}}
</pre>
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), null);</script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{if not .Standalone}}{{template "css" .}}{{end}}
{{template "weblistcss" .}}
{{template "weblistjs" .}}
</head>
<body>{{"\n" -}}
{{/* emit different header in standalone mode */ -}}
{{if .Standalone}}{{"\n" -}}
<div class="legend">{{"" -}}
{{range $i, $e := .Legend -}}
{{if $i}}<br>{{"\n"}}{{end}}{{. -}}
{{end}}<br>Total: {{.Listing.Total -}}
</div>{{"" -}}
{{else -}}
{{template "header" .}}
<div id="content" class="source">{{"" -}}
{{end -}}
{{range .Listing.Files -}}
{{range .Funcs -}}
<h2>{{.Name}}</h2>{{"" -}}
<p class="filename">{{.File}}</p>{{"\n" -}}
<pre onClick="pprof_toggle_asm(event)">{{"\n" -}}
{{printf " Total: %10s %10s (flat, cum) %s" .Flat .Cumulative .Percent -}}
{{range .Lines -}}{{"\n" -}}
{{/* source line */ -}}
<span class=line>{{printf " %6d" .Line}}</span>{{" " -}}
<span class={{.HTMLClass}}>
{{- printf " %10s %10s %8s %s " .Flat .Cumulative "" .SrcLine -}}
</span>{{"" -}}
{{if .Instructions -}}
{{/* instructions for this source line */ -}}
<span class=asm>{{"" -}}
{{range .Instructions -}}
{{/* separate when we hit a new basic block */ -}}
{{if .NewBlock -}}{{printf " %8s %28s\n" "" "⋮"}}{{end -}}
{{/* inlined calls leading to this instruction */ -}}
{{range .InlinedCalls -}}
{{printf " %8s %10s %10s %8s " "" "" "" "" -}}
<span class=inlinesrc>{{.SrcLine}}</span>{{" " -}}
<span class=unimportant>{{.FileBase}}:{{.Line}}</span>{{"\n" -}}
{{end -}}
{{if not .Synthetic -}}
{{/* disassembled instruction */ -}}
{{printf " %8s %10s %10s %8x: %s " "" .Flat .Cumulative .Address .Disasm -}}
<span class=unimportant>{{.FileLine}}</span>{{"\n" -}}
{{end -}}
{{end -}}
</span>{{"" -}}
{{end -}}
{{/* end of line */ -}}
{{end}}{{"\n" -}}
</pre>{{"\n" -}}
{{/* end of function */ -}}
{{end -}}
{{/* end of file */ -}}
{{end -}}
{{if not .Standalone}}{{"\n " -}}
</div>{{"\n" -}}
{{template "script" .}}{{"\n" -}}
<script>viewer(new URL(window.location.href), null);</script>{{"" -}}
{{end}}
</body>
</html>

View File

@@ -0,0 +1,89 @@
body {
overflow: hidden; /* Want scrollbar not here, but in #stack-holder */
}
/* Scrollable container for flame graph */
#stack-holder {
width: 100%;
flex-grow: 1;
overflow-y: auto;
background: #eee; /* Light grey gives better contrast with boxes */
position: relative; /* Allows absolute positioning of child boxes */
}
/* Flame graph */
#stack-chart {
width: 100%;
position: relative; /* Allows absolute positioning of child boxes */
}
/* Holder for current frame details. */
#current-details {
position: relative;
background: #eee; /* Light grey gives better contrast with boxes */
font-size: 12pt;
padding: 0 4px;
width: 100%;
}
/* Shows details of frame that is under the mouse */
#current-details-left {
float: left;
max-width: 60%;
white-space: nowrap;
overflow-x: hidden;
}
#current-details-right {
float: right;
max-width: 40%;
white-space: nowrap;
overflow-x: hidden;
}
/* Background of a single flame-graph frame */
.boxbg {
border-width: 0px;
position: absolute;
overflow: hidden;
box-sizing: border-box;
background: #d8d8d8;
}
.positive { position: absolute; background: #caa; }
.negative { position: absolute; background: #aca; }
/* Not-inlined frames are visually separated from their caller. */
.not-inlined {
border-top: 1px solid black;
}
/* Function name */
.boxtext {
position: absolute;
width: 100%;
padding-left: 2px;
line-height: 18px;
cursor: default;
font-family: "Google Sans", Arial, sans-serif;
font-size: 12pt;
z-index: 2;
}
/* Box highlighting via shadows to avoid size changes */
.hilite { box-shadow: 0px 0px 0px 2px #000; z-index: 1; }
.hilite2 { box-shadow: 0px 0px 0px 2px #000; z-index: 1; }
/* Gap left between callers and callees */
.separator {
position: absolute;
text-align: center;
font-size: 12pt;
font-weight: bold;
}
/* Right-click menu */
#action-menu {
max-width: 15em;
}
/* Right-click menu title */
#action-title {
display: block;
padding: 0.5em 1em;
background: #888;
text-overflow: ellipsis;
overflow: hidden;
}
/* Internal canvas used to measure text size when picking fonts */
#textsizer {
position: absolute;
bottom: -100px;
}

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
{{template "stacks_css"}}
</head>
<body>
{{template "header" .}}
<div id="current-details">
<div id="current-details-left"></div>
<div id="current-details-right"> </div>
</div>
<div id="stack-holder">
<div id="stack-chart"></div>
</div>
<div id="action-menu" class="submenu">
<span id="action-title"></span>
<hr>
<a title="{{.Help.list}}" id="action-source" href="./source">Show source code</a>
<a title="{{.Help.list}}" id="action-source-tab" href="./source" target="_blank">Show source in new tab</a>
<hr>
<a title="{{.Help.focus}}" id="action-focus" href="?">Focus</a>
<a title="{{.Help.ignore}}" id="action-ignore" href="?">Ignore</a>
<a title="{{.Help.hide}}" id="action-hide" href="?">Hide</a>
<a title="{{.Help.show_from}}" id="action-showfrom" href="?">Show from</a>
</div>
{{template "script" .}}
{{template "stacks_js"}}
<script>
pprofUnitDefs = {{.UnitDefs}};
stackViewer({{.Stacks}}, {{.Nodes}});
</script>
</body>
</html>

View File

@@ -0,0 +1,638 @@
// stackViewer displays a flame-graph like view (extended to show callers).
// stacks - report.StackSet
// nodes - List of names for each source in report.StackSet
function stackViewer(stacks, nodes) {
'use strict';
// Constants used in rendering.
const ROW = 20;
const PADDING = 2;
const MIN_WIDTH = 4;
const MIN_TEXT_WIDTH = 16;
const TEXT_MARGIN = 2;
const FONT_SIZE = 12;
const MIN_FONT_SIZE = 8;
// Fields
let pivots = []; // Indices of currently selected data.Sources entries.
let matches = new Set(); // Indices of sources that match search
let elems = new Map(); // Mapping from source index to display elements
let displayList = []; // List of boxes to display.
let actionMenuOn = false; // Is action menu visible?
let actionTarget = null; // Box on which action menu is operating.
let diff = false; // Are we displaying a diff?
let shown = 0; // How many profile values are being displayed?
for (const stack of stacks.Stacks) {
if (stack.Value < 0) {
diff = true;
break;
}
}
// Setup to allow measuring text width.
const textSizer = document.createElement('canvas');
textSizer.id = 'textsizer';
const textContext = textSizer.getContext('2d');
// Get DOM elements.
const chart = find('stack-chart');
const search = find('search');
const actions = find('action-menu');
const actionTitle = find('action-title');
const leftDetailBox = find('current-details-left');
const rightDetailBox = find('current-details-right');
window.addEventListener('resize', render);
window.addEventListener('popstate', render);
search.addEventListener('keydown', handleSearchKey);
// Withdraw action menu when clicking outside, or when item selected.
document.addEventListener('mousedown', (e) => {
if (!actions.contains(e.target)) {
hideActionMenu();
}
});
actions.addEventListener('click', hideActionMenu);
// Initialize menus and other general UI elements.
viewer(new URL(window.location.href), nodes, {
hiliter: (n, on) => { return hilite(n, on); },
current: () => {
let r = new Map();
if (pivots.length == 1 && pivots[0] == 0) {
// Not pivoting
} else {
for (let p of pivots) {
r.set(p, true);
}
}
return r;
}});
render();
clearDetails();
// Helper functions follow:
// hilite changes the highlighting of elements corresponding to specified src.
function hilite(src, on) {
if (on) {
matches.add(src);
} else {
matches.delete(src);
}
toggleClass(src, 'hilite', on);
return true;
}
// Display action menu (triggered by right-click on a frame)
function showActionMenu(e, box) {
if (box.src == 0) return; // No action menu for root
e.preventDefault(); // Disable browser context menu
const src = stacks.Sources[box.src];
actionTitle.innerText = src.Display[src.Display.length-1];
const menu = actions;
menu.style.display = 'block';
// Compute position so menu stays visible and near the mouse.
const x = Math.min(e.clientX - 10, document.body.clientWidth - menu.clientWidth);
const y = Math.min(e.clientY - 10, document.body.clientHeight - menu.clientHeight);
menu.style.left = x + 'px';
menu.style.top = y + 'px';
// Set menu links to operate on clicked box.
setHrefParam('action-source', 'f', box.src);
setHrefParam('action-source-tab', 'f', box.src);
setHrefParam('action-focus', 'f', box.src);
setHrefParam('action-ignore', 'i', box.src);
setHrefParam('action-hide', 'h', box.src);
setHrefParam('action-showfrom', 'sf', box.src);
toggleClass(box.src, 'hilite2', true);
actionTarget = box;
actionMenuOn = true;
}
function hideActionMenu() {
actions.style.display = 'none';
actionMenuOn = false;
if (actionTarget != null) {
toggleClass(actionTarget.src, 'hilite2', false);
}
}
// setHrefParam updates the specified parameter in the href of an <a>
// element to make it operate on the specified src.
function setHrefParam(id, param, src) {
const elem = document.getElementById(id);
if (!elem) return;
let url = new URL(elem.href);
url.hash = '';
// Copy params from this page's URL.
const params = url.searchParams;
for (const p of new URLSearchParams(window.location.search)) {
params.set(p[0], p[1]);
}
// Update params to include src.
// When `pprof` is invoked with `-lines`, FullName will be suffixed with `:<line>`,
// which we need to remove.
let v = pprofQuoteMeta(stacks.Sources[src].FullName.replace(/:[0-9]+$/, ''));
if (param != 'f' && param != 'sf') { // old f,sf values are overwritten
// Add new source to current parameter value.
const old = params.get(param);
if (old && old != '') {
v += '|' + old;
}
}
params.set(param, v);
elem.href = url.toString();
}
// Capture Enter key in the search box to make it pivot instead of focus.
function handleSearchKey(e) {
if (e.key != 'Enter') return;
e.stopImmediatePropagation(); // Disable normal enter key handling
const val = search.value;
try {
new RegExp(search.value);
} catch (error) {
return; // TODO: Display error state in search box
}
switchPivots(val);
}
function switchPivots(regexp) {
// Switch URL without hitting the server.
const url = new URL(document.URL);
if (regexp === '' || regexp === '^$') {
url.searchParams.delete('p'); // Not pivoting
} else {
url.searchParams.set('p', regexp);
}
history.pushState('', '', url.toString()); // Makes back-button work
matches = new Set();
search.value = '';
render();
}
function handleEnter(box, div) {
if (actionMenuOn) return;
const src = stacks.Sources[box.src];
div.title = details(box) + ' │ ' + src.FullName + (src.Inlined ? "\n(inlined)" : "");
leftDetailBox.innerText = src.FullName + (src.Inlined ? " (inlined)" : "");
let timing = summary(box.sumpos, box.sumneg);
if (box.self != 0) {
timing = "self " + unitText(box.self) + " │ " + timing;
}
rightDetailBox.innerText = timing;
// Highlight all boxes that have the same source as box.
toggleClass(box.src, 'hilite2', true);
}
function handleLeave(box) {
if (actionMenuOn) return;
clearDetails();
toggleClass(box.src, 'hilite2', false);
}
function clearDetails() {
leftDetailBox.innerText = '';
rightDetailBox.innerText = percentText(shown);
}
// Return list of sources that match the regexp given by the 'p' URL parameter.
function urlPivots() {
const pivots = [];
const params = (new URL(document.URL)).searchParams;
const val = params.get('p');
if (val !== null && val != '') {
try {
const re = new RegExp(val);
for (let i = 0; i < stacks.Sources.length; i++) {
const src = stacks.Sources[i];
if (re.test(src.UniqueName) || re.test(src.FileName)) {
pivots.push(i);
}
}
} catch (error) {}
}
if (pivots.length == 0) {
pivots.push(0);
}
return pivots;
}
// render re-generates the stack display.
function render() {
pivots = urlPivots();
// Get places where pivots occur.
let places = [];
for (let pivot of pivots) {
const src = stacks.Sources[pivot];
for (let p of src.Places) {
places.push(p);
}
}
const width = chart.clientWidth;
elems.clear();
actionTarget = null;
const [pos, neg] = totalValue(places);
const total = pos + neg;
const xscale = (width-2*PADDING) / total; // Converts from profile value to X pixels
const x = PADDING;
const y = 0;
// Show summary for pivots if we are actually pivoting.
const showPivotSummary = !(pivots.length == 1 && pivots[0] == 0);
shown = pos + neg;
displayList.length = 0;
renderStacks(0, xscale, x, y, places, +1); // Callees
renderStacks(0, xscale, x, y-ROW, places, -1); // Callers (ROW left for separator)
display(xscale, pos, neg, displayList, showPivotSummary);
}
// renderStacks creates boxes with top-left at x,y with children drawn as
// nested stacks (below or above based on the sign of direction).
// Returns the largest y coordinate filled.
function renderStacks(depth, xscale, x, y, places, direction) {
// Example: suppose we are drawing the following stacks:
// a->b->c
// a->b->d
// a->e->f
// After rendering a, we will call renderStacks, with places pointing to
// the preceding stacks.
//
// We first group all places with the same leading entry. In this example
// we get [b->c, b->d] and [e->f]. We render the two groups side-by-side.
const groups = partitionPlaces(places);
for (const g of groups) {
renderGroup(depth, xscale, x, y, g, direction);
x += groupWidth(xscale, g);
}
}
// Some of the types used below:
//
// // Group represents a displayed (sub)tree.
// interface Group {
// name: string; // Full name of source
// src: number; // Index in stacks.Sources
// self: number; // Contribution as leaf (may be < 0 for diffs)
// sumpos: number; // Sum of |self| of positive nodes in tree (>= 0)
// sumneg: number; // Sum of |self| of negative nodes in tree (>= 0)
// places: Place[]; // Stack slots that contributed to this group
// }
//
// // Box is a rendered item.
// interface Box {
// x: number; // X coordinate of top-left
// y: number; // Y coordinate of top-left
// width: number; // Width of box to display
// src: number; // Index in stacks.Sources
// sumpos: number; // From corresponding Group
// sumneg: number; // From corresponding Group
// self: number; // From corresponding Group
// };
function groupWidth(xscale, g) {
return xscale * (g.sumpos + g.sumneg);
}
function renderGroup(depth, xscale, x, y, g, direction) {
// Skip if not wide enough.
const width = groupWidth(xscale, g);
if (width < MIN_WIDTH) return;
// Draw the box for g.src (except for selected element in upwards direction
// since that duplicates the box we added in downwards direction).
if (depth != 0 || direction > 0) {
const box = {
x: x,
y: y,
width: width,
src: g.src,
sumpos: g.sumpos,
sumneg: g.sumneg,
self: g.self,
};
displayList.push(box);
if (direction > 0) {
// Leave gap on left hand side to indicate self contribution.
x += xscale*Math.abs(g.self);
}
}
y += direction * ROW;
// Find child or parent stacks.
const next = [];
for (const place of g.places) {
const stack = stacks.Stacks[place.Stack];
const nextSlot = place.Pos + direction;
if (nextSlot >= 0 && nextSlot < stack.Sources.length) {
next.push({Stack: place.Stack, Pos: nextSlot});
}
}
renderStacks(depth+1, xscale, x, y, next, direction);
}
// partitionPlaces partitions a set of places into groups where each group
// contains places with the same source. If a stack occurs multiple times
// in places, only the outer-most occurrence is kept.
function partitionPlaces(places) {
// Find outer-most slot per stack (used later to elide duplicate stacks).
const stackMap = new Map(); // Map from stack index to outer-most slot#
for (const place of places) {
const prevSlot = stackMap.get(place.Stack);
if (prevSlot && prevSlot <= place.Pos) {
// We already have a higher slot in this stack.
} else {
stackMap.set(place.Stack, place.Pos);
}
}
// Now partition the stacks.
const groups = []; // Array of Group {name, src, sum, self, places}
const groupMap = new Map(); // Map from Source to Group
for (const place of places) {
if (stackMap.get(place.Stack) != place.Pos) {
continue;
}
const stack = stacks.Stacks[place.Stack];
const src = stack.Sources[place.Pos];
let group = groupMap.get(src);
if (!group) {
const name = stacks.Sources[src].FullName;
group = {name: name, src: src, sumpos: 0, sumneg: 0, self: 0, places: []};
groupMap.set(src, group);
groups.push(group);
}
if (stack.Value < 0) {
group.sumneg += -stack.Value;
} else {
group.sumpos += stack.Value;
}
group.self += (place.Pos == stack.Sources.length-1) ? stack.Value : 0;
group.places.push(place);
}
// Order by decreasing cost (makes it easier to spot heavy functions).
// Though alphabetical ordering is a potential alternative that will make
// profile comparisons easier.
groups.sort(function(a, b) {
return (b.sumpos + b.sumneg) - (a.sumpos + a.sumneg);
});
return groups;
}
function display(xscale, posTotal, negTotal, list, showPivotSummary) {
// Sort boxes so that text selection follows a predictable order.
list.sort(function(a, b) {
if (a.y != b.y) return a.y - b.y;
return a.x - b.x;
});
// Adjust Y coordinates so that zero is at top.
let adjust = (list.length > 0) ? list[0].y : 0;
const divs = [];
for (const box of list) {
box.y -= adjust;
divs.push(drawBox(xscale, box));
}
if (showPivotSummary) {
divs.push(drawSep(-adjust, posTotal, negTotal));
}
const h = (list.length > 0 ? list[list.length-1].y : 0) + 4*ROW;
chart.style.height = h+'px';
chart.replaceChildren(...divs);
}
function drawBox(xscale, box) {
const srcIndex = box.src;
const src = stacks.Sources[srcIndex];
function makeRect(cl, x, y, w, h) {
const r = document.createElement('div');
r.style.left = x+'px';
r.style.top = y+'px';
r.style.width = w+'px';
r.style.height = h+'px';
r.classList.add(cl);
return r;
}
// Background
const w = box.width - 1; // Leave 1px gap
const r = makeRect('boxbg', box.x, box.y, w, ROW);
if (!diff) r.style.background = makeColor(src.Color);
addElem(srcIndex, r);
if (!src.Inlined) {
r.classList.add('not-inlined');
}
// Positive/negative indicator for diff mode.
if (diff) {
const delta = box.sumpos - box.sumneg;
const partWidth = xscale * Math.abs(delta);
if (partWidth >= MIN_WIDTH) {
r.appendChild(makeRect((delta < 0 ? 'negative' : 'positive'),
0, 0, partWidth, ROW-1));
}
}
// Label
if (box.width >= MIN_TEXT_WIDTH) {
const t = document.createElement('div');
t.classList.add('boxtext');
fitText(t, box.width-2*TEXT_MARGIN, src.Display);
r.appendChild(t);
}
onClick(r, () => { switchPivots(pprofQuoteMeta(src.UniqueName)); });
r.addEventListener('mouseenter', () => { handleEnter(box, r); });
r.addEventListener('mouseleave', () => { handleLeave(box); });
r.addEventListener('contextmenu', (e) => { showActionMenu(e, box); });
return r;
}
// Handle clicks, but only if the mouse did not move during the click.
function onClick(target, handler) {
// Disable click if mouse moves more than threshold pixels since mousedown.
const threshold = 3;
let [x, y] = [-1, -1];
target.addEventListener('mousedown', (e) => {
[x, y] = [e.clientX, e.clientY];
});
target.addEventListener('click', (e) => {
if (Math.abs(e.clientX - x) <= threshold &&
Math.abs(e.clientY - y) <= threshold) {
handler();
}
});
}
function drawSep(y, posTotal, negTotal) {
const m = document.createElement('div');
m.innerText = summary(posTotal, negTotal);
m.style.top = (y-ROW) + 'px';
m.style.left = PADDING + 'px';
m.style.width = (chart.clientWidth - PADDING*2) + 'px';
m.classList.add('separator');
return m;
}
// addElem registers an element that belongs to the specified src.
function addElem(src, elem) {
let list = elems.get(src);
if (!list) {
list = [];
elems.set(src, list);
}
list.push(elem);
elem.classList.toggle('hilite', matches.has(src));
}
// Adds or removes cl from classList of all elements for the specified source.
function toggleClass(src, cl, value) {
const list = elems.get(src);
if (list) {
for (const elem of list) {
elem.classList.toggle(cl, value);
}
}
}
// fitText sets text and font-size clipped to the specified width w.
function fitText(t, avail, textList) {
// Find first entry in textList that fits.
let width = avail;
textContext.font = FONT_SIZE + 'pt Arial';
for (let i = 0; i < textList.length; i++) {
let text = textList[i];
width = textContext.measureText(text).width;
if (width <= avail) {
t.innerText = text;
return;
}
}
// Try to fit by dropping font size.
let text = textList[textList.length-1];
const fs = Math.max(MIN_FONT_SIZE, FONT_SIZE * (avail / width));
t.style.fontSize = fs + 'pt';
t.innerText = text;
}
// totalValue returns the positive and negative sums of the Values of stacks
// listed in places.
function totalValue(places) {
const seen = new Set();
let pos = 0;
let neg = 0;
for (const place of places) {
if (seen.has(place.Stack)) continue; // Do not double-count stacks
seen.add(place.Stack);
const stack = stacks.Stacks[place.Stack];
if (stack.Value < 0) {
neg += -stack.Value;
} else {
pos += stack.Value;
}
}
return [pos, neg];
}
function summary(pos, neg) {
// Examples:
// 6s (10%)
// 12s (20%) 🠆 18s (30%)
return diff ? diffText(neg, pos) : percentText(pos);
}
function details(box) {
// Examples:
// 6s (10%)
// 6s (10%) │ self 3s (5%)
// 6s (10%) │ 12s (20%) 🠆 18s (30%)
let result = percentText(box.sumpos - box.sumneg);
if (box.self != 0) {
result += " │ self " + unitText(box.self);
}
if (diff && box.sumpos > 0 && box.sumneg > 0) {
result += " │ " + diffText(box.sumneg, box.sumpos);
}
return result;
}
// diffText returns text that displays from and to alongside their percentages.
// E.g., 9s (45%) 🠆 10s (50%)
function diffText(from, to) {
return percentText(from) + " 🠆 " + percentText(to);
}
// percentText returns text that displays v in appropriate units alongside its
// percentange.
function percentText(v) {
function percent(v, total) {
return Number(((100.0 * v) / total).toFixed(1)) + '%';
}
return unitText(v) + " (" + percent(v, stacks.Total) + ")";
}
// unitText returns a formatted string to display for value.
function unitText(value) {
return pprofUnitText(value*stacks.Scale, stacks.Unit);
}
function find(name) {
const elem = document.getElementById(name);
if (!elem) {
throw 'element not found: ' + name
}
return elem;
}
function makeColor(index) {
// Rotate hue around a circle. Multiple by phi to spread things
// out better. Use 50% saturation to make subdued colors, and
// 80% lightness to have good contrast with black foreground text.
const PHI = 1.618033988;
const hue = (index+1) * PHI * 2 * Math.PI; // +1 to avoid 0
const hsl = `hsl(${hue}rad 50% 80%)`;
return hsl;
}
}
// pprofUnitText returns a formatted string to display for value in the specified unit.
function pprofUnitText(value, unit) {
const sign = (value < 0) ? "-" : "";
let v = Math.abs(value);
// Rescale to appropriate display unit.
let list = null;
for (const def of pprofUnitDefs) {
if (def.DefaultUnit.CanonicalName == unit) {
list = def.Units;
v *= def.DefaultUnit.Factor;
break;
}
}
if (list) {
// Stop just before entry that is too large.
for (let i = 0; i < list.length; i++) {
if (i == list.length-1 || list[i+1].Factor > v) {
v /= list[i].Factor;
unit = list[i].CanonicalName;
break;
}
}
}
return sign + Number(v.toFixed(2)) + unit;
}

View File

@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
<style type="text/css">
</style>
</head>
<body>
{{template "header" .}}
<div id="top">
<table id="toptable">
<thead>
<tr>
<th id="flathdr1">Flat</th>
<th id="flathdr2">Flat%</th>
<th>Sum%</th>
<th id="cumhdr1">Cum</th>
<th id="cumhdr2">Cum%</th>
<th id="namehdr">Name</th>
<th>Inlined?</th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
</div>
{{template "script" .}}
<script>
function makeTopTable(total, entries) {
const rows = document.getElementById('rows');
if (rows == null) return;
// Store initial index in each entry so we have stable node ids for selection.
for (let i = 0; i < entries.length; i++) {
entries[i].Id = 'node' + i;
}
// Which column are we currently sorted by and in what order?
let currentColumn = '';
let descending = false;
sortBy('Flat');
function sortBy(column) {
// Update sort criteria
if (column == currentColumn) {
descending = !descending; // Reverse order
} else {
currentColumn = column;
descending = (column != 'Name');
}
// Sort according to current criteria.
function cmp(a, b) {
const av = a[currentColumn];
const bv = b[currentColumn];
if (av < bv) return -1;
if (av > bv) return +1;
return 0;
}
entries.sort(cmp);
if (descending) entries.reverse();
function addCell(tr, val) {
const td = document.createElement('td');
td.textContent = val;
tr.appendChild(td);
}
function percent(v) {
return (v * 100.0 / total).toFixed(2) + '%';
}
// Generate rows
const fragment = document.createDocumentFragment();
let sum = 0;
for (const row of entries) {
const tr = document.createElement('tr');
tr.id = row.Id;
sum += row.Flat;
addCell(tr, row.FlatFormat);
addCell(tr, percent(row.Flat));
addCell(tr, percent(sum));
addCell(tr, row.CumFormat);
addCell(tr, percent(row.Cum));
addCell(tr, row.Name);
addCell(tr, row.InlineLabel);
fragment.appendChild(tr);
}
rows.textContent = ''; // Remove old rows
rows.appendChild(fragment);
}
// Make different column headers trigger sorting.
function bindSort(id, column) {
const hdr = document.getElementById(id);
if (hdr == null) return;
const fn = function() { sortBy(column) };
hdr.addEventListener('click', fn);
hdr.addEventListener('touch', fn);
}
bindSort('flathdr1', 'Flat');
bindSort('flathdr2', 'Flat');
bindSort('cumhdr1', 'Cum');
bindSort('cumhdr2', 'Cum');
bindSort('namehdr', 'Name');
}
viewer(new URL(window.location.href), {{.Nodes}});
makeTopTable({{.Total}}, {{.Top}});
</script>
</body>
</html>

View File

@@ -0,0 +1,422 @@
// 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 driver
import (
"fmt"
"io"
"regexp"
"sort"
"strconv"
"strings"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/internal/report"
"m7s.live/v5/plugin/debug/pkg/profile"
)
var commentStart = "//:" // Sentinel for comments on options
var tailDigitsRE = regexp.MustCompile("[0-9]+$")
// interactive starts a shell to read pprof commands.
func interactive(p *profile.Profile, o *plugin.Options) error {
// Enter command processing loop.
o.UI.SetAutoComplete(newCompleter(functionNames(p)))
configure("compact_labels", "true")
configHelp["sample_index"] += fmt.Sprintf("Or use sample_index=name, with name in %v.\n", sampleTypes(p))
// Do not wait for the visualizer to complete, to allow multiple
// graphs to be visualized simultaneously.
interactiveMode = true
shortcuts := profileShortcuts(p)
copier := makeProfileCopier(p)
greetings(p, o.UI)
for {
input, err := o.UI.ReadLine("(pprof) ")
if err != nil {
if err != io.EOF {
return err
}
if input == "" {
return nil
}
}
for _, input := range shortcuts.expand(input) {
// Process assignments of the form variable=value
if s := strings.SplitN(input, "=", 2); len(s) > 0 {
name := strings.TrimSpace(s[0])
var value string
if len(s) == 2 {
value = s[1]
if comment := strings.LastIndex(value, commentStart); comment != -1 {
value = value[:comment]
}
value = strings.TrimSpace(value)
}
if isConfigurable(name) {
// All non-bool options require inputs
if len(s) == 1 && !isBoolConfig(name) {
o.UI.PrintErr(fmt.Errorf("please specify a value, e.g. %s=<val>", name))
continue
}
if name == "sample_index" {
// Error check sample_index=xxx to ensure xxx is a valid sample type.
index, err := p.SampleIndexByName(value)
if err != nil {
o.UI.PrintErr(err)
continue
}
if index < 0 || index >= len(p.SampleType) {
o.UI.PrintErr(fmt.Errorf("invalid sample_index %q", value))
continue
}
value = p.SampleType[index].Type
}
if err := configure(name, value); err != nil {
o.UI.PrintErr(err)
}
continue
}
}
tokens := strings.Fields(input)
if len(tokens) == 0 {
continue
}
switch tokens[0] {
case "o", "options":
printCurrentOptions(p, o.UI)
continue
case "exit", "quit", "q":
return nil
case "help":
commandHelp(strings.Join(tokens[1:], " "), o.UI)
continue
}
args, cfg, err := parseCommandLine(tokens)
if err == nil {
err = generateReportWrapper(copier.newCopy(), args, cfg, o)
}
if err != nil {
o.UI.PrintErr(err)
}
}
}
}
var generateReportWrapper = generateReport // For testing purposes.
// greetings prints a brief welcome and some overall profile
// information before accepting interactive commands.
func greetings(p *profile.Profile, ui plugin.UI) {
numLabelUnits := identifyNumLabelUnits(p, ui)
ropt, err := reportOptions(p, numLabelUnits, currentConfig())
if err == nil {
rpt := report.New(p, ropt)
ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
if rpt.Total() == 0 && len(p.SampleType) > 1 {
ui.Print(`No samples were found with the default sample value type.`)
ui.Print(`Try "sample_index" command to analyze different sample values.`, "\n")
}
}
ui.Print(`Entering interactive mode (type "help" for commands, "o" for options)`)
}
// shortcuts represents composite commands that expand into a sequence
// of other commands.
type shortcuts map[string][]string
func (a shortcuts) expand(input string) []string {
input = strings.TrimSpace(input)
if a != nil {
if r, ok := a[input]; ok {
return r
}
}
return []string{input}
}
var pprofShortcuts = shortcuts{
":": []string{"focus=", "ignore=", "hide=", "tagfocus=", "tagignore="},
}
// profileShortcuts creates macros for convenience and backward compatibility.
func profileShortcuts(p *profile.Profile) shortcuts {
s := pprofShortcuts
// Add shortcuts for sample types
for _, st := range p.SampleType {
command := fmt.Sprintf("sample_index=%s", st.Type)
s[st.Type] = []string{command}
s["total_"+st.Type] = []string{"mean=0", command}
s["mean_"+st.Type] = []string{"mean=1", command}
}
return s
}
func sampleTypes(p *profile.Profile) []string {
types := make([]string, len(p.SampleType))
for i, t := range p.SampleType {
types[i] = t.Type
}
return types
}
func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
var args []string
current := currentConfig()
for _, f := range configFields {
n := f.name
v := current.get(f)
comment := ""
switch {
case len(f.choices) > 0:
values := append([]string{}, f.choices...)
sort.Strings(values)
comment = "[" + strings.Join(values, " | ") + "]"
case n == "sample_index":
st := sampleTypes(p)
if v == "" {
// Apply default (last sample index).
v = st[len(st)-1]
}
// Add comments for all sample types in profile.
comment = "[" + strings.Join(st, " | ") + "]"
case n == "source_path":
continue
case n == "nodecount" && v == "-1":
comment = "default"
case v == "":
// Add quotes for empty values.
v = `""`
}
if n == "granularity" && v == "" {
v = "(default)"
}
if comment != "" {
comment = commentStart + " " + comment
}
args = append(args, fmt.Sprintf(" %-25s = %-20s %s", n, v, comment))
}
sort.Strings(args)
ui.Print(strings.Join(args, "\n"))
}
// parseCommandLine parses a command and returns the pprof command to
// execute and the configuration to use for the report.
func parseCommandLine(input []string) ([]string, config, error) {
cmd, args := input[:1], input[1:]
name := cmd[0]
c := pprofCommands[name]
if c == nil {
// Attempt splitting digits on abbreviated commands (eg top10)
if d := tailDigitsRE.FindString(name); d != "" && d != name {
name = name[:len(name)-len(d)]
cmd[0], args = name, append([]string{d}, args...)
c = pprofCommands[name]
}
}
if c == nil {
if _, ok := configHelp[name]; ok {
value := "<val>"
if len(args) > 0 {
value = args[0]
}
return nil, config{}, fmt.Errorf("did you mean: %s=%s", name, value)
}
return nil, config{}, fmt.Errorf("unrecognized command: %q", name)
}
if c.hasParam {
if len(args) == 0 {
return nil, config{}, fmt.Errorf("command %s requires an argument", name)
}
cmd = append(cmd, args[0])
args = args[1:]
}
// Copy config since options set in the command line should not persist.
vcopy := currentConfig()
var focus, ignore string
for i := 0; i < len(args); i++ {
t := args[i]
if n, err := strconv.ParseInt(t, 10, 32); err == nil {
vcopy.NodeCount = int(n)
continue
}
switch t[0] {
case '>':
outputFile := t[1:]
if outputFile == "" {
i++
if i >= len(args) {
return nil, config{}, fmt.Errorf("unexpected end of line after >")
}
outputFile = args[i]
}
vcopy.Output = outputFile
case '-':
if t == "--cum" || t == "-cum" {
vcopy.Sort = "cum"
continue
}
ignore = catRegex(ignore, t[1:])
default:
focus = catRegex(focus, t)
}
}
if name == "tags" {
if focus != "" {
vcopy.TagFocus = focus
}
if ignore != "" {
vcopy.TagIgnore = ignore
}
} else {
if focus != "" {
vcopy.Focus = focus
}
if ignore != "" {
vcopy.Ignore = ignore
}
}
if vcopy.NodeCount == -1 && (name == "text" || name == "top") {
vcopy.NodeCount = 10
}
return cmd, vcopy, nil
}
func catRegex(a, b string) string {
if a != "" && b != "" {
return a + "|" + b
}
return a + b
}
// commandHelp displays help and usage information for all Commands
// and Variables or a specific Command or Variable.
func commandHelp(args string, ui plugin.UI) {
if args == "" {
help := usage(false)
help = help + `
: Clear focus/ignore/hide/tagfocus/tagignore
type "help <cmd|option>" for more information
`
ui.Print(help)
return
}
if c := pprofCommands[args]; c != nil {
ui.Print(c.help(args))
return
}
if help, ok := configHelp[args]; ok {
ui.Print(help + "\n")
return
}
ui.PrintErr("Unknown command: " + args)
}
// newCompleter creates an autocompletion function for a set of commands.
func newCompleter(fns []string) func(string) string {
return func(line string) string {
switch tokens := strings.Fields(line); len(tokens) {
case 0:
// Nothing to complete
case 1:
// Single token -- complete command name
if match := matchVariableOrCommand(tokens[0]); match != "" {
return match
}
case 2:
if tokens[0] == "help" {
if match := matchVariableOrCommand(tokens[1]); match != "" {
return tokens[0] + " " + match
}
return line
}
fallthrough
default:
// Multiple tokens -- complete using functions, except for tags
if cmd := pprofCommands[tokens[0]]; cmd != nil && tokens[0] != "tags" {
lastTokenIdx := len(tokens) - 1
lastToken := tokens[lastTokenIdx]
if strings.HasPrefix(lastToken, "-") {
lastToken = "-" + functionCompleter(lastToken[1:], fns)
} else {
lastToken = functionCompleter(lastToken, fns)
}
return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ")
}
}
return line
}
}
// matchVariableOrCommand attempts to match a string token to the prefix of a Command.
func matchVariableOrCommand(token string) string {
token = strings.ToLower(token)
var matches []string
for cmd := range pprofCommands {
if strings.HasPrefix(cmd, token) {
matches = append(matches, cmd)
}
}
matches = append(matches, completeConfig(token)...)
if len(matches) == 1 {
return matches[0]
}
return ""
}
// functionCompleter replaces provided substring with a function
// name retrieved from a profile if a single match exists. Otherwise,
// it returns unchanged substring. It defaults to no-op if the profile
// is not specified.
func functionCompleter(substring string, fns []string) string {
found := ""
for _, fName := range fns {
if strings.Contains(fName, substring) {
if found != "" {
return substring
}
found = fName
}
}
if found != "" {
return found
}
return substring
}
func functionNames(p *profile.Profile) []string {
var fns []string
for _, fn := range p.Function {
fns = append(fns, fn.Name)
}
return fns
}

View File

@@ -0,0 +1,270 @@
// 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 driver
import (
"fmt"
"math/rand"
"strings"
"testing"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/internal/proftest"
"m7s.live/v5/plugin/debug/pkg/internal/report"
"m7s.live/v5/plugin/debug/pkg/internal/transport"
"m7s.live/v5/plugin/debug/pkg/profile"
)
func TestShell(t *testing.T) {
p := &profile.Profile{}
generateReportWrapper = checkValue
defer func() { generateReportWrapper = generateReport }()
// Use test commands and variables to exercise interactive processing
var savedCommands commands
savedCommands, pprofCommands = pprofCommands, testCommands
defer func() { pprofCommands = savedCommands }()
savedConfig := currentConfig()
defer setCurrentConfig(savedConfig)
shortcuts1, scScript1 := makeShortcuts(interleave(script, 2), 1)
shortcuts2, scScript2 := makeShortcuts(interleave(script, 1), 2)
var testcases = []struct {
name string
input []string
shortcuts shortcuts
allowRx string
numAllowRxMatches int
propagateError bool
}{
{"Random interleave of independent scripts 1", interleave(script, 0), pprofShortcuts, "", 0, false},
{"Random interleave of independent scripts 2", interleave(script, 1), pprofShortcuts, "", 0, false},
{"Random interleave of independent scripts with shortcuts 1", scScript1, shortcuts1, "", 0, false},
{"Random interleave of independent scripts with shortcuts 2", scScript2, shortcuts2, "", 0, false},
{"Group with invalid value", []string{"sort=this"}, pprofShortcuts, `invalid "sort" value`, 1, false},
{"No special value provided for the option", []string{"sample_index"}, pprofShortcuts, `please specify a value, e.g. sample_index=<val>`, 1, false},
{"No string value provided for the option", []string{"focus"}, pprofShortcuts, `please specify a value, e.g. focus=<val>`, 1, false},
{"No float value provided for the option", []string{"divide_by"}, pprofShortcuts, `please specify a value, e.g. divide_by=<val>`, 1, false},
{"Helpful input format reminder", []string{"sample_index 0"}, pprofShortcuts, `did you mean: sample_index=0`, 1, false},
{"Verify propagation of IO errors", []string{"**error**"}, pprofShortcuts, "", 0, true},
}
o := setDefaults(&plugin.Options{HTTPTransport: transport.New(nil)})
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
setCurrentConfig(savedConfig)
pprofShortcuts = tc.shortcuts
ui := &proftest.TestUI{
T: t,
Input: tc.input,
AllowRx: tc.allowRx,
}
o.UI = ui
err := interactive(p, o)
if (tc.propagateError && err == nil) || (!tc.propagateError && err != nil) {
t.Errorf("%s: %v", tc.name, err)
}
// Confirm error message written out once.
if tc.numAllowRxMatches != ui.NumAllowRxMatches {
t.Errorf("want error message to be printed %d time(s), got %d",
tc.numAllowRxMatches, ui.NumAllowRxMatches)
}
})
}
}
var testCommands = commands{
"check": &command{report.Raw, nil, nil, true, "", ""},
}
// script contains sequences of commands to be executed for testing. Commands
// are split by semicolon and interleaved randomly, so they must be
// independent from each other.
var script = []string{
"call_tree=true;call_tree=false;check call_tree=false;call_tree=yes;check call_tree=true",
"mean=1;check mean=true;mean=n;check mean=false",
"nodecount=-1;nodecount=-2;check nodecount=-2;nodecount=999999;check nodecount=999999",
"nodefraction=-1;nodefraction=-2.5;check nodefraction=-2.5;nodefraction=0.0001;check nodefraction=0.0001",
"focus=one;focus=two;check focus=two",
"flat=true;check sort=flat;cum=1;check sort=cum",
}
func makeShortcuts(input []string, seed int64) (shortcuts, []string) {
rand := rand.New(rand.NewSource(seed))
s := shortcuts{}
var output, chunk []string
for _, l := range input {
chunk = append(chunk, l)
switch rand.Intn(3) {
case 0:
// Create a macro for commands in 'chunk'.
macro := fmt.Sprintf("alias%d", len(s))
s[macro] = chunk
output = append(output, macro)
chunk = nil
case 1:
// Append commands in 'chunk' by themselves.
output = append(output, chunk...)
chunk = nil
case 2:
// Accumulate commands into 'chunk'
}
}
output = append(output, chunk...)
return s, output
}
func checkValue(p *profile.Profile, cmd []string, cfg config, o *plugin.Options) error {
if len(cmd) != 2 {
return fmt.Errorf("expected len(cmd)==2, got %v", cmd)
}
input := cmd[1]
args := strings.SplitN(input, "=", 2)
if len(args) == 0 {
return fmt.Errorf("unexpected empty input")
}
name, value := args[0], ""
if len(args) == 2 {
value = args[1]
}
f, ok := configFieldMap[name]
if !ok {
return fmt.Errorf("Could not find variable named %s", name)
}
if got := cfg.get(f); got != value {
return fmt.Errorf("Variable %s, want %s, got %s", name, value, got)
}
return nil
}
func interleave(input []string, seed int64) []string {
var inputs [][]string
for _, s := range input {
inputs = append(inputs, strings.Split(s, ";"))
}
rand := rand.New(rand.NewSource(seed))
var output []string
for len(inputs) > 0 {
next := rand.Intn(len(inputs))
output = append(output, inputs[next][0])
if tail := inputs[next][1:]; len(tail) > 0 {
inputs[next] = tail
} else {
inputs = append(inputs[:next], inputs[next+1:]...)
}
}
return output
}
func TestInteractiveCommands(t *testing.T) {
type interactiveTestcase struct {
input string
want map[string]string
}
testcases := []interactiveTestcase{
{
"top 10 --cum focus1 -ignore focus2",
map[string]string{
"nodecount": "10",
"sort": "cum",
"focus": "focus1|focus2",
"ignore": "ignore",
},
},
{
"top10 --cum focus1 -ignore focus2",
map[string]string{
"nodecount": "10",
"sort": "cum",
"focus": "focus1|focus2",
"ignore": "ignore",
},
},
{
"dot",
map[string]string{
"nodecount": "80",
"sort": "flat",
},
},
{
"tags -ignore1 -ignore2 focus1 >out",
map[string]string{
"nodecount": "80",
"sort": "flat",
"output": "out",
"tagfocus": "focus1",
"tagignore": "ignore1|ignore2",
},
},
{
"weblist find -test",
map[string]string{
"granularity": "addresses",
"noinlines": "false",
"nodecount": "0",
"sort": "flat",
"ignore": "test",
},
},
{
"callgrind fun -ignore >out",
map[string]string{
"granularity": "addresses",
"nodecount": "0",
"sort": "flat",
"output": "out",
},
},
{
"999",
nil, // Error
},
}
for _, tc := range testcases {
cmd, cfg, err := parseCommandLine(strings.Fields(tc.input))
if tc.want == nil && err != nil {
// Error expected
continue
}
if err != nil {
t.Errorf("failed on %q: %v", tc.input, err)
continue
}
// Get report output format
c := pprofCommands[cmd[0]]
if c == nil {
t.Fatalf("unexpected nil command")
}
cfg = applyCommandOverrides(cmd[0], c.format, cfg)
for n, want := range tc.want {
if got := cfg.get(configFieldMap[n]); got != want {
t.Errorf("failed on %q, cmd=%q, %s got %s, want %s", tc.input, cmd, n, got, want)
}
}
}
}

View File

@@ -0,0 +1,100 @@
// 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 driver
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"m7s.live/v5/plugin/debug/pkg/internal/binutils"
"m7s.live/v5/plugin/debug/pkg/internal/plugin"
"m7s.live/v5/plugin/debug/pkg/internal/symbolizer"
"m7s.live/v5/plugin/debug/pkg/internal/transport"
)
// setDefaults returns a new plugin.Options with zero fields sets to
// sensible defaults.
func setDefaults(o *plugin.Options) *plugin.Options {
d := &plugin.Options{}
if o != nil {
*d = *o
}
if d.Writer == nil {
d.Writer = oswriter{}
}
if d.Flagset == nil {
d.Flagset = &GoFlags{}
}
if d.Obj == nil {
d.Obj = &binutils.Binutils{}
}
if d.UI == nil {
d.UI = &stdUI{r: bufio.NewReader(os.Stdin)}
}
if d.HTTPTransport == nil {
d.HTTPTransport = transport.New(d.Flagset)
}
if d.Sym == nil {
d.Sym = &symbolizer.Symbolizer{Obj: d.Obj, UI: d.UI, Transport: d.HTTPTransport}
}
return d
}
type stdUI struct {
r *bufio.Reader
}
func (ui *stdUI) ReadLine(prompt string) (string, error) {
os.Stdout.WriteString(prompt)
return ui.r.ReadString('\n')
}
func (ui *stdUI) Print(args ...interface{}) {
ui.fprint(os.Stderr, args)
}
func (ui *stdUI) PrintErr(args ...interface{}) {
ui.fprint(os.Stderr, args)
}
func (ui *stdUI) IsTerminal() bool {
return false
}
func (ui *stdUI) WantBrowser() bool {
return true
}
func (ui *stdUI) SetAutoComplete(func(string) string) {
}
func (ui *stdUI) fprint(f *os.File, args []interface{}) {
text := fmt.Sprint(args...)
if !strings.HasSuffix(text, "\n") {
text += "\n"
}
f.WriteString(text)
}
// oswriter implements the Writer interface using a regular file.
type oswriter struct{}
func (oswriter) Open(name string) (io.WriteCloser, error) {
f, err := os.Create(name)
return f, err
}

View File

@@ -0,0 +1,158 @@
package driver
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
)
// settings holds pprof settings.
type settings struct {
// Configs holds a list of named UI configurations.
Configs []namedConfig `json:"configs"`
}
// namedConfig associates a name with a config.
type namedConfig struct {
Name string `json:"name"`
config
}
// settingsFileName returns the name of the file where settings should be saved.
func settingsFileName() (string, error) {
// Return "pprof/settings.json" under os.UserConfigDir().
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "pprof", "settings.json"), nil
}
// readSettings reads settings from fname.
func readSettings(fname string) (*settings, error) {
data, err := os.ReadFile(fname)
if err != nil {
if os.IsNotExist(err) {
return &settings{}, nil
}
return nil, fmt.Errorf("could not read settings: %w", err)
}
settings := &settings{}
if err := json.Unmarshal(data, settings); err != nil {
return nil, fmt.Errorf("could not parse settings: %w", err)
}
for i := range settings.Configs {
settings.Configs[i].resetTransient()
}
return settings, nil
}
// writeSettings saves settings to fname.
func writeSettings(fname string, settings *settings) error {
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return fmt.Errorf("could not encode settings: %w", err)
}
// create the settings directory if it does not exist
// XDG specifies permissions 0700 when creating settings dirs:
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil {
return fmt.Errorf("failed to create settings directory: %w", err)
}
if err := os.WriteFile(fname, data, 0644); err != nil {
return fmt.Errorf("failed to write settings: %w", err)
}
return nil
}
// configMenuEntry holds information for a single config menu entry.
type configMenuEntry struct {
Name string
URL string
Current bool // Is this the currently selected config?
UserConfig bool // Is this a user-provided config?
}
// configMenu returns a list of items to add to a menu in the web UI.
func configMenu(fname string, u url.URL) []configMenuEntry {
// Start with system configs.
configs := []namedConfig{{Name: "Default", config: defaultConfig()}}
if settings, err := readSettings(fname); err == nil {
// Add user configs.
configs = append(configs, settings.Configs...)
}
// Convert to menu entries.
result := make([]configMenuEntry, len(configs))
lastMatch := -1
for i, cfg := range configs {
dst, changed := cfg.config.makeURL(u)
if !changed {
lastMatch = i
}
// Use a relative URL to work in presence of stripping/redirects in webui.go.
rel := &url.URL{RawQuery: dst.RawQuery, ForceQuery: true}
result[i] = configMenuEntry{
Name: cfg.Name,
URL: rel.String(),
UserConfig: (i != 0),
}
}
// Mark the last matching config as current
if lastMatch >= 0 {
result[lastMatch].Current = true
}
return result
}
// editSettings edits settings by applying fn to them.
func editSettings(fname string, fn func(s *settings) error) error {
settings, err := readSettings(fname)
if err != nil {
return err
}
if err := fn(settings); err != nil {
return err
}
return writeSettings(fname, settings)
}
// setConfig saves the config specified in request to fname.
func setConfig(fname string, request url.URL) error {
q := request.Query()
name := q.Get("config")
if name == "" {
return fmt.Errorf("invalid config name")
}
cfg := currentConfig()
if err := cfg.applyURL(q); err != nil {
return err
}
return editSettings(fname, func(s *settings) error {
for i, c := range s.Configs {
if c.Name == name {
s.Configs[i].config = cfg
return nil
}
}
s.Configs = append(s.Configs, namedConfig{Name: name, config: cfg})
return nil
})
}
// removeConfig removes config from fname.
func removeConfig(fname, config string) error {
return editSettings(fname, func(s *settings) error {
for i, c := range s.Configs {
if c.Name == config {
s.Configs = append(s.Configs[:i], s.Configs[i+1:]...)
return nil
}
}
return fmt.Errorf("config %s not found", config)
})
}

View File

@@ -0,0 +1,247 @@
package driver
import (
"net/url"
"os"
"path/filepath"
"reflect"
"testing"
)
// settingsDirAndFile returns a directory in which settings should be stored
// and the name of the settings file. The caller must delete the directory when
// done.
func settingsDirAndFile(t *testing.T) (string, string) {
tmpDir, err := os.MkdirTemp("", "pprof_settings_test")
if err != nil {
t.Fatalf("error creating temporary directory: %v", err)
}
return tmpDir, filepath.Join(tmpDir, "settings.json")
}
func TestSettings(t *testing.T) {
tmpDir, fname := settingsDirAndFile(t)
defer os.RemoveAll(tmpDir)
s, err := readSettings(fname)
if err != nil {
t.Fatalf("error reading empty settings: %v", err)
}
if len(s.Configs) != 0 {
t.Fatalf("expected empty settings; got %v", s)
}
s.Configs = append(s.Configs, namedConfig{
Name: "Foo",
config: config{
Focus: "focus",
// Ensure that transient fields are not saved/restored.
Output: "output",
SourcePath: "source",
TrimPath: "trim",
DivideBy: -2,
},
})
if err := writeSettings(fname, s); err != nil {
t.Fatal(err)
}
s2, err := readSettings(fname)
if err != nil {
t.Fatal(err)
}
// Change the transient fields to their expected values.
s.Configs[0].resetTransient()
if !reflect.DeepEqual(s, s2) {
t.Fatalf("ReadSettings = %v; expected %v", s2, s)
}
}
func TestParseConfig(t *testing.T) {
// Use all the fields to check they are saved/restored from URL.
cfg := config{
Output: "",
DropNegative: true,
CallTree: true,
RelativePercentages: true,
Unit: "auto",
CompactLabels: true,
SourcePath: "",
TrimPath: "",
NodeCount: 10,
NodeFraction: 0.1,
EdgeFraction: 0.2,
Trim: true,
Focus: "focus",
Ignore: "ignore",
PruneFrom: "prune_from",
Hide: "hide",
Show: "show",
ShowFrom: "show_from",
TagFocus: "tagfocus",
TagIgnore: "tagignore",
TagShow: "tagshow",
TagHide: "taghide",
DivideBy: 1,
Mean: true,
Normalize: true,
Sort: "cum",
Granularity: "functions",
NoInlines: true,
ShowColumns: true,
}
url, changed := cfg.makeURL(url.URL{})
if !changed {
t.Error("applyConfig returned changed=false after applying non-empty config")
}
cfg2 := defaultConfig()
if err := cfg2.applyURL(url.Query()); err != nil {
t.Fatalf("fromURL failed: %v", err)
}
if !reflect.DeepEqual(cfg, cfg2) {
t.Fatalf("parsed config = %+v; expected match with %+v", cfg2, cfg)
}
if url2, changed := cfg.makeURL(url); changed {
t.Errorf("ApplyConfig returned changed=true after applying same config (%q instead of expected %q", url2.String(), url.String())
}
}
// TestDefaultConfig verifies that default config values are omitted from URL.
func TestDefaultConfig(t *testing.T) {
cfg := defaultConfig()
url, changed := cfg.makeURL(url.URL{})
if changed {
t.Error("applyConfig returned changed=true after applying default config")
}
if url.String() != "" {
t.Errorf("applyConfig returned %q; expecting %q", url.String(), "")
}
}
func TestConfigMenu(t *testing.T) {
// Save some test settings.
tmpDir, fname := settingsDirAndFile(t)
defer os.RemoveAll(tmpDir)
a, b := defaultConfig(), defaultConfig()
a.Focus, b.Focus = "foo", "bar"
s := &settings{
Configs: []namedConfig{
{Name: "A", config: a},
{Name: "B", config: b},
},
}
if err := writeSettings(fname, s); err != nil {
t.Fatal("error writing settings", err)
}
pageURL, _ := url.Parse("/top?f=foo")
menu := configMenu(fname, *pageURL)
want := []configMenuEntry{
{Name: "Default", URL: "?", Current: false, UserConfig: false},
{Name: "A", URL: "?f=foo", Current: true, UserConfig: true},
{Name: "B", URL: "?f=bar", Current: false, UserConfig: true},
}
if !reflect.DeepEqual(menu, want) {
t.Errorf("ConfigMenu returned %v; want %v", menu, want)
}
}
func TestEditConfig(t *testing.T) {
tmpDir, fname := settingsDirAndFile(t)
defer os.RemoveAll(tmpDir)
type testConfig struct {
name string
focus string
hide string
}
type testCase struct {
remove bool
request string
expect []testConfig
}
for _, c := range []testCase{
// Create setting c1
{false, "/?config=c1&f=foo", []testConfig{
{"c1", "foo", ""},
}},
// Create setting c2
{false, "/?config=c2&h=bar", []testConfig{
{"c1", "foo", ""},
{"c2", "", "bar"},
}},
// Overwrite c1
{false, "/?config=c1&f=baz", []testConfig{
{"c1", "baz", ""},
{"c2", "", "bar"},
}},
// Delete c2
{true, "c2", []testConfig{
{"c1", "baz", ""},
}},
} {
if c.remove {
if err := removeConfig(fname, c.request); err != nil {
t.Errorf("error removing config %s: %v", c.request, err)
continue
}
} else {
req, err := url.Parse(c.request)
if err != nil {
t.Errorf("error parsing request %q: %v", c.request, err)
continue
}
if err := setConfig(fname, *req); err != nil {
t.Errorf("error saving request %q: %v", c.request, err)
continue
}
}
// Check resulting settings.
s, err := readSettings(fname)
if err != nil {
t.Errorf("error reading settings after applying %q: %v", c.request, err)
continue
}
// Convert to a list that can be compared to c.expect
got := make([]testConfig, len(s.Configs))
for i, c := range s.Configs {
got[i] = testConfig{c.Name, c.Focus, c.Hide}
}
if !reflect.DeepEqual(got, c.expect) {
t.Errorf("Settings after applying %q = %v; want %v", c.request, got, c.expect)
}
}
}
func TestAssign(t *testing.T) {
baseConfig := currentConfig()
defer setCurrentConfig(baseConfig)
// Test assigning to a simple field.
if err := configure("nodecount", "20"); err != nil {
t.Errorf("error setting nodecount: %v", err)
}
if n := currentConfig().NodeCount; n != 20 {
t.Errorf("incorrect nodecount; expecting 20, got %d", n)
}
// Test assignment to a group field.
if err := configure("granularity", "files"); err != nil {
t.Errorf("error setting granularity: %v", err)
}
if g := currentConfig().Granularity; g != "files" {
t.Errorf("incorrect granularity; expecting %v, got %v", "files", g)
}
// Test assignment to one choice of a group field.
if err := configure("lines", "t"); err != nil {
t.Errorf("error setting lines: %v", err)
}
if g := currentConfig().Granularity; g != "lines" {
t.Errorf("incorrect granularity; expecting %v, got %v", "lines", g)
}
// Test assignment to invalid choice,
if err := configure("granularity", "cheese"); err == nil {
t.Errorf("allowed assignment of invalid granularity")
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2022 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 driver
import (
"encoding/json"
"html/template"
"net/http"
"m7s.live/v5/plugin/debug/pkg/internal/measurement"
)
// stackView generates the flamegraph view.
func (ui *webInterface) stackView(w http.ResponseWriter, req *http.Request) {
// Get all data in a report.
rpt, errList := ui.makeReport(w, req, []string{"svg"}, func(cfg *config) {
cfg.CallTree = true
cfg.Trim = false
if cfg.Granularity == "" {
cfg.Granularity = "filefunctions"
}
})
if rpt == nil {
return // error already reported
}
// Make stack data and generate corresponding JSON.
stacks := rpt.Stacks()
b, err := json.Marshal(stacks)
if err != nil {
http.Error(w, "error serializing stacks for flame graph",
http.StatusInternalServerError)
ui.options.UI.PrintErr(err)
return
}
nodes := make([]string, len(stacks.Sources))
for i, src := range stacks.Sources {
nodes[i] = src.FullName
}
nodes[0] = "" // root is not a real node
ui.render(w, req, "stacks", rpt, errList, stacks.Legend(), webArgs{
Stacks: template.JS(b),
Nodes: nodes,
UnitDefs: measurement.UnitTypes,
})
}

View File

@@ -0,0 +1,80 @@
// 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 driver
import (
"regexp"
"strings"
"m7s.live/v5/plugin/debug/pkg/third_party/svgpan"
)
var (
viewBox = regexp.MustCompile(`<svg\s*width="[^"]+"\s*height="[^"]+"\s*viewBox="[^"]+"`)
graphID = regexp.MustCompile(`<g id="graph\d"`)
svgClose = regexp.MustCompile(`</svg>`)
)
// massageSVG enhances the SVG output from DOT to provide better
// panning inside a web browser. It uses the svgpan library, which is
// embedded into the svgpan.JSSource variable.
func massageSVG(svg string) string {
// Work around for dot bug which misses quoting some ampersands,
// resulting on unparsable SVG.
svg = strings.Replace(svg, "&;", "&amp;;", -1)
// Dot's SVG output is
//
// <svg width="___" height="___"
// viewBox="___" xmlns=...>
// <g id="graph0" transform="...">
// ...
// </g>
// </svg>
//
// Change it to
//
// <svg width="100%" height="100%"
// xmlns=...>
// <script type="text/ecmascript"><![CDATA[` ..$(svgpan.JSSource)... `]]></script>`
// <g id="viewport" transform="translate(0,0)">
// <g id="graph0" transform="...">
// ...
// </g>
// </g>
// </svg>
if loc := viewBox.FindStringIndex(svg); loc != nil {
svg = svg[:loc[0]] +
`<svg width="100%" height="100%"` +
svg[loc[1]:]
}
if loc := graphID.FindStringIndex(svg); loc != nil {
svg = svg[:loc[0]] +
`<script type="text/ecmascript"><![CDATA[` + svgpan.JSSource + `]]></script>` +
`<g id="viewport" transform="scale(0.5,0.5) translate(0,0)">` +
svg[loc[0]:]
}
if loc := svgClose.FindStringIndex(svg); loc != nil {
svg = svg[:loc[0]] +
`</g>` +
svg[loc[0]:]
}
return svg
}

View File

@@ -0,0 +1,133 @@
package driver
import (
"strings"
"m7s.live/v5/plugin/debug/pkg/internal/measurement"
"m7s.live/v5/plugin/debug/pkg/profile"
)
// addLabelNodes adds pseudo stack frames "label:value" to each Sample with
// labels matching the supplied keys.
//
// rootKeys adds frames at the root of the callgraph (first key becomes new root).
// leafKeys adds frames at the leaf of the callgraph (last key becomes new leaf).
//
// Returns whether there were matches found for the label keys.
func addLabelNodes(p *profile.Profile, rootKeys, leafKeys []string, outputUnit string) (rootm, leafm bool) {
// Find where to insert the new locations and functions at the end of
// their ID spaces.
var maxLocID uint64
var maxFunctionID uint64
for _, loc := range p.Location {
if loc.ID > maxLocID {
maxLocID = loc.ID
}
}
for _, f := range p.Function {
if f.ID > maxFunctionID {
maxFunctionID = f.ID
}
}
nextLocID := maxLocID + 1
nextFuncID := maxFunctionID + 1
// Intern the new locations and functions we are generating.
type locKey struct {
functionName, fileName string
}
locs := map[locKey]*profile.Location{}
internLoc := func(locKey locKey) *profile.Location {
loc, found := locs[locKey]
if found {
return loc
}
function := &profile.Function{
ID: nextFuncID,
Name: locKey.functionName,
Filename: locKey.fileName,
}
nextFuncID++
p.Function = append(p.Function, function)
loc = &profile.Location{
ID: nextLocID,
Line: []profile.Line{
{
Function: function,
},
},
}
nextLocID++
p.Location = append(p.Location, loc)
locs[locKey] = loc
return loc
}
makeLabelLocs := func(s *profile.Sample, keys []string) ([]*profile.Location, bool) {
var locs []*profile.Location
var match bool
for i := range keys {
// Loop backwards, ensuring the first tag is closest to the root,
// and the last tag is closest to the leaves.
k := keys[len(keys)-1-i]
values := formatLabelValues(s, k, outputUnit)
if len(values) > 0 {
match = true
}
locKey := locKey{
functionName: strings.Join(values, ","),
fileName: k,
}
loc := internLoc(locKey)
locs = append(locs, loc)
}
return locs, match
}
for _, s := range p.Sample {
rootsToAdd, sampleMatchedRoot := makeLabelLocs(s, rootKeys)
if sampleMatchedRoot {
rootm = true
}
leavesToAdd, sampleMatchedLeaf := makeLabelLocs(s, leafKeys)
if sampleMatchedLeaf {
leafm = true
}
if len(leavesToAdd)+len(rootsToAdd) == 0 {
continue
}
var newLocs []*profile.Location
newLocs = append(newLocs, leavesToAdd...)
newLocs = append(newLocs, s.Location...)
newLocs = append(newLocs, rootsToAdd...)
s.Location = newLocs
}
return
}
// formatLabelValues returns all the string and numeric labels in Sample, with
// the numeric labels formatted according to outputUnit.
func formatLabelValues(s *profile.Sample, k string, outputUnit string) []string {
var values []string
values = append(values, s.Label[k]...)
numLabels := s.NumLabel[k]
numUnits := s.NumUnit[k]
if len(numLabels) != len(numUnits) && len(numUnits) != 0 {
return values
}
for i, numLabel := range numLabels {
var value string
if len(numUnits) != 0 {
value = measurement.ScaledLabel(numLabel, numUnits[i], outputUnit)
} else {
value = measurement.ScaledLabel(numLabel, "", "")
}
values = append(values, value)
}
return values
}

View File

@@ -0,0 +1,385 @@
package driver
import (
"fmt"
"strings"
"testing"
"m7s.live/v5/plugin/debug/pkg/internal/proftest"
"m7s.live/v5/plugin/debug/pkg/profile"
)
const mainBinary = "/bin/main"
var cpuF = []*profile.Function{
{ID: 1, Name: "main", SystemName: "main", Filename: "main.c"},
{ID: 2, Name: "foo", SystemName: "foo", Filename: "foo.c"},
{ID: 3, Name: "foo_caller", SystemName: "foo_caller", Filename: "foo.c"},
{ID: 4, Name: "bar", SystemName: "bar", Filename: "bar.c"},
}
var cpuM = []*profile.Mapping{
{
ID: 1,
Start: 0x10000,
Limit: 0x40000,
File: mainBinary,
HasFunctions: true,
HasFilenames: true,
HasLineNumbers: true,
HasInlineFrames: true,
},
{
ID: 2,
Start: 0x1000,
Limit: 0x4000,
File: "/lib/lib.so",
HasFunctions: true,
HasFilenames: true,
HasLineNumbers: true,
HasInlineFrames: true,
},
}
var cpuL = []*profile.Location{
{
ID: 1000,
Mapping: cpuM[1],
Address: 0x1000,
Line: []profile.Line{
{Function: cpuF[0], Line: 1},
},
},
{
ID: 2000,
Mapping: cpuM[0],
Address: 0x2000,
Line: []profile.Line{
{Function: cpuF[1], Line: 2},
{Function: cpuF[2], Line: 1},
},
},
{
ID: 3000,
Mapping: cpuM[0],
Address: 0x3000,
Line: []profile.Line{
{Function: cpuF[1], Line: 2},
{Function: cpuF[2], Line: 1},
},
},
{
ID: 3001,
Mapping: cpuM[0],
Address: 0x3001,
Line: []profile.Line{
{Function: cpuF[2], Line: 2},
},
},
{
ID: 3002,
Mapping: cpuM[0],
Address: 0x3002,
Line: []profile.Line{
{Function: cpuF[2], Line: 3},
},
},
{
ID: 3003,
Mapping: cpuM[0],
Address: 0x3003,
Line: []profile.Line{
{Function: cpuF[3], Line: 1},
},
},
}
var testProfile1 = &profile.Profile{
TimeNanos: 10000,
PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
Period: 1,
DurationNanos: 10e9,
SampleType: []*profile.ValueType{
{Type: "samples", Unit: "count"},
{Type: "cpu", Unit: "milliseconds"},
},
Sample: []*profile.Sample{
{
Location: []*profile.Location{cpuL[0]},
Value: []int64{1000, 1000},
Label: map[string][]string{
"key1": {"tag1"},
"key2": {"tag1"},
},
},
{
Location: []*profile.Location{cpuL[1], cpuL[0]},
Value: []int64{100, 100},
Label: map[string][]string{
"key1": {"tag2"},
"key3": {"tag2"},
},
},
{
Location: []*profile.Location{cpuL[2], cpuL[0]},
Value: []int64{10, 10},
Label: map[string][]string{
"key1": {"tag3"},
"key2": {"tag2"},
},
NumLabel: map[string][]int64{
"allocations": {1024},
},
NumUnit: map[string][]string{
"allocations": {""},
},
},
{
Location: []*profile.Location{cpuL[3], cpuL[0]},
Value: []int64{10000, 10000},
Label: map[string][]string{
"key1": {"tag4"},
"key2": {"tag1"},
},
NumLabel: map[string][]int64{
"allocations": {1024, 2048},
},
NumUnit: map[string][]string{
"allocations": {"bytes", "b"},
},
},
{
Location: []*profile.Location{cpuL[4], cpuL[0]},
Value: []int64{1, 1},
Label: map[string][]string{
"key1": {"tag4"},
"key2": {"tag1", "tag5"},
},
NumLabel: map[string][]int64{
"allocations": {1024, 1},
},
NumUnit: map[string][]string{
"allocations": {"byte", "kilobyte"},
},
},
{
Location: []*profile.Location{cpuL[5], cpuL[0]},
Value: []int64{200, 200},
NumLabel: map[string][]int64{
"allocations": {1024},
},
},
},
Location: cpuL,
Function: cpuF,
Mapping: cpuM,
}
func TestAddLabelNodesMatchBooleans(t *testing.T) {
type addLabelNodesTestcase struct {
name string
tagroot, tagleaf []string
outputUnit string
rootm, leafm bool
// wantSampleFuncs contains expected stack functions and sample value after
// adding nodes, in the same order as in the profile. The format is as
// returned by stackCollapse function, which is "callee caller: <num>".
wantSampleFuncs []string
}
for _, tc := range []addLabelNodesTestcase{
{
name: "Without tagroot or tagleaf, add no extra nodes, and should not match",
wantSampleFuncs: []string{
"main(main.c) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c) 10",
"main(main.c);foo_caller(foo.c) 10000",
"main(main.c);foo_caller(foo.c) 1",
"main(main.c);bar(bar.c) 200",
},
},
{
name: "Keys that aren't found add empty nodes, and should not match",
tagroot: []string{"key404"},
tagleaf: []string{"key404"},
wantSampleFuncs: []string{
"(key404);main(main.c);(key404) 1000",
"(key404);main(main.c);foo(foo.c);foo_caller(foo.c);(key404) 100",
"(key404);main(main.c);foo(foo.c);foo_caller(foo.c);(key404) 10",
"(key404);main(main.c);foo_caller(foo.c);(key404) 10000",
"(key404);main(main.c);foo_caller(foo.c);(key404) 1",
"(key404);main(main.c);bar(bar.c);(key404) 200",
},
},
{
name: "tagroot adds nodes for key1 and reports a match",
tagroot: []string{"key1"},
rootm: true,
wantSampleFuncs: []string{
"tag1(key1);main(main.c) 1000",
"tag2(key1);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
"tag3(key1);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
"tag4(key1);main(main.c);foo_caller(foo.c) 10000",
"tag4(key1);main(main.c);foo_caller(foo.c) 1",
"(key1);main(main.c);bar(bar.c) 200",
},
},
{
name: "tagroot adds nodes for key2 and reports a match",
tagroot: []string{"key2"},
rootm: true,
wantSampleFuncs: []string{
"tag1(key2);main(main.c) 1000",
"(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
"tag2(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
"tag1(key2);main(main.c);foo_caller(foo.c) 10000",
"tag1,tag5(key2);main(main.c);foo_caller(foo.c) 1",
"(key2);main(main.c);bar(bar.c) 200",
},
},
{
name: "tagleaf adds nodes for key1 and reports a match",
tagleaf: []string{"key1"},
leafm: true,
wantSampleFuncs: []string{
"main(main.c);tag1(key1) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c);tag2(key1) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c);tag3(key1) 10",
"main(main.c);foo_caller(foo.c);tag4(key1) 10000",
"main(main.c);foo_caller(foo.c);tag4(key1) 1",
"main(main.c);bar(bar.c);(key1) 200",
},
},
{
name: "tagleaf adds nodes for key3 and reports a match",
tagleaf: []string{"key3"},
leafm: true,
wantSampleFuncs: []string{
"main(main.c);(key3) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c);tag2(key3) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c);(key3) 10",
"main(main.c);foo_caller(foo.c);(key3) 10000",
"main(main.c);foo_caller(foo.c);(key3) 1",
"main(main.c);bar(bar.c);(key3) 200",
},
},
{
name: "tagroot adds nodes for key1,key2 in order and reports a match",
tagroot: []string{"key1", "key2"},
rootm: true,
wantSampleFuncs: []string{
"tag1(key1);tag1(key2);main(main.c) 1000",
"tag2(key1);(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
"tag3(key1);tag2(key2);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
"tag4(key1);tag1(key2);main(main.c);foo_caller(foo.c) 10000",
"tag4(key1);tag1,tag5(key2);main(main.c);foo_caller(foo.c) 1",
"(key1);(key2);main(main.c);bar(bar.c) 200",
},
},
{
name: "tagleaf adds nodes for key1,key2 in order and reports a match",
tagleaf: []string{"key1", "key2"},
leafm: true,
wantSampleFuncs: []string{
"main(main.c);tag1(key1);tag1(key2) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c);tag2(key1);(key2) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c);tag3(key1);tag2(key2) 10",
"main(main.c);foo_caller(foo.c);tag4(key1);tag1(key2) 10000",
"main(main.c);foo_caller(foo.c);tag4(key1);tag1,tag5(key2) 1",
"main(main.c);bar(bar.c);(key1);(key2) 200",
},
},
{
name: "Numeric units are added with units with tagleaf",
tagleaf: []string{"allocations"},
leafm: true,
wantSampleFuncs: []string{
"main(main.c);(allocations) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
"main(main.c);foo_caller(foo.c);1024B,2048B(allocations) 10000",
"main(main.c);foo_caller(foo.c);1024B,1024B(allocations) 1",
"main(main.c);bar(bar.c);1024(allocations) 200",
},
},
{
name: "Numeric units are added with units with tagroot",
tagroot: []string{"allocations"},
rootm: true,
wantSampleFuncs: []string{
"(allocations);main(main.c) 1000",
"(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 100",
"1024(allocations);main(main.c);foo(foo.c);foo_caller(foo.c) 10",
"1024B,2048B(allocations);main(main.c);foo_caller(foo.c) 10000",
"1024B,1024B(allocations);main(main.c);foo_caller(foo.c) 1",
"1024(allocations);main(main.c);bar(bar.c) 200",
},
},
{
name: "Numeric labels are formatted according to outputUnit",
outputUnit: "kB",
tagleaf: []string{"allocations"},
leafm: true,
wantSampleFuncs: []string{
"main(main.c);(allocations) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
"main(main.c);foo_caller(foo.c);1kB,2kB(allocations) 10000",
"main(main.c);foo_caller(foo.c);1kB,1kB(allocations) 1",
"main(main.c);bar(bar.c);1024(allocations) 200",
},
},
{
name: "Numeric units with no units are handled properly by tagleaf",
tagleaf: []string{"allocations"},
leafm: true,
wantSampleFuncs: []string{
"main(main.c);(allocations) 1000",
"main(main.c);foo(foo.c);foo_caller(foo.c);(allocations) 100",
"main(main.c);foo(foo.c);foo_caller(foo.c);1024(allocations) 10",
"main(main.c);foo_caller(foo.c);1024B,2048B(allocations) 10000",
"main(main.c);foo_caller(foo.c);1024B,1024B(allocations) 1",
"main(main.c);bar(bar.c);1024(allocations) 200",
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
p := testProfile1.Copy()
rootm, leafm := addLabelNodes(p, tc.tagroot, tc.tagleaf, tc.outputUnit)
if rootm != tc.rootm {
t.Errorf("Got rootm=%v, want=%v", rootm, tc.rootm)
}
if leafm != tc.leafm {
t.Errorf("Got leafm=%v, want=%v", leafm, tc.leafm)
}
if got, want := strings.Join(stackCollapse(p), "\n")+"\n", strings.Join(tc.wantSampleFuncs, "\n")+"\n"; got != want {
diff, err := proftest.Diff([]byte(want), []byte(got))
if err != nil {
t.Fatalf("Failed to get diff: %v", err)
}
t.Errorf("Profile samples got diff(want->got):\n%s", diff)
}
})
}
}
// stackCollapse returns a slice of strings where each string represents one
// profile sample in Brendan Gregg's "Folded Stacks" format:
// "<root_fn>(filename);<fun2>(filename);<leaf_fn>(filename) <value>". This
// allows the expected values for test cases to be specified in human-readable
// strings.
func stackCollapse(p *profile.Profile) []string {
var ret []string
for _, s := range p.Sample {
var funcs []string
for i := range s.Location {
loc := s.Location[len(s.Location)-1-i]
for _, line := range loc.Line {
funcs = append(funcs, fmt.Sprintf("%s(%s)", line.Function.Name, line.Function.Filename))
}
}
ret = append(ret, fmt.Sprintf("%s %d", strings.Join(funcs, ";"), s.Value[0]))
}
return ret
}

View File

@@ -0,0 +1,60 @@
// 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 driver
import (
"fmt"
"os"
"path/filepath"
"sync"
)
// newTempFile returns a new output file in dir with the provided prefix and suffix.
func newTempFile(dir, prefix, suffix string) (*os.File, error) {
for index := 1; index < 10000; index++ {
switch f, err := os.OpenFile(filepath.Join(dir, fmt.Sprintf("%s%03d%s", prefix, index, suffix)), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666); {
case err == nil:
return f, nil
case !os.IsExist(err):
return nil, err
}
}
// Give up
return nil, fmt.Errorf("could not create file of the form %s%03d%s", prefix, 1, suffix)
}
var tempFiles []string
var tempFilesMu = sync.Mutex{}
// deferDeleteTempFile marks a file to be deleted by next call to Cleanup()
func deferDeleteTempFile(path string) {
tempFilesMu.Lock()
tempFiles = append(tempFiles, path)
tempFilesMu.Unlock()
}
// cleanupTempFiles removes any temporary files selected for deferred cleaning.
func cleanupTempFiles() error {
tempFilesMu.Lock()
defer tempFilesMu.Unlock()
var lastErr error
for _, f := range tempFiles {
if err := os.Remove(f); err != nil {
lastErr = err
}
}
tempFiles = nil
return lastErr
}

View File

@@ -0,0 +1,55 @@
package driver
import (
"os"
"sync"
"testing"
)
func TestNewTempFile(t *testing.T) {
const n = 100
// Line up ready to execute goroutines with a read-write lock.
var mu sync.RWMutex
mu.Lock()
var wg sync.WaitGroup
errc := make(chan error, n)
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
mu.RLock()
defer mu.RUnlock()
defer wg.Done()
f, err := newTempFile(os.TempDir(), "profile", ".tmp")
errc <- err
deferDeleteTempFile(f.Name())
f.Close()
}()
}
// Start the file creation race.
mu.Unlock()
// Wait for the goroutines to finish.
wg.Wait()
for i := 0; i < n; i++ {
if err := <-errc; err != nil {
t.Fatalf("newTempFile(): got %v, want no error", err)
}
}
if len(tempFiles) != n {
t.Errorf("len(tempFiles): got %d, want %d", len(tempFiles), n)
}
names := map[string]bool{}
for _, name := range tempFiles {
if names[name] {
t.Errorf("got temp file %s created multiple times", name)
break
}
names[name] = true
}
if err := cleanupTempFiles(); err != nil {
t.Errorf("cleanupTempFiles(): got error %v, want no error", err)
}
if len(tempFiles) != 0 {
t.Errorf("len(tempFiles) after the cleanup: got %d, want 0", len(tempFiles))
}
}

View File

@@ -0,0 +1,24 @@
--- contentionz 1 ---
cycles/second = 3201000000
sampling period = 100
ms since reset = 16502830
discarded samples = 0
19490304 27 @ 0xbccc97 0xc61202 0x42ed5f 0x42edc1 0x42e15a 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
768 1 @ 0xbccc97 0xa42dc7 0xa456e4 0x7fcdc2ff214e
5760 2 @ 0xbccc97 0xb82b73 0xb82bcb 0xb87eab 0xb8814c 0x4e969d 0x4faa17 0x4fc5f6 0x4fd028 0x4fd230 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
569088 1 @ 0xbccc97 0xb82b73 0xb82bcb 0xb87f08 0xb8814c 0x42ed5f 0x42edc1 0x42e15a 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
2432 1 @ 0xbccc97 0xb82b73 0xb82bcb 0xb87eab 0xb8814c 0x7aa74c 0x7ab844 0x7ab914 0x79e9e9 0x79e326 0x4d299e 0x4d4b7b 0x4b7be8 0x4b7ff1 0x4d2dae 0x79e80a
2034816 3 @ 0xbccc97 0xb82f0f 0xb83003 0xb87d50 0xc635f0 0x42ecc3 0x42e14c 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
--- Memory map: ---
00400000-00fcb000: cppbench_server_main
7fcdc231e000-7fcdc2321000: /libnss_cache-2.15.so
7fcdc2522000-7fcdc252e000: /libnss_files-2.15.so
7fcdc272f000-7fcdc28dd000: /libc-2.15.so
7fcdc2ae7000-7fcdc2be2000: /libm-2.15.so
7fcdc2de3000-7fcdc2dea000: /librt-2.15.so
7fcdc2feb000-7fcdc3003000: /libpthread-2.15.so
7fcdc3208000-7fcdc320a000: /libdl-2.15.so
7fcdc340c000-7fcdc3415000: /libcrypt-2.15.so
7fcdc3645000-7fcdc3669000: /ld-2.15.so
7fff86bff000-7fff86c00000: [vdso]
ffffffffff600000-ffffffffff601000: [vsyscall]

Binary file not shown.

View File

@@ -0,0 +1,19 @@
--- contentionz 1 ---
cycles/second = 3201000000
sampling period = 100
ms since reset = 16502830
discarded samples = 0
100 10 @ 0xbccc97 0xc61202 0x42ed5f 0x42edc1 0x42e15a 0x5261af 0x526edf 0x5280ab 0x79e80a 0x7a251b 0x7a296d 0xa456e4 0x7fcdc2ff214e
--- Memory map: ---
00400000-00fcb000: cppbench_server_main
7fcdc231e000-7fcdc2321000: /libnss_cache-2.15.so
7fcdc2522000-7fcdc252e000: /libnss_files-2.15.so
7fcdc272f000-7fcdc28dd000: /libc-2.15.so
7fcdc2ae7000-7fcdc2be2000: /libm-2.15.so
7fcdc2de3000-7fcdc2dea000: /librt-2.15.so
7fcdc2feb000-7fcdc3003000: /libpthread-2.15.so
7fcdc3208000-7fcdc320a000: /libdl-2.15.so
7fcdc340c000-7fcdc3415000: /libcrypt-2.15.so
7fcdc3645000-7fcdc3669000: /ld-2.15.so
7fff86bff000-7fff86c00000: [vdso]
ffffffffff600000-ffffffffff601000: [vsyscall]

View File

@@ -0,0 +1,17 @@
line1
line2
line3
line4
line5
line6
line7
line8
line9
line0
line1
line2
line3
line4
line5

View File

@@ -0,0 +1,17 @@
line1
line2
line3
line4
line5
line6
line7
line8
line9
line0
line1
line2
line3
line4
line5

View File

@@ -0,0 +1,17 @@
line1
line2
line3
line4
line5
line6
line7
line8
line9
line0
line1
line2
line3
line4
line5

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
digraph "unnamed" {
node [style=filled fillcolor="#f8f8f8"]
subgraph cluster_L { "Build ID: buildid-contention" [shape=box fontsize=16 label="Build ID: buildid-contention\lComment #1\lComment #2\lType: delay\lShowing nodes accounting for 149.50ms, 100% of 149.50ms total\l\lSee https://git.io/JfYMW for how to read the graph\l"] }
N1 [label="file3000.src\n32.77ms (21.92%)\nof 149.50ms (100%)" id="node1" fontsize=20 shape=box tooltip="testdata/file3000.src (149.50ms)" color="#b20000" fillcolor="#edd5d5"]
N2 [label="file1000.src\n51.20ms (34.25%)" id="node2" fontsize=23 shape=box tooltip="testdata/file1000.src (51.20ms)" color="#b23100" fillcolor="#eddbd5"]
N3 [label="file2000.src\n65.54ms (43.84%)\nof 75.78ms (50.68%)" id="node3" fontsize=24 shape=box tooltip="testdata/file2000.src (75.78ms)" color="#b22000" fillcolor="#edd9d5"]
N1 -> N3 [label=" 75.78ms" weight=51 penwidth=3 color="#b22000" tooltip="testdata/file3000.src -> testdata/file2000.src (75.78ms)" labeltooltip="testdata/file3000.src -> testdata/file2000.src (75.78ms)"]
N1 -> N2 [label=" 40.96ms" weight=28 penwidth=2 color="#b23900" tooltip="testdata/file3000.src -> testdata/file1000.src (40.96ms)" labeltooltip="testdata/file3000.src -> testdata/file1000.src (40.96ms)"]
N3 -> N2 [label=" 10.24ms" weight=7 color="#b29775" tooltip="testdata/file2000.src -> testdata/file1000.src (10.24ms)" labeltooltip="testdata/file2000.src -> testdata/file1000.src (10.24ms)"]
}

View File

@@ -0,0 +1,9 @@
digraph "unnamed" {
node [style=filled fillcolor="#f8f8f8"]
subgraph cluster_L { "Build ID: buildid-contention" [shape=box fontsize=16 label="Build ID: buildid-contention\lComment #1\lComment #2\lType: delay\lActive filters:\l focus=[X1]000\l ignore=[X3]002\lShowing nodes accounting for 40.96ms, 27.40% of 149.50ms total\l\lSee https://git.io/JfYMW for how to read the graph\l"] }
N1 [label="0000000000001000\nline1000\nfile1000.src:1\n40.96ms (27.40%)" id="node1" fontsize=24 shape=box tooltip="0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
N2 [label="0000000000003001\nline3000\nfile3000.src:5\n0 of 40.96ms (27.40%)" id="node2" fontsize=8 shape=box tooltip="0000000000003001 line3000 testdata/file3000.src:5 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
N3 [label="0000000000003001\nline3001\nfile3000.src:3\n0 of 40.96ms (27.40%)" id="node3" fontsize=8 shape=box tooltip="0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)" color="#b23900" fillcolor="#edddd5"]
N2 -> N3 [label=" 40.96ms\n (inline)" weight=28 penwidth=2 color="#b23900" tooltip="0000000000003001 line3000 testdata/file3000.src:5 -> 0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)" labeltooltip="0000000000003001 line3000 testdata/file3000.src:5 -> 0000000000003001 line3001 testdata/file3000.src:3 (40.96ms)"]
N3 -> N1 [label=" 40.96ms" weight=28 penwidth=2 color="#b23900" tooltip="0000000000003001 line3001 testdata/file3000.src:3 -> 0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)" labeltooltip="0000000000003001 line3001 testdata/file3000.src:3 -> 0000000000001000 line1000 testdata/file1000.src:1 (40.96ms)"]
}

View File

@@ -0,0 +1,32 @@
File: testbinary
Type: cpu
Duration: 10s, Total samples = 1.12s (11.20%)
-----------+-------------------------------------------------------
key1: tag1
key2: tag1
1s 0000000000001000 line1000 testdata/file1000.src:1
0000000000002000 line2001 testdata/file2000.src:9 (inline)
0000000000002000 line2000 testdata/file2000.src:4
0000000000003000 line3002 testdata/file3000.src:2 (inline)
0000000000003000 line3001 testdata/file3000.src:5 (inline)
0000000000003000 line3000 testdata/file3000.src:6
-----------+-------------------------------------------------------
key1: tag2
key3: tag2
100ms 0000000000001000 line1000 testdata/file1000.src:1
0000000000003001 line3001 testdata/file3000.src:8 (inline)
0000000000003001 line3000 testdata/file3000.src:9
-----------+-------------------------------------------------------
key1: tag3
key2: tag2
10ms 0000000000002000 line2001 testdata/file2000.src:9 (inline)
0000000000002000 line2000 testdata/file2000.src:4
0000000000003002 line3002 testdata/file3000.src:5 (inline)
0000000000003002 line3000 testdata/file3000.src:9
-----------+-------------------------------------------------------
key1: tag4
key2: tag1
10ms 0000000000003000 line3002 testdata/file3000.src:2 (inline)
0000000000003000 line3001 testdata/file3000.src:5 (inline)
0000000000003000 line3000 testdata/file3000.src:6
-----------+-------------------------------------------------------

View File

@@ -0,0 +1,99 @@
positions: instr line
events: cpu(ms)
ob=(1) /path/to/testbinary
fl=(1) testdata/file1000.src
fn=(1) line1000
0x1000 1 1000
* 1 100
ob=(1)
fl=(2) testdata/file2000.src
fn=(2) line2001
+4096 9 10
ob=(1)
fl=(3) testdata/file3000.src
fn=(3) line3002
+4096 2 10
cfl=(2)
cfn=(4) line2000 [1/2]
calls=0 * 4
* * 1000
ob=(1)
fl=(2)
fn=(5) line2000
-4096 4 0
cfl=(2)
cfn=(6) line2001 [2/2]
calls=0 -4096 9
* * 1000
* 4 0
cfl=(2)
cfn=(7) line2001 [1/2]
calls=0 * 9
* * 10
ob=(1)
fl=(2)
fn=(2)
* 9 0
cfl=(1)
cfn=(8) line1000 [1/2]
calls=0 -4096 1
* * 1000
ob=(1)
fl=(3)
fn=(9) line3000
+4096 6 0
cfl=(3)
cfn=(10) line3001 [1/2]
calls=0 +4096 5
* * 1010
ob=(1)
fl=(3)
fn=(11) line3001
* 5 0
cfl=(3)
cfn=(12) line3002 [1/2]
calls=0 * 2
* * 1010
ob=(1)
fl=(3)
fn=(9)
+1 9 0
cfl=(3)
cfn=(13) line3001 [2/2]
calls=0 +1 8
* * 100
ob=(1)
fl=(3)
fn=(11)
* 8 0
cfl=(1)
cfn=(14) line1000 [2/2]
calls=0 -8193 1
* * 100
ob=(1)
fl=(3)
fn=(9)
+1 9 0
cfl=(3)
cfn=(15) line3002 [2/2]
calls=0 +1 5
* * 10
ob=(1)
fl=(3)
fn=(3)
* 5 0
cfl=(2)
cfn=(16) line2000 [2/2]
calls=0 -4098 4
* * 10

View File

@@ -0,0 +1,88 @@
positions: instr line
events: cpu(ms)
ob=(1) /path/to/testbinary
fl=(1) testdata/file1000.src
fn=(1) line1000
0x1000 1 1100
ob=(1)
fl=(2) testdata/file2000.src
fn=(2) line2001
+4096 9 10
cfl=(1)
cfn=(1)
calls=0 * 1
* * 1000
ob=(1)
fl=(3) testdata/file3000.src
fn=(3) line3002
+4096 2 10
cfl=(2)
cfn=(4) line2000
calls=0 * 4
* * 1000
ob=(1)
fl=(2)
fn=(4)
-4096 4 0
cfl=(2)
cfn=(2)
calls=0 -4096 9
* * 1010
ob=(1)
fl=(3)
fn=(5) line3000
+4096 6 0
cfl=(3)
cfn=(6) line3001
calls=0 +4096 5
* * 1010
ob=(1)
fl=(3)
fn=(6)
* 5 0
cfl=(3)
cfn=(3)
calls=0 * 2
* * 1010
ob=(1)
fl=(3)
fn=(5)
+1 9 0
cfl=(3)
cfn=(6)
calls=0 +1 8
* * 100
ob=(1)
fl=(3)
fn=(6)
* 8 0
cfl=(1)
cfn=(1)
calls=0 -8193 1
* * 100
ob=(1)
fl=(3)
fn=(5)
+1 9 0
cfl=(3)
cfn=(3)
calls=0 +1 5
* * 10
ob=(1)
fl=(3)
fn=(3)
* 5 0
cfl=(2)
cfn=(4)
calls=0 -4098 4
* * 10

View File

@@ -0,0 +1 @@
some-comment

View File

@@ -0,0 +1,8 @@
Active filters:
focus=[12]00
hide=line[X3]0
Showing nodes accounting for 1.11s, 99.11% of 1.12s total
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% line1000 testdata/file1000.src:1
0 0% 98.21% 1.01s 90.18% line2000 testdata/file2000.src:4
0.01s 0.89% 99.11% 1.01s 90.18% line2001 testdata/file2000.src:9 (inline)

View File

@@ -0,0 +1,7 @@
Active filters:
hide=line[X3]0
Showing nodes accounting for 1.11s, 99.11% of 1.12s total
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% line1000 testdata/file1000.src:1
0 0% 98.21% 1.01s 90.18% line2000 testdata/file2000.src:4
0.01s 0.89% 99.11% 1.01s 90.18% line2001 testdata/file2000.src:9 (inline)

View File

@@ -0,0 +1,7 @@
Active filters:
show=[12]00
Showing nodes accounting for 1.11s, 99.11% of 1.12s total
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% line1000 testdata/file1000.src:1
0 0% 98.21% 1.01s 90.18% line2000 testdata/file2000.src:4
0.01s 0.89% 99.11% 1.01s 90.18% line2001 testdata/file2000.src:9 (inline)

View File

@@ -0,0 +1,5 @@
Active filters:
hide=mangled[X3]0
Showing nodes accounting for 1s, 100% of 1s total
flat flat% sum% cum cum%
1s 100% 100% 1s 100% mangled1000 testdata/file1000.src:1

View File

@@ -0,0 +1,16 @@
Active filters:
show_from=line2
Showing nodes accounting for 1.01s, 90.18% of 1.12s total
----------------------------------------------------------+-------------
flat flat% sum% cum cum% calls calls% + context
----------------------------------------------------------+-------------
0 0% 0% 1.01s 90.18% | line2000 testdata/file2000.src:4
1.01s 100% | line2001 testdata/file2000.src:9 (inline)
----------------------------------------------------------+-------------
1.01s 100% | line2000 testdata/file2000.src:4 (inline)
0.01s 0.89% 0.89% 1.01s 90.18% | line2001 testdata/file2000.src:9
1s 99.01% | line1000 testdata/file1000.src:1
----------------------------------------------------------+-------------
1s 100% | line2001 testdata/file2000.src:9
1s 89.29% 90.18% 1s 89.29% | line1000 testdata/file1000.src:1
----------------------------------------------------------+-------------

View File

@@ -0,0 +1,14 @@
Total: 1.12s
ROUTINE ======================== line1000
1.10s 1.10s (flat, cum) 98.21% of Total
1.10s 1.10s 1000: instruction one ;line1000 file1000.src:1
. . 1001: instruction two
. . 1002: instruction three ;line1000 file1000.src:2
. . 1003: instruction four ;line1000 file1000.src:1
ROUTINE ======================== line3000
10ms 1.12s (flat, cum) 100% of Total
10ms 1.01s 3000: instruction one ;line3000 file3000.src:6
. 100ms 3001: instruction two ;line3000 file3000.src:9
. 10ms 3002: instruction three
. . 3003: instruction four ;line3000 file3000.src
. . 3004: instruction five

View File

@@ -0,0 +1,7 @@
Showing nodes accounting for 1.12s, 100% of 1.12s total
Dropped 1 node (cum <= 0.06s)
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% 0000000000001000 line1000 testdata/file1000.src:1
0.01s 0.89% 99.11% 1.01s 90.18% 0000000000002000 line2000 testdata/file2000.src:4
0.01s 0.89% 100% 1.01s 90.18% 0000000000003000 line3000 testdata/file3000.src:6
0 0% 100% 0.10s 8.93% 0000000000003001 line3000 testdata/file3000.src:9

View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>testbinary cpu</title>
<style type="text/css">
body #content{
font-family: sans-serif;
}
h1 {
font-size: 1.5em;
}
.legend {
font-size: 1.25em;
}
.line, .nop, .unimportant {
color: #aaaaaa;
}
.inlinesrc {
color: #000066;
}
.livesrc {
cursor: pointer;
}
.livesrc:hover {
background-color: #eeeeee;
}
.asm {
color: #008800;
display: none;
}
</style>
<script type="text/javascript">
function pprof_toggle_asm(e) {
var target;
if (!e) e = window.event;
if (e.target) target = e.target;
else if (e.srcElement) target = e.srcElement;
if (target) {
var asm = target.nextSibling;
if (asm && asm.className == "asm") {
asm.style.display = (asm.style.display == "block" ? "" : "block");
e.preventDefault();
return false;
}
}
}
</script>
</head>
<body>
<div class="legend">File: testbinary<br>
Type: cpu<br>
Duration: 10s, Total samples = 1.12s (11.20%)<br>Total: 1.12s</div><h2>line1000</h2><p class="filename">testdata/file1000.src</p>
<pre onClick="pprof_toggle_asm(event)">
Total: 1.10s 1.10s (flat, cum) 98.21%
<span class=line> 1</span> <span class=livesrc> 1.10s 1.10s line1 </span><span class=asm> 1.10s 1.10s 1000: instruction one <span class=unimportant>file1000.src:1</span>
. . 1001: instruction two <span class=unimportant>file1000.src:1</span>
. . 1003: instruction four <span class=unimportant>file1000.src:1</span>
</span>
<span class=line> 2</span> <span class=livesrc> . . line2 </span><span class=asm> . . 1002: instruction three <span class=unimportant>file1000.src:2</span>
</span>
<span class=line> 3</span> <span class=nop> . . line3 </span>
<span class=line> 4</span> <span class=nop> . . line4 </span>
<span class=line> 5</span> <span class=nop> . . line5 </span>
<span class=line> 6</span> <span class=nop> . . line6 </span>
<span class=line> 7</span> <span class=nop> . . line7 </span>
</pre>
<h2>line3000</h2><p class="filename">testdata/file3000.src</p>
<pre onClick="pprof_toggle_asm(event)">
Total: 10ms 1.12s (flat, cum) 100%
<span class=line> 1</span> <span class=nop> . . line1 </span>
<span class=line> 2</span> <span class=nop> . . line2 </span>
<span class=line> 3</span> <span class=nop> . . line3 </span>
<span class=line> 4</span> <span class=nop> . . line4 </span>
<span class=line> 5</span> <span class=nop> . . line5 </span>
<span class=line> 6</span> <span class=livesrc> 10ms 1.01s line6 </span><span class=asm> <span class=inlinesrc> line5 </span> <span class=unimportant>file3000.src:5</span>
<span class=inlinesrc> line2 </span> <span class=unimportant>file3000.src:2</span>
10ms 1.01s 3000: instruction one <span class=unimportant>file3000.src:2</span>
</span>
<span class=line> 7</span> <span class=nop> . . line7 </span>
<span class=line> 8</span> <span class=nop> . . line8 </span>
<span class=line> 9</span> <span class=livesrc> . 110ms line9 </span><span class=asm> <span class=inlinesrc> line8 </span> <span class=unimportant>file3000.src:8</span>
. 100ms 3001: instruction two <span class=unimportant>file3000.src:8</span>
<span class=inlinesrc> line5 </span> <span class=unimportant>file3000.src:5</span>
. 10ms 3002: instruction three <span class=unimportant>file3000.src:5</span>
. . 3003: instruction four <span class=unimportant></span>
. . 3004: instruction five <span class=unimportant></span>
</span>
<span class=line> 10</span> <span class=nop> . . line0 </span>
<span class=line> 11</span> <span class=nop> . . line1 </span>
<span class=line> 12</span> <span class=nop> . . line2 </span>
<span class=line> 13</span> <span class=nop> . . line3 </span>
<span class=line> 14</span> <span class=nop> . . line4 </span>
</pre>
</body>
</html>

View File

@@ -0,0 +1,5 @@
Showing nodes accounting for 1.12s, 100% of 1.12s total
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% line1000 testdata/file1000.src
0.01s 0.89% 99.11% 1.01s 90.18% line2000 testdata/file2000.src
0.01s 0.89% 100% 1.12s 100% line3000 testdata/file3000.src

View File

@@ -0,0 +1,21 @@
digraph "testbinary" {
node [style=filled fillcolor="#f8f8f8"]
subgraph cluster_L { "File: testbinary" [shape=box fontsize=16 label="File: testbinary\lType: cpu\lDuration: 10s, Total samples = 1.12s (11.20%)\lShowing nodes accounting for 1.11s, 99.11% of 1.12s total\lDropped 3 nodes (cum <= 0.06s)\l\lSee https://git.io/JfYMW for how to read the graph\l" tooltip="testbinary"] }
N1 [label="line1000\n1s (89.29%)" id="node1" fontsize=24 shape=box tooltip="line1000 (1s)" color="#b20500" fillcolor="#edd6d5"]
N1_0 [label = "key1:tag1\nkey2:tag1" id="N1_0" fontsize=8 shape=box3d tooltip="1s"]
N1 -> N1_0 [label=" 1s" weight=100 tooltip="1s" labeltooltip="1s"]
N2 [label="line3000\n0 of 1.12s (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (1.12s)" color="#b20000" fillcolor="#edd5d5"]
N3 [label="line3001\n0 of 1.11s (99.11%)" id="node3" fontsize=8 shape=box tooltip="line3001 (1.11s)" color="#b20000" fillcolor="#edd5d5"]
N4 [label="line1000\n0.10s (8.93%)" id="node4" fontsize=14 shape=box tooltip="line1000 (0.10s)" color="#b28b62" fillcolor="#ede8e2"]
N4_0 [label = "key1:tag2\nkey3:tag2" id="N4_0" fontsize=8 shape=box3d tooltip="0.10s"]
N4 -> N4_0 [label=" 0.10s" weight=100 tooltip="0.10s" labeltooltip="0.10s"]
N5 [label="line3002\n0.01s (0.89%)\nof 1.01s (90.18%)" id="node5" fontsize=10 shape=box tooltip="line3002 (1.01s)" color="#b20500" fillcolor="#edd6d5"]
N6 [label="line2000\n0 of 1s (89.29%)" id="node6" fontsize=8 shape=box tooltip="line2000 (1s)" color="#b20500" fillcolor="#edd6d5"]
N7 [label="line2001\n0 of 1s (89.29%)" id="node7" fontsize=8 shape=box tooltip="line2001 (1s)" color="#b20500" fillcolor="#edd6d5"]
N2 -> N3 [label=" 1.11s\n (inline)" weight=100 penwidth=5 color="#b20000" tooltip="line3000 -> line3001 (1.11s)" labeltooltip="line3000 -> line3001 (1.11s)"]
N3 -> N5 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line3001 -> line3002 (1.01s)" labeltooltip="line3001 -> line3002 (1.01s)"]
N6 -> N7 [label=" 1s\n (inline)" weight=90 penwidth=5 color="#b20500" tooltip="line2000 -> line2001 (1s)" labeltooltip="line2000 -> line2001 (1s)"]
N7 -> N1 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line2001 -> line1000 (1s)" labeltooltip="line2001 -> line1000 (1s)"]
N5 -> N6 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line3002 -> line2000 (1s)" labeltooltip="line3002 -> line2000 (1s)"]
N3 -> N4 [label=" 0.10s" weight=9 color="#b28b62" tooltip="line3001 -> line1000 (0.10s)" labeltooltip="line3001 -> line1000 (0.10s)"]
}

View File

@@ -0,0 +1,20 @@
digraph "testbinary" {
node [style=filled fillcolor="#f8f8f8"]
subgraph cluster_L { "File: testbinary" [shape=box fontsize=16 label="File: testbinary\lType: cpu\lDuration: 10s, Total samples = 1.12s (11.20%)\lShowing nodes accounting for 1.12s, 100% of 1.12s total\l\lSee https://git.io/JfYMW for how to read the graph\l" tooltip="testbinary"] }
N1 [label="line1000\n1.10s (98.21%)" id="node1" fontsize=24 shape=box tooltip="line1000 (1.10s)" color="#b20000" fillcolor="#edd5d5"]
N1_0 [label = "key1:tag1\nkey2:tag1" id="N1_0" fontsize=8 shape=box3d tooltip="1s"]
N1 -> N1_0 [label=" 1s" weight=100 tooltip="1s" labeltooltip="1s"]
N1_1 [label = "key1:tag2\nkey3:tag2" id="N1_1" fontsize=8 shape=box3d tooltip="0.10s"]
N1 -> N1_1 [label=" 0.10s" weight=100 tooltip="0.10s" labeltooltip="0.10s"]
N2 [label="line3000\n0 of 1.12s (100%)" id="node2" fontsize=8 shape=box tooltip="line3000 (1.12s)" color="#b20000" fillcolor="#edd5d5"]
N3 [label="line3001\n0 of 1.11s (99.11%)" id="node3" fontsize=8 shape=box tooltip="line3001 (1.11s)" color="#b20000" fillcolor="#edd5d5"]
N4 [label="line3002\n0.01s (0.89%)\nof 1.02s (91.07%)" id="node4" fontsize=10 shape=box tooltip="line3002 (1.02s)" color="#b20400" fillcolor="#edd6d5"]
N5 [label="line2001\n0.01s (0.89%)\nof 1.01s (90.18%)" id="node5" fontsize=10 shape=box tooltip="line2001 (1.01s)" color="#b20500" fillcolor="#edd6d5"]
N6 [label="line2000\n0 of 1.01s (90.18%)" id="node6" fontsize=8 shape=box tooltip="line2000 (1.01s)" color="#b20500" fillcolor="#edd6d5"]
N2 -> N3 [label=" 1.11s\n (inline)" weight=100 penwidth=5 color="#b20000" tooltip="line3000 -> line3001 (1.11s)" labeltooltip="line3000 -> line3001 (1.11s)"]
N6 -> N5 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line2000 -> line2001 (1.01s)" labeltooltip="line2000 -> line2001 (1.01s)"]
N3 -> N4 [label=" 1.01s\n (inline)" weight=91 penwidth=5 color="#b20500" tooltip="line3001 -> line3002 (1.01s)" labeltooltip="line3001 -> line3002 (1.01s)"]
N4 -> N6 [label=" 1.01s" weight=91 penwidth=5 color="#b20500" tooltip="line3002 -> line2000 (1.01s)" labeltooltip="line3002 -> line2000 (1.01s)"]
N5 -> N1 [label=" 1s" weight=90 penwidth=5 color="#b20500" tooltip="line2001 -> line1000 (1s)" labeltooltip="line2001 -> line1000 (1s)"]
N3 -> N1 [label=" 0.10s" weight=9 color="#b28b62" tooltip="line3001 -> line1000 (0.10s)" labeltooltip="line3001 -> line1000 (0.10s)"]
}

View File

@@ -0,0 +1,5 @@
Showing nodes accounting for 1.12s, 100% of 1.12s total
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% line1000
0.01s 0.89% 99.11% 1.01s 90.18% line2000
0.01s 0.89% 100% 1.12s 100% line3000

View File

@@ -0,0 +1,8 @@
Showing nodes accounting for 1.12s, 100% of 1.12s total
flat flat% sum% cum cum%
1.10s 98.21% 98.21% 1.10s 98.21% line1000
0.01s 0.89% 99.11% 1.01s 90.18% line2001 (inline)
0.01s 0.89% 100% 1.02s 91.07% line3002 (inline)
0 0% 100% 1.01s 90.18% line2000
0 0% 100% 1.12s 100% line3000
0 0% 100% 1.11s 99.11% line3001 (inline)

View File

@@ -0,0 +1,3 @@
Showing nodes accounting for 1s, 100% of 1s total
flat flat% sum% cum cum%
1s 100% 100% 1s 100% mangled1000 testdata/file1000.src:1

View File

@@ -0,0 +1,13 @@
Showing nodes accounting for 1.12s, 100% of 1.12s total
----------------------------------------------------------+-------------
flat flat% sum% cum cum% calls calls% + context
----------------------------------------------------------+-------------
1.01s 100% | line2000 (inline)
0.01s 0.89% 0.89% 1.01s 90.18% | line2001
1s 99.01% | line1000
----------------------------------------------------------+-------------
1.11s 100% | line3000 (inline)
0 0% 0.89% 1.11s 99.11% | line3001
1.01s 90.99% | line3002 (inline)
0.10s 9.01% | line1000
----------------------------------------------------------+-------------

View File

@@ -0,0 +1,13 @@
key1: Total 1.1s
1.0s (89.29%): tag1
100.0ms ( 8.93%): tag2
10.0ms ( 0.89%): tag3
10.0ms ( 0.89%): tag4
key2: Total 1.0s
1.0s (99.02%): tag1
10.0ms ( 0.98%): tag2
key3: Total 100.0ms
100.0ms ( 100%): tag2

View File

@@ -0,0 +1,6 @@
key1: Total 100.0ms
100.0ms ( 100%): tag2
key3: Total 100.0ms
100.0ms ( 100%): tag2

View File

@@ -0,0 +1,32 @@
File: testbinary
Type: cpu
Duration: 10s, Total samples = 1.12s (11.20%)
-----------+-------------------------------------------------------
key1: tag1
key2: tag1
1s line1000
line2001 (inline)
line2000
line3002 (inline)
line3001 (inline)
line3000
-----------+-------------------------------------------------------
key1: tag2
key3: tag2
100ms line1000
line3001 (inline)
line3000
-----------+-------------------------------------------------------
key1: tag3
key2: tag2
10ms line2001 (inline)
line2000
line3002 (inline)
line3000
-----------+-------------------------------------------------------
key1: tag4
key2: tag1
10ms line3002 (inline)
line3001 (inline)
line3000
-----------+-------------------------------------------------------

View File

@@ -0,0 +1,17 @@
Showing nodes accounting for 4s, 100% of 4s total
Showing top 4 nodes out of 5
----------------------------------------------------------+-------------
flat flat% sum% cum cum% calls calls% + context
----------------------------------------------------------+-------------
1s 100% | 0000000000003000 [testbinary]
1s 25.00% 25.00% 1s 25.00% | 0000000000001000 [testbinary]
----------------------------------------------------------+-------------
1s 25.00% 50.00% 2s 50.00% | 0000000000003000 [testbinary]
1s 50.00% | 0000000000001000 [testbinary]
----------------------------------------------------------+-------------
1s 100% | 0000000000005000 [testbinary]
1s 25.00% 75.00% 1s 25.00% | 0000000000004000 [testbinary]
----------------------------------------------------------+-------------
1s 25.00% 100% 2s 50.00% | 0000000000005000 [testbinary]
1s 50.00% | 0000000000004000 [testbinary]
----------------------------------------------------------+-------------

View File

@@ -0,0 +1,88 @@
positions: instr line
events: inuse_space(MB)
ob=
fl=(1) testdata/file2000.src
fn=(1) line2001
0x2000 2 62
cfl=(2) testdata/file1000.src
cfn=(2) line1000
calls=0 0x1000 1
* * 0
ob=
fl=(3) testdata/file3000.src
fn=(3) line3002
+4096 3 31
cfl=(1)
cfn=(4) line2000
calls=0 * 3
* * 0
ob=
fl=(2)
fn=(2)
-8192 1 4
ob=
fl=(1)
fn=(4)
+4096 3 0
cfl=(1)
cfn=(1)
calls=0 +4096 2
* * 63
ob=
fl=(3)
fn=(5) line3000
+4096 4 0
cfl=(3)
cfn=(6) line3001
calls=0 +4096 2
* * 32
ob=
fl=(3)
fn=(6)
* 2 0
cfl=(3)
cfn=(3)
calls=0 * 3
* * 32
ob=
fl=(3)
fn=(5)
+1 4 0
cfl=(3)
cfn=(6)
calls=0 +1 2
* * 3
ob=
fl=(3)
fn=(6)
* 2 0
cfl=(2)
cfn=(2)
calls=0 -8193 1
* * 3
ob=
fl=(3)
fn=(5)
+1 4 0
cfl=(3)
cfn=(3)
calls=0 +1 3
* * 62
ob=
fl=(3)
fn=(3)
* 3 0
cfl=(1)
cfn=(4)
calls=0 -4098 3
* * 62

View File

@@ -0,0 +1,2 @@
comment
#hidden comment

Some files were not shown because too many files have changed in this diff Show More