github.com/grafana/pyroscope-go/godeltaprof@v0.1.8-0.20240513050943-1b1f97373e2a/internal/pprof/proto.go (about)

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package pprof
     6  
     7  import (
     8  	"bytes"
     9  	"io"
    10  	"os"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  )
    16  
    17  // lostProfileEvent is the function to which lost profiling
    18  // events are attributed.
    19  // (The name shows up in the pprof graphs.)
    20  func lostProfileEvent() { lostProfileEvent() }
    21  
    22  type ProfileBuilderOptions struct {
    23  	// for go1.21+ if true - use runtime_FrameSymbolName - produces frames with generic types, for example [go.shape.int]
    24  	// for go1.21+ if false - use runtime.Frame->Function - produces frames with generic types ommited [...]
    25  	// pre 1.21 - always use runtime.Frame->Function - produces frames with generic types ommited [...]
    26  	GenericsFrames bool
    27  	LazyMapping    bool
    28  }
    29  
    30  // A profileBuilder writes a profile incrementally from a
    31  // stream of profile samples delivered by the runtime.
    32  type profileBuilder struct {
    33  	start      time.Time
    34  	end        time.Time
    35  	havePeriod bool
    36  	period     int64
    37  
    38  	// encoding state
    39  	w         io.Writer
    40  	zw        gzipWriter
    41  	pb        protobuf
    42  	strings   []string
    43  	stringMap map[string]int
    44  	locs      map[uintptr]locInfo // list of locInfo starting with the given PC.
    45  	funcs     map[string]int      // Package path-qualified function name to Function.ID
    46  	mem       []memMap
    47  	deck      pcDeck
    48  
    49  	opt ProfileBuilderOptions
    50  }
    51  
    52  type memMap struct {
    53  	// initialized as reading mapping
    54  	start   uintptr // Address at which the binary (or DLL) is loaded into memory.
    55  	end     uintptr // The limit of the address range occupied by this mapping.
    56  	offset  uint64  // Offset in the binary that corresponds to the first mapped address.
    57  	file    string  // The object this entry is loaded from.
    58  	buildID string  // A string that uniquely identifies a particular program version with high probability.
    59  
    60  	funcs symbolizeFlag
    61  	fake  bool // map entry was faked; /proc/self/maps wasn't available
    62  }
    63  
    64  // symbolizeFlag keeps track of symbolization result.
    65  //
    66  //	0                  : no symbol lookup was performed
    67  //	1<<0 (lookupTried) : symbol lookup was performed
    68  //	1<<1 (lookupFailed): symbol lookup was performed but failed
    69  type symbolizeFlag uint8
    70  
    71  const (
    72  	lookupTried  symbolizeFlag = 1 << iota
    73  	lookupFailed symbolizeFlag = 1 << iota
    74  )
    75  
    76  const (
    77  	// message Profile
    78  	tagProfile_SampleType        = 1  // repeated ValueType
    79  	tagProfile_Sample            = 2  // repeated Sample
    80  	tagProfile_Mapping           = 3  // repeated Mapping
    81  	tagProfile_Location          = 4  // repeated Location
    82  	tagProfile_Function          = 5  // repeated Function
    83  	tagProfile_StringTable       = 6  // repeated string
    84  	tagProfile_DropFrames        = 7  // int64 (string table index)
    85  	tagProfile_KeepFrames        = 8  // int64 (string table index)
    86  	tagProfile_TimeNanos         = 9  // int64
    87  	tagProfile_DurationNanos     = 10 // int64
    88  	tagProfile_PeriodType        = 11 // ValueType (really optional string???)
    89  	tagProfile_Period            = 12 // int64
    90  	tagProfile_Comment           = 13 // repeated int64
    91  	tagProfile_DefaultSampleType = 14 // int64
    92  
    93  	// message ValueType
    94  	tagValueType_Type = 1 // int64 (string table index)
    95  	tagValueType_Unit = 2 // int64 (string table index)
    96  
    97  	// message Sample
    98  	tagSample_Location = 1 // repeated uint64
    99  	tagSample_Value    = 2 // repeated int64
   100  	tagSample_Label    = 3 // repeated Label
   101  
   102  	// message Label
   103  	tagLabel_Key = 1 // int64 (string table index)
   104  	tagLabel_Str = 2 // int64 (string table index)
   105  	tagLabel_Num = 3 // int64
   106  
   107  	// message Mapping
   108  	tagMapping_ID              = 1  // uint64
   109  	tagMapping_Start           = 2  // uint64
   110  	tagMapping_Limit           = 3  // uint64
   111  	tagMapping_Offset          = 4  // uint64
   112  	tagMapping_Filename        = 5  // int64 (string table index)
   113  	tagMapping_BuildID         = 6  // int64 (string table index)
   114  	tagMapping_HasFunctions    = 7  // bool
   115  	tagMapping_HasFilenames    = 8  // bool
   116  	tagMapping_HasLineNumbers  = 9  // bool
   117  	tagMapping_HasInlineFrames = 10 // bool
   118  
   119  	// message Location
   120  	tagLocation_ID        = 1 // uint64
   121  	tagLocation_MappingID = 2 // uint64
   122  	tagLocation_Address   = 3 // uint64
   123  	tagLocation_Line      = 4 // repeated Line
   124  
   125  	// message Line
   126  	tagLine_FunctionID = 1 // uint64
   127  	tagLine_Line       = 2 // int64
   128  
   129  	// message Function
   130  	tagFunction_ID         = 1 // uint64
   131  	tagFunction_Name       = 2 // int64 (string table index)
   132  	tagFunction_SystemName = 3 // int64 (string table index)
   133  	tagFunction_Filename   = 4 // int64 (string table index)
   134  	tagFunction_StartLine  = 5 // int64
   135  )
   136  
   137  // stringIndex adds s to the string table if not already present
   138  // and returns the index of s in the string table.
   139  func (b *profileBuilder) stringIndex(s string) int64 {
   140  	id, ok := b.stringMap[s]
   141  	if !ok {
   142  		id = len(b.strings)
   143  		b.strings = append(b.strings, s)
   144  		b.stringMap[s] = id
   145  	}
   146  	return int64(id)
   147  }
   148  
   149  func (b *profileBuilder) flush() {
   150  	const dataFlush = 4096
   151  	if b.pb.nest == 0 && len(b.pb.data) > dataFlush {
   152  		b.zw.Write(b.pb.data)
   153  		b.pb.data = b.pb.data[:0]
   154  	}
   155  }
   156  
   157  // pbValueType encodes a ValueType message to b.pb.
   158  func (b *profileBuilder) pbValueType(tag int, typ, unit string) {
   159  	start := b.pb.startMessage()
   160  	b.pb.int64(tagValueType_Type, b.stringIndex(typ))
   161  	b.pb.int64(tagValueType_Unit, b.stringIndex(unit))
   162  	b.pb.endMessage(tag, start)
   163  }
   164  
   165  // pbSample encodes a Sample message to b.pb.
   166  func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) {
   167  	start := b.pb.startMessage()
   168  	b.pb.int64s(tagSample_Value, values)
   169  	b.pb.uint64s(tagSample_Location, locs)
   170  	if labels != nil {
   171  		labels()
   172  	}
   173  	b.pb.endMessage(tagProfile_Sample, start)
   174  	b.flush()
   175  }
   176  
   177  // pbLabel encodes a Label message to b.pb.
   178  func (b *profileBuilder) pbLabel(tag int, key, str string, num int64) {
   179  	start := b.pb.startMessage()
   180  	b.pb.int64Opt(tagLabel_Key, b.stringIndex(key))
   181  	b.pb.int64Opt(tagLabel_Str, b.stringIndex(str))
   182  	b.pb.int64Opt(tagLabel_Num, num)
   183  	b.pb.endMessage(tag, start)
   184  }
   185  
   186  // pbLine encodes a Line message to b.pb.
   187  func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) {
   188  	start := b.pb.startMessage()
   189  	b.pb.uint64Opt(tagLine_FunctionID, funcID)
   190  	b.pb.int64Opt(tagLine_Line, line)
   191  	b.pb.endMessage(tag, start)
   192  }
   193  
   194  // pbMapping encodes a Mapping message to b.pb.
   195  func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file, buildID string, hasFuncs bool) {
   196  	start := b.pb.startMessage()
   197  	b.pb.uint64Opt(tagMapping_ID, id)
   198  	b.pb.uint64Opt(tagMapping_Start, base)
   199  	b.pb.uint64Opt(tagMapping_Limit, limit)
   200  	b.pb.uint64Opt(tagMapping_Offset, offset)
   201  	b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file))
   202  	b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID))
   203  	// TODO: we set HasFunctions if all symbols from samples were symbolized (hasFuncs).
   204  	// Decide what to do about HasInlineFrames and HasLineNumbers.
   205  	// Also, another approach to handle the mapping entry with
   206  	// incomplete symbolization results is to dupliace the mapping
   207  	// entry (but with different Has* fields values) and use
   208  	// different entries for symbolized locations and unsymbolized locations.
   209  	if hasFuncs {
   210  		b.pb.bool(tagMapping_HasFunctions, true)
   211  	}
   212  	b.pb.endMessage(tag, start)
   213  }
   214  
   215  func allFrames(addr uintptr) ([]runtime.Frame, symbolizeFlag) {
   216  	// Expand this one address using CallersFrames so we can cache
   217  	// each expansion. In general, CallersFrames takes a whole
   218  	// stack, but in this case we know there will be no skips in
   219  	// the stack and we have return PCs anyway.
   220  	frames := runtime.CallersFrames([]uintptr{addr})
   221  	frame, more := frames.Next()
   222  	if frame.Function == "runtime.goexit" {
   223  		// Short-circuit if we see runtime.goexit so the loop
   224  		// below doesn't allocate a useless empty location.
   225  		return nil, 0
   226  	}
   227  
   228  	symbolizeResult := lookupTried
   229  	if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 {
   230  		symbolizeResult |= lookupFailed
   231  	}
   232  
   233  	if frame.PC == 0 {
   234  		// If we failed to resolve the frame, at least make up
   235  		// a reasonable call PC. This mostly happens in tests.
   236  		frame.PC = addr - 1
   237  	}
   238  	ret := []runtime.Frame{frame}
   239  	for frame.Function != "runtime.goexit" && more {
   240  		frame, more = frames.Next()
   241  		ret = append(ret, frame)
   242  	}
   243  	return ret, symbolizeResult
   244  }
   245  
   246  type locInfo struct {
   247  	// location id assigned by the profileBuilder
   248  	id uint64
   249  
   250  	// sequence of PCs, including the fake PCs returned by the traceback
   251  	// to represent inlined functions
   252  	// https://github.com/golang/go/blob/d6f2f833c93a41ec1c68e49804b8387a06b131c5/src/runtime/traceback.go#L347-L368
   253  	pcs []uintptr
   254  
   255  	// firstPCFrames and firstPCSymbolizeResult hold the results of the
   256  	// allFrames call for the first (leaf-most) PC this locInfo represents
   257  	firstPCFrames          []runtime.Frame
   258  	firstPCSymbolizeResult symbolizeFlag
   259  }
   260  
   261  // newProfileBuilder returns a new profileBuilder.
   262  // CPU profiling data obtained from the runtime can be added
   263  // by calling b.addCPUData, and then the eventual profile
   264  // can be obtained by calling b.finish.
   265  func newProfileBuilder(w io.Writer, opt ProfileBuilderOptions, mapping []memMap) *profileBuilder {
   266  	zw := newGzipWriter(w)
   267  	b := &profileBuilder{
   268  		w:         w,
   269  		zw:        zw,
   270  		start:     time.Now(),
   271  		strings:   []string{""},
   272  		stringMap: map[string]int{"": 0},
   273  		locs:      map[uintptr]locInfo{},
   274  		funcs:     map[string]int{},
   275  		opt:       opt,
   276  	}
   277  	b.mem = mapping
   278  	return b
   279  }
   280  
   281  // build completes and returns the constructed profile.
   282  func (b *profileBuilder) build() {
   283  	b.end = time.Now()
   284  
   285  	b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano())
   286  	if b.havePeriod { // must be CPU profile
   287  		b.pbValueType(tagProfile_SampleType, "samples", "count")
   288  		b.pbValueType(tagProfile_SampleType, "cpu", "nanoseconds")
   289  		b.pb.int64Opt(tagProfile_DurationNanos, b.end.Sub(b.start).Nanoseconds())
   290  		b.pbValueType(tagProfile_PeriodType, "cpu", "nanoseconds")
   291  		b.pb.int64Opt(tagProfile_Period, b.period)
   292  	}
   293  
   294  	for i, m := range b.mem {
   295  		hasFunctions := m.funcs == lookupTried // lookupTried but not lookupFailed
   296  		b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(m.start), uint64(m.end), m.offset, m.file, m.buildID, hasFunctions)
   297  	}
   298  
   299  	// TODO: Anything for tagProfile_DropFrames?
   300  	// TODO: Anything for tagProfile_KeepFrames?
   301  
   302  	b.pb.strings(tagProfile_StringTable, b.strings)
   303  	b.zw.Write(b.pb.data)
   304  	b.zw.Close()
   305  }
   306  
   307  // appendLocsForStack appends the location IDs for the given stack trace to the given
   308  // location ID slice, locs. The addresses in the stack are return PCs or 1 + the PC of
   309  // an inline marker as the runtime traceback function returns.
   310  //
   311  // It may return an empty slice even if locs is non-empty, for example if locs consists
   312  // solely of runtime.goexit. We still count these empty stacks in profiles in order to
   313  // get the right cumulative sample count.
   314  //
   315  // It may emit to b.pb, so there must be no message encoding in progress.
   316  func (b *profileBuilder) appendLocsForStack(locs []uint64, stk []uintptr) (newLocs []uint64) {
   317  	b.deck.reset()
   318  
   319  	// The last frame might be truncated. Recover lost inline frames.
   320  	stk = runtime_expandFinalInlineFrame(stk)
   321  
   322  	for len(stk) > 0 {
   323  		addr := stk[0]
   324  		if l, ok := b.locs[addr]; ok {
   325  			// When generating code for an inlined function, the compiler adds
   326  			// NOP instructions to the outermost function as a placeholder for
   327  			// each layer of inlining. When the runtime generates tracebacks for
   328  			// stacks that include inlined functions, it uses the addresses of
   329  			// those NOPs as "fake" PCs on the stack as if they were regular
   330  			// function call sites. But if a profiling signal arrives while the
   331  			// CPU is executing one of those NOPs, its PC will show up as a leaf
   332  			// in the profile with its own Location entry. So, always check
   333  			// whether addr is a "fake" PC in the context of the current call
   334  			// stack by trying to add it to the inlining deck before assuming
   335  			// that the deck is complete.
   336  			if len(b.deck.pcs) > 0 {
   337  				if added := b.deck.tryAdd(addr, l.firstPCFrames, l.firstPCSymbolizeResult); added {
   338  					stk = stk[1:]
   339  					continue
   340  				}
   341  			}
   342  
   343  			// first record the location if there is any pending accumulated info.
   344  			if id := b.emitLocation(); id > 0 {
   345  				locs = append(locs, id)
   346  			}
   347  
   348  			// then, record the cached location.
   349  			locs = append(locs, l.id)
   350  
   351  			// Skip the matching pcs.
   352  			//
   353  			// Even if stk was truncated due to the stack depth
   354  			// limit, expandFinalInlineFrame above has already
   355  			// fixed the truncation, ensuring it is long enough.
   356  			stk = stk[len(l.pcs):]
   357  			continue
   358  		}
   359  
   360  		frames, symbolizeResult := allFrames(addr)
   361  		if len(frames) == 0 { // runtime.goexit.
   362  			if id := b.emitLocation(); id > 0 {
   363  				locs = append(locs, id)
   364  			}
   365  			stk = stk[1:]
   366  			continue
   367  		}
   368  
   369  		if added := b.deck.tryAdd(addr, frames, symbolizeResult); added {
   370  			stk = stk[1:]
   371  			continue
   372  		}
   373  		// add failed because this addr is not inlined with the
   374  		// existing PCs in the deck. Flush the deck and retry handling
   375  		// this pc.
   376  		if id := b.emitLocation(); id > 0 {
   377  			locs = append(locs, id)
   378  		}
   379  
   380  		// check cache again - previous emitLocation added a new entry
   381  		if l, ok := b.locs[addr]; ok {
   382  			locs = append(locs, l.id)
   383  			stk = stk[len(l.pcs):] // skip the matching pcs.
   384  		} else {
   385  			b.deck.tryAdd(addr, frames, symbolizeResult) // must succeed.
   386  			stk = stk[1:]
   387  		}
   388  	}
   389  	if id := b.emitLocation(); id > 0 { // emit remaining location.
   390  		locs = append(locs, id)
   391  	}
   392  	return locs
   393  }
   394  
   395  // Here's an example of how Go 1.17 writes out inlined functions, compiled for
   396  // linux/amd64. The disassembly of main.main shows two levels of inlining: main
   397  // calls b, b calls a, a does some work.
   398  //
   399  //   inline.go:9   0x4553ec  90              NOPL                 // func main()    { b(v) }
   400  //   inline.go:6   0x4553ed  90              NOPL                 // func b(v *int) { a(v) }
   401  //   inline.go:5   0x4553ee  48c7002a000000  MOVQ $0x2a, 0(AX)    // func a(v *int) { *v = 42 }
   402  //
   403  // If a profiling signal arrives while executing the MOVQ at 0x4553ee (for line
   404  // 5), the runtime will report the stack as the MOVQ frame being called by the
   405  // NOPL at 0x4553ed (for line 6) being called by the NOPL at 0x4553ec (for line
   406  // 9).
   407  //
   408  // The role of pcDeck is to collapse those three frames back into a single
   409  // location at 0x4553ee, with file/line/function symbolization info representing
   410  // the three layers of calls. It does that via sequential calls to pcDeck.tryAdd
   411  // starting with the leaf-most address. The fourth call to pcDeck.tryAdd will be
   412  // for the caller of main.main. Because main.main was not inlined in its caller,
   413  // the deck will reject the addition, and the fourth PC on the stack will get
   414  // its own location.
   415  
   416  // pcDeck is a helper to detect a sequence of inlined functions from
   417  // a stack trace returned by the runtime.
   418  //
   419  // The stack traces returned by runtime's trackback functions are fully
   420  // expanded (at least for Go functions) and include the fake pcs representing
   421  // inlined functions. The profile proto expects the inlined functions to be
   422  // encoded in one Location message.
   423  // https://github.com/google/pprof/blob/5e965273ee43930341d897407202dd5e10e952cb/proto/profile.proto#L177-L184
   424  //
   425  // Runtime does not directly expose whether a frame is for an inlined function
   426  // and looking up debug info is not ideal, so we use a heuristic to filter
   427  // the fake pcs and restore the inlined and entry functions. Inlined functions
   428  // have the following properties:
   429  //
   430  //	Frame's Func is nil (note: also true for non-Go functions), and
   431  //	Frame's Entry matches its entry function frame's Entry (note: could also be true for recursive calls and non-Go functions), and
   432  //	Frame's Name does not match its entry function frame's name (note: inlined functions cannot be directly recursive).
   433  //
   434  // As reading and processing the pcs in a stack trace one by one (from leaf to the root),
   435  // we use pcDeck to temporarily hold the observed pcs and their expanded frames
   436  // until we observe the entry function frame.
   437  type pcDeck struct {
   438  	pcs             []uintptr
   439  	frames          []runtime.Frame
   440  	symbolizeResult symbolizeFlag
   441  
   442  	// firstPCFrames indicates the number of frames associated with the first
   443  	// (leaf-most) PC in the deck
   444  	firstPCFrames int
   445  	// firstPCSymbolizeResult holds the results of the allFrames call for the
   446  	// first (leaf-most) PC in the deck
   447  	firstPCSymbolizeResult symbolizeFlag
   448  }
   449  
   450  func (d *pcDeck) reset() {
   451  	d.pcs = d.pcs[:0]
   452  	d.frames = d.frames[:0]
   453  	d.symbolizeResult = 0
   454  	d.firstPCFrames = 0
   455  	d.firstPCSymbolizeResult = 0
   456  }
   457  
   458  // tryAdd tries to add the pc and Frames expanded from it (most likely one,
   459  // since the stack trace is already fully expanded) and the symbolizeResult
   460  // to the deck. If it fails the caller needs to flush the deck and retry.
   461  func (d *pcDeck) tryAdd(pc uintptr, frames []runtime.Frame, symbolizeResult symbolizeFlag) (success bool) {
   462  	if existing := len(d.frames); existing > 0 {
   463  		// 'd.frames' are all expanded from one 'pc' and represent all
   464  		// inlined functions so we check only the last one.
   465  		newFrame := frames[0]
   466  		last := d.frames[existing-1]
   467  		if last.Func != nil { // the last frame can't be inlined. Flush.
   468  			return false
   469  		}
   470  		if last.Entry == 0 || newFrame.Entry == 0 { // Possibly not a Go function. Don't try to merge.
   471  			return false
   472  		}
   473  
   474  		if last.Entry != newFrame.Entry { // newFrame is for a different function.
   475  			return false
   476  		}
   477  		if last.Function == newFrame.Function { // maybe recursion.
   478  			return false
   479  		}
   480  	}
   481  	d.pcs = append(d.pcs, pc)
   482  	d.frames = append(d.frames, frames...)
   483  	d.symbolizeResult |= symbolizeResult
   484  	if len(d.pcs) == 1 {
   485  		d.firstPCFrames = len(d.frames)
   486  		d.firstPCSymbolizeResult = symbolizeResult
   487  	}
   488  	return true
   489  }
   490  
   491  // emitLocation emits the new location and function information recorded in the deck
   492  // and returns the location ID encoded in the profile protobuf.
   493  // It emits to b.pb, so there must be no message encoding in progress.
   494  // It resets the deck.
   495  func (b *profileBuilder) emitLocation() uint64 {
   496  	if len(b.deck.pcs) == 0 {
   497  		return 0
   498  	}
   499  	defer b.deck.reset()
   500  
   501  	addr := b.deck.pcs[0]
   502  	firstFrame := b.deck.frames[0]
   503  
   504  	// We can't write out functions while in the middle of the
   505  	// Location message, so record new functions we encounter and
   506  	// write them out after the Location.
   507  	type newFunc struct {
   508  		id         uint64
   509  		name, file string
   510  		startLine  int64
   511  	}
   512  	newFuncs := make([]newFunc, 0, 8)
   513  
   514  	id := uint64(len(b.locs)) + 1
   515  	b.locs[addr] = locInfo{
   516  		id:                     id,
   517  		pcs:                    append([]uintptr{}, b.deck.pcs...),
   518  		firstPCSymbolizeResult: b.deck.firstPCSymbolizeResult,
   519  		firstPCFrames:          append([]runtime.Frame{}, b.deck.frames[:b.deck.firstPCFrames]...),
   520  	}
   521  
   522  	start := b.pb.startMessage()
   523  	b.pb.uint64Opt(tagLocation_ID, id)
   524  	b.pb.uint64Opt(tagLocation_Address, uint64(firstFrame.PC))
   525  	for _, frame := range b.deck.frames {
   526  		// Write out each line in frame expansion.
   527  		funcID := uint64(b.funcs[frame.Function])
   528  		if funcID == 0 {
   529  			funcID = uint64(len(b.funcs)) + 1
   530  			b.funcs[frame.Function] = int(funcID)
   531  			var name string
   532  			if b.opt.GenericsFrames {
   533  				name = runtime_FrameSymbolName(&frame)
   534  			} else {
   535  				name = frame.Function
   536  			}
   537  			newFuncs = append(newFuncs, newFunc{
   538  				id:        funcID,
   539  				name:      name,
   540  				file:      frame.File,
   541  				startLine: int64(runtime_FrameStartLine(&frame)),
   542  			})
   543  		}
   544  		b.pbLine(tagLocation_Line, funcID, int64(frame.Line))
   545  	}
   546  	for i := range b.mem {
   547  		if b.mem[i].start <= addr && addr < b.mem[i].end || b.mem[i].fake {
   548  			b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1))
   549  
   550  			m := b.mem[i]
   551  			m.funcs |= b.deck.symbolizeResult
   552  			b.mem[i] = m
   553  			break
   554  		}
   555  	}
   556  	b.pb.endMessage(tagProfile_Location, start)
   557  
   558  	// Write out functions we found during frame expansion.
   559  	for _, fn := range newFuncs {
   560  		start := b.pb.startMessage()
   561  		b.pb.uint64Opt(tagFunction_ID, fn.id)
   562  		b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name))
   563  		b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name))
   564  		b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file))
   565  		b.pb.int64Opt(tagFunction_StartLine, fn.startLine)
   566  		b.pb.endMessage(tagProfile_Function, start)
   567  	}
   568  
   569  	b.flush()
   570  	return id
   571  }
   572  
   573  func readMapping() []memMap {
   574  	data, _ := os.ReadFile("/proc/self/maps")
   575  	var mem []memMap
   576  	parseProcSelfMaps(data, func(lo, hi, offset uint64, file, buildID string) {
   577  		mem = append(mem, memMap{
   578  			start:   uintptr(lo),
   579  			end:     uintptr(hi),
   580  			offset:  offset,
   581  			file:    file,
   582  			buildID: buildID,
   583  			fake:    false,
   584  		})
   585  	})
   586  	if len(mem) == 0 { // pprof expects a map entry, so fake one.
   587  		mem = []memMap{{
   588  			start:   uintptr(0),
   589  			end:     uintptr(0),
   590  			offset:  0,
   591  			file:    "",
   592  			buildID: "",
   593  			fake:    true,
   594  		}}
   595  	}
   596  	return mem
   597  }
   598  
   599  var space = []byte(" ")
   600  var newline = []byte("\n")
   601  
   602  func parseProcSelfMaps(data []byte, addMapping func(lo, hi, offset uint64, file, buildID string)) {
   603  	// $ cat /proc/self/maps
   604  	// 00400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat
   605  	// 0060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat
   606  	// 0060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat
   607  	// 014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
   608  	// 7f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
   609  	// 7f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   610  	// 7f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   611  	// 7f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   612  	// 7f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
   613  	// 7f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
   614  	// 7f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   615  	// 7f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
   616  	// 7f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
   617  	// 7f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   618  	// 7f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
   619  	// 7f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
   620  	// 7ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
   621  	// 7ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
   622  	// ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
   623  
   624  	var line []byte
   625  	// next removes and returns the next field in the line.
   626  	// It also removes from line any spaces following the field.
   627  	next := func() []byte {
   628  		var f []byte
   629  		f, line, _ = bytesCut(line, space)
   630  		line = bytes.TrimLeft(line, " ")
   631  		return f
   632  	}
   633  
   634  	for len(data) > 0 {
   635  		line, data, _ = bytesCut(data, newline)
   636  		addr := next()
   637  		loStr, hiStr, ok := stringsCut(string(addr), "-")
   638  		if !ok {
   639  			continue
   640  		}
   641  		lo, err := strconv.ParseUint(loStr, 16, 64)
   642  		if err != nil {
   643  			continue
   644  		}
   645  		hi, err := strconv.ParseUint(hiStr, 16, 64)
   646  		if err != nil {
   647  			continue
   648  		}
   649  		perm := next()
   650  		if len(perm) < 4 || perm[2] != 'x' {
   651  			// Only interested in executable mappings.
   652  			continue
   653  		}
   654  		offset, err := strconv.ParseUint(string(next()), 16, 64)
   655  		if err != nil {
   656  			continue
   657  		}
   658  		next()          // dev
   659  		inode := next() // inode
   660  		if line == nil {
   661  			continue
   662  		}
   663  		file := string(line)
   664  
   665  		// Trim deleted file marker.
   666  		deletedStr := " (deleted)"
   667  		deletedLen := len(deletedStr)
   668  		if len(file) >= deletedLen && file[len(file)-deletedLen:] == deletedStr {
   669  			file = file[:len(file)-deletedLen]
   670  		}
   671  
   672  		if len(inode) == 1 && inode[0] == '0' && file == "" {
   673  			// Huge-page text mappings list the initial fragment of
   674  			// mapped but unpopulated memory as being inode 0.
   675  			// Don't report that part.
   676  			// But [vdso] and [vsyscall] are inode 0, so let non-empty file names through.
   677  			continue
   678  		}
   679  
   680  		// TODO: pprof's remapMappingIDs makes one adjustment:
   681  		// 1. If there is an /anon_hugepage mapping first and it is
   682  		// consecutive to a next mapping, drop the /anon_hugepage.
   683  		// There's no indication why this is needed.
   684  		// Let's try not doing this and see what breaks.
   685  		// If we do need it, it would go here, before we
   686  		// enter the mappings into b.mem in the first place.
   687  
   688  		buildID, _ := elfBuildID(file)
   689  		addMapping(lo, hi, offset, file, buildID)
   690  	}
   691  }
   692  
   693  // Cut slices s around the first instance of sep,
   694  // returning the text before and after sep.
   695  // The found result reports whether sep appears in s.
   696  // If sep does not appear in s, cut returns s, nil, false.
   697  //
   698  // Cut returns slices of the original slice s, not copies.
   699  func bytesCut(s, sep []byte) (before, after []byte, found bool) {
   700  	if i := bytes.Index(s, sep); i >= 0 {
   701  		return s[:i], s[i+len(sep):], true
   702  	}
   703  	return s, nil, false
   704  }
   705  
   706  // Cut slices s around the first instance of sep,
   707  // returning the text before and after sep.
   708  // The found result reports whether sep appears in s.
   709  // If sep does not appear in s, cut returns s, "", false.
   710  func stringsCut(s, sep string) (before, after string, found bool) {
   711  	if i := strings.Index(s, sep); i >= 0 {
   712  		return s[:i], s[i+len(sep):], true
   713  	}
   714  	return s, "", false
   715  }