mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
384 lines
8.7 KiB
Go
384 lines
8.7 KiB
Go
//go:build darwin || linux || windows
|
|
|
|
package oomprofile
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
type stackRecord struct {
|
|
Stack []uintptr
|
|
}
|
|
|
|
type memProfileRecord struct {
|
|
AllocBytes, FreeBytes int64
|
|
AllocObjects, FreeObjects int64
|
|
Stack []uintptr
|
|
}
|
|
|
|
func (r *memProfileRecord) InUseBytes() int64 {
|
|
return r.AllocBytes - r.FreeBytes
|
|
}
|
|
|
|
func (r *memProfileRecord) InUseObjects() int64 {
|
|
return r.AllocObjects - r.FreeObjects
|
|
}
|
|
|
|
type blockProfileRecord struct {
|
|
Count int64
|
|
Cycles int64
|
|
Stack []uintptr
|
|
}
|
|
|
|
type label struct {
|
|
key string
|
|
value string
|
|
}
|
|
|
|
type labelSet struct {
|
|
list []label
|
|
}
|
|
|
|
type labelMap struct {
|
|
labelSet
|
|
}
|
|
|
|
func WriteFile(destPath string, name string) (string, error) {
|
|
writer, ok := profileWriters[name]
|
|
if !ok {
|
|
return "", fmt.Errorf("unsupported profile %q", name)
|
|
}
|
|
|
|
filePath := filepath.Join(destPath, name+".pb")
|
|
file, err := os.Create(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer file.Close()
|
|
|
|
if err := writer(file); err != nil {
|
|
_ = os.Remove(filePath)
|
|
return "", err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
_ = os.Remove(filePath)
|
|
return "", err
|
|
}
|
|
return filePath, nil
|
|
}
|
|
|
|
var profileWriters = map[string]func(io.Writer) error{
|
|
"allocs": writeAlloc,
|
|
"block": writeBlock,
|
|
"goroutine": writeGoroutine,
|
|
"heap": writeHeap,
|
|
"mutex": writeMutex,
|
|
"threadcreate": writeThreadCreate,
|
|
}
|
|
|
|
func writeHeap(w io.Writer) error {
|
|
return writeHeapInternal(w, "")
|
|
}
|
|
|
|
func writeAlloc(w io.Writer) error {
|
|
return writeHeapInternal(w, "alloc_space")
|
|
}
|
|
|
|
func writeHeapInternal(w io.Writer, defaultSampleType string) error {
|
|
var profile []memProfileRecord
|
|
n, _ := runtimeMemProfileInternal(nil, true)
|
|
var ok bool
|
|
for {
|
|
profile = make([]memProfileRecord, n+50)
|
|
n, ok = runtimeMemProfileInternal(profile, true)
|
|
if ok {
|
|
profile = profile[:n]
|
|
break
|
|
}
|
|
}
|
|
return writeHeapProto(w, profile, int64(runtime.MemProfileRate), defaultSampleType)
|
|
}
|
|
|
|
func writeGoroutine(w io.Writer) error {
|
|
return writeRuntimeProfile(w, "goroutine", runtimeGoroutineProfileWithLabels)
|
|
}
|
|
|
|
func writeThreadCreate(w io.Writer) error {
|
|
return writeRuntimeProfile(w, "threadcreate", func(p []stackRecord, _ []unsafe.Pointer) (int, bool) {
|
|
return runtimeThreadCreateInternal(p)
|
|
})
|
|
}
|
|
|
|
func writeRuntimeProfile(w io.Writer, name string, fetch func([]stackRecord, []unsafe.Pointer) (int, bool)) error {
|
|
var profile []stackRecord
|
|
var labels []unsafe.Pointer
|
|
|
|
n, _ := fetch(nil, nil)
|
|
var ok bool
|
|
for {
|
|
profile = make([]stackRecord, n+10)
|
|
labels = make([]unsafe.Pointer, n+10)
|
|
n, ok = fetch(profile, labels)
|
|
if ok {
|
|
profile = profile[:n]
|
|
labels = labels[:n]
|
|
break
|
|
}
|
|
}
|
|
|
|
return writeCountProfile(w, name, &runtimeProfile{profile, labels})
|
|
}
|
|
|
|
func writeBlock(w io.Writer) error {
|
|
return writeCycleProfile(w, "contentions", "delay", runtimeBlockProfileInternal)
|
|
}
|
|
|
|
func writeMutex(w io.Writer) error {
|
|
return writeCycleProfile(w, "contentions", "delay", runtimeMutexProfileInternal)
|
|
}
|
|
|
|
func writeCycleProfile(w io.Writer, countName string, cycleName string, fetch func([]blockProfileRecord) (int, bool)) error {
|
|
var profile []blockProfileRecord
|
|
n, _ := fetch(nil)
|
|
var ok bool
|
|
for {
|
|
profile = make([]blockProfileRecord, n+50)
|
|
n, ok = fetch(profile)
|
|
if ok {
|
|
profile = profile[:n]
|
|
break
|
|
}
|
|
}
|
|
|
|
sort.Slice(profile, func(i, j int) bool {
|
|
return profile[i].Cycles > profile[j].Cycles
|
|
})
|
|
|
|
builder := newProfileBuilder(w)
|
|
builder.pbValueType(tagProfile_PeriodType, countName, "count")
|
|
builder.pb.int64Opt(tagProfile_Period, 1)
|
|
builder.pbValueType(tagProfile_SampleType, countName, "count")
|
|
builder.pbValueType(tagProfile_SampleType, cycleName, "nanoseconds")
|
|
|
|
cpuGHz := float64(runtimeCyclesPerSecond()) / 1e9
|
|
values := []int64{0, 0}
|
|
var locs []uint64
|
|
expandedStack := runtimeMakeProfStack()
|
|
for _, record := range profile {
|
|
values[0] = record.Count
|
|
if cpuGHz > 0 {
|
|
values[1] = int64(float64(record.Cycles) / cpuGHz)
|
|
} else {
|
|
values[1] = 0
|
|
}
|
|
n := expandInlinedFrames(expandedStack, record.Stack)
|
|
locs = builder.appendLocsForStack(locs[:0], expandedStack[:n])
|
|
builder.pbSample(values, locs, nil)
|
|
}
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
type countProfile interface {
|
|
Len() int
|
|
Stack(i int) []uintptr
|
|
Label(i int) *labelMap
|
|
}
|
|
|
|
type runtimeProfile struct {
|
|
stk []stackRecord
|
|
labels []unsafe.Pointer
|
|
}
|
|
|
|
func (p *runtimeProfile) Len() int {
|
|
return len(p.stk)
|
|
}
|
|
|
|
func (p *runtimeProfile) Stack(i int) []uintptr {
|
|
return p.stk[i].Stack
|
|
}
|
|
|
|
func (p *runtimeProfile) Label(i int) *labelMap {
|
|
return (*labelMap)(p.labels[i])
|
|
}
|
|
|
|
func writeCountProfile(w io.Writer, name string, profile countProfile) error {
|
|
var buf strings.Builder
|
|
key := func(stk []uintptr, labels *labelMap) string {
|
|
buf.Reset()
|
|
buf.WriteByte('@')
|
|
for _, pc := range stk {
|
|
fmt.Fprintf(&buf, " %#x", pc)
|
|
}
|
|
if labels != nil {
|
|
buf.WriteString("\n# labels:")
|
|
for _, label := range labels.list {
|
|
fmt.Fprintf(&buf, " %q:%q", label.key, label.value)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
counts := make(map[string]int)
|
|
index := make(map[string]int)
|
|
var keys []string
|
|
for i := 0; i < profile.Len(); i++ {
|
|
k := key(profile.Stack(i), profile.Label(i))
|
|
if counts[k] == 0 {
|
|
index[k] = i
|
|
keys = append(keys, k)
|
|
}
|
|
counts[k]++
|
|
}
|
|
|
|
sort.Sort(&keysByCount{keys: keys, count: counts})
|
|
|
|
builder := newProfileBuilder(w)
|
|
builder.pbValueType(tagProfile_PeriodType, name, "count")
|
|
builder.pb.int64Opt(tagProfile_Period, 1)
|
|
builder.pbValueType(tagProfile_SampleType, name, "count")
|
|
|
|
values := []int64{0}
|
|
var locs []uint64
|
|
for _, k := range keys {
|
|
values[0] = int64(counts[k])
|
|
idx := index[k]
|
|
locs = builder.appendLocsForStack(locs[:0], profile.Stack(idx))
|
|
|
|
var labels func()
|
|
if profile.Label(idx) != nil {
|
|
labels = func() {
|
|
for _, label := range profile.Label(idx).list {
|
|
builder.pbLabel(tagSample_Label, label.key, label.value, 0)
|
|
}
|
|
}
|
|
}
|
|
builder.pbSample(values, locs, labels)
|
|
}
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
type keysByCount struct {
|
|
keys []string
|
|
count map[string]int
|
|
}
|
|
|
|
func (x *keysByCount) Len() int {
|
|
return len(x.keys)
|
|
}
|
|
|
|
func (x *keysByCount) Swap(i int, j int) {
|
|
x.keys[i], x.keys[j] = x.keys[j], x.keys[i]
|
|
}
|
|
|
|
func (x *keysByCount) Less(i int, j int) bool {
|
|
ki, kj := x.keys[i], x.keys[j]
|
|
ci, cj := x.count[ki], x.count[kj]
|
|
if ci != cj {
|
|
return ci > cj
|
|
}
|
|
return ki < kj
|
|
}
|
|
|
|
func expandInlinedFrames(dst []uintptr, pcs []uintptr) int {
|
|
frames := runtime.CallersFrames(pcs)
|
|
var n int
|
|
for n < len(dst) {
|
|
frame, more := frames.Next()
|
|
dst[n] = frame.PC + 1
|
|
n++
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func writeHeapProto(w io.Writer, profile []memProfileRecord, rate int64, defaultSampleType string) error {
|
|
builder := newProfileBuilder(w)
|
|
builder.pbValueType(tagProfile_PeriodType, "space", "bytes")
|
|
builder.pb.int64Opt(tagProfile_Period, rate)
|
|
builder.pbValueType(tagProfile_SampleType, "alloc_objects", "count")
|
|
builder.pbValueType(tagProfile_SampleType, "alloc_space", "bytes")
|
|
builder.pbValueType(tagProfile_SampleType, "inuse_objects", "count")
|
|
builder.pbValueType(tagProfile_SampleType, "inuse_space", "bytes")
|
|
if defaultSampleType != "" {
|
|
builder.pb.int64Opt(tagProfile_DefaultSampleType, builder.stringIndex(defaultSampleType))
|
|
}
|
|
|
|
values := []int64{0, 0, 0, 0}
|
|
var locs []uint64
|
|
for _, record := range profile {
|
|
hideRuntime := true
|
|
for tries := 0; tries < 2; tries++ {
|
|
stk := record.Stack
|
|
if hideRuntime {
|
|
for i, addr := range stk {
|
|
if f := runtime.FuncForPC(addr); f != nil && (strings.HasPrefix(f.Name(), "runtime.") || strings.HasPrefix(f.Name(), "internal/runtime/")) {
|
|
continue
|
|
}
|
|
stk = stk[i:]
|
|
break
|
|
}
|
|
}
|
|
locs = builder.appendLocsForStack(locs[:0], stk)
|
|
if len(locs) > 0 {
|
|
break
|
|
}
|
|
hideRuntime = false
|
|
}
|
|
|
|
values[0], values[1] = scaleHeapSample(record.AllocObjects, record.AllocBytes, rate)
|
|
values[2], values[3] = scaleHeapSample(record.InUseObjects(), record.InUseBytes(), rate)
|
|
|
|
var blockSize int64
|
|
if record.AllocObjects > 0 {
|
|
blockSize = record.AllocBytes / record.AllocObjects
|
|
}
|
|
builder.pbSample(values, locs, func() {
|
|
if blockSize != 0 {
|
|
builder.pbLabel(tagSample_Label, "bytes", "", blockSize)
|
|
}
|
|
})
|
|
}
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
func scaleHeapSample(count int64, size int64, rate int64) (int64, int64) {
|
|
if count == 0 || size == 0 {
|
|
return 0, 0
|
|
}
|
|
if rate <= 1 {
|
|
return count, size
|
|
}
|
|
|
|
avgSize := float64(size) / float64(count)
|
|
scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
|
|
return int64(float64(count) * scale), int64(float64(size) * scale)
|
|
}
|
|
|
|
type profileBuilder struct {
|
|
start time.Time
|
|
w io.Writer
|
|
err error
|
|
|
|
pb protobuf
|
|
strings []string
|
|
stringMap map[string]int
|
|
locs map[uintptr]locInfo
|
|
funcs map[string]int
|
|
mem []memMap
|
|
deck pcDeck
|
|
}
|