3 # Parse Mach IPC kmsg data trace from XNU
5 # Jeremy C. Andrus <jeremy_andrus@apple.com>
7 from __future__
import division
13 from collections
import deque
17 from collections
import defaultdict
21 g_rolling_window
= 200
23 def RunCommand(cmd_string
):
25 returns: (int,str) : exit_code and output_str
29 sys
.stderr
.write("\tCMD:{}\n".format(cmd_string
))
33 output_str
= subprocess
.check_output(cmd_string
, shell
=True)
34 except subprocess
.CalledProcessError
, e
:
35 exit_code
= e
.returncode
37 return (exit_code
, output_str
.strip())
41 """ Class interface to a graph node representing a logical service name.
42 In general, this should correspond to a unique binary on the system
43 which could be started / stopped as different PIDs throughout the life
46 def __init__(self
, name
= ''):
48 self
.nname
= "L_" + name
.replace(".", "_").replace("-", "_")
52 self
.msg_stat
= {'o.num':0, 'o.first':0.0, 'o.last':0.0, 'o.window':deque(), 'o.avg':0, 'o.peak':0, \
53 'i.num':0, 'i.first':0.0, 'i.last':0.0, 'i.window':deque(), 'i.avg':0, 'i.peak':0}
55 self
.scalefactor
= 100.0
57 sys
.stderr
.write(' New node: "{}"{}\n'.format(self
.nname
, ' '*50))
59 def add_outgoing_edge(self
, edge
, time
):
60 self
.outgoing
[edge
.ename()] = [edge
, time
]
62 def add_incoming_edge(self
, edge
, time
):
63 self
.incoming
[edge
.ename()] = [edge
, time
]
65 def addpid(self
, pid
, time
):
66 if not pid
in self
.pidset
:
67 self
.pidset
[pid
] = [time
, 0]
68 self
.pidset
[pid
][1] = time
70 def incoming_msg(self
, size
, time_us
):
72 global g_rolling_window
73 num
= self
.msg_stat
['i.num'] + 1
74 self
.msg_stat
['i.num'] = num
75 time_us
= float(time_us
)
76 if self
.msg_stat
['i.first'] == 0.0:
77 self
.msg_stat
['i.first'] = time_us
78 self
.msg_stat
['i.last'] = time_us
80 self
.msg_stat
['i.last'] = time_us
81 if num
> g_min_messages
:
82 avg
= (num
* self
.scalefactor
) / (time_us
- self
.msg_stat
['i.first'])
83 self
.msg_stat
['i.avg'] = avg
85 self
.msg_stat
['i.window'].append(time_us
)
86 if len(self
.msg_stat
['i.window']) > g_rolling_window
:
87 self
.msg_stat
['i.window'].popleft()
88 n
= len(self
.msg_stat
['i.window'])
89 ravg
= float(len(self
.msg_stat
['i.window']) * self
.scalefactor
) / \
90 (self
.msg_stat
['i.window'][-1] - self
.msg_stat
['i.window'][0])
91 if ravg
> self
.msg_stat
['i.peak']:
92 self
.msg_stat
['i.peak'] = ravg
94 def outgoing_msg(self
, size
, time_us
):
96 global g_rolling_window
97 num
= self
.msg_stat
['o.num'] + 1
98 self
.msg_stat
['o.num'] = num
99 time_us
= float(time_us
)
100 if self
.msg_stat
['o.first'] == 0.0:
101 self
.msg_stat
['o.first'] = time_us
102 self
.msg_stat
['o.last'] = time_us
104 self
.msg_stat
['o.last'] = time_us
105 if num
> g_min_messages
:
106 avg
= (num
* self
.scalefactor
) / (time_us
- self
.msg_stat
['o.first'])
107 self
.msg_stat
['o.avg'] = avg
109 self
.msg_stat
['o.window'].append(time_us
)
110 if len(self
.msg_stat
['o.window']) > g_rolling_window
:
111 self
.msg_stat
['o.window'].popleft()
112 n
= len(self
.msg_stat
['o.window'])
113 ravg
= float(len(self
.msg_stat
['o.window']) * self
.scalefactor
) / \
114 (self
.msg_stat
['o.window'][-1] - self
.msg_stat
['o.window'][0])
115 if ravg
> self
.msg_stat
['o.peak']:
116 self
.msg_stat
['o.peak'] = ravg
119 return self
.msg_stat
['o.num'], self
.msg_stat
['i.num']
122 return len(self
.pidset
)
124 def label(self
, timebase
= 1000000.0):
125 oavg
= float(self
.msg_stat
['o.avg']) / self
.scalefactor
126 opeak
= float(self
.msg_stat
['o.peak']) / self
.scalefactor
127 oactive
= self
.msg_stat
['o.last'] - self
.msg_stat
['o.first']
128 iavg
= float(self
.msg_stat
['i.avg']) / self
.scalefactor
129 ipeak
= float(self
.msg_stat
['i.peak']) / self
.scalefactor
130 iactive
= self
.msg_stat
['i.last'] - self
.msg_stat
['i.first']
132 oavg
= oavg
* timebase
133 opeak
= opeak
* timebase
134 oactive
= oactive
/ timebase
135 iavg
= iavg
* timebase
136 ipeak
= ipeak
* timebase
137 iactive
= iactive
/ timebase
138 return "{:s}\\no:{:d}/({:d}:{:.1f}s)/{:.1f}:{:.1f})\\ni:{:d}({:d}:{:.1f}s)/{:.1f}:{:.1f})\\nR:{:d}"\
139 .format(self
.nicename
, \
140 len(self
.outgoing
), self
.msg_stat
['o.num'], oactive
, oavg
, opeak
, \
141 len(self
.incoming
), self
.msg_stat
['i.num'], iactive
, iavg
, ipeak
, \
145 """ Class interface to an graph edge representing two services / programs
146 communicating via Mach IPC. Note that this communication could
147 use many different PIDs. The connected graph nodes (see IPCNode)
148 represent logical services on the system which could be instantiated
149 as many different PIDs depending on the lifecycle of the process
150 (dictated in part by launchd).
153 F_TRACED
= 0x00000100
154 F_COMPLEX
= 0x00000200
155 F_OOLMEM
= 0x00000400
159 F_RAISEIMP
= 0x00004000
160 F_APP_SRC
= 0x00008000
161 F_APP_DST
= 0x00010000
162 F_DAEMON_SRC
= 0x00020000
163 F_DAEMON_DST
= 0x00040000
164 F_DST_NDFLTQ
= 0x00080000
165 F_SRC_NDFLTQ
= 0x00100000
166 F_DST_SONCE
= 0x00200000
167 F_SRC_SONCE
= 0x00400000
168 F_CHECKIN
= 0x00800000
169 F_ONEWAY
= 0x01000000
171 F_SNDRCV
= 0x04000000
172 F_DSTQFULL
= 0x08000000
173 F_VOUCHER
= 0x10000000
176 F_PORTS_MASK
= 0x000000FF
178 DTYPES
= [ 'std', 'xpc', 'iokit', 'std.reply', 'xpc.reply', 'iokit.reply' ]
179 DFLAVORS
= [ 'std', 'ool', 'vcpy', 'iokit' ]
181 def __init__(self
, src
= IPCNode(), dst
= IPCNode(), data
= '0', flags
= '0', time
= 0.0):
188 self
._data
= { 'std':0, 'ool':0, 'vcpy':0, 'iokit':0 }
189 self
._dtype
= { 'std':0, 'xpc':0, 'iokit':0, 'std.reply':0, 'xpc.reply':0, 'iokit.reply':0 }
190 self
._msgs
= { 'std':0, 'ool':0, 'vcpy':0, 'iokit':0 }
191 self
._mtype
= { 'std':0, 'xpc':0, 'iokit':0, 'std.reply':0, 'xpc.reply':0, 'iokit.reply':0 }
195 self
.src
.add_outgoing_edge(self
, time
)
196 self
.dst
.add_incoming_edge(self
, time
)
197 self
.addmsg(data
, flags
, time
)
200 return self
.src
.nname
+ " -> " + self
.dst
.nname
203 return self
._data
, self
._dtype
205 def data(self
, flavor
= None):
207 return sum(self
._data
.itervalues())
208 elif flavor
in self
._data
:
209 return self
._data
[flavor
]
213 def dtype(self
, type):
215 return sum(self
._dtype
.itervalues())
216 elif type in self
._dtype
:
217 return self
._dtype
[type]
221 def msgs(self
, flavor
= None):
223 return sum(self
._msgs
.itervalues())
224 elif flavor
in self
._msgs
:
225 return self
._msgs
[flavor
]
229 def mtype(self
, type):
231 return sum(self
._mtype
.itervalues())
232 elif type in self
._mtype
:
233 return self
._mtype
[type]
238 if self
.src
.nname
== self
.dst
.nname
:
242 def addmsg(self
, data_hex_str
, flags_str
, time
):
244 f
= int(flags_str
, 16)
246 df
= {f:0 for f in self.DFLAVORS}
247 dt
= {t:0 for t in self.DTYPES}
248 if not f
& self
.F_TRACED
:
255 if not f
& self
.F_COMPLEX
:
257 df
['std'] = int(data_hex_str
, 16)
259 df
['iokit'] = df
['std']
261 self
._data
['iokit'] += df
['iokit']
262 self
._msgs
['iokit'] += 1
264 self
._data
['std'] += df
['std']
265 self
._msgs
['std'] += 1
266 elif f
& self
.F_OOLMEM
:
268 df
['ool'] = int(data_hex_str
, 16)
270 df
['iokit'] = df
['ool']
272 self
._data
['iokit'] += df
['iokit']
273 self
._msgs
['iokit'] += 1
274 elif f
& self
.F_VCPY
:
275 df
['vcpy'] = df
['ool']
277 self
._data
['vcpy'] += df
['vcpy']
278 self
._msgs
['vcpy'] += 1
280 self
._data
['ool'] += df
['ool']
281 self
._msgs
['ool'] += 1
282 # Complex messages can contain ports and data
283 if f
& self
.F_COMPLEX
:
284 nports
= f
& self
.F_PORTS_MASK
288 dsize
= sum(df
.values())
289 if f
& self
.F_DST_SONCE
:
291 dt
['iokit.reply'] = dsize
292 self
._dtype
['iokit.reply'] += dsize
293 self
._mtype
['iokit.reply'] += 1
294 elif f
& (self
.F_DST_NDFLTQ | self
.F_SRC_NDFLTQ
):
295 dt
['xpc.reply'] = dsize
296 self
._dtype
['xpc.reply'] += dsize
297 self
._mtype
['xpc.reply'] += 1
299 dt
['std.reply'] = dsize
300 self
._dtype
['std.reply'] += dsize
301 self
._mtype
['std.reply'] += 1
302 elif f
& self
.F_IOKIT
:
304 self
._dtype
['iokit'] += dsize
305 self
._mtype
['iokit'] += 1
306 elif f
& (self
.F_DST_NDFLTQ | self
.F_SRC_NDFLTQ
):
308 self
._dtype
['xpc'] += dsize
309 self
._mtype
['xpc'] += 1
312 self
._dtype
['std'] += dsize
313 self
._mtype
['std'] += 1
314 self
.src
.outgoing_msg(dsize
, time
)
315 self
.dst
.incoming_msg(dsize
, time
)
317 sys
.stderr
.write(' {}->{} ({}/{}){}\r'.format(self
.src
.nname
, self
.dst
.nname
, df
['ool'], df
['std'], ' ' *50))
321 avgsz
= self
.data() / self
.dweight
322 msgs_with_data
= self
.dweight
/ self
.weight
323 avgports
= self
.ports
/ self
.pweight
324 msgs_with_ports
= self
.pweight
/ self
.weight
325 return (avgsz
, msgs_with_data
, avgports
, msgs_with_ports
)
328 class EdgeError(Exception):
329 """ IPCEdge exception class
331 def __init__(self
, edge
, nm
):
332 self
.msg
= "Edge {} (w:{}) didn't match incoming name {}!".format(edge
.ename(), edge
.weight
, nm
)
335 """ Class interface to a directed graph of IPC interconnectivity
337 def __init__(self
, name
= '', timebase
= 0.0):
340 self
.name
= 'ipcgraph'
344 sys
.stderr
.write('Creating new IPCGraph named {}...\n'.format(self
.name
))
347 self
.msgs
= defaultdict(lambda: {f:0 for f in IPCEdge.DFLAVORS}
)
348 self
.msgtypes
= defaultdict(lambda: {t:0 for t in IPCEdge.DTYPES}
)
352 for f
in IPCEdge
.DFLAVORS
:
353 self
.totals
['n'+f
] = 0
354 self
.totals
['D'+f
] = 0
355 if timebase
and timebase
> 0.0:
356 self
.timebase
= timebase
363 def edgename(self
, src
, dst
):
365 return src
.nname
+ ' -> ' + dst
.nname
368 def addmsg(self
, src_str
, src_pid
, dst_str
, dst_pid
, data_hex_str
, flags_str
, time
):
371 for k
, v
in self
.nodes
.iteritems():
372 if not src
and k
== src_str
:
374 if not dst
and k
== dst_str
:
379 src
= IPCNode(src_str
)
380 self
.nodes
[src_str
] = src
;
382 dst
= IPCNode(dst_str
)
383 self
.nodes
[dst_str
] = dst
384 src
.addpid(src_pid
, time
)
385 dst
.addpid(dst_pid
, time
)
387 nm
= self
.edgename(src
, dst
)
390 e
= self
.edges
.get(nm
)
393 raise EdgeError(e
,nm
)
394 msgdata
, msgDtype
= e
.addmsg(data_hex_str
, flags_str
, time
)
396 e
= IPCEdge(src
, dst
, data_hex_str
, flags_str
, time
)
397 msgdata
, msgDtype
= e
.msgdata()
400 if self
.maxdweight
< e
.dweight
:
401 self
.maxdweight
= e
.dweight
403 if sum(msgdata
.values()) == 0:
404 self
.msgs
[0]['std'] += 1
405 self
.msgtypes
[0]['std'] += 1
406 if not 'enames' in self
.msgs
[0]:
407 self
.msgs
[0]['enames'] = [ nm
]
408 elif not nm
in self
.msgs
[0]['enames']:
409 self
.msgs
[0]['enames'].append(nm
)
411 for k
,d
in msgdata
.iteritems():
414 self
.totals
['n'+k
] += 1
415 self
.totals
['D'+k
] += d
416 if not 'enames' in self
.msgs
[d
]:
417 self
.msgs
[d
]['enames'] = [ nm
]
418 elif not nm
in self
.msgs
[d
]['enames']:
419 self
.msgs
[d
]['enames'].append(nm
)
420 for k
,d
in msgDtype
.iteritems():
422 self
.msgtypes
[d
][k
] += 1
424 if self
.nmsgs
% 1024 == 0:
425 sys
.stderr
.write(" {:d}...\r".format(self
.nmsgs
));
427 def print_dot_node(self
, ofile
, node
):
428 omsgs
, imsgs
= node
.nmsgs()
429 recycled
= node
.recycled() * 5
435 elif recycled
<= 100:
436 bgcolor
= 'grey{:d}'.format(100 - recycled
)
439 ofile
.write("\t{:s} [style=filled,fontcolor={:s},fillcolor={:s},label=\"{:s}\"];\n"\
440 .format(node
.nname
, tcolor
, bgcolor
, node
.label()))
442 def print_dot_edge(self
, nm
, edge
, ofile
):
443 #weight = 100 * edge.dweight / self.maxdweight
446 weight
= edge
.dweight
447 penwidth
= edge
.weight
/ 512
452 attrs
= "weight={},penwidth={}".format(round(weight
,2), round(penwidth
,2))
454 if edge
.flags
& edge
.F_RAISEIMP
:
455 attrs
+= ",arrowhead=dot"
457 xpc
= edge
.dtype('xpc') + edge
.dtype('xpc.reply')
458 iokit
= edge
.dtype('iokit') + edge
.dtype('iokit.reply')
459 std
= edge
.dtype('std') + edge
.dtype('std.reply')
460 if xpc
> (iokit
+ std
):
461 attrs
+= ',color=blue'
462 elif iokit
> (std
+ xpc
):
463 attrs
+= ',color=red'
465 if edge
.data('vcpy') > (edge
.data('ool') + edge
.data('std')):
466 attrs
+= ',style="dotted"'
469 if edge.flags & (edge.F_DST_NDFLTQ | edge.F_SRC_NDFLTQ):
470 ltype.append('dotted')
471 if edge.flags & edge.F_APP_SRC:
474 attrs += ',style="' + reduce(lambda a, v: a + ',' + v, ltype) + '"'
476 if edge.data('ool') > (edge.data('std') + edge.data('vcpy')):
477 attrs += ",color=blue"
478 if edge.data('vcpy') > (edge.data('ool') + edge.data('std')):
479 attrs += ",color=green"
482 ofile
.write("\t{:s} [{:s}];\n".format(nm
, attrs
))
484 def print_follow_graph(self
, ofile
, follow
, visited
= None):
485 ofile
.write("digraph {:s} {{\n".format(self
.name
))
486 ofile
.write("\tsplines=ortho;\n")
490 sys
.stderr
.write("following {}\n".format(f
))
493 while len(follow
) > 0:
495 for nm
, e
in self
.edges
.iteritems():
496 nicename
= e
.src
.nicename
497 # Find all nodes to which 'follow' nodes communicate
498 if e
.src
.nicename
in follow
:
500 if not e
.selfedge() and not e
.dst
in cnodes
:
502 visited
.extend(follow
)
505 if not n
.nicename
in visited
:
506 follow
.append(n
.nicename
)
509 sys
.stderr
.write("{}following {}\n".format(' |--'*lvl
, f
))
510 # END: while len(follow)
511 for k
, v
in self
.nodes
.iteritems():
512 if v
.nicename
in visited
:
513 self
.print_dot_node(ofile
, v
)
514 for nm
, edge
in printedges
.iteritems():
515 self
.print_dot_edge(nm
, edge
, ofile
)
518 def print_graph(self
, ofile
, follow
):
519 ofile
.write("digraph {:s} {{\n".format(self
.name
))
520 ofile
.write("\tsplines=ortho;\n")
521 for k
, v
in self
.nodes
.iteritems():
522 self
.print_dot_node(ofile
, v
)
523 for nm
, edge
in self
.edges
.iteritems():
524 self
.print_dot_edge(nm
, edge
, ofile
)
527 def print_nodegrid(self
, ofile
, type='msg', dfilter
=None):
534 ofile
.write("{} Data sent between nodes.\nRow == SOURCE; Column == DESTINATION\n".format(dfname
))
536 ofile
.write("{} Messages sent between nodes.\nRow == SOURCE; Column == DESTINATION\n".format(dfname
))
539 dfilter
= IPCEdge
.DTYPES
540 ofile
.write(' ,' + ','.join(self
.nodes
.keys()) + '\n')
541 for snm
, src
in self
.nodes
.iteritems():
543 for dnm
, dst
in self
.nodes
.iteritems():
544 enm
= self
.edgename(src
, dst
)
545 e
= self
.edges
.get(enm
)
546 if e
and enm
in src
.outgoing
.keys():
548 dsize
= reduce(lambda accum
, t
: accum
+ e
.dtype(t
), dfilter
, 0)
549 odata
.append('{:d}'.format(dsize
))
551 nmsg
= reduce(lambda accum
, t
: accum
+ e
.mtype(t
), dfilter
, 0)
552 odata
.append('{:d}'.format(nmsg
))
555 ofile
.write(snm
+ ',' + ','.join(odata
) + '\n')
557 def print_datasummary(self
, ofile
):
559 for type in IPCEdge
.DTYPES
:
561 for k
, v
in self
.edges
.iteritems():
562 for t
in IPCEdge
.DTYPES
:
563 m
[t
][0] += v
.mtype(t
)
564 m
[t
][1] += v
.dtype(t
)
567 for f
in IPCEdge
.DFLAVORS
:
568 tdata
+= self
.totals
['D'+f
]
569 tmsgs
+= self
.totals
['n'+f
]
570 # we account for 0-sized messages differently
571 tmsgs
+= self
.msgs
[0]['std']
572 ofile
.write("Nodes:{:d}\nEdges:{:d}\n".format(len(self
.nodes
),len(self
.edges
)))
573 ofile
.write("Total Messages,{}\nTotal Data,{}\n".format(tmsgs
, tdata
))
574 ofile
.write("Flavor,Messages,Data,\n")
575 for f
in IPCEdge
.DFLAVORS
:
576 ofile
.write("{:s},{:d},{:d}\n".format(f
, self
.totals
['n'+f
], self
.totals
['D'+f
]))
577 ofile
.write("Style,Messages,Data,\n")
578 for t
in IPCEdge
.DTYPES
:
579 ofile
.write("{:s},{:d},{:d}\n".format(t
, m
[t
][0], m
[t
][1]))
581 def print_freqdata(self
, ofile
, gnuplot
= False):
583 ostr
= "Message Size"
585 for f
in IPCEdge
.DFLAVORS
:
586 ostr
+= ',{fmt:s} Freq,{fmt:s} CDF,{fmt:s} Data CDF,{fmt:s} Cumulative Data'.format(fmt
=f
)
590 ostr
+= ',#Unique SVC pairs\n'
597 for f
in IPCEdge
.DFLAVORS
:
602 for k
, v
in sorted(self
.msgs
.iteritems()):
605 for f
in IPCEdge
.DFLAVORS
:
607 if v
[f
] > maxmsgs
[f
]:
613 cdf
= {f:0 for f in IPCEdge.DFLAVORS}
614 dcdf
= {f:0 for f in IPCEdge.DFLAVORS}
615 if k
> 0: # Only use messages with data size > 0
616 for f
in IPCEdge
.DFLAVORS
:
617 if self
.totals
['n'+f
] > 0:
618 cdf
[f
] = int(100 * totalmsgs
[f
] / self
.totals
['n'+f
])
619 if self
.totals
['D'+f
] > 0:
620 dcdf
[f
] = int(100 * Tdata
[f
] / self
.totals
['D'+f
])
622 ostr
= "{:d}".format(k
)
623 for f
in IPCEdge
.DFLAVORS
:
624 ostr
+= ",{:d},{:d},{:d},{:d}".format(_nmsgs
[f
],cdf
[f
],dcdf
[f
],Tdata
[f
])
625 ostr
+= ",{:d}\n".format(len(v
['enames']))
631 colors
= [ 'blue', 'red', 'green', 'black', 'grey', 'yellow' ]
635 maxmsg
= max(maxmsgs
.values())
636 for f
in IPCEdge
.DFLAVORS
:
637 flavorcolor
[f
] = colors
[idx
]
638 if self
.totals
['D'+f
] > maxdata
:
639 maxdata
= self
.totals
['D'+f
]
642 sys
.stderr
.write("Creating GNUPlot...\n")
645 set terminal postscript eps enhanced color solid 'Courier' 12
652 set ylabel font 'Courier,14' "Total Message CDF\\n(% of total number of messages)"
653 set xlabel font 'Courier,14' "Message Size (bytes)"
654 set datafile separator ","
655 set ytics ( '0' 0, '10' 10, '20' 20, '30' 30, '40' 40, '50' 50, '60' 60, '70' 70, '80' 80, '90' 90, '100' 100)
658 for f
in IPCEdge
.DFLAVORS
:
659 plots
.append("'{{csvfile:s}}' using 1:{:d} title '{:s} Messages' with lines lw 2 lt 1 lc rgb \"{:s}\"".format(flavoridx
[f
]+1, f
, flavorcolor
[f
]))
660 cdf_data_fmt
+= ', \\\n'.join(plots
)
663 set terminal postscript eps enhanced color solid 'Courier' 12
670 set ylabel font 'Courier,14' "Total Data CDF\\n(% of total data transmitted)"
671 set xlabel font 'Courier,14' "Message Size (bytes)"
672 set datafile separator ","
673 set ytics ( '0' 0, '10' 10, '20' 20, '30' 30, '40' 40, '50' 50, '60' 60, '70' 70, '80' 80, '90' 90, '100' 100)
676 for f
in IPCEdge
.DFLAVORS
:
677 plots
.append("'{{csvfile:s}}' using 1:{:d} title '{:s} Message Data' with lines lw 2 lt 1 lc rgb \"{:s}\"".format(flavoridx
[f
]+2, f
, flavorcolor
[f
]))
678 dcdf_data_fmt
+= ', \\\n'.join(plots
)
681 set terminal postscript eps enhanced color solid 'Courier' 12
685 set x2range [1:32768]
686 set y2range [0:{maxdata:d}]
692 set ylabel font 'Courier,14' "Number of Messages"
693 set y2label font 'Courier,14' "Data Transferred (bytes)"
694 set xlabel font 'Courier,14' "Message Size (bytes)"
695 set datafile separator ","
701 for f
in IPCEdge
.DFLAVORS
:
702 plots
.append("'{{csvfile:s}}' using 1:{:d} axes x1y1 title '{:s} Messages' with boxes lt 1 lc rgb \"{:s}\"".format(flavoridx
[f
], f
, flavorcolor
[f
]))
703 plots
.append("'{{csvfile:s}}' using 1:{:d} axes x2y2 title '{:s} Data' with line lt 1 lw 2 lc rgb \"{:s}\"".format(flavoridx
[f
]+3, f
, flavorcolor
[f
]))
704 freq_data_fmt
+= ', \\\n'.join(plots
)
706 new_file
= re
.sub(r
'(.*)\.\w+$', r
'\1_cdf.plot', ofile
.name
)
707 sys
.stderr
.write("\t{:s}...\n".format(new_file
))
708 plotfile
= open(new_file
, 'w')
709 plotfile
.write(cdf_data_fmt
.format(lastmsg
=lastmsg
, maxdata
=maxdata
, maxmsg
=maxmsg
, csvfile
=ofile
.name
))
713 new_file
= re
.sub(r
'(.*)\.\w+$', r
'\1_dcdf.plot', ofile
.name
)
714 sys
.stderr
.write("\t{:s}...\n".format(new_file
))
715 plotfile
= open(new_file
, 'w')
716 plotfile
.write(dcdf_data_fmt
.format(lastmsg
=lastmsg
, maxdata
=maxdata
, maxmsg
=maxmsg
, csvfile
=ofile
.name
))
720 new_file
= re
.sub(r
'(.*)\.\w+$', r
'\1_hist.plot', ofile
.name
)
721 sys
.stderr
.write("\t{:s}...\n".format(new_file
))
722 plotfile
= open(new_file
, 'w')
723 plotfile
.write(freq_data_fmt
.format(lastmsg
=lastmsg
, maxdata
=maxdata
, maxmsg
=maxmsg
, csvfile
=ofile
.name
))
727 sys
.stderr
.write("\nFailed to write gnuplot script!\n");
731 def convert_raw_tracefiles(args
):
732 if not args
.raw
or len(args
.raw
) < 1:
735 if not args
.tracefile
:
738 for rawfile
in args
.raw
:
739 sys
.stderr
.write("Converting RAW tracefile '{:s}'...\n".format(rawfile
.name
))
740 if args
.tbfreq
and len(args
.tbfreq
) > 0:
741 args
.tbfreq
= " -F " + args
.tbfreq
744 tfile
= re
.sub(r
'(.*)(\.\w+)*$', r
'\1.ascii', rawfile
.name
)
745 cmd
= 'trace -R {:s}{:s} -o {:s}'.format(rawfile
.name
, args
.tbfreq
, tfile
)
746 if args
.tracecodes
and len(args
.tracecodes
) > 0:
747 cmd
+= " -N {}".format(args
.tracecodes
[0])
748 elif os
.path
.isfile('bsd/kern/trace.codes'):
749 cmd
+= " -N bsd/kern/trace.codes"
750 if args
.traceargs
and len(args
.traceargs
) > 0:
751 cmd
+= ' '.join(args
.traceargs
)
752 (ret
, outstr
) = RunCommand(cmd
)
754 os
.stderr
.write("Couldn't convert raw trace file. ret=={:d}\nE: {:s}\n".format(ret
, outstr
))
757 if not os
.path
.isfile(tfile
):
758 sys
.stderr
.write("Failure to convert raw trace file '{:s}'\ncmd: '{:s}'\n".format(args
.raw
[0].name
, cmd
))
760 args
.tracefile
.append(open(tfile
, 'r'))
761 # END: for rawfile in args.raw
764 def parse_tracefile_line(line
, exclude
, include
, exflags
, incflags
, active_proc
, graph
, base
=16):
768 if val
[2] == "proc_exec" or val
[2] == "TRACE_DATA_EXEC":
769 pid
= int(val
[3], base
)
770 active_proc
[pid
] = val
[9]
771 if val
[2] == "MACH_IPC_kmsg_info":
772 sendpid
= int(val
[3], base
)
773 destpid
= int(val
[4], base
)
776 elif sendpid
in active_proc
:
777 src
= active_proc
[sendpid
]
779 src
= "{:d}".format(sendpid
)
782 elif destpid
in active_proc
:
783 dst
= active_proc
[destpid
]
785 dst
= "{:d}".format(destpid
)
786 if exclude
and len(exclude
) > 0 and (src
in exclude
or dst
in exclude
):
788 if include
and len(include
) > 0 and (not (src
in include
or dst
in include
)):
790 flags
= int(val
[6], 16)
791 if exflags
or incflags
:
792 if exflags
and (flags
& int(exflags
[0], 0)):
794 if incflags
and (flags
& int(incflags
[0], 0)) != int(incflags
[0], 0):
796 # create a graph edge
797 if (flags
& IPCEdge
.F_TRACED
):
798 graph
.addmsg(src
, sendpid
, dst
, destpid
, val
[5], val
[6], float(val
[0]))
799 # END: MACH_IPC_kmsg_info
804 def main(argv
=sys
.argv
):
805 """ Main program entry point.
807 Trace file output lines look like this:
808 {abstime} {delta} MACH_IPC_kmsg_info {src_pid} {dst_pid} {msg_len} {flags} {threadid} {cpu} {proc_name}
810 4621921.2 33.8(0.0) MACH_IPC_kmsg_info ac 9d c 230002 b2e 1 MobileMail
813 {abstime} {delta} proc_exec {pid} 0 0 0 {threadid} {cpu} {proc_name}
815 4292212.3 511.2 proc_exec c8 0 0 0 b44 0 voiced
819 parser
= argparse
.ArgumentParser(description
='Parse an XNU Mach IPC kmsg ktrace file')
821 # output a DOT formatted graph file
822 parser
.add_argument('--printgraph', '-g', dest
='graph', default
=None, type=argparse
.FileType('w'), help='Output a DOT connectivity graph from the trace data')
823 parser
.add_argument('--graphname', dest
='name', default
='ipcgraph', help='A name for the DOT graph output')
824 parser
.add_argument('--graphfollow', dest
='follow', nargs
='+', metavar
='NAME', help='Graph only the transitive closure of services / processes which communicate with the given service(s)')
826 # output a CDF of message data
827 parser
.add_argument('--printfreq', '-f', dest
='freq', default
=None, type=argparse
.FileType('w'), help='Output a frequency distribution of message data (in CSV format)')
828 parser
.add_argument('--gnuplot', dest
='gnuplot', action
='store_true', help='Write out a gnuplot file along with the frequency distribution data')
830 # output a simple summary of message data
831 parser
.add_argument('--printsummary', '-s', dest
='summary', default
=None, type=argparse
.FileType('w'), help='Output a summary of all messages in the trace data')
833 # Output a CSV grid of node data/messages
834 parser
.add_argument('--printnodegrid', '-n', dest
='nodegrid', default
=None, type=argparse
.FileType('w'), help='Output a CSV grid of all messages/data sent between nodes (defaults to # messages)')
835 parser
.add_argument('--ngridtype', dest
='ngridtype', default
=None, choices
=['msgs', 'data'], help='Used with the --printnodegrid argument, this option control whether the grid will be # of messages sent between nodes, or amount of data sent between nodes')
836 parser
.add_argument('--ngridfilter', dest
='ngridfilter', default
=None, nargs
='+', choices
=IPCEdge
.DTYPES
, help='Used with the --printnodegrid argument, this option controls the type of messages or data counted')
838 parser
.add_argument('--raw', '-R', dest
='raw', nargs
='+', type=argparse
.FileType('r'), metavar
='tracefile', help='Process a raw tracefile using the "trace" utility on the host. This requires an ssh connection to the device, or a manual specification of the tbfrequency.')
839 parser
.add_argument('--tbfreq', '-T', dest
='tbfreq', default
=None, help='The value of sysctl hw.tbfrequency run on the device')
840 parser
.add_argument('--device', '-D', dest
='device', nargs
=1, metavar
='DEV', help='The name of the iOS device reachable via "ssh DEV"')
841 parser
.add_argument('--tracecodes', '-N', dest
='tracecodes', nargs
=1, metavar
='TRACE.CODES', help='Path to a custom trace.codes file. By default, the script will look for bsd/kern/trace.codes from the current directory)')
842 parser
.add_argument('--traceargs', dest
='traceargs', nargs
='+', metavar
='TRACE_OPT', help='Extra options to the "trace" program run on the host')
844 parser
.add_argument('--psfile', dest
='psfile', nargs
='+', type=argparse
.FileType('r'), help='Process list file output by ios_trace_ipc.sh')
846 parser
.add_argument('--exclude', dest
='exclude', metavar
='NAME', nargs
='+', help='List of services to exclude from processing. Any messages sent to or originating from these services will be discarded.')
847 parser
.add_argument('--include', dest
='include', metavar
='NAME', nargs
='+', help='List of services to include in processing. Only messages sent to or originating from these services will be processed.')
848 parser
.add_argument('--exflags', dest
='exflags', metavar
='0xFLAGS', nargs
=1, help='Messages with any of these flags bits set will be discarded')
849 parser
.add_argument('--incflags', dest
='incflags', metavar
='0xFLAGS', nargs
=1, type=int, help='Only messages with all of these flags bits set will be processed')
851 parser
.add_argument('--verbose', '-v', dest
='verbose', action
='count', help='be verbose (can be used multiple times)')
852 parser
.add_argument('tracefile', nargs
='*', type=argparse
.FileType('r'), help='Input trace file')
854 args
= parser
.parse_args()
856 g_verbose
= args
.verbose
858 if not args
.graph
and not args
.freq
and not args
.summary
and not args
.nodegrid
:
859 sys
.stderr
.write("Please select at least one output format: [-gfsn] {file}\n")
862 convert_raw_tracefiles(args
)
864 graph
= IPCGraph(args
.name
, args
.tbfreq
)
866 nfiles
= len(args
.tracefile
)
870 # Parse a ps output file (generated by ios_trace_ipc.sh)
871 # This pre-fills the active_proc list
872 if args
.psfile
and len(args
.psfile
) > idx
:
873 sys
.stderr
.write("Parsing {:s}...\n".format(args
.psfile
[idx
].name
))
874 for line
in args
.psfile
[idx
]:
875 if line
.strip() == '':
877 parse_tracefile_line(line
.strip(), None, None, None, None, active_proc
, graph
, 10)
878 # END: for line in psfile
880 sys
.stderr
.write("Parsing {:s}...\n".format(args
.tracefile
[idx
].name
))
881 for line
in args
.tracefile
[idx
]:
882 if line
.strip() == '':
884 parse_tracefile_line(line
.strip(), args
.exclude
, args
.include
, args
.exflags
, args
.incflags
, active_proc
, graph
)
885 # END: for line in tracefile
887 # END: foreach tracefile/psfile
890 if args
.follow
and len(args
.follow
) > 0:
891 sys
.stderr
.write("Writing follow-graph to {:s}...\n".format(args
.graph
.name
))
892 graph
.print_follow_graph(args
.graph
, args
.follow
)
894 sys
.stderr
.write("Writing graph output to {:s}...\n".format(args
.graph
.name
))
895 graph
.print_graph(args
.graph
, args
.follow
)
897 sys
.stderr
.write("Writing CDF data to {:s}...\n".format(args
.freq
.name
))
898 graph
.print_freqdata(args
.freq
, args
.gnuplot
)
900 sys
.stderr
.write("Writing summary data to {:s}...\n".format(args
.summary
.name
))
901 graph
.print_datasummary(args
.summary
)
904 sys
.stderr
.write("Writing node grid data to {:s}...]\n".format(args
.nodegrid
.name
))
905 graph
.print_nodegrid(args
.nodegrid
, args
.ngridtype
, args
.ngridfilter
)
907 if __name__
== '__main__':