Source code for ducky.profiler

"""
This module provides support for profiling the virtual machine (`machine`
profilers) and running programs (`code` profilers). Wrappers for python's
deterministic profilers, and simple statistical profiler of running code
are available for usage thoughout the ducky sources.

``cProfile`` is the prefered choice of machine profiling backend, with
``profile`` is used as a backup option.

Beware, each thread needs its own profiler instance. These instances can be
acquired from ProfilerStore class which handles saving their data when virtual
machine exits.

To simplify the usage of profilers in ducky sources in case when user does not
need profiling, I chose to provide classes with the same API but these classes
does not capture any data. These are called `dummy` profilers, as opposed to
the `real` ones. Both kinds mimic API of :py:class:`profile.Profile` - the
`real` machine profiler **is** :py:class:`profile.Profile` object.
"""

import collections
import os
import os.path

from six.moves import cPickle as pickle

try:
  from cProfile import Profile as RealMachineProfiler

except ImportError:
  from profile import Profile as RealMachineProfiler

[docs]class DummyCPUCoreProfiler(object): """ Dummy code profiler class. Base class for all code profilers. :param ducky.cpu.CPUCore core: core this profiler captures data from. :param int frequency: sampling frequency, given as an instruction count. """ def __init__(self, core, frequency = 17): super(DummyCPUCoreProfiler, self).__init__() self.core = core self.frequency = frequency self.enabled = False
[docs] def enable(self): """ Enable collection of profiling data. """ self.enabled = True
[docs] def disable(self): """ Disable collection of profiling data. """ self.enabled = False
[docs] def take_sample(self): """ Take a sample of current state of CPU core, and store any necessary data. """ pass
[docs] def create_stats(self): """ Preprocess collected data before they can be printed, searched or saved. """ pass
[docs] def dump_stats(self, filename): """ Save collected data into file. :param string filename: path to file. """ pass
[docs]class ProfileRecord(object): def __init__(self): self.ip = None self.instruction_set_id = None self.count = 0
[docs] def merge(self, other): assert self.ip is None or self.ip == other.ip self.ip = other.ip self.instruction_set_id = other.instruction_set_id self.count += other.count
[docs]class RealCPUCoreProfiler(DummyCPUCoreProfiler): """ Real code profiler. This class actually does collect data. """ def __init__(self, core): super(RealCPUCoreProfiler, self).__init__(core) self.data = []
[docs] def take_sample(self): if self.enabled is not True: return if self.core.registers.cnt.value % self.frequency != 0: return self.data.append((self.core.instruction_set.instruction_set_id, self.core.current_ip))
[docs] def dump_stats(self, filename): processed = collections.defaultdict(ProfileRecord) for inst_set, ip in self.data: processed[ip].ip = ip processed[ip].instruction_set_id = inst_set processed[ip].count += 1 with open(filename, 'wb') as f: pickle.dump(processed, f)
[docs]class DummyMachineProfiler(object): """ Dummy machine profiler. Does absolutely nothing. """ def __init__(self, *args, **kwargs): super(DummyMachineProfiler, self).__init__()
[docs] def enable(self): """ Start collecting profiling data. """ pass
[docs] def disable(self): """ Stop collecting profiling data. """ pass
[docs] def create_stats(self): """ Preprocess collected data before they can be printed, searched or saved. """ pass
[docs] def dump_stats(self, filename): """ Save collected data into file. :param string filename: path to file. """ pass
[docs]class ProfilerStore(object): """ This class manages profiler instances. When in need of a profiler (e.g. in a new thread) get one by calling proper method of ProfilerStore object. """ def __init__(self): super(ProfilerStore, self).__init__() self.machine_profiler_class = DummyMachineProfiler self.core_profiler_class = DummyCPUCoreProfiler self.profilers = []
[docs] def enable_machine(self): """ Each newly created virtual machine profiler will be the real one. """ self.machine_profiler_class = RealMachineProfiler
[docs] def enable_cpu(self): """ Each newly created code profiler will be the real one. """ self.core_profiler_class = RealCPUCoreProfiler
[docs] def is_machine_enabled(self): """ Returns ``True`` when virtual machine profiling is enabled. :rtype: bool """ return self.machine_profiler_class == RealMachineProfiler
[docs] def is_cpu_enabled(self): """ Returns ``True`` when code profiling is enabled. :rtype: bool """ return self.core_profiler_class == RealCPUCoreProfiler
[docs] def get_machine_profiler(self): """ Create and return new machine profiler. :returns: new machine profiler. """ p = self.machine_profiler_class(builtins = False) self.profilers.append(p) return p
[docs] def get_core_profiler(self, core): """ Create new code profiler. :rtype: DummyCPUCoreProfiler """ p = self.core_profiler_class(core) self.profilers.append(p) return p
[docs] def save(self, logger, directory): """ Save all captured data to files. Each created profiler stores its data in separate file. :param string directory: directory where all files are stored. """ filename_pattern = os.path.join(directory, 'profiler-%s-%s-%i.dat') for index, profiler in enumerate(self.profilers): profiler.disable() profiler.create_stats() filename = filename_pattern % (os.getpid(), profiler.__class__.__name__, index) profiler.dump_stats(filename) logger.info('Profiling data: %s', filename)
#: Main profiler store STORE = ProfilerStore()