Source code for score.perf._init

# Copyright © 2016 STRG.AT GmbH, Vienna, Austria
#
# This file is part of the The SCORE Framework.
#
# The SCORE Framework and all its parts are free software: you can redistribute
# them and/or modify them under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation which is in the
# file named COPYING.LESSER.txt.
#
# The SCORE Framework and all its parts are distributed without any WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. For more details see the GNU Lesser General Public
# License.
#
# If you have not received a copy of the GNU Lesser General Public License see
# http://www.gnu.org/licenses/.
#
# The License-Agreement realised between you as Licensee and STRG.AT GmbH as
# Licenser including the issue of its valid conclusion and its pre- and
# post-contractual effects is governed by the laws of Austria. Any disputes
# concerning this License-Agreement including the issue of its valid conclusion
# and its pre- and post-contractual effects are exclusively decided by the
# competent court, in whose district STRG.AT GmbH has its registered seat, at
# the discretion of STRG.AT GmbH also the competent court, in whose district the
# Licensee has his registered seat, an establishment or assets.

import collections
from datetime import datetime
import os
import time
import sys
import subprocess
import threading
from score.init import ConfiguredModule, ConfigurationError, parse_time_interval
from score.serve import SimpleWorker


defaults = {
    'sample_interval': '100ms',
    'output_interval': '5s',
    'output.folder': '.',
    'output.file': None,
}


[docs]def init(confdict): """ Initializes this module according to :ref:`our module initialization guidelines <module_initialization>` with the following configuration keys: :confkey:`sample_interval` :confdefault:`100ms` Time duration to wait between measurements. :confkey:`output_interval` :confdefault:`5s` Interval in which to update resulting graph. :confkey:`output.folder` :confdefault:`.` Folder to write results to. Will create a file with this process's start time and PID as file name. :confkey:`output.file` This may be provided instead of ``output.folder``, in which case the output will be written to this specific file. """ conf = dict(defaults.items()) conf.update(confdict) sample_interval = parse_time_interval(conf['sample_interval']) output_interval = parse_time_interval(conf['output_interval']) if conf['output.file']: file = conf['output.file'] if not os.path.isdir(os.path.dirname(file)): import score.perf raise ConfigurationError( score.perf, 'Folder of configured `output.file` does not exist') else: if not os.path.isdir(conf['output.folder']): import score.perf raise ConfigurationError( score.perf, 'Configured `output.folder` does not exist') now = datetime.now() file = os.path.join( conf['output.folder'], '%s-%d.svg' % ( now.strftime('%Y-%m-%d %H:%M:%S'), os.getpid())) return ConfiguredPerfModule(sample_interval, output_interval, file)
class ConfiguredPerfModule(ConfiguredModule): """ This module's :class:`configuration object <score.init.ConfiguredModule>`. """ def __init__(self, sample_interval, output_interval, file): import score.perf super().__init__(score.perf) self.sample_interval = sample_interval self.output_interval = output_interval self._stack_counts = collections.defaultdict(int) self.file = file def score_serve_workers(self): return Worker(self) def _sample(self, frame=None): if frame is None: for thread_id, frame in sys._current_frames().items(): if thread_id == threading.current_thread().ident: continue top_frame = frame while top_frame.f_back: top_frame = top_frame.f_back if top_frame.f_globals.get('__name__') == '__main__': continue self._sample(frame) return stack = [] while frame is not None: formatted_frame = '{}({})'.format(frame.f_code.co_name, frame.f_globals.get('__name__')) stack.append(formatted_frame) frame = frame.f_back formatted_stack = ';'.join(reversed(stack)) self._stack_counts[formatted_stack] += 1 def update_graph(self): proc = subprocess.Popen( ['flamegraph.pl', '--hash', '--minwidth', '1'], universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: outs, errs = proc.communicate(self.flamegraph_string(), timeout=15) open(self.file, 'w').write(outs) except subprocess.TimeoutExpired: proc.kill() proc.communicate() raise def flamegraph_string(self): ordered_stacks = sorted(self._stack_counts.items(), key=lambda kv: kv[1], reverse=True) lines = ['{} {}\n'.format(frame, count) for frame, count in ordered_stacks] return ''.join(lines) class Worker(SimpleWorker): def __init__(self, conf): super().__init__() self.conf = conf def loop(self): last_output = time.time() while self.running: time.sleep(self.conf.sample_interval) self.conf._sample() if time.time() - last_output >= self.conf.output_interval: self.conf.update_graph() last_output = time.time() self.conf.update_graph()