File: ptreetime.py

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (746 lines) | stat: -rwxr-xr-x 27,616 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
#!/usr/bin/env python

# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2015 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors

import datetime
import inspect
import json
import optparse
import os
import re
import subprocess
import tempfile

###

def _write_message(kind, message):
    # Get the file/line where this message was generated.
    f = inspect.currentframe()
    # Step out of _write_message, and then out of wrapper.
    f = f.f_back.f_back
    file,line,_,_,_ = inspect.getframeinfo(f)
    location = '%s:%d' % (os.path.basename(file), line)

    print >>sys.stderr, '%s: %s: %s' % (location, kind, message)

note = lambda message: _write_message('note', message)
warning = lambda message: _write_message('warning', message)
error = lambda message: _write_message('error', message)
fatal = lambda message: (_write_message('fatal error', message), sys.exit(1))

###

class MultiTool(object):
    """
    This object defines a generic command line tool instance, which dynamically
    builds its commands from a module dictionary.

    Example usage::

      import multitool

      def action_foo(name, args):
          "the foo command"

          ... 

      tool = multitool.MultiTool(locals())
      if __name__ == '__main__':
        tool.main(sys.argv)

    Any function beginning with "action_" is considered a tool command. It's
    name is defined by the function name suffix. Underscores in the function
    name are converted to '-' in the command line syntax. Actions ending ith
    "-debug" are not listed in the help.
    """

    def __init__(self, locals, version=None):
        self.version = version

        # Create the list of commands.
        self.commands = dict((name[7:].replace('_','-'), f)
                             for name,f in locals.items()
                             if name.startswith('action_'))

    def usage(self, name):
        print >>sys.stderr, "Usage: %s <command> [options] ... arguments ..." %(
            os.path.basename(name),)
        print >>sys.stderr
        print >>sys.stderr, """\
Use ``%s <command> --help`` for more information on a specific command.\n""" % (
            os.path.basename(name),)
        print >>sys.stderr, "Available commands:"
        cmds_width = max(map(len, self.commands))
        for name,func in sorted(self.commands.items()):
            if name.endswith("-debug"):
                continue

            print >>sys.stderr, "  %-*s - %s" % (cmds_width, name, func.__doc__)
        sys.exit(1)

    def main(self, args=None):
        if args is None:
            args = sys.argv

        progname = os.path.basename(args.pop(0))

        # Parse immediate command line options.
        while args and args[0].startswith("-"):
            option = args.pop(0)
            if option in ("-h", "--help"):
                self.usage(progname)
            elif option in ("-v", "--version") and self.version is not None:
                print self.version
                return
            else:
                print >>sys.stderr, "error: invalid option %r\n" % (option,)
                self.usage(progname)

        if not args:
            self.usage(progname)

        cmd = args.pop(0)
        if cmd not in self.commands:
            print >>sys.stderr,"error: invalid command %r\n" % cmd
            self.usage(progname)

        self.commands[cmd]('%s %s' % (progname, cmd), args)

###
# Event descriptions

class PTreeTimeEvent(object):
    pass

class PTreeStartEvent(PTreeTimeEvent):
    def __init__(self, ts, target):
        self.timestamp = ts
        self.target = target

class PTreeStopEvent(PTreeTimeEvent):
    def __init__(self, ts):
        self.timestamp = ts

class PTreeProcCreateEvent(PTreeTimeEvent):
    def __init__(self, ts, pid, parent):
        self.timestamp = ts
        self.pid = pid
        self.parent = parent

class PTreeUserStartupEvent(PTreeTimeEvent):
    def __init__(self, ts, pid, parent, utime, stime):
        self.timestamp = ts
        self.pid = pid
        self.parent = parent
        self.user_time = utime
        self.system_time = stime

class PTreeUserExecEvent(PTreeTimeEvent):
    def __init__(self, ts, pid, parent, utime, stime, args):
        self.timestamp = ts
        self.pid = pid
        self.parent = parent
        self.user_time = utime
        self.system_time = stime
        self.args = args

class PTreeUserSpawnEvent(PTreeTimeEvent):
    def __init__(self, ts, pid, parent, utime, stime, args):
        self.timestamp = ts
        self.pid = pid
        self.parent = parent
        self.args = args

class PTreeUserExitEvent(PTreeTimeEvent):
    def __init__(self, ts, pid, parent, utime, stime):
        self.timestamp = ts
        self.pid = pid
        self.parent = parent
        self.user_time = utime
        self.system_time = stime

class PTreeProcExitEvent(PTreeTimeEvent):
    def __init__(self, ts, name, pid, parent):
        self.timestamp = ts
        self.name = name
        self.pid = pid
        self.parent = parent

###

_event_classes = {
    'START' : PTreeStartEvent,
    'proc:::create' : PTreeProcCreateEvent,
    'user startup' : PTreeUserStartupEvent,
    'user exec' : PTreeUserExecEvent,
    'user spawn' : PTreeUserSpawnEvent,
    'user exit' : PTreeUserExitEvent,
    'proc:::exit' : PTreeProcExitEvent,
    'END' : PTreeStopEvent }

###

class ProcessInfo(object):
    def __init__(self, pid):
        self.pid = pid
        self.children = []
        self.name = None
        self.args = None
        self.start_timestamp = None
        self.exit_timestamp = None
        self.user_start_timestamp = None
        self.user_exit_timestamp = None
        self.user_time = None
        self.system_time = None
        self.startup_user_time = None
        self.startup_system_time = None
        self.parent = None

    @property
    def total_wall_time(self):
        return (self.exit_timestamp - self.start_timestamp) / 1000000.
    @property
    def total_user_time(self):
        if self.user_time is None:
            return 0
        return (self.user_time[0] + self.user_time[1] / 1000000.)
    @property
    def total_system_time(self):
        if self.system_time is None:
            return 0
        return (self.system_time[0] + self.system_time[1] / 1000000.)
    @property
    def startlag(self):
        if self.user_start_timestamp is None:
            return 0
        return (self.user_start_timestamp - self.start_timestamp) / 1000000.
    @property
    def exitlag(self):
        if self.user_exit_timestamp is None:
            return 0
        return (self.exit_timestamp - self.user_exit_timestamp) / 1000000.

    @property
    def start_timestamp_in_seconds(self):
        return self.start_timestamp / 1000000.
    @property
    def exit_timestamp_in_seconds(self):
        return self.exit_timestamp / 1000000.

    def todata(self):
        return { 'pid' : self.pid,
                 'name' : self.name,
                 'children' : [c.todata()
                               for c in self.children],
                 'start_timestamp' : self.start_timestamp,
                 'exit_timestamp' : self.exit_timestamp,
                 'user_start_timestamp' : self.user_start_timestamp,
                 'user_exit_timestamp' : self.user_exit_timestamp,
                 'user_time' : self.user_time,
                 'system_time' : self.system_time }

###


def _parse_event_log(path):
    # Load the file as JSON.
    with open(path) as f:
        data = json.load(f)
        command = data['command']
        events_data = data['events']

    # Convert the events data into events.
    def convert_event(data):
        evt_name = data.pop('evt', '').strip()
        evt_class = _event_classes.get(evt_name)
        if evt_class is None:
            raise ValueError("unknown event: %r" % ((evt_name, data),))

        try:
            return evt_class(**data)
        except TypeError:
            raise ValueError("invalid event: %r (unable to instantiate %r)" % (
                    (evt_name, data), evt_class))
    events = [convert_event(d)
              for d in events_data]

    # Order all of the events by timestamp.
    events.sort(key = lambda event: event.timestamp)

    # Check that the file is complete.
    if not events:
        raise ValueError("data file has no events: %r" % (path,))
    if not isinstance(events[0], PTreeStartEvent):
        raise ValueError("data file %r starts with unexpected event %r" % (
              path, events[0]))
    if not isinstance(events[-1], PTreeStopEvent):
        raise ValueError("data file %r ends with unexpected event %r" % (
              path, events[-1]))

    # Get the target PID.
    target = events[0].target
    target_start_timestamp = events[0].timestamp

    # Drop the start and stop events.
    events = events[1:-1]

    # Build the process tree list.
    def get_record_for_pid(pid):
        # Lookup the PID.
        record = active_processes.get(pid)
        if record is not None:
            return record

        # The PID was unknown, create a record for it.
        active_processes[pid] = record = ProcessInfo(pid)

        # Add to the list of all records.
        all_records.append(record)
        roots.add(record)

        return record

    all_records = []
    roots = set()
    target_root = None
    active_processes = {}
    active_spawn_args = {}
    for event in events:
        # Handle the exit of the target process specially.
        if event.pid == target:
            if not isinstance(event, PTreeProcExitEvent):
                raise RuntimeError("unexpected event for target %r" % (
                        event,))

            assert isinstance(event, PTreeProcExitEvent)
            target_root = get_record_for_pid(event.pid)
            roots.remove(target_root)
            target_root.start_timestamp = target_start_timestamp
            target_root.exit_timestamp = event.timestamp
            target_root.name = event.name
            del active_processes[event.pid]
            continue

        # First, lookup or create an entry for the parent process.
        parent_process = get_record_for_pid(event.parent)

        # Get the record for this PID.
        child_process = get_record_for_pid(event.pid)

        # If this is a create event, store the timestamp and add it to the
        # parent.
        if isinstance(event, PTreeProcCreateEvent):
            if child_process.parent:
                raise RuntimeError("multiple create events for %r" % (
                        event,))
            child_process.parent = parent_process
            child_process.start_timestamp = event.timestamp
            parent_process.children.append(child_process)
            roots.remove(child_process)

            child_process.args = active_spawn_args.pop(event.parent, None)
            if child_process.args is not None:
                child_process.name = os.path.basename(child_process.args[0])
            continue

        # Otherwise, handle the various "user" events.
        if isinstance(event, PTreeUserStartupEvent):
            child_process.user_start_timestamp = event.timestamp
            child_process.startup_user_time = event.user_time
            child_process.startup_system_time = event.system_time
            continue
        if isinstance(event, PTreeUserExecEvent):
            # If the child process already has arguments defined, then the
            # process is re-exec'ing itself. Create a new process entry.
            if child_process.args is not None:
                child_process.exit_timestamp = \
                    child_process.user_exit_timestamp = event.timestamp
                child_process.user_time = event.user_time
                child_process.system_time = event.system_time
                # FIXME: Get utime and stime available here, and copy.
                del active_processes[event.pid]

                child_process = get_record_for_pid(event.pid)
                child_process.parent = parent_process
                child_process.start_timestamp = event.timestamp
                child_process.args = event.args
                child_process.name = os.path.basename(child_process.args[0])
                parent_process.children.append(child_process)
                roots.remove(child_process)
            else:
                child_process.args = event.args
                child_process.name = os.path.basename(child_process.args[0])
            continue
        if isinstance(event, PTreeUserSpawnEvent):
            active_spawn_args[event.pid] = event.args
            continue
        if isinstance(event, PTreeUserExitEvent):
            child_process.user_exit_timestamp = event.timestamp
            child_process.user_time = event.user_time
            child_process.system_time = event.system_time
            continue

        # Otherwise, this is an exit event, store the event information and
        # update the parent and active process list.
        if child_process.parent is None:
            warning('found stray exit: %r' % (
                    (event.timestamp, event.pid, event.name),))
            child_process.parent = parent_process
            parent_process.children.append(child_process)
            roots.remove(child_process)
        assert isinstance(event, PTreeProcExitEvent)
        child_process.exit_timestamp = event.timestamp
        child_process.name = event.name
        del active_processes[event.pid]

    return target_root, roots, active_processes

def action_analyze(name, args):
    """analyze a ptreetime data file"""
    parser = optparse.OptionParser("""\
usage: %%prog %(name)s [options] <path>

Execute the given command and track the execution time of all the processes
which are created during the execution.""" % locals())
    parser.add_option("-f", "--focus", dest="focus",
                      help="focus on the given process name",
                      action="store", default=None)
    parser.add_option("--show-tree", dest="show_tree",
                      help="Print the entire call tree",
                      action="store_true")
    parser.add_option("", "--gen-report", dest="lnt_report_path",
                      help="generate an LNT report",
                      action="store", default=None)
    parser.add_option("", "--report-machine", dest="report_machine",
                      help="machine name to include in the LNT report",
                      action="store", default=None)
    parser.add_option("", "--report-run-order", dest="report_run_order",
                      help="run order to include in the LNT report",
                      action="store", default=None)
    parser.add_option("", "--report-sub-arg", dest="report_sub_args",
                      help="add an argument replacement pattern",
                      metavar="PATTERN=REPLACEMENT",
                      action="append", default=[])

    (opts, args) = parser.parse_args(args)

    if len(args) < 1:
       parser.error("invalid number of arguments")
    if opts.lnt_report_path:
        if opts.report_machine is None:
            parser.error("--report-machine is required with --gen-report")
        if opts.report_run_order is None:
            parser.error("--report-run-order is required with --gen-report")
    else:
        if opts.report_machine is not None:
            parser.error("--report-machine is unused without --gen-report")
        if opts.report_run_order is not None:
            parser.error("--report-run-order is unused without --gen-report")
        if opts.report_sub_args:
            parser.error("--report-sub-arg is unused without --gen-report")

    paths = args

    def find_focused_roots(node):
        # If this is a focused node, add it to the list.
        if opts.focus is None or node.name == opts.focus:
            focused_roots.append(node)
            return

        # Otherwise, recurse on each child.
        for child in node.children:
            find_focused_roots(child)
    focused_roots = []
    for path in paths:
        # Load each file.
        target_root,roots,active_processes = _parse_event_log(path)
        find_focused_roots(target_root)

    # For each root, show the flattened performance.
    def print_node(node, depth):
        # If this node isn't complete, ignore it.
        if node.start_timestamp is None:
            warning("ignoring incomplete node %r (didn't see create)" % (
                    (node.pid, node.name),))
            return
        if node.exit_timestamp is None:
            warning("ignoring incomplete node %r (didn't see exit)" % (
                    (node.pid,),))
            return

        indent = '  '*depth
        wall = node.total_wall_time
        user = node.total_user_time
        sys = node.total_system_time
        startlag = node.startlag
        exitlag = node.exitlag
        if opts.show_tree:
            print ('%s%s -- wall: %.4fs, user: %.4fs, sys: %.4fs, '
                   'startlag: %.4fs, exitlag: %.4fs') % (
                indent, node.name, wall, user, sys, startlag, exitlag)

        key = '%s @ %d' % (node.name, depth)
        aggregate_info = aggregate.get(key)
        if aggregate_info is None:
            aggregate[key] = aggregate_info = {
                'name' : key,
                'wall' : 0.0,
                'user' : 0.0,
                'sys' : 0.0,
                'startlag' : 0.0,
                'exitlag' : 0.0,
                'count' : 0 }
        aggregate_info['wall'] += wall
        aggregate_info['user'] += user
        aggregate_info['sys'] += sys
        aggregate_info['startlag'] += startlag
        aggregate_info['exitlag'] += exitlag
        aggregate_info['count'] += 1

        for c in node.children:
            print_node(c, depth+1)

    if not focused_roots:
        parser.error("no roots found!")

    aggregate = {}
    for node in focused_roots:
       print_node(node, depth=0)
       if node is not focused_roots[-1]:
          print

    print
    print 'Aggregate Times'
    print '---------------'
    items = aggregate.values()
    items.sort(key = lambda i: i['wall'],
               reverse = True)
    name_length = max(len(item['name'])
                      for item in items)
    for item in items:
       print ('%-*s -- wall: %8.4fs, user: %8.4fs, sys: %8.4fs, '
              'startlag: %8.4fs, exitlag: %.4fs, count: %d') % (
          name_length, item['name'], item['wall'], item['user'], item['sys'],
          item['startlag'], item['exitlag'], item['count'])

    # Write out an LNT test report with the data, if requested.
    if opts.lnt_report_path:
        import lnt.testing

        # Build the regular expression substitution arguments.
        substitution_args = []
        for value in opts.report_sub_args:
            if '=' in value:
                pattern,replacement = value.split('=',1)
            else:
                pattern,replacement = value,''
            substitution_args.append((re.compile(pattern), replacement))

        # First, organize all the nodes by deriving a test name key for them.
        def group_node_and_children(node, depth = 0):
            # Ignore incomplete nodes.
            if node.start_timestamp is None or node.exit_timestamp is None:
                return

            args = node.args
            if args is not None:
                # If this is a clang -cc1 invocation, do some special stuff to
                # normalize.
                if node.name == "clang" and args and args[1] == '-cc1':
                    triple_index = args.index('-triple')
                    main_file_name_index = args.index('-main-file-name')
                    args = (
                        args[:2] +
                        args[triple_index:triple_index+2] + \
                        args[main_file_name_index:main_file_name_index+2])


                # Apply substitutions to each argument.
                def substitute(arg):
                    for pattern_re,replacement in substitution_args:
                        arg = pattern_re.sub(replacement, arg)
                    return arg
                args = [substitute(arg)
                        for arg in args]

                # Eliminate empty arguments.
                args = filter(None, args)

            key = '%s(depth=%d,args=%r)' % (node.name, depth, args)
            items = nodes_by_key.get(key)
            if items is None:
                nodes_by_key[key] = items = []
            items.append(node)

            for child in node.children:
                group_node_and_children(child, depth+1)
        nodes_by_key = {}
        for node in focused_roots:
            group_node_and_children(node)

        tag = "compile"
        run_info = { "tag" : tag,
                     "run_order" : opts.report_run_order }
        machine = lnt.testing.Machine(opts.report_machine, {})
        run = lnt.testing.Run(datetime.datetime.fromtimestamp(
                target_root.start_timestamp_in_seconds).\
                                  strftime('%Y-%m-%d %H:%M:%S'),
                              datetime.datetime.fromtimestamp(
                target_root.exit_timestamp_in_seconds).\
                                  strftime('%Y-%m-%d %H:%M:%S'),
                              info=run_info)

        # We report in a scheme compatible with the compile time suite.
        testsamples = []
        for key,nodes in nodes_by_key.items():
            for subkey,accessor in (('wall', 'total_wall_time'),
                                    ('user', 'total_user_time'),
                                    ('sys', 'total_system_time')):
                values = [getattr(node, accessor)
                          for node in nodes]
                name = '%s.%s.%s' % (tag, key, subkey)
                testsamples.append(lnt.testing.TestSamples(
                        name, values))

        report = lnt.testing.Report(machine, run, testsamples)
        with open(opts.lnt_report_path, "w") as f:
            print >>f, report.render()

###

def action_profile(name, args):
    """time a process tree"""

    parser = optparse.OptionParser("""\
usage: %%prog %(name)s [options] ... test command args ...

Use dtrace and dyld interpositioning to collect precise time information on an
entire process tree.""" % locals())
    parser.add_option("-v", "--verbose", dest="verbose",
                      help="output more test information",
                      action="store_true", default=False)
    parser.add_option("-o", "--output", dest="output_path",
                      help="path for data output",
                      action="store", default=None)

    parser.disable_interspersed_args()

    (opts, args) = parser.parse_args(args)
    if opts.output_path is None:
        parser.error("--output argument is required")

    command_arguments = args

    # Check that the ptreetime interpose library has been built.
    basepath = os.path.dirname(os.path.realpath(__file__))
    ptreetime_dtrace_path = os.path.join(
        basepath, 'libptreetime', 'ptreetime.dtrace')
    interpose_path = os.path.join(
        basepath, 'libptreetime', 'libptreetime_interpose.dylib')
    interpose_enabled = os.path.exists(interpose_path)
    if not interpose_enabled:
        warning(("interpose library not built, please 'make' in directory: %r"
                 "(some information will not be gathered)") % (
                     os.path.dirname(interpose_path)))

    # First, establish temporary files to contain the dtrace log and the
    # interposed event log.
    ptreetime_dtrace_log = tempfile.NamedTemporaryFile(
        suffix='-ptreetime-dtrace.log')
    ptreetime_interpose_log = tempfile.NamedTemporaryFile(
        suffix='-ptreetime-interpose.log')

    # Write the environment overrides and execution command into a script.
    with tempfile.NamedTemporaryFile(suffix='-ptreetime.sh') as \
            ptreetime_script:
        os.chmod(ptreetime_script.name, 0755)
        print >>ptreetime_script, '#!/bin/sh'
        if interpose_enabled:
            print >>ptreetime_script, 'export PTREETIME_LOG_PATH="%s"' % (
                ptreetime_interpose_log.name)
            print >>ptreetime_script, 'export DYLD_INSERT_LIBRARIES="%s"' % (
                interpose_path)
        # FIXME: Quote arguments better.
        print >>ptreetime_script, ' '.join("'%s'" % arg
                                           for arg in command_arguments)
        ptreetime_script.flush()

        # Form the dtrace command to execute. Notably we:
        #   1. Force dtrace to set up probes at exec time.
        #   2. Extend the buffer size.
        #   3. Send dtrace logging output to the (shared) log file.
        #   4. Execute the script we created.
        dtrace_args = ['sudo', '/usr/sbin/dtrace',
                       '-xevaltime=exec', '-xbufsize=50m',
                       '-s', ptreetime_dtrace_path,
                       '-o', ptreetime_dtrace_log.name,
                       '-c', ptreetime_script.name]

        # Execute the dtrace command.
        note("executing dtrace (with sudo)...")
        if opts.verbose:
            note("executing: %r" % (' '.join(dtrace_args),))
        p = subprocess.Popen(dtrace_args)
        res = p.wait()
        if res != 0:
            warning("dtrace exited with a error (%d)" % (res,))

    # Read in the log file.
    #
    # We simply load all the events lines as text and order them by timestamp,
    # but otherwise we don't process them at all.
    prefix = "PTREETIME "
    prefix_len = len(prefix)
    note("reading dtrace event log...")
    with open(ptreetime_dtrace_log.name) as f:
        event_data_lines = [ln[prefix_len:-1]
                            for ln in f
                            if ln.startswith(prefix)]
    if interpose_enabled:
        note("reading interposed event log...")
        with open(ptreetime_interpose_log.name) as f:
            event_data_lines.extend([ln[prefix_len:-1]
                                     for ln in f
                                     if ln.startswith(prefix)])
    note("loaded %d events" % (len(event_data_lines),))

    # Delete the temporary log files.
    del ptreetime_interpose_log
    del ptreetime_dtrace_log

    # Sort the event lines. Note that this is purely to make for more readable
    # output files, the ptreetime parser will also order.
    def extract_timestamp(ln):
        # Extract the timestamp, if we can.
        timestamp = ln.split(':', 1)[1].split(',', 1)[0]
        if timestamp.isdigit():
            timestamp = int(timestamp)
        return timestamp
    event_data_lines.sort(key = extract_timestamp)

    # Write out the JSON file. We don't care to guarantee this file is well
    # formed (the consumer of the data files can handle that).
    note("writing output file: %r" % (opts.output_path,))
    with open(opts.output_path, "w") as f:
        print >>f, '{'
        print >>f, '  "command" : %s,' % (
            json.dumps(command_arguments),)
        print >>f, '  "events"  : ['
        last_line = event_data_lines[-1]
        for ln in event_data_lines:
            print >>f, '    %s%c' % (ln, ',]'[ln is last_line])
        print >>f, '}'

###

tool = MultiTool(locals(), "ptreetime")

if __name__ == '__main__':
    import sys
    tool.main(sys.argv)