Skip to content

Commit 10c71f7

Browse files
nsrip-ddfelixge
andauthored
profiler/internal/cmemprof: add full mappings to the profile (DataDog#1402)
* profiler/internal/cmemprof: add full mappings to the profile Since the C allocation profile might contain samples from shared libraries, the profile should have appropriate mappings for the different libraries, rather than a single "default" mapping. In addition, even if we don't have function & file information for a sample, we can at least show the library it came from. The /proc/self/maps parser comes from the Go runtime. To comply with the conditions of the Go license, include a copy of the license together with the copyright notice. To comply with Datadog policy, include a reference to the license in LICENE-3rdparty.csv. Co-authored-by: Felix Geisendörfer <felix@datadoghq.com>
1 parent 9ffc9ef commit 10c71f7

File tree

4 files changed

+243
-7
lines changed

4 files changed

+243
-7
lines changed

LICENSE-3rdparty.csv

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Component,Origin,License,Copyright
22
import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors
33
appsec,https://github.com/DataDog/libddwaf,Apache-2.0 OR BSD-3-Clause,Copyright (c) 2021 Datadog <info@datadoghq.com>
4+
golang,https://go.googlesource.com/go,BSD-3-Clause,Copyright (c) 2009 The Go Authors

profiler/internal/cmemprof/LICENSE-go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Copyright (c) 2009 The Go Authors. All rights reserved.
2+
Redistribution and use in source and binary forms, with or without
3+
modification, are permitted provided that the following conditions are
4+
met:
5+
* Redistributions of source code must retain the above copyright
6+
notice, this list of conditions and the following disclaimer.
7+
* Redistributions in binary form must reproduce the above
8+
copyright notice, this list of conditions and the following disclaimer
9+
in the documentation and/or other materials provided with the
10+
distribution.
11+
* Neither the name of Google Inc. nor the names of its
12+
contributors may be used to endorse or promote products derived from
13+
this software without specific prior written permission.
14+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

profiler/internal/cmemprof/pprof.go

+157-7
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,171 @@
22
// under the Apache License Version 2.0.
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2022 Datadog, Inc.
5+
//
6+
// The parseMapping code comes from the Google Go source code. The code is
7+
// licensed under the BSD 3-clause license (a copy is in LICENSE-go in this
8+
// directory, see also LICENSE-3rdparty.csv) and comes with the following
9+
// notice:
10+
//
11+
// Copyright 2016 The Go Authors. All rights reserved.
12+
// Use of this source code is governed by a BSD-style
13+
// license that can be found in the LICENSE file.
514

615
package cmemprof
716

817
import (
18+
"bytes"
919
"os"
1020
"runtime"
21+
"sort"
22+
"strconv"
1123
"strings"
1224

1325
"github.com/google/pprof/profile"
1426
)
1527

28+
var defaultMapping = &profile.Mapping{
29+
ID: 1,
30+
File: os.Args[0],
31+
}
32+
33+
var mappings []*profile.Mapping
34+
35+
func getMapping(addr uint64) *profile.Mapping {
36+
i := sort.Search(len(mappings), func(n int) bool {
37+
return mappings[n].Limit >= addr
38+
})
39+
if i == len(mappings) || addr < mappings[i].Start {
40+
return defaultMapping
41+
}
42+
return mappings[i]
43+
}
44+
45+
func init() {
46+
if runtime.GOOS != "linux" {
47+
mappings = append(mappings, defaultMapping)
48+
return
49+
}
50+
51+
// To have a more accurate profile, we need to provide mappings for the
52+
// executable and linked libraries, since the profile might contain
53+
// samples from outside the executable if the allocation profiler is
54+
// used in conjunction with a cgo traceback library.
55+
//
56+
// We can find this information in /proc/self/maps on Linux. Code comes
57+
// from mappings with executable permissions (r-xp), and a record looks
58+
// like
59+
// address perms offset dev inode pathname
60+
// 00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/dbus-daemon
61+
// (see "man 5 proc" for details)
62+
63+
data, err := os.ReadFile("/proc/self/maps")
64+
if err != nil {
65+
mappings = append(mappings, defaultMapping)
66+
return
67+
}
68+
69+
mappings = parseMappings(data)
70+
}
71+
72+
// bytes.Cut, but backported so we can still support Go 1.17
73+
func bytesCut(s, sep []byte) (before, after []byte, found bool) {
74+
if i := bytes.Index(s, sep); i >= 0 {
75+
return s[:i], s[i+len(sep):], true
76+
}
77+
return s, nil, false
78+
}
79+
80+
func stringsCut(s, sep string) (before, after string, found bool) {
81+
if i := strings.Index(s, sep); i >= 0 {
82+
return s[:i], s[i+len(sep):], true
83+
}
84+
return s, "", false
85+
}
86+
87+
func parseMappings(data []byte) []*profile.Mapping {
88+
// This code comes from parseProcSelfMaps in the
89+
// official Go repository. See
90+
// https://go.googlesource.com/go/+/refs/tags/go1.18.4/src/runtime/pprof/proto.go#596
91+
92+
var results []*profile.Mapping
93+
var line []byte
94+
// next removes and returns the next field in the line.
95+
// It also removes from line any spaces following the field.
96+
next := func() []byte {
97+
var f []byte
98+
f, line, _ = bytesCut(line, []byte(" "))
99+
line = bytes.TrimLeft(line, " ")
100+
return f
101+
}
102+
103+
for len(data) > 0 {
104+
line, data, _ = bytesCut(data, []byte("\n"))
105+
addr := next()
106+
loStr, hiStr, ok := stringsCut(string(addr), "-")
107+
if !ok {
108+
continue
109+
}
110+
lo, err := strconv.ParseUint(loStr, 16, 64)
111+
if err != nil {
112+
continue
113+
}
114+
hi, err := strconv.ParseUint(hiStr, 16, 64)
115+
if err != nil {
116+
continue
117+
}
118+
perm := next()
119+
if len(perm) < 4 || perm[2] != 'x' {
120+
// Only interested in executable mappings.
121+
continue
122+
}
123+
offset, err := strconv.ParseUint(string(next()), 16, 64)
124+
if err != nil {
125+
continue
126+
}
127+
next() // dev
128+
inode := next() // inode
129+
if line == nil {
130+
continue
131+
}
132+
file := string(line)
133+
134+
// Trim deleted file marker.
135+
deletedStr := " (deleted)"
136+
deletedLen := len(deletedStr)
137+
if len(file) >= deletedLen && file[len(file)-deletedLen:] == deletedStr {
138+
file = file[:len(file)-deletedLen]
139+
}
140+
141+
if len(inode) == 1 && inode[0] == '0' && file == "" {
142+
// Huge-page text mappings list the initial fragment of
143+
// mapped but unpopulated memory as being inode 0.
144+
// Don't report that part.
145+
// But [vdso] and [vsyscall] are inode 0, so let non-empty file names through.
146+
continue
147+
}
148+
149+
results = append(results, &profile.Mapping{
150+
ID: uint64(len(results) + 1),
151+
Start: lo,
152+
Limit: hi,
153+
Offset: offset,
154+
File: file,
155+
// Go normally sets the HasFunctions, HasLineNumbers,
156+
// etc. fields for the main executable when it consists
157+
// solely of Go code. However, users of this C
158+
// allocation profiler will necessarily be using non-Go
159+
// code and we don't know whether there are functions,
160+
// line numbers, etc. available for the non-Go code.
161+
})
162+
}
163+
sort.Slice(results, func(i, j int) bool {
164+
return results[i].Start < results[j].Start
165+
})
166+
167+
return results
168+
}
169+
16170
func (c *Profile) build() *profile.Profile {
17171
// TODO: can we be sure that there won't be other allocation samples
18172
// ongoing that write to the sample map? Right now it's called with c.mu
@@ -32,13 +186,9 @@ func (c *Profile) build() *profile.Profile {
32186
// field can be left 0, and the TimeNanos field of the Go allocation
33187
// profile will be used.
34188
p := &profile.Profile{}
35-
m := &profile.Mapping{
36-
ID: 1,
37-
File: os.Args[0], // XXX: Is there a better way to get the executable?
38-
}
39189
p.PeriodType = &profile.ValueType{Type: "space", Unit: "bytes"}
40190
p.Period = 1
41-
p.Mapping = []*profile.Mapping{m}
191+
p.Mapping = mappings
42192
p.SampleType = []*profile.ValueType{
43193
{
44194
Type: "alloc_objects",
@@ -88,8 +238,8 @@ func (c *Profile) build() *profile.Profile {
88238
if !ok {
89239
loc = &profile.Location{
90240
ID: uint64(len(locations)) + 1,
91-
Mapping: m,
92-
Address: uint64(frame.PC),
241+
Mapping: getMapping(addr),
242+
Address: addr,
93243
}
94244
function, ok := functions[frame.Function]
95245
if !ok {
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2022 Datadog, Inc.
5+
6+
package cmemprof
7+
8+
import (
9+
"testing"
10+
11+
"github.com/google/pprof/profile"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// There are a few special cases that the Go /proc/self/maps parsing code
16+
// handles that we want to preserve:
17+
//
18+
// * If a file is deleted after the progam starts, there will be a
19+
// " (deleted)" suffix which should be removed
20+
// * Per "man 5 proc", the file name will be unescaped except for
21+
// newlines. In particular, this means the filename can contain
22+
// whitespace.
23+
//
24+
// The following /proc/self/maps example was obtained by compiling a C program
25+
// which just calls "sleep(1000)", giving it a name with a space ("space daemon"),
26+
// and deleting the executable after starting the program.
27+
28+
var procMaps = []byte(`55f747992000-55f747993000 r--p 00000000 08:01 788178 /usr/bin/space daemon (deleted)
29+
55f747993000-55f747994000 r-xp 00001000 08:01 788178 /usr/bin/space daemon (deleted)
30+
55f747994000-55f747995000 r--p 00002000 08:01 788178 /usr/bin/space daemon (deleted)
31+
55f747995000-55f747996000 r--p 00002000 08:01 788178 /usr/bin/space daemon (deleted)
32+
55f747996000-55f747997000 rw-p 00003000 08:01 788178 /usr/bin/space daemon (deleted)
33+
7fa3b3c00000-7fa3b3c22000 r--p 00000000 08:01 3463 /usr/lib/x86_64-linux-gnu/libc-2.31.so
34+
7fa3b3c22000-7fa3b3d9a000 r-xp 00022000 08:01 3463 /usr/lib/x86_64-linux-gnu/libc-2.31.so
35+
7fa3b3d9a000-7fa3b3de8000 r--p 0019a000 08:01 3463 /usr/lib/x86_64-linux-gnu/libc-2.31.so
36+
7fa3b3de8000-7fa3b3dec000 r--p 001e7000 08:01 3463 /usr/lib/x86_64-linux-gnu/libc-2.31.so
37+
7fa3b3dec000-7fa3b3dee000 rw-p 001eb000 08:01 3463 /usr/lib/x86_64-linux-gnu/libc-2.31.so
38+
7fa3b3dee000-7fa3b3df4000 rw-p 00000000 00:00 0
39+
7fa3b3dfc000-7fa3b3dfd000 r--p 00000000 08:01 3459 /usr/lib/x86_64-linux-gnu/ld-2.31.so
40+
7fa3b3dfd000-7fa3b3e20000 r-xp 00001000 08:01 3459 /usr/lib/x86_64-linux-gnu/ld-2.31.so
41+
7fa3b3e20000-7fa3b3e28000 r--p 00024000 08:01 3459 /usr/lib/x86_64-linux-gnu/ld-2.31.so
42+
7fa3b3e29000-7fa3b3e2a000 r--p 0002c000 08:01 3459 /usr/lib/x86_64-linux-gnu/ld-2.31.so
43+
7fa3b3e2a000-7fa3b3e2b000 rw-p 0002d000 08:01 3459 /usr/lib/x86_64-linux-gnu/ld-2.31.so
44+
7fa3b3e2b000-7fa3b3e2c000 rw-p 00000000 00:00 0
45+
7fff1e028000-7fff1e049000 rw-p 00000000 00:00 0 [stack]
46+
7fff1e04c000-7fff1e04f000 r--p 00000000 00:00 0 [vvar]
47+
7fff1e04f000-7fff1e050000 r-xp 00000000 00:00 0 [vdso]
48+
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
49+
`)
50+
51+
func Test_parseMapping(t *testing.T) {
52+
want := []*profile.Mapping{
53+
{ID: 1, Start: 0x55f747993000, Limit: 0x55f747994000, Offset: 0x00001000, File: "/usr/bin/space daemon"},
54+
{ID: 2, Start: 0x7fa3b3c22000, Limit: 0x7fa3b3d9a000, Offset: 0x00022000, File: "/usr/lib/x86_64-linux-gnu/libc-2.31.so"},
55+
{ID: 3, Start: 0x7fa3b3dfd000, Limit: 0x7fa3b3e20000, Offset: 0x00001000, File: "/usr/lib/x86_64-linux-gnu/ld-2.31.so"},
56+
{ID: 4, Start: 0x7fff1e04f000, Limit: 0x7fff1e050000, Offset: 0x00000000, File: "[vdso]"},
57+
{ID: 5, Start: 0xffffffffff600000, Limit: 0xffffffffff601000, Offset: 0x00000000, File: "[vsyscall]"},
58+
}
59+
got := parseMappings(procMaps)
60+
require.Equal(t, want, got)
61+
}

0 commit comments

Comments
 (0)