blob: 4c0a0fefd876000c62dcf211ed2c4cdd85dfd57b [file] [log] [blame] [edit]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !ios
package pprof
import (
"bufio"
"bytes"
"fmt"
"internal/abi"
"internal/testenv"
"os"
"os/exec"
"strconv"
"strings"
"testing"
"time"
)
func TestVMInfo(t *testing.T) {
var begin, end, offset uint64
var filename string
first := true
machVMInfo(func(lo, hi, off uint64, file, buildID string) {
if first {
begin = lo
end = hi
offset = off
filename = file
}
// May see multiple text segments if rosetta is used for running
// the go toolchain itself.
first = false
})
lo, hi, err := useVMMapWithRetry(t)
if err != nil {
t.Fatal(err)
}
if got, want := begin, lo; got != want {
t.Errorf("got %x, want %x", got, want)
}
if got, want := end, hi; got != want {
t.Errorf("got %x, want %x", got, want)
}
if got, want := offset, uint64(0); got != want {
t.Errorf("got %x, want %x", got, want)
}
if !strings.HasSuffix(filename, "pprof.test") {
t.Errorf("got %s, want pprof.test", filename)
}
addr := uint64(abi.FuncPCABIInternal(TestVMInfo))
if addr < lo || addr > hi {
t.Errorf("%x..%x does not contain function %p (%x)", lo, hi, TestVMInfo, addr)
}
}
type mapping struct {
hi, lo uint64
err error
}
func useVMMapWithRetry(t *testing.T) (hi, lo uint64, err error) {
var retryable bool
ch := make(chan mapping)
go func() {
for {
hi, lo, retryable, err = useVMMap(t)
if err == nil {
ch <- mapping{hi, lo, nil}
return
}
if !retryable {
ch <- mapping{0, 0, err}
return
}
t.Logf("retrying vmmap after error: %v", err)
}
}()
select {
case m := <-ch:
return m.hi, m.lo, m.err
case <-time.After(time.Minute):
t.Skip("vmmap taking too long")
}
return 0, 0, fmt.Errorf("unreachable")
}
func useVMMap(t *testing.T) (hi, lo uint64, retryable bool, err error) {
pid := strconv.Itoa(os.Getpid())
testenv.MustHaveExecPath(t, "vmmap")
cmd := testenv.Command(t, "vmmap", pid)
out, cmdErr := cmd.Output()
if cmdErr != nil {
t.Logf("vmmap output: %s", out)
if ee, ok := cmdErr.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
t.Logf("%v: %v\n%s", cmd, cmdErr, ee.Stderr)
if testing.Short() && strings.Contains(string(ee.Stderr), "No process corpse slots currently available, waiting to get one") {
t.Skipf("Skipping knwn flake in short test mode")
}
retryable = bytes.Contains(ee.Stderr, []byte("resource shortage"))
}
t.Logf("%v: %v\n", cmd, cmdErr)
if retryable {
return 0, 0, true, cmdErr
}
}
// Always parse the output of vmmap since it may return an error
// code even if it successfully reports the text segment information
// required for this test.
hi, lo, err = parseVmmap(out)
if err != nil {
if cmdErr != nil {
return 0, 0, false, fmt.Errorf("failed to parse vmmap output, vmmap reported an error: %v", err)
}
t.Logf("vmmap output: %s", out)
return 0, 0, false, fmt.Errorf("failed to parse vmmap output, vmmap did not report an error: %v", err)
}
return hi, lo, false, nil
}
// parseVmmap parses the output of vmmap and calls addMapping for the first r-x TEXT segment in the output.
func parseVmmap(data []byte) (hi, lo uint64, err error) {
// vmmap 53799
// Process: gopls [53799]
// Path: /Users/USER/*/gopls
// Load Address: 0x1029a0000
// Identifier: gopls
// Version: ???
// Code Type: ARM64
// Platform: macOS
// Parent Process: Code Helper (Plugin) [53753]
//
// Date/Time: 2023-05-25 09:45:49.331 -0700
// Launch Time: 2023-05-23 09:35:37.514 -0700
// OS Version: macOS 13.3.1 (22E261)
// Report Version: 7
// Analysis Tool: /Applications/Xcode.app/Contents/Developer/usr/bin/vmmap
// Analysis Tool Version: Xcode 14.3 (14E222b)
//
// Physical footprint: 1.2G
// Physical footprint (peak): 1.2G
// Idle exit: untracked
// ----
//
// Virtual Memory Map of process 53799 (gopls)
// Output report format: 2.4 -64-bit process
// VM page size: 16384 bytes
//
// ==== Non-writable regions for process 53799
// REGION TYPE START END [ VSIZE RSDNT DIRTY SWAP] PRT/MAX SHRMOD PURGE REGION DETAIL
// __TEXT 1029a0000-1033bc000 [ 10.1M 7360K 0K 0K] r-x/rwx SM=COW /Users/USER/*/gopls
// __DATA_CONST 1033bc000-1035bc000 [ 2048K 2000K 0K 0K] r--/rwSM=COW /Users/USER/*/gopls
// __DATA_CONST 1035bc000-103a48000 [ 4656K 3824K 0K 0K] r--/rwSM=COW /Users/USER/*/gopls
// __LINKEDIT 103b00000-103c98000 [ 1632K 1616K 0K 0K] r--/r-SM=COW /Users/USER/*/gopls
// dyld private memory 103cd8000-103cdc000 [ 16K 0K 0K 0K] ---/--SM=NUL
// shared memory 103ce4000-103ce8000 [ 16K 16K 16K 0K] r--/r-SM=SHM
// MALLOC metadata 103ce8000-103cec000 [ 16K 16K 16K 0K] r--/rwx SM=COW DefaultMallocZone_0x103ce8000 zone structure
// MALLOC guard page 103cf0000-103cf4000 [ 16K 0K 0K 0K] ---/rwx SM=COW
// MALLOC guard page 103cfc000-103d00000 [ 16K 0K 0K 0K] ---/rwx SM=COW
// MALLOC guard page 103d00000-103d04000 [ 16K 0K 0K 0K] ---/rwx SM=NUL
banner := "==== Non-writable regions for process"
grabbing := false
sc := bufio.NewScanner(bytes.NewReader(data))
for sc.Scan() {
l := sc.Text()
if grabbing {
p := strings.Fields(l)
if len(p) > 7 && p[0] == "__TEXT" && p[7] == "r-x/rwx" {
locs := strings.Split(p[1], "-")
start, _ := strconv.ParseUint(locs[0], 16, 64)
end, _ := strconv.ParseUint(locs[1], 16, 64)
return start, end, nil
}
}
if strings.HasPrefix(l, banner) {
grabbing = true
}
}
return 0, 0, fmt.Errorf("vmmap no text segment found")
}