[utils] Add rudimentary process tree timing tool.
- This uses dtrace + interpositioning to try and track timing information for a
full process tree, which can be useful when analyzing build system
performance.
- This has only been tested on macOS, in very limited circumstances.
diff --git a/utils/ptreetime/README.md b/utils/ptreetime/README.md
new file mode 100644
index 0000000..5e72c6f
--- /dev/null
+++ b/utils/ptreetime/README.md
@@ -0,0 +1,7 @@
+# ptreetime
+
+A utility for profiling process tree times.
+
+This currently only has been tested on macOS. It also will only fully work if
+DYLD is able to inject interpositioning libraries into the target process (this
+won't work on processes with library validation enabled, for example).
diff --git a/utils/ptreetime/libptreetime/.gitignore b/utils/ptreetime/libptreetime/.gitignore
new file mode 100644
index 0000000..e5d0467
--- /dev/null
+++ b/utils/ptreetime/libptreetime/.gitignore
@@ -0,0 +1 @@
+*.dylib
diff --git a/utils/ptreetime/libptreetime/Makefile b/utils/ptreetime/libptreetime/Makefile
new file mode 100644
index 0000000..de3b4b0
--- /dev/null
+++ b/utils/ptreetime/libptreetime/Makefile
@@ -0,0 +1,31 @@
+#===- Makefile -------------------------------------------------------------===#
+#
+# This source file is part of the Swift.org open source project
+#
+# Copyright (c) 2017 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
+#
+#===------------------------------------------------------------------------===#
+
+CC := clang
+ARCHS := x86_64
+CFLAGS := -O3 -mmacosx-version-min=10.11
+LDFLAGS :=
+LIBPTREETIME_INTERPOSE_CFLAGS := $(CFLAGS)
+LIBPTREETIME_INTERPOSE_LDFLAGS := $(LDFLAGS)
+
+all: libptreetime_interpose.dylib
+
+libptreetime_interpose.dylib: ptreetime_interpose.c
+ $(CC) \
+ $(ARCHS:%=-arch %) \
+ $(LIBPTREETIME_INTERPOSE_CFLAGS) \
+ -dynamiclib \
+ -o $@ $^ $(LIBPTREETIME_INTERPOSE_LDFLAGS)
+
+clean:
+ rm -f libptreetime_interpose.dylib *~
+.PHONY: clean
diff --git a/utils/ptreetime/libptreetime/ptreetime.dtrace b/utils/ptreetime/libptreetime/ptreetime.dtrace
new file mode 100644
index 0000000..dbec729
--- /dev/null
+++ b/utils/ptreetime/libptreetime/ptreetime.dtrace
@@ -0,0 +1,52 @@
+/*===- ptreetime.dtrace ---------------------------------------------------===
+ *
+ * This source file is part of the Swift.org open source project
+ *
+ * Copyright (c) 2017 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
+ *
+ *===----------------------------------------------------------------------===*/
+
+/* DTrace module for use with `ptreetime`.
+ *
+ * This dtrace module adds probes on the kernel process creation and exit
+ * events, in order to precisely track an entire process tree.
+ *
+ * By itself, the events generated by this module can only be used to determine
+ * the wall time a process used and the name of the primary executable. However,
+ * in conjunction with the ``ptreetime_interpose`` dylib, the events can be
+ * combined to gather information about the exact command lines that were
+ * executed, the process resource usage (user and CPU times), and the wall times
+ * of additional events (like the time between when the kernel created the
+ * process and when the processes' first user constructor ran).
+ */
+
+#pragma D option quiet
+
+dtrace:::BEGIN {
+ printf("PTREETIME { \"ts\" : %d, \"evt\" : \"%-14s\", ",
+ walltimestamp/1000, "START");
+ printf("\"target\" : %d }\n", $target);
+}
+
+proc:::create {
+ printf("PTREETIME { \"ts\" : %d, \"evt\" : \"%-14s\", ",
+ walltimestamp/1000, "proc:::create");
+ printf("\"pid\" : %d, \"parent\" : %d }\n",
+ ((struct proc*) arg0)->p_pid, ((struct proc*) arg0)->p_ppid);
+}
+
+proc:::exit {
+ printf("PTREETIME { \"ts\" : %d, \"evt\" : \"%-14s\", ",
+ walltimestamp/1000, "proc:::exit");
+ printf("\"pid\" : %d, \"parent\" : %d, \"name\" : \"%s\" }\n",
+ curproc->p_pid, curproc->p_ppid, curproc->p_comm);
+}
+
+dtrace:::END {
+ printf("PTREETIME { \"ts\" : %d, \"evt\" : \"%-14s\" }\n",
+ walltimestamp/1000, "END");
+}
diff --git a/utils/ptreetime/libptreetime/ptreetime_interpose.c b/utils/ptreetime/libptreetime/ptreetime_interpose.c
new file mode 100644
index 0000000..d6d61f9
--- /dev/null
+++ b/utils/ptreetime/libptreetime/ptreetime_interpose.c
@@ -0,0 +1,244 @@
+//===- ptreetime_interpose.c ----------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2017 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
+//
+//===----------------------------------------------------------------------===//
+
+/* This file defines a simple interposition library which is intended for use
+ * with `ptreetime`.
+ *
+ * The library is not intended to be used directly, rather it is automatically
+ * inserted using ``DYLD_INSERT_LIBRARIES``.
+ *
+ * The library itself interpositions on ``_exit``, ``execve``, and
+ * ``posix_spawn``. The library also inserts a static constructor which will be
+ * run at load time. Due to the way ``dyld`` executes static constructors, the
+ * startup constructor will be run after all of ``libSystem``'s initializers
+ * (because the library itself needs to load libSystem), but will always be run
+ * before any of the primary application constructors.
+ *
+ * The library logs various events to an output file specified by the
+ * ``PTREETIME_LOG_PATH`` environment variable. The events are written with
+ * timestamps defined by gettimeofday(), which allows the events to be properly
+ * ordered with respect to dtrace events.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <mach/mach_time.h>
+#include <spawn.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#define DYLD_INTERPOSE(_src,_dst) \
+ __attribute__((used)) static struct { \
+ const void* src; \
+ const void* dst; \
+ } _interpose_##_dst __attribute__ ((section ("__DATA,__interpose"))) = { \
+ (const void*)(uintptr_t)&_src, \
+ (const void*)(uintptr_t)&_dst \
+ };
+
+const char *ptreetime_log_path = 0;
+static int ptreetime_log_file = -1;
+
+static void error(const char *fmt, ...) {
+ /* Get the current time of day. */
+ struct timeval t;
+ gettimeofday(&t, NULL);
+
+ char buffer[1024];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(buffer, sizeof(buffer), fmt, ap);
+ va_end(ap);
+
+ fprintf(stderr,
+ "PTREETIME { \"ts\" : %lld, \"error\" : '%s', \"errno\" : %d }\n",
+ (uint64_t) t.tv_sec * 1000000 + t.tv_usec, buffer, errno);
+}
+
+static void ptreetime_write_buffer(const char *buffer, int size,
+ int allow_reopen) {
+ /* If the log file hasn't been opened, open it. */
+ if (ptreetime_log_file == -1) {
+ /* Only log if the PTREETIME_LOG_PATH environment variable is set. */
+ ptreetime_log_path = getenv("PTREETIME_LOG_PATH");
+ if (!ptreetime_log_path) {
+ errno = 0;
+ error("no PTREETIME_LOG_PATH variable set");
+ return;
+ }
+
+ ptreetime_log_file = open(ptreetime_log_path, O_WRONLY|O_CREAT|O_APPEND,
+ 0664);
+ if (ptreetime_log_file == -1) {
+ error("unable to open log file '%s' (%s)", ptreetime_log_path,
+ strerror(errno));
+ return;
+ }
+ }
+
+ struct iovec iov = { (void*) buffer, size };
+ if (writev(ptreetime_log_file, &iov, 1) == -1) {
+ /* If the write failed with a bad file descriptor, retry with a reopen, in
+ case someone decided to close our output file descriptor. */
+ if (allow_reopen && errno == EBADF) {
+ ptreetime_log_file = -1;
+ ptreetime_write_buffer(buffer, size, /*allow_reopen=*/0);
+ return;
+ }
+
+ /* Otherwise, report the error. */
+ error("unable to write to log file '%s' (%s)", ptreetime_log_path,
+ strerror(errno));
+ }
+}
+
+static void write_log_entry(const char *event) {
+ /* Get the program's resource usage. */
+ struct rusage usage;
+ getrusage(RUSAGE_SELF, &usage);
+
+ /* Get the current time of day. */
+ struct timeval t;
+ gettimeofday(&t, NULL);
+
+ /* Write the log event. */
+ char buffer[1024];
+ int size = snprintf(buffer, sizeof(buffer),
+ ("PTREETIME { \"ts\" : %lld, \"evt\" : \"%-14s\", "
+ "\"pid\" : %d, \"parent\" : %d, "
+ "\"utime\" : [%ld, %d], \"stime\" : [%ld, %d] }\n"),
+ (uint64_t) t.tv_sec * 1000000 + t.tv_usec, event,
+ getpid(), getppid(),
+ usage.ru_utime.tv_sec, usage.ru_utime.tv_usec,
+ usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
+
+ /* Use writev to get an atomic write. */
+ ptreetime_write_buffer(buffer, size, /*allow_reopen=*/1);
+}
+
+static int clean_arg(const char *arg, char *dst, unsigned dst_size) {
+ unsigned pos = 0;
+
+ for (unsigned i = 0; ; ++i) {
+ char c = arg[i];
+
+ /* If we have an unclean character, escape it. */
+ if (arg[i] == '\\' || arg[i] == '"') {
+ if (pos == dst_size)
+ return 0;
+ dst[pos++] = '\\';
+ }
+
+ if (pos == dst_size)
+ return 0;
+ dst[pos++] = c;
+
+ if (c == '\0')
+ return 1;
+ }
+}
+
+static void write_create_log_entry(const char *event, char *const argv[]) {
+ /* Get the program's resource usage. */
+ struct rusage usage;
+ getrusage(RUSAGE_SELF, &usage);
+
+ /* Get the current time of day. */
+ struct timeval t;
+ gettimeofday(&t, NULL);
+
+ /* Write the log event start. */
+ char buffer[4096];
+ int size = snprintf(buffer, sizeof(buffer),
+ ("PTREETIME { \"ts\" : %lld, \"evt\" : \"%-14s\", "
+ "\"pid\" : %d, \"parent\" : %d, "
+ "\"utime\" : [%ld, %d], \"stime\" : [%ld, %d], "
+ "\"args\" : ["),
+ (uint64_t) t.tv_sec * 1000000 + t.tv_usec, event,
+ getpid(), getppid(),
+ usage.ru_utime.tv_sec, usage.ru_utime.tv_usec,
+ usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
+
+ if (size >= sizeof(buffer))
+ abort();
+
+ /* Write the arguments (or as many will fit). */
+ unsigned safe_space = strlen("\"...\"") + strlen("] }\n");
+ for (unsigned i = 0; argv[i]; ++i) {
+ /* Clean the argument, to escape any characters for JSON. */
+ char arg_buffer[1024];
+ if (!clean_arg(argv[i], arg_buffer, sizeof(arg_buffer))) {
+ size += snprintf(&buffer[size], sizeof(buffer) - size, "\"...\"");
+ break;
+ }
+
+ /* Write the argument into the buffer, if we have enough room. */
+ int bytes = snprintf(&buffer[size], sizeof(buffer) - size - safe_space,
+ "\"%s\"%s", arg_buffer, argv[i+1] ? ", " : "");
+
+ /* If we are out of space, break. */
+ if (bytes >= sizeof(buffer) - size - safe_space) {
+ size += snprintf(&buffer[size], sizeof(buffer) - size, "\"...\"");
+ break;
+ }
+
+ size += bytes;
+ }
+
+ /* Write the log event end. */
+ size += snprintf(&buffer[size], sizeof(buffer) - size, "] }\n");
+
+ /* Write the event. */
+ ptreetime_write_buffer(buffer, size, /*allow_reopen=*/1);
+}
+
+void interposed_exit(int status) {
+ write_log_entry("user exit");
+
+ return _exit(status);
+}
+DYLD_INTERPOSE(interposed_exit, _exit);
+
+static void interposed_startup() \
+ __attribute__((used)) __attribute__((constructor));
+static void interposed_startup() {
+ write_log_entry("user startup");
+}
+
+int interposed_execve(const char *path, char *const argv[],
+ char *const envp[]) {
+ /* On Darwin, execvp will call execve in a loop. Only write the log entry if
+ we see we have execute permission on the file. */
+ if (access(path, X_OK) == 0)
+ write_create_log_entry("user exec", argv);
+
+ return execve(path, argv, envp);
+}
+DYLD_INTERPOSE(interposed_execve, execve);
+
+int interposed_posix_spawn(pid_t *restrict pid, const char *restrict path,
+ const posix_spawn_file_actions_t *file_actions,
+ const posix_spawnattr_t *restrict attrp,
+ char *const argv[],
+ char *const envp[restrict]) {
+ write_create_log_entry("user spawn", argv);
+ return posix_spawn(pid, path, file_actions, attrp, argv, envp);
+}
+DYLD_INTERPOSE(interposed_posix_spawn, posix_spawn);
+
diff --git a/utils/ptreetime/ptreetime.py b/utils/ptreetime/ptreetime.py
new file mode 100755
index 0000000..984b494
--- /dev/null
+++ b/utils/ptreetime/ptreetime.py
@@ -0,0 +1,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)