2
2
// under the Apache License Version 2.0.
3
3
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4
4
// 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.
5
14
6
15
package cmemprof
7
16
8
17
import (
18
+ "bytes"
9
19
"os"
10
20
"runtime"
21
+ "sort"
22
+ "strconv"
11
23
"strings"
12
24
13
25
"github.com/google/pprof/profile"
14
26
)
15
27
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
+
16
170
func (c * Profile ) build () * profile.Profile {
17
171
// TODO: can we be sure that there won't be other allocation samples
18
172
// 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 {
32
186
// field can be left 0, and the TimeNanos field of the Go allocation
33
187
// profile will be used.
34
188
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
- }
39
189
p .PeriodType = & profile.ValueType {Type : "space" , Unit : "bytes" }
40
190
p .Period = 1
41
- p .Mapping = [] * profile. Mapping { m }
191
+ p .Mapping = mappings
42
192
p .SampleType = []* profile.ValueType {
43
193
{
44
194
Type : "alloc_objects" ,
@@ -88,8 +238,8 @@ func (c *Profile) build() *profile.Profile {
88
238
if ! ok {
89
239
loc = & profile.Location {
90
240
ID : uint64 (len (locations )) + 1 ,
91
- Mapping : m ,
92
- Address : uint64 ( frame . PC ) ,
241
+ Mapping : getMapping ( addr ) ,
242
+ Address : addr ,
93
243
}
94
244
function , ok := functions [frame .Function ]
95
245
if ! ok {
0 commit comments