├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── profiling
├── __about__.py
├── __init__.py
├── __main__.py
├── profiler.py
├── remote
│ ├── __init__.py
│ ├── asyncio.py
│ ├── background.py
│ ├── client.py
│ ├── gevent.py
│ └── select.py
├── sampling
│ ├── __init__.py
│ └── samplers.py
├── sortkeys.py
├── speedup.c
├── stats.py
├── tracing
│ ├── __init__.py
│ └── timers.py
├── utils.py
└── viewer.py
├── requirements.txt
├── screenshots
├── sampling.png
└── tracing.png
├── setup.cfg
├── setup.py
├── test
├── _utils.py
├── fit_requirements.py
├── requirements.txt
├── test_cli.py
├── test_profiler.py
├── test_sampling.py
├── test_stats.py
├── test_timers.py
├── test_tracing.py
├── test_utils.py
└── test_viewer.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 |
21 | # Installer logs
22 | pip-log.txt
23 |
24 | # Unit test / coverage reports
25 | .coverage
26 | .tox
27 | nosetests.xml
28 |
29 | # Translations
30 | *.mo
31 |
32 | # Mr Developer
33 | .mr.developer.cfg
34 | .project
35 | .pydevproject
36 |
37 | # Vim
38 | .*.sw[pqonm]
39 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 | python:
4 | - 2.7
5 | - 3.3
6 | - 3.4
7 | - 3.5
8 | - 3.6
9 | - pypy
10 | install:
11 | - pip install -e .
12 | - pip install -U `python test/fit_requirements.py test/requirements.txt`
13 | - pip install flake8 flake8-import-order pytest-cov coveralls
14 | script:
15 | - | # flake8
16 | if python -c 'import sys; sys.version_info < (3, 3) or sys.exit(1)'
17 | then
18 | EXCLUDE=profiling/remote/asyncio.py
19 | fi
20 | flake8 profiling test setup.py -v --show-source --exclude=$EXCLUDE
21 | - | # pytest
22 | py.test -v --cov=profiling --cov-report=term-missing
23 | after_success:
24 | - coveralls
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 0.1.1
5 | -----
6 |
7 | Released on Christmas 2015.
8 |
9 | The first public release. Finally you can install `profiling` via PyPI:
10 |
11 | ```sh
12 | $ pip install profiling
13 | ```
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014-2017, What! Studio
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | Redistributions in binary form must reproduce the above copyright notice, this
11 | list of conditions and the following disclaimer in the documentation and/or
12 | other materials provided with the distribution.
13 |
14 | Neither the name of the copyright holder nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE test/test_*.py requirements.txt test/requirements.txt
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ☠ This project is not maintained anymore. We highly recommend switching to
2 | [py-spy](https://github.com/benfred/py-spy) which provides better performance
3 | and usability.
4 |
5 | ---
6 |
7 | Profiling
8 | =========
9 |
10 | The profiling package is an interactive continuous Python profiler. It is
11 | inspired from [Unity 3D] profiler. This package provides these features:
12 |
13 | - Profiling statistics keep the frame stack.
14 | - An interactive TUI profiling statistics viewer.
15 | - Provides both of statistical and deterministic profiling.
16 | - Utilities for remote profiling.
17 | - Thread or greenlet aware CPU timer.
18 | - Supports Python 2.7, 3.3, 3.4 and 3.5.
19 | - Currently supports only Linux.
20 |
21 | [](https://travis-ci.org/what-studio/profiling)
22 | [](https://coveralls.io/r/what-studio/profiling)
23 |
24 | [Unity 3D]: http://unity3d.com/
25 |
26 | Installation
27 | ------------
28 |
29 | Install the latest release via PyPI:
30 |
31 | ```sh
32 | $ pip install profiling
33 | ```
34 |
35 | Profiling
36 | ---------
37 |
38 | To profile a single program, simply run the `profiling` command:
39 |
40 | ```sh
41 | $ profiling your-program.py
42 | ```
43 |
44 | Then an interactive viewer will be executed:
45 |
46 | 
47 |
48 | If your program uses greenlets, choose `greenlet` timer:
49 |
50 | ```sh
51 | $ profiling --timer=greenlet your-program.py
52 | ```
53 |
54 | With `--dump` option, it saves the profiling result to a file. You can
55 | browse the saved result by using the `view` subcommand:
56 |
57 | ```sh
58 | $ profiling --dump=your-program.prf your-program.py
59 | $ profiling view your-program.prf
60 | ```
61 |
62 | If your script reads ``sys.argv``, append your arguments after ``--``.
63 | It isolates your arguments from the ``profiling`` command:
64 |
65 | ```sh
66 | $ profiling your-program.py -- --your-flag --your-param=42
67 | ```
68 |
69 | Live Profiling
70 | --------------
71 |
72 | If your program has a long life time like a web server, a profiling result
73 | at the end of program is not helpful enough. Probably you need a continuous
74 | profiler. It can be achived by the `live-profile` subcommand:
75 |
76 | ```sh
77 | $ profiling live-profile webserver.py
78 | ```
79 |
80 | See a demo:
81 |
82 | [](https://asciinema.org/a/25394)
83 |
84 | There's a live-profiling server also. The server doesn't profile the
85 | program at ordinary times. But when a client connects to the server, it
86 | starts to profile and reports the results to the all connected clients.
87 |
88 | Start a profling server by the `remote-profile` subcommand:
89 |
90 | ```sh
91 | $ profiling remote-profile webserver.py --bind 127.0.0.1:8912
92 | ```
93 |
94 | And also run a client for the server by the `view` subcommand:
95 |
96 | ```sh
97 | $ profiling view 127.0.0.1:8912
98 | ```
99 |
100 | Statistical Profiling
101 | ---------------------
102 |
103 | `TracingProfiler`, the default profiler, implements a deterministic profiler
104 | for deep call graph. Of course, it has heavy overhead. The overhead can
105 | pollute your profiling result or can make your application to be slow.
106 |
107 | In contrast, `SamplingProfiler` implements a statistical profiler. Like other
108 | statistical profilers, it also has only very cheap overhead. When you profile
109 | you can choose it by just `--sampling` (shortly `-S`) option:
110 |
111 | ```sh
112 | $ profiling live-profile -S webserver.py
113 | ^^
114 | ```
115 |
116 | 
117 |
118 | Timeit then Profiling
119 | ---------------------
120 |
121 | Do you use `timeit` to check the performance of your code?
122 |
123 | ```sh
124 | $ python -m timeit -s 'from trueskill import *' 'rate_1vs1(Rating(), Rating())'
125 | 1000 loops, best of 3: 722 usec per loop
126 | ```
127 |
128 | If you want to profile the checked code, simply use the `timeit` subcommand:
129 |
130 | ```sh
131 | $ profiling timeit -s 'from trueskill import *' 'rate_1vs1(Rating(), Rating())'
132 | ^^^^^^^^^
133 | ```
134 |
135 | Profiling from Code
136 | -------------------
137 |
138 | You can also profile your program by ``profiling.tracing.TracingProfiler`` or
139 | ``profiling.sampling.SamplingProfiler`` directly:
140 |
141 | ```python
142 | from profiling.tracing import TracingProfiler
143 |
144 | # profile your program.
145 | profiler = TracingProfiler()
146 | profiler.start()
147 | ... # run your program.
148 | profiler.stop()
149 |
150 | # or using context manager.
151 | with profiler:
152 | ... # run your program.
153 |
154 | # view and interact with the result.
155 | profiler.run_viewer()
156 | # or save profile data to file
157 | profiler.dump('path/to/file')
158 | ```
159 |
160 | Viewer Key Bindings
161 | -------------------
162 |
163 | - q - Quit.
164 | - space - Pause/Resume.
165 | - \\ - Toggle layout between NESTED and FLAT.
166 | - ↑ and ↓ - Navigate frames.
167 | - → - Expand the frame.
168 | - ← - Fold the frame.
169 | - > - Go to the hotspot.
170 | - esc - Defocus.
171 | - [ and ] - Change sorting column.
172 |
173 | Columns
174 | -------
175 |
176 | ### Common
177 |
178 | - `FUNCTION`
179 | 1. The function name with the code location.
180 | (e.g. `my_func (my_code.py:42)`, `my_func (my_module:42)`)
181 | 1. Only the location without line number. (e.g. `my_code.py`, `my_module`)
182 |
183 | ### Tracing Profiler
184 |
185 | - `CALLS` - Total call count of the function.
186 | - `OWN` (Exclusive Time) - Total spent time in the function excluding sub
187 | calls.
188 | - `/CALL` after `OWN` - Exclusive time per call.
189 | - `%` after `OWN` - Exclusive time per total spent time.
190 | - `DEEP` (Inclusive Time) - Total spent time in the function.
191 | - `/CALL` after `DEEP` - Inclusive time per call.
192 | - `%` after `DEEP` - Inclusive time per total spent time.
193 |
194 | ### Sampling Profiler
195 |
196 | - `OWN` (Exclusive Samples) - Number of samples which are collected during the
197 | direct execution of the function.
198 | - `%` after `OWN` - Exclusive samples per number of the total samples.
199 | - `DEEP` (Inclusive Samples) - Number of samples which are collected during the
200 | excution of the function.
201 | - `%` after `DEEP` - Inclusive samples per number of the total samples.
202 |
203 | Testing
204 | -------
205 |
206 | There are some additional requirements to run the test code, which can be
207 | installed by running the following command.
208 |
209 | ```sh
210 | $ pip install $(python test/fit_requirements.py test/requirements.txt)
211 | ```
212 |
213 | Then you should be able to run `pytest`.
214 |
215 | ```sh
216 | $ pytest -v
217 | ```
218 |
219 | Thanks to
220 | ---------
221 |
222 | - [Seungmyeong Yang](https://github.com/sequoiayang)
223 | who suggested this project.
224 | - [Pavel](https://github.com/htch)
225 | who inspired to implement ``-m`` option.
226 |
227 | Licensing
228 | ---------
229 |
230 | Written by [Heungsub Lee] at [What! Studio] in [Nexon], and
231 | distributed under the [BSD 3-Clause] license.
232 |
233 | [Heungsub Lee]: http://subl.ee/
234 | [What! Studio]: https://github.com/what-studio
235 | [Nexon]: http://nexon.com/
236 | [BSD 3-Clause]: http://opensource.org/licenses/BSD-3-Clause
237 |
--------------------------------------------------------------------------------
/profiling/__about__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.__about__
4 | ~~~~~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | __version__ = '0.1.3'
11 | __license__ = 'BSD'
12 | __author__ = 'What! Studio'
13 | __maintainer__ = 'Heungsub Lee'
14 | __maintainer_email__ = 'sub@nexon.co.kr'
15 | __url__ = 'https://github.com/what-studio/profiling'
16 | __description__ = 'An interactive continuous Python profiler.'
17 |
--------------------------------------------------------------------------------
/profiling/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling
4 | ~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 | from profiling.__about__ import __version__ # noqa
13 | from profiling.profiler import Profiler
14 |
15 |
16 | __all__ = ['Profiler']
17 |
--------------------------------------------------------------------------------
/profiling/__main__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.__main__
4 | ~~~~~~~~~~~~~~~~~~
5 |
6 | The command-line interface to profile a script or view profiling results.
7 |
8 | .. sourcecode:: console
9 |
10 | $ profiling --help
11 |
12 | :copyright: (c) 2014-2017, What! Studio
13 | :license: BSD, see LICENSE for more details.
14 |
15 | """
16 | from __future__ import absolute_import
17 |
18 | from datetime import datetime
19 | from functools import partial, wraps
20 | import importlib
21 | import os
22 | try:
23 | import cPickle as pickle
24 | except ImportError:
25 | import pickle
26 | import runpy
27 | import signal
28 | import socket
29 | from stat import S_ISREG, S_ISSOCK
30 | import sys
31 | import threading
32 | import time
33 | import traceback
34 |
35 | import click
36 | from click_default_group import DefaultGroup
37 | from six import exec_
38 | from six.moves import builtins
39 | from six.moves.configparser import ConfigParser, NoOptionError, NoSectionError
40 |
41 | from profiling import remote, sampling, tracing
42 | from profiling.__about__ import __version__
43 | from profiling.profiler import Profiler
44 | from profiling.remote.background import BackgroundProfiler
45 | from profiling.remote.client import FailoverProfilingClient, ProfilingClient
46 | from profiling.remote.select import SelectProfilingServer
47 | from profiling.sampling import samplers, SamplingProfiler
48 | from profiling.tracing import timers, TracingProfiler
49 | from profiling.viewer import bind_game_keys, bind_vim_keys, StatisticsViewer
50 |
51 |
52 | __all__ = ['cli', 'profile', 'view']
53 |
54 |
55 | DEFAULT_ENDPOINT = '127.0.0.1:8912'
56 |
57 |
58 | class ProfilingCLI(DefaultGroup):
59 |
60 | def __init__(self, *args, **kwargs):
61 | super(ProfilingCLI, self).__init__(*args, **kwargs)
62 | self.command_name_aliases = {}
63 |
64 | def command(self, *args, **kwargs):
65 | """Usage::
66 |
67 | @cli.command(aliases=['ci'])
68 | def commit():
69 | ...
70 |
71 | """
72 | aliases = kwargs.pop('aliases', None)
73 | decorator = super(ProfilingCLI, self).command(*args, **kwargs)
74 | if aliases is None:
75 | return decorator
76 | def _decorator(f):
77 | cmd = decorator(f)
78 | for alias in aliases:
79 | self.command_name_aliases[alias] = cmd.name
80 | return cmd
81 | return _decorator
82 |
83 | def get_command(self, ctx, cmd_name):
84 | # Resolve alias.
85 | try:
86 | cmd_name = self.command_name_aliases[cmd_name]
87 | except KeyError:
88 | pass
89 | return super(ProfilingCLI, self).get_command(ctx, cmd_name)
90 |
91 |
92 | @click.command('profiling', cls=ProfilingCLI, default='profile')
93 | @click.version_option(__version__)
94 | def cli():
95 | sys.path.insert(0, os.curdir)
96 | bind_vim_keys()
97 | bind_game_keys()
98 |
99 |
100 | class read_config(object):
101 | """Reads a config once in a Click context."""
102 |
103 | filenames = ['setup.cfg', '.profiling']
104 | ctx_and_config = (None, None)
105 |
106 | def __new__(cls):
107 | ctx, config = cls.ctx_and_config
108 | current_ctx = click.get_current_context(silent=True)
109 | if current_ctx != ctx:
110 | config = ConfigParser()
111 | config.read(cls.filenames)
112 | cls.ctx_and_config = (current_ctx, config)
113 | return config
114 |
115 |
116 | def option_getter(type):
117 | """Gets an unbound method to get a configuration option as the given type.
118 | """
119 | option_getters = {None: ConfigParser.get,
120 | int: ConfigParser.getint,
121 | float: ConfigParser.getfloat,
122 | bool: ConfigParser.getboolean}
123 | return option_getters.get(type, option_getters[None])
124 |
125 |
126 | def config_default(option, default=None, type=None, section=cli.name):
127 | """Guesses a default value of a CLI option from the configuration.
128 |
129 | ::
130 |
131 | @click.option('--locale', default=config_default('locale'))
132 |
133 | """
134 | def f(option=option, default=default, type=type, section=section):
135 | config = read_config()
136 | if type is None and default is not None:
137 | # detect type from default.
138 | type = builtins.type(default)
139 | get_option = option_getter(type)
140 | try:
141 | return get_option(config, section, option)
142 | except (NoOptionError, NoSectionError):
143 | return default
144 | return f
145 |
146 |
147 | def config_flag(option, value, default=False, section=cli.name):
148 | """Guesses whether a CLI flag should be turned on or off from the
149 | configuration. If the configuration option value is same with the given
150 | value, it returns ``True``.
151 |
152 | ::
153 |
154 | @click.option('--ko-kr', 'locale', is_flag=True,
155 | default=config_flag('locale', 'ko_KR'))
156 |
157 | """
158 | class x(object):
159 | def __bool__(self, option=option, value=value,
160 | default=default, section=section):
161 | config = read_config()
162 | type = builtins.type(value)
163 | get_option = option_getter(type)
164 | try:
165 | return get_option(config, section, option) == value
166 | except (NoOptionError, NoSectionError):
167 | return default
168 | __nonzero__ = __bool__
169 | return x()
170 |
171 |
172 | def get_title(src_name, src_type=None):
173 | """Normalizes a source name as a string to be used for viewer's title."""
174 | if src_type == 'tcp':
175 | return '{0}:{1}'.format(*src_name)
176 | return os.path.basename(src_name)
177 |
178 |
179 | def make_viewer(mono=False, *loop_args, **loop_kwargs):
180 | """Makes a :class:`profiling.viewer.StatisticsViewer` with common options.
181 | """
182 | viewer = StatisticsViewer()
183 | loop = viewer.loop(*loop_args, **loop_kwargs)
184 | if mono:
185 | loop.screen.set_terminal_properties(1)
186 | return (viewer, loop)
187 |
188 |
189 | def spawn_thread(func, *args, **kwargs):
190 | """Spawns a daemon thread."""
191 | thread = threading.Thread(target=func, args=args, kwargs=kwargs)
192 | thread.daemon = True
193 | thread.start()
194 | return thread
195 |
196 |
197 | def spawn(mode, func, *args, **kwargs):
198 | """Spawns a thread-like object which runs the given function concurrently.
199 |
200 | Available modes:
201 |
202 | - `threading`
203 | - `greenlet`
204 | - `eventlet`
205 |
206 | """
207 | if mode is None:
208 | # 'threading' is the default mode.
209 | mode = 'threading'
210 | elif mode not in spawn.modes:
211 | # validate the given mode.
212 | raise ValueError('Invalid spawn mode: %s' % mode)
213 | if mode == 'threading':
214 | return spawn_thread(func, *args, **kwargs)
215 | elif mode == 'gevent':
216 | import gevent
217 | import gevent.monkey
218 | gevent.monkey.patch_select()
219 | gevent.monkey.patch_socket()
220 | return gevent.spawn(func, *args, **kwargs)
221 | elif mode == 'eventlet':
222 | import eventlet
223 | eventlet.patcher.monkey_patch(select=True, socket=True)
224 | return eventlet.spawn(func, *args, **kwargs)
225 | assert False
226 |
227 |
228 | spawn.modes = ['threading', 'gevent', 'eventlet']
229 |
230 |
231 | #: Just returns the first argument.
232 | noop = lambda x: x
233 |
234 |
235 | def import_(module_name, name):
236 | """Imports an object by a relative module path::
237 |
238 | Profiler = import_('profiling.profiler', 'Profiler')
239 |
240 | """
241 | module = importlib.import_module(module_name, __package__)
242 | return getattr(module, name)
243 |
244 |
245 | #: Makes a function which import an object by :func:`import_` lazily.
246 | importer = lambda module_name, name: partial(import_, module_name, name)
247 |
248 |
249 | # custom parameter types
250 |
251 |
252 | class Class(click.ParamType):
253 |
254 | def __init__(self, modules, base, base_name=None, postfix=True):
255 | self.modules = modules
256 | self.base = base
257 | self.base_name = base_name
258 | self.postfix = postfix
259 |
260 | def convert(self, value, param, ctx):
261 | if value == self.base_name:
262 | return self.base
263 | name = value.title()
264 | if self.postfix:
265 | name += self.base.__name__.title()
266 | for mod in self.modules:
267 | try:
268 | cls = getattr(mod, name)
269 | except AttributeError:
270 | continue
271 | if not isinstance(cls, type):
272 | continue
273 | elif not issubclass(cls, self.base):
274 | continue
275 | return cls
276 | self.fail('%s not found' % name)
277 |
278 | def get_metavar(self, param):
279 | return self.base.__name__.upper()
280 |
281 |
282 | class Script(click.File):
283 | """A parameter type for Python script."""
284 |
285 | def __init__(self):
286 | super(Script, self).__init__('rb')
287 |
288 | def convert(self, value, param, ctx):
289 | with super(Script, self).convert(value, param, ctx) as f:
290 | filename = f.name
291 | code = compile(f.read(), filename, 'exec')
292 | globals_ = {'__file__': filename, '__name__': '__main__',
293 | '__package__': None, '__doc__': None}
294 | return (filename, code, globals_)
295 |
296 | def get_metavar(self, param):
297 | return 'PYTHON'
298 |
299 |
300 | class Module(click.ParamType):
301 |
302 | def convert(self, value, param, ctx):
303 | # inspired by @htch's fork.
304 | # https://github.com/htch/profiling/commit/4a4eb6e
305 | try:
306 | detail = runpy._get_module_details(value)
307 | except ImportError as exc:
308 | ctx.fail(str(exc))
309 | try:
310 | # since Python 3.4.
311 | mod_name, mod_spec, code = detail
312 | except ValueError:
313 | mod_name, loader, code, filename = detail
314 | else:
315 | loader = mod_spec.loader
316 | filename = mod_spec.origin
317 | # follow runpy's behavior.
318 | pkg_name = mod_name.rpartition('.')[0]
319 | globals_ = sys.modules['__main__'].__dict__.copy()
320 | globals_.update(__name__='__main__', __file__=filename,
321 | __loader__=loader, __package__=pkg_name)
322 | return (filename, code, globals_)
323 |
324 | def get_metavar(self, param):
325 | return 'PYTHON-MODULE'
326 |
327 |
328 | class Command(click.ParamType):
329 |
330 | def convert(self, value, param, ctx):
331 | filename = ''
332 | code = compile(value, filename, 'exec')
333 | globals_ = {'__name__': '__main__',
334 | '__package__': None, '__doc__': None}
335 | return (filename, code, globals_)
336 |
337 | def get_metavar(self, param):
338 | return 'PYTHON-COMMAND'
339 |
340 |
341 | class Endpoint(click.ParamType):
342 | """A parameter type for IP endpoint."""
343 |
344 | def convert(self, value, param, ctx):
345 | host, port = value.split(':')
346 | port = int(port)
347 | return (host, port)
348 |
349 | def get_metavar(self, param):
350 | return 'HOST:PORT'
351 |
352 |
353 | class ViewerSource(click.ParamType):
354 | """A parameter type for :class:`profiling.viewer.StatisticsViewer` source.
355 | """
356 |
357 | def convert(self, value, param, ctx):
358 | src_type = False
359 | try:
360 | mode = os.stat(value).st_mode
361 | except OSError:
362 | try:
363 | src_name = Endpoint().convert(value, param, ctx)
364 | except ValueError:
365 | pass
366 | else:
367 | src_type = 'tcp'
368 | else:
369 | src_name = value
370 | if S_ISSOCK(mode):
371 | src_type = 'sock'
372 | elif S_ISREG(mode):
373 | src_type = 'dump'
374 | if not src_type:
375 | raise ValueError('Dump file or socket address required.')
376 | return (src_type, src_name)
377 |
378 | def get_metavar(self, param):
379 | return 'SOURCE'
380 |
381 |
382 | class SignalNumber(click.ParamType):
383 | """A parameter type for signal number."""
384 |
385 | @staticmethod
386 | def name_of(signum):
387 | for name, value in signal.__dict__.items():
388 | if signum == value:
389 | if name.startswith('SIG') and not name.startswith('SIG_'):
390 | return name
391 | return str(signum)
392 |
393 | def convert(self, value, param, ctx):
394 | if isinstance(value, int):
395 | return value
396 | elif value.isdigit():
397 | return int(value)
398 | signame = value.upper()
399 | if not signame.startswith('SIG'):
400 | signame = 'SIG' + signame
401 | if signame.startswith('SIG_'):
402 | self.fail('Invalid signal %s' % signame)
403 | try:
404 | signum = getattr(signal, signame)
405 | except AttributeError:
406 | self.fail('Unknown signal %s' % signame)
407 | return signum
408 |
409 | def get_metavar(self, param):
410 | return 'SIGNUM'
411 |
412 |
413 | # common parameters
414 |
415 |
416 | class Params(object):
417 |
418 | def __init__(self, params):
419 | self.params = params
420 |
421 | def __call__(self, f):
422 | for param in self.params[::-1]:
423 | f = param(f)
424 | return f
425 |
426 | def __add__(self, params):
427 | return self.__class__(self.params + params)
428 |
429 |
430 | def profiler_options(f):
431 | # tracing profiler options
432 | @click.option(
433 | '-T', '--tracing', 'import_profiler_class',
434 | flag_value=importer('profiling.tracing', 'TracingProfiler'),
435 | default=config_flag('profiler', 'tracing', True),
436 | help='Use tracing profiler. (default)')
437 | @click.option(
438 | '--timer', 'timer_class',
439 | type=Class([timers], timers.Timer, 'basic'),
440 | default=config_default('timer', 'basic'),
441 | help='Choose CPU timer for tracing profiler. (basic|thread|greenlet)')
442 | # sampling profiler options
443 | @click.option(
444 | '-S', '--sampling', 'import_profiler_class',
445 | flag_value=importer('profiling.sampling', 'SamplingProfiler'),
446 | default=config_flag('profiler', 'sampling', False),
447 | help='Use sampling profiler.')
448 | @click.option(
449 | '--sampler', 'sampler_class',
450 | type=Class([samplers], samplers.Sampler),
451 | default=config_default('sampler', 'itimer'),
452 | help='Choose frames sampler for sampling profiler. (itimer|tracing)')
453 | @click.option(
454 | '--sampling-interval', type=float,
455 | default=config_default('sampling-interval', samplers.INTERVAL),
456 | help='How often sample. (default: %.3f cpu sec)' % samplers.INTERVAL)
457 | # etc
458 | @click.option(
459 | '--pickle-protocol', type=int,
460 | default=config_default('pickle-protocol', remote.PICKLE_PROTOCOL),
461 | help='Pickle protocol to dump result.')
462 | @wraps(f)
463 | def wrapped(import_profiler_class, timer_class, sampler_class,
464 | sampling_interval, **kwargs):
465 | profiler_class = import_profiler_class()
466 | assert issubclass(profiler_class, Profiler)
467 | if issubclass(profiler_class, TracingProfiler):
468 | # profiler requires timer.
469 | timer_class = timer_class or tracing.TIMER_CLASS
470 | timer = timer_class()
471 | profiler_kwargs = {'timer': timer}
472 | elif issubclass(profiler_class, SamplingProfiler):
473 | sampler_class = sampler_class or sampling.SAMPLER_CLASS
474 | sampler = sampler_class(sampling_interval)
475 | profiler_kwargs = {'sampler': sampler}
476 | else:
477 | profiler_kwargs = {}
478 | profiler_factory = partial(profiler_class, **profiler_kwargs)
479 | return f(profiler_factory=profiler_factory, **kwargs)
480 | return wrapped
481 |
482 |
483 | def profiler_arguments(f):
484 | @click.argument('argv', nargs=-1)
485 | @click.option('-m', 'module', type=Module(),
486 | help='Run library module as a script.')
487 | @click.option('-c', 'command', type=Command(),
488 | help='Program passed in as string.')
489 | @wraps(f)
490 | def wrapped(argv, module, command, **kwargs):
491 | if module is not None and command is not None:
492 | raise click.UsageError('Option -m and -c are exclusive')
493 | script = module or command
494 | if script is None:
495 | # -m and -c not passed.
496 | try:
497 | script_filename, argv = argv[0], argv[1:]
498 | except IndexError:
499 | raise click.UsageError('Script not specified')
500 | script = Script().convert(script_filename, None, None)
501 | kwargs.update(script=script, argv=argv)
502 | return f(**kwargs)
503 | return wrapped
504 |
505 |
506 | viewer_options = Params([
507 | click.option('--mono', is_flag=True, help='Disable coloring.'),
508 | ])
509 | onetime_profiler_options = Params([
510 | click.option(
511 | '-d', '--dump', 'dump_filename', type=click.Path(writable=True),
512 | help='Profiling result dump filename.'),
513 | ])
514 | live_profiler_options = Params([
515 | click.option(
516 | '-i', '--interval', type=float,
517 | default=config_default('interval', remote.INTERVAL),
518 | help='How often update result. (default: %.0f sec)' % remote.INTERVAL),
519 | click.option(
520 | '--spawn', type=click.Choice(spawn.modes),
521 | default=config_default('spawn'),
522 | callback=lambda c, p, v: partial(spawn, v),
523 | help='How to spawn profiler server in background.'),
524 | click.option(
525 | '--signum', type=SignalNumber(),
526 | default=config_default('signum', BackgroundProfiler.signum),
527 | help=(
528 | 'For communication between server and application. (default: %s)' %
529 | SignalNumber.name_of(BackgroundProfiler.signum)
530 | ))
531 | ])
532 |
533 |
534 | # sub-commands
535 |
536 |
537 | def __profile__(filename, code, globals_, profiler_factory,
538 | pickle_protocol=remote.PICKLE_PROTOCOL, dump_filename=None,
539 | mono=False):
540 | frame = sys._getframe()
541 | profiler = profiler_factory(base_frame=frame, base_code=code)
542 | profiler.start()
543 | try:
544 | exec_(code, globals_)
545 | except BaseException:
546 | # don't profile print_exc().
547 | profiler.stop()
548 | traceback.print_exc()
549 | else:
550 | profiler.stop()
551 | # discard this __profile__ function from the result.
552 | profiler.stats.discard_child(frame.f_code)
553 | if dump_filename is None:
554 | try:
555 | profiler.run_viewer(get_title(filename), mono=mono)
556 | except KeyboardInterrupt:
557 | pass
558 | else:
559 | profiler.dump(dump_filename, pickle_protocol)
560 |
561 | click.echo('To view statistics:')
562 | click.echo(' $ profiling view ', nl=False)
563 | click.secho(dump_filename, underline=True)
564 |
565 |
566 | class ProfilingCommand(click.Command):
567 |
568 | def collect_usage_pieces(self, ctx):
569 | """Prepend "[--]" before "[ARGV]..."."""
570 | pieces = super(ProfilingCommand, self).collect_usage_pieces(ctx)
571 | assert pieces[-1] == '[ARGV]...'
572 | pieces.insert(-1, 'SCRIPT')
573 | pieces.insert(-1, '[--]')
574 | return pieces
575 |
576 |
577 | @cli.command(cls=ProfilingCommand)
578 | @profiler_arguments
579 | @profiler_options
580 | @onetime_profiler_options
581 | @viewer_options
582 | def profile(script, argv, profiler_factory,
583 | pickle_protocol, dump_filename, mono):
584 | """Profile a Python script."""
585 | filename, code, globals_ = script
586 | sys.argv[:] = [filename] + list(argv)
587 | __profile__(filename, code, globals_, profiler_factory,
588 | pickle_protocol=pickle_protocol, dump_filename=dump_filename,
589 | mono=mono)
590 |
591 |
592 | @cli.command('live-profile', aliases=['live'], cls=ProfilingCommand)
593 | @profiler_arguments
594 | @profiler_options
595 | @live_profiler_options
596 | @viewer_options
597 | def live_profile(script, argv, profiler_factory, interval, spawn, signum,
598 | pickle_protocol, mono):
599 | """Profile a Python script continuously."""
600 | filename, code, globals_ = script
601 | sys.argv[:] = [filename] + list(argv)
602 | parent_sock, child_sock = socket.socketpair()
603 | stderr_r_fd, stderr_w_fd = os.pipe()
604 | pid = os.fork()
605 | if pid:
606 | # parent
607 | os.close(stderr_w_fd)
608 | viewer, loop = make_viewer(mono)
609 | # loop.screen._term_output_file = open(os.devnull, 'w')
610 | title = get_title(filename)
611 | client = ProfilingClient(viewer, loop.event_loop, parent_sock, title)
612 | client.start()
613 | try:
614 | loop.run()
615 | except KeyboardInterrupt:
616 | os.kill(pid, signal.SIGINT)
617 | except BaseException:
618 | # unexpected profiler error.
619 | os.kill(pid, signal.SIGTERM)
620 | raise
621 | finally:
622 | parent_sock.close()
623 | # get exit code of child.
624 | w_pid, status = os.waitpid(pid, os.WNOHANG)
625 | if w_pid == 0:
626 | os.kill(pid, signal.SIGTERM)
627 | exit_code = os.WEXITSTATUS(status)
628 | # print stderr of child.
629 | with os.fdopen(stderr_r_fd, 'r') as f:
630 | child_stderr = f.read()
631 | if child_stderr:
632 | sys.stdout.flush()
633 | sys.stderr.write(child_stderr)
634 | # exit with exit code of child.
635 | sys.exit(exit_code)
636 | else:
637 | # child
638 | os.close(stderr_r_fd)
639 | # mute stdin, stdout.
640 | devnull = os.open(os.devnull, os.O_RDWR)
641 | for f in [sys.stdin, sys.stdout]:
642 | os.dup2(devnull, f.fileno())
643 | # redirect stderr to parent.
644 | os.dup2(stderr_w_fd, sys.stderr.fileno())
645 | frame = sys._getframe()
646 | profiler = profiler_factory(base_frame=frame, base_code=code)
647 | profiler_trigger = BackgroundProfiler(profiler, signum)
648 | profiler_trigger.prepare()
649 | server_args = (interval, noop, pickle_protocol)
650 | server = SelectProfilingServer(None, profiler_trigger, *server_args)
651 | server.clients.add(child_sock)
652 | spawn(server.connected, child_sock)
653 | try:
654 | exec_(code, globals_)
655 | finally:
656 | os.close(stderr_w_fd)
657 | child_sock.shutdown(socket.SHUT_WR)
658 |
659 |
660 | @cli.command('remote-profile', aliases=['remote'], cls=ProfilingCommand)
661 | @profiler_arguments
662 | @profiler_options
663 | @live_profiler_options
664 | @click.option('-b', '--bind', 'endpoint', type=Endpoint(),
665 | default=config_default('endpoint', DEFAULT_ENDPOINT),
666 | help='IP endpoint to serve profiling results.')
667 | @click.option('-v', '--verbose', is_flag=True,
668 | help='Print profiling server logs.')
669 | def remote_profile(script, argv, profiler_factory, interval, spawn, signum,
670 | pickle_protocol, endpoint, verbose):
671 | """Launch a server to profile continuously. The default endpoint is
672 | 127.0.0.1:8912.
673 | """
674 | filename, code, globals_ = script
675 | sys.argv[:] = [filename] + list(argv)
676 | # create listener.
677 | listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
678 | listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
679 | listener.bind(endpoint)
680 | listener.listen(1)
681 | # be verbose or quiet.
682 | if verbose:
683 | log = lambda x: click.echo(click.style('> ', fg='cyan') + x)
684 | bound_addr = listener.getsockname()
685 | log('Listening on {0}:{1} for profiling...'.format(*bound_addr))
686 | else:
687 | log = noop
688 | # start profiling server.
689 | frame = sys._getframe()
690 | profiler = profiler_factory(base_frame=frame, base_code=code)
691 | profiler_trigger = BackgroundProfiler(profiler, signum)
692 | profiler_trigger.prepare()
693 | server_args = (interval, log, pickle_protocol)
694 | server = SelectProfilingServer(listener, profiler_trigger, *server_args)
695 | spawn(server.serve_forever)
696 | # exec the script.
697 | try:
698 | exec_(code, globals_)
699 | except KeyboardInterrupt:
700 | pass
701 |
702 |
703 | @cli.command()
704 | @click.argument('src', type=ViewerSource(),
705 | default=config_default('endpoint', DEFAULT_ENDPOINT))
706 | @viewer_options
707 | def view(src, mono):
708 | """Inspect statistics by TUI view."""
709 | src_type, src_name = src
710 | title = get_title(src_name, src_type)
711 | viewer, loop = make_viewer(mono)
712 | if src_type == 'dump':
713 | time = datetime.fromtimestamp(os.path.getmtime(src_name))
714 | with open(src_name, 'rb') as f:
715 | profiler_class, (stats, cpu_time, wall_time) = pickle.load(f)
716 | viewer.set_profiler_class(profiler_class)
717 | viewer.set_result(stats, cpu_time, wall_time, title=title, at=time)
718 | viewer.activate()
719 | elif src_type in ('tcp', 'sock'):
720 | family = {'tcp': socket.AF_INET, 'sock': socket.AF_UNIX}[src_type]
721 | client = FailoverProfilingClient(viewer, loop.event_loop,
722 | src_name, family, title=title)
723 | client.start()
724 | try:
725 | loop.run()
726 | except KeyboardInterrupt:
727 | pass
728 |
729 |
730 | @cli.command('timeit-profile', aliases=['timeit'])
731 | @click.argument('stmt', metavar='STATEMENT', default='pass')
732 | @click.option('-n', '--number', type=int,
733 | help='How many times to execute the statement.')
734 | @click.option('-r', '--repeat', type=int, default=3,
735 | help='How many times to repeat the timer.')
736 | @click.option('-s', '--setup', default='pass',
737 | help='Statement to be executed once initially.')
738 | @click.option('-t', '--time', help='Ignored.')
739 | @click.option('-c', '--clock', help='Ignored.')
740 | @click.option('-v', '--verbose', help='Ignored.')
741 | @profiler_options
742 | @onetime_profiler_options
743 | @viewer_options
744 | def timeit_profile(stmt, number, repeat, setup,
745 | profiler_factory, pickle_protocol, dump_filename, mono,
746 | **_ignored):
747 | """Profile a Python statement like timeit."""
748 | del _ignored
749 | globals_ = {}
750 | exec_(setup, globals_)
751 | if number is None:
752 | # determine number so that 0.2 <= total time < 2.0 like timeit.
753 | dummy_profiler = profiler_factory()
754 | dummy_profiler.start()
755 | for x in range(1, 10):
756 | number = 10 ** x
757 | t = time.time()
758 | for y in range(number):
759 | exec_(stmt, globals_)
760 | if time.time() - t >= 0.2:
761 | break
762 | dummy_profiler.stop()
763 | del dummy_profiler
764 | code = compile('for _ in range(%d): %s' % (number, stmt),
765 | 'STATEMENT', 'exec')
766 | __profile__(stmt, code, globals_, profiler_factory,
767 | pickle_protocol=pickle_protocol, dump_filename=dump_filename,
768 | mono=mono)
769 |
770 |
771 | # Deprecated.
772 | main = cli
773 |
774 |
775 | if __name__ == '__main__':
776 | cli(prog_name='python -m profiling')
777 |
--------------------------------------------------------------------------------
/profiling/profiler.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.profiler
4 | ~~~~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 | try:
13 | import cPickle as pickle
14 | except ImportError:
15 | import pickle
16 | import time
17 |
18 | from profiling.stats import RecordingStatistics
19 | from profiling.utils import frame_stack, Runnable
20 | from profiling.viewer import StatisticsTable, StatisticsViewer
21 |
22 |
23 | __all__ = ['Profiler', 'ProfilerWrapper']
24 |
25 |
26 | class Profiler(Runnable):
27 | """The base class for profiler."""
28 |
29 | #: A widget class which extends :class:`profiling.viewer.StatisticsTable`.
30 | table_class = StatisticsTable
31 |
32 | #: The root recording statistics.
33 | stats = None
34 |
35 | base_frame = None
36 | base_code = None
37 | ignored_frames = ()
38 | ignored_codes = ()
39 |
40 | def __init__(self, base_frame=None, base_code=None,
41 | ignored_frames=(), ignored_codes=()):
42 | self.base_frame = base_frame
43 | self.base_code = base_code
44 | self.ignored_frames = ignored_frames
45 | self.ignored_codes = ignored_codes
46 | self.stats = RecordingStatistics()
47 |
48 | def start(self):
49 | self._cpu_time_started = time.clock()
50 | self._wall_time_started = time.time()
51 | self.stats.clear()
52 | return super(Profiler, self).start()
53 |
54 | def frame_stack(self, frame):
55 | return frame_stack(frame, self.base_frame, self.base_code,
56 | self.ignored_frames, self.ignored_codes)
57 |
58 | def exclude_code(self, code):
59 | """Excludes statistics of the given code."""
60 | try:
61 | self.stats.remove_child(code)
62 | except KeyError:
63 | pass
64 |
65 | def result(self):
66 | """Gets the frozen statistics to serialize by Pickle."""
67 | try:
68 | cpu_time = max(0, time.clock() - self._cpu_time_started)
69 | wall_time = max(0, time.time() - self._wall_time_started)
70 | except AttributeError:
71 | cpu_time = wall_time = 0.0
72 | return self.stats, cpu_time, wall_time
73 |
74 | def dump(self, dump_filename, pickle_protocol=pickle.HIGHEST_PROTOCOL):
75 | """Saves the profiling result to a file
76 |
77 | :param dump_filename: path to a file
78 | :type dump_filename: str
79 |
80 | :param pickle_protocol: version of pickle protocol
81 | :type pickle_protocol: int
82 | """
83 | result = self.result()
84 |
85 | with open(dump_filename, 'wb') as f:
86 | pickle.dump((self.__class__, result), f, pickle_protocol)
87 |
88 | def make_viewer(self, title=None, at=None):
89 | """Makes a statistics viewer from the profiling result.
90 | """
91 | viewer = StatisticsViewer()
92 | viewer.set_profiler_class(self.__class__)
93 | stats, cpu_time, wall_time = self.result()
94 | viewer.set_result(stats, cpu_time, wall_time, title=title, at=at)
95 | viewer.activate()
96 | return viewer
97 |
98 | def run_viewer(self, title=None, at=None, mono=False,
99 | *loop_args, **loop_kwargs):
100 | """A shorter form of:
101 |
102 | ::
103 |
104 | viewer = profiler.make_viewer()
105 | loop = viewer.loop()
106 | loop.run()
107 |
108 | """
109 | viewer = self.make_viewer(title, at=at)
110 | loop = viewer.loop(*loop_args, **loop_kwargs)
111 | if mono:
112 | loop.screen.set_terminal_properties(1)
113 | loop.run()
114 |
115 |
116 | class ProfilerWrapper(Profiler):
117 |
118 | for attr in ['table_class', 'stats', 'top_frame', 'top_code', 'result',
119 | 'is_running']:
120 | f = lambda self, attr=attr: getattr(self.profiler, attr)
121 | locals()[attr] = property(f)
122 | del f
123 |
124 | def __init__(self, profiler):
125 | self.profiler = profiler
126 |
--------------------------------------------------------------------------------
/profiling/remote/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.remote
4 | ~~~~~~~~~~~~~~~~
5 |
6 | Utilities for remote profiling. They help you to implement profiling
7 | server and client.
8 |
9 | :copyright: (c) 2014-2017, What! Studio
10 | :license: BSD, see LICENSE for more details.
11 |
12 | """
13 | from __future__ import absolute_import
14 |
15 | from errno import EBADF, ECONNRESET, EPIPE
16 | import functools
17 | import io
18 | from logging import getLogger as get_logger
19 | try:
20 | import cPickle as pickle
21 | except ImportError:
22 | import pickle
23 | import socket
24 | import struct
25 |
26 | from profiling.__about__ import __version__
27 |
28 |
29 | __all__ = ['LOGGER', 'LOG', 'INTERVAL', 'PICKLE_PROTOCOL',
30 | 'SIZE_STRUCT_FORMAT', 'pack_result', 'recv_msg', 'fmt_connected',
31 | 'fmt_disconnected', 'fmt_profiler_started', 'fmt_profiler_stopped',
32 | 'ProfilingServer']
33 |
34 |
35 | #: The standard logger.
36 | LOGGER = get_logger('Profiling')
37 |
38 | #: The standard log function.
39 | LOG = LOGGER.debug
40 |
41 | #: The default profiling interval.
42 | INTERVAL = 1
43 |
44 | #: The default Pickle protocol.
45 | PICKLE_PROTOCOL = getattr(pickle, 'DEFAULT_PROTOCOL', pickle.HIGHEST_PROTOCOL)
46 |
47 | #: The struct format to pack message size. (uint32)
48 | SIZE_STRUCT_FORMAT = '!I'
49 |
50 | #: The struct format to pack method. (uint8)
51 | METHOD_STRUCT_FORMAT = '!B'
52 |
53 |
54 | # methods
55 | WELCOME = 0x10
56 | PROFILER = 0x11
57 | RESULT = 0x12
58 |
59 |
60 | def pack_msg(method, msg, pickle_protocol=PICKLE_PROTOCOL):
61 | """Packs a method and message."""
62 | dump = io.BytesIO()
63 | pickle.dump(msg, dump, pickle_protocol)
64 | size = dump.tell()
65 | return (struct.pack(METHOD_STRUCT_FORMAT, method) +
66 | struct.pack(SIZE_STRUCT_FORMAT, size) + dump.getvalue())
67 |
68 |
69 | def recv(sock, size):
70 | """Receives exactly `size` bytes. This function blocks the thread."""
71 | data = sock.recv(size, socket.MSG_WAITALL)
72 | if len(data) < size:
73 | raise socket.error(ECONNRESET, 'Connection closed')
74 | return data
75 |
76 |
77 | def recv_msg(sock):
78 | """Receives a method and message from the socket. This function blocks the
79 | current thread.
80 | """
81 | data = recv(sock, struct.calcsize(METHOD_STRUCT_FORMAT))
82 | method, = struct.unpack(METHOD_STRUCT_FORMAT, data)
83 | data = recv(sock, struct.calcsize(SIZE_STRUCT_FORMAT))
84 | size, = struct.unpack(SIZE_STRUCT_FORMAT, data)
85 | data = recv(sock, size)
86 | msg = pickle.loads(data)
87 | return method, msg
88 |
89 |
90 | def fmt_connected(addr, num_clients):
91 | if addr:
92 | fmt = 'Connected from {0[0]}:{0[1]} (total: {1})'
93 | else:
94 | fmt = 'A client connected (total: {1})'
95 | return fmt.format(addr, num_clients)
96 |
97 |
98 | def fmt_disconnected(addr, num_clients):
99 | if addr:
100 | fmt = 'Disconnected from {0[0]}:{0[1]} (total: {1})'
101 | else:
102 | fmt = 'A client disconnected (total: {1})'
103 | return fmt.format(addr, num_clients)
104 |
105 |
106 | def fmt_profiler_started(interval):
107 | return 'Profiling every {0} seconds...'.format(interval)
108 |
109 |
110 | def fmt_profiler_stopped():
111 | return 'Profiler stopped'
112 |
113 |
114 | def abstract(message):
115 | def decorator(f):
116 | @functools.wraps(f)
117 | def wrapped(*args, **kwargs):
118 | raise NotImplementedError(message)
119 | return wrapped
120 | return decorator
121 |
122 |
123 | class ProfilingServer(object):
124 | """The base class for profiling server implementations. Implement abstract
125 | methods and call :meth:`connected` when a client connected.
126 | """
127 |
128 | _latest_result_data = None
129 |
130 | def __init__(self, profiler, interval=INTERVAL,
131 | log=LOG, pickle_protocol=PICKLE_PROTOCOL):
132 | self.profiler = profiler
133 | self.interval = interval
134 | self.log = log
135 | self.pickle_protocol = pickle_protocol
136 | self.clients = set()
137 |
138 | @abstract('Implement serve_forever() to run a server synchronously.')
139 | def serve_forever(self):
140 | pass
141 |
142 | @abstract('Implement _send() to send data to the client.')
143 | def _send(self, client, data):
144 | pass
145 |
146 | @abstract('Implement _close() to close the client.')
147 | def _close(self, client):
148 | pass
149 |
150 | @abstract('Implement _addr() to get the address from the client.')
151 | def _addr(self, client):
152 | pass
153 |
154 | @abstract('Implement _start_profiling() to start a profiling loop.')
155 | def _start_profiling(self):
156 | pass
157 |
158 | @abstract('Implement _start_watching() to add a disconnection callback to '
159 | 'the client')
160 | def _start_watching(self, client):
161 | pass
162 |
163 | def profiling(self):
164 | """A generator which profiles then broadcasts the result. Implement
165 | sleeping loop using this::
166 |
167 | def profile_periodically(self):
168 | for __ in self.profiling():
169 | time.sleep(self.interval)
170 |
171 | """
172 | self._log_profiler_started()
173 | while self.clients:
174 | try:
175 | self.profiler.start()
176 | except RuntimeError:
177 | pass
178 | # should sleep.
179 | yield
180 | self.profiler.stop()
181 | result = self.profiler.result()
182 | data = pack_msg(RESULT, result,
183 | pickle_protocol=self.pickle_protocol)
184 | self._latest_result_data = data
185 | # broadcast.
186 | closed_clients = []
187 | for client in self.clients:
188 | try:
189 | self._send(client, data)
190 | except socket.error as exc:
191 | if exc.errno == EPIPE:
192 | closed_clients.append(client)
193 | del data
194 | # handle disconnections.
195 | for client in closed_clients:
196 | self.disconnected(client)
197 | self._log_profiler_stopped()
198 |
199 | def send_msg(self, client, method, msg, pickle_protocol=None):
200 | if pickle_protocol is None:
201 | pickle_protocol = self.pickle_protocol
202 | data = pack_msg(method, msg, pickle_protocol=pickle_protocol)
203 | self._send(client, data)
204 |
205 | def connected(self, client):
206 | """Call this method when a client connected."""
207 | self.clients.add(client)
208 | self._log_connected(client)
209 | self._start_watching(client)
210 | self.send_msg(client, WELCOME, (self.pickle_protocol, __version__),
211 | pickle_protocol=0)
212 | profiler = self.profiler
213 | while True:
214 | try:
215 | profiler = profiler.profiler
216 | except AttributeError:
217 | break
218 | self.send_msg(client, PROFILER, type(profiler))
219 | if self._latest_result_data is not None:
220 | try:
221 | self._send(client, self._latest_result_data)
222 | except socket.error as exc:
223 | if exc.errno in (EBADF, EPIPE):
224 | self.disconnected(client)
225 | return
226 | raise
227 | if len(self.clients) == 1:
228 | self._start_profiling()
229 |
230 | def disconnected(self, client):
231 | """Call this method when a client disconnected."""
232 | if client not in self.clients:
233 | # already disconnected.
234 | return
235 | self.clients.remove(client)
236 | self._log_disconnected(client)
237 | self._close(client)
238 |
239 | def _log_connected(self, client):
240 | addr = self._addr(client)
241 | addr = addr if isinstance(addr, tuple) else None
242 | self.log(fmt_connected(addr, len(self.clients)))
243 |
244 | def _log_disconnected(self, client):
245 | addr = self._addr(client)
246 | addr = addr if isinstance(addr, tuple) else None
247 | self.log(fmt_disconnected(addr, len(self.clients)))
248 |
249 | def _log_profiler_started(self):
250 | self.log(fmt_profiler_started(self.interval))
251 |
252 | def _log_profiler_stopped(self):
253 | self.log(fmt_profiler_stopped())
254 |
--------------------------------------------------------------------------------
/profiling/remote/asyncio.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.remote.async
4 | ~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | Implements a profiling server based on `asyncio`_. Only for Python 3.4 or
7 | later.
8 |
9 | .. _asyncio: https://docs.python.org/3/library/asyncio.html
10 |
11 | :copyright: (c) 2014-2017, What! Studio
12 | :license: BSD, see LICENSE for more details.
13 |
14 | """
15 | from __future__ import absolute_import
16 |
17 | import asyncio
18 |
19 | from profiling.remote import ProfilingServer
20 |
21 |
22 | __all__ = ['AsyncIOProfilingServer']
23 |
24 |
25 | class AsyncIOProfilingServer(ProfilingServer):
26 | """A profiling server implementation based on `asyncio`_. Launch a server
27 | by ``asyncio.start_server`` or ``asyncio.start_unix_server``::
28 |
29 | server = AsyncIOProfilingServer(interval=10)
30 | ready = asyncio.start_server(server)
31 | loop = asyncio.get_event_loop()
32 | loop.run_until_complete(ready)
33 | loop.run_forever()
34 |
35 | .. _asyncio: https://docs.python.org/3/library/asyncio.html
36 |
37 | """
38 |
39 | def __init__(self, *args, **kwargs):
40 | super(AsyncIOProfilingServer, self).__init__(*args, **kwargs)
41 | self.clients = set()
42 |
43 | def serve_forever(self, addr):
44 | host, port = addr
45 | loop = asyncio.get_event_loop()
46 | ready_to_serve = asyncio.start_server(self, host, port)
47 | loop.run_until_complete(ready_to_serve)
48 | loop.run_forever()
49 |
50 | def _send(self, client, data):
51 | reader, writer = client
52 | writer.write(data)
53 |
54 | def _close(self, client):
55 | reader, writer = client
56 | writer.close()
57 |
58 | def _addr(self, client):
59 | reader, writer = client
60 | return writer.get_extra_info('peername')
61 |
62 | def _start_profiling(self):
63 | asyncio.async(self.profile_periodically())
64 |
65 | def _start_watching(self, client):
66 | reader, writer = client
67 | disconnected = lambda x: self.disconnected(reader, writer)
68 | asyncio.async(reader.read()).add_done_callback(disconnected)
69 |
70 | @asyncio.coroutine
71 | def profile_periodically(self):
72 | for __ in self.profiling():
73 | yield from asyncio.sleep(self.interval)
74 |
75 | def __call__(self, reader, writer):
76 | client = (reader, writer)
77 | self.connected(client)
78 |
--------------------------------------------------------------------------------
/profiling/remote/background.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.remote.background
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | Utilities to run a profiler in a background thread.
7 |
8 | :copyright: (c) 2014-2017, What! Studio
9 | :license: BSD, see LICENSE for more details.
10 |
11 | """
12 | from __future__ import absolute_import
13 |
14 | import os
15 | import signal
16 | import threading
17 |
18 | from profiling.profiler import ProfilerWrapper
19 |
20 |
21 | __all__ = ['BackgroundProfiler']
22 |
23 |
24 | class BackgroundProfiler(ProfilerWrapper):
25 |
26 | signum = signal.SIGUSR2
27 |
28 | def __init__(self, profiler, signum=None):
29 | super(BackgroundProfiler, self).__init__(profiler)
30 | if signum is not None:
31 | self.signum = signum
32 | self.event = threading.Event()
33 |
34 | def prepare(self):
35 | """Registers :meth:`_signal_handler` as a signal handler to start
36 | and/or stop the profiler from the background thread. So this function
37 | must be called at the main thread.
38 | """
39 | return signal.signal(self.signum, self._signal_handler)
40 |
41 | def run(self):
42 | self._send_signal()
43 | yield
44 | self._send_signal()
45 |
46 | def _send_signal(self):
47 | self.event.clear()
48 | os.kill(os.getpid(), self.signum)
49 | self.event.wait()
50 |
51 | def _signal_handler(self, signum, frame):
52 | if self.profiler.is_running():
53 | self.profiler.stop()
54 | else:
55 | self.profiler.start()
56 | self.event.set()
57 |
--------------------------------------------------------------------------------
/profiling/remote/client.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.remote.client
4 | ~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 | from datetime import datetime
13 | from errno import ECONNREFUSED, EINPROGRESS, ENOENT
14 | import socket
15 |
16 | from valuedispatch import valuedispatch
17 |
18 | from profiling.remote import PROFILER, recv_msg, RESULT, WELCOME
19 |
20 |
21 | __all__ = ['ProfilingClient', 'FailoverProfilingClient']
22 |
23 |
24 | @valuedispatch
25 | def protocol(method, msg, client):
26 | pass
27 |
28 |
29 | @protocol.register(WELCOME)
30 | def handle_welcome(_, __, client):
31 | client.viewer.activate()
32 |
33 |
34 | @protocol.register(PROFILER)
35 | def handle_profiler(_, profiler_class, client):
36 | client.viewer.set_profiler_class(profiler_class)
37 |
38 |
39 | @protocol.register(RESULT)
40 | def handle_result(_, result, client):
41 | stats, cpu_time, wall_time = result
42 | client.viewer.set_result(stats, cpu_time, wall_time,
43 | client.title, datetime.now())
44 |
45 |
46 | class ProfilingClient(object):
47 | """A client of profiling server which is running behind the `Urwid`_ event
48 | loop.
49 |
50 | .. _Urwid: http://urwid.org/
51 |
52 | """
53 |
54 | def __init__(self, viewer, event_loop, sock,
55 | title=None, protocol=protocol):
56 | self.viewer = viewer
57 | self.event_loop = event_loop
58 | self.sock = sock
59 | self.title = title
60 | self.protocol = protocol
61 |
62 | def start(self):
63 | self.event_loop.watch_file(self.sock.fileno(), self.handle)
64 |
65 | def handle(self):
66 | try:
67 | method, msg = recv_msg(self.sock)
68 | except socket.error as exc:
69 | self.erred(exc.errno)
70 | return
71 | self.protocol(method, msg, self)
72 |
73 | def erred(self, errno):
74 | self.event_loop.remove_watch_file(self.sock.fileno())
75 | self.viewer.inactivate()
76 |
77 |
78 | class FailoverProfilingClient(ProfilingClient):
79 | """A profiling client but it tries to reconnect constantly."""
80 |
81 | failover_interval = 1
82 |
83 | def __init__(self, viewer, event_loop, addr=None, family=socket.AF_INET,
84 | title=None, protocol=protocol):
85 | self.addr = addr
86 | self.family = family
87 | base = super(FailoverProfilingClient, self)
88 | base.__init__(viewer, event_loop, None, title, protocol)
89 |
90 | def connect(self):
91 | while True:
92 | errno = self.sock.connect_ex(self.addr)
93 | if not errno:
94 | # connected immediately.
95 | break
96 | elif errno == EINPROGRESS:
97 | # will be connected.
98 | break
99 | elif errno == ENOENT:
100 | # no such socket file.
101 | self.create_connection(self.failover_interval)
102 | return
103 | else:
104 | raise ValueError('Unexpected socket errno: %d' % errno)
105 | self.event_loop.watch_file(self.sock.fileno(), self.handle)
106 |
107 | def disconnect(self, errno):
108 | self.sock.close()
109 | # try to reconnect.
110 | delay = self.failover_interval if errno == ECONNREFUSED else 0
111 | self.create_connection(delay)
112 |
113 | def create_connection(self, delay=0):
114 | self.sock = socket.socket(self.family)
115 | self.sock.setblocking(0)
116 | self.event_loop.alarm(delay, self.connect)
117 |
118 | def start(self):
119 | self.create_connection()
120 |
121 | def erred(self, errno):
122 | super(FailoverProfilingClient, self).erred(errno)
123 | self.disconnect(errno)
124 |
--------------------------------------------------------------------------------
/profiling/remote/gevent.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.remote.gevent
4 | ~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | Implements a profiling server based on `gevent`_.
7 |
8 | .. _gevent: http://gevent.org/
9 |
10 | :copyright: (c) 2014-2017, What! Studio
11 | :license: BSD, see LICENSE for more details.
12 |
13 | """
14 | from __future__ import absolute_import
15 |
16 | import socket
17 |
18 | import gevent
19 | from gevent.lock import Semaphore
20 | from gevent.server import StreamServer
21 | from gevent.util import wrap_errors
22 |
23 | from profiling.remote import INTERVAL, LOG, PICKLE_PROTOCOL, ProfilingServer
24 |
25 |
26 | __all__ = ['GeventProfilingServer']
27 |
28 |
29 | class GeventProfilingServer(StreamServer, ProfilingServer):
30 | """A profiling server implementation based on `gevent`_. When you choose
31 | it, you should set a :class:`profiling.timers.greenlet.GreenletTimer` for
32 | the profiler's timer::
33 |
34 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
35 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
36 | sock.bind(('', 0))
37 | sock.listen(1)
38 |
39 | profiler = Profiler(GreenletTimer())
40 | server = GeventProfilingServer(sock, profiler)
41 | server.serve_forever()
42 |
43 | .. _gevent: http://gevent.org/
44 |
45 | """
46 |
47 | def __init__(self, listener, profiler=None, interval=INTERVAL,
48 | log=LOG, pickle_protocol=PICKLE_PROTOCOL, **server_kwargs):
49 | StreamServer.__init__(self, listener, **server_kwargs)
50 | ProfilingServer.__init__(self, profiler, interval,
51 | log, pickle_protocol)
52 | self.lock = Semaphore()
53 | self.profiling_greenlet = None
54 |
55 | def _send(self, sock, data):
56 | sock.sendall(data)
57 |
58 | def _close(self, sock):
59 | sock.close()
60 |
61 | def _addr(self, sock):
62 | return sock.getsockname()
63 |
64 | def _start_profiling(self):
65 | self.profiling_greenlet = gevent.spawn(self.profile_periodically)
66 |
67 | def _start_watching(self, sock):
68 | disconnected = lambda x: self.disconnected(sock)
69 | recv = wrap_errors(socket.error, sock.recv)
70 | gevent.spawn(recv, 1).link(disconnected)
71 |
72 | def profile_periodically(self):
73 | with self.lock:
74 | for __ in self.profiling():
75 | gevent.sleep(self.interval)
76 |
77 | def handle(self, sock, addr=None):
78 | self.connected(sock)
79 |
80 | # Disconnect once profiling has stopped.
81 | if self.profiling_greenlet:
82 | self.profiling_greenlet.join()
83 |
--------------------------------------------------------------------------------
/profiling/remote/select.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.remote.select
4 | ~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | Implements a profiling server based on `select`_.
7 |
8 | .. _select: https://docs.python.org/library/select.html
9 |
10 | :copyright: (c) 2014-2017, What! Studio
11 | :license: BSD, see LICENSE for more details.
12 |
13 | """
14 | from __future__ import absolute_import
15 |
16 | from errno import ECONNRESET, EINTR, ENOTCONN
17 | import select
18 | import socket
19 | import time
20 |
21 | from profiling.remote import ProfilingServer
22 |
23 |
24 | __all__ = ['SelectProfilingServer']
25 |
26 |
27 | class SelectProfilingServer(ProfilingServer):
28 |
29 | def __init__(self, listener, *args, **kwargs):
30 | super(SelectProfilingServer, self).__init__(*args, **kwargs)
31 | self.listener = listener
32 |
33 | def serve_forever(self):
34 | while True:
35 | self.dispatch_sockets()
36 |
37 | def _send(self, sock, data):
38 | sock.sendall(data, socket.MSG_DONTWAIT)
39 |
40 | def _close(self, sock):
41 | sock.close()
42 |
43 | def _addr(self, sock):
44 | try:
45 | return sock.getpeername()
46 | except socket.error as exc:
47 | if exc.errno == ENOTCONN:
48 | return None
49 | else:
50 | raise
51 |
52 | def _start_profiling(self):
53 | self.profile_periodically()
54 |
55 | def profile_periodically(self):
56 | for __ in self.profiling():
57 | self.dispatch_sockets(self.interval)
58 |
59 | def _start_watching(self, sock):
60 | pass
61 |
62 | def sockets(self):
63 | """Returns the set of the sockets."""
64 | if self.listener is None:
65 | return self.clients
66 | else:
67 | return self.clients.union([self.listener])
68 |
69 | def select_sockets(self, timeout=None):
70 | """EINTR safe version of `select`. It focuses on just incoming
71 | sockets.
72 | """
73 | if timeout is not None:
74 | t = time.time()
75 | while True:
76 | try:
77 | ready, __, __ = select.select(self.sockets(), (), (), timeout)
78 | except ValueError:
79 | # there's fd=0 socket.
80 | pass
81 | except select.error as exc:
82 | # ignore an interrupted system call.
83 | if exc.args[0] != EINTR:
84 | raise
85 | else:
86 | # succeeded.
87 | return ready
88 | # retry.
89 | if timeout is None:
90 | continue
91 | # decrease timeout.
92 | t2 = time.time()
93 | timeout -= t2 - t
94 | t = t2
95 | if timeout <= 0:
96 | # timed out.
97 | return []
98 |
99 | def dispatch_sockets(self, timeout=None):
100 | """Dispatches incoming sockets."""
101 | for sock in self.select_sockets(timeout=timeout):
102 | if sock is self.listener:
103 | listener = sock
104 | sock, addr = listener.accept()
105 | self.connected(sock)
106 | else:
107 | try:
108 | sock.recv(1)
109 | except socket.error as exc:
110 | if exc.errno != ECONNRESET:
111 | raise
112 | self.disconnected(sock)
113 |
--------------------------------------------------------------------------------
/profiling/sampling/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.sampling
4 | ~~~~~~~~~~~~~~~~~~
5 |
6 | Statistical profiling.
7 |
8 | :copyright: (c) 2014-2017, What! Studio
9 | :license: BSD, see LICENSE for more details.
10 |
11 | """
12 | from __future__ import absolute_import
13 |
14 | from profiling import sortkeys
15 | from profiling.profiler import Profiler
16 | from profiling.sampling.samplers import ItimerSampler, Sampler
17 | from profiling.stats import (
18 | RecordingStatistics, VoidRecordingStatistics as void)
19 | from profiling.viewer import fmt, StatisticsTable
20 |
21 |
22 | __all__ = ['SamplingProfiler', 'SamplingStatisticsTable']
23 |
24 |
25 | SAMPLER_CLASS = ItimerSampler
26 |
27 |
28 | class SamplingStatisticsTable(StatisticsTable):
29 |
30 | columns = [
31 | ('FUNCTION', 'left', ('weight', 1), sortkeys.by_function),
32 | ('OWN', 'right', (6,), sortkeys.by_own_hits),
33 | ('%', 'left', (4,), None),
34 | ('DEEP', 'right', (6,), sortkeys.by_deep_hits),
35 | ('%', 'left', (4,), None),
36 | ]
37 | order = sortkeys.by_deep_hits
38 |
39 | def make_cells(self, node, stats):
40 | root_stats = node.get_root().get_value()
41 | yield fmt.make_stat_text(stats)
42 | yield fmt.make_int_or_na_text(stats.own_hits)
43 | yield fmt.make_percent_text(stats.own_hits, root_stats.deep_hits)
44 | yield fmt.make_int_or_na_text(stats.deep_hits)
45 | yield fmt.make_percent_text(stats.deep_hits, root_stats.deep_hits)
46 |
47 |
48 | class SamplingProfiler(Profiler):
49 |
50 | table_class = SamplingStatisticsTable
51 |
52 | #: The frames sampler. Usually it is an instance of :class:`profiling.
53 | #: sampling.samplers.Sampler`.
54 | sampler = None
55 |
56 | def __init__(self, base_frame=None, base_code=None,
57 | ignored_frames=(), ignored_codes=(), sampler=None):
58 | sampler = sampler or SAMPLER_CLASS()
59 | if not isinstance(sampler, Sampler):
60 | raise TypeError('Not a sampler instance')
61 | base = super(SamplingProfiler, self)
62 | base.__init__(base_frame, base_code, ignored_frames, ignored_codes)
63 | self.sampler = sampler
64 |
65 | def sample(self, frame):
66 | """Samples the given frame."""
67 | frames = self.frame_stack(frame)
68 | if frames:
69 | frames.pop()
70 | parent_stats = self.stats
71 | for f in frames:
72 | parent_stats = parent_stats.ensure_child(f.f_code, void)
73 | stats = parent_stats.ensure_child(frame.f_code, RecordingStatistics)
74 | stats.own_hits += 1
75 |
76 | def run(self):
77 | self.sampler.start(self)
78 | yield
79 | self.sampler.stop()
80 |
--------------------------------------------------------------------------------
/profiling/sampling/samplers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.sampling.samplers
4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 | import functools
13 | import signal
14 | import sys
15 | import threading
16 | import weakref
17 |
18 | import six.moves._thread as _thread
19 |
20 | from profiling.utils import deferral, Runnable, thread_clock
21 |
22 |
23 | __all__ = ['Sampler', 'ItimerSampler', 'TracingSampler']
24 |
25 |
26 | INTERVAL = 1e-3 # 1ms
27 |
28 |
29 | class Sampler(Runnable):
30 | """The base class for samplers."""
31 |
32 | #: Sampling interval.
33 | interval = INTERVAL
34 |
35 | def __init__(self, interval=INTERVAL):
36 | self.interval = interval
37 |
38 |
39 | class ItimerSampler(Sampler):
40 | """Uses ``signal.ITIMER_PROF`` to sample running frames.
41 |
42 | .. note::
43 |
44 | ``signal.SIGPROF`` is triggeres by only the main thread. If you need
45 | sample multiple threads, use :class:`TracingSampler` instead.
46 |
47 | """
48 |
49 | def handle_signal(self, profiler, signum, frame):
50 | profiler.sample(frame)
51 |
52 | def run(self, profiler):
53 | weak_profiler = weakref.proxy(profiler)
54 | handle = functools.partial(self.handle_signal, weak_profiler)
55 | t = self.interval
56 | with deferral() as defer:
57 | prev_handle = signal.signal(signal.SIGPROF, handle)
58 | if prev_handle == signal.SIG_DFL:
59 | # sometimes the process receives SIGPROF although the sampler
60 | # unsets the itimer. If the previous handler was SIG_DFL, the
61 | # process will crash when received SIGPROF. To prevent this
62 | # risk, it makes the process to ignore SIGPROF when it isn't
63 | # running if the previous handler was SIG_DFL.
64 | prev_handle = signal.SIG_IGN
65 | defer(signal.signal, signal.SIGPROF, prev_handle)
66 | prev_itimer = signal.setitimer(signal.ITIMER_PROF, t, t)
67 | defer(signal.setitimer, signal.ITIMER_PROF, *prev_itimer)
68 | yield
69 |
70 |
71 | class TracingSampler(Sampler):
72 | """Uses :func:`sys.setprofile` and :func:`threading.setprofile` to sample
73 | running frames per thread. It can be used at systems which do not support
74 | profiling signals.
75 |
76 | Just like :class:`profiling.tracing.timers.ThreadTimer`, `Yappi`_ is
77 | required for earlier than Python 3.3.
78 |
79 | .. _Yappi: https://code.google.com/p/yappi/
80 |
81 | """
82 |
83 | def __init__(self, *args, **kwargs):
84 | super(TracingSampler, self).__init__(*args, **kwargs)
85 | self.sampled_times = {}
86 | self.counter = 0
87 |
88 | def _profile(self, profiler, frame, event, arg):
89 | t = thread_clock()
90 | thread_id = _thread.get_ident()
91 | sampled_at = self.sampled_times.get(thread_id, 0)
92 | if t - sampled_at < self.interval:
93 | return
94 | self.sampled_times[thread_id] = t
95 | profiler.sample(frame)
96 | self.counter += 1
97 | if self.counter % 10000 == 0:
98 | self._clear_for_dead_threads()
99 |
100 | def _clear_for_dead_threads(self):
101 | for thread_id in sys._current_frames().keys():
102 | self.sampled_times.pop(thread_id, None)
103 |
104 | def run(self, profiler):
105 | profile = functools.partial(self._profile, profiler)
106 | with deferral() as defer:
107 | sys.setprofile(profile)
108 | defer(sys.setprofile, None)
109 | threading.setprofile(profile)
110 | defer(threading.setprofile, None)
111 | yield
112 |
--------------------------------------------------------------------------------
/profiling/sortkeys.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.sortkeys
4 | ~~~~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 |
13 | __all__ = ['by_name', 'by_module', 'by_deep_hits', 'by_own_hits',
14 | 'by_deep_time', 'by_own_time', 'by_deep_time_per_call',
15 | 'by_own_time_per_call']
16 |
17 |
18 | class SortKey(object):
19 |
20 | def __init__(self, func):
21 | super(SortKey, self).__init__()
22 | self.func = func
23 |
24 | def __call__(self, stat):
25 | return self.func(stat)
26 |
27 | def __invert__(self):
28 | cls = type(self)
29 | return cls(lambda stat: -self.func(stat))
30 |
31 |
32 | #: Sorting by name in ascending order.
33 | by_name = SortKey(lambda stat: stat.name)
34 |
35 | #: Sorting by module in ascending order.
36 | by_module = SortKey(lambda stat: stat.module)
37 |
38 | #: Sorting by module and name in ascending order.
39 | by_function = SortKey(lambda stat: (stat.module, stat.name))
40 |
41 | #: Sorting by number of inclusive hits in descending order.
42 | by_deep_hits = SortKey(lambda stat: -stat.deep_hits)
43 |
44 | #: Sorting by number of exclusive hits in descending order.
45 | by_own_hits = SortKey(lambda stat: -stat.own_hits)
46 |
47 | #: Sorting by inclusive elapsed time in descending order.
48 | by_deep_time = SortKey(lambda stat: -stat.deep_time)
49 |
50 | #: Sorting by exclusive elapsed time in descending order.
51 | by_own_time = SortKey(lambda stat: (-stat.own_time, -stat.deep_time))
52 |
53 |
54 | @SortKey
55 | def by_deep_time_per_call(stat):
56 | """Sorting by inclusive elapsed time per call in descending order."""
57 | return -stat.deep_time_per_call if stat.deep_hits else -stat.deep_time
58 |
59 |
60 | @SortKey
61 | def by_own_time_per_call(stat):
62 | """Sorting by exclusive elapsed time per call in descending order."""
63 | return (-stat.own_time_per_call if stat.own_hits else -stat.own_time,
64 | by_deep_time_per_call(stat))
65 |
--------------------------------------------------------------------------------
/profiling/speedup.c:
--------------------------------------------------------------------------------
1 | #include "Python.h"
2 | #include "frameobject.h"
3 |
4 | #define PYOBJ PyObject*
5 |
6 | static PyObject *
7 | frame_stack(PyObject *self, PyObject *args)
8 | {
9 | // frame_stack(frame, base_frame, base_code, ignored_frames, ignored_codes)
10 | // returns a list of frames.
11 | PyFrameObject* frame;
12 | const PyFrameObject* base_frame;
13 | const PyCodeObject* base_code;
14 | const PySetObject* ignored_frames;
15 | const PySetObject* ignored_codes;
16 | if (!PyArg_ParseTuple(args, "OOOOO", &frame, &base_frame, &base_code,
17 | &ignored_frames, &ignored_codes))
18 | {
19 | return NULL;
20 | }
21 | PyObject* frame_stack = PyList_New(0);
22 | if (frame_stack == NULL)
23 | {
24 | return NULL;
25 | }
26 | while (frame != NULL)
27 | {
28 | if (frame == base_frame || frame->f_code == base_code)
29 | {
30 | break;
31 | }
32 | if (PySet_Contains((PYOBJ)ignored_frames, (PYOBJ)frame) == 0 &&
33 | PySet_Contains((PYOBJ)ignored_codes, (PYOBJ)frame->f_code) == 0)
34 | {
35 | // Not ignored.
36 | if (PyList_Append(frame_stack, (PyObject*)frame) == -1)
37 | {
38 | return NULL;
39 | }
40 | }
41 | frame = frame->f_back;
42 | }
43 | PyList_Reverse(frame_stack);
44 | return frame_stack;
45 | }
46 |
47 | static PyMethodDef SpeedupMethods[] = {
48 | {"frame_stack", frame_stack, METH_VARARGS, ""},
49 | {NULL, NULL, 0, NULL}
50 | };
51 |
52 | // https://docs.python.org/3/howto/cporting.html
53 | #if PY_MAJOR_VERSION >= 3
54 | static struct PyModuleDef SpeedupModule = {
55 | PyModuleDef_HEAD_INIT,
56 | "speedup",
57 | NULL,
58 | -1,
59 | SpeedupMethods
60 | };
61 | PyMODINIT_FUNC
62 | PyInit_speedup(void)
63 | {
64 | return PyModule_Create(&SpeedupModule);
65 | }
66 | #else
67 | PyMODINIT_FUNC
68 | initspeedup(void)
69 | {
70 | (void) Py_InitModule("speedup", SpeedupMethods);
71 | }
72 | #endif
73 |
--------------------------------------------------------------------------------
/profiling/stats.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.stats
4 | ~~~~~~~~~~~~~~~
5 |
6 | Statistics classes.
7 |
8 | :copyright: (c) 2014-2017, What! Studio
9 | :license: BSD, see LICENSE for more details.
10 |
11 | """
12 | from __future__ import absolute_import, division
13 |
14 | from collections import deque
15 | import inspect
16 | import itertools
17 |
18 | from six import itervalues, with_metaclass
19 | from six.moves import zip
20 |
21 | from profiling.sortkeys import by_deep_time
22 | from profiling.utils import noop
23 |
24 |
25 | __all__ = ['Statistics', 'RecordingStatistics', 'VoidRecordingStatistics',
26 | 'FrozenStatistics', 'FlatFrozenStatistics']
27 |
28 |
29 | class spread_t(object):
30 | __slots__ = ('flag',)
31 | __bool__ = __nonzero__ = lambda x: x.flag
32 | def clear(self):
33 | self.flag = False
34 | def __call__(self):
35 | self.flag = True
36 |
37 |
38 | def spread_stats(stats, spreader=False):
39 | """Iterates all descendant statistics under the given root statistics.
40 |
41 | When ``spreader=True``, each iteration yields a descendant statistics and
42 | `spread()` function together. You should call `spread()` if you want to
43 | spread the yielded statistics also.
44 |
45 | """
46 | spread = spread_t() if spreader else True
47 | descendants = deque(stats)
48 | while descendants:
49 | _stats = descendants.popleft()
50 | if spreader:
51 | spread.clear()
52 | yield _stats, spread
53 | else:
54 | yield _stats
55 | if spread:
56 | descendants.extend(_stats)
57 |
58 |
59 | class default(object):
60 |
61 | __slots__ = ('value',)
62 |
63 | def __init__(self, value):
64 | self.value = value
65 |
66 |
67 | class StatisticsMeta(type):
68 |
69 | def __new__(meta, name, bases, attrs):
70 | slots = attrs.get('__slots__', ())
71 | defaults = {}
72 | for attr in slots:
73 | if attr not in attrs:
74 | continue
75 | elif isinstance(attrs[attr], default):
76 | defaults[attr] = attrs.pop(attr).value
77 | cls = super(StatisticsMeta, meta).__new__(meta, name, bases, attrs)
78 | try:
79 | base_defaults = cls.__defaults__
80 | except AttributeError:
81 | pass
82 | else:
83 | # inherit defaults from the base classes.
84 | for attr in slots:
85 | if attr not in defaults and attr in base_defaults:
86 | defaults[attr] = base_defaults[attr]
87 | cls.__defaults__ = defaults
88 | return cls
89 |
90 | def __call__(cls, *args, **kwargs):
91 | obj = super(StatisticsMeta, cls).__call__(*args, **kwargs)
92 | for attr, value in cls.__defaults__.items():
93 | if not hasattr(obj, attr):
94 | setattr(obj, attr, value)
95 | return obj
96 |
97 |
98 | class Statistics(with_metaclass(StatisticsMeta)):
99 | """Statistics of a function."""
100 |
101 | __slots__ = ('name', 'filename', 'lineno', 'module',
102 | 'own_hits', 'deep_time')
103 |
104 | name = default(None)
105 | filename = default(None)
106 | lineno = default(None)
107 | module = default(None)
108 | #: The inclusive calling/sampling number.
109 | own_hits = default(0)
110 | #: The exclusive execution time.
111 | deep_time = default(0.0)
112 |
113 | def __init__(self, *args, **kwargs):
114 | for attr, value in zip(self.__slots__, args):
115 | setattr(self, attr, value)
116 | for attr, value in kwargs.items():
117 | setattr(self, attr, value)
118 |
119 | @property
120 | def regular_name(self):
121 | name, module = self.name, self.module
122 | if name and module:
123 | return ':'.join([module, name])
124 | return name or module
125 |
126 | @property
127 | def deep_hits(self):
128 | """The inclusive calling/sampling number.
129 |
130 | Calculates as sum of the own hits and deep hits of the children.
131 | """
132 | hits = [self.own_hits]
133 | hits.extend(stats.own_hits for stats in spread_stats(self))
134 | return sum(hits)
135 |
136 | @property
137 | def own_time(self):
138 | """The exclusive execution time."""
139 | sub_time = sum(stats.deep_time for stats in self)
140 | return max(0., self.deep_time - sub_time)
141 |
142 | @property
143 | def deep_time_per_call(self):
144 | try:
145 | return self.deep_time / self.own_hits
146 | except ZeroDivisionError:
147 | return 0.0
148 |
149 | @property
150 | def own_time_per_call(self):
151 | try:
152 | return self.own_time / self.own_hits
153 | except ZeroDivisionError:
154 | return 0.0
155 |
156 | def sorted(self, order=by_deep_time):
157 | return sorted(self, key=order)
158 |
159 | def __iter__(self):
160 | """Override it to walk statistics children."""
161 | return iter(())
162 |
163 | def __len__(self):
164 | """Override it to count statistics children."""
165 | return 0
166 |
167 | def __hash__(self):
168 | """Statistics can be a key."""
169 | return hash((self.name, self.filename, self.lineno))
170 |
171 | def __reduce__(self):
172 | """Freezes this statistics to safen to pack/unpack in Pickle."""
173 | tree = make_frozen_stats_tree(self)
174 | return (frozen_stats_from_tree, (tree,))
175 |
176 | def __repr__(self):
177 | # format name
178 | regular_name = self.regular_name
179 | name_string = "'{0}' ".format(regular_name) if regular_name else ''
180 | # format hits
181 | deep_hits = self.deep_hits
182 | if self.own_hits == deep_hits:
183 | hits_string = str(self.own_hits)
184 | else:
185 | hits_string = '{0}/{1}'.format(self.own_hits, deep_hits)
186 | # format time
187 | own_time = self.own_time
188 | if own_time == self.deep_time:
189 | time_string = '{0:.6f}'.format(self.deep_time)
190 | else:
191 | time_string = '{0:.6f}/{1:.6f}'.format(own_time, self.deep_time)
192 | # join all
193 | class_name = type(self).__name__
194 | return ('<{0} {1}hits={2} time={3}>'
195 | ''.format(class_name, name_string, hits_string, time_string))
196 |
197 |
198 | class RecordingStatistics(Statistics):
199 | """Recordig statistics measures execution time of a code."""
200 |
201 | __slots__ = ('own_hits', 'deep_time', 'code', '_children')
202 |
203 | own_hits = default(0)
204 | deep_time = default(0.0)
205 |
206 | def __init__(self, code=None):
207 | self.code = code
208 | self._children = {}
209 |
210 | @property
211 | def name(self):
212 | if self.code is None:
213 | return
214 | name = self.code.co_name
215 | if name == '':
216 | return
217 | return name
218 |
219 | @property
220 | def filename(self):
221 | return self.code and self.code.co_filename
222 |
223 | @property
224 | def lineno(self):
225 | return self.code and self.code.co_firstlineno
226 |
227 | @property
228 | def module(self):
229 | if self.code is None:
230 | return
231 | module = inspect.getmodule(self.code)
232 | if not module:
233 | return
234 | return module.__name__
235 |
236 | @property
237 | def children(self):
238 | return list(itervalues(self._children))
239 |
240 | def get_child(self, code):
241 | return self._children[code]
242 |
243 | def add_child(self, code, stats):
244 | self._children[code] = stats
245 |
246 | def remove_child(self, code):
247 | del self._children[code]
248 |
249 | def discard_child(self, code):
250 | self._children.pop(code, None)
251 |
252 | def ensure_child(self, code, adding_stat_class=None):
253 | stats = self._children.get(code)
254 | if stats is None:
255 | stat_class = adding_stat_class or type(self)
256 | stats = stat_class(code)
257 | self.add_child(code, stats)
258 | return stats
259 |
260 | def clear(self):
261 | self._children.clear()
262 | for attr, value in self.__defaults__.items():
263 | setattr(self, attr, value)
264 |
265 | def __iter__(self):
266 | return itervalues(self._children)
267 |
268 | def __len__(self):
269 | return len(self._children)
270 |
271 | def __contains__(self, code):
272 | return code in self._children
273 |
274 |
275 | class VoidRecordingStatistics(RecordingStatistics):
276 | """Statistics for an absent frame."""
277 |
278 | __slots__ = ('code', '_children')
279 |
280 | own_hits = property(lambda x: 0, noop)
281 |
282 | def deep_time(self):
283 | times = []
284 | for stats, spread in spread_stats(self, spreader=True):
285 | if isinstance(stats, VoidRecordingStatistics):
286 | spread()
287 | else:
288 | times.append(stats.deep_time)
289 | return sum(times)
290 |
291 | deep_time = property(deep_time, noop)
292 |
293 |
294 | class FrozenStatistics(Statistics):
295 | """Frozen :class:`Statistics` to serialize by Pickle."""
296 |
297 | __slots__ = ('name', 'filename', 'lineno', 'module',
298 | 'own_hits', 'deep_time', 'children')
299 |
300 | def __init__(self, *args, **kwargs):
301 | super(FrozenStatistics, self).__init__(*args, **kwargs)
302 | if not hasattr(self, 'children'):
303 | self.children = []
304 |
305 | def __iter__(self):
306 | return iter(self.children)
307 |
308 | def __len__(self):
309 | return len(self.children)
310 |
311 |
312 | def make_frozen_stats_tree(stats):
313 | """Makes a flat members tree of the given statistics. The statistics can
314 | be restored by :func:`frozen_stats_from_tree`.
315 | """
316 | tree, stats_tree = [], [(None, stats)]
317 | for x in itertools.count():
318 | try:
319 | parent_offset, _stats = stats_tree[x]
320 | except IndexError:
321 | break
322 | stats_tree.extend((x, s) for s in _stats)
323 | members = (_stats.name, _stats.filename, _stats.lineno,
324 | _stats.module, _stats.own_hits, _stats.deep_time)
325 | tree.append((parent_offset, members))
326 | return tree
327 |
328 |
329 | def frozen_stats_from_tree(tree):
330 | """Restores a statistics from the given flat members tree.
331 | :func:`make_frozen_stats_tree` makes a tree for this function.
332 | """
333 | if not tree:
334 | raise ValueError('Empty tree')
335 | stats_index = []
336 | for parent_offset, members in tree:
337 | stats = FrozenStatistics(*members)
338 | stats_index.append(stats)
339 | if parent_offset is not None:
340 | stats_index[parent_offset].children.append(stats)
341 | return stats_index[0]
342 |
343 |
344 | class FlatFrozenStatistics(FrozenStatistics):
345 |
346 | __slots__ = ('name', 'filename', 'lineno', 'module',
347 | 'own_hits', 'deep_hits', 'own_time', 'deep_time',
348 | 'children')
349 |
350 | own_hits = default(0)
351 | deep_hits = default(0)
352 | own_time = default(0.0)
353 | deep_time = default(0.0)
354 | children = default(())
355 |
356 | @classmethod
357 | def flatten(cls, stats):
358 | """Makes a flat statistics from the given statistics."""
359 | flat_children = {}
360 | for _stats in spread_stats(stats):
361 | key = (_stats.name, _stats.filename, _stats.lineno, _stats.module)
362 | try:
363 | flat_stats = flat_children[key]
364 | except KeyError:
365 | flat_stats = flat_children[key] = cls(*key)
366 | flat_stats.own_hits += _stats.own_hits
367 | flat_stats.deep_hits += _stats.deep_hits
368 | flat_stats.own_time += _stats.own_time
369 | flat_stats.deep_time += _stats.deep_time
370 | children = list(itervalues(flat_children))
371 | return cls(stats.name, stats.filename, stats.lineno, stats.module,
372 | stats.own_hits, stats.deep_hits, stats.own_time,
373 | stats.deep_time, children)
374 |
--------------------------------------------------------------------------------
/profiling/tracing/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.tracing
4 | ~~~~~~~~~~~~~~~~~
5 |
6 | Profiles deterministically by :func:`sys.setprofile`.
7 |
8 | :copyright: (c) 2014-2017, What! Studio
9 | :license: BSD, see LICENSE for more details.
10 |
11 | """
12 | from __future__ import absolute_import
13 |
14 | import sys
15 | import threading
16 |
17 | from profiling import sortkeys
18 | from profiling.profiler import Profiler
19 | from profiling.stats import (
20 | RecordingStatistics, VoidRecordingStatistics as void)
21 | from profiling.tracing.timers import Timer
22 | from profiling.utils import deferral
23 | from profiling.viewer import fmt, StatisticsTable
24 |
25 |
26 | __all__ = ['TracingProfiler', 'TracingStatisticsTable']
27 |
28 |
29 | TIMER_CLASS = Timer
30 |
31 |
32 | class TracingStatisticsTable(StatisticsTable):
33 |
34 | columns = [
35 | ('FUNCTION', 'left', ('weight', 1), sortkeys.by_function),
36 | ('CALLS', 'right', (6,), sortkeys.by_own_hits),
37 | ('OWN', 'right', (6,), sortkeys.by_own_time),
38 | ('/CALL', 'right', (6,), sortkeys.by_own_time_per_call),
39 | ('%', 'left', (4,), None),
40 | ('DEEP', 'right', (6,), sortkeys.by_deep_time),
41 | ('/CALL', 'right', (6,), sortkeys.by_deep_time_per_call),
42 | ('%', 'left', (4,), None),
43 | ]
44 | order = sortkeys.by_deep_time
45 |
46 | def make_cells(self, node, stats):
47 | yield fmt.make_stat_text(stats)
48 | yield fmt.make_int_or_na_text(stats.own_hits)
49 | yield fmt.make_time_text(stats.own_time)
50 | yield fmt.make_time_text(stats.own_time_per_call)
51 | yield fmt.make_percent_text(stats.own_time, self.cpu_time)
52 | yield fmt.make_time_text(stats.deep_time)
53 | yield fmt.make_time_text(stats.deep_time_per_call)
54 | yield fmt.make_percent_text(stats.deep_time, self.cpu_time)
55 |
56 |
57 | class TracingProfiler(Profiler):
58 | """The tracing profiler."""
59 |
60 | table_class = TracingStatisticsTable
61 |
62 | #: The CPU timer. Usually it is an instance of :class:`profiling.tracing.
63 | #: timers.Timer`.
64 | timer = None
65 |
66 | #: The CPU time of profiling overhead. It's the time spent in
67 | #: :meth:`_profile`.
68 | overhead = 0.0
69 |
70 | def __init__(self, base_frame=None, base_code=None,
71 | ignored_frames=(), ignored_codes=(), timer=None):
72 | timer = timer or TIMER_CLASS()
73 | if not isinstance(timer, Timer):
74 | raise TypeError('Not a timer instance')
75 | base = super(TracingProfiler, self)
76 | base.__init__(base_frame, base_code, ignored_frames, ignored_codes)
77 | self.timer = timer
78 | self._times_entered = {}
79 |
80 | def _profile(self, frame, event, arg):
81 | """The callback function to register by :func:`sys.setprofile`."""
82 | # c = event.startswith('c_')
83 | if event.startswith('c_'):
84 | return
85 | time1 = self.timer()
86 | frames = self.frame_stack(frame)
87 | if frames:
88 | frames.pop()
89 | parent_stats = self.stats
90 | for f in frames:
91 | parent_stats = parent_stats.ensure_child(f.f_code, void)
92 | code = frame.f_code
93 | frame_key = id(frame)
94 | # if c:
95 | # event = event[2:]
96 | # code = mock_code(arg.__name__)
97 | # frame_key = id(arg)
98 | # record
99 | time2 = self.timer()
100 | self.overhead += time2 - time1
101 | if event == 'call':
102 | time = time2 - self.overhead
103 | self.record_entering(time, code, frame_key, parent_stats)
104 | elif event == 'return':
105 | time = time1 - self.overhead
106 | self.record_leaving(time, code, frame_key, parent_stats)
107 | time3 = self.timer()
108 | self.overhead += time3 - time2
109 |
110 | def record_entering(self, time, code, frame_key, parent_stats):
111 | """Entered to a function call."""
112 | stats = parent_stats.ensure_child(code, RecordingStatistics)
113 | self._times_entered[(code, frame_key)] = time
114 | stats.own_hits += 1
115 |
116 | def record_leaving(self, time, code, frame_key, parent_stats):
117 | """Left from a function call."""
118 | try:
119 | stats = parent_stats.get_child(code)
120 | time_entered = self._times_entered.pop((code, frame_key))
121 | except KeyError:
122 | return
123 | time_elapsed = time - time_entered
124 | stats.deep_time += max(0, time_elapsed)
125 |
126 | def result(self):
127 | base = super(TracingProfiler, self)
128 | frozen_stats, cpu_time, wall_time = base.result()
129 | return (frozen_stats, cpu_time - self.overhead, wall_time)
130 |
131 | def run(self):
132 | if sys.getprofile() is not None:
133 | # NOTE: There's no threading.getprofile().
134 | # The profiling function will be stored at threading._profile_hook
135 | # but it's not documented.
136 | raise RuntimeError('Another profiler already registered')
137 | with deferral() as defer:
138 | self._times_entered.clear()
139 | self.overhead = 0.0
140 | sys.setprofile(self._profile)
141 | defer(sys.setprofile, None)
142 | threading.setprofile(self._profile)
143 | defer(threading.setprofile, None)
144 | self.timer.start(self)
145 | defer(self.timer.stop)
146 | yield
147 |
--------------------------------------------------------------------------------
/profiling/tracing/timers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.tracing.timers
4 | ~~~~~~~~~~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 | import time
13 |
14 | from profiling.utils import lazy_import, Runnable, thread_clock
15 |
16 |
17 | __all__ = ['Timer', 'ContextualTimer', 'ThreadTimer', 'GreenletTimer']
18 |
19 |
20 | class Timer(Runnable):
21 | """The basic timer."""
22 |
23 | #: The raw function to get the CPU time.
24 | clock = time.clock
25 |
26 | def __call__(self):
27 | return self.clock()
28 |
29 | def run(self, profiler):
30 | yield
31 |
32 |
33 | class ContextualTimer(Timer):
34 |
35 | def __new__(cls, *args, **kwargs):
36 | timer = super(ContextualTimer, cls).__new__(cls, *args, **kwargs)
37 | timer._contextual_times = {}
38 | return timer
39 |
40 | def __call__(self, context=None):
41 | if context is None:
42 | context = self.detect_context()
43 | paused_at, resumed_at = self._contextual_times.get(context, (0, 0))
44 | if resumed_at is None: # paused
45 | return paused_at
46 | return paused_at + self.clock() - resumed_at
47 |
48 | def pause(self, context=None):
49 | if context is None:
50 | context = self.detect_context()
51 | self._contextual_times[context] = (self(context), None)
52 |
53 | def resume(self, context=None):
54 | if context is None:
55 | context = self.detect_context()
56 | paused_at, __ = self._contextual_times.get(context, (0, 0))
57 | self._contextual_times[context] = (paused_at, self.clock())
58 |
59 | def detect_context(self):
60 | raise NotImplementedError('detect_context() should be implemented')
61 |
62 |
63 | class ThreadTimer(Timer):
64 | """A timer to get CPU time per thread. Python 3.3 or later uses the
65 | built-in :mod:`time` module. Earlier Python versions requires `Yappi`_ to
66 | be installed.
67 |
68 | .. _Yappi: https://code.google.com/p/yappi/
69 |
70 | """
71 |
72 | def __call__(self):
73 | return thread_clock()
74 |
75 |
76 | class GreenletTimer(ContextualTimer):
77 | """A timer to get CPU time per greenlet."""
78 |
79 | greenlet = lazy_import('greenlet')
80 |
81 | def detect_context(self):
82 | if self.greenlet:
83 | return id(self.greenlet.getcurrent())
84 |
85 | def _trace(self, event, args):
86 | origin, target = args
87 | self.pause(id(origin))
88 | self.resume(id(target))
89 |
90 | def run(self, profiler):
91 | self.greenlet.settrace(self._trace)
92 | yield
93 | self.greenlet.settrace(None)
94 |
--------------------------------------------------------------------------------
/profiling/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.utils
4 | ~~~~~~~~~~~~~~~
5 |
6 | :copyright: (c) 2014-2017, What! Studio
7 | :license: BSD, see LICENSE for more details.
8 |
9 | """
10 | from __future__ import absolute_import
11 |
12 | from collections import deque
13 | from contextlib import contextmanager
14 | import sys
15 |
16 | try:
17 | from profiling import speedup
18 | except ImportError:
19 | speedup = False
20 |
21 |
22 | __all__ = ['Runnable', 'frame_stack', 'repr_frame', 'lazy_import', 'deferral',
23 | 'thread_clock', 'noop']
24 |
25 |
26 | class Runnable(object):
27 | """The base class for runnable classes such as :class:`profiling.profiler.
28 | Profiler`.
29 | """
30 |
31 | #: The generator :meth:`run` returns. It will be set by :meth:`start`.
32 | _running = None
33 |
34 | def is_running(self):
35 | """Whether the instance is running."""
36 | return self._running is not None
37 |
38 | def start(self, *args, **kwargs):
39 | """Starts the instance.
40 |
41 | :raises RuntimeError: has been already started.
42 | :raises TypeError: :meth:`run` is not canonical.
43 |
44 | """
45 | if self.is_running():
46 | raise RuntimeError('Already started')
47 | self._running = self.run(*args, **kwargs)
48 | try:
49 | yielded = next(self._running)
50 | except StopIteration:
51 | raise TypeError('run() must yield just one time')
52 | if yielded is not None:
53 | raise TypeError('run() must yield without value')
54 |
55 | def stop(self):
56 | """Stops the instance.
57 |
58 | :raises RuntimeError: has not been started.
59 | :raises TypeError: :meth:`run` is not canonical.
60 |
61 | """
62 | if not self.is_running():
63 | raise RuntimeError('Not started')
64 | running, self._running = self._running, None
65 | try:
66 | next(running)
67 | except StopIteration:
68 | # expected.
69 | pass
70 | else:
71 | raise TypeError('run() must yield just one time')
72 |
73 | def run(self, *args, **kwargs):
74 | """Override it to implement the starting and stopping behavior.
75 |
76 | An overriding method must be a generator function which yields just one
77 | time without any value. :meth:`start` creates and iterates once the
78 | generator it returns. Then :meth:`stop` will iterates again.
79 |
80 | :raises NotImplementedError: :meth:`run` is not overridden.
81 |
82 | """
83 | raise NotImplementedError('Implement run()')
84 | yield
85 |
86 | def __enter__(self):
87 | self.start()
88 | return self
89 |
90 | def __exit__(self, *exc_info):
91 | self.stop()
92 |
93 |
94 | if speedup:
95 | def frame_stack(frame, base_frame=None, base_code=None,
96 | ignored_frames=(), ignored_codes=()):
97 | return speedup.frame_stack(frame, base_frame, base_code,
98 | set(ignored_frames), set(ignored_codes))
99 | else:
100 | def frame_stack(frame, base_frame=None, base_code=None,
101 | ignored_frames=(), ignored_codes=()):
102 | """Returns a deque of frame stack."""
103 | frames = deque()
104 | while frame is not None:
105 | if frame is base_frame or frame.f_code is base_code:
106 | break
107 | if frame in ignored_frames or frame.f_code in ignored_codes:
108 | pass
109 | else:
110 | frames.appendleft(frame)
111 | frame = frame.f_back
112 | return frames
113 |
114 |
115 | def repr_frame(frame):
116 | return '%s:%d' % (frame.f_code.co_filename, frame.f_lineno)
117 |
118 |
119 | class LazyImport(object):
120 |
121 | def __init__(self, module_name):
122 | self.module_name = module_name
123 | self.module = None
124 |
125 | def __get__(self, obj, cls):
126 | if self.module is None:
127 | self.module = __import__(self.module_name)
128 | return self.module
129 |
130 |
131 | lazy_import = LazyImport
132 |
133 |
134 | @contextmanager
135 | def deferral():
136 | """Defers a function call when it is being required like Go.
137 |
138 | ::
139 |
140 | with deferral() as defer:
141 | sys.setprofile(f)
142 | defer(sys.setprofile, None)
143 | # do something.
144 |
145 | """
146 | deferred = []
147 | defer = lambda f, *a, **k: deferred.append((f, a, k))
148 | try:
149 | yield defer
150 | finally:
151 | while deferred:
152 | f, a, k = deferred.pop()
153 | f(*a, **k)
154 |
155 |
156 | if sys.version_info < (3, 3):
157 | class _yappi_holder_type(object):
158 | yappi = lazy_import('yappi')
159 | _yappi_holder = _yappi_holder_type()
160 | def thread_clock():
161 | return _yappi_holder.yappi.get_clock_time()
162 | else:
163 | import time
164 | def thread_clock():
165 | return time.clock_gettime(time.CLOCK_THREAD_CPUTIME_ID)
166 |
167 |
168 | #: Does nothing. It allows any arguments.
169 | noop = lambda x, *a, **k: None
170 |
--------------------------------------------------------------------------------
/profiling/viewer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling.viewer
4 | ~~~~~~~~~~~~~~~~
5 |
6 | A text user interface application which inspects statistics. To run it
7 | easily do:
8 |
9 | .. sourcecode:: console
10 |
11 | $ profiling view SOURCE
12 |
13 | ::
14 |
15 | viewer = StatisticsViewer()
16 | loop = viewer.loop()
17 | loop.run()
18 |
19 | :copyright: (c) 2014-2017, What! Studio
20 | :license: BSD, see LICENSE for more details.
21 |
22 | """
23 | from __future__ import absolute_import
24 |
25 | from collections import deque
26 |
27 | import urwid
28 | from urwid import connect_signal as on
29 |
30 | from profiling import sortkeys
31 | from profiling.stats import FlatFrozenStatistics
32 |
33 |
34 | __all__ = ['StatisticsTable', 'StatisticsViewer', 'fmt',
35 | 'bind_vim_keys', 'bind_game_keys']
36 |
37 |
38 | NESTED = 0
39 | FLAT = 1
40 |
41 |
42 | def get_func(f):
43 | if isinstance(f, staticmethod):
44 | return f.__func__
45 | return f
46 |
47 |
48 | class Formatter(object):
49 |
50 | def _markup(get_string, get_attr=None):
51 | get_string = get_func(get_string)
52 | get_attr = get_func(get_attr)
53 | @staticmethod
54 | def markup(*args, **kwargs):
55 | string = get_string(*args, **kwargs)
56 | if get_attr is None:
57 | return string
58 | attr = get_attr(*args, **kwargs)
59 | return (attr, string)
60 | return markup
61 |
62 | _numeric = {'align': 'right', 'wrap': 'clip'}
63 |
64 | def _make_text(get_markup, **text_kwargs):
65 | get_markup = get_func(get_markup)
66 | @staticmethod
67 | def make_text(*args, **kwargs):
68 | markup = get_markup(*args, **kwargs)
69 | return urwid.Text(markup, **text_kwargs)
70 | return make_text
71 |
72 | # percent
73 |
74 | @staticmethod
75 | def format_percent(ratio, denom=1, unit=False):
76 | # width: 4~5 (with unit)
77 | # examples:
78 | # 0.01: 1.00%
79 | # 0.1: 10.0%
80 | # 1: 100%
81 | try:
82 | ratio /= float(denom)
83 | except ZeroDivisionError:
84 | ratio = 0
85 | if round(ratio, 2) >= 1:
86 | precision = 0
87 | elif round(ratio, 2) >= 0.1:
88 | precision = 1
89 | else:
90 | precision = 2
91 | string = ('{:.' + str(precision) + 'f}').format(ratio * 100)
92 | if unit:
93 | return string + '%'
94 | else:
95 | return string
96 |
97 | @staticmethod
98 | def attr_ratio(ratio, denom=1, unit=False):
99 | try:
100 | ratio /= float(denom)
101 | except ZeroDivisionError:
102 | ratio = 0
103 | if ratio > 0.9:
104 | return 'danger'
105 | elif ratio > 0.7:
106 | return 'caution'
107 | elif ratio > 0.3:
108 | return 'warning'
109 | elif ratio > 0.1:
110 | return 'notice'
111 | elif ratio <= 0:
112 | return 'zero'
113 |
114 | markup_percent = _markup(format_percent, attr_ratio)
115 | make_percent_text = _make_text(markup_percent, **_numeric)
116 |
117 | # int
118 |
119 | @staticmethod
120 | def format_int(num, units='kMGTPEZY'):
121 | # width: 1~6
122 | # examples:
123 | # 0: 0
124 | # 1: 1
125 | # 10: 10
126 | # 100: 100
127 | # 1000: 1.0K
128 | # 10000: 10.0K
129 | # 100000: 100.0K
130 | # 1000000: 1.0M
131 | # -10: -11
132 | unit = None
133 | unit_iter = iter(units)
134 | while abs(round(num, 1)) >= 1e3:
135 | num /= 1e3
136 | try:
137 | unit = next(unit_iter)
138 | except StopIteration:
139 | # overflow or underflow.
140 | return 'o/f' if num > 0 else 'u/f'
141 | if unit is None:
142 | return '{:.0f}'.format(num)
143 | else:
144 | return '{:.1f}{}'.format(num, unit)
145 |
146 | @staticmethod
147 | def attr_int(num):
148 | return None if num else 'zero'
149 |
150 | markup_int = _markup(format_int, attr_int)
151 | make_int_text = _make_text(markup_int, **_numeric)
152 |
153 | # int or n/a
154 |
155 | @staticmethod
156 | def format_int_or_na(num):
157 | # width: 1~6
158 | # examples:
159 | # 0: n/a
160 | # 1: 1
161 | # 10: 10
162 | # 100: 100
163 | # 1000: 1.0K
164 | # 10000: 10.0K
165 | # 100000: 100.0K
166 | # 1000000: 1.0M
167 | # -10: -11
168 | if num == 0:
169 | return 'n/a'
170 | else:
171 | return Formatter.format_int(num)
172 |
173 | markup_int_or_na = _markup(format_int_or_na, attr_int)
174 | make_int_or_na_text = _make_text(markup_int_or_na, **_numeric)
175 |
176 | # time
177 |
178 | @staticmethod
179 | def format_time(sec):
180 | # width: 1~6 (most cases)
181 | # examples:
182 | # 0: 0
183 | # 0.000001: 1us
184 | # 0.000123: 123us
185 | # 0.012345: 12ms
186 | # 0.123456: 123ms
187 | # 1.234567: 1.2sec
188 | # 12.34567: 12.3sec
189 | # 123.4567: 2min3s
190 | # 6120: 102min
191 | if sec == 0:
192 | return '0'
193 | elif sec < 1e-3:
194 | # 1us ~ 999us
195 | return '{:.0f}us'.format(sec * 1e6)
196 | elif sec < 1:
197 | # 1ms ~ 999ms
198 | return '{:.0f}ms'.format(sec * 1e3)
199 | elif sec < 60:
200 | # 1.0sec ~ 59.9sec
201 | return '{:.1f}sec'.format(sec)
202 | elif sec < 600:
203 | # 1min0s ~ 9min59s
204 | return '{:.0f}m{:.0f}s'.format(sec // 60, sec % 60)
205 | else:
206 | return '{:.0f}m'.format(sec // 60)
207 |
208 | @staticmethod
209 | def attr_time(sec):
210 | if sec == 0:
211 | return 'zero'
212 | elif sec < 1e-3:
213 | return 'usec'
214 | elif sec < 1:
215 | return 'msec'
216 | elif sec < 60:
217 | return 'sec'
218 | else:
219 | return 'min'
220 |
221 | markup_time = _markup(format_time, attr_time)
222 | make_time_text = _make_text(markup_time, **_numeric)
223 |
224 | # stats
225 |
226 | @staticmethod
227 | def markup_stats(stats):
228 | if stats.name:
229 | loc = ('({0}:{1})'
230 | ''.format(stats.module or stats.filename, stats.lineno))
231 | return [('name', stats.name), ' ', ('loc', loc)]
232 | else:
233 | return ('loc', stats.module or stats.filename)
234 |
235 | make_stat_text = _make_text(markup_stats, wrap='clip')
236 |
237 | del _markup
238 | del _make_text
239 |
240 |
241 | fmt = Formatter
242 |
243 |
244 | class StatisticsWidget(urwid.TreeWidget):
245 |
246 | signals = ['expanded', 'collapsed']
247 | icon_chars = ('+', '-', ' ') # collapsed, expanded, leaf
248 |
249 | def __init__(self, node):
250 | super(StatisticsWidget, self).__init__(node)
251 | self._w = urwid.AttrWrap(self._w, None, StatisticsViewer.focus_map)
252 |
253 | def selectable(self):
254 | return True
255 |
256 | @property
257 | def expanded(self):
258 | return self._expanded
259 |
260 | @expanded.setter
261 | def expanded(self, expanded):
262 | in_init = not hasattr(self, 'expanded')
263 | self._expanded = expanded
264 | if in_init:
265 | return
266 | if expanded:
267 | urwid.emit_signal(self, 'expanded')
268 | else:
269 | urwid.emit_signal(self, 'collapsed')
270 |
271 | def get_mark(self):
272 | """Gets an expanded, collapsed, or leaf icon."""
273 | if self.is_leaf:
274 | char = self.icon_chars[2]
275 | else:
276 | char = self.icon_chars[int(self.expanded)]
277 | return urwid.SelectableIcon(('mark', char), 0)
278 |
279 | def load_inner_widget(self):
280 | node = self.get_node()
281 | return node.table.make_row(node)
282 |
283 | def get_indented_widget(self):
284 | icon = self.get_mark()
285 | widget = self.get_inner_widget()
286 | node = self.get_node()
287 | widget = urwid.Columns([('fixed', 1, icon), widget], 1)
288 | indent = (node.get_depth() - 1)
289 | widget = urwid.Padding(widget, left=indent)
290 | return widget
291 |
292 | def update_mark(self):
293 | widget = self._w.base_widget
294 | try:
295 | widget.widget_list[0] = self.get_mark()
296 | except (TypeError, AttributeError):
297 | return
298 |
299 | def update_expanded_icon(self):
300 | self.update_mark()
301 |
302 | def expand(self):
303 | self.expanded = True
304 | self.update_mark()
305 |
306 | def collapse(self):
307 | self.expanded = False
308 | self.update_mark()
309 |
310 | def keypress(self, size, key):
311 | command = self._command_map[key]
312 | if command == urwid.ACTIVATE:
313 | key = '-' if self.expanded else '+'
314 | elif command == urwid.CURSOR_RIGHT:
315 | key = '+'
316 | elif self.expanded and command == urwid.CURSOR_LEFT:
317 | key = '-'
318 | return super(StatisticsWidget, self).keypress(size, key)
319 |
320 |
321 | class EmptyWidget(urwid.Widget):
322 | """A widget which doesn't render anything."""
323 |
324 | def __init__(self, rows=0):
325 | super(EmptyWidget, self).__init__()
326 | self._rows = rows
327 |
328 | def rows(self, size, focus=False):
329 | return self._rows
330 |
331 | def render(self, size, focus=False):
332 | return urwid.SolidCanvas(' ', size[0], self.rows(size, focus))
333 |
334 |
335 | class RootStatisticsWidget(StatisticsWidget):
336 |
337 | def load_inner_widget(self):
338 | return EmptyWidget()
339 |
340 | def get_indented_widget(self):
341 | return self.get_inner_widget()
342 |
343 | def get_mark(self):
344 | raise TypeError('Statistics widget has no mark')
345 |
346 | def update(self):
347 | pass
348 |
349 | def unexpand(self):
350 | pass
351 |
352 |
353 | class StatisticsNodeBase(urwid.TreeNode):
354 |
355 | def __init__(self, stats=None, parent=None, key=None, depth=None,
356 | table=None):
357 | super(StatisticsNodeBase, self).__init__(stats, parent, key, depth)
358 | self.table = table
359 |
360 | def get_focus(self):
361 | widget, focus = super(StatisticsNodeBase, self).get_focus()
362 | if self.table is not None:
363 | self.table.walker.set_focus(self)
364 | return widget, focus
365 |
366 | def get_widget(self, reload=False):
367 | if self._widget is None or reload:
368 | self._widget = self.load_widget()
369 | self.setup_widget(self._widget)
370 | return self._widget
371 |
372 | def load_widget(self):
373 | return self._widget_class(self)
374 |
375 | def setup_widget(self, widget):
376 | if self.table is None:
377 | return
378 | stats = self.get_value()
379 | if hash(stats) in self.table._expanded_stat_hashes:
380 | widget.expand()
381 |
382 |
383 | class NullStatisticsWidget(StatisticsWidget):
384 |
385 | def __init__(self, node):
386 | urwid.TreeWidget.__init__(self, node)
387 |
388 | def get_inner_widget(self):
389 | widget = urwid.Text(('weak', '- Not Available -'), align='center')
390 | widget = urwid.Filler(widget)
391 | widget = urwid.BoxAdapter(widget, 3)
392 | return widget
393 |
394 |
395 | class NullStatisticsNode(StatisticsNodeBase):
396 |
397 | _widget_class = NullStatisticsWidget
398 |
399 |
400 | class LeafStatisticsNode(StatisticsNodeBase):
401 |
402 | _widget_class = StatisticsWidget
403 |
404 |
405 | class StatisticsNode(StatisticsNodeBase, urwid.ParentNode):
406 |
407 | def deep_usage(self):
408 | stats = self.get_value()
409 | table = self.get_root()
410 | try:
411 | return stats.deep_time / table.cpu_time
412 | except AttributeError:
413 | return 0.0
414 |
415 | def load_widget(self):
416 | if self.is_root():
417 | widget_class = RootStatisticsWidget
418 | else:
419 | widget_class = StatisticsWidget
420 | widget = widget_class(self)
421 | widget.collapse()
422 | return widget
423 |
424 | def setup_widget(self, widget):
425 | super(StatisticsNode, self).setup_widget(widget)
426 | if self.get_depth() == 0:
427 | # Just expand the root node.
428 | widget.expand()
429 | return
430 | table = self.table
431 | if table is None:
432 | return
433 | on(widget, 'expanded', table._widget_expanded, widget)
434 | on(widget, 'collapsed', table._widget_collapsed, widget)
435 |
436 | def load_child_keys(self):
437 | stats = self.get_value()
438 | if stats is None:
439 | return ()
440 | return stats.sorted(self.table.order)
441 |
442 | def load_child_node(self, stats):
443 | depth = self.get_depth() + 1
444 | node_class = StatisticsNode if len(stats) else LeafStatisticsNode
445 | return node_class(stats, self, stats, depth, self.table)
446 |
447 |
448 | class StatisticsListBox(urwid.TreeListBox):
449 |
450 | signals = ['focus_changed']
451 |
452 | def change_focus(self, *args, **kwargs):
453 | super(StatisticsListBox, self).change_focus(*args, **kwargs)
454 | focus = self.get_focus()
455 | urwid.emit_signal(self, 'focus_changed', focus)
456 |
457 |
458 | class StatisticsWalker(urwid.TreeWalker):
459 |
460 | signals = ['focus_changed']
461 |
462 | def set_focus(self, focus):
463 | super(StatisticsWalker, self).set_focus(focus)
464 | urwid.emit_signal(self, 'focus_changed', focus)
465 |
466 |
467 | class StatisticsTable(urwid.WidgetWrap):
468 |
469 | #: The column declarations. Define it with a list of (name, align, width,
470 | #: order) tuples.
471 | columns = [('FUNCTION', 'left', ('weight', 1), sortkeys.by_function)]
472 |
473 | #: The initial order.
474 | order = sortkeys.by_function
475 |
476 | #: The children statistics layout. One of `NESTED` or `FLAT`.
477 | layout = NESTED
478 |
479 | title = None
480 | stats = None
481 | time = None
482 |
483 | def __init__(self, viewer):
484 | self._expanded_stat_hashes = set()
485 | self.walker = StatisticsWalker(NullStatisticsNode())
486 | on(self.walker, 'focus_changed', self._walker_focus_changed)
487 | tbody = StatisticsListBox(self.walker)
488 | thead = urwid.AttrMap(self.make_columns([
489 | urwid.Text(name, align, 'clip')
490 | for name, align, __, __ in self.columns
491 | ]), None)
492 | header = urwid.Columns([])
493 | widget = urwid.Frame(tbody, urwid.Pile([header, thead]))
494 | super(StatisticsTable, self).__init__(widget)
495 | self.viewer = viewer
496 | self.update_frame()
497 |
498 | def make_row(self, node):
499 | stats = node.get_value()
500 | return self.make_columns(self.make_cells(node, stats))
501 |
502 | def make_cells(self, node, stats):
503 | yield fmt.make_stat_text(stats)
504 |
505 | @classmethod
506 | def make_columns(cls, column_widgets):
507 | widget_list = []
508 | widths = (width for __, __, width, __ in cls.columns)
509 | for width, widget in zip(widths, column_widgets):
510 | widget_list.append(width + (widget,))
511 | return urwid.Columns(widget_list, 1)
512 |
513 | @property
514 | def tbody(self):
515 | return self._w.body
516 |
517 | @tbody.setter
518 | def tbody(self, body):
519 | self._w.body = body
520 |
521 | @property
522 | def thead(self):
523 | return self._w.header.contents[1][0]
524 |
525 | @thead.setter
526 | def thead(self, thead):
527 | self._w.header.contents[1] = (thead, ('pack', None))
528 |
529 | @property
530 | def header(self):
531 | return self._w.header.contents[0][0]
532 |
533 | @header.setter
534 | def header(self, header):
535 | self._w.header.contents[0] = (header, ('pack', None))
536 |
537 | @property
538 | def footer(self):
539 | return self._w.footer
540 |
541 | @footer.setter
542 | def footer(self, footer):
543 | self._w.footer = footer
544 |
545 | def get_focus(self):
546 | return self.tbody.get_focus()
547 |
548 | def set_focus(self, focus):
549 | self.tbody.set_focus(focus)
550 |
551 | def get_path(self):
552 | """Gets the path to the focused statistics. Each step is a hash of
553 | statistics object.
554 | """
555 | path = deque()
556 | __, node = self.get_focus()
557 | while not node.is_root():
558 | stats = node.get_value()
559 | path.appendleft(hash(stats))
560 | node = node.get_parent()
561 | return path
562 |
563 | def find_node(self, node, path):
564 | """Finds a node by the given path from the given node."""
565 | for hash_value in path:
566 | if isinstance(node, LeafStatisticsNode):
567 | break
568 | for stats in node.get_child_keys():
569 | if hash(stats) == hash_value:
570 | node = node.get_child_node(stats)
571 | break
572 | else:
573 | break
574 | return node
575 |
576 | def get_stats(self):
577 | return self.stats
578 |
579 | def set_result(self, stats, cpu_time=0.0, wall_time=0.0,
580 | title=None, at=None):
581 | self.stats = stats
582 | self.cpu_time = cpu_time
583 | self.wall_time = wall_time
584 | self.title = title
585 | self.at = at
586 | self.refresh()
587 |
588 | def set_layout(self, layout):
589 | if layout == self.layout:
590 | return # Ignore.
591 | self.layout = layout
592 | self.refresh()
593 |
594 | def sort_stats(self, order=sortkeys.by_deep_time):
595 | assert callable(order)
596 | if order == self.order:
597 | return # Ignore.
598 | self.order = order
599 | self.refresh()
600 |
601 | def shift_order(self, delta):
602 | orders = [order for __, __, __, order in self.columns if order]
603 | x = orders.index(self.order)
604 | order = orders[(x + delta) % len(orders)]
605 | self.sort_stats(order)
606 |
607 | def refresh(self):
608 | stats = self.get_stats()
609 | if stats is None:
610 | return
611 | if self.layout == FLAT:
612 | stats = FlatFrozenStatistics.flatten(stats)
613 | node = StatisticsNode(stats, table=self)
614 | path = self.get_path()
615 | node = self.find_node(node, path)
616 | self.set_focus(node)
617 |
618 | def update_frame(self, focus=None):
619 | # Set thead attr.
620 | if self.viewer.paused:
621 | thead_attr = 'thead.paused'
622 | elif not self.viewer.active:
623 | thead_attr = 'thead.inactive'
624 | else:
625 | thead_attr = 'thead'
626 | self.thead.set_attr_map({None: thead_attr})
627 | # Set sorting column in thead attr.
628 | for x, (__, __, __, order) in enumerate(self.columns):
629 | attr = thead_attr + '.sorted' if order is self.order else None
630 | widget = self.thead.base_widget.contents[x][0]
631 | text, __ = widget.get_text()
632 | widget.set_text((attr, text))
633 | if self.viewer.paused:
634 | return
635 | # Update header.
636 | stats = self.get_stats()
637 | if stats is None:
638 | return
639 | title = self.title
640 | time = self.time
641 | if title or time:
642 | if time is not None:
643 | time_string = '{:%H:%M:%S}'.format(time)
644 | if title and time:
645 | markup = [('weak', title), ' ', time_string]
646 | elif title:
647 | markup = title
648 | else:
649 | markup = time_string
650 | meta_info = urwid.Text(markup, align='right')
651 | else:
652 | meta_info = None
653 | fraction_string = '({0}/{1})'.format(
654 | fmt.format_time(self.cpu_time),
655 | fmt.format_time(self.wall_time))
656 | try:
657 | cpu_usage = self.cpu_time / self.wall_time
658 | except ZeroDivisionError:
659 | cpu_usage = 0.0
660 | cpu_info = urwid.Text([
661 | 'CPU ', fmt.markup_percent(cpu_usage, unit=True),
662 | ' ', ('weak', fraction_string)])
663 | # Set header columns.
664 | col_opts = ('weight', 1, False)
665 | self.header.contents = \
666 | [(w, col_opts) for w in [cpu_info, meta_info] if w]
667 |
668 | def focus_hotspot(self, size):
669 | widget, __ = self.tbody.get_focus()
670 | while widget:
671 | node = widget.get_node()
672 | widget.expand()
673 | widget = widget.first_child()
674 | self.tbody.change_focus(size, node)
675 |
676 | def defocus(self):
677 | __, node = self.get_focus()
678 | self.set_focus(node.get_root())
679 |
680 | def keypress(self, size, key):
681 | command = self._command_map[key]
682 | if key == ']':
683 | self.shift_order(+1)
684 | return True
685 | elif key == '[':
686 | self.shift_order(-1)
687 | return True
688 | elif key == '>':
689 | self.focus_hotspot(size)
690 | return True
691 | elif key == '\\':
692 | layout = {FLAT: NESTED, NESTED: FLAT}[self.layout]
693 | self.set_layout(layout)
694 | return True
695 | command = self._command_map[key]
696 | if command == 'menu':
697 | # key: ESC.
698 | self.defocus()
699 | return True
700 | elif command == urwid.CURSOR_RIGHT:
701 | if self.layout == FLAT:
702 | return True # Ignore.
703 | widget, node = self.tbody.get_focus()
704 | if widget.expanded:
705 | heavy_widget = widget.first_child()
706 | if heavy_widget is not None:
707 | heavy_node = heavy_widget.get_node()
708 | self.tbody.change_focus(size, heavy_node)
709 | return True
710 | elif command == urwid.CURSOR_LEFT:
711 | if self.layout == FLAT:
712 | return True # Ignore.
713 | widget, node = self.tbody.get_focus()
714 | if not widget.expanded:
715 | parent_node = node.get_parent()
716 | if parent_node is not None and not parent_node.is_root():
717 | self.tbody.change_focus(size, parent_node)
718 | return True
719 | elif command == urwid.ACTIVATE:
720 | # key: Enter or Space.
721 | if self.viewer.paused:
722 | self.viewer.resume()
723 | else:
724 | self.viewer.pause()
725 | return True
726 | return super(StatisticsTable, self).keypress(size, key)
727 |
728 | # Signal handlers.
729 |
730 | def _walker_focus_changed(self, focus):
731 | self.update_frame(focus)
732 |
733 | def _widget_expanded(self, widget):
734 | stats = widget.get_node().get_value()
735 | self._expanded_stat_hashes.add(hash(stats))
736 |
737 | def _widget_collapsed(self, widget):
738 | stats = widget.get_node().get_value()
739 | self._expanded_stat_hashes.discard(hash(stats))
740 |
741 |
742 | class StatisticsViewer(object):
743 |
744 | weak_color = 'light green'
745 | palette = [
746 | ('weak', weak_color, ''),
747 | ('focus', 'standout', '', 'standout'),
748 | # ui
749 | ('thead', 'dark cyan, standout', '', 'standout'),
750 | ('thead.paused', 'dark red, standout', '', 'standout'),
751 | ('thead.inactive', 'brown, standout', '', 'standout'),
752 | ('mark', 'dark magenta', ''),
753 | # risk
754 | ('danger', 'dark red', '', 'blink'),
755 | ('caution', 'light red', '', 'blink'),
756 | ('warning', 'brown', '', 'blink'),
757 | ('notice', 'dark green', '', 'blink'),
758 | # clock
759 | ('min', 'dark red', ''),
760 | ('sec', 'brown', ''),
761 | ('msec', '', ''),
762 | ('usec', weak_color, ''),
763 | # etc
764 | ('zero', weak_color, ''),
765 | ('name', 'bold', ''),
766 | ('loc', 'dark blue', ''),
767 | ]
768 | # add thead.*.sorted palette entries
769 | for entry in palette[:]:
770 | attr = entry[0]
771 | if attr is not None and attr.startswith('thead'):
772 | fg, bg, mono = entry[1:4]
773 | palette.append((attr + '.sorted', fg + ', underline',
774 | bg, mono + ', underline'))
775 |
776 | focus_map = {None: 'focus'}
777 | focus_map.update((x[0], 'focus') for x in palette)
778 |
779 | #: Whether the viewer is active.
780 | active = False
781 |
782 | #: Whether the viewer is paused.
783 | paused = False
784 |
785 | def unhandled_input(self, key):
786 | if key in ('q', 'Q'):
787 | raise urwid.ExitMainLoop()
788 |
789 | def __init__(self):
790 | self.table = StatisticsTable(self)
791 | self.widget = urwid.Padding(self.table, right=1)
792 |
793 | def loop(self, *args, **kwargs):
794 | kwargs.setdefault('unhandled_input', self.unhandled_input)
795 | loop = urwid.MainLoop(self.widget, self.palette, *args, **kwargs)
796 | return loop
797 |
798 | def set_profiler_class(self, profiler_class):
799 | table_class = profiler_class.table_class
800 | # NOTE: Don't use isinstance() at the below line.
801 | if type(self.table) is table_class:
802 | return
803 | self.table = table_class(self)
804 | self.widget.original_widget = self.table
805 |
806 | def set_result(self, stats, cpu_time=0.0, wall_time=0.0,
807 | title=None, at=None):
808 | self._final_result = (stats, cpu_time, wall_time, title, at)
809 | if not self.paused:
810 | self.update_result()
811 |
812 | def update_result(self):
813 | """Updates the result on the table."""
814 | try:
815 | if self.paused:
816 | result = self._paused_result
817 | else:
818 | result = self._final_result
819 | except AttributeError:
820 | self.table.update_frame()
821 | return
822 | stats, cpu_time, wall_time, title, at = result
823 | self.table.set_result(stats, cpu_time, wall_time, title, at)
824 |
825 | def activate(self):
826 | self.active = True
827 | self.table.update_frame()
828 |
829 | def inactivate(self):
830 | self.active = False
831 | self.table.update_frame()
832 |
833 | def pause(self):
834 | self.paused = True
835 | try:
836 | self._paused_result = self._final_result
837 | except AttributeError:
838 | pass
839 | self.table.update_frame()
840 |
841 | def resume(self):
842 | self.paused = False
843 | try:
844 | del self._paused_result
845 | except AttributeError:
846 | pass
847 | self.update_result()
848 |
849 |
850 | def bind_vim_keys(urwid=urwid):
851 | urwid.command_map['h'] = urwid.command_map['left']
852 | urwid.command_map['j'] = urwid.command_map['down']
853 | urwid.command_map['k'] = urwid.command_map['up']
854 | urwid.command_map['l'] = urwid.command_map['right']
855 |
856 |
857 | def bind_game_keys(urwid=urwid):
858 | urwid.command_map['a'] = urwid.command_map['left']
859 | urwid.command_map['s'] = urwid.command_map['down']
860 | urwid.command_map['w'] = urwid.command_map['up']
861 | urwid.command_map['d'] = urwid.command_map['right']
862 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | valuedispatch>=0.0.1
2 | click>=4.1
3 | click-default-group>=1.2
4 | six>=1.8.0
5 | urwid>=1.2.1
6 |
--------------------------------------------------------------------------------
/screenshots/sampling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/what-studio/profiling/824ae554b35909ecbb8aead8853429573e70aeee/screenshots/sampling.png
--------------------------------------------------------------------------------
/screenshots/tracing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/what-studio/profiling/824ae554b35909ecbb8aead8853429573e70aeee/screenshots/tracing.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E301, E306, E731
3 | import-order-style = google
4 | application-import-names = profiling, _utils
5 |
6 | [tool:pytest]
7 | python_files = test/test_*.py
8 | norecursedirs = profiling .* *.egg* *env*
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | profiling
4 | ~~~~~~~~~
5 |
6 | .. image:: https://img.shields.io/travis/what-studio/profiling.svg
7 | :target: https://travis-ci.org/what-studio/profiling
8 |
9 | .. image:: https://img.shields.io/coveralls/what-studio/profiling.svg
10 | :target: https://coveralls.io/r/what-studio/profiling
11 |
12 | The profiling package is an interactive continuous Python profiler. It is
13 | inspired from `Unity 3D `_ profiler. This package
14 | provides these features:
15 |
16 | - Profiling statistics keep the frame stack.
17 | - An interactive TUI profiling statistics viewer.
18 | - Provides both of statistical and deterministic profiling.
19 | - Utilities for remote profiling.
20 | - Thread or greenlet aware CPU timer.
21 | - Supports Python 2.7, 3.3, 3.4 and 3.5.
22 | - Currently supports only Linux.
23 |
24 | Links
25 | '''''
26 |
27 | GitHub:
28 | https://github.com/what-studio/profiling
29 | Demo:
30 | https://asciinema.org/a/25394
31 |
32 | """
33 | from __future__ import with_statement
34 |
35 | import os
36 | import sys
37 | from textwrap import dedent
38 |
39 | from setuptools import find_packages, setup
40 | from setuptools.command.install import install
41 | from setuptools.command.test import test
42 | from setuptools.extension import Extension
43 |
44 | try:
45 | import __pypy__
46 | except ImportError:
47 | __pypy__ = False
48 |
49 |
50 | # include __about__.py.
51 | __dir__ = os.path.dirname(__file__)
52 | about = {}
53 | with open(os.path.join(__dir__, 'profiling', '__about__.py')) as f:
54 | exec(f.read(), about)
55 |
56 |
57 | # these files require specific python version or later. they will be replaced
58 | # with a placeholder which raises a runtime error on installation.
59 | PYTHON_VERSION_REQUIREMENTS = {
60 | 'profiling/remote/asyncio.py': (3, 4),
61 | }
62 | INCOMPATIBLE_PYTHON_VERSION_PLACEHOLDER = dedent('''
63 | # -*- coding: utf-8 -*-
64 | raise RuntimeError('Python {version} or later required.')
65 | ''').strip()
66 |
67 |
68 | def requirements(filename):
69 | """Reads requirements from a file."""
70 | with open(filename) as f:
71 | return [x.strip() for x in f.readlines() if x.strip()]
72 |
73 |
74 | # use pytest instead.
75 | def run_tests(self):
76 | raise SystemExit(__import__('pytest').main(['-v']))
77 |
78 |
79 | # replace files which are incompatible with the current python version.
80 | def replace_incompatible_files():
81 | for filename, version_info in PYTHON_VERSION_REQUIREMENTS.items():
82 | if sys.version_info >= version_info:
83 | continue
84 | version = '.'.join(str(v) for v in version_info)
85 | code = INCOMPATIBLE_PYTHON_VERSION_PLACEHOLDER.format(version=version)
86 | with open(filename, 'w') as f:
87 | f.write(code)
88 |
89 |
90 | test.run_tests = run_tests
91 | run_install = install.run
92 | install.run = lambda x: (replace_incompatible_files(), run_install(x))
93 |
94 |
95 | # build profiling.speedup on cpython.
96 | if __pypy__:
97 | ext_modules = []
98 | else:
99 | ext_modules = [Extension('profiling.speedup', ['profiling/speedup.c'])]
100 |
101 |
102 | setup(
103 | name='profiling',
104 | version=about['__version__'],
105 | license=about['__license__'],
106 | author=about['__author__'],
107 | maintainer=about['__maintainer__'],
108 | maintainer_email=about['__maintainer_email__'],
109 | url=about['__url__'],
110 | description=about['__description__'],
111 | long_description=dedent(__doc__),
112 | platforms='linux',
113 | packages=find_packages(),
114 | ext_modules=ext_modules,
115 | entry_points={
116 | 'console_scripts': ['profiling = profiling.__main__:cli']
117 | },
118 | classifiers=[
119 | 'Development Status :: 4 - Beta',
120 | 'Environment :: Console',
121 | 'Intended Audience :: Developers',
122 | 'License :: OSI Approved :: BSD License',
123 | 'Operating System :: POSIX :: Linux',
124 | 'Programming Language :: Python',
125 | 'Programming Language :: Python :: 2',
126 | 'Programming Language :: Python :: 2.7',
127 | 'Programming Language :: Python :: 3',
128 | 'Programming Language :: Python :: 3.3',
129 | 'Programming Language :: Python :: 3.4',
130 | 'Programming Language :: Python :: 3.5',
131 | 'Programming Language :: Python :: 3.6',
132 | 'Programming Language :: Python :: Implementation :: CPython',
133 | 'Programming Language :: Python :: Implementation :: PyPy',
134 | 'Topic :: Software Development :: Debuggers',
135 | ],
136 | install_requires=requirements('requirements.txt'),
137 | tests_require=requirements('test/requirements.txt'),
138 | test_suite='...',
139 | )
140 |
--------------------------------------------------------------------------------
/test/_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from textwrap import dedent
3 | import time
4 |
5 |
6 | __all__ = ['find_multiple_stats', 'find_stats', 'factorial', 'spin',
7 | 'mock_code_names']
8 |
9 |
10 | def find_multiple_stats(stats, name, _found=None, _on_found=None):
11 | if _found is None:
12 | _found = []
13 | for child_stats in stats:
14 | if child_stats.name == name:
15 | _found.append(child_stats)
16 | if callable(_on_found):
17 | _on_found(_found)
18 | find_multiple_stats(child_stats, name, _found)
19 | return _found
20 |
21 |
22 | def find_stats(stats, name):
23 | def _on_found(found):
24 | raise StopIteration
25 | return find_multiple_stats(stats, name)[0]
26 |
27 |
28 | def factorial(n):
29 | f = 1
30 | while n:
31 | f *= n
32 | n -= 1
33 | return f
34 |
35 |
36 | def spin(seconds):
37 | t = time.time()
38 | x = 0
39 | while time.time() - t < seconds:
40 | x += 1
41 | return x
42 |
43 |
44 | foo = None # placeheolder
45 | mock_code_names = ['foo', 'bar', 'baz']
46 | source = ''
47 | for name, next_name in zip(mock_code_names[:-1], mock_code_names[1:]):
48 | source += dedent('''
49 | def {0}():
50 | return {1}()
51 | ''').format(name, next_name)
52 | source += '''
53 | def {0}():
54 | return __import__('sys')._getframe()
55 | '''.format(mock_code_names[-1])
56 | exec(source)
57 | __all__.extend(mock_code_names)
58 |
--------------------------------------------------------------------------------
/test/fit_requirements.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import print_function
3 |
4 | import re
5 | import sys
6 |
7 | import pkg_resources
8 |
9 |
10 | #: Matches with comment string starts with `#`.
11 | comment_re = re.compile(r'#\s*(.+)\s*$')
12 |
13 |
14 | # Detect the Python and PyPy versions.
15 | PYTHON_VERSION = '{0}.{1}.{2}'.format(*sys.version_info)
16 | try:
17 | PYPY_VERSION = '{0}.{1}.{2}'.format(*sys.pypy_version_info)
18 | except AttributeError:
19 | PYPY_VERSION = None
20 |
21 |
22 | def installs(sysreq_string,
23 | python_version=PYTHON_VERSION, pypy_version=PYPY_VERSION):
24 | for sysreq in sysreq_string.split():
25 | if sysreq == '!pypy' and pypy_version is not None:
26 | return False
27 | if sysreq.startswith('python'):
28 | if python_version not in pkg_resources.Requirement.parse(sysreq):
29 | return False
30 | elif sysreq.startswith('pypy'):
31 | if pypy_version is None:
32 | return False
33 | elif pypy_version not in pkg_resources.Requirement.parse(sysreq):
34 | return False
35 | return True
36 |
37 |
38 | def fit_requirements(requirements,
39 | python_version=PYTHON_VERSION, pypy_version=PYPY_VERSION):
40 | """Yields requirement lines only compatible with the current system.
41 |
42 | It parses comments of the given requirement lines. A comment string can
43 | include python version requirement and `!pypy` flag to skip to install on
44 | incompatible system:
45 |
46 | .. sourcecode::
47 |
48 | # requirements.txt
49 | pytest>=2.6.1
50 | eventlet>=0.15 # python>=2.6,<3
51 | gevent>=1 # python>=2.5,<3 !pypy
52 | gevent==1.1rc1 # pypy<2.6.1
53 | greenlet>=0.4.4 # python>=2.4
54 | yappi>=0.92 # python>=2.6,!=3.0 !pypy
55 |
56 | .. sourcecode:: console
57 |
58 | $ pypy3.4 fit_requirements.py requirements.txt
59 | pytest>=2.6.1
60 | greenlet>=0.4.4
61 |
62 | """
63 | for line in requirements:
64 | match = comment_re.search(line)
65 | if match is None:
66 | yield line
67 | continue
68 | comment = match.group(1)
69 | if installs(comment, python_version, pypy_version):
70 | yield line[:match.start()].rstrip() + '\n'
71 |
72 |
73 | if __name__ == '__main__':
74 | filename = sys.argv[1]
75 | with open(filename) as f:
76 | requirements = f.readlines()
77 | requirements = fit_requirements(requirements)
78 | print(''.join(requirements))
79 |
--------------------------------------------------------------------------------
/test/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest<4.0.0
2 | pytest-benchmark>=3.0.0
3 | pytest-rerunfailures>=0.05
4 | eventlet>=0.15 # python>=2.6,<3
5 | gevent>=1 # python>=2.5,<3 !pypy
6 | gevent>=1.1a1 # python>=3 !pypy
7 | gevent==1.1rc1 # pypy<2.6.1
8 | gevent>=1.1rc2 # pypy>=2.6.1
9 | greenlet>=0.4.4 # python>=2.4
10 | yappi>=0.92 # python>=2.6,!=3.0 !pypy
11 |
--------------------------------------------------------------------------------
/test/test_cli.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import io
3 | import textwrap
4 |
5 | import click
6 | from click.testing import CliRunner
7 | from six.moves import builtins
8 | from valuedispatch import valuedispatch
9 |
10 | from profiling.__about__ import __version__
11 | from profiling.__main__ import cli, Module, profiler_options, ProfilingCLI
12 | from profiling.sampling import SamplingProfiler
13 | from profiling.sampling.samplers import TracingSampler
14 | from profiling.tracing import TracingProfiler
15 |
16 |
17 | class MockFileIO(io.StringIO):
18 |
19 | def close(self):
20 | self.seek(0)
21 |
22 |
23 | def mock_file(indented_content):
24 | return MockFileIO(textwrap.dedent(indented_content))
25 |
26 |
27 | cli_runner = CliRunner()
28 |
29 |
30 | def test_module_param_type():
31 | t = Module()
32 | # timeit
33 | filename, code, globals_ = t.convert('timeit', None, None)
34 | assert filename.endswith('timeit.py')
35 | assert code.co_filename.endswith('timeit.py')
36 | assert globals_['__name__'] == '__main__'
37 | assert globals_['__file__'].endswith('timeit.py')
38 | assert globals_['__package__'] == ''
39 | # profiling.__main__
40 | filename, code, globals_ = t.convert('profiling', None, None)
41 | assert filename.endswith('profiling/__main__.py')
42 | assert code.co_filename.endswith('profiling/__main__.py')
43 | assert globals_['__name__'] == '__main__'
44 | assert globals_['__file__'].endswith('profiling/__main__.py')
45 | assert globals_['__package__'] == 'profiling'
46 |
47 |
48 | def test_customized_cli():
49 | cli = ProfilingCLI(default='bar')
50 | @cli.command(aliases=['fooo', 'foooo'])
51 | def foo():
52 | pass
53 | @cli.command()
54 | @click.argument('l', default='answer')
55 | @click.option('-n', type=int, default=0)
56 | def bar(l, n=0):
57 | click.echo('%s: %d' % (l, n))
58 | assert len(cli.commands) == 2
59 | ctx = click.Context(cli)
60 | assert cli.get_command(ctx, 'foo').name == 'foo'
61 | assert cli.get_command(ctx, 'fooo').name == 'foo'
62 | assert cli.get_command(ctx, 'foooo').name == 'foo'
63 | assert cli.get_command(ctx, 'bar').name == 'bar'
64 | assert cli.get_command(ctx, 'hello.txt').name == 'bar'
65 | assert 'Usage:' in cli_runner.invoke(cli, []).output
66 | assert cli_runner.invoke(cli, ['zero']).output == 'zero: 0\n'
67 | assert cli_runner.invoke(cli, ['one', '-n', '1']).output == 'one: 1\n'
68 | assert cli_runner.invoke(cli, ['-n', '42']).output == 'answer: 42\n'
69 | assert 'no such option' in cli_runner.invoke(cli, ['-x']).output
70 |
71 |
72 | def test_profiling_command_usage():
73 | for cmd in ['profile', 'live-profile', 'remote-profile']:
74 | r = cli_runner.invoke(cli, [cmd, '--help'])
75 | assert 'SCRIPT [--] [ARGV]...' in r.output
76 |
77 |
78 | def test_version():
79 | r = cli_runner.invoke(cli, ['--version'])
80 | assert r.output.strip() == 'profiling, version %s' % __version__
81 |
82 |
83 | def test_config(monkeypatch):
84 | @click.command()
85 | @profiler_options
86 | def f(profiler_factory, **kwargs):
87 | profiler = profiler_factory()
88 | return profiler, kwargs
89 | # no config.
90 | def io_error(*args, **kwargs):
91 | raise IOError
92 | monkeypatch.setattr(builtins, 'open', io_error)
93 | profiler, kwargs = f([], standalone_mode=False)
94 | assert isinstance(profiler, TracingProfiler)
95 | # config to use SamplingProfiler.
96 | monkeypatch.setattr(builtins, 'open', lambda *a, **k: mock_file(u'''
97 | [profiling]
98 | profiler = sampling
99 | sampler = tracing
100 | '''))
101 | profiler, kwargs = f([], standalone_mode=False)
102 | assert isinstance(profiler, SamplingProfiler)
103 | assert isinstance(profiler.sampler, TracingSampler)
104 | # set both of setup.cfg and .profiling.
105 | @valuedispatch
106 | def mock_open(path, *args, **kwargs):
107 | raise IOError
108 | @mock_open.register('setup.cfg')
109 | def open_setup_cfg(*_, **__):
110 | return mock_file(u'''
111 | [profiling]
112 | profiler = sampling
113 | pickle-protocol = 3
114 | ''')
115 | @mock_open.register('.profiling')
116 | def open_profiling(*_, **__):
117 | return mock_file(u'''
118 | [profiling]
119 | pickle-protocol = 0
120 | ''')
121 | monkeypatch.setattr(builtins, 'open', mock_open)
122 | profiler, kwargs = f([], standalone_mode=False)
123 | assert isinstance(profiler, SamplingProfiler) # from setup.cfg
124 | assert kwargs['pickle_protocol'] == 0 # from .profiling
125 |
--------------------------------------------------------------------------------
/test/test_profiler.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | try:
3 | import cPickle as pickle
4 | except ImportError:
5 | import pickle
6 | from os import path
7 | import tempfile
8 |
9 | import pytest
10 |
11 | from _utils import foo, spin
12 | from profiling.profiler import Profiler, ProfilerWrapper
13 |
14 |
15 | class NullProfiler(Profiler):
16 |
17 | def run(self):
18 | yield
19 |
20 |
21 | class NullProfilerWrapper(ProfilerWrapper):
22 |
23 | def run(self):
24 | with self.profiler:
25 | yield
26 |
27 |
28 | @pytest.fixture
29 | def profiler():
30 | return NullProfiler()
31 |
32 |
33 | def test_exclude_code(profiler):
34 | foo_code = foo().f_code
35 | with profiler:
36 | assert foo_code not in profiler.stats
37 | profiler.stats.ensure_child(foo_code)
38 | assert foo_code in profiler.stats
39 | profiler.exclude_code(foo_code)
40 | assert foo_code not in profiler.stats
41 | profiler.exclude_code(foo_code)
42 | assert foo_code not in profiler.stats
43 |
44 |
45 | def test_result(profiler):
46 | __, cpu_time, wall_time = profiler.result()
47 | assert cpu_time == wall_time == 0.0
48 | with profiler:
49 | spin(0.1)
50 | __, cpu_time, wall_time = profiler.result()
51 | assert cpu_time > 0.0
52 | assert wall_time >= 0.1
53 |
54 |
55 | def test_dump(profiler):
56 | temp_dir = tempfile.mkdtemp()
57 | temp_file = path.join(temp_dir, "file.prf")
58 |
59 | profiler.dump(temp_file)
60 |
61 | assert path.getsize(temp_file) > 0
62 |
63 | with open(temp_file, 'rb') as f:
64 | profiler_class, (stats, cpu_time, wall_time) = pickle.load(f)
65 |
66 | assert profiler.__class__ == profiler_class
67 | assert cpu_time == wall_time == 0.0
68 |
69 |
70 | def test_wrapper(profiler):
71 | wrapper = NullProfilerWrapper(profiler)
72 | assert isinstance(wrapper, Profiler)
73 | assert wrapper.table_class is profiler.table_class
74 | assert wrapper.stats is profiler.stats
75 | __, cpu_time, wall_time = wrapper.result()
76 | assert cpu_time == wall_time == 0.0
77 | with wrapper:
78 | assert wrapper.is_running()
79 | assert profiler.is_running()
80 | assert not wrapper.is_running()
81 | assert not profiler.is_running()
82 |
--------------------------------------------------------------------------------
/test/test_sampling.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import division
3 |
4 | import os
5 | import signal
6 | import sys
7 |
8 | import pytest
9 |
10 | from _utils import find_stats, spin
11 | from profiling.sampling import SamplingProfiler
12 | from profiling.sampling.samplers import ItimerSampler, TracingSampler
13 |
14 |
15 | def spin_100ms():
16 | spin(0.1)
17 |
18 |
19 | def spin_500ms():
20 | spin(0.5)
21 |
22 |
23 | def _test_sampling_profiler(sampler):
24 | profiler = SamplingProfiler(base_frame=sys._getframe(), sampler=sampler)
25 | with profiler:
26 | spin_100ms()
27 | spin_500ms()
28 | stat1 = find_stats(profiler.stats, 'spin_100ms')
29 | stat2 = find_stats(profiler.stats, 'spin_500ms')
30 | ratio = stat1.deep_hits / stat2.deep_hits
31 | # 1:5 expected, but tolerate (0.8~1.2):5
32 | assert 0.8 <= ratio * 5 <= 1.2
33 |
34 |
35 | @pytest.mark.flaky(reruns=10)
36 | def test_itimer_sampler():
37 | assert signal.getsignal(signal.SIGPROF) == signal.SIG_DFL
38 | try:
39 | _test_sampling_profiler(ItimerSampler(0.0001))
40 | # no crash caused by SIGPROF.
41 | assert signal.getsignal(signal.SIGPROF) == signal.SIG_IGN
42 | for x in range(10):
43 | os.kill(os.getpid(), signal.SIGPROF)
44 | # respect custom handler.
45 | handler = lambda *x: x
46 | signal.signal(signal.SIGPROF, handler)
47 | _test_sampling_profiler(ItimerSampler(0.0001))
48 | assert signal.getsignal(signal.SIGPROF) == handler
49 | finally:
50 | signal.signal(signal.SIGPROF, signal.SIG_DFL)
51 |
52 |
53 | @pytest.mark.flaky(reruns=10)
54 | def test_tracing_sampler():
55 | pytest.importorskip('yappi')
56 | _test_sampling_profiler(TracingSampler(0.0001))
57 |
58 |
59 | @pytest.mark.flaky(reruns=10)
60 | def test_tracing_sampler_does_not_sample_too_often():
61 | pytest.importorskip('yappi')
62 | # pytest-cov cannot detect a callback function registered by
63 | # :func:`sys.setprofile`.
64 | class fake_profiler(object):
65 | samples = []
66 | @classmethod
67 | def sample(cls, frame):
68 | cls.samples.append(frame)
69 | @classmethod
70 | def count_and_clear_samples(cls):
71 | count = len(cls.samples)
72 | del cls.samples[:]
73 | return count
74 | sampler = TracingSampler(0.1)
75 | sampler._profile(fake_profiler, None, None, None)
76 | assert fake_profiler.count_and_clear_samples() == 1
77 | sampler._profile(fake_profiler, None, None, None)
78 | assert fake_profiler.count_and_clear_samples() == 0
79 | spin(0.5)
80 | sampler._profile(fake_profiler, None, None, None)
81 | assert fake_profiler.count_and_clear_samples() == 1
82 |
83 |
84 | def test_not_sampler():
85 | with pytest.raises(TypeError):
86 | SamplingProfiler(sampler=123)
87 |
88 |
89 | def test_sample_1_depth():
90 | frame = sys._getframe()
91 | while frame.f_back is not None:
92 | frame = frame.f_back
93 | assert frame.f_back is None
94 | profiler = SamplingProfiler()
95 | profiler.sample(frame)
96 |
--------------------------------------------------------------------------------
/test/test_stats.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | try:
3 | import cPickle as pickle
4 | except ImportError:
5 | import pickle
6 | import sys
7 | from textwrap import dedent
8 | from types import CodeType
9 |
10 | import pytest
11 | from six import exec_, PY3
12 |
13 | from _utils import spin
14 | import profiling
15 | from profiling.sortkeys import \
16 | by_deep_time_per_call, by_name, by_own_hits, by_own_time_per_call
17 | from profiling.stats import \
18 | FlatFrozenStatistics, FrozenStatistics, RecordingStatistics, \
19 | spread_stats, Statistics
20 | from profiling.tracing import TracingProfiler
21 |
22 |
23 | def mock_code(name):
24 | """Makes a fake code object by name for built-in functions."""
25 | left_args = (0, 0, 0, 0, b'', (), (), (), '')
26 | if PY3:
27 | left_args = (0,) + left_args
28 | args = left_args + (name, 0, b'')
29 | return CodeType(*args)
30 |
31 |
32 | def test_stats():
33 | stats = Statistics(name='foo', filename='bar', lineno=42)
34 | assert stats.regular_name == 'foo'
35 | stats.module = 'baz'
36 | assert stats.regular_name == 'baz:foo'
37 | assert stats.deep_time_per_call == 0
38 | assert stats.own_time_per_call == 0
39 | stats.deep_time = 128
40 | stats.own_hits = 4
41 | assert stats.deep_time_per_call == 32
42 | assert stats.own_time_per_call == 32
43 | assert len(stats) == 0
44 | assert not list(stats)
45 |
46 |
47 | def test_repr():
48 | stats = Statistics(name='foo', own_hits=4, deep_time=128)
49 | assert repr(stats) == ""
50 | frozen = FrozenStatistics(name='foo', own_hits=4, deep_time=128,
51 | children=[])
52 | frozen.children.append(Statistics(name='baz', own_hits=1, deep_time=120))
53 | assert \
54 | repr(frozen) == \
55 | ""
56 |
57 |
58 | def test_hash():
59 | stats1 = Statistics(name='foo', filename='bar', lineno=42)
60 | stats2 = Statistics(name='baz', filename='bar', lineno=89)
61 | stats_dics = {stats1: 1, stats2: 2}
62 | assert stats_dics[stats1] == 1
63 | assert stats_dics[stats2] == 2
64 |
65 |
66 | def test_recording():
67 | # profiling is a module not code.
68 | # but inspect.getmodule() works like with code of a module.
69 | assert RecordingStatistics(profiling).module == 'profiling'
70 | # use discard_child.
71 | stats = RecordingStatistics()
72 | assert None not in stats
73 | stats.ensure_child(None)
74 | assert None in stats
75 | stats.discard_child(None)
76 | assert None not in stats
77 | stats.discard_child(None)
78 | assert None not in stats
79 |
80 |
81 | # def test_recording():
82 | # stats.wall = lambda: 10
83 | # stats.record_starting(0)
84 | # code = mock_code('foo')
85 | # stats = RecordingStatistics(code)
86 | # assert stats.name == 'foo'
87 | # assert stats.own_hits == 0
88 | # assert stats.deep_time == 0
89 | # stats.record_entering(100)
90 | # stats.record_leaving(200)
91 | # assert stats.own_hits == 1
92 | # assert stats.deep_time == 100
93 | # stats.record_entering(200)
94 | # stats.record_leaving(400)
95 | # assert stats.own_hits == 2
96 | # assert stats.deep_time == 300
97 | # code2 = mock_code('bar')
98 | # stats2 = RecordingStatistics(code2)
99 | # assert code2 not in stats
100 | # stats.add_child(code2, stats2)
101 | # assert code2 in stats
102 | # assert stats.get_child(code2) is stats2
103 | # assert len(stats) == 1
104 | # assert list(stats) == [stats2]
105 | # assert stats.deep_time == 300
106 | # assert stats.own_time == 300
107 | # stats2.record_entering(1000)
108 | # stats2.record_leaving(1004)
109 | # assert stats2.deep_time == 4
110 | # assert stats2.own_time == 4
111 | # assert stats.deep_time == 300
112 | # assert stats.own_time == 296
113 | # stats.clear()
114 | # assert len(stats) == 0
115 | # with pytest.raises(TypeError):
116 | # pickle.dumps(stats)
117 | # stats3 = stats.ensure_child(mock_code('baz'), VoidRecordingStatistics)
118 | # assert isinstance(stats3, VoidRecordingStatistics)
119 | # stats.wall = lambda: 2000
120 | # stats.record_stopping(400)
121 | # assert stats.cpu_time == 400
122 | # assert stats.wall_time == 1990
123 | # assert stats.cpu_usage == 400 / 1990.
124 |
125 |
126 | def test_pickle():
127 | stats = Statistics(name='ok')
128 | for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
129 | assert pickle.loads(pickle.dumps(stats, protocol)).name == 'ok'
130 |
131 |
132 | def test_frozen():
133 | code = mock_code('foo')
134 | stats = RecordingStatistics(code)
135 | stats.deep_time = 10
136 | stats.ensure_child(None)
137 | # RecordingStatistics are frozen at pickling.
138 | frozen_stats = pickle.loads(pickle.dumps(stats))
139 | assert frozen_stats.name == 'foo'
140 | assert frozen_stats.deep_time == 10
141 | assert len(frozen_stats) == 1
142 | restored_frozen_stats = pickle.loads(pickle.dumps(frozen_stats))
143 | assert restored_frozen_stats.name == 'foo'
144 | assert restored_frozen_stats.deep_time == 10
145 | assert len(restored_frozen_stats) == 1
146 |
147 |
148 | def test_flatten():
149 | stats = FrozenStatistics(children=[
150 | FrozenStatistics('foo', own_hits=10, children=[
151 | FrozenStatistics('foo', own_hits=20, children=[]),
152 | FrozenStatistics('bar', own_hits=30, children=[]),
153 | ]),
154 | FrozenStatistics('bar', own_hits=40, children=[]),
155 | FrozenStatistics('baz', own_hits=50, children=[]),
156 | ])
157 | flat_stats = FlatFrozenStatistics.flatten(stats)
158 | children = {stats.name: stats for stats in flat_stats}
159 | assert len(children) == 3
160 | assert children['foo'].own_hits == 30
161 | assert children['bar'].own_hits == 70
162 | assert children['baz'].own_hits == 50
163 |
164 |
165 | def test_spread_stats():
166 | stats = FrozenStatistics(children=[
167 | FrozenStatistics('foo', own_hits=10, children=[
168 | FrozenStatistics('foo', own_hits=20, children=[]),
169 | FrozenStatistics('bar', own_hits=30, children=[]),
170 | ]),
171 | FrozenStatistics('bar', own_hits=40, children=[]),
172 | FrozenStatistics('baz', own_hits=50, children=[]),
173 | ])
174 | descendants = list(spread_stats(stats))
175 | assert len(descendants) == 5
176 | assert descendants[0].name == 'foo'
177 | assert descendants[1].name == 'bar'
178 | assert descendants[2].name == 'baz'
179 | assert descendants[3].name == 'foo'
180 | assert descendants[4].name == 'bar'
181 |
182 |
183 | def test_sorting():
184 | stats = RecordingStatistics(mock_code('foo'))
185 | stats1 = RecordingStatistics(mock_code('bar'))
186 | stats2 = RecordingStatistics(mock_code('baz'))
187 | stats3 = RecordingStatistics(mock_code('qux'))
188 | stats.add_child(stats1.code, stats1)
189 | stats.add_child(stats2.code, stats2)
190 | stats.add_child(stats3.code, stats3)
191 | stats.deep_time = 100
192 | stats1.deep_time = 20
193 | stats1.own_hits = 3
194 | stats2.deep_time = 30
195 | stats2.own_hits = 2
196 | stats3.deep_time = 40
197 | stats3.own_hits = 4
198 | assert stats.sorted() == [stats3, stats2, stats1]
199 | assert stats.sorted(by_own_hits) == [stats3, stats1, stats2]
200 | assert stats.sorted(~by_own_hits) == [stats2, stats1, stats3]
201 | assert stats.sorted(by_deep_time_per_call) == [stats2, stats3, stats1]
202 | assert stats.sorted(by_own_time_per_call) == [stats2, stats3, stats1]
203 | assert stats.sorted(by_name) == [stats1, stats2, stats3]
204 |
205 |
206 | @pytest.fixture
207 | def deep_stats(depth=sys.getrecursionlimit(), skip_if_no_recursion_error=True):
208 | # Define a function with deep recursion.
209 | def x0(frames):
210 | frames.append(sys._getframe())
211 | locals_ = locals()
212 | for x in range(1, depth):
213 | code = dedent('''
214 | import sys
215 | def x%d(frames):
216 | frames.append(sys._getframe())
217 | x%d(frames)
218 | ''' % (x, x - 1))
219 | exec_(code, locals_)
220 | f = locals_['x%d' % (depth - 1)]
221 | frames = []
222 | try:
223 | f(frames)
224 | except RuntimeError:
225 | # Expected.
226 | pass
227 | else:
228 | # Maybe PyPy.
229 | if skip_if_no_recursion_error:
230 | pytest.skip('Recursion limit not exceeded')
231 | # Profile the deepest frame.
232 | profiler = TracingProfiler()
233 | profiler._profile(frames[-1], 'call', None)
234 | spin(0.5)
235 | profiler._profile(frames[-1], 'return', None)
236 | # Test with the result.
237 | stats, __, __ = profiler.result()
238 | return stats
239 |
240 |
241 | def test_recursion_limit(deep_stats):
242 | deepest_stats = list(spread_stats(deep_stats))[-1]
243 | assert deepest_stats.deep_time > 0
244 | # It exceeded the recursion limit until 6fe1b48.
245 | assert deep_stats.children[0].deep_time == deepest_stats.deep_time
246 | # Pickling.
247 | assert isinstance(deep_stats, RecordingStatistics)
248 | data = pickle.dumps(deep_stats)
249 | frozen_stats = pickle.loads(data)
250 | assert isinstance(frozen_stats, FrozenStatistics)
251 | deepest_frozen_stats = list(spread_stats(frozen_stats))[-1]
252 | assert deepest_stats.deep_time == deepest_frozen_stats.deep_time
253 |
254 |
255 | def test_deep_stats_dump_performance(benchmark):
256 | stats = deep_stats(100, skip_if_no_recursion_error=False)
257 | benchmark(lambda: pickle.dumps(stats))
258 |
259 |
260 | def test_deep_stats_load_performance(benchmark):
261 | stats = deep_stats(100, skip_if_no_recursion_error=False)
262 | data = pickle.dumps(stats)
263 | benchmark(lambda: pickle.loads(data))
264 |
265 |
266 | def test_shallow_stats_dump_performance(benchmark):
267 | stats = deep_stats(5, skip_if_no_recursion_error=False)
268 | benchmark(lambda: pickle.dumps(stats))
269 |
270 |
271 | def test_shallow_stats_load_performance(benchmark):
272 | stats = deep_stats(5, skip_if_no_recursion_error=False)
273 | data = pickle.dumps(stats)
274 | benchmark(lambda: pickle.loads(data))
275 |
--------------------------------------------------------------------------------
/test/test_timers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import re
3 | import sys
4 | import time
5 |
6 | import pytest
7 |
8 | from _utils import factorial, find_stats
9 | from profiling.__main__ import spawn_thread
10 | from profiling.tracing import TracingProfiler
11 | from profiling.tracing.timers import GreenletTimer, ThreadTimer
12 |
13 |
14 | # is it running on pypy?
15 | try:
16 | import __pypy__
17 | except ImportError:
18 | PYPY = False
19 | else:
20 | PYPY = True
21 | del __pypy__
22 |
23 |
24 | def _test_timer_with_threads(timer, sleep, spawn, join=lambda x: x.join()):
25 | def light():
26 | factorial(10)
27 | sleep(0.1)
28 | factorial(10)
29 | def heavy():
30 | factorial(10000)
31 | def profile(profiler):
32 | with profiler:
33 | c1 = spawn(light)
34 | c2 = spawn(heavy)
35 | for c in [c1, c2]:
36 | join(c)
37 | stat1 = find_stats(profiler.stats, 'light')
38 | stat2 = find_stats(profiler.stats, 'heavy')
39 | return (stat1, stat2)
40 | # using the default timer.
41 | # light() ends later than heavy(). its total time includes heavy's also.
42 | normal_profiler = TracingProfiler(base_frame=sys._getframe())
43 | stat1, stat2 = profile(normal_profiler)
44 | assert stat1.deep_time >= stat2.deep_time
45 | # using the given timer.
46 | # light() ends later than heavy() like the above case. but the total time
47 | # doesn't include heavy's. each contexts should have isolated cpu time.
48 | contextual_profiler = TracingProfiler(base_frame=sys._getframe(),
49 | timer=timer)
50 | stat1, stat2 = profile(contextual_profiler)
51 | assert stat1.deep_time < stat2.deep_time
52 |
53 |
54 | @pytest.mark.flaky(reruns=10)
55 | def test_thread_timer():
56 | if sys.version_info < (3, 3):
57 | pytest.importorskip('yappi')
58 | _test_timer_with_threads(ThreadTimer(), time.sleep, spawn_thread)
59 |
60 |
61 | @pytest.mark.xfail(PYPY, reason='greenlet.settrace() not available on PyPy.')
62 | def test_greenlet_timer_with_gevent():
63 | try:
64 | gevent = pytest.importorskip('gevent', '1')
65 | except ValueError:
66 | # gevent Alpha or Beta versions doesn't respect Semantic Versioning.
67 | # e.g. 1.1a1, 1.1b5, 1.1rc1
68 | gevent = pytest.importorskip('gevent')
69 | assert re.match(r'^1\.1([ab]|rc\d+)', gevent.__version__)
70 | _test_timer_with_threads(GreenletTimer(), gevent.sleep, gevent.spawn)
71 |
72 |
73 | @pytest.mark.xfail(PYPY, reason='greenlet.settrace() not available on PyPy.')
74 | def test_greenlet_timer_with_eventlet():
75 | eventlet = pytest.importorskip('eventlet', '0.15')
76 | _test_timer_with_threads(GreenletTimer(), eventlet.sleep, eventlet.spawn,
77 | eventlet.greenthread.GreenThread.wait)
78 |
--------------------------------------------------------------------------------
/test/test_tracing.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import sys
3 |
4 | import pytest
5 |
6 | from _utils import factorial, find_stats, foo
7 | from profiling.stats import RecordingStatistics
8 | from profiling.tracing import TracingProfiler
9 |
10 |
11 | def test_setprofile():
12 | profiler = TracingProfiler()
13 | assert sys.getprofile() is None
14 | with profiler:
15 | assert sys.getprofile() == profiler._profile
16 | assert sys.getprofile() is None
17 | sys.setprofile(lambda *x: x)
18 | with pytest.raises(RuntimeError):
19 | profiler.start()
20 | sys.setprofile(None)
21 |
22 |
23 | def test_profile():
24 | profiler = TracingProfiler()
25 | frame = foo()
26 | profiler._profile(frame, 'call', None)
27 | profiler._profile(frame, 'return', None)
28 | assert len(profiler.stats) == 1
29 | stats1 = find_stats(profiler.stats, 'foo')
30 | stats2 = find_stats(profiler.stats, 'bar')
31 | stats3 = find_stats(profiler.stats, 'baz')
32 | assert stats1.own_hits == 0
33 | assert stats2.own_hits == 0
34 | assert stats3.own_hits == 1
35 | assert stats1.deep_hits == 1
36 | assert stats2.deep_hits == 1
37 | assert stats3.deep_hits == 1
38 |
39 |
40 | def test_profiler():
41 | profiler = TracingProfiler(base_frame=sys._getframe())
42 | assert isinstance(profiler.stats, RecordingStatistics)
43 | stats, cpu_time, wall_time = profiler.result()
44 | assert len(stats) == 0
45 | with profiler:
46 | factorial(1000)
47 | factorial(10000)
48 | stats1 = find_stats(profiler.stats, 'factorial')
49 | stats2 = find_stats(profiler.stats, '__enter__')
50 | stats3 = find_stats(profiler.stats, '__exit__')
51 | assert stats1.deep_time != 0
52 | assert stats1.deep_time == stats1.own_time
53 | assert stats1.own_time > stats2.own_time
54 | assert stats1.own_time > stats3.own_time
55 | assert stats1.own_hits == 2
56 | assert stats2.own_hits == 0 # entering to __enter__() wasn't profiled.
57 | assert stats3.own_hits == 1
58 |
--------------------------------------------------------------------------------
/test/test_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from collections import deque
3 |
4 | import pytest
5 | import six
6 |
7 | from _utils import baz, foo, mock_code_names
8 | from profiling.utils import frame_stack, lazy_import, repr_frame, Runnable
9 |
10 |
11 | def test_runnable():
12 | # not implemented.
13 | runnable = Runnable()
14 | with pytest.raises(NotImplementedError):
15 | runnable.start()
16 | # implemented well.
17 | class Implementation(Runnable):
18 | step = 1
19 | def run(self):
20 | self.step = 2
21 | yield
22 | self.step = 3
23 | runnable = Implementation()
24 | assert runnable.step == 1
25 | assert not runnable.is_running()
26 | runnable.start()
27 | assert runnable.step == 2
28 | assert runnable.is_running()
29 | with pytest.raises(RuntimeError):
30 | runnable.start()
31 | runnable.stop()
32 | assert runnable.step == 3
33 | assert not runnable.is_running()
34 | with pytest.raises(RuntimeError):
35 | runnable.stop()
36 | # implemented not well.
37 | class NotYield(Runnable):
38 | def run(self):
39 | if False:
40 | yield
41 | runnable = NotYield()
42 | with pytest.raises(TypeError):
43 | runnable.start()
44 | class YieldSomething(Runnable):
45 | def run(self):
46 | yield 123
47 | runnable = YieldSomething()
48 | with pytest.raises(TypeError):
49 | runnable.start()
50 | class YieldTwice(Runnable):
51 | def run(self):
52 | yield
53 | yield
54 | runnable = YieldTwice()
55 | runnable.start()
56 | with pytest.raises(TypeError):
57 | runnable.stop()
58 |
59 |
60 | def test_frame_stack():
61 | def to_code_names(frames):
62 | code_names = deque()
63 | for frame in reversed(frames):
64 | code_name = frame.f_code.co_name
65 | if code_name not in mock_code_names:
66 | break
67 | code_names.appendleft(code_name)
68 | return list(code_names)
69 | baz_frame = foo()
70 | foo_frame = baz_frame.f_back.f_back
71 | frames = frame_stack(baz_frame)
72 | assert to_code_names(frames) == ['foo', 'bar', 'baz']
73 | # base frame.
74 | frames = frame_stack(baz_frame, base_frame=foo_frame)
75 | assert to_code_names(frames) == ['bar', 'baz']
76 | # ignored codes.
77 | frames = frame_stack(baz_frame, ignored_codes=[
78 | six.get_function_code(foo),
79 | six.get_function_code(baz),
80 | ])
81 | assert to_code_names(frames) == ['bar']
82 |
83 |
84 | def test_lazy_import():
85 | class MathHolder(object):
86 | math = lazy_import('math')
87 | assert MathHolder.math is __import__('math')
88 |
89 |
90 | def test_repr_frame():
91 | frame = foo()
92 | assert repr_frame(frame) == ':9'
93 | assert repr_frame(frame.f_back) == ':6'
94 |
--------------------------------------------------------------------------------
/test/test_viewer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from profiling.viewer import fmt
3 |
4 |
5 | def test_fmt():
6 | assert fmt.markup_percent(1.00) == ('danger', '100')
7 | assert fmt.markup_percent(0.80) == ('caution', '80.0')
8 | assert fmt.markup_percent(0.50) == ('warning', '50.0')
9 | assert fmt.markup_percent(0.20) == ('notice', '20.0')
10 | assert fmt.markup_percent(0.05) == (None, '5.00')
11 | assert fmt.markup_percent(0.00) == ('zero', '0.00')
12 | assert fmt.markup_percent(0.00, unit=True) == ('zero', '0.00%')
13 | assert fmt.markup_int(1.234) == (None, '1')
14 | assert fmt.markup_int(4.567) == (None, '5')
15 | assert fmt.markup_int(0) == ('zero', '0')
16 | assert fmt.markup_int_or_na(1.234) == (None, '1')
17 | assert fmt.markup_int_or_na(0) == ('zero', 'n/a')
18 | assert fmt.markup_time(0) == ('zero', '0')
19 | assert fmt.markup_time(0.123456) == ('msec', '123ms')
20 | assert fmt.markup_time(12.34567) == ('sec', '12.3sec')
21 |
22 |
23 | def test_format_int():
24 | assert fmt.format_int(0) == '0'
25 | assert fmt.format_int(123) == '123'
26 | assert fmt.format_int(12345) == '12.3k'
27 | assert fmt.format_int(-12345) == '-12.3k'
28 | assert fmt.format_int(99999999) == '100.0M'
29 | assert fmt.format_int(-99999999) == '-100.0M'
30 | assert fmt.format_int(999999999) == '1.0G'
31 | assert fmt.format_int(-999999999) == '-1.0G'
32 | assert fmt.format_int(1e255) == 'o/f'
33 | assert fmt.format_int(-1e255) == 'u/f'
34 |
35 |
36 | def test_format_int_or_na():
37 | assert fmt.format_int_or_na(0) == 'n/a'
38 | assert fmt.format_int_or_na(12345) == '12.3k'
39 |
40 |
41 | def test_format_time():
42 | assert fmt.format_time(0) == '0'
43 | assert fmt.format_time(0.000001) == '1us'
44 | assert fmt.format_time(0.000123) == '123us'
45 | assert fmt.format_time(0.012345) == '12ms'
46 | assert fmt.format_time(0.123456) == '123ms'
47 | assert fmt.format_time(1.234567) == '1.2sec'
48 | assert fmt.format_time(12.34567) == '12.3sec'
49 | assert fmt.format_time(123.4567) == '2m3s'
50 | assert fmt.format_time(6120.000) == '102m'
51 |
52 |
53 | def test_format_percent():
54 | assert fmt.format_percent(1) == '100'
55 | assert fmt.format_percent(0.999999) == '100'
56 | assert fmt.format_percent(0.9999) == '100'
57 | assert fmt.format_percent(0.988) == '98.8'
58 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = pypy, py27, py32, py33, py34, flake8
3 | minversion = 1.6.0
4 |
5 | [testenv:flake8]
6 | basepython = python3.4
7 | deps = flake8
8 | commands = flake8 profiling test setup.py -v --show-source --ignore=E301
9 |
10 | [testenv]
11 | deps = pytest
12 | # conditional settings require tox-1.8 or later.
13 | # py26: gevent
14 | # py27: gevent
15 | commands = py.test {posargs:-v}
16 |
--------------------------------------------------------------------------------