|
| 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