Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(26)

Unified Diff: tools/isolate/trace_inputs.py

Issue 10536078: Enable parallel tracing support on Dtrace (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « tools/isolate/trace_child_process.py ('k') | tools/isolate/trace_inputs_smoke_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tools/isolate/trace_inputs.py
diff --git a/tools/isolate/trace_inputs.py b/tools/isolate/trace_inputs.py
index 29ce4926feb0f989c8c21e2b16574c79e0934edf..515199f59731446eee88a113471f5a9abda36535 100755
--- a/tools/isolate/trace_inputs.py
+++ b/tools/isolate/trace_inputs.py
@@ -29,7 +29,9 @@ import os
import re
import subprocess
import sys
+import tempfile
import threading
+import time
import weakref
## OS-specific imports
@@ -1145,13 +1147,15 @@ class Dtrace(ApiBase):
class Process(ApiBase.Context.Process):
pass
- def __init__(self, blacklist):
+ def __init__(self, blacklist, tracer_pid, initial_cwd):
+ logging.info(
+ '%s(%d, %s)' % (self.__class__.__name__, tracer_pid, initial_cwd))
super(Dtrace.Context, self).__init__(blacklist)
# Process ID of the trace_child_process.py wrapper script instance.
- self._tracer_pid = None
+ self._tracer_pid = tracer_pid
+ self._initial_cwd = initial_cwd
# First process to be started by self._tracer_pid.
self._initial_pid = None
- self._initial_cwd = None
self._line_number = 0
def on_line(self, line):
@@ -1202,13 +1206,11 @@ class Dtrace(ApiBase):
sorted(p.pid for p in process.all))
return Results(process)
- def handle_dtrace_BEGIN(self, pid, args):
- if self._tracer_pid or self._initial_pid:
+ def handle_dtrace_BEGIN(self, _pid, args):
+ if not self.RE_DTRACE_BEGIN.match(args):
raise TracingFailure(
'Found internal inconsitency in dtrace_BEGIN log line',
None, None, None)
- self._tracer_pid = pid
- self._initial_cwd = self.RE_DTRACE_BEGIN.match(args).group(1)
def handle_proc_start(self, pid, args):
"""Transfers cwd.
@@ -1228,26 +1230,24 @@ class Dtrace(ApiBase):
'Failed to parse arguments: %s' % args,
None, None, None)
ppid = int(match.group(1))
- if (ppid == self._tracer_pid) == bool(self._initial_pid):
- raise TracingFailure(
- ( 'Parent process is _tracer_pid(%d) but _initial_pid(%d) is '
- 'already set') % (self._tracer_pid, self._initial_pid),
- None, None, None)
- if not self._initial_pid:
+ if ppid == self._tracer_pid and not self._initial_pid:
self._initial_pid = pid
ppid = None
cwd = self._initial_cwd
- else:
+ elif ppid in self.processes:
parent = self.processes[ppid]
cwd = parent.cwd
+ else:
+ # Another process tree, ignore.
+ return
proc = self.processes[pid] = self.Process(self, pid, cwd, ppid)
proc.cwd = cwd
- logging.debug('New child: %s -> %d' % (ppid, pid))
+ logging.debug('New child: %s -> %d cwd:%s' % (ppid, pid, unicode(cwd)))
def handle_proc_exit(self, pid, _args):
"""Removes cwd."""
- if pid != self._tracer_pid:
- # self._tracer_pid is not traced itself.
+ if pid in self.processes:
+ # self._tracer_pid is not traced itself and other traces run neither.
self.processes[pid].cwd = None
def handle_execve(self, pid, args):
@@ -1260,6 +1260,9 @@ class Dtrace(ApiBase):
Will have to put the answer at http://stackoverflow.com/questions/7556249.
:)
"""
+ if not pid in self.processes:
+ # Another process tree, ignore.
+ return
match = self.RE_EXECVE.match(args)
if not match:
raise TracingFailure(
@@ -1275,10 +1278,9 @@ class Dtrace(ApiBase):
def handle_chdir(self, pid, args):
"""Updates cwd."""
- if not self._tracer_pid or pid not in self.processes:
- raise TracingFailure(
- 'Found inconsistency in the log, is it an old log?',
- None, None, None)
+ if pid not in self.processes:
+ # Another process tree, ignore.
+ return
cwd = self.RE_CHDIR.match(args).group(1)
if not cwd.startswith('/'):
cwd2 = os.path.join(self.processes[pid].cwd, cwd)
@@ -1293,6 +1295,9 @@ class Dtrace(ApiBase):
return self.handle_open(pid, args)
def handle_open(self, pid, args):
+ if pid not in self.processes:
+ # Another process tree, ignore.
+ return
match = self.RE_OPEN.match(args)
if not match:
raise TracingFailure(
@@ -1305,6 +1310,9 @@ class Dtrace(ApiBase):
self._handle_file(pid, match.group(1))
def handle_rename(self, pid, args):
+ if pid not in self.processes:
+ # Another process tree, ignore.
+ return
match = self.RE_RENAME.match(args)
if not match:
raise TracingFailure(
@@ -1324,6 +1332,10 @@ class Dtrace(ApiBase):
return
self.processes[pid].add_file(filepath)
+ def handle_ftruncate(self, pid, args):
+ """Just used as a signal to kill dtrace, ignoring."""
+ pass
+
@staticmethod
def _handle_ignored(pid, args):
"""Is called for all the event traces that are not handled."""
@@ -1372,15 +1384,17 @@ class Dtrace(ApiBase):
# 1 is for tracked processes.
# 2 is for trace_child_process.py only. It is not tracked itself but
# all its decendants are.
+ #
+ # The script will kill itself only once waiting_to_die == 1 and
+ # current_processes == 0, so that both getlogin() was called and that
+ # all traced processes exited.
D_CODE = """
dtrace:::BEGIN {
- /* Since the child process is already started, initialize
- current_processes to 1. */
- current_processes = 1;
+ waiting_to_die = 0;
+ current_processes = 0;
logindex = 0;
- trackedpid[TRACER_PID] = 2;
printf("%d %d %s_%s(\\"%s\\")\\n",
- logindex, TRACER_PID, probeprov, probename, CWD);
+ logindex, PID, probeprov, probename, SCRIPT);
logindex++;
}
@@ -1394,8 +1408,21 @@ class Dtrace(ApiBase):
current_processes);
logindex++;
}
- proc:::exit /trackedpid[pid] && current_processes == 1/ {
- /* Last process is exiting. */
+ /* Should use SCRIPT but there is no access to this variable at that
+ * point. */
+ proc:::start /ppid == PID && execname == "Python"/ {
+ trackedpid[pid] = 2;
+ current_processes += 1;
+ printf("%d %d %s_%s(%d, \\"%s\\", %d)\\n",
+ logindex, pid, probeprov, probename,
+ ppid,
+ execname,
+ current_processes);
+ logindex++;
+ }
+ proc:::exit /trackedpid[pid] &&
+ current_processes == 1 &&
+ waiting_to_die == 1/ {
trackedpid[pid] = 0;
current_processes -= 1;
printf("%d %d %s_%s(%d)\\n",
@@ -1413,6 +1440,17 @@ class Dtrace(ApiBase):
logindex++;
}
+ /* Use an arcane function to detect when we need to die */
+ syscall::ftruncate:entry /pid == PID && arg0 == FILE_ID/ {
+ waiting_to_die = 1;
+ printf("%d %d %s()\\n", logindex, pid, probefunc);
+ logindex++;
+ }
+ syscall::ftruncate:entry /
+ pid == PID && arg0 == FILE_ID && current_processes == 0/ {
+ exit(0);
+ }
+
syscall::open*:entry /trackedpid[pid] == 1/ {
self->open_arg0 = arg0;
self->open_arg1 = arg1;
@@ -1629,23 +1667,65 @@ class Dtrace(ApiBase):
*/
"""
- @classmethod
- def code(cls, pid, cwd):
+ def __init__(self, logname):
+ """Starts the log collection with dtrace.
+
+ Requires root access or chmod 4555 on dtrace. dtrace is asynchronous so
+ this needs to wait for dtrace to be "warmed up".
+ """
+ super(Dtrace.Tracer, self).__init__(logname)
+ self._script = os.path.join(BASE_DIR, 'trace_child_process.py')
+ # This unique dummy temp file is used to signal the dtrace script that it
+ # should stop as soon as all the child processes are done. A bit hackish
+ # but works fine enough.
+ self._dummy_file_id, self._dummy_file_name = tempfile.mkstemp(
+ prefix='trace_signal_file')
+
+ # Note: do not use the -p flag. It's useless if the initial process quits
+ # too fast, resulting in missing traces from the grand-children. The D
+ # code manages the dtrace lifetime itself.
+ trace_cmd = [
+ 'sudo',
+ 'dtrace',
+ '-x', 'dynvarsize=4m',
+ '-x', 'evaltime=exec',
+ '-o', '/dev/stderr',
+ '-q',
+ '-n', self._get_dtrace_code(),
+ ]
+ with open(self._logname + '.log', 'wb') as logfile:
+ self._dtrace = subprocess.Popen(
+ trace_cmd, stdout=logfile, stderr=subprocess.STDOUT)
+ logging.debug('Started dtrace pid: %d' % self._dtrace.pid)
+
+ # Reads until one line is printed, which signifies dtrace is up and ready.
+ with open(self._logname + '.log', 'rb') as logfile:
+ while 'dtrace_BEGIN' not in logfile.readline():
+ if self._dtrace.poll() is not None:
+ # Do a busy wait. :/
+ break
+ logging.debug('dtrace started')
+
+ def _get_dtrace_code(self):
"""Setups the D code to implement child process tracking.
- Injects the pid and the initial cwd into the trace header for context.
- The reason is that the child process trace_child_process.py is already
- running at that point so:
- - no proc_start() is logged for it.
- - there is no way to figure out the absolute path of cwd in kernel on OSX
+ Injects the cookie in the script so it knows when to stop.
+
+ The script will detect any trace_child_process.py instance and will start
+ tracing it.
"""
- cwd = os.path.realpath(cwd).replace('\\', '\\\\').replace('%', '%%')
return (
- 'inline int TRACER_PID = %d;\n'
- 'inline string CWD = "%s";\n'
+ 'inline int PID = %d;\n'
+ 'inline string SCRIPT = "%s";\n'
+ 'inline int FILE_ID = %d;\n'
'\n'
'%s\n'
- '%s') % (pid, cwd, cls.D_CODE, cls.D_CODE_EXECVE)
+ '%s') % (
+ os.getpid(),
+ self._script,
+ self._dummy_file_id,
+ self.D_CODE,
+ self.D_CODE_EXECVE)
def trace(self, cmd, cwd, tracename, output):
"""Runs dtrace on an executable.
@@ -1657,7 +1737,6 @@ class Dtrace(ApiBase):
"""
logging.info('trace(%s, %s, %s, %s)' % (cmd, cwd, tracename, output))
logging.info('Running: %s' % cmd)
- signal = 'Go!'
logging.debug('Our pid: %d' % os.getpid())
with self._lock:
if not self._initialized:
@@ -1666,16 +1745,20 @@ class Dtrace(ApiBase):
None, None, None, tracename)
assert tracename not in (i['trace'] for i in self._traces)
- # Part 1: start the child process.
+ # Starts the script wrapper to start the child process. This signals the
+ # dtrace script that this process is to be traced.
stdout = stderr = None
if output:
stdout = subprocess.PIPE
stderr = subprocess.STDOUT
child_cmd = [
sys.executable,
- os.path.join(BASE_DIR, 'trace_child_process.py'),
- '--wait',
+ self._script,
+ tracename,
]
+ # Call a dummy function so that dtrace knows I'm about to launch a process
+ # that needs to be traced.
+ # Yummy.
child = subprocess.Popen(
child_cmd + cmd,
stdin=subprocess.PIPE,
@@ -1684,44 +1767,9 @@ class Dtrace(ApiBase):
cwd=cwd)
logging.debug('Started child pid: %d' % child.pid)
- # Part 2: start dtrace process.
- # Note: do not use the -p flag. It's useless if the initial process quits
- # too fast, resulting in missing traces from the grand-children. The D
- # code manages the dtrace lifetime itself.
- trace_cmd = [
- 'sudo',
- 'dtrace',
- '-x', 'dynvarsize=4m',
- '-x', 'evaltime=exec',
- '-n', self.code(child.pid, cwd),
- '-o', '/dev/stderr',
- '-q',
- ]
- logname = self._logname + '.log'
- with open(logname, 'w') as logfile:
- dtrace = subprocess.Popen(
- trace_cmd, stdout=logfile, stderr=subprocess.STDOUT)
- logging.debug('Started dtrace pid: %d' % dtrace.pid)
-
- # Part 3: Read until one line is printed, which signifies dtrace is up and
- # ready.
- with open(logname, 'r') as logfile:
- while 'dtrace_BEGIN' not in logfile.readline():
- if dtrace.poll() is not None:
- break
-
- # Part 4: We can now tell our child to go.
- # TODO(maruel): Another pipe than stdin could be used instead. This
- # would be more consistent with the other tracing methods.
- out = child.communicate(signal)[0]
-
- dtrace.wait()
- if dtrace.returncode != 0:
- print 'dtrace failure: %d' % dtrace.returncode
- with open(logname) as logfile:
- print ''.join(logfile.readlines()[-100:])
- # Find a better way.
- os.remove(logname)
+ out = child.communicate()[0]
+ # This doesn't mean tracing is done, one of the grand-child process may
+ # still be alive. It will be tracked with the dtrace script.
with self._lock:
assert tracename not in (i['trace'] for i in self._traces)
@@ -1734,7 +1782,39 @@ class Dtrace(ApiBase):
'trace': tracename,
'output': out,
})
- return dtrace.returncode or child.returncode, out
+ return child.returncode, out
+
+ def close(self, timeout=None):
+ """Terminates dtrace."""
+ logging.debug('close(%s)' % timeout)
+ try:
+ try:
+ super(Dtrace.Tracer, self).close(timeout)
+ # Signal dtrace that it should stop now.
+ os.ftruncate(self._dummy_file_id, 0)
+ if timeout:
+ start = time.time()
+ # Use polling. :/
+ while (self._dtrace.poll() is None and
+ (time.time() - start) < timeout):
+ time.sleep(0.1)
+ self._dtrace.kill()
+ self._dtrace.wait()
+ finally:
+ # Make sure to kill it in any case.
+ if self._dtrace.poll() is None:
+ try:
+ self._dtrace.kill()
+ self._dtrace.wait()
+ except OSError:
+ pass
+
+ if self._dtrace.returncode != 0:
+ # Warn about any dtrace failure but basically ignore it.
+ print 'dtrace failure: %s' % self._dtrace.returncode
+ finally:
+ os.close(self._dummy_file_id)
+ os.remove(self._dummy_file_name)
def post_process_log(self):
"""Sorts the log back in order when each call occured.
@@ -1773,7 +1853,7 @@ class Dtrace(ApiBase):
data = read_json(logname)
out = []
for item in data['traces']:
- context = cls.Context(blacklist)
+ context = cls.Context(blacklist, item['pid'], item['cwd'])
for line in open(logname + '.log', 'rb'):
context.on_line(line)
out.append(
« no previous file with comments | « tools/isolate/trace_child_process.py ('k') | tools/isolate/trace_inputs_smoke_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698