diff options
Diffstat (limited to 'chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl')
9 files changed, 643 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/__init__.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/__init__.py new file mode 100644 index 00000000000..d250e0312e4 --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +from log import * +from decorators import * +from meta_class import * +import multiprocessing_shim diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators.py new file mode 100644 index 00000000000..dc753f1f61b --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators.py @@ -0,0 +1,87 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import contextlib +import inspect +import time +import functools + +import log +from py_trace_event import trace_time + + +@contextlib.contextmanager +def trace(name, **kwargs): + category = "python" + start = trace_time.Now() + args_to_log = {key: repr(value) for key, value in kwargs.iteritems()} + log.add_trace_event("B", start, category, name, args_to_log) + try: + yield + finally: + end = trace_time.Now() + log.add_trace_event("E", end, category, name) + +def traced(*args): + def get_wrapper(func): + if inspect.isgeneratorfunction(func): + raise Exception("Can not trace generators.") + + category = "python" + + arg_spec = inspect.getargspec(func) + is_method = arg_spec.args and arg_spec.args[0] == "self" + + def arg_spec_tuple(name): + arg_index = arg_spec.args.index(name) + defaults_length = len(arg_spec.defaults) if arg_spec.defaults else 0 + default_index = arg_index + defaults_length - len(arg_spec.args) + if default_index >= 0: + default = arg_spec.defaults[default_index] + else: + default = None + return (name, arg_index, default) + + args_to_log = map(arg_spec_tuple, arg_names) + + @functools.wraps(func) + def traced_function(*args, **kwargs): + # Everything outside traced_function is done at decoration-time. + # Everything inside traced_function is done at run-time and must be fast. + if not log._enabled: # This check must be at run-time. + return func(*args, **kwargs) + + def get_arg_value(name, index, default): + if name in kwargs: + return kwargs[name] + elif index < len(args): + return args[index] + else: + return default + + if is_method: + name = "%s.%s" % (args[0].__class__.__name__, func.__name__) + else: + name = "%s.%s" % (func.__module__, func.__name__) + + # Be sure to repr before calling func. Argument values may change. + arg_values = { + name: repr(get_arg_value(name, index, default)) + for name, index, default in args_to_log} + + start = trace_time.Now() + log.add_trace_event("B", start, category, name, arg_values) + try: + return func(*args, **kwargs) + finally: + end = trace_time.Now() + log.add_trace_event("E", end, category, name) + return traced_function + + no_decorator_arguments = len(args) == 1 and callable(args[0]) + if no_decorator_arguments: + arg_names = () + return get_wrapper(args[0]) + else: + arg_names = args + return get_wrapper diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py new file mode 100644 index 00000000000..5bb13ad8859 --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/decorators_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import decorators +import logging +import unittest + +from trace_test import TraceTest +#from .trace_test import TraceTest + +def generator(): + yield 1 + yield 2 + +class DecoratorTests(unittest.TestCase): + def test_tracing_object_fails(self): + self.assertRaises(Exception, lambda: decorators.trace(1)) + self.assertRaises(Exception, lambda: decorators.trace("")) + self.assertRaises(Exception, lambda: decorators.trace([])) + + def test_tracing_generators_fail(self): + self.assertRaises(Exception, lambda: decorators.trace(generator)) + +class ClassToTest(object): + @decorators.traced + def method1(self): + return 1 + + @decorators.traced + def method2(self): + return 1 + +@decorators.traced +def traced_func(): + return 1 + +class DecoratorTests(TraceTest): + def _get_decorated_method_name(self, f): + res = self.go(f) + events = res.findEventsOnThread(res.findThreadIds()[0]) + + # Sanity checks. + self.assertEquals(2, len(events)) + self.assertEquals(events[0]["name"], events[1]["name"]) + return events[1]["name"] + + + def test_func_names_work(self): + self.assertEquals('__main__.traced_func', + self._get_decorated_method_name(traced_func)) + + def test_method_names_work(self): + ctt = ClassToTest() + self.assertEquals('ClassToTest.method1', + self._get_decorated_method_name(ctt.method1)) + self.assertEquals('ClassToTest.method2', + self._get_decorated_method_name(ctt.method2)) + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.DEBUG) + unittest.main(verbosity=2) diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py new file mode 100644 index 00000000000..2d69f083af0 --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log.py @@ -0,0 +1,183 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import atexit +import json +import os +import sys +import time +import threading + +from py_trace_event import trace_time + +from py_utils import lock + + +_lock = threading.Lock() + +_enabled = False +_log_file = None + +_cur_events = [] # events that have yet to be buffered + +_tls = threading.local() # tls used to detect forking/etc +_atexit_regsitered_for_pid = None + +_control_allowed = True + + +class TraceException(Exception): + pass + +def _note(msg, *args): + pass +# print "%i: %s" % (os.getpid(), msg) + + +def _locked(fn): + def locked_fn(*args,**kwargs): + _lock.acquire() + try: + ret = fn(*args,**kwargs) + finally: + _lock.release() + return ret + return locked_fn + +def _disallow_tracing_control(): + global _control_allowed + _control_allowed = False + +def trace_enable(log_file=None): + _trace_enable(log_file) + +@_locked +def _trace_enable(log_file=None): + global _enabled + if _enabled: + raise TraceException("Already enabled") + if not _control_allowed: + raise TraceException("Tracing control not allowed in child processes.") + _enabled = True + global _log_file + if log_file == None: + if sys.argv[0] == '': + n = 'trace_event' + else: + n = sys.argv[0] + log_file = open("%s.json" % n, "ab", False) + _note("trace_event: tracelog name is %s.json" % n) + elif isinstance(log_file, basestring): + _note("trace_event: tracelog name is %s" % log_file) + log_file = open("%s" % log_file, "ab", False) + elif not hasattr(log_file, 'fileno'): + raise TraceException( + "Log file must be None, a string, or file-like object with a fileno()") + + _log_file = log_file + with lock.FileLock(_log_file, lock.LOCK_EX): + _log_file.seek(0, os.SEEK_END) + + lastpos = _log_file.tell() + creator = lastpos == 0 + if creator: + _note("trace_event: Opened new tracelog, lastpos=%i", lastpos) + _log_file.write('[') + + tid = threading.current_thread().ident + if not tid: + tid = os.getpid() + x = {"ph": "M", "category": "process_argv", + "pid": os.getpid(), "tid": threading.current_thread().ident, + "ts": trace_time.Now(), + "name": "process_argv", "args": {"argv": sys.argv}} + _log_file.write("%s\n" % json.dumps(x)) + else: + _note("trace_event: Opened existing tracelog") + _log_file.flush() + +@_locked +def trace_flush(): + if _enabled: + _flush() + +@_locked +def trace_disable(): + global _enabled + if not _control_allowed: + raise TraceException("Tracing control not allowed in child processes.") + if not _enabled: + return + _enabled = False + _flush(close=True) + +def _flush(close=False): + global _log_file + with lock.FileLock(_log_file, lock.LOCK_EX): + _log_file.seek(0, os.SEEK_END) + if len(_cur_events): + _log_file.write(",\n") + _log_file.write(",\n".join([json.dumps(e) for e in _cur_events])) + del _cur_events[:] + + if close: + # We might not be the only process writing to this logfile. So, + # we will simply close the file rather than writign the trailing ] that + # it technically requires. The trace viewer understands that this may + # happen and will insert a trailing ] during loading. + pass + _log_file.flush() + + if close: + _note("trace_event: Closed") + _log_file.close() + _log_file = None + else: + _note("trace_event: Flushed") + +@_locked +def trace_is_enabled(): + return _enabled + +@_locked +def add_trace_event(ph, ts, category, name, args=None): + global _enabled + if not _enabled: + return + if not hasattr(_tls, 'pid') or _tls.pid != os.getpid(): + _tls.pid = os.getpid() + global _atexit_regsitered_for_pid + if _tls.pid != _atexit_regsitered_for_pid: + _atexit_regsitered_for_pid = _tls.pid + atexit.register(_trace_disable_atexit) + _tls.pid = os.getpid() + del _cur_events[:] # we forked, clear the event buffer! + tid = threading.current_thread().ident + if not tid: + tid = os.getpid() + _tls.tid = tid + + _cur_events.append({"ph": ph, + "category": category, + "pid": _tls.pid, + "tid": _tls.tid, + "ts": ts, + "name": name, + "args": args or {}}); + +def trace_begin(name, args=None): + add_trace_event("B", trace_time.Now(), "python", name, args) + +def trace_end(name, args=None): + add_trace_event("E", trace_time.Now(), "python", name, args) + +def trace_set_thread_name(thread_name): + add_trace_event("M", trace_time.Now(), "__metadata", "thread_name", + {"name": thread_name}) + +def _trace_disable_atexit(): + trace_disable() + +def is_tracing_controllable(): + global _control_allowed + return _control_allowed diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py new file mode 100644 index 00000000000..99a062115bc --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/log_io_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import logging +import os +import sys +import tempfile +import unittest + +from log import * +from parsed_trace_events import * + + +class LogIOTest(unittest.TestCase): + def test_enable_with_file(self): + file = tempfile.NamedTemporaryFile() + trace_enable(open(file.name, 'w+')) + trace_disable() + e = ParsedTraceEvents(trace_filename = file.name) + file.close() + self.assertTrue(len(e) > 0) + + def test_enable_with_filename(self): + file = tempfile.NamedTemporaryFile() + trace_enable(file.name) + trace_disable() + e = ParsedTraceEvents(trace_filename = file.name) + file.close() + self.assertTrue(len(e) > 0) + + def test_enable_with_implicit_filename(self): + expected_filename = "%s.json" % sys.argv[0] + def do_work(): + file = tempfile.NamedTemporaryFile() + trace_enable() + trace_disable() + e = ParsedTraceEvents(trace_filename = expected_filename) + file.close() + self.assertTrue(len(e) > 0) + try: + do_work() + finally: + if os.path.exists(expected_filename): + os.unlink(expected_filename) + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.DEBUG) + unittest.main(verbosity=2) + diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py new file mode 100644 index 00000000000..4ede79bd338 --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/meta_class.py @@ -0,0 +1,16 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import types + +from py_trace_event.trace_event_impl import decorators + + +class TracedMetaClass(type): + def __new__(cls, name, bases, attrs): + for attr_name, attr_value in attrs.iteritems(): + if isinstance(attr_value, types.FunctionType): + attrs[attr_name] = decorators.traced(attr_value) + + return super(TracedMetaClass, cls).__new__(cls, name, bases, attrs) diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py new file mode 100644 index 00000000000..9796bdf9665 --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/multiprocessing_shim.py @@ -0,0 +1,92 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import multiprocessing +import log +import time + + +_RealProcess = multiprocessing.Process +__all__ = [] + + +class ProcessSubclass(_RealProcess): + def __init__(self, shim, *args, **kwards): + _RealProcess.__init__(self, *args, **kwards) + self._shim = shim + + def run(self,*args,**kwargs): + log._disallow_tracing_control() + try: + r = _RealProcess.run(self, *args, **kwargs) + finally: + if log.trace_is_enabled(): + log.trace_flush() # todo, reduce need for this... + return r + +class ProcessShim(): + def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): + self._proc = ProcessSubclass(self, group, target, name, args, kwargs) + # hint to testing code that the shimming worked + self._shimmed_by_trace_event = True + + def run(self): + self._proc.run() + + def start(self): + self._proc.start() + + def terminate(self): + if log.trace_is_enabled(): + # give the flush a chance to finish --> TODO: find some other way. + time.sleep(0.25) + self._proc.terminate() + + def join(self, timeout=None): + self._proc.join( timeout) + + def is_alive(self): + return self._proc.is_alive() + + @property + def name(self): + return self._proc.name + + @name.setter + def name(self, name): + self._proc.name = name + + @property + def daemon(self): + return self._proc.daemon + + @daemon.setter + def daemon(self, daemonic): + self._proc.daemon = daemonic + + @property + def authkey(self): + return self._proc._authkey + + @authkey.setter + def authkey(self, authkey): + self._proc.authkey = AuthenticationString(authkey) + + @property + def exitcode(self): + return self._proc.exitcode + + @property + def ident(self): + return self._proc.ident + + @property + def pid(self): + return self._proc.pid + + def __repr__(self): + return self._proc.__repr__() + +# Monkeypatch in our process replacement. +if multiprocessing.Process != ProcessShim: + multiprocessing.Process = ProcessShim diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/parsed_trace_events.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/parsed_trace_events.py new file mode 100644 index 00000000000..fdc7514542d --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/parsed_trace_events.py @@ -0,0 +1,98 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import math +import json + + +class ParsedTraceEvents(object): + def __init__(self, events = None, trace_filename = None): + """ + Utility class for filtering and manipulating trace data. + + events -- An iterable object containing trace events + trace_filename -- A file object that contains a complete trace. + + """ + if trace_filename and events: + raise Exception("Provide either a trace file or event list") + if not trace_filename and events == None: + raise Exception("Provide either a trace file or event list") + + if trace_filename: + f = open(trace_filename, 'r') + t = f.read() + f.close() + + # If the event data begins with a [, then we know it should end with a ]. + # The reason we check for this is because some tracing implementations + # cannot guarantee that a ']' gets written to the trace file. So, we are + # forgiving and if this is obviously the case, we fix it up before + # throwing the string at JSON.parse. + if t[0] == '[': + n = len(t); + if t[n - 1] != ']' and t[n - 1] != '\n': + t = t + ']' + elif t[n - 2] != ']' and t[n - 1] == '\n': + t = t + ']' + elif t[n - 3] != ']' and t[n - 2] == '\r' and t[n - 1] == '\n': + t = t + ']' + + try: + events = json.loads(t) + except ValueError: + raise Exception("Corrupt trace, did not parse. Value: %s" % t) + + if 'traceEvents' in events: + events = events['traceEvents'] + + if not hasattr(events, '__iter__'): + raise Exception, 'events must be iteraable.' + self.events = events + self.pids = None + self.tids = None + + def __len__(self): + return len(self.events) + + def __getitem__(self, i): + return self.events[i] + + def __setitem__(self, i, v): + self.events[i] = v + + def __repr__(self): + return "[%s]" % ",\n ".join([repr(e) for e in self.events]) + + def findProcessIds(self): + if self.pids: + return self.pids + pids = set() + for e in self.events: + if "pid" in e and e["pid"]: + pids.add(e["pid"]) + self.pids = list(pids) + return self.pids + + def findThreadIds(self): + if self.tids: + return self.tids + tids = set() + for e in self.events: + if "tid" in e and e["tid"]: + tids.add(e["tid"]) + self.tids = list(tids) + return self.tids + + def findEventsOnProcess(self, pid): + return ParsedTraceEvents([e for e in self.events if e["pid"] == pid]) + + def findEventsOnThread(self, tid): + return ParsedTraceEvents( + [e for e in self.events if e["ph"] != "M" and e["tid"] == tid]) + + def findByPhase(self, ph): + return ParsedTraceEvents([e for e in self.events if e["ph"] == ph]) + + def findByName(self, n): + return ParsedTraceEvents([e for e in self.events if e["name"] == n]) diff --git a/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py new file mode 100644 index 00000000000..7047e0eae86 --- /dev/null +++ b/chromium/third_party/catapult/common/py_trace_event/py_trace_event/trace_event_impl/trace_test.py @@ -0,0 +1,48 @@ +# Copyright 2016 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +import tempfile +import unittest + +#from .log import * +#from .parsed_trace_events import * + +from log import * +from parsed_trace_events import * + +class TraceTest(unittest.TestCase): + def __init__(self, *args): + """ + Infrastructure for running tests of the tracing system. + + Does not actually run any tests. Look at subclasses for those. + """ + unittest.TestCase.__init__(self, *args) + self._file = None + + def go(self, cb): + """ + Enables tracing, runs the provided callback, and if successful, returns a + TraceEvents object with the results. + """ + self._file = tempfile.NamedTemporaryFile() + trace_enable(open(self._file.name, 'a+')) + + try: + cb() + finally: + trace_disable() + e = ParsedTraceEvents(trace_filename = self._file.name) + self._file.close() + self._file = None + return e + + @property + def trace_filename(self): + return self._file.name + + def tearDown(self): + if trace_is_enabled(): + trace_disable() + if self._file: + self._file.close() |