Skip to content

Commit 73a4780

Browse files
committed
Allow stacks to be captured from /usr/bin/sample on macOS
1 parent 74764af commit 73a4780

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed

stackcollapse-sample.awk

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/awk -f
2+
#
3+
# Uses MacOS' /usr/bin/sample to generate a flamegraph of a process
4+
#
5+
# Usage:
6+
#
7+
# sudo sample [pid] -file /dev/stdout | stackcollapse-sample.awk | flamegraph.pl
8+
#
9+
# Options:
10+
#
11+
# The output will show the name of the library/framework at the call-site
12+
# with the form AppKit`NSApplication or libsystem`start_wqthread.
13+
#
14+
# If showing the framework or library name is not required, pass
15+
# MODULES=0 as an argument of the sample program.
16+
#
17+
# The generated SVG will be written to the output stream, and can be piped
18+
# into flamegraph.pl directly, or written to a file for conversion later.
19+
#
20+
# ---
21+
#
22+
# Copyright (c) 2017, Apple Inc.
23+
#
24+
# Redistribution and use in source and binary forms, with or without
25+
# modification, are permitted provided that the following conditions are met:
26+
#
27+
# 1. Redistributions of source code must retain the above copyright notice,
28+
# this list of conditions and the following disclaimer.
29+
#
30+
# 2. Redistributions in binary form must reproduce the above copyright notice,
31+
# this list of conditions and the following disclaimer in the documentation
32+
# and/or other materials provided with the distribution.
33+
#
34+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
35+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
36+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
37+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
38+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
39+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
40+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
41+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
42+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
43+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
44+
# POSSIBILITY OF SUCH DAMAGE.
45+
#
46+
47+
BEGIN {
48+
49+
# Command line options
50+
MODULES = 1 # Allows the user to enable/disable printing of modules.
51+
52+
# Internal variables
53+
_FOUND_STACK = 0 # Found the stack traces in the output.
54+
_LEVEL = -1 # The current level of indentation we are running.
55+
56+
# The set of symbols to ignore for 'waiting' threads, for ease of use.
57+
# This will hide waiting threads from the view, making it easier to
58+
# see what is actually running in the sample. These may be adjusted
59+
# as necessary or appended to if other symbols need to be filtered out.
60+
61+
_IGNORE["libsystem_kernel`__psynch_cvwait"] = 1
62+
_IGNORE["libsystem_kernel`__select"] = 1
63+
_IGNORE["libsystem_kernel`__semwait_signal"] = 1
64+
_IGNORE["libsystem_kernel`__ulock_wait"] = 1
65+
_IGNORE["libsystem_kernel`__wait4"] = 1
66+
_IGNORE["libsystem_kernel`__workq_kernreturn"] = 1
67+
_IGNORE["libsystem_kernel`kevent"] = 1
68+
_IGNORE["libsystem_kernel`mach_msg_trap"] = 1
69+
_IGNORE["libsystem_kernel`read"] = 1
70+
_IGNORE["libsystem_kernel`semaphore_wait_trap"] = 1
71+
72+
# The same set of symbols as above, without the module name.
73+
_IGNORE["__psynch_cvwait"] = 1
74+
_IGNORE["__select"] = 1
75+
_IGNORE["__semwait_signal"] = 1
76+
_IGNORE["__ulock_wait"] = 1
77+
_IGNORE["__wait4"] = 1
78+
_IGNORE["__workq_kernreturn"] = 1
79+
_IGNORE["kevent"] = 1
80+
_IGNORE["mach_msg_trap"] = 1
81+
_IGNORE["read"] = 1
82+
_IGNORE["semaphore_wait_trap"] = 1
83+
84+
}
85+
86+
# This is the first line in the /usr/bin/sample output that indicates the
87+
# samples follow subsequently. Until we see this line, the rest is ignored.
88+
89+
/^Call graph/ {
90+
_FOUND_STACK = 1
91+
}
92+
93+
# This is found when we have reached the end of the stack output.
94+
# Identified by the string "Total number in stack (...)".
95+
96+
/^Total number/ {
97+
_FOUND_STACK = 0
98+
printStack(_NEST,0)
99+
}
100+
101+
# Prints the stack from FROM to TO (where FROM > TO)
102+
# Called when indenting back from a previous level, or at the end
103+
# of processing to flush the last recorded sample
104+
105+
function printStack(FROM,TO) {
106+
107+
# We ignore certain blocking wait states, in the absence of being
108+
# able to filter these threads from collection, otherwise
109+
# we'll end up with many threads of equal length that represent
110+
# the total time the sample was collected.
111+
#
112+
# Note that we need to collect the information to ensure that the
113+
# timekeeping for the parental functions is appropriately adjusted
114+
# so we just avoid printing it out when that occurs.
115+
_PRINT_IT = !_IGNORE[_NAMES[FROM]]
116+
117+
# We run through all the names, from the root to the leaf, so that
118+
# we generate a line that flamegraph.pl will like, of the form:
119+
# Thread1234;example`main;example`otherFn 1234
120+
121+
for(l = FROM; l>=TO; l--) {
122+
if (_PRINT_IT) {
123+
printf("%s", _NAMES[0])
124+
for(i=1; i<=l; i++) {
125+
printf(";%s", _NAMES[i])
126+
}
127+
print " " _TIMES[l]
128+
}
129+
130+
# We clean up our current state to avoid bugs.
131+
delete _NAMES[l]
132+
delete _TIMES[l]
133+
}
134+
}
135+
136+
# This is where we process each line, of the form:
137+
# 5130 Thread_8749954
138+
# + 5130 start_wqthread (in libsystem_pthread.dylib) ...
139+
# + 4282 _pthread_wqthread (in libsystem_pthread.dylib) ...
140+
# + ! 4282 __doworkq_kernreturn (in libsystem_kernel.dylib) ...
141+
# + 848 _pthread_wqthread (in libsystem_pthread.dylib) ...
142+
# + 848 __doworkq_kernreturn (in libsystem_kernel.dylib) ...
143+
144+
_FOUND_STACK && match($0,/^ [^0-9]*[0-9]/) {
145+
146+
# We maintain two counters:
147+
# _LEVEL: the high water mark of the indentation level we have seen.
148+
# _NEST: the current indentation level.
149+
#
150+
# We keep track of these two levels such that when the nesting level
151+
# decreases, we print out the current state of where we are.
152+
153+
_NEST=(RLENGTH-5)/2
154+
sub(/^[^0-9]*/,"") # Normalise the leading content so we start with time.
155+
_TIME=$1 # The time recorded by 'sample', first integer value.
156+
157+
# The function name is in one or two parts, depending on what kind of
158+
# function it is.
159+
#
160+
# If it is a standard C or C++ function, it will be of the form:
161+
# exampleFunction
162+
# Example::Function
163+
#
164+
# If it is an Objective-C funtion, it will be of the form:
165+
# -[NSExample function]
166+
# +[NSExample staticFunction]
167+
# -[NSExample function:withParameter]
168+
# +[NSExample staticFunction:withParameter:andAnother]
169+
170+
_FN1 = $2
171+
_FN2 = $3
172+
173+
# If it is a standard C or C++ function then the following word will
174+
# either be blank, or the text '(in', so we jut use the first one:
175+
176+
if (_FN2 == "(in" || _FN2 == "") {
177+
_FN =_FN1
178+
} else {
179+
# Otherwise we concatenate the first two parts with .
180+
_FN = _FN1 "." _FN2
181+
}
182+
183+
# Modules are shown with '(in libfoo.dylib)' or '(in AppKit)'
184+
185+
_MODULE = ""
186+
match($0, /\(in [^)]*\)/)
187+
188+
if (RSTART > 0 && MODULES) {
189+
190+
# Strip off the '(in ' (4 chars) and the final ')' char (1 char)
191+
_MODULE = substr($0, RSTART+4, RLENGTH-5)
192+
193+
# Remove the .dylib function, since it adds no value.
194+
gsub(/\.dylib/, "", _MODULE)
195+
196+
# The function name is 'module`functionName'
197+
_FN = _MODULE "`" _FN
198+
}
199+
200+
# Now we have set up the variables, we can decide how to apply it
201+
# If we are descending in the nesting, we don't print anything out:
202+
# a
203+
# ab
204+
# abc
205+
#
206+
# We only print out something when we go back a level, or hit the end:
207+
# abcd
208+
# abe < prints out the stack up until this point, i.e. abcd
209+
210+
# We store a pair of arrays, indexed by the nesting level:
211+
#
212+
# _TIMES - a list of the time reported to that function
213+
# _NAMES - a list of the function names for each current stack trace
214+
215+
# If we are backtracking, we need to flush the current output.
216+
if (_NEST <= _LEVEL) {
217+
printStack(_LEVEL,_NEST)
218+
}
219+
220+
# Record the name and time of the function where we are.
221+
_NAMES[_NEST] = _FN
222+
_TIMES[_NEST] = _TIME
223+
224+
# We subtract the time we took from our parent so we don't double count.
225+
if (_NEST > 0) {
226+
_TIMES[_NEST-1] -= _TIME
227+
}
228+
229+
# Raise the high water mark of the level we have reached.
230+
_LEVEL = _NEST
231+
}

0 commit comments

Comments
 (0)