1// Copyright 2023 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//go:build !ios
6
7package pprof
8
9import (
10	"bufio"
11	"bytes"
12	"fmt"
13	"internal/abi"
14	"internal/testenv"
15	"os"
16	"os/exec"
17	"strconv"
18	"strings"
19	"testing"
20)
21
22func TestVMInfo(t *testing.T) {
23	var begin, end, offset uint64
24	var filename string
25	first := true
26	machVMInfo(func(lo, hi, off uint64, file, buildID string) {
27		if first {
28			begin = lo
29			end = hi
30			offset = off
31			filename = file
32		}
33		// May see multiple text segments if rosetta is used for running
34		// the go toolchain itself.
35		first = false
36	})
37	lo, hi, err := useVMMapWithRetry(t)
38	if err != nil {
39		t.Fatal(err)
40	}
41	if got, want := begin, lo; got != want {
42		t.Errorf("got %x, want %x", got, want)
43	}
44	if got, want := end, hi; got != want {
45		t.Errorf("got %x, want %x", got, want)
46	}
47	if got, want := offset, uint64(0); got != want {
48		t.Errorf("got %x, want %x", got, want)
49	}
50	if !strings.HasSuffix(filename, "pprof.test") {
51		t.Errorf("got %s, want pprof.test", filename)
52	}
53	addr := uint64(abi.FuncPCABIInternal(TestVMInfo))
54	if addr < lo || addr > hi {
55		t.Errorf("%x..%x does not contain function %p (%x)", lo, hi, TestVMInfo, addr)
56	}
57}
58
59func useVMMapWithRetry(t *testing.T) (hi, lo uint64, err error) {
60	var retryable bool
61	for {
62		hi, lo, retryable, err = useVMMap(t)
63		if err == nil {
64			return hi, lo, nil
65		}
66		if !retryable {
67			return 0, 0, err
68		}
69		t.Logf("retrying vmmap after error: %v", err)
70	}
71}
72
73func useVMMap(t *testing.T) (hi, lo uint64, retryable bool, err error) {
74	pid := strconv.Itoa(os.Getpid())
75	testenv.MustHaveExecPath(t, "vmmap")
76	cmd := testenv.Command(t, "vmmap", pid)
77	out, cmdErr := cmd.Output()
78	if cmdErr != nil {
79		t.Logf("vmmap output: %s", out)
80		if ee, ok := cmdErr.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
81			t.Logf("%v: %v\n%s", cmd, cmdErr, ee.Stderr)
82			if testing.Short() && strings.Contains(string(ee.Stderr), "No process corpse slots currently available, waiting to get one") {
83				t.Skipf("Skipping knwn flake in short test mode")
84			}
85			retryable = bytes.Contains(ee.Stderr, []byte("resource shortage"))
86		}
87		t.Logf("%v: %v\n", cmd, cmdErr)
88		if retryable {
89			return 0, 0, true, cmdErr
90		}
91	}
92	// Always parse the output of vmmap since it may return an error
93	// code even if it successfully reports the text segment information
94	// required for this test.
95	hi, lo, err = parseVmmap(out)
96	if err != nil {
97		if cmdErr != nil {
98			return 0, 0, false, fmt.Errorf("failed to parse vmmap output, vmmap reported an error: %v", err)
99		}
100		t.Logf("vmmap output: %s", out)
101		return 0, 0, false, fmt.Errorf("failed to parse vmmap output, vmmap did not report an error: %v", err)
102	}
103	return hi, lo, false, nil
104}
105
106// parseVmmap parses the output of vmmap and calls addMapping for the first r-x TEXT segment in the output.
107func parseVmmap(data []byte) (hi, lo uint64, err error) {
108	// vmmap 53799
109	// Process:         gopls [53799]
110	// Path:            /Users/USER/*/gopls
111	// Load Address:    0x1029a0000
112	// Identifier:      gopls
113	// Version:         ???
114	// Code Type:       ARM64
115	// Platform:        macOS
116	// Parent Process:  Code Helper (Plugin) [53753]
117	//
118	// Date/Time:       2023-05-25 09:45:49.331 -0700
119	// Launch Time:     2023-05-23 09:35:37.514 -0700
120	// OS Version:      macOS 13.3.1 (22E261)
121	// Report Version:  7
122	// Analysis Tool:   /Applications/Xcode.app/Contents/Developer/usr/bin/vmmap
123	// Analysis Tool Version:  Xcode 14.3 (14E222b)
124	//
125	// Physical footprint:         1.2G
126	// Physical footprint (peak):  1.2G
127	// Idle exit:                  untracked
128	// ----
129	//
130	// Virtual Memory Map of process 53799 (gopls)
131	// Output report format:  2.4  -64-bit process
132	// VM page size:  16384 bytes
133	//
134	// ==== Non-writable regions for process 53799
135	// REGION TYPE                    START END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
136	// __TEXT                      1029a0000-1033bc000    [ 10.1M  7360K     0K     0K] r-x/rwx SM=COW          /Users/USER/*/gopls
137	// __DATA_CONST                1033bc000-1035bc000    [ 2048K  2000K     0K     0K] r--/rwSM=COW          /Users/USER/*/gopls
138	// __DATA_CONST                1035bc000-103a48000    [ 4656K  3824K     0K     0K] r--/rwSM=COW          /Users/USER/*/gopls
139	// __LINKEDIT                  103b00000-103c98000    [ 1632K  1616K     0K     0K] r--/r-SM=COW          /Users/USER/*/gopls
140	// dyld private memory         103cd8000-103cdc000    [   16K     0K     0K     0K] ---/--SM=NUL
141	// shared memory               103ce4000-103ce8000    [   16K    16K    16K     0K] r--/r-SM=SHM
142	// MALLOC metadata             103ce8000-103cec000    [   16K    16K    16K     0K] r--/rwx SM=COW          DefaultMallocZone_0x103ce8000 zone structure
143	// MALLOC guard page           103cf0000-103cf4000    [   16K     0K     0K     0K] ---/rwx SM=COW
144	// MALLOC guard page           103cfc000-103d00000    [   16K     0K     0K     0K] ---/rwx SM=COW
145	// MALLOC guard page           103d00000-103d04000    [   16K     0K     0K     0K] ---/rwx SM=NUL
146
147	banner := "==== Non-writable regions for process"
148	grabbing := false
149	sc := bufio.NewScanner(bytes.NewReader(data))
150	for sc.Scan() {
151		l := sc.Text()
152		if grabbing {
153			p := strings.Fields(l)
154			if len(p) > 7 && p[0] == "__TEXT" && p[7] == "r-x/rwx" {
155				locs := strings.Split(p[1], "-")
156				start, _ := strconv.ParseUint(locs[0], 16, 64)
157				end, _ := strconv.ParseUint(locs[1], 16, 64)
158				return start, end, nil
159			}
160		}
161		if strings.HasPrefix(l, banner) {
162			grabbing = true
163		}
164	}
165	return 0, 0, fmt.Errorf("vmmap no text segment found")
166}
167