├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pylintrc ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev_requirements.txt ├── examples ├── guestbook.py └── permutations.py ├── package.json ├── requirements.txt ├── setup.py └── vprof ├── __init__.py ├── __main__.py ├── base_profiler.py ├── code_heatmap.py ├── flame_graph.py ├── memory_profiler.py ├── profiler.py ├── runner.py ├── stats_server.py ├── tests ├── __init__.py ├── base_profiler_test.py ├── code_heatmap_e2e.py ├── code_heatmap_test.py ├── flame_graph_e2e.py ├── flame_graph_test.py ├── memory_profiler_e2e.py ├── memory_profiler_test.py ├── profiler_e2e.py ├── profiler_test.py ├── runner_test.py └── test_pkg │ ├── __init__.py │ ├── __main__.py │ └── dummy_module.py └── ui ├── __tests__ ├── code_heatmap_test.js ├── common_test.js ├── flame_graph_test.js ├── memory_stats_test.js └── profiler_test.js ├── code_heatmap.js ├── color.js ├── common.js ├── css ├── code_heatmap.css ├── flame_graph.css ├── highlight.css ├── memory_stats.css ├── profiler.css ├── progress.gif └── vprof.css ├── favicon.ico ├── flame_graph.js ├── main.js ├── memory_stats.js ├── profile.html └── profiler.js /.eslintignore: -------------------------------------------------------------------------------- 1 | vprof/ui/vprof_min.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "sourceType": "script" 8 | }, 9 | "rules": { 10 | "camelcase": ["error", { "properties": "never" }], 11 | "curly": "error", 12 | "comma-spacing": ["error", { "before": false, "after": true }], 13 | "eqeqeq": "error", 14 | "indent": ["error", 2], 15 | "linebreak-style": ["error", "unix"], 16 | "no-cond-assign": ["error", "always"], 17 | "strict": ["error", "global"], 18 | "semi": "error", 19 | "max-len": ["error", 80, { "ignoreComments": true }] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: vprof 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python3 setup.py deps_install 22 | 23 | lint-python: 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9] 28 | steps: 29 | - uses: actions/checkout@v1 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v1 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | python setup.py deps_install 38 | - name: Run linter 39 | run: 40 | python setup.py lint_python 41 | 42 | lint-javascript: 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | python-version: [3.8] 47 | steps: 48 | - uses: actions/checkout@v1 49 | - name: Set up Python ${{ matrix.python-version }} 50 | uses: actions/setup-python@v1 51 | with: 52 | python-version: ${{ matrix.python-version }} 53 | - name: Install dependencies 54 | run: | 55 | python -m pip install --upgrade pip 56 | python setup.py deps_install 57 | - name: Run linter 58 | run: 59 | python setup.py lint_javascript 60 | 61 | unittest_python: 62 | runs-on: ubuntu-latest 63 | strategy: 64 | matrix: 65 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9] 66 | steps: 67 | - uses: actions/checkout@v1 68 | - name: Set up Python ${{ matrix.python-version }} 69 | uses: actions/setup-python@v1 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | - name: Install dependencies 73 | run: | 74 | python -m pip install --upgrade pip 75 | python setup.py deps_install 76 | - name: Run tests 77 | run: 78 | python setup.py test_python 79 | 80 | unittest_javascript: 81 | runs-on: ubuntu-latest 82 | strategy: 83 | matrix: 84 | python-version: [3.8] 85 | steps: 86 | - uses: actions/checkout@v1 87 | - name: Set up Python ${{ matrix.python-version }} 88 | uses: actions/setup-python@v1 89 | with: 90 | python-version: ${{ matrix.python-version }} 91 | - name: Install dependencies 92 | run: | 93 | python -m pip install --upgrade pip 94 | python setup.py deps_install 95 | - name: Run tests 96 | run: 97 | python setup.py test_javascript 98 | 99 | e2e_tests: 100 | runs-on: ubuntu-latest 101 | strategy: 102 | matrix: 103 | os: [ubuntu-latest, macos-latest, windows-latest] 104 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9] 105 | steps: 106 | - uses: actions/checkout@v1 107 | - name: Set up Python ${{ matrix.python-version }} 108 | uses: actions/setup-python@v1 109 | with: 110 | python-version: ${{ matrix.python-version }} 111 | - name: Install dependencies 112 | run: | 113 | python -m pip install --upgrade pip 114 | python setup.py deps_install 115 | - name: Run E2E tests 116 | run: 117 | python setup.py e2e_test 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | vprof/ui/vprof_min.js 3 | .DS_Store 4 | node_modules 5 | build 6 | dist 7 | vprof.egg-info 8 | .vscode 9 | .pypirc 10 | .venv -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=exec-used, wrong-import-order, fixme, 3 | invalid-name, no-member, too-many-arguments, too-many-locals, duplicate-code 4 | # Disable duplicate-code globally, since can't disable locally 5 | # https://github.com/PyCQA/pylint/issues/214 6 | [FORMAT] 7 | 8 | # Maximum number of characters on a single line. 9 | max-line-length=80 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to vprof 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | This is a set of guidelines for contributing to vprof. These are not rules! 6 | Feel free to propose changes to this document in a pull request. 7 | 8 | ## How can I contribute? 9 | 10 | ### Spreading the word 11 | * Tell your friends or co-workers about vprof. 12 | * Write a blog post about vprof. 13 | * Write a tutorial for vprof. 14 | 15 | ### Reporting bugs 16 | 17 | * Before submitting bug report, please check if the problem has already been 18 | reported. 19 | * Use clear and descriptive title for the issue. 20 | * Describe the exact steps to reproduce the problem. 21 | * Describe behavior you observed after following the steps. 22 | * Describe behavior you expect to see instead. 23 | * Include screenshots if it helps to demonstrate the problem. 24 | * Include version vprof, Python, name and version of OS you are using. 25 | 26 | ### Suggesting enhancements 27 | Before creating feature request, please check if the feature has already been 28 | requested or implemented. 29 | 30 | ### Contributing code 31 | * If you want to contribute a new feature, please create feature request first. Your new feature request can be out of vprof scope and might not be added to the project. 32 | * Use clear and descriptive title for the pull request. 33 | * New code should adhere to respective styleguides. 34 | * Add unit and integration tests if necessary. 35 | * All new code should be documented. 36 | 37 | ## Styleguide 38 | * PEP 8 for Python. 39 | * Check `.eslintrc` in the root folder of the project for Javascript styleguide. 40 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ##### Description 3 | 4 | ##### How to reproduce 5 | 6 | ##### Actual results 7 | 8 | ##### Expected results 9 | 10 | ##### Version and platform 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016, nvdv 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, 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, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include vprof/ui/profile.html 2 | include vprof/ui/vprof_min.js 3 | include vprof/ui/css/code_heatmap.css 4 | include vprof/ui/css/flame_graph.css 5 | include vprof/ui/css/memory_stats.css 6 | include vprof/ui/css/profiler.css 7 | include vprof/ui/css/vprof.css 8 | include vprof/ui/css/progress.gif 9 | include vprof/ui/favicon.ico 10 | include requirements.txt 11 | include dev_requirements.txt 12 | include LICENSE 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://img.shields.io/pypi/v/vprof.svg)](https://pypi.python.org/pypi/vprof/) 2 | 3 | # vprof 4 | 5 | vprof is a Python package providing rich and interactive visualizations for 6 | various Python program characteristics such as running time and memory usage. 7 | It supports Python 3.4+ and distributed under BSD license. 8 | 9 | The project is in active development and some of its features might not work as 10 | expected. 11 | 12 | ## Screenshots 13 | ![vprof-gif](http://i.imgur.com/ikBlfvQ.gif) 14 | 15 | ## Contributing 16 | All contributions are highly encouraged! You can add new features, 17 | report and fix existing bugs and write docs and tutorials. 18 | Feel free to open an issue or send a pull request! 19 | 20 | ## Prerequisites 21 | Dependencies to build `vprof` from source code: 22 | * Python 3.4+ 23 | * `pip` 24 | * `npm` >= 3.3.12 25 | 26 | `npm` is required to build `vprof` from sources only. 27 | 28 | ## Dependencies 29 | All Python and `npm` module dependencies are listed in `package.json` and 30 | `requirements.txt`. 31 | 32 | ## Installation 33 | `vprof` can be installed from PyPI 34 | 35 | ```sh 36 | pip install vprof 37 | ``` 38 | 39 | To build `vprof` from sources, clone this repository and execute 40 | 41 | ```sh 42 | python3 setup.py deps_install && python3 setup.py build_ui && python3 setup.py install 43 | ``` 44 | 45 | To install just `vprof` dependencies, run 46 | 47 | ```sh 48 | python3 setup.py deps_install 49 | ``` 50 | 51 | ## Usage 52 | 53 | ```sh 54 | vprof -c 55 | ``` 56 | `` is a combination of supported modes: 57 | 58 | * `c` - CPU flame graph ⚠️ **Not available for windows [#62](https://github.com/nvdv/vprof/issues/62)** 59 | 60 | Shows CPU flame graph for ``. 61 | 62 | * `p` - profiler 63 | 64 | Runs built-in Python profiler on `` and displays results. 65 | 66 | * `m` - memory graph 67 | 68 | Shows objects that are tracked by CPython GC and left in memory after code 69 | execution. Also shows process memory usage after execution of each line of ``. 70 | 71 | * `h` - code heatmap 72 | 73 | Displays all executed code of `` with line run times and execution counts. 74 | 75 | `` can be Python source file (e.g. `testscript.py`) or path to package 76 | (e.g. `myproject/test_package`). 77 | 78 | To run scripts with arguments use double quotes 79 | 80 | ```sh 81 | vprof -c cmh "testscript.py --foo --bar" 82 | ``` 83 | 84 | Modes can be combined 85 | 86 | ```sh 87 | vprof -c cm testscript.py 88 | ``` 89 | 90 | `vprof` can also profile functions. In order to do this, 91 | launch `vprof` in remote mode: 92 | 93 | ```sh 94 | vprof -r 95 | ``` 96 | 97 | `vprof` will open new tab in default web browser and then wait for stats. 98 | 99 | To profile a function run 100 | 101 | ```python 102 | from vprof import runner 103 | 104 | def foo(arg1, arg2): 105 | ... 106 | 107 | runner.run(foo, 'cmhp', args=(arg1, arg2), host='localhost', port=8000) 108 | ``` 109 | 110 | where `cmhp` is profiling mode, `host` and `port` are hostname and port of 111 | `vprof` server launched in remote mode. Obtained stats will be rendered in new 112 | tab of default web browser, opened by `vprof -r` command. 113 | 114 | `vprof` can save profile stats to file and render visualizations from 115 | previously saved file. 116 | 117 | ```sh 118 | vprof -c cmh src.py --output-file profile.json 119 | ``` 120 | 121 | writes profile to file and 122 | 123 | ```sh 124 | vprof --input-file profile.json 125 | ``` 126 | renders visualizations from previously saved file. 127 | 128 | Check `vprof -h` for full list of supported parameters. 129 | 130 | To show UI help, press `h` when visualizations are displayed. 131 | 132 | Also you can check `examples` directory for more profiling examples. 133 | 134 | ## Testing 135 | 136 | ```sh 137 | python3 setup.py test_python && python3 setup.py test_javascript && python3 setup.py e2e_test 138 | ``` 139 | 140 | ## License 141 | 142 | BSD 143 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | pylint>=2.0.0 2 | -------------------------------------------------------------------------------- /examples/guestbook.py: -------------------------------------------------------------------------------- 1 | """Remote profiling example with Flask. 2 | 3 | First of all launch vprof in remote mode: 4 | 5 | vprof -r 6 | 7 | and launch this script: 8 | 9 | python guestbook.py 10 | 11 | Then you can profile '/' and '/add' handlers: 12 | 13 | curl http://127.0.0.1:5000/profile/main 14 | 15 | and 16 | 17 | curl --data "name=foo&message=bar" http://127.0.0.1:5000/profile/add 18 | 19 | Check profiler_handler source code below for details. 20 | """ 21 | 22 | import contextlib 23 | import jinja2 24 | import flask 25 | import sqlite3 26 | 27 | from vprof import runner 28 | 29 | DB = '/tmp/guestbook.db' 30 | DB_SCHEMA = """ 31 | DROP TABLE IF EXISTS entry; 32 | CREATE TABLE entry ( 33 | id INTEGER PRIMARY KEY, 34 | name TEXT NOT NULL, 35 | message TEXT NOT NULL 36 | ); 37 | """ 38 | LAYOUT = """ 39 | 40 | Guestbook 41 | 42 |
43 | Name:
44 | Message:
45 |
46 |
47 |
    48 | {% for entry in entries %} 49 |
  • {{ entry.name }}

    50 | {{ entry.message | safe }} 51 |
  • 52 | {% endfor %} 53 |
54 | 55 | 56 | """ 57 | 58 | 59 | def connect_to_db(): 60 | """Establishes connection to SQLite.""" 61 | return sqlite3.connect(DB) 62 | 63 | 64 | def init_db(): 65 | """Initializes DB.""" 66 | with contextlib.closing(connect_to_db()) as db: 67 | db.cursor().executescript(DB_SCHEMA) 68 | db.commit() 69 | 70 | 71 | app = flask.Flask('guestbook') 72 | 73 | 74 | @app.before_request 75 | def before_request(): 76 | """Establishes SQLite connection before request.""" 77 | flask.g.db = connect_to_db() 78 | 79 | 80 | @app.teardown_request 81 | def teardown_request(exception): 82 | """Closes SQLite connection after request is processed.""" 83 | db = getattr(flask.g, 'db', None) 84 | if db is not None: 85 | db.close() 86 | 87 | 88 | @app.route('/') 89 | def show_guestbook(): 90 | """Returns all existing guestbook records.""" 91 | cursor = flask.g.db.execute( 92 | 'SELECT name, message FROM entry ORDER BY id DESC;') 93 | entries = [{'name': row[0], 'message': row[1]} for row in cursor.fetchall()] 94 | return jinja2.Template(LAYOUT).render(entries=entries) 95 | 96 | 97 | @app.route('/add', methods=['POST']) 98 | def add_entry(): 99 | """Adds single guestbook record.""" 100 | name, msg = flask.request.form['name'], flask.request.form['message'] 101 | flask.g.db.execute( 102 | 'INSERT INTO entry (name, message) VALUES (?, ?)', (name, msg)) 103 | flask.g.db.commit() 104 | return flask.redirect('/') 105 | 106 | 107 | @app.route('/profile/', methods=['GET', 'POST']) 108 | def profiler_handler(uri): 109 | """Profiler handler.""" 110 | # HTTP method should be GET. 111 | if uri == 'main': 112 | runner.run(show_guestbook, 'cmhp') 113 | # In this case HTTP method should be POST singe add_entry uses POST 114 | elif uri == 'add': 115 | runner.run(add_entry, 'cmhp') 116 | return flask.redirect('/') 117 | 118 | 119 | if __name__ == '__main__': 120 | init_db() 121 | app.run() 122 | -------------------------------------------------------------------------------- /examples/permutations.py: -------------------------------------------------------------------------------- 1 | """Permutations example. 2 | 3 | Example is taken from https://docs.python.org/3.5/library/itertools.html. 4 | 5 | To profile this with vprof run: 6 | vprof -c cmh permutations.py 7 | """ 8 | 9 | def permutations(iterable, r=None): 10 | pool = tuple(iterable) 11 | n = len(pool) 12 | r = n if r is None else r 13 | if r > n: 14 | return 15 | indices = list(range(n)) 16 | cycles = list(range(n, n-r, -1)) 17 | yield tuple(pool[i] for i in indices[:r]) 18 | while n: 19 | for i in reversed(range(r)): 20 | cycles[i] -= 1 21 | if cycles[i] == 0: 22 | indices[i:] = indices[i+1:] + indices[i:i+1] 23 | cycles[i] = n - i 24 | else: 25 | j = cycles[i] 26 | indices[i], indices[-j] = indices[-j], indices[i] 27 | yield tuple(pool[i] for i in indices[:r]) 28 | break 29 | else: 30 | return 31 | 32 | print(list(permutations('ABCDEFGED', 2))) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vprof-ui", 3 | "author": "nvdv", 4 | "main": "vprof/ui/main.js", 5 | "dependencies": { 6 | "browserify": "13", 7 | "browserify-css": "latest", 8 | "d3": "4.9.1", 9 | "eslint": "latest", 10 | "highlight.js": "latest", 11 | "jest": "18.0.0", 12 | "uglify-es": "3.0.24", 13 | "watchify": "3.7.0" 14 | }, 15 | "scripts": { 16 | "lint": "eslint vprof/ui", 17 | "build": "browserify -g browserify-css vprof/ui/main.js | uglifyjs -o vprof/ui/vprof_min.js", 18 | "watch": "watchify vprof/ui/main.js -o vprof/ui/vprof_min.js -v", 19 | "test": "jest" 20 | }, 21 | "browserify": { 22 | "transform": [ 23 | "browserify-css" 24 | ] 25 | }, 26 | "license": "BSD-2-Clause", 27 | "repository": { 28 | "type": "git", 29 | "url": "http://github.com/nvdv/vprof.git" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil>=3 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script for vprof.""" 2 | import glob 3 | import re 4 | import shlex 5 | import subprocess 6 | import unittest 7 | 8 | import pkg_resources 9 | from distutils import cmd 10 | from setuptools import setup 11 | from setuptools.command.install import install 12 | 13 | 14 | class RunUnittestsBackendCommand(cmd.Command): 15 | """Class that runs backend unit tests.""" 16 | description = 'Run backend unittests' 17 | user_options = [] 18 | 19 | def initialize_options(self): 20 | pass 21 | 22 | def finalize_options(self): 23 | pass 24 | 25 | def run(self): 26 | suite = unittest.TestLoader().discover( 27 | 'vprof/tests/.', pattern="*_test.py") 28 | unittest.TextTestRunner(verbosity=2, buffer=True).run(suite) 29 | 30 | 31 | class RunUnittestsFrontendCommand(cmd.Command): 32 | """Class that runs frontend unit tests.""" 33 | description = 'Run frontend unittests' 34 | user_options = [] 35 | 36 | def initialize_options(self): 37 | pass 38 | 39 | def finalize_options(self): 40 | pass 41 | 42 | def run(self): 43 | subprocess.check_call(shlex.split('npm run test')) 44 | 45 | 46 | class RunEndToEndTestCommand(cmd.Command): 47 | """Class that runs end-to-end tests.""" 48 | description = 'Run all end to end tests' 49 | user_options = [] 50 | 51 | def initialize_options(self): 52 | pass 53 | 54 | def finalize_options(self): 55 | pass 56 | 57 | def run(self): 58 | suite = unittest.TestLoader().discover( 59 | 'vprof/tests/.', pattern="*_e2e.py") 60 | unittest.TextTestRunner(verbosity=2, buffer=True).run(suite) 61 | 62 | 63 | class RunLintBackendCommand(cmd.Command): 64 | """Class that runs Python linter.""" 65 | description = 'Run Python linter' 66 | user_options = [] 67 | 68 | def initialize_options(self): 69 | pass 70 | 71 | def finalize_options(self): 72 | pass 73 | 74 | def run(self): 75 | subprocess.check_call(shlex.split( 76 | 'pylint --reports=n --rcfile=.pylintrc ' + ' '.join( 77 | glob.glob('vprof/*.py')))) 78 | subprocess.check_call(shlex.split( 79 | 'pylint --reports=n --rcfile=.pylintrc ' + ' '.join( 80 | glob.glob('vprof/tests/*.py')))) 81 | 82 | 83 | class RunLintFrontendCommand(cmd.Command): 84 | """Class that runs Javascript linter.""" 85 | description = 'Run Javascript linter' 86 | user_options = [] 87 | 88 | def initialize_options(self): 89 | pass 90 | 91 | def finalize_options(self): 92 | pass 93 | 94 | def run(self): 95 | subprocess.check_call(shlex.split('npm run lint')) 96 | 97 | 98 | class RunCleanCommand(cmd.Command): 99 | """Class that runs cleanup command.""" 100 | description = 'Clean temporary files up' 101 | user_options = [] 102 | 103 | def initialize_options(self): 104 | pass 105 | 106 | def finalize_options(self): 107 | pass 108 | 109 | def run(self): 110 | subprocess.check_output( 111 | shlex.split('rm -rf vprof/ui/vprof_min.js')) 112 | 113 | 114 | class RunDepsInstallCommand(cmd.Command): 115 | """Class that installs dependencies.""" 116 | description = 'Install dependencies' 117 | user_options = [] 118 | 119 | def initialize_options(self): 120 | pass 121 | 122 | def finalize_options(self): 123 | pass 124 | 125 | def run(self): 126 | subprocess.check_call( 127 | ["python3", '-m', 'pip', 'install', '-r', 'requirements.txt']) 128 | subprocess.check_call( 129 | ["python3", '-m', 'pip', 'install', '-r', 'dev_requirements.txt']) 130 | subprocess.check_call(shlex.split('npm install')) 131 | 132 | 133 | class VProfBuild(install): 134 | """Class that represents UI build command.""" 135 | def run(self): 136 | subprocess.check_call(shlex.split('npm run build')) 137 | 138 | 139 | class VProfInstall(install): 140 | """Class that represents install command.""" 141 | def run(self): 142 | install.run(self) 143 | 144 | 145 | def get_vprof_version(filename): 146 | """Returns actual version specified in filename.""" 147 | with open(filename) as src_file: 148 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 149 | src_file.read(), re.M) 150 | if version_match: 151 | return version_match.group(1) 152 | raise RuntimeError('Unable to find version info.') 153 | 154 | 155 | def get_description(): 156 | """Reads README.md file.""" 157 | with open('README.md') as readme_file: 158 | return readme_file.read() 159 | 160 | 161 | def get_requirements(): 162 | """Reads package dependencies.""" 163 | with open('requirements.txt') as fp: 164 | return [str(r) for r in pkg_resources.parse_requirements(fp)] 165 | 166 | 167 | setup( 168 | name='vprof', 169 | version=get_vprof_version('vprof/__main__.py'), 170 | packages=['vprof'], 171 | description="Visual profiler for Python", 172 | url='http://github.com/nvdv/vprof', 173 | license='BSD', 174 | author='nvdv', 175 | author_email='aflatnine@gmail.com', 176 | include_package_data=True, 177 | keywords=['debugging', 'profiling'], 178 | entry_points={ 179 | 'console_scripts': [ 180 | 'vprof = vprof.__main__:main' 181 | ] 182 | }, 183 | classifiers=[ 184 | 'Development Status :: 3 - Alpha', 185 | 'Environment :: Web Environment', 186 | 'Intended Audience :: Developers', 187 | 'Programming Language :: Python', 188 | 'Programming Language :: Python :: 3', 189 | 'Topic :: Software Development', 190 | ], 191 | install_requires=get_requirements(), 192 | long_description=get_description(), 193 | long_description_content_type="text/markdown", 194 | cmdclass={ 195 | 'test_python': RunUnittestsBackendCommand, 196 | 'test_javascript': RunUnittestsFrontendCommand, 197 | 'e2e_test': RunEndToEndTestCommand, 198 | 'lint_python': RunLintBackendCommand, 199 | 'lint_javascript': RunLintFrontendCommand, 200 | 'deps_install': RunDepsInstallCommand, 201 | 'build_ui': VProfBuild, 202 | 'install': VProfInstall, 203 | }, 204 | ) 205 | -------------------------------------------------------------------------------- /vprof/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvdv/vprof/99bb5cd5691a5bfbca23c14ad3b70a7bca6e7ac7/vprof/__init__.py -------------------------------------------------------------------------------- /vprof/__main__.py: -------------------------------------------------------------------------------- 1 | """Main module of the profiler.""" 2 | # pylint: disable=wrong-import-position 3 | import builtins 4 | import os 5 | import psutil 6 | 7 | # Take process RSS in order to compute profiler memory overhead. 8 | builtins.initial_rss_size = psutil.Process(os.getpid()).memory_info().rss 9 | 10 | import argparse 11 | import json 12 | import sys 13 | 14 | from vprof import runner 15 | from vprof import stats_server 16 | 17 | __version__ = '0.38' 18 | 19 | _PROGRAN_NAME = 'vprof' 20 | _MODULE_DESC = 'Visual profiler for Python' 21 | _HOST, _PORT = 'localhost', 8000 22 | _CONFIG_DESC = ( 23 | """profile program SRC with configuration CONFIG 24 | available CONFIG options 25 | c - flame graph 26 | m - memory graph 27 | h - code heatmap""") 28 | _ERR_CODES = { 29 | 'ambiguous_configuration': 1, 30 | 'bad_option': 2, 31 | 'input_file_error': 3 32 | } 33 | 34 | 35 | def main(): 36 | """Main function of the module.""" 37 | parser = argparse.ArgumentParser( 38 | prog=_PROGRAN_NAME, description=_MODULE_DESC, 39 | formatter_class=argparse.RawTextHelpFormatter) 40 | launch_modes = parser.add_mutually_exclusive_group(required=True) 41 | launch_modes.add_argument('-r', '--remote', dest='remote', 42 | action='store_true', default=False, 43 | help='launch in remote mode') 44 | launch_modes.add_argument('-i', '--input-file', dest='input_file', 45 | type=str, default='', 46 | help='render UI from file') 47 | launch_modes.add_argument('-c', '--config', nargs=2, dest='config', 48 | help=_CONFIG_DESC, metavar=('CONFIG', 'SRC')) 49 | parser.add_argument('-H', '--host', dest='host', default=_HOST, type=str, 50 | help='set internal webserver host') 51 | parser.add_argument('-p', '--port', dest='port', default=_PORT, type=int, 52 | help='set internal webserver port') 53 | parser.add_argument('-n', '--no-browser', dest='dont_start_browser', 54 | action='store_true', default=False, 55 | help="don't start browser automatically") 56 | parser.add_argument('-o', '--output-file', dest='output_file', 57 | type=str, default='', help='save profile to file') 58 | parser.add_argument('--debug', dest='debug_mode', 59 | action='store_true', default=False, 60 | help="don't suppress error messages") 61 | parser.add_argument('--version', action='version', 62 | version='vprof %s' % __version__) 63 | args = parser.parse_args() 64 | 65 | # Render UI from file. 66 | if args.input_file: 67 | with open(args.input_file) as ifile: 68 | saved_stats = json.loads(ifile.read()) 69 | if saved_stats['version'] != __version__: 70 | print('Incorrect profiler version - %s. %s is required.' % ( 71 | saved_stats['version'], __version__)) 72 | sys.exit(_ERR_CODES['input_file_error']) 73 | stats_server.start(args.host, args.port, saved_stats, 74 | args.dont_start_browser, args.debug_mode) 75 | # Launch in remote mode. 76 | elif args.remote: 77 | stats_server.start(args.host, args.port, {}, 78 | args.dont_start_browser, args.debug_mode) 79 | # Profiler mode. 80 | else: 81 | config, source = args.config 82 | try: 83 | program_stats = runner.run_profilers(source, config, verbose=True) 84 | except runner.AmbiguousConfigurationError: 85 | print('Profiler configuration %s is ambiguous. ' 86 | 'Please, remove duplicates.' % config) 87 | sys.exit(_ERR_CODES['ambiguous_configuration']) 88 | except runner.BadOptionError as exc: 89 | print(exc) 90 | sys.exit(_ERR_CODES['bad_option']) 91 | 92 | if args.output_file: 93 | with open(args.output_file, 'w') as outfile: 94 | program_stats['version'] = __version__ 95 | outfile.write(json.dumps(program_stats, indent=2)) 96 | else: 97 | stats_server.start( 98 | args.host, args.port, program_stats, 99 | args.dont_start_browser, args.debug_mode) 100 | 101 | if __name__ == "__main__": 102 | main() 103 | -------------------------------------------------------------------------------- /vprof/base_profiler.py: -------------------------------------------------------------------------------- 1 | """Base class of a profiler wrapper.""" 2 | import inspect 3 | import multiprocessing 4 | import os 5 | import pkgutil 6 | import sys 7 | import zlib 8 | 9 | 10 | def get_pkg_module_names(package_path): 11 | """Returns module filenames from package. 12 | 13 | Args: 14 | package_path: Path to Python package. 15 | Returns: 16 | A set of module filenames. 17 | """ 18 | module_names = set() 19 | for fobj, modname, _ in pkgutil.iter_modules(path=[package_path]): 20 | filename = os.path.join(fobj.path, '%s.py' % modname) 21 | if os.path.exists(filename): 22 | module_names.add(os.path.abspath(filename)) 23 | return module_names 24 | 25 | 26 | def hash_name(name): 27 | """Computes hash of the name.""" 28 | return zlib.adler32(name.encode('utf-8')) 29 | 30 | 31 | class ProcessWithException(multiprocessing.Process): 32 | """Process subclass that propagates exceptions to parent process. 33 | 34 | Also handles sending function output to parent process. 35 | Args: 36 | parent_conn: Parent end of multiprocessing.Pipe. 37 | child_conn: Child end of multiprocessing.Pipe. 38 | result: Result of the child process. 39 | """ 40 | 41 | def __init__(self, result, *args, **kwargs): 42 | super().__init__(*args, **kwargs) 43 | self.parent_conn, self.child_conn = multiprocessing.Pipe() 44 | self.result = result 45 | 46 | def run(self): 47 | try: 48 | self.result.update( 49 | self._target(*self._args, **self._kwargs)) 50 | self.child_conn.send(None) 51 | except Exception as exc: # pylint: disable=broad-except 52 | self.child_conn.send(exc) 53 | 54 | @property 55 | def exception(self): 56 | """Returns exception from child process.""" 57 | return self.parent_conn.recv() 58 | 59 | @property 60 | def output(self): 61 | """Returns target function output.""" 62 | return self.result._getvalue() # pylint: disable=protected-access 63 | 64 | 65 | def run_in_separate_process(func, *args, **kwargs): 66 | """Runs function in separate process. 67 | 68 | This function is used instead of a decorator, since Python multiprocessing 69 | module can't serialize decorated function on all platforms. 70 | """ 71 | manager = multiprocessing.Manager() 72 | manager_dict = manager.dict() 73 | process = ProcessWithException( 74 | manager_dict, target=func, args=args, kwargs=kwargs) 75 | process.start() 76 | process.join() 77 | exc = process.exception 78 | if exc: 79 | raise exc 80 | return process.output 81 | 82 | 83 | class BaseProfiler: 84 | """Base class for a profiler wrapper.""" 85 | 86 | def __init__(self, run_object): 87 | """Initializes profiler. 88 | 89 | Args: 90 | run_object: object to be profiled. 91 | """ 92 | run_obj_type = self.get_run_object_type(run_object) 93 | if run_obj_type == 'module': 94 | self.init_module(run_object) 95 | elif run_obj_type == 'package': 96 | self.init_package(run_object) 97 | else: 98 | self.init_function(run_object) 99 | 100 | @staticmethod 101 | def get_run_object_type(run_object): 102 | """Determines run object type.""" 103 | if isinstance(run_object, tuple): 104 | return 'function' 105 | run_object, _, _ = run_object.partition(' ') 106 | if os.path.isdir(run_object): 107 | return 'package' 108 | return 'module' 109 | 110 | def init_module(self, run_object): 111 | """Initializes profiler with a module.""" 112 | self.profile = self.profile_module 113 | self._run_object, _, self._run_args = run_object.partition(' ') 114 | self._object_name = '%s (module)' % self._run_object 115 | self._globs = { 116 | '__file__': self._run_object, 117 | '__name__': '__main__', 118 | '__package__': None, 119 | } 120 | program_path = os.path.dirname(self._run_object) 121 | if sys.path[0] != program_path: 122 | sys.path.insert(0, program_path) 123 | self._replace_sysargs() 124 | 125 | def init_package(self, run_object): 126 | """Initializes profiler with a package.""" 127 | self.profile = self.profile_package 128 | self._run_object, _, self._run_args = run_object.partition(' ') 129 | self._object_name = '%s (package)' % self._run_object 130 | self._replace_sysargs() 131 | 132 | def init_function(self, run_object): 133 | """Initializes profiler with a function.""" 134 | self.profile = self.profile_function 135 | self._run_object, self._run_args, self._run_kwargs = run_object 136 | filename = inspect.getsourcefile(self._run_object) 137 | self._object_name = '%s @ %s (function)' % ( 138 | self._run_object.__name__, filename) 139 | 140 | def _replace_sysargs(self): 141 | """Replaces sys.argv with proper args to pass to script.""" 142 | sys.argv[:] = [self._run_object] 143 | if self._run_args: 144 | sys.argv += self._run_args.split() 145 | 146 | def profile_package(self): 147 | """Profiles package specified by filesystem path. 148 | 149 | Runs object self._run_object as a package specified by filesystem path. 150 | Must be overridden. 151 | """ 152 | raise NotImplementedError 153 | 154 | def profile_module(self): 155 | """Profiles a module. 156 | 157 | Runs object self._run_object as a Python module. 158 | Must be overridden. 159 | """ 160 | raise NotImplementedError 161 | 162 | def profile_function(self): 163 | """Profiles a function. 164 | 165 | Runs object self._run_object as a Python function. 166 | Must be overridden. 167 | """ 168 | raise NotImplementedError 169 | 170 | def run(self): 171 | """Runs a profiler and returns collected stats.""" 172 | return self.profile() 173 | -------------------------------------------------------------------------------- /vprof/code_heatmap.py: -------------------------------------------------------------------------------- 1 | """Code heatmap module.""" 2 | import inspect 3 | import fnmatch 4 | import os 5 | import runpy 6 | import sys 7 | import time 8 | 9 | from collections import defaultdict 10 | from collections import deque 11 | from vprof import base_profiler 12 | 13 | _STDLIB_PATHS = [ 14 | os.path.abspath(path) for path in sys.path 15 | if os.path.isdir(path) and path.startswith(sys.prefix)] 16 | 17 | 18 | def check_standard_dir(module_path): 19 | """Checks whether path belongs to standard library or installed modules.""" 20 | if 'site-packages' in module_path: 21 | return True 22 | for stdlib_path in _STDLIB_PATHS: 23 | if fnmatch.fnmatchcase(module_path, stdlib_path + '*'): 24 | return True 25 | return False 26 | 27 | 28 | class _CodeHeatmapCalculator: 29 | """Calculates Python code heatmap. 30 | 31 | Class that contains all logic related to calculating code heatmap 32 | for a Python program. 33 | """ 34 | 35 | def __init__(self): 36 | self.original_trace_function = sys.gettrace() 37 | self.prev_lineno = None 38 | self.prev_timestamp = None 39 | self.prev_path = None 40 | self.lines = deque() 41 | self._execution_count = defaultdict(lambda: defaultdict(int)) 42 | self._heatmap = defaultdict(lambda: defaultdict(float)) 43 | 44 | def __enter__(self): 45 | """Enables heatmap calculator.""" 46 | sys.settrace(self.record_line) 47 | return self 48 | 49 | def __exit__(self, exc_type, exc_val, exc_tbf): 50 | """Disables heatmap calculator.""" 51 | sys.settrace(self.original_trace_function) 52 | if self.prev_timestamp: 53 | runtime = time.time() - self.prev_timestamp 54 | self.lines.append([self.prev_path, self.prev_lineno, runtime]) 55 | 56 | def record_line(self, frame, event, arg): # pylint: disable=unused-argument 57 | """Records line execution time.""" 58 | if event == 'line': 59 | if self.prev_timestamp: 60 | runtime = time.time() - self.prev_timestamp 61 | self.lines.append([self.prev_path, self.prev_lineno, runtime]) 62 | self.prev_lineno = frame.f_lineno 63 | self.prev_path = frame.f_code.co_filename 64 | self.prev_timestamp = time.time() 65 | return self.record_line 66 | 67 | @property 68 | def lines_without_stdlib(self): 69 | """Filters code from standard library from self.lines.""" 70 | prev_line = None 71 | current_module_path = inspect.getabsfile(inspect.currentframe()) 72 | for module_path, lineno, runtime in self.lines: 73 | module_abspath = os.path.abspath(module_path) 74 | if not prev_line: 75 | prev_line = [module_abspath, lineno, runtime] 76 | else: 77 | if (not check_standard_dir(module_path) and 78 | module_abspath != current_module_path): 79 | yield prev_line 80 | prev_line = [module_abspath, lineno, runtime] 81 | else: 82 | prev_line[2] += runtime 83 | yield prev_line 84 | 85 | def fill_heatmap(self): 86 | """Fills code heatmap and execution count dictionaries.""" 87 | for module_path, lineno, runtime in self.lines_without_stdlib: 88 | self._execution_count[module_path][lineno] += 1 89 | self._heatmap[module_path][lineno] += runtime 90 | 91 | @property 92 | def heatmap(self): 93 | """Returns heatmap with absolute path names.""" 94 | if not self._heatmap: 95 | self.fill_heatmap() 96 | return self._heatmap 97 | 98 | @property 99 | def execution_count(self): 100 | """Returns execution count map with absolute path names.""" 101 | if not self._execution_count: 102 | self.fill_heatmap() 103 | return self._execution_count 104 | 105 | 106 | class CodeHeatmapProfiler(base_profiler.BaseProfiler): 107 | """Code heatmap wrapper.""" 108 | 109 | SKIP_LINES = 10 110 | MIN_SKIP_SIZE = 100 111 | 112 | def _calc_skips(self, heatmap, num_lines): 113 | """Calculates skip map for large sources. 114 | Skip map is a list of tuples where first element of tuple is a line 115 | number and second is a length of the skip region: 116 | [(1, 10), (15, 10)] means skipping 10 lines after line 1 and 117 | 10 lines after line 15. 118 | """ 119 | if num_lines < self.MIN_SKIP_SIZE: 120 | return [] 121 | skips, prev_line = [], 0 122 | for line in sorted(heatmap): 123 | curr_skip = line - prev_line - 1 124 | if curr_skip > self.SKIP_LINES: 125 | skips.append((prev_line, curr_skip)) 126 | prev_line = line 127 | if num_lines - prev_line > self.SKIP_LINES: 128 | skips.append((prev_line, num_lines - prev_line)) 129 | return skips 130 | 131 | @staticmethod 132 | def _skip_lines(src_code, skip_map): 133 | """Skips lines in src_code specified by a skip map.""" 134 | if not skip_map: 135 | return [['line', j + 1, l] for j, l in enumerate(src_code)] 136 | code_with_skips, i = [], 0 137 | for line, length in skip_map: 138 | code_with_skips.extend( 139 | ['line', i + j + 1, l] for j, l in enumerate(src_code[i:line])) 140 | if (code_with_skips 141 | and code_with_skips[-1][0] == 'skip'): # Merge skips. 142 | code_with_skips[-1][1] += length 143 | else: 144 | code_with_skips.append(['skip', length]) 145 | i = line + length 146 | code_with_skips.extend( 147 | ['line', i + j + 1, l] for j, l in enumerate(src_code[i:])) 148 | return code_with_skips 149 | 150 | def _profile_package(self): 151 | """Calculates heatmap for a package.""" 152 | with _CodeHeatmapCalculator() as prof: 153 | try: 154 | runpy.run_path(self._run_object, run_name='__main__') 155 | except SystemExit: 156 | pass 157 | 158 | heatmaps = [] 159 | for filename, heatmap in prof.heatmap.items(): 160 | if os.path.isfile(filename): 161 | heatmaps.append( 162 | self._format_heatmap( 163 | filename, heatmap, prof.execution_count[filename])) 164 | 165 | run_time = sum(heatmap['runTime'] for heatmap in heatmaps) 166 | return { 167 | 'objectName': self._run_object, 168 | 'runTime': run_time, 169 | 'heatmaps': heatmaps 170 | } 171 | 172 | def profile_package(self): 173 | """Runs package profiler in a separate process.""" 174 | return base_profiler.run_in_separate_process(self._profile_package) 175 | 176 | def _format_heatmap(self, filename, heatmap, execution_count): 177 | """Formats heatmap for UI.""" 178 | with open(filename) as src_file: 179 | file_source = src_file.read().split('\n') 180 | skip_map = self._calc_skips(heatmap, len(file_source)) 181 | run_time = sum(time for time in heatmap.values()) 182 | return { 183 | 'name': filename, 184 | 'heatmap': heatmap, 185 | 'executionCount': execution_count, 186 | 'srcCode': self._skip_lines(file_source, skip_map), 187 | 'runTime': run_time 188 | } 189 | 190 | def _profile_module(self): 191 | """Calculates heatmap for a module.""" 192 | with open(self._run_object, 'r') as srcfile: 193 | src_code = srcfile.read() 194 | code = compile(src_code, self._run_object, 'exec') 195 | try: 196 | with _CodeHeatmapCalculator() as prof: 197 | exec(code, self._globs, None) 198 | except SystemExit: 199 | pass 200 | 201 | heatmaps = [] 202 | for filename, heatmap in prof.heatmap.items(): 203 | if os.path.isfile(filename): 204 | heatmaps.append( 205 | self._format_heatmap( 206 | filename, heatmap, prof.execution_count[filename])) 207 | 208 | run_time = sum(heatmap['runTime'] for heatmap in heatmaps) 209 | return { 210 | 'objectName': self._run_object, 211 | 'runTime': run_time, 212 | 'heatmaps': heatmaps 213 | } 214 | 215 | def profile_module(self): 216 | """Runs module profiler in a separate process.""" 217 | return base_profiler.run_in_separate_process(self._profile_module) 218 | 219 | def profile_function(self): 220 | """Calculates heatmap for a function.""" 221 | with _CodeHeatmapCalculator() as prof: 222 | result = self._run_object(*self._run_args, **self._run_kwargs) 223 | code_lines, start_line = inspect.getsourcelines(self._run_object) 224 | 225 | source_lines = [] 226 | for line in code_lines: 227 | source_lines.append(('line', start_line, line)) 228 | start_line += 1 229 | 230 | filename = os.path.abspath(inspect.getsourcefile(self._run_object)) 231 | heatmap = prof.heatmap[filename] 232 | run_time = sum(time for time in heatmap.values()) 233 | return { 234 | 'objectName': self._object_name, 235 | 'runTime': run_time, 236 | 'result': result, 237 | 'timestamp': int(time.time()), 238 | 'heatmaps': [{ 239 | 'name': self._object_name, 240 | 'heatmap': heatmap, 241 | 'executionCount': prof.execution_count[filename], 242 | 'srcCode': source_lines, 243 | 'runTime': run_time 244 | }] 245 | } 246 | -------------------------------------------------------------------------------- /vprof/flame_graph.py: -------------------------------------------------------------------------------- 1 | """Flame graph module.""" 2 | import inspect 3 | import runpy 4 | import signal 5 | import time 6 | 7 | from collections import defaultdict 8 | from vprof import base_profiler 9 | 10 | _SAMPLE_INTERVAL = 0.001 11 | 12 | 13 | class _StatProfiler: 14 | """Statistical profiler. 15 | 16 | Samples call stack at regular intervals specified by _SAMPLE_INTERVAL. 17 | """ 18 | 19 | def __init__(self): 20 | self._stats = defaultdict(int) 21 | self._start_time = None 22 | self.base_frame = None 23 | self.run_time = None 24 | 25 | def __enter__(self): 26 | """Enables statistical profiler.""" 27 | signal.signal(signal.SIGPROF, self.sample) 28 | signal.setitimer(signal.ITIMER_PROF, _SAMPLE_INTERVAL) 29 | self._start_time = time.time() 30 | return self 31 | 32 | def __exit__(self, exc_type, exc_val, exc_tbf): 33 | """Disables statistical profiler.""" 34 | self.run_time = time.time() - self._start_time 35 | signal.setitimer(signal.ITIMER_PROF, 0) 36 | 37 | def sample(self, signum, frame): #pylint: disable=unused-argument 38 | """Samples current stack and writes result in self._stats. 39 | 40 | Args: 41 | signum: Signal that activates handler. 42 | frame: Frame on top of the stack when signal is handled. 43 | """ 44 | stack = [] 45 | while frame and frame != self.base_frame: 46 | stack.append(( 47 | frame.f_code.co_name, 48 | frame.f_code.co_filename, 49 | frame.f_code.co_firstlineno)) 50 | frame = frame.f_back 51 | self._stats[tuple(stack)] += 1 52 | signal.setitimer(signal.ITIMER_PROF, _SAMPLE_INTERVAL) 53 | 54 | @staticmethod 55 | def _insert_stack(stack, sample_count, call_tree): 56 | """Inserts a stack into the call tree. 57 | 58 | Args: 59 | stack: Call stack. 60 | sample_count: Sample count of call stack. 61 | call_tree: Call tree. 62 | """ 63 | curr_level = call_tree 64 | for func in stack: 65 | next_level_index = { 66 | node['stack']: node for node in curr_level['children']} 67 | if func not in next_level_index: 68 | new_node = {'stack': func, 'children': [], 'sampleCount': 0} 69 | curr_level['children'].append(new_node) 70 | curr_level = new_node 71 | else: 72 | curr_level = next_level_index[func] 73 | curr_level['sampleCount'] = sample_count 74 | 75 | def _fill_sample_count(self, node): 76 | """Counts and fills sample counts inside call tree.""" 77 | node['sampleCount'] += sum( 78 | self._fill_sample_count(child) for child in node['children']) 79 | return node['sampleCount'] 80 | 81 | @staticmethod 82 | def _get_percentage(sample_count, total_samples): 83 | """Return percentage of sample_count in total_samples.""" 84 | if total_samples != 0: 85 | return 100 * round(float(sample_count) / total_samples, 3) 86 | return 0.0 87 | 88 | def _format_tree(self, node, total_samples): 89 | """Reformats call tree for the UI.""" 90 | funcname, filename, _ = node['stack'] 91 | sample_percent = self._get_percentage( 92 | node['sampleCount'], total_samples) 93 | color_hash = base_profiler.hash_name('%s @ %s' % (funcname, filename)) 94 | return { 95 | 'stack': node['stack'], 96 | 'children': [self._format_tree(child, total_samples) 97 | for child in node['children']], 98 | 'sampleCount': node['sampleCount'], 99 | 'samplePercentage': sample_percent, 100 | 'colorHash': color_hash 101 | } 102 | 103 | @property 104 | def call_tree(self): 105 | """Returns call tree.""" 106 | call_tree = {'stack': 'base', 'sampleCount': 0, 'children': []} 107 | for stack, sample_count in self._stats.items(): 108 | self._insert_stack(reversed(stack), sample_count, call_tree) 109 | self._fill_sample_count(call_tree) 110 | if not call_tree['children']: 111 | return {} 112 | return self._format_tree( 113 | call_tree['children'][0], call_tree['sampleCount']) 114 | 115 | 116 | class FlameGraphProfiler(base_profiler.BaseProfiler): 117 | """Statistical profiler wrapper. 118 | 119 | Runs statistical profiler and returns collected stats. 120 | """ 121 | 122 | def _profile_package(self): 123 | """Runs statistical profiler on a package.""" 124 | with _StatProfiler() as prof: 125 | prof.base_frame = inspect.currentframe() 126 | try: 127 | runpy.run_path(self._run_object, run_name='__main__') 128 | except SystemExit: 129 | pass 130 | 131 | call_tree = prof.call_tree 132 | return { 133 | 'objectName': self._object_name, 134 | 'sampleInterval': _SAMPLE_INTERVAL, 135 | 'runTime': prof.run_time, 136 | 'callStats': call_tree, 137 | 'totalSamples': call_tree.get('sampleCount', 0), 138 | 'timestamp': int(time.time()) 139 | } 140 | 141 | def profile_package(self): 142 | """Runs package profiler in a separate process.""" 143 | return base_profiler.run_in_separate_process(self._profile_package) 144 | 145 | def _profile_module(self): 146 | """Runs statistical profiler on a module.""" 147 | with open(self._run_object, 'rb') as srcfile, _StatProfiler() as prof: 148 | code = compile(srcfile.read(), self._run_object, 'exec') 149 | prof.base_frame = inspect.currentframe() 150 | try: 151 | exec(code, self._globs, None) 152 | except SystemExit: 153 | pass 154 | 155 | call_tree = prof.call_tree 156 | return { 157 | 'objectName': self._object_name, 158 | 'sampleInterval': _SAMPLE_INTERVAL, 159 | 'runTime': prof.run_time, 160 | 'callStats': call_tree, 161 | 'totalSamples': call_tree.get('sampleCount', 0), 162 | 'timestamp': int(time.time()) 163 | } 164 | 165 | def profile_module(self): 166 | """Runs module profiler in a separate process.""" 167 | return base_profiler.run_in_separate_process(self._profile_module) 168 | 169 | def profile_function(self): 170 | """Runs statistical profiler on a function.""" 171 | with _StatProfiler() as prof: 172 | result = self._run_object(*self._run_args, **self._run_kwargs) 173 | 174 | call_tree = prof.call_tree 175 | return { 176 | 'objectName': self._object_name, 177 | 'sampleInterval': _SAMPLE_INTERVAL, 178 | 'runTime': prof.run_time, 179 | 'callStats': call_tree, 180 | 'totalSamples': call_tree.get('sampleCount', 0), 181 | 'result': result, 182 | 'timestamp': int(time.time()) 183 | } 184 | -------------------------------------------------------------------------------- /vprof/memory_profiler.py: -------------------------------------------------------------------------------- 1 | """Memory profiler module.""" 2 | import builtins 3 | import gc 4 | import inspect 5 | import os 6 | import operator 7 | import psutil 8 | import re 9 | import runpy 10 | import sys 11 | import time 12 | 13 | from collections import deque 14 | from collections import Counter 15 | from vprof import base_profiler 16 | 17 | 18 | _BYTES_IN_MB = 1024 * 1024 19 | 20 | 21 | def _remove_duplicates(objects): 22 | """Removes duplicate objects from collection. 23 | 24 | http://www.peterbe.com/plog/uniqifiers-benchmark. 25 | """ 26 | seen, uniq = set(), [] 27 | for obj in objects: 28 | obj_id = id(obj) 29 | if obj_id in seen: 30 | continue 31 | seen.add(obj_id) 32 | uniq.append(obj) 33 | return uniq 34 | 35 | 36 | def _get_in_memory_objects(): 37 | """Returns all objects in memory.""" 38 | gc.collect() 39 | return gc.get_objects() 40 | 41 | 42 | def _process_in_memory_objects(objects): 43 | """Processes objects tracked by GC. 44 | 45 | Processing is done in separate function to avoid generating overhead. 46 | """ 47 | return _remove_duplicates(obj for obj in objects 48 | if not inspect.isframe(obj)) 49 | 50 | 51 | def _get_object_count_by_type(objects): 52 | """Counts Python objects by type.""" 53 | return Counter(map(type, objects)) 54 | 55 | 56 | def _get_obj_count_difference(objs1, objs2): 57 | """Returns count difference in two collections of Python objects.""" 58 | clean_obj_list1 = _process_in_memory_objects(objs1) 59 | clean_obj_list2 = _process_in_memory_objects(objs2) 60 | obj_count_1 = _get_object_count_by_type(clean_obj_list1) 61 | obj_count_2 = _get_object_count_by_type(clean_obj_list2) 62 | return obj_count_1 - obj_count_2 63 | 64 | 65 | def _format_obj_count(objects): 66 | """Formats object count.""" 67 | result = [] 68 | regex = re.compile(r'<(?P\w+) \'(?P\S+)\'>') 69 | for obj_type, obj_count in objects.items(): 70 | if obj_count != 0: 71 | match = re.findall(regex, repr(obj_type)) 72 | if match: 73 | obj_type, obj_name = match[0] 74 | result.append(("%s %s" % (obj_type, obj_name), obj_count)) 75 | return sorted(result, key=operator.itemgetter(1), reverse=True) 76 | 77 | 78 | class _CodeEventsTracker: 79 | """Tracks specified events during code execution. 80 | 81 | Contains all logic related to measuring memory usage. 82 | """ 83 | 84 | def __init__(self, target_modules): 85 | self._events_list = deque() 86 | self._original_trace_function = sys.gettrace() 87 | self._process = psutil.Process(os.getpid()) 88 | self._resulting_events = [] 89 | self.mem_overhead = None 90 | self.target_modules = target_modules 91 | 92 | def __enter__(self): 93 | """Enables events tracker.""" 94 | sys.settrace(self._trace_memory_usage) 95 | return self 96 | 97 | def __exit__(self, exc_type, exc_val, exc_tbf): 98 | """Disables events tracker.""" 99 | sys.settrace(self._original_trace_function) 100 | 101 | def _trace_memory_usage(self, frame, event, arg): #pylint: disable=unused-argument 102 | """Checks memory usage when 'line' event occur.""" 103 | if event == 'line' and frame.f_code.co_filename in self.target_modules: 104 | self._events_list.append( 105 | (frame.f_lineno, self._process.memory_info().rss, 106 | frame.f_code.co_name, frame.f_code.co_filename)) 107 | return self._trace_memory_usage 108 | 109 | @property 110 | def code_events(self): 111 | """Returns processed memory usage.""" 112 | if self._resulting_events: 113 | return self._resulting_events 114 | for i, (lineno, mem, func, fname) in enumerate(self._events_list): 115 | mem_in_mb = float(mem - self.mem_overhead) / _BYTES_IN_MB 116 | if (self._resulting_events and 117 | self._resulting_events[-1][0] == lineno and 118 | self._resulting_events[-1][2] == func and 119 | self._resulting_events[-1][3] == fname and 120 | self._resulting_events[-1][1] < mem_in_mb): 121 | self._resulting_events[-1][1] = mem_in_mb 122 | else: 123 | self._resulting_events.append( 124 | [i + 1, lineno, mem_in_mb, func, fname]) 125 | return self._resulting_events 126 | 127 | @property 128 | def obj_overhead(self): 129 | """Returns all objects that are considered as profiler overhead. 130 | Objects are hardcoded for convenience. 131 | """ 132 | overhead = [ 133 | self, 134 | self._resulting_events, 135 | self._events_list, 136 | self._process 137 | ] 138 | overhead_count = _get_object_count_by_type(overhead) 139 | # One for reference to __dict__ and one for reference to 140 | # the current module. 141 | overhead_count[dict] += 2 142 | return overhead_count 143 | 144 | def compute_mem_overhead(self): 145 | """Returns memory overhead.""" 146 | self.mem_overhead = (self._process.memory_info().rss - 147 | builtins.initial_rss_size) 148 | 149 | 150 | class MemoryProfiler(base_profiler.BaseProfiler): 151 | """Memory profiler wrapper. 152 | 153 | Runs memory profiler and processes collected stats. 154 | """ 155 | 156 | def profile_package(self): 157 | """Returns memory stats for a package.""" 158 | target_modules = base_profiler.get_pkg_module_names(self._run_object) 159 | try: 160 | with _CodeEventsTracker(target_modules) as prof: 161 | prof.compute_mem_overhead() 162 | runpy.run_path(self._run_object, run_name='__main__') 163 | except SystemExit: 164 | pass 165 | return prof, None 166 | 167 | def profile_module(self): 168 | """Returns memory stats for a module.""" 169 | target_modules = {self._run_object} 170 | try: 171 | with open(self._run_object, 'rb') as srcfile,\ 172 | _CodeEventsTracker(target_modules) as prof: 173 | code = compile(srcfile.read(), self._run_object, 'exec') 174 | prof.compute_mem_overhead() 175 | exec(code, self._globs, None) 176 | except SystemExit: 177 | pass 178 | return prof, None 179 | 180 | def profile_function(self): 181 | """Returns memory stats for a function.""" 182 | target_modules = {self._run_object.__code__.co_filename} 183 | with _CodeEventsTracker(target_modules) as prof: 184 | prof.compute_mem_overhead() 185 | result = self._run_object(*self._run_args, **self._run_kwargs) 186 | return prof, result 187 | 188 | def run(self): 189 | """Collects memory stats for a specified Python program.""" 190 | existing_objects = _get_in_memory_objects() 191 | prof, result = self.profile() 192 | new_objects = _get_in_memory_objects() 193 | 194 | new_obj_count = _get_obj_count_difference(new_objects, existing_objects) 195 | result_obj_count = new_obj_count - prof.obj_overhead 196 | 197 | # existing_objects list is also profiler overhead 198 | result_obj_count[list] -= 1 199 | pretty_obj_count = _format_obj_count(result_obj_count) 200 | return { 201 | 'objectName': self._object_name, 202 | 'codeEvents': prof.code_events, 203 | 'totalEvents': len(prof.code_events), 204 | 'objectsCount': pretty_obj_count, 205 | 'result': result, 206 | 'timestamp': int(time.time()) 207 | } 208 | -------------------------------------------------------------------------------- /vprof/profiler.py: -------------------------------------------------------------------------------- 1 | """Profiler wrapper module.""" 2 | import cProfile 3 | import operator 4 | import pstats 5 | import runpy 6 | import time 7 | 8 | from vprof import base_profiler 9 | 10 | 11 | class Profiler(base_profiler.BaseProfiler): 12 | """Python profiler wrapper. 13 | 14 | Runs cProfile on specified program and returns collected stats. 15 | """ 16 | 17 | @staticmethod 18 | def _transform_stats(prof): 19 | """Processes collected stats for UI.""" 20 | records = [] 21 | for info, params in prof.stats.items(): 22 | filename, lineno, funcname = info 23 | cum_calls, num_calls, time_per_call, cum_time, _ = params 24 | if prof.total_tt == 0: 25 | percentage = 0 26 | else: 27 | percentage = round(100 * (cum_time / prof.total_tt), 4) 28 | cum_time = round(cum_time, 4) 29 | func_name = '%s @ %s' % (funcname, filename) 30 | color_hash = base_profiler.hash_name(func_name) 31 | records.append( 32 | (filename, lineno, funcname, cum_time, percentage, num_calls, 33 | cum_calls, time_per_call, filename, color_hash)) 34 | return sorted(records, key=operator.itemgetter(4), reverse=True) 35 | 36 | def _profile_package(self): 37 | """Runs cProfile on a package.""" 38 | prof = cProfile.Profile() 39 | prof.enable() 40 | try: 41 | runpy.run_path(self._run_object, run_name='__main__') 42 | except SystemExit: 43 | pass 44 | prof.disable() 45 | prof_stats = pstats.Stats(prof) 46 | prof_stats.calc_callees() 47 | return { 48 | 'objectName': self._object_name, 49 | 'callStats': self._transform_stats(prof_stats), 50 | 'totalTime': prof_stats.total_tt, 51 | 'primitiveCalls': prof_stats.prim_calls, 52 | 'totalCalls': prof_stats.total_calls, 53 | 'timestamp': int(time.time()) 54 | } 55 | 56 | def profile_package(self): 57 | """Runs package profiler in a separate process.""" 58 | return base_profiler.run_in_separate_process(self._profile_package) 59 | 60 | def _profile_module(self): 61 | """Runs cProfile on a module.""" 62 | prof = cProfile.Profile() 63 | try: 64 | with open(self._run_object, 'rb') as srcfile: 65 | code = compile(srcfile.read(), self._run_object, 'exec') 66 | prof.runctx(code, self._globs, None) 67 | except SystemExit: 68 | pass 69 | prof_stats = pstats.Stats(prof) 70 | prof_stats.calc_callees() 71 | return { 72 | 'objectName': self._object_name, 73 | 'callStats': self._transform_stats(prof_stats), 74 | 'totalTime': prof_stats.total_tt, 75 | 'primitiveCalls': prof_stats.prim_calls, 76 | 'totalCalls': prof_stats.total_calls, 77 | 'timestamp': int(time.time()) 78 | } 79 | 80 | def profile_module(self): 81 | """Runs module profiler in a separate process.""" 82 | return base_profiler.run_in_separate_process(self._profile_module) 83 | 84 | def profile_function(self): 85 | """Runs cProfile on a function.""" 86 | prof = cProfile.Profile() 87 | prof.enable() 88 | result = self._run_object(*self._run_args, **self._run_kwargs) 89 | prof.disable() 90 | prof_stats = pstats.Stats(prof) 91 | prof_stats.calc_callees() 92 | return { 93 | 'objectName': self._object_name, 94 | 'callStats': self._transform_stats(prof_stats), 95 | 'totalTime': prof_stats.total_tt, 96 | 'primitiveCalls': prof_stats.prim_calls, 97 | 'totalCalls': prof_stats.total_calls, 98 | 'result': result, 99 | 'timestamp': int(time.time()) 100 | } 101 | -------------------------------------------------------------------------------- /vprof/runner.py: -------------------------------------------------------------------------------- 1 | """Module with functions that run profilers.""" 2 | # pylint: disable=wrong-import-position 3 | import builtins 4 | import gzip 5 | import os 6 | import psutil 7 | import urllib.request 8 | 9 | # Take initial RSS in order to compute profiler memory overhead 10 | # when profiling single functions. 11 | if not hasattr(builtins, 'initial_rss_size'): 12 | builtins.initial_rss_size = psutil.Process(os.getpid()).memory_info().rss 13 | 14 | import json 15 | 16 | from collections import OrderedDict 17 | from vprof import code_heatmap 18 | from vprof import flame_graph 19 | from vprof import memory_profiler 20 | from vprof import profiler 21 | 22 | _PROFILERS = ( 23 | ('m', memory_profiler.MemoryProfiler), 24 | ('c', flame_graph.FlameGraphProfiler), 25 | ('h', code_heatmap.CodeHeatmapProfiler), 26 | ('p', profiler.Profiler) 27 | ) 28 | 29 | 30 | class Error(Exception): 31 | """Base exception for current module.""" 32 | pass # pylint: disable=unnecessary-pass 33 | 34 | 35 | class AmbiguousConfigurationError(Error): 36 | """Raised when profiler configuration is ambiguous.""" 37 | pass # pylint: disable=unnecessary-pass 38 | 39 | 40 | class BadOptionError(Error): 41 | """Raised when unknown options are present in the configuration.""" 42 | pass # pylint: disable=unnecessary-pass 43 | 44 | 45 | def run_profilers(run_object, prof_config, verbose=False): 46 | """Runs profilers on run_object. 47 | 48 | Args: 49 | run_object: An object (string or tuple) for profiling. 50 | prof_config: A string with profilers configuration. 51 | verbose: True if info about running profilers should be shown. 52 | Returns: 53 | An ordered dictionary with collected stats. 54 | Raises: 55 | AmbiguousConfigurationError: when prof_config is ambiguous. 56 | BadOptionError: when unknown options are present in configuration. 57 | """ 58 | if len(prof_config) > len(set(prof_config)): 59 | raise AmbiguousConfigurationError( 60 | 'Profiler configuration %s is ambiguous' % prof_config) 61 | 62 | available_profilers = {opt for opt, _ in _PROFILERS} 63 | for option in prof_config: 64 | if option not in available_profilers: 65 | raise BadOptionError('Unknown option: %s' % option) 66 | 67 | run_stats = OrderedDict() 68 | present_profilers = ((o, p) for o, p in _PROFILERS if o in prof_config) 69 | for option, prof in present_profilers: 70 | curr_profiler = prof(run_object) 71 | if verbose: 72 | print('Running %s...' % curr_profiler.__class__.__name__) 73 | run_stats[option] = curr_profiler.run() 74 | return run_stats 75 | 76 | 77 | def run(func, options, args=(), kwargs={}, host='localhost', port=8000): # pylint: disable=dangerous-default-value 78 | """Runs profilers on a function. 79 | 80 | Args: 81 | func: A Python function. 82 | options: A string with profilers configuration (i.e. 'cmh'). 83 | args: func non-keyword arguments. 84 | kwargs: func keyword arguments. 85 | host: Host name to send collected data. 86 | port: Port number to send collected data. 87 | 88 | Returns: 89 | A result of func execution. 90 | """ 91 | run_stats = run_profilers((func, args, kwargs), options) 92 | 93 | result = None 94 | for prof in run_stats: 95 | if result is None: 96 | result = run_stats[prof]['result'] 97 | del run_stats[prof]['result'] # Don't send result to remote host 98 | 99 | post_data = gzip.compress( 100 | json.dumps(run_stats).encode('utf-8')) 101 | urllib.request.urlopen('http://%s:%s' % (host, port), post_data) 102 | return result 103 | -------------------------------------------------------------------------------- /vprof/stats_server.py: -------------------------------------------------------------------------------- 1 | """Profiler server.""" 2 | import functools 3 | import gzip 4 | import io 5 | import json 6 | import os 7 | import socketserver 8 | import sys 9 | import webbrowser 10 | 11 | from http import server 12 | 13 | _STATIC_DIR = 'ui' 14 | _PROFILE_HTML = '%s/profile.html' % _STATIC_DIR 15 | 16 | 17 | class StatsServer(socketserver.ThreadingMixIn, socketserver.TCPServer): 18 | """Declares multithreaded HTTP server.""" 19 | allow_reuse_address = True 20 | 21 | 22 | class StatsHandler(server.SimpleHTTPRequestHandler): 23 | """Program stats request handler.""" 24 | 25 | def __init__(self, profile_json, *args, **kwargs): 26 | self._profile_json = profile_json 27 | self.uri_map = { 28 | '/': self._handle_root, 29 | '/profile': self._handle_profile, 30 | } 31 | # Since this class is old-style - call parent method directly. 32 | server.SimpleHTTPRequestHandler.__init__( 33 | self, *args, **kwargs) 34 | 35 | @staticmethod 36 | def _handle_root(): 37 | """Handles index.html requests.""" 38 | res_filename = os.path.join( 39 | os.path.dirname(__file__), _PROFILE_HTML) 40 | with io.open(res_filename, 'rb') as res_file: 41 | content = res_file.read() 42 | return content, 'text/html' 43 | 44 | def _handle_profile(self): 45 | """Handles profile stats requests.""" 46 | return json.dumps(self._profile_json).encode(), 'text/json' 47 | 48 | def _handle_other(self): 49 | """Handles static files requests.""" 50 | res_filename = os.path.join( 51 | os.path.dirname(__file__), _STATIC_DIR, self.path[1:]) 52 | with io.open(res_filename, 'rb') as res_file: 53 | content = res_file.read() 54 | _, extension = os.path.splitext(self.path) 55 | return content, 'text/%s' % extension[1:] # Skip dot in the extension. 56 | 57 | def do_GET(self): 58 | """Handles HTTP GET requests.""" 59 | handler = self.uri_map.get(self.path) or self._handle_other 60 | content, content_type = handler() 61 | compressed_content = gzip.compress(content) 62 | self._send_response( 63 | 200, headers=(('Content-type', '%s; charset=utf-8' % content_type), 64 | ('Content-Encoding', 'gzip'), 65 | ('Content-Length', len(compressed_content)))) 66 | self.wfile.write(compressed_content) 67 | 68 | def do_POST(self): 69 | """Handles HTTP POST requests.""" 70 | post_data = self.rfile.read(int(self.headers['Content-Length'])) 71 | json_data = gzip.decompress(post_data) 72 | self._profile_json.update(json.loads(json_data.decode('utf-8'))) 73 | self._send_response( 74 | 200, headers=(('Content-type', '%s; charset=utf-8' % 'text/json'), 75 | ('Content-Encoding', 'gzip'), 76 | ('Content-Length', len(post_data)))) 77 | 78 | def _send_response(self, http_code, message=None, headers=None): 79 | """Sends HTTP response code, message and headers.""" 80 | self.send_response(http_code, message) 81 | if headers: 82 | for header in headers: 83 | self.send_header(*header) 84 | self.end_headers() 85 | 86 | 87 | def start(host, port, profiler_stats, dont_start_browser, debug_mode): 88 | """Starts HTTP server with specified parameters. 89 | 90 | Args: 91 | host: Server host name. 92 | port: Server port. 93 | profiler_stats: A dict with collected program stats. 94 | dont_start_browser: Whether to open browser after profiling. 95 | debug_mode: Whether to redirect stderr to /dev/null. 96 | """ 97 | stats_handler = functools.partial(StatsHandler, profiler_stats) 98 | if not debug_mode: 99 | sys.stderr = open(os.devnull, 'w') 100 | print('Starting HTTP server...') 101 | if not dont_start_browser: 102 | webbrowser.open('http://{}:{}/'.format(host, port)) 103 | try: 104 | StatsServer((host, port), stats_handler).serve_forever() 105 | except KeyboardInterrupt: 106 | print('Stopping...') 107 | sys.exit(0) 108 | -------------------------------------------------------------------------------- /vprof/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvdv/vprof/99bb5cd5691a5bfbca23c14ad3b70a7bca6e7ac7/vprof/tests/__init__.py -------------------------------------------------------------------------------- /vprof/tests/base_profiler_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access, missing-docstring 2 | import sys 3 | import unittest 4 | 5 | from vprof import base_profiler 6 | from unittest import mock 7 | 8 | 9 | class GetPkgModuleNamesUnittest(unittest.TestCase): 10 | 11 | @mock.patch('pkgutil.iter_modules') 12 | @mock.patch('os.path.exists') 13 | def testGetPackageCode(self, exists_mock, iter_modules_mock): 14 | package_path = mock.MagicMock() 15 | _ = mock.MagicMock() 16 | modname1, modname2 = 'module1', 'module2' 17 | fobj1, fobj2 = mock.MagicMock(), mock.MagicMock() 18 | fobj1.path = '/path/to/module' 19 | fobj2.path = '/path/to/module' 20 | iter_modules_mock.return_value = [ 21 | (fobj1, modname1, _), (fobj2, modname2, _)] 22 | exists_mock.return_value = True 23 | result = base_profiler.get_pkg_module_names(package_path) 24 | self.assertEqual( 25 | result, {'/path/to/module/module1.py', 26 | '/path/to/module/module2.py'}) 27 | 28 | 29 | class BaseProfileUnittest(unittest.TestCase): 30 | def setUp(self): 31 | self.profiler = object.__new__(base_profiler.BaseProfiler) 32 | 33 | def testGetRunObjectType_Function(self): 34 | func = (lambda x: x, ('foo',), ('bar',)) 35 | self.assertEqual( 36 | self.profiler.get_run_object_type(func), 'function') 37 | 38 | @mock.patch('os.path.isdir') 39 | def testGetRunObjectType_Module(self, isdir_mock): 40 | isdir_mock.return_value = False 41 | modpath = 'foo.py -v' 42 | self.assertEqual( 43 | self.profiler.get_run_object_type(modpath), 'module') 44 | 45 | @mock.patch('os.path.isdir') 46 | def testGetRunObjectType_Package(self, isdir_mock): 47 | isdir_mock.return_value = True 48 | pkgpath = 'foo' 49 | self.assertEqual( 50 | self.profiler.get_run_object_type(pkgpath), 'package') 51 | 52 | def testInitFunction(self): 53 | _func = lambda foo: foo 54 | self.profiler.__init__((_func, ('bar'), {'bar': 'baz'})) 55 | self.assertEqual(self.profiler._run_object, _func) 56 | self.assertEqual(self.profiler._run_args, ('bar')) 57 | self.assertDictEqual(self.profiler._run_kwargs, {'bar': 'baz'}) 58 | 59 | @mock.patch('os.path.isdir') 60 | def testInitPackage(self, isdir_mock): 61 | isdir_mock.return_value = True 62 | self.profiler.__init__('test/test_pkg') 63 | self.assertEqual(self.profiler._run_object, 'test/test_pkg') 64 | self.assertEqual(self.profiler._run_args, '') 65 | self.profiler.__init__('test/test_pkg --help') 66 | self.assertEqual(self.profiler._run_object, 'test/test_pkg') 67 | self.assertEqual(self.profiler._run_args, '--help') 68 | 69 | @mock.patch('os.path.isdir') 70 | def testInitModule(self, isdir_mock): 71 | isdir_mock.return_value = False 72 | self.profiler.__init__('foo.py') 73 | self.assertEqual(self.profiler._run_object, 'foo.py') 74 | self.assertEqual(self.profiler._run_args, '') 75 | self.profiler.__init__('foo.py --bar --baz') 76 | self.assertEqual(self.profiler._run_object, 'foo.py') 77 | self.assertEqual(self.profiler._run_args, '--bar --baz') 78 | self.assertDictEqual(self.profiler._globs, { 79 | '__file__': 'foo.py', 80 | '__name__': '__main__', 81 | '__package__': None 82 | }) 83 | 84 | def testRun(self): 85 | self.profiler.profile = lambda: 1 86 | self.assertEqual(self.profiler.run(), 1) 87 | 88 | def testRunAsModule(self): 89 | with self.assertRaises(NotImplementedError): 90 | self.profiler.profile_module() 91 | 92 | def testRunAsPackage(self): 93 | with self.assertRaises(NotImplementedError): 94 | self.profiler.profile_package() 95 | 96 | def testRunAsFunction(self): 97 | with self.assertRaises(NotImplementedError): 98 | self.profiler.profile_function() 99 | 100 | def testReplaceSysargs(self): 101 | self.profiler._run_object = mock.MagicMock() 102 | self.profiler._run_args = '' 103 | with mock.patch.object(sys, 'argv', []): 104 | self.profiler._replace_sysargs() 105 | self.assertEqual(sys.argv, [self.profiler._run_object]) 106 | 107 | self.profiler._run_args = '-s foo -a bar -e baz' 108 | with mock.patch.object(sys, 'argv', []): 109 | self.profiler._replace_sysargs() 110 | self.assertEqual( 111 | sys.argv, 112 | [self.profiler._run_object, 113 | '-s', 'foo', '-a', 'bar', '-e', 'baz'] 114 | ) 115 | 116 | # pylint: enable=protected-access, missing-docstring 117 | -------------------------------------------------------------------------------- /vprof/tests/code_heatmap_e2e.py: -------------------------------------------------------------------------------- 1 | """End-to-end tests for code heatmap module.""" 2 | # pylint: disable=missing-docstring, blacklisted-name 3 | import functools 4 | import gzip 5 | import json 6 | import inspect 7 | import threading 8 | import os 9 | import unittest 10 | import urllib.request 11 | 12 | from vprof import code_heatmap 13 | from vprof import stats_server 14 | from vprof import runner 15 | from vprof.tests import test_pkg # pylint: disable=unused-import 16 | 17 | _HOST, _PORT = 'localhost', 12345 18 | _MODULE_FILENAME = 'vprof/tests/test_pkg/dummy_module.py' 19 | _PACKAGE_PATH = 'vprof/tests/test_pkg/' 20 | _DUMMY_MODULE_SOURCELINES = [ 21 | ['line', 1, 'def dummy_fib(n):'], 22 | ['line', 2, ' if n < 2:'], 23 | ['line', 3, ' return n'], 24 | ['line', 4, ' return dummy_fib(n - 1) + dummy_fib(n - 2)'], 25 | ['line', 5, '']] 26 | _MAIN_MODULE_SOURCELINES = [ 27 | ['line', 1, 'from test_pkg import dummy_module'], 28 | ['line', 2, ''], 29 | ['line', 3, 'dummy_module.dummy_fib(5)'], 30 | ['line', 4, '']] 31 | _POLL_INTERVAL = 0.01 32 | 33 | 34 | class CodeHeatmapModuleEndToEndTest(unittest.TestCase): 35 | 36 | def setUp(self): 37 | program_stats = code_heatmap.CodeHeatmapProfiler( 38 | _MODULE_FILENAME).run() 39 | stats_handler = functools.partial( 40 | stats_server.StatsHandler, program_stats) 41 | self.server = stats_server.StatsServer( 42 | (_HOST, _PORT), stats_handler) 43 | threading.Thread( 44 | target=self.server.serve_forever, 45 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 46 | 47 | def tearDown(self): 48 | self.server.shutdown() 49 | self.server.server_close() 50 | 51 | def testRequest(self): 52 | response = urllib.request.urlopen( 53 | 'http://%s:%s/profile' % (_HOST, _PORT)) 54 | response_data = gzip.decompress(response.read()) 55 | stats = json.loads(response_data.decode('utf-8')) 56 | self.assertEqual(stats['objectName'], _MODULE_FILENAME) 57 | self.assertTrue(stats['runTime'] > 0) 58 | heatmaps = stats['heatmaps'] 59 | self.assertEqual(len(heatmaps), 1) 60 | self.assertTrue(_MODULE_FILENAME in heatmaps[0]['name']) 61 | self.assertDictEqual(heatmaps[0]['executionCount'], {'1': 1}) 62 | self.assertListEqual(heatmaps[0]['srcCode'], _DUMMY_MODULE_SOURCELINES) 63 | 64 | 65 | class CodeHeatmapPackageEndToEndTest(unittest.TestCase): 66 | 67 | def setUp(self): 68 | program_stats = code_heatmap.CodeHeatmapProfiler( 69 | _PACKAGE_PATH).run() 70 | stats_handler = functools.partial( 71 | stats_server.StatsHandler, program_stats) 72 | self.server = stats_server.StatsServer( 73 | (_HOST, _PORT), stats_handler) 74 | threading.Thread( 75 | target=self.server.serve_forever, 76 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 77 | 78 | def tearDown(self): 79 | self.server.shutdown() 80 | self.server.server_close() 81 | 82 | def testRequest(self): 83 | response = urllib.request.urlopen( 84 | 'http://%s:%s/profile' % (_HOST, _PORT)) 85 | response_data = gzip.decompress(response.read()) 86 | stats = json.loads(response_data.decode('utf-8')) 87 | self.assertEqual(stats['objectName'], _PACKAGE_PATH) 88 | self.assertTrue(stats['runTime'] > 0) 89 | heatmap_files = {heatmap['name'] for heatmap in stats['heatmaps']} 90 | self.assertTrue(os.path.abspath( 91 | 'vprof/tests/test_pkg/__main__.py') in heatmap_files) 92 | self.assertTrue(os.path.abspath( 93 | 'vprof/tests/test_pkg/dummy_module.py') in heatmap_files) 94 | 95 | 96 | class CodeHeatmapFunctionEndToEndTest(unittest.TestCase): 97 | 98 | def setUp(self): 99 | 100 | def _func(foo, bar): 101 | baz = foo + bar 102 | return baz 103 | self._func = _func 104 | 105 | stats_handler = functools.partial( 106 | stats_server.StatsHandler, {}) 107 | self.server = stats_server.StatsServer( 108 | (_HOST, _PORT), stats_handler) 109 | threading.Thread( 110 | target=self.server.serve_forever, 111 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 112 | 113 | def tearDown(self): 114 | self.server.shutdown() 115 | self.server.server_close() 116 | 117 | def testRequest(self): 118 | runner.run( 119 | self._func, 'h', ('foo', 'bar'), host=_HOST, port=_PORT) 120 | response = urllib.request.urlopen( 121 | 'http://%s:%s/profile' % (_HOST, _PORT)) 122 | response_data = gzip.decompress(response.read()) 123 | stats = json.loads(response_data.decode('utf-8')) 124 | self.assertTrue(stats['h']['runTime'] > 0) 125 | heatmaps = stats['h']['heatmaps'] 126 | curr_filename = inspect.getabsfile(inspect.currentframe()) 127 | self.assertEqual(stats['h']['objectName'], 128 | '_func @ %s (function)' % curr_filename) 129 | self.assertEqual(len(heatmaps), 1) 130 | self.assertDictEqual( 131 | heatmaps[0]['executionCount'], {'101': 1, '102': 1}) 132 | self.assertListEqual( 133 | heatmaps[0]['srcCode'], 134 | [['line', 100, u' def _func(foo, bar):\n'], 135 | ['line', 101, u' baz = foo + bar\n'], 136 | ['line', 102, u' return baz\n']]) 137 | 138 | # pylint: enable=missing-docstring, blacklisted-name 139 | -------------------------------------------------------------------------------- /vprof/tests/code_heatmap_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access, missing-docstring 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from collections import defaultdict 7 | from vprof import code_heatmap 8 | 9 | 10 | class CheckStandardDirUnittest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.original_paths = code_heatmap._STDLIB_PATHS 14 | code_heatmap._STDLIB_PATHS = ['/usr/local/python/lib'] 15 | 16 | def tearDown(self): 17 | code_heatmap._STDLIB_PATHS = self.original_paths 18 | 19 | def testCheckStandardDir(self): 20 | self.assertTrue( 21 | code_heatmap.check_standard_dir( 22 | '/usr/local/python/lib/foo')) 23 | self.assertTrue( 24 | code_heatmap.check_standard_dir( 25 | '/usr/local/python/lib/foo/bar')) 26 | 27 | self.assertTrue( 28 | code_heatmap.check_standard_dir( 29 | '/Users/foobar/test/lib/python3.6/site-packages')) 30 | 31 | self.assertFalse( 32 | code_heatmap.check_standard_dir('/usr/local/bin')) 33 | self.assertFalse( 34 | code_heatmap.check_standard_dir('/usr/local')) 35 | 36 | 37 | class CodeHeatmapCalculatorUnittest(unittest.TestCase): 38 | def setUp(self): 39 | self._calc = object.__new__(code_heatmap._CodeHeatmapCalculator) 40 | 41 | def testInit(self): 42 | self._calc.__init__() 43 | self.assertEqual(self._calc.original_trace_function, sys.gettrace()) 44 | self.assertEqual( 45 | self._calc._heatmap, defaultdict(lambda: defaultdict(float))) 46 | self.assertEqual( 47 | self._calc._execution_count, defaultdict(lambda: defaultdict(int))) 48 | 49 | def testLinesWithoutStdlibSimple(self): 50 | self._calc.lines = [ 51 | ['foo.py', 1, 0.5], 52 | ['foo.py', 2, 0.6], 53 | ['foo.py', 3, 0.1], 54 | ] 55 | result = list(self._calc.lines_without_stdlib) 56 | basename_result = [ 57 | [os.path.basename(abspath), lineno, runtime] 58 | for abspath, lineno, runtime in result] 59 | self.assertListEqual( 60 | basename_result, 61 | [['foo.py', 1, 0.5], 62 | ['foo.py', 2, 0.6], 63 | ['foo.py', 3, 0.1]] 64 | ) 65 | 66 | def testLinesWithoutStdlib(self): 67 | self._calc.lines = [ 68 | ['foo.py', 1, 0.5], 69 | ['foo.py', 2, 0.6], 70 | ['site-packages/bar.py', 1, 0.4], 71 | ['foo.py', 3, 0.1], 72 | ['site-packages/baz.py', 1, 0.25], 73 | ['site-packages/baz.py', 2, 0.11], 74 | ['site-packages/baz.py', 3, 0.33], 75 | ['foo.py', 4, 0.77], 76 | ] 77 | result = list(self._calc.lines_without_stdlib) 78 | basename_result = [ 79 | [os.path.basename(abspath), lineno, runtime] 80 | for abspath, lineno, runtime in result] 81 | self.assertListEqual( 82 | basename_result, 83 | [['foo.py', 1, 0.5], 84 | ['foo.py', 2, 1.0], 85 | ['foo.py', 3, 0.79], 86 | ['foo.py', 4, 0.77]] 87 | ) 88 | 89 | 90 | class CodeHeatmapProfileUnitTest(unittest.TestCase): 91 | def setUp(self): 92 | self._profile = object.__new__(code_heatmap.CodeHeatmapProfiler) 93 | 94 | def testCalcSkips(self): 95 | heatmap = {1: 1, 2: 1, 3: 1} 96 | self.assertListEqual(self._profile._calc_skips(heatmap, 3), []) 97 | 98 | heatmap = {1: 1, 2: 1, 99: 1, 102: 1, 115: 10} 99 | self.assertListEqual( 100 | self._profile._calc_skips(heatmap, 115), [(2, 96), (102, 12)]) 101 | 102 | heatmap = {1: 1, 102: 1, 103: 1, 104: 1, 105: 1} 103 | self.assertListEqual( 104 | self._profile._calc_skips(heatmap, 115), [(1, 100)]) 105 | 106 | def testSkipLines(self): 107 | self._profile._MIN_SKIP_SIZE = 0 108 | 109 | src_lines, skip_map = ['foo', 'bar', 'baz'], [] 110 | expected_result = [ 111 | ['line', 1, 'foo'], ['line', 2, 'bar'], ['line', 3, 'baz']] 112 | self.assertListEqual( 113 | self._profile._skip_lines(src_lines, skip_map), expected_result) 114 | 115 | src_lines, skip_map = ['foo', 'bar', 'baz', 'hahaha'], [(1, 2)] 116 | self._profile._SKIP_LINES = 1 117 | expected_result = [ 118 | ['line', 1, 'foo'], ['skip', 2], ['line', 4, 'hahaha']] 119 | self.assertListEqual( 120 | self._profile._skip_lines(src_lines, skip_map), expected_result) 121 | 122 | src_lines = ['foo', 'bar', 'baz', 'ha', 'haha'] 123 | skip_map = [(2, 2)] 124 | expected_result = [ 125 | ['line', 1, 'foo'], ['line', 2, 'bar'], 126 | ['skip', 2], ['line', 5, 'haha']] 127 | self.assertListEqual( 128 | self._profile._skip_lines(src_lines, skip_map), expected_result) 129 | 130 | src_lines = ['foo', 'bar', 'baz', 'ha', 'haha'] 131 | skip_map = [(2, 1), (3, 1)] 132 | expected_result = [ 133 | ['line', 1, 'foo'], ['line', 2, 'bar'], 134 | ['skip', 2], ['line', 5, 'haha']] 135 | self.assertListEqual( 136 | self._profile._skip_lines(src_lines, skip_map), expected_result) 137 | 138 | # pylint: enable=protected-access, missing-docstring 139 | -------------------------------------------------------------------------------- /vprof/tests/flame_graph_e2e.py: -------------------------------------------------------------------------------- 1 | """End-to-end tests for flame graph module.""" 2 | # pylint: disable=missing-docstring, protected-access, blacklisted-name 3 | import functools 4 | import gzip 5 | import json 6 | import inspect 7 | import threading 8 | import unittest 9 | import urllib.request 10 | 11 | from vprof import flame_graph 12 | from vprof import stats_server 13 | from vprof import runner 14 | from vprof.tests import test_pkg # pylint: disable=unused-import 15 | 16 | _HOST, _PORT = 'localhost', 12345 17 | _MODULE_FILENAME = 'vprof/tests/test_pkg/dummy_module.py' 18 | _PACKAGE_PATH = 'vprof/tests/test_pkg/' 19 | _POLL_INTERVAL = 0.01 20 | 21 | 22 | class FlameGraphModuleEndToEndTest(unittest.TestCase): 23 | 24 | def setUp(self): 25 | program_stats = flame_graph.FlameGraphProfiler( 26 | _MODULE_FILENAME).run() 27 | stats_handler = functools.partial( 28 | stats_server.StatsHandler, program_stats) 29 | self.server = stats_server.StatsServer( 30 | (_HOST, _PORT), stats_handler) 31 | threading.Thread( 32 | target=self.server.serve_forever, 33 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 34 | 35 | def tearDown(self): 36 | self.server.shutdown() 37 | self.server.server_close() 38 | 39 | def testRequest(self): 40 | response = urllib.request.urlopen( 41 | 'http://%s:%s/profile' % (_HOST, _PORT)) 42 | response_data = gzip.decompress(response.read()) 43 | stats = json.loads(response_data.decode('utf-8')) 44 | self.assertEqual(stats['objectName'], '%s (module)' % _MODULE_FILENAME) 45 | self.assertEqual(stats['sampleInterval'], flame_graph._SAMPLE_INTERVAL) 46 | self.assertTrue(stats['runTime'] > 0) 47 | self.assertTrue(len(stats['callStats']) >= 0) 48 | self.assertTrue(stats['totalSamples'] >= 0) 49 | 50 | 51 | class FlameGraphPackageEndToEndTest(unittest.TestCase): 52 | 53 | def setUp(self): 54 | program_stats = flame_graph.FlameGraphProfiler( 55 | _PACKAGE_PATH).run() 56 | stats_handler = functools.partial( 57 | stats_server.StatsHandler, program_stats) 58 | self.server = stats_server.StatsServer( 59 | (_HOST, _PORT), stats_handler) 60 | threading.Thread( 61 | target=self.server.serve_forever, 62 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 63 | 64 | def tearDown(self): 65 | self.server.shutdown() 66 | self.server.server_close() 67 | 68 | def testRequest(self): 69 | response = urllib.request.urlopen( 70 | 'http://%s:%s/profile' % (_HOST, _PORT)) 71 | response_data = gzip.decompress(response.read()) 72 | stats = json.loads(response_data.decode('utf-8')) 73 | self.assertEqual(stats['objectName'], '%s (package)' % _PACKAGE_PATH) 74 | self.assertEqual(stats['sampleInterval'], flame_graph._SAMPLE_INTERVAL) 75 | self.assertTrue(stats['runTime'] > 0) 76 | self.assertTrue(len(stats['callStats']) >= 0) 77 | self.assertTrue(stats['totalSamples'] >= 0) 78 | 79 | 80 | class FlameGraphFunctionEndToEndTest(unittest.TestCase): 81 | 82 | def setUp(self): 83 | 84 | def _func(foo, bar): 85 | baz = foo + bar 86 | return baz 87 | self._func = _func 88 | 89 | stats_handler = functools.partial( 90 | stats_server.StatsHandler, {}) 91 | self.server = stats_server.StatsServer( 92 | (_HOST, _PORT), stats_handler) 93 | threading.Thread( 94 | target=self.server.serve_forever, 95 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 96 | 97 | def tearDown(self): 98 | self.server.shutdown() 99 | self.server.server_close() 100 | 101 | def testRequest(self): 102 | runner.run( 103 | self._func, 'c', ('foo', 'bar'), host=_HOST, port=_PORT) 104 | response = urllib.request.urlopen( 105 | 'http://%s:%s/profile' % (_HOST, _PORT)) 106 | response_data = gzip.decompress(response.read()) 107 | stats = json.loads(response_data.decode('utf-8')) 108 | curr_filename = inspect.getabsfile(inspect.currentframe()) 109 | self.assertEqual(stats['c']['objectName'], 110 | '_func @ %s (function)' % curr_filename) 111 | self.assertEqual( 112 | stats['c']['sampleInterval'], flame_graph._SAMPLE_INTERVAL) 113 | self.assertTrue(stats['c']['runTime'] > 0) 114 | self.assertTrue(len(stats['c']['callStats']) >= 0) 115 | self.assertTrue(stats['c']['totalSamples'] >= 0) 116 | 117 | # pylint: enable=missing-docstring, blacklisted-name, protected-access 118 | -------------------------------------------------------------------------------- /vprof/tests/flame_graph_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access, missing-docstring 2 | import unittest 3 | 4 | from vprof import flame_graph 5 | 6 | 7 | class StatProfilerUnittest(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self._profiler = object.__new__(flame_graph._StatProfiler) 11 | 12 | def testCallTreeProperty(self): 13 | self.maxDiff = None 14 | self._profiler._call_tree = {} 15 | self._profiler._stats = { 16 | (('baz', 'f', 3), ('bar', 'f', 2), ('foo', 'f', 1)): 10, 17 | (('bar', 'f', 2), ('foo', 'f', 1)): 20, 18 | (('foo', 'f', 1),): 30, 19 | (('0', 'e', 4), ('baz', 'f', 3), 20 | ('bar', 'f', 2), ('foo', 'f', 1)): 40, 21 | } 22 | expected_result = { 23 | 'stack': ('foo', 'f', 1), 24 | 'sampleCount': 100, 25 | 'colorHash': 159121963, 26 | 'samplePercentage': 100.0, 27 | 'children': [{ 28 | 'stack': ('bar', 'f', 2), 29 | 'sampleCount': 70, 30 | 'colorHash': 152764956, 31 | 'samplePercentage': 70.0, 32 | 'children': [{ 33 | 'stack': ('baz', 'f', 3), 34 | 'sampleCount': 50, 35 | 'colorHash': 155386404, 36 | 'samplePercentage': 50.0, 37 | 'children': [{ 38 | 'stack': ('0', 'e', 4), 39 | 'colorHash': 47841558, 40 | 'sampleCount': 40, 41 | 'samplePercentage': 40.0, 42 | 'children': [] 43 | }] 44 | }] 45 | }] 46 | } 47 | self.assertDictEqual(self._profiler.call_tree, expected_result) 48 | 49 | # pylint: enable=protected-access, missing-docstring 50 | -------------------------------------------------------------------------------- /vprof/tests/memory_profiler_e2e.py: -------------------------------------------------------------------------------- 1 | """End-to-end tests for memory profiler module.""" 2 | # pylint: disable=missing-docstring, blacklisted-name 3 | import functools 4 | import gzip 5 | import json 6 | import inspect 7 | import threading 8 | import unittest 9 | import urllib.request 10 | 11 | from vprof import memory_profiler 12 | from vprof import stats_server 13 | from vprof import runner 14 | from vprof.tests import test_pkg # pylint: disable=unused-import 15 | 16 | try: 17 | import __builtin__ as builtins 18 | except ImportError: # __builtin__ was renamed to builtins in Python 3. 19 | import builtins 20 | builtins.initial_rss_size = 0 21 | 22 | _HOST, _PORT = 'localhost', 12345 23 | _MODULE_FILENAME = 'vprof/tests/test_pkg/dummy_module.py' 24 | _PACKAGE_PATH = 'vprof/tests/test_pkg/' 25 | _POLL_INTERVAL = 0.01 26 | 27 | 28 | class MemoryProfilerModuleEndToEndTest(unittest.TestCase): 29 | 30 | def setUp(self): 31 | program_stats = memory_profiler.MemoryProfiler( 32 | _MODULE_FILENAME).run() 33 | stats_handler = functools.partial( 34 | stats_server.StatsHandler, program_stats) 35 | self.server = stats_server.StatsServer( 36 | (_HOST, _PORT), stats_handler) 37 | threading.Thread( 38 | target=self.server.serve_forever, 39 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 40 | 41 | def tearDown(self): 42 | self.server.shutdown() 43 | self.server.server_close() 44 | 45 | def testRequest(self): 46 | response = urllib.request.urlopen( 47 | 'http://%s:%s/profile' % (_HOST, _PORT)) 48 | response_data = gzip.decompress(response.read()) 49 | stats = json.loads(response_data.decode('utf-8')) 50 | self.assertEqual(stats['objectName'], '%s (module)' % _MODULE_FILENAME) 51 | self.assertEqual(stats['totalEvents'], 1) 52 | self.assertEqual(stats['codeEvents'][0][0], 1) 53 | self.assertEqual(stats['codeEvents'][0][1], 1) 54 | self.assertEqual(stats['codeEvents'][0][3], '') 55 | self.assertEqual( 56 | stats['codeEvents'][0][4], 'vprof/tests/test_pkg/dummy_module.py') 57 | 58 | 59 | class MemoryProfilerPackageEndToEndTest(unittest.TestCase): 60 | 61 | def setUp(self): 62 | program_stats = memory_profiler.MemoryProfiler( 63 | _PACKAGE_PATH).run() 64 | stats_handler = functools.partial( 65 | stats_server.StatsHandler, program_stats) 66 | self.server = stats_server.StatsServer( 67 | (_HOST, _PORT), stats_handler) 68 | threading.Thread( 69 | target=self.server.serve_forever, 70 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 71 | 72 | def tearDown(self): 73 | self.server.shutdown() 74 | self.server.server_close() 75 | 76 | def testRequest(self): 77 | response = urllib.request.urlopen( 78 | 'http://%s:%s/profile' % (_HOST, _PORT)) 79 | response_data = gzip.decompress(response.read()) 80 | stats = json.loads(response_data.decode('utf-8')) 81 | self.assertEqual(stats['objectName'], '%s (package)' % _PACKAGE_PATH) 82 | self.assertTrue(stats['totalEvents'] > 0) 83 | self.assertTrue(len(stats['objectsCount']) > 0) 84 | 85 | 86 | class MemoryProfilerFunctionEndToEndTest(unittest.TestCase): 87 | 88 | def setUp(self): 89 | 90 | def _func(foo, bar): 91 | baz = foo + bar 92 | return baz 93 | self._func = _func 94 | 95 | stats_handler = functools.partial( 96 | stats_server.StatsHandler, {}) 97 | self.server = stats_server.StatsServer( 98 | (_HOST, _PORT), stats_handler) 99 | threading.Thread( 100 | target=self.server.serve_forever, 101 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 102 | 103 | def tearDown(self): 104 | self.server.shutdown() 105 | self.server.server_close() 106 | 107 | def testRequest(self): 108 | runner.run( 109 | self._func, 'm', ('foo', 'bar'), host=_HOST, port=_PORT) 110 | response = urllib.request.urlopen( 111 | 'http://%s:%s/profile' % (_HOST, _PORT)) 112 | response_data = gzip.decompress(response.read()) 113 | stats = json.loads(response_data.decode('utf-8')) 114 | curr_filename = inspect.getabsfile(inspect.currentframe()) 115 | self.assertEqual(stats['m']['objectName'], 116 | '_func @ %s (function)' % curr_filename) 117 | self.assertEqual(stats['m']['totalEvents'], 2) 118 | self.assertEqual(stats['m']['codeEvents'][0][0], 1) 119 | self.assertEqual(stats['m']['codeEvents'][0][1], 91) 120 | self.assertEqual(stats['m']['codeEvents'][0][3], '_func') 121 | self.assertEqual(stats['m']['codeEvents'][1][0], 2) 122 | self.assertEqual(stats['m']['codeEvents'][1][1], 92) 123 | self.assertEqual(stats['m']['codeEvents'][1][3], '_func') 124 | 125 | # pylint: enable=missing-docstring, blacklisted-name 126 | -------------------------------------------------------------------------------- /vprof/tests/memory_profiler_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access, missing-docstring, too-many-locals 2 | import unittest 3 | 4 | from collections import deque 5 | from vprof import memory_profiler 6 | 7 | from unittest import mock # pylint: disable=ungrouped-imports 8 | 9 | 10 | class GetObjectCountByTypeUnittest(unittest.TestCase): 11 | 12 | def testGetObjectByType(self): 13 | objects = [1, 2, 3, 'a', 'b', 'c', {}, []] 14 | obj_count = memory_profiler._get_object_count_by_type(objects) 15 | self.assertEqual(obj_count[int], 3) 16 | self.assertEqual(obj_count[str], 3) 17 | self.assertEqual(obj_count[dict], 1) 18 | self.assertEqual(obj_count[list], 1) 19 | 20 | 21 | class GetObjCountDifferenceUnittest(unittest.TestCase): 22 | 23 | def testGetCountObjByType(self): 24 | objects1 = [1, 2, 3, 'a', 'b', 'c', {}, []] 25 | objects2 = [1, 2, 'a', 'b', {}] 26 | self.assertDictEqual( 27 | memory_profiler._get_obj_count_difference(objects1, objects2), 28 | {int: 1, str: 1, list: 1}) 29 | 30 | 31 | class CodeEventsTrackerUnittest(unittest.TestCase): 32 | def setUp(self): 33 | self._tracker = object.__new__(memory_profiler._CodeEventsTracker) 34 | 35 | def testTraceMemoryUsage(self): 36 | self._tracker._process = mock.MagicMock() 37 | event, arg = 'line', mock.MagicMock() 38 | memory_info = mock.MagicMock() 39 | curr_memory = memory_info.rss 40 | self._tracker._process.memory_info.return_value = memory_info 41 | frame1, frame2 = mock.MagicMock(), mock.MagicMock() 42 | frame3, frame4 = mock.MagicMock(), mock.MagicMock() 43 | frame1.f_lineno, frame2.f_lineno = 1, 2 44 | frame3.f_lineno, frame4.f_lineno = 3, 4 45 | code1, code2 = frame1.f_code, frame2.f_code 46 | code3, code4 = frame3.f_code, frame4.f_code 47 | name1, name2 = code1.co_name, code2.co_name 48 | name3, name4 = code3.co_name, code4.co_name 49 | fname1, fname2 = code1.co_filename, code2.co_filename 50 | fname3, fname4 = code3.co_filename, code4.co_filename 51 | self._tracker.target_modules = { 52 | code1.co_filename, code2.co_filename, 53 | code3.co_filename, code4.co_filename} 54 | self._tracker._events_list = deque() 55 | 56 | self._tracker._trace_memory_usage(frame1, event, arg) 57 | self._tracker._trace_memory_usage(frame2, event, arg) 58 | self._tracker._trace_memory_usage(frame3, event, arg) 59 | self._tracker._trace_memory_usage(frame4, event, arg) 60 | 61 | self.assertEqual( 62 | self._tracker._events_list, 63 | deque(((1, curr_memory, name1, fname1), 64 | (2, curr_memory, name2, fname2), 65 | (3, curr_memory, name3, fname3), 66 | (4, curr_memory, name4, fname4)))) 67 | 68 | def testCodeEvents_NoDuplicates(self): 69 | self._tracker._resulting_events = [] 70 | self._tracker.mem_overhead = 0 71 | frame1, frame2 = mock.MagicMock(), mock.MagicMock() 72 | frame3, frame4 = mock.MagicMock(), mock.MagicMock() 73 | code1, code2 = frame1.f_code, frame2.f_code 74 | code3, code4 = frame3.f_code, frame4.f_code 75 | name1, name2 = code1.co_name, code2.co_name 76 | name3, name4 = code3.co_name, code4.co_name 77 | fname1, fname2 = code1.co_filename, code2.co_filename 78 | fname3, fname4 = code3.co_filename, code4.co_filename 79 | 80 | self._tracker._events_list = deque(( 81 | (1, 1024 * 1024, name1, fname1), 82 | (2, 1024 * 1024, name2, fname2), 83 | (3, 1024 * 1024, name3, fname3), 84 | (4, 1024 * 1024, name4, fname4))) 85 | 86 | self.assertListEqual( 87 | self._tracker.code_events, 88 | [[1, 1, 1.0, name1, fname1], 89 | [2, 2, 1.0, name2, fname2], 90 | [3, 3, 1.0, name3, fname3], 91 | [4, 4, 1.0, name4, fname4]]) 92 | 93 | def testCodeEvents_Duplicates(self): 94 | self._tracker._resulting_events = [] 95 | self._tracker.mem_overhead = 0 96 | frame1, frame2 = mock.MagicMock(), mock.MagicMock() 97 | code1, code2 = frame1.f_code, frame2.f_code 98 | name1, name2 = code1.co_name, code2.co_name 99 | fname1, fname2 = code1.co_filename, code2.co_filename 100 | 101 | self._tracker._events_list = deque(( 102 | (1, 1024 * 1024, name1, fname1), 103 | (1, 1024 * 1024 * 2, name1, fname1), 104 | (1, 1024 * 1024 * 3, name1, fname1), 105 | (2, 1024 * 1024, name2, fname2))) 106 | 107 | self.assertListEqual( 108 | self._tracker.code_events, 109 | [[1, 1, 1.0, name1, fname1], 110 | [2, 1, 2.0, name1, fname1], 111 | [3, 1, 3.0, name1, fname1], 112 | [4, 2, 1.0, name2, fname2]]) 113 | 114 | # pylint: enable=protected-access, missing-docstring, too-many-locals 115 | -------------------------------------------------------------------------------- /vprof/tests/profiler_e2e.py: -------------------------------------------------------------------------------- 1 | """End-to-end tests for Python profiler wrapper.""" 2 | # pylint: disable=missing-docstring, blacklisted-name 3 | import functools 4 | import gzip 5 | import json 6 | import inspect 7 | import threading 8 | import time 9 | import unittest 10 | import urllib.request 11 | 12 | from vprof import profiler 13 | from vprof import stats_server 14 | from vprof import runner 15 | from vprof.tests import test_pkg # pylint: disable=unused-import 16 | 17 | _HOST, _PORT = 'localhost', 12345 18 | _MODULE_FILENAME = 'vprof/tests/test_pkg/dummy_module.py' 19 | _PACKAGE_PATH = 'vprof/tests/test_pkg/' 20 | _POLL_INTERVAL = 0.01 21 | 22 | 23 | class ProfilerModuleEndToEndTest(unittest.TestCase): 24 | 25 | def setUp(self): 26 | program_stats = profiler.Profiler( 27 | _MODULE_FILENAME).run() 28 | stats_handler = functools.partial( 29 | stats_server.StatsHandler, program_stats) 30 | self.server = stats_server.StatsServer( 31 | (_HOST, _PORT), stats_handler) 32 | threading.Thread( 33 | target=self.server.serve_forever, 34 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 35 | 36 | def tearDown(self): 37 | self.server.shutdown() 38 | self.server.server_close() 39 | 40 | def testRequest(self): 41 | response = urllib.request.urlopen( 42 | 'http://%s:%s/profile' % (_HOST, _PORT)) 43 | response_data = gzip.decompress(response.read()) 44 | stats = json.loads(response_data.decode('utf-8')) 45 | self.assertEqual(stats['objectName'], '%s (module)' % _MODULE_FILENAME) 46 | self.assertTrue(len(stats['callStats']) > 0) 47 | self.assertTrue(stats['totalTime'] > 0) 48 | self.assertTrue(stats['primitiveCalls'] > 0) 49 | self.assertTrue(stats['totalCalls'] > 0) 50 | 51 | 52 | class ProfilerPackageEndToEndTest(unittest.TestCase): 53 | 54 | def setUp(self): 55 | program_stats = profiler.Profiler( 56 | _PACKAGE_PATH).run() 57 | stats_handler = functools.partial( 58 | stats_server.StatsHandler, program_stats) 59 | self.server = stats_server.StatsServer( 60 | (_HOST, _PORT), stats_handler) 61 | threading.Thread( 62 | target=self.server.serve_forever, 63 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 64 | 65 | def tearDown(self): 66 | self.server.shutdown() 67 | self.server.server_close() 68 | 69 | def testRequest(self): 70 | response = urllib.request.urlopen( 71 | 'http://%s:%s/profile' % (_HOST, _PORT)) 72 | response_data = gzip.decompress(response.read()) 73 | stats = json.loads(response_data.decode('utf-8')) 74 | self.assertEqual(stats['objectName'], '%s (package)' % _PACKAGE_PATH) 75 | self.assertTrue(len(stats['callStats']) > 0) 76 | self.assertTrue(stats['totalTime'] > 0) 77 | self.assertTrue(stats['primitiveCalls'] > 0) 78 | self.assertTrue(stats['totalCalls'] > 0) 79 | 80 | 81 | class ProfilerFunctionEndToEndTest(unittest.TestCase): 82 | 83 | def setUp(self): 84 | 85 | def _func(foo, bar): 86 | baz = foo + bar 87 | time.sleep(0.1) 88 | return baz 89 | self._func = _func 90 | 91 | stats_handler = functools.partial( 92 | stats_server.StatsHandler, {}) 93 | self.server = stats_server.StatsServer( 94 | (_HOST, _PORT), stats_handler) 95 | threading.Thread( 96 | target=self.server.serve_forever, 97 | kwargs={'poll_interval': _POLL_INTERVAL}).start() 98 | 99 | def tearDown(self): 100 | self.server.shutdown() 101 | self.server.server_close() 102 | 103 | def testRequest(self): 104 | runner.run( 105 | self._func, 'p', ('foo', 'bar'), host=_HOST, port=_PORT) 106 | response = urllib.request.urlopen( 107 | 'http://%s:%s/profile' % (_HOST, _PORT)) 108 | response_data = gzip.decompress(response.read()) 109 | stats = json.loads(response_data.decode('utf-8')) 110 | curr_filename = inspect.getabsfile(inspect.currentframe()) 111 | self.assertEqual(stats['p']['objectName'], 112 | '_func @ %s (function)' % curr_filename) 113 | self.assertTrue(len(stats['p']['callStats']) > 0) 114 | self.assertTrue(stats['p']['totalTime'] > 0) 115 | self.assertTrue(stats['p']['primitiveCalls'] > 0) 116 | self.assertTrue(stats['p']['totalCalls'] > 0) 117 | 118 | 119 | # pylint: enable=missing-docstring, blacklisted-name 120 | -------------------------------------------------------------------------------- /vprof/tests/profiler_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access, missing-docstring 2 | import unittest 3 | 4 | from vprof import profiler 5 | from unittest import mock 6 | 7 | 8 | class ProfilerUnittest(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self._profiler = object.__new__(profiler.Profiler) 12 | 13 | def testTransformStats(self): 14 | prof = mock.MagicMock() 15 | prof.total_tt = 0.075 16 | prof.stats = { 17 | ('fname1', 1, 'func1'): (5, 10, 0.001, 0.01, ()), 18 | ('fname1', 5, 'func2'): (10, 15, 0.002, 0.02, ()), 19 | ('fname2', 11, ''): (15, 20, 0.003, 0.045, ()) 20 | } 21 | expected_results = [ 22 | ('fname2', 11, '', 0.045, 60.0, 23 | 20, 15, 0.003, 'fname2', 727188755), 24 | ('fname1', 5, 'func2', 0.02, 26.6667, 25 | 15, 10, 0.002, 'fname1', 591398039), 26 | ('fname1', 1, 'func1', 0.01, 13.3333, 27 | 10, 5, 0.001, 'fname1', 590742678), 28 | ] 29 | 30 | self.assertListEqual( 31 | self._profiler._transform_stats(prof), expected_results) 32 | 33 | # pylint: enable=protected-access, missing-docstring 34 | -------------------------------------------------------------------------------- /vprof/tests/runner_test.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access, missing-docstring 2 | import unittest 3 | 4 | from vprof import runner 5 | from unittest import mock 6 | 7 | 8 | class RunnerUnittest(unittest.TestCase): 9 | 10 | @mock.patch('vprof.runner.run_profilers') 11 | @mock.patch('gzip.compress') 12 | @mock.patch('urllib.request.urlopen') 13 | def testRun_CheckResult(self, unused_urlopen_mock, 14 | unused_compress_mock, run_mock): 15 | run_mock.return_value = { 16 | 'h': {'result': 'foobar', 'total': 200}, 17 | 'p': {'result': 'foobar', 'total': 500} 18 | } 19 | func = lambda x, y: x + y 20 | result = runner.run(func, 'hp', args=('foo', 'bar')) 21 | self.assertEqual(result, 'foobar') 22 | 23 | @mock.patch('vprof.runner.run_profilers') 24 | @mock.patch('gzip.compress') 25 | @mock.patch('urllib.request.urlopen') 26 | @mock.patch('json.dumps') 27 | def testRun_CheckStats(self, json_mock, unused_urlopen_mock, # pylint: disable=no-self-use 28 | unused_compress_mock, run_mock): 29 | run_mock.return_value = { 30 | 'h': {'result': 'foobar', 'total': 200}, 31 | 'p': {'result': 'foobar', 'total': 500} 32 | } 33 | func = lambda x, y: x + y 34 | runner.run(func, 'hp', args=('foo', 'bar')) 35 | json_mock.assert_called_with({ 36 | 'h': {'total': 200}, 37 | 'p': {'total': 500} 38 | }) 39 | 40 | # pylint: enable=protected-access, missing-docstring 41 | -------------------------------------------------------------------------------- /vprof/tests/test_pkg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvdv/vprof/99bb5cd5691a5bfbca23c14ad3b70a7bca6e7ac7/vprof/tests/test_pkg/__init__.py -------------------------------------------------------------------------------- /vprof/tests/test_pkg/__main__.py: -------------------------------------------------------------------------------- 1 | from test_pkg import dummy_module 2 | 3 | dummy_module.dummy_fib(5) 4 | -------------------------------------------------------------------------------- /vprof/tests/test_pkg/dummy_module.py: -------------------------------------------------------------------------------- 1 | def dummy_fib(n): 2 | if n < 2: 3 | return n 4 | return dummy_fib(n - 1) + dummy_fib(n - 2) 5 | -------------------------------------------------------------------------------- /vprof/ui/__tests__/code_heatmap_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const codeHeatmapModule = require('../code_heatmap.js'); 4 | 5 | describe('Code heatmap test suite', () => { 6 | it('Check renderCode', () => { 7 | codeHeatmapModule.CodeHeatmap.prototype.formatSrcLine_ = (n, l, c) => n; 8 | let data = {}, parent = {}; 9 | let calculator = new codeHeatmapModule.CodeHeatmap(parent, data); 10 | 11 | let srcCode = [['line', 1, 'foo'], ['line', 2, 'bar'], ['line', 3, 'baz']]; 12 | let heatmap = {1: 0.1, 2: 0.2, 3: 0.2}; 13 | let executionCount = {1: 1, 2: 1, 3: 1}; 14 | let codeStats = { 15 | 'srcCode': srcCode, 16 | 'heatmap': heatmap, 17 | 'executionCount': executionCount, 18 | }; 19 | 20 | let expectedResult = { 21 | 'srcCode': "123", 22 | 'timeMap': {0: 0.1, 1: 0.2, 2: 0.2}, 23 | 'countMap': {0: 1, 1: 1, 2: 1} 24 | }; 25 | expect(calculator.renderCode_(codeStats)).toEqual(expectedResult); 26 | 27 | srcCode = [ 28 | ['line', 1, 'foo'], ['line', 2, 'bar'], 29 | ['skip', 5], ['line', 8, 'hahaha']]; 30 | heatmap = {1: 0.1, 2: 0.1, 8: 0.3}; 31 | executionCount = {1: 1, 2: 1, 8: 1}; 32 | codeStats = { 33 | 'srcCode': srcCode, 34 | 'heatmap': heatmap, 35 | 'executionCount': executionCount, 36 | }; 37 | 38 | expectedResult = { 39 | 'srcCode': "12
5 lines skipped
8", 40 | 'timeMap': {0: 0.1, 1: 0.1, 2: 0.3}, 41 | 'countMap': {0: 1, 1: 1, 2: 1} 42 | }; 43 | expect(calculator.renderCode_(codeStats)).toEqual(expectedResult); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /vprof/ui/__tests__/common_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const commonModule = require('../common.js'); 4 | 5 | describe('Profiler test suite', () => { 6 | it('Check shortenString', () => { 7 | expect(commonModule.shortenString('foobar', 6, true)).toBe('foobar'); 8 | expect(commonModule.shortenString('foobarfoobar', 10, true)).toBe( 9 | '...rfoobar'); 10 | expect(commonModule.shortenString('foobarfoobar', 10, false)).toBe( 11 | 'foobarf...'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /vprof/ui/__tests__/flame_graph_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const flameGraphModule = require('../flame_graph.js'); 4 | 5 | describe('CPU flame graph test suite', () => { 6 | it('Check getNodeName', () => { 7 | let node = {'stack': ['foo', 'bar', 10]}; 8 | expect(flameGraphModule.FlameGraph.getNodeName_(node)).toBe( 9 | 'foo:10 (bar)'); 10 | }); 11 | 12 | it('Check getTruncatedNodeName', () => { 13 | let node = {'stack': ['foo', 'bar.py', 10]}; 14 | 15 | expect(flameGraphModule.FlameGraph.getTruncatedNodeName_(node, 1)).toBe(''); 16 | expect(flameGraphModule.FlameGraph.getTruncatedNodeName_(node, 50)) 17 | .toBe('foo:...'); 18 | expect(flameGraphModule.FlameGraph.getTruncatedNodeName_(node, 500)) 19 | .toBe('foo:10 (bar.py)'); 20 | }); 21 | 22 | it('Check getLeafWithMaxY0', () => { 23 | let node1 = {'y0': 1}; 24 | let node2 = {'y0': 2}; 25 | let nodes = {'children': [node1, node2]}; 26 | expect(flameGraphModule.FlameGraph.getLeafWithMaxY0_(nodes)).toBe(node2); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /vprof/ui/__tests__/memory_stats_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const memoryStatsModule = require('../memory_stats.js'); 4 | 5 | describe('Memory stats test suite', () => { 6 | it('Check generateTooltipText_', () => { 7 | let stats = [10, 11, 15, '', 'foo.py']; 8 | let expectedResult = ( 9 | '

Executed line: 10

' + 10 | '

Line number: 11

' + 11 | '

Function name: [func]

' + 12 | '

Filename: foo.py

' + 13 | '

Memory usage: 15 MB

'); 14 | 15 | expect(memoryStatsModule.MemoryChart.generateTooltipText_(stats)).toBe( 16 | expectedResult); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /vprof/ui/__tests__/profiler_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const profilerModule = require('../profiler.js'); 4 | 5 | describe('Profiler test suite', () => { 6 | it('Dummy test', () => { 7 | expect(1).toBe(1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /vprof/ui/code_heatmap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Code heatmap UI module. 3 | */ 4 | 5 | 'use strict'; 6 | const d3 = require('d3'); 7 | const hljs = require('highlight.js'); 8 | try { 9 | require('./css/highlight.css'); // Necessary for code highlighter to work. 10 | } catch (e) { 11 | // Do nothing, it's workaround for Jest test runner. 12 | } 13 | 14 | /** 15 | * Represents code heatmap. 16 | * @constructor 17 | * @param {Object} parent - Parent element for code heatmap. 18 | * @param {Object} data - Data for code heatmap rendering. 19 | * @property {number} MIN_RUN_COUNT - Min value for line execution count. 20 | * @property {number} MAX_RUN_COUNT - Max value for line execution count. 21 | * @property {string} MIN_RUN_COLOR - Color that represents MIN_RUN_COUNT. 22 | * @property {string} MAX_RUN_COLOR - Color that represents MAX_RUN_COUNT. 23 | * @property {string} HELP_MESSAGE - Tooltip help message. 24 | */ 25 | class CodeHeatmap { 26 | constructor(parent, data) { 27 | this.MIN_RUN_TIME = 0.000001; 28 | this.MAX_RUN_TIME = data.runTime; 29 | this.MIN_RUN_COLOR = '#ebfaeb'; 30 | this.MAX_RUN_COLOR = '#47d147'; 31 | this.HELP_MESSAGE = ( 32 | '

• Hover over line to see execution time and ' + 33 | 'line execution count.

'); 34 | 35 | this.data_ = data; 36 | this.parent_ = parent; 37 | this.heatmapScale_ = d3.scalePow() 38 | .exponent(0.6) 39 | .domain([this.MIN_RUN_TIME, this.MAX_RUN_TIME]) 40 | .range([this.MIN_RUN_COLOR, this.MAX_RUN_COLOR]); 41 | } 42 | 43 | /** Renders code heatmap. */ 44 | render() { 45 | let pageContainer = this.parent_.append('div') 46 | .attr('id', 'heatmap-layout'); 47 | 48 | this.renderHelp_(); 49 | 50 | let moduleList = pageContainer.append('div') 51 | .attr('class', 'heatmap-module-list'); 52 | 53 | moduleList.append('div') 54 | .attr('class', 'heatmap-module-header') 55 | .html('Inspected modules'); 56 | 57 | let moduleTooltip = pageContainer.append('div') 58 | .attr('class', 'content-tooltip content-tooltip-invisible'); 59 | 60 | moduleList.selectAll('.heatmap-module-name') 61 | .data(this.data_.heatmaps) 62 | .enter() 63 | .append('a') 64 | .attr('href', (d) => '#' + d.name) 65 | .append('div') 66 | .attr('class', 'heatmap-module-name') 67 | .style('background-color', (d) => this.heatmapScale_(d.runTime)) 68 | .on('mouseover', (d) => this.showModuleTooltip_( 69 | moduleTooltip, d.runTime, this.data_.runTime)) 70 | .on('mouseout', () => this.hideModuleTooltip_(moduleTooltip)) 71 | .append('text') 72 | .html((d) => d.name); 73 | 74 | let codeContainer = pageContainer.append('div') 75 | .attr('class', 'heatmap-code-container'); 76 | 77 | let heatmapContainer = codeContainer.selectAll('div') 78 | .data(this.data_.heatmaps) 79 | .enter() 80 | .append('div') 81 | .attr('class', 'heatmap-src-file'); 82 | 83 | heatmapContainer.append('a') 84 | .attr('href', (d) => '#' + d.name) 85 | .attr('class', 'heatmap-src-code-header') 86 | .attr('id', (d) => d.name) 87 | .append('text') 88 | .html((d) => d.name); 89 | 90 | let renderedSources = []; 91 | for (let i = 0; i < this.data_.heatmaps.length; i++) { 92 | renderedSources.push(this.renderCode_(this.data_.heatmaps[i])); 93 | } 94 | 95 | let fileContainers = heatmapContainer.append('div') 96 | .attr('class', 'heatmap-src-code') 97 | .append('text') 98 | .html((d, i) => renderedSources[i].srcCode) 99 | .nodes(); 100 | 101 | let codeTooltip = pageContainer.append('div') 102 | .attr('class', 'content-tooltip content-tooltip-invisible'); 103 | 104 | codeContainer.selectAll('.heatmap-src-file') 105 | .each((d, i) => { 106 | d3.select(fileContainers[i]).selectAll('.heatmap-src-line-normal') 107 | .on('mouseover', (d, j, nodes) => { 108 | this.showCodeTooltip_( 109 | nodes[j], codeTooltip, renderedSources, i, j, this.data_.runTime); 110 | }) 111 | .on('mouseout', (d, j, nodes) => { 112 | this.hideCodeTooltip_(nodes[j], codeTooltip); }); 113 | }); 114 | } 115 | 116 | /** 117 | * Shows tooltip with module running time. 118 | * @param {Object} tooltip - Tooltip element. 119 | * @param {number} moduleTime - Module running time. 120 | * @param {number} totalTime - Total running time. 121 | */ 122 | showModuleTooltip_(tooltip, moduleTime, totalTime) { 123 | let percentage = Math.round(10000 * moduleTime / totalTime) / 100; 124 | tooltip.attr('class', 'content-tooltip content-tooltip-visible') 125 | .html('

Time spent: '+ moduleTime + ' s

' + 126 | '

Total running time: ' + totalTime + ' s

' + 127 | '

Percentage: ' + percentage + '%

') 128 | .style('left', d3.event.pageX) 129 | .style('top', d3.event.pageY); 130 | } 131 | 132 | /** 133 | * Hides tooltip with module running time. 134 | * @param {Object} tooltip - Tooltip element. 135 | */ 136 | hideModuleTooltip_(tooltip) { 137 | tooltip.attr('class', 'content-tooltip content-tooltip-invisible'); 138 | } 139 | 140 | /** 141 | * Shows tooltip with line running time. 142 | * @param {Object} element - Highlighted line. 143 | * @param {Object} tooltip - Tooltip element. 144 | * @param {Object} sources - Code and code stats. 145 | * @param {number} fileIndex - Index of source code file. 146 | * @param {number} lineIndex - Index of line in file. 147 | * @param {number} totalTime - Module running time. 148 | */ 149 | showCodeTooltip_(element, tooltip, sources, fileIndex, lineIndex, totalTime) { 150 | if (!sources[fileIndex].countMap[lineIndex]) { 151 | return; 152 | } 153 | let lineRuntime = sources[fileIndex].timeMap[lineIndex]; 154 | let lineRuncount = sources[fileIndex].countMap[lineIndex]; 155 | let percentage = Math.round(10000 * lineRuntime / totalTime) / 100; 156 | d3.select(element).attr('class', 'heatmap-src-line-highlight'); 157 | tooltip.attr('class', 'content-tooltip content-tooltip-visible') 158 | .html('

Time spent: ' + lineRuntime + ' s

' + 159 | '

Total running time: ' + totalTime + ' s

' + 160 | '

Percentage: ' + percentage + '%

' + 161 | '

Run count: ' + lineRuncount + '

') 162 | .style('left', d3.event.pageX) 163 | .style('top', d3.event.pageY); 164 | } 165 | 166 | /** 167 | * Hides tooltip with line running time. 168 | * @param {Object} element - Element representing highlighted line. 169 | * @param {Object} tooltip - Element representing tooltip. 170 | */ 171 | hideCodeTooltip_(element, tooltip) { 172 | d3.select(element).attr('class', 'heatmap-src-line-normal'); 173 | tooltip.attr('class', 'content-tooltip content-tooltip-invisible'); 174 | } 175 | 176 | /** 177 | * Renders profiled sources. 178 | * @param {Object} stats - Source code and all code stats. 179 | * @returns {Object} 180 | */ 181 | renderCode_(stats) { 182 | let outputCode = [], timeMap = {}, srcIndex = 0, countMap = {}; 183 | for (let i = 0; i < stats.srcCode.length; i++) { 184 | if (stats.srcCode[i][0] === 'line') { 185 | let lineNumber = stats.srcCode[i][1], codeLine = stats.srcCode[i][2]; 186 | outputCode.push( 187 | this.formatSrcLine_(lineNumber, codeLine, stats.heatmap[lineNumber])); 188 | timeMap[srcIndex] = stats.heatmap[lineNumber]; 189 | countMap[srcIndex] = stats.executionCount[lineNumber]; 190 | srcIndex++; 191 | } else if (stats.srcCode[i][0] === 'skip') { 192 | outputCode.push( 193 | "
" + stats.srcCode[i][1] + 194 | ' lines skipped
'); 195 | } 196 | } 197 | return { 198 | 'srcCode': outputCode.join(''), 199 | 'timeMap': timeMap, 200 | 'countMap': countMap 201 | }; 202 | } 203 | 204 | /** 205 | * Formats single line of source code. 206 | * @param {number} lineNumber - Line number in code browser. 207 | * @param {string} codeLine - Current line of source code. 208 | * @param {number} lineRuntime - Line runtime. 209 | * @returns {string} 210 | */ 211 | formatSrcLine_(lineNumber, codeLine, lineRuntime) { 212 | let highlightedLine = hljs.highlight('python', codeLine).value; 213 | let backgroundColor = lineRuntime ? this.heatmapScale_(lineRuntime) : ''; 214 | return ( 215 | "
" + 217 | "
" + lineNumber + "
" + 218 | "
" + highlightedLine + "
" + 219 | "
"); 220 | } 221 | 222 | /** Renders code heatmap help. */ 223 | renderHelp_() { 224 | this.parent_.append('div') 225 | .attr('class', 'tabhelp inactive-tabhelp') 226 | .html(this.HELP_MESSAGE); 227 | } 228 | } 229 | 230 | /** 231 | * Renders code heatmap and attaches it to the parent. 232 | * @param {Object} parent - Code heatmap parent element. 233 | * @param {Object} data - Data for code heatmap rendering. 234 | */ 235 | function renderCodeHeatmap(data, parent) { 236 | let heatmap = new CodeHeatmap(parent, data); 237 | heatmap.render(); 238 | } 239 | 240 | module.exports = { 241 | 'CodeHeatmap': CodeHeatmap, 242 | 'renderCodeHeatmap': renderCodeHeatmap, 243 | }; 244 | -------------------------------------------------------------------------------- /vprof/ui/color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Module for coloring functions. 3 | */ 4 | 5 | 'use strict'; 6 | const d3 = require('d3'); 7 | 8 | const MAX_COLOR_DOMAIN_VALUE = Math.pow(2, 32); 9 | 10 | /** 11 | * Creates color scale that maps hash (integer) to specified color range. 12 | */ 13 | function createColorScale() { 14 | return d3.scaleSequential(d3.interpolateRainbow) 15 | .domain([0, MAX_COLOR_DOMAIN_VALUE]); 16 | }; 17 | 18 | module.exports = { 19 | 'createColorScale': createColorScale 20 | }; 21 | -------------------------------------------------------------------------------- /vprof/ui/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Module for common UI functions. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Truncates string if length is greater than maxLength. 9 | * @param {string} s - Input string. 10 | * @param {number} maxLength - Length of the output string. 11 | * @param {boolean} front - Whether append ellipsis at the start or at the end 12 | * of the string. 13 | * @returns {string} 14 | */ 15 | function shortenString(s, maxLength, front) { 16 | if (s.length <= maxLength) { 17 | return s; 18 | } 19 | if (front) { 20 | return '...' + s.substr(3 + (s.length - maxLength), s.length); 21 | } 22 | return s.substr(0, maxLength - 3) + '...'; 23 | } 24 | 25 | /** 26 | * Converts UNIX timestamp to string with specified format. 27 | * @param {number} timestamp - UNIX timestamp.. 28 | * @returns {string} 29 | */ 30 | function formatTimestamp(timestamp) { 31 | let lt = new Date(timestamp * 1000); 32 | return ( 33 | lt.getMonth() + 1 + "/" + lt.getDate() + "/" + lt.getFullYear() + " " + 34 | lt.getHours() + ":" + lt.getMinutes() + ":" + 35 | lt.getSeconds()); 36 | } 37 | 38 | module.exports = { 39 | 'shortenString': shortenString, 40 | 'formatTimestamp': formatTimestamp, 41 | }; 42 | -------------------------------------------------------------------------------- /vprof/ui/css/code_heatmap.css: -------------------------------------------------------------------------------- 1 | #heatmap-layout { 2 | display: flex; 3 | height: 99%; 4 | flex-direction: row; 5 | } 6 | 7 | .heatmap-module-list { 8 | padding: 8px; 9 | padding-left: 50px; 10 | width: 25%; 11 | } 12 | 13 | .heatmap-module-header { 14 | font-size: 14px; 15 | padding-top: 10px; 16 | } 17 | 18 | .heatmap-module-name { 19 | direction: ltr; 20 | font-family: "Lucida Sans Typewriter", "Lucida Console", Monaco; 21 | font-size: 12px; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | white-space: pre; 25 | } 26 | 27 | .heatmap-code-container { 28 | min-width: 45%; 29 | overflow-x: auto; 30 | overflow-y: auto; 31 | padding: 5px; 32 | } 33 | 34 | .heatmap-src-file { 35 | padding-bottom: 15px; 36 | } 37 | 38 | .heatmap-src-code { 39 | background: #FBFBFB; 40 | font-family: "Lucida Sans Typewriter", "Lucida Console", Monaco; 41 | font-size: 12px; 42 | white-space: pre; 43 | } 44 | 45 | .heatmap-src-code-header { 46 | font-family: "Lucida Sans Typewriter", "Lucida Console", Monaco; 47 | font-size: 12px; 48 | } 49 | 50 | .heatmap-src-line-normal { 51 | display: block; 52 | opacity: 1; 53 | } 54 | 55 | .heatmap-src-line-highlight { 56 | display: block; 57 | opacity: 0.6; 58 | } 59 | 60 | .heatmap-src-line-number { 61 | background-color: #D2D7D3; 62 | display: table-cell; 63 | text-align: right; 64 | padding-right: 8px; 65 | width: 28px; 66 | } 67 | 68 | .heatmap-src-line-code { 69 | display: table-cell; 70 | padding-left: 5px; 71 | } 72 | 73 | .heatmap-skip-line { 74 | padding: 5px; 75 | text-align: center; 76 | } 77 | 78 | -------------------------------------------------------------------------------- /vprof/ui/css/flame_graph.css: -------------------------------------------------------------------------------- 1 | .flame-graph-rect-normal { 2 | stroke: white; 3 | stroke-width: 1; 4 | } 5 | 6 | .flame-graph-rect-hightlight { 7 | stroke: white; 8 | stroke-width: 2; 9 | } 10 | 11 | .flame-graph-no-data-message { 12 | background: #BCBCBC; 13 | border-radius: 5px; 14 | font-size: 16px; 15 | top: 50%; 16 | left: 50%; 17 | opacity: 0.9; 18 | transform: translate(-50%, -50%); 19 | padding: 15px; 20 | position: fixed; 21 | } 22 | -------------------------------------------------------------------------------- /vprof/ui/css/highlight.css: -------------------------------------------------------------------------------- 1 | @import url('node_modules/highlight.js/styles/github-gist.css'); 2 | -------------------------------------------------------------------------------- /vprof/ui/css/memory_stats.css: -------------------------------------------------------------------------------- 1 | .memory-info-container { 2 | display: flex; 3 | flex-direction: row; 4 | height: 100%; 5 | } 6 | 7 | .memory-usage-graph { 8 | width: 70%; 9 | } 10 | 11 | .memory-table-wrapper { 12 | overflow-x: scroll; 13 | overflow-y: auto; 14 | width: 30%; 15 | } 16 | 17 | .memory-objects-table { 18 | display: table; 19 | background: white; 20 | font-size: 14px; 21 | margin: 5px; 22 | margin-right: 10px; 23 | width: 100%; 24 | } 25 | 26 | .memory-objects-table td { 27 | padding-left: 5px; 28 | padding-right: 5px; 29 | } 30 | 31 | .memory-objects-table tr:hover { 32 | background-color: #FD8D3C; 33 | } 34 | 35 | .memory-table-header { 36 | background: #BCBCBC; 37 | font-weight: bold; 38 | } 39 | 40 | .memory-table-name { 41 | background: #F1F1F1; 42 | font-weight: bold; 43 | text-align: center; 44 | padding: 3px; 45 | width: 100%; 46 | } 47 | 48 | .memory-table-row:nth-child(even) { 49 | background-color: #F2F2F2 50 | } 51 | 52 | .memory-graph { 53 | fill: #9EDAE5; 54 | opacity: 0.6; 55 | } 56 | 57 | .memory-graph-axis { 58 | fill: none; 59 | stroke: #636363; 60 | stroke-width: 2px; 61 | } 62 | 63 | .memory-graph-axis text { 64 | font-size: 13px; 65 | fill: black; 66 | stroke: none; 67 | text-anchor: end; 68 | } 69 | 70 | .memory-graph-focus { 71 | fill: #E6550D; 72 | } 73 | 74 | .memory-graph-dot { 75 | fill: #6BAED6; 76 | } 77 | 78 | .memory-graph-focus-line { 79 | stroke: #E6550D; 80 | stroke-dasharray: 4; 81 | } 82 | -------------------------------------------------------------------------------- /vprof/ui/css/profiler.css: -------------------------------------------------------------------------------- 1 | .profiler-content { 2 | display: flex; 3 | flex-direction: row; 4 | height: 100%; 5 | padding-left: 220px; 6 | } 7 | 8 | .profiler-record-table { 9 | display: table; 10 | font-size: 15px; 11 | width: 100%; 12 | } 13 | 14 | .profiler-record-table td { 15 | padding-left: 8px; 16 | } 17 | 18 | .profiler-record-table-wrapper { 19 | height: 100%; 20 | overflow-y: scroll; 21 | width: 70% 22 | } 23 | 24 | .profiler-record-table-header { 25 | background: #A3A3A3; 26 | font-weight: bold; 27 | } 28 | 29 | .profiler-record-normal { 30 | background: #C9C9C9; 31 | } 32 | 33 | .profiler-record-highlight { 34 | background: #FD8D3C; 35 | } 36 | 37 | .profiler-record-cumtime { 38 | padding-right: 8px; 39 | } 40 | 41 | .profiler-legend { 42 | margin-left: 25px; 43 | margin-top: 20px; 44 | } 45 | -------------------------------------------------------------------------------- /vprof/ui/css/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvdv/vprof/99bb5cd5691a5bfbca23c14ad3b70a7bca6e7ac7/vprof/ui/css/progress.gif -------------------------------------------------------------------------------- /vprof/ui/css/vprof.css: -------------------------------------------------------------------------------- 1 | @import "code_heatmap.css"; 2 | @import "flame_graph.css"; 3 | @import "memory_stats.css"; 4 | @import "profiler.css"; 5 | 6 | body { 7 | background: #4C4C4C; 8 | font-family: 'Tahoma', Geneva, sans-serif; 9 | margin-bottom: 5px; 10 | margin-left: 0; 11 | margin-right: 0; 12 | margin-top: 5px; 13 | } 14 | 15 | a:link { 16 | color: #3182BD; 17 | text-decoration: none; 18 | } 19 | 20 | a:visited { 21 | color: #756BB1; 22 | text-decoration: none; 23 | } 24 | 25 | a:hover { 26 | text-decoration: underline; 27 | } 28 | 29 | .main-tab-header { 30 | margin: 0; 31 | } 32 | 33 | .active-tab { 34 | display: block; 35 | } 36 | 37 | .inactive-tab { 38 | display: none; 39 | } 40 | 41 | .main-tab-content { 42 | background: #F1F1F1; 43 | height: 96%; 44 | } 45 | 46 | .tab-content-hidden { 47 | display: none; 48 | } 49 | 50 | .tabhelp { 51 | background: #BCBCBC; 52 | border-radius: 5px; 53 | font-size: 16px; 54 | top: 50%; 55 | left: 50%; 56 | opacity: 0.9; 57 | transform: translate(-50%, -50%); 58 | padding: 15px; 59 | position: fixed; 60 | } 61 | 62 | .inactive-tabhelp { 63 | display: none; 64 | } 65 | 66 | .active-tabhelp { 67 | display: block; 68 | } 69 | 70 | .main-tab-header li { 71 | border-radius: 5px 5px 0px 0px; 72 | font-family: 'Tahoma', Geneva, sans-serif; 73 | font-size: 15px; 74 | cursor: pointer; 75 | display: inline-block; 76 | padding: 3 30px; 77 | } 78 | 79 | .help-button { 80 | background: #BCBCBC; 81 | border-radius: 15px; 82 | cursor: pointer; 83 | display: inline-block; 84 | padding-left: 8px; 85 | padding-right: 8px; 86 | position: absolute; 87 | right: 10px; 88 | } 89 | 90 | .main-tab-selected { 91 | background: #F1F1F1; 92 | } 93 | 94 | .main-tab-not-selected { 95 | background: #BCBCBC; 96 | } 97 | 98 | svg { 99 | font-size: 13px; 100 | padding: 5px; 101 | } 102 | 103 | .content-tooltip-visible { 104 | display: block; 105 | } 106 | 107 | .content-tooltip-invisible { 108 | display: none; 109 | } 110 | 111 | .content-legend { 112 | background: #BCBCBC; 113 | border: 0px; 114 | border-radius: 5px-container; 115 | font: 15px sans-serif; 116 | opacity: 0.9; 117 | padding: 8px; 118 | pointer-events: none; 119 | position: absolute; 120 | text-align: center; 121 | } 122 | 123 | .content-tooltip { 124 | background: white; 125 | border: 0px; 126 | border-radius: 5px; 127 | font-family: sans-serif; 128 | font-size: 12px; 129 | padding: 5px; 130 | pointer-events: none; 131 | position: absolute; 132 | text-align: center; 133 | } 134 | 135 | p { 136 | margin: 0px; 137 | text-align: left; 138 | } 139 | 140 | #main-progress-indicator { 141 | background: url('progress.gif') no-repeat center center; 142 | background-color: #F1F1F1; 143 | height: 100%; 144 | position: absolute; 145 | width: 100%; 146 | } 147 | -------------------------------------------------------------------------------- /vprof/ui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nvdv/vprof/99bb5cd5691a5bfbca23c14ad3b70a7bca6e7ac7/vprof/ui/favicon.ico -------------------------------------------------------------------------------- /vprof/ui/flame_graph.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Flame graph UI module. 3 | */ 4 | 5 | 'use strict'; 6 | const color = require('./color'); 7 | const common = require('./common'); 8 | const d3 = require('d3'); 9 | 10 | /** 11 | * Represents flame graph. 12 | * @constructor 13 | * @param {Object} parent - Parent element for flame graph. 14 | * @param {Object} data - Data for flame graph rendering. 15 | */ 16 | class FlameGraph { 17 | constructor(parent, data) { 18 | this.PAD_SIZE = 10; 19 | this.HEIGHT = parent.node().scrollHeight - this.PAD_SIZE; 20 | this.WIDTH = parent.node().scrollWidth - this.PAD_SIZE; 21 | this.TEXT_OFFSET_X = 5; 22 | this.TEXT_OFFSET_Y= 14; 23 | this.TEXT_CUTOFF = 0.075 * this.WIDTH; 24 | this.LEGEND_X = this.WIDTH - 500; 25 | this.LEGEND_Y = 100; 26 | this.MIN_TEXT_HEIGHT = 18; 27 | this.HELP_MESSAGE = ( 28 | '

• Hover over node to see node call stats

' + 29 | '

• Click on node to zoom

'+ 30 | '

• Double click to restore original scale

'); 31 | this.NO_DATA_MESSAGE = ( 32 | 'Sorry, no samples. Seems like run time is less than sampling interval.'); 33 | this.data_ = data; 34 | this.parent_ = parent; 35 | this.xScale_ = d3.scaleLinear().domain([0, 1]).range([0, this.WIDTH]); 36 | this.yScale_ = d3.scaleLinear().domain([0, 1]).range([0, this.HEIGHT]); 37 | this.flameGraph_ = d3.partition(); 38 | this.color_ = color.createColorScale(); 39 | } 40 | 41 | /** Renders flame graph. */ 42 | render() { 43 | let canvas = this.parent_.append('svg') 44 | .attr('width', this.WIDTH) 45 | .attr('height', this.HEIGHT) 46 | .attr('transform', 'rotate(180)'); 47 | 48 | let tooltip = this.parent_.append('div') 49 | .attr('class', 'content-tooltip content-tooltip-invisible'); 50 | 51 | this.renderLegend_(); 52 | this.renderHelp_(); 53 | 54 | // Display message and stop if callStats is empty. 55 | if (Object.keys(this.data_.callStats).length === 0) { 56 | this.renderNoDataMessage_(); 57 | return; 58 | } 59 | 60 | let nodes = d3.hierarchy(this.data_.callStats) 61 | .each((d) => d.value = d.data.sampleCount); 62 | 63 | this.flameGraph_(nodes); 64 | 65 | let cells = canvas.selectAll('.flame-graph-cell') 66 | .data(nodes.descendants()) 67 | .enter() 68 | .append('g') 69 | .attr('class', 'flame-graph-cell'); 70 | 71 | // Render flame graph nodes. 72 | nodes = cells.append('rect') 73 | .attr('class', 'flame-graph-rect-normal') 74 | .attr('x', (d) => this.xScale_(d.x0)) 75 | .attr('y', (d) => this.yScale_(d.y0)) 76 | .attr('width', (d) => this.xScale_(d.x1 - d.x0)) 77 | .attr('height', (d) => this.yScale_(d.y1 - d.y0)) 78 | .style('fill', (d) => this.color_(d.data.colorHash)) 79 | .on('mouseover', (d, i, n) => this.showTooltip_(n[i], tooltip, d.data)) 80 | .on('mouseout', (d, i, n) => this.hideTooltip_(n[i], tooltip)); 81 | 82 | let titles = cells.append('text') 83 | .attr('x', (d) => this.xScale_(d.x0) + this.TEXT_OFFSET_X) 84 | .attr('y', (d) => this.yScale_(d.y0) + this.TEXT_OFFSET_Y) 85 | .attr('transform', (d) => { 86 | // Reverse text back. 87 | let rx = this.xScale_(d.x0) + ( 88 | this.xScale_(d.x1) - this.xScale_(d.x0)) / 2; 89 | let ry = this.yScale_(d.y0) + ( 90 | this.yScale_(d.y1) - this.yScale_(d.y0)) / 2; 91 | return 'rotate(180, ' + rx + ', ' + ry + ')'; 92 | }) 93 | .text((d, i, n) => { 94 | let nodeWidth = n[i].previousElementSibling.getAttribute('width'); 95 | return FlameGraph.getTruncatedNodeName_(d.data, nodeWidth); }) 96 | .attr('visibility', (d, i, n) => { 97 | let nodeHeight = n[i].previousElementSibling.getAttribute('height'); 98 | return nodeHeight > this.MIN_TEXT_HEIGHT ? 'visible': 'hidden'; 99 | }); 100 | 101 | nodes.on('click', (d) => this.zoomIn_(d, nodes, titles)); 102 | canvas.on('dblclick', (d) => this.zoomOut_(nodes, titles)); 103 | } 104 | 105 | /** 106 | * Handles zoom in. 107 | * @param {Object} node - Focus node. 108 | * @param {Object} allNodes - All flame graph nodes. 109 | * @param {Object} titles - All flame graph node titles. 110 | */ 111 | zoomIn_(node, allNodes, titles) { 112 | let leaf = FlameGraph.getLeafWithMaxY0_(node); 113 | this.xScale_.domain([node.x0, node.x1]); 114 | this.yScale_.domain([node.y0, leaf.y0 + (leaf.y1 - leaf.y0)]); 115 | allNodes.attr('x', (d) => this.xScale_(d.x0)) 116 | .attr('y', (d) => this.yScale_(d.y0)) 117 | .attr('width', (d) => this.xScale_(d.x1) - this.xScale_(d.x0)) 118 | .attr('height', (d) => this.yScale_(d.y1) - this.yScale_(d.y0)); 119 | this.redrawTitles_(titles); 120 | } 121 | 122 | /** 123 | * Returns leaf node that has max y0 value. 124 | * Used to determine zoom region. 125 | * @param {Object} node - Focus node. 126 | * @returns {Object} 127 | */ 128 | static getLeafWithMaxY0_(node) { 129 | let leaves = []; 130 | let getLeaves = (node) => { 131 | if (!node.hasOwnProperty('children')) { 132 | leaves.push(node); 133 | } else { 134 | for (let i = 0; i < node.children.length; i++) { 135 | getLeaves(node.children[i]); 136 | } 137 | } 138 | }; 139 | getLeaves(node); 140 | 141 | let leafWithMaxY0 = leaves[0]; 142 | for (let i = 0; i < leaves.length; i++) { 143 | if (leaves[i].y0 > leafWithMaxY0.y0) { 144 | leafWithMaxY0 = leaves[i]; 145 | } 146 | } 147 | return leafWithMaxY0; 148 | } 149 | 150 | /** 151 | * Handles zoom out. 152 | * @param {Object} allNodes - All flame graph nodes. 153 | * @param {Object} titles - All flame graph node titles. 154 | */ 155 | zoomOut_(allNodes, titles) { 156 | this.xScale_.domain([0, 1]); 157 | this.yScale_.domain([0, 1]); 158 | allNodes.attr('x', (d) => this.xScale_(d.x0)) 159 | .attr('y', (d) => this.yScale_(d.y0)) 160 | .attr('width', (d) => this.xScale_(d.x1 - d.x0)) 161 | .attr('height', (d) => this.yScale_(d.y1 - d.y0)); 162 | this.redrawTitles_(titles); 163 | } 164 | 165 | /** 166 | * Redraws node titles based on current xScale and yScale states. 167 | * @param {Object} titles - All flame graph node titles. 168 | */ 169 | redrawTitles_(titles) { 170 | titles.attr('x', (d) => this.xScale_(d.x0) + this.TEXT_OFFSET_X) 171 | .attr('y', (d) => this.yScale_(d.y0) + this.TEXT_OFFSET_Y) 172 | .attr('transform', (d) => { 173 | // Reverse text back. 174 | let rx = this.xScale_(d.x0) + ( 175 | this.xScale_(d.x1) - this.xScale_(d.x0)) / 2; 176 | let ry = this.yScale_(d.y0) + ( 177 | this.yScale_(d.y1) - this.yScale_(d.y0)) / 2; 178 | return 'rotate(180, ' + rx + ', ' + ry + ')'; 179 | }) 180 | .text((d) => { 181 | let nodeWidth = this.xScale_(d.x1) - this.xScale_(d.x0); 182 | return FlameGraph.getTruncatedNodeName_(d.data, nodeWidth); 183 | return d.data; 184 | }) 185 | .attr('visibility', (d, i, n) => { 186 | let nodeHeight = n[i].previousElementSibling.getAttribute('height'); 187 | return (nodeHeight > this.MIN_TEXT_HEIGHT) ? 'visible': 'hidden'; 188 | }); 189 | } 190 | 191 | /** 192 | * Shows tooltip and highlights flame graph node. 193 | * @param {Object} element - Flame graph node. 194 | * @param {Object} tooltip - Tooltip element. 195 | * @param {Object} node - Function call info. 196 | */ 197 | showTooltip_(element, tooltip, node) { 198 | d3.select(element).attr('class', 'flame-graph-rect-highlight'); 199 | let funcName = node.stack[0].replace(//g, ">"); 200 | let filename = node.stack[1].replace(//g, ">"); 201 | tooltip.attr('class', 'content-tooltip content-tooltip-visible') 202 | .html('

Function name: ' + funcName + '

' + 203 | '

Line number: ' + node.stack[2] +'

' + 204 | '

Filename: ' + filename +'

' + 205 | '

Sample count: ' + node.sampleCount + '

' + 206 | '

Percentage: ' + node.samplePercentage +'%

') 207 | .style('left', d3.event.pageX) 208 | .style('top', d3.event.pageY); 209 | } 210 | 211 | /** 212 | * Hides tooltip. 213 | * @param {Object} element - Highlighted flame graph node. 214 | * @param {Object} tooltip - Tooltip element. 215 | */ 216 | hideTooltip_(element, tooltip) { 217 | d3.select(element).attr('class', 'flame-graph-rect-normal'); 218 | tooltip.attr('class', 'content-tooltip content-tooltip-invisible'); 219 | }; 220 | 221 | /** Renders flame graph legend. */ 222 | renderLegend_() { 223 | let launchTime = common.formatTimestamp(this.data_.timestamp); 224 | this.parent_.append('div') 225 | .attr('class', 'content-legend') 226 | .html('

Object name: ' + this.data_.objectName + '

' + 227 | '

Run time: ' + this.data_.runTime + ' s

' + 228 | '

Total samples: ' + this.data_.totalSamples + '

' + 229 | '

Sample interval: ' + this.data_.sampleInterval + 230 | ' s

' + 231 | '

Timestamp: ' + launchTime +'

') 232 | .style('left', this.LEGEND_X) 233 | .style('top', this.LEGEND_Y); 234 | } 235 | 236 | /** Renders flame graph help. */ 237 | renderHelp_() { 238 | this.parent_.append('div') 239 | .attr('class', 'tabhelp inactive-tabhelp') 240 | .html(this.HELP_MESSAGE); 241 | } 242 | 243 | /** Renders message when callStats is empty. */ 244 | renderNoDataMessage_() { 245 | this.parent_.append('div') 246 | .attr('class', 'flame-graph-no-data-message') 247 | .html(this.NO_DATA_MESSAGE); 248 | } 249 | 250 | /** 251 | * Returns function info. 252 | * @static 253 | * @param {Object} d - Object representing function call info. 254 | * @returns {string} 255 | */ 256 | static getNodeName_(d) { 257 | return d.stack[0] + ':' + d.stack[2] + ' (' + d.stack[1] + ')'; 258 | } 259 | 260 | /** 261 | * Truncates function name depending on flame graph node length. 262 | * @static 263 | * @param {Object} d - Function info. 264 | * @param {number} rectLength - Length of flame graph node. 265 | * @returns {string} 266 | */ 267 | static getTruncatedNodeName_(d, rectLength) { 268 | let fullname = FlameGraph.getNodeName_(d); 269 | let maxSymbols = rectLength / 7; 270 | return maxSymbols <= 3 ? '' : common.shortenString( 271 | fullname, maxSymbols, false); 272 | } 273 | } 274 | 275 | /** 276 | * Renders flame graph and attaches it to the parent. 277 | * @param {Object} parent - Flame graph parent element. 278 | * @param {Object} data - Data for flame graph rendering. 279 | */ 280 | function renderFlameGraph(data, parent) { 281 | let flameGraph = new FlameGraph(parent, data); 282 | flameGraph.render(); 283 | } 284 | 285 | module.exports = { 286 | 'FlameGraph': FlameGraph, 287 | 'renderFlameGraph': renderFlameGraph, 288 | }; 289 | -------------------------------------------------------------------------------- /vprof/ui/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Page rendering module. 3 | */ 4 | 5 | 'use strict'; 6 | const d3 = require('d3'); 7 | const codeHeatmapModule = require('./code_heatmap.js'); 8 | const flameGraphModule = require('./flame_graph.js'); 9 | const memoryStatsModule = require('./memory_stats.js'); 10 | const profilerModule = require('./profiler.js'); 11 | 12 | const JSON_URI = 'profile'; 13 | const POLL_INTERVAL = 100; // msec 14 | 15 | /** 16 | * Creates empty div with specified ID. 17 | * @param {string} id - div ID. 18 | */ 19 | function createTabContent_(id) { 20 | return d3.select('body') 21 | .append('div') 22 | .attr('class', 'main-tab-content') 23 | .attr('id', id); 24 | } 25 | 26 | /** 27 | * Creates flame graph tab header with specified status and 28 | * appends it to the parent node. 29 | * @param {Object} parent - Parent element to append tab to. 30 | * @param {status} status - Specified tab status. 31 | */ 32 | function createFlameGraphTab_(parent, status) { 33 | parent.append('li') 34 | .attr('class', status) 35 | .text('Flame graph') 36 | .on('click', (d, i, n) => { 37 | d3.selectAll('li') 38 | .attr('class', 'main-tab-not-selected'); 39 | d3.select(n[i]) 40 | .attr('class', 'main-tab-selected'); 41 | showTab_('flame-graph-tab'); 42 | }); 43 | } 44 | 45 | /** 46 | * Creates memory stats tab header with specified status and 47 | * appends it to the parent node. 48 | * @param {Object} parent - Parent element to append tab to. 49 | * @param {status} status - Specified tab status. 50 | */ 51 | function createMemoryChartTab_(parent, status) { 52 | parent.append('li') 53 | .attr('class', status) 54 | .text('Memory stats') 55 | .on('click', (d, i, n) => { 56 | d3.selectAll('li') 57 | .attr('class', 'main-tab-not-selected'); 58 | d3.select(n[i]) 59 | .attr('class', 'main-tab-selected'); 60 | showTab_('memory-chart-tab'); 61 | }); 62 | } 63 | 64 | /** 65 | * Creates code heatmap tab header with specified status and 66 | * appends it to the parent node. 67 | * @param {Object} parent - Parent element to append tab to. 68 | * @param {status} status - Specified tab status. 69 | */ 70 | function createCodeHeatmapTab_(parent, status) { 71 | parent.append('li') 72 | .attr('class', status) 73 | .text('Code heatmap') 74 | .on('click', (d, i, n) => { 75 | d3.selectAll('li') 76 | .attr('class', 'main-tab-not-selected'); 77 | d3.select(n[i]) 78 | .attr('class', 'main-tab-selected'); 79 | showTab_('code-heatmap-tab'); 80 | }); 81 | } 82 | 83 | /** 84 | * Creates profiler tab header with specified status and 85 | * appends it to the parent node. 86 | * @param {Object} parent - Parent element to append tab to. 87 | * @param {status} status - Specified tab status. 88 | */ 89 | function createProfilerTab_(parent, status) { 90 | parent.append('li') 91 | .attr('class', status) 92 | .text('Profiler') 93 | .on('click', (d, i, n) => { 94 | d3.selectAll('li') 95 | .attr('class', 'main-tab-not-selected'); 96 | d3.select(n[i]) 97 | .attr('class', 'main-tab-selected'); 98 | showTab_('profiler-tab'); 99 | }); 100 | } 101 | 102 | /** 103 | * Renders stats page. 104 | * @param {Object} data - Data for page rendering. 105 | */ 106 | function renderPage(data) { 107 | // Remove all existing tabs and their content 108 | // in case if user is refreshing main page. 109 | d3.select('body').selectAll('*').remove(); 110 | 111 | let tabHeader = d3.select('body') 112 | .append('ul') 113 | .attr('class', 'main-tab-header'); 114 | 115 | let props = Object.keys(data); 116 | props.sort(); 117 | for (let i = 0; i < props.length; i++) { 118 | let status = (i === 0) ? 'main-tab-selected' : 'main-tab-not-selected'; 119 | let displayClass = (i === 0) ? 'active-tab' : 'inactive-tab'; 120 | switch (props[i]) { 121 | case 'c': 122 | createFlameGraphTab_(tabHeader, status); 123 | let flameGraph = createTabContent_('flame-graph-tab'); 124 | flameGraphModule.renderFlameGraph(data.c, flameGraph); 125 | flameGraph.classed(displayClass, true); 126 | break; 127 | case 'm': 128 | createMemoryChartTab_(tabHeader, status); 129 | let memoryChart = createTabContent_('memory-chart-tab'); 130 | memoryStatsModule.renderMemoryStats(data.m, memoryChart); 131 | memoryChart.classed(displayClass, true); 132 | break; 133 | case 'h': 134 | createCodeHeatmapTab_(tabHeader, status); 135 | let codeHeatmap = createTabContent_('code-heatmap-tab'); 136 | codeHeatmapModule.renderCodeHeatmap(data.h, codeHeatmap); 137 | codeHeatmap.classed(displayClass, true); 138 | break; 139 | case 'p': 140 | createProfilerTab_(tabHeader, status); 141 | let profilerOutput = createTabContent_('profiler-tab'); 142 | profilerModule.renderProfilerOutput(data.p, profilerOutput); 143 | profilerOutput.classed(displayClass, true); 144 | break; 145 | } 146 | } 147 | 148 | let helpButton = tabHeader.append('div') 149 | .attr('class', 'help-button') 150 | .text('?'); 151 | 152 | document.addEventListener( 153 | 'click', (event) => handleHelpDisplay(event, helpButton)); 154 | } 155 | 156 | /** 157 | * Handles tab help display.. 158 | * @param {Object} event - Mouse click event. 159 | */ 160 | function handleHelpDisplay(event, helpButton) { 161 | if (event.target === helpButton.node()) { 162 | let helpActiveTab = d3.select('.active-tab .tabhelp'); 163 | helpActiveTab.classed( 164 | 'active-tabhelp', !helpActiveTab.classed('active-tabhelp')) 165 | .classed('inactive-tabhelp', !helpActiveTab.classed('inactive-tabhelp')); 166 | } 167 | } 168 | 169 | /** 170 | * Makes tab specified by tabId active. 171 | * @param {string} tabId - Next active tab identifier. 172 | */ 173 | function showTab_(tabId) { 174 | d3.selectAll('.main-tab-content') 175 | .classed('active-tab', false) 176 | .classed('inactive-tab', true); 177 | d3.select('#' + tabId) 178 | .classed('active-tab', true) 179 | .classed('inactive-tab', false); 180 | } 181 | 182 | /** Makes request to server and renders page with received data. */ 183 | function main() { 184 | let progressIndicator = d3.select('body') 185 | .append('div') 186 | .attr('id', 'main-progress-indicator'); 187 | 188 | let timerId = setInterval(() => { 189 | d3.json(JSON_URI, (data) => { 190 | if (Object.keys(data).length !== 0) { 191 | progressIndicator.remove(); 192 | clearInterval(timerId); 193 | renderPage(data); 194 | } 195 | }); 196 | }, POLL_INTERVAL); 197 | } 198 | 199 | main(); 200 | -------------------------------------------------------------------------------- /vprof/ui/memory_stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Memory profiler UI module. 3 | */ 4 | 5 | 'use strict'; 6 | const common = require('./common'); 7 | const d3 = require('d3'); 8 | 9 | /** 10 | * Represents memory chart. 11 | * @constructor 12 | * @param {Object} parent - Parent element for memory chart. 13 | * @param {Object} data - Data for memory chart rendering. 14 | */ 15 | class MemoryChart { 16 | constructor(parent, data) { 17 | this.MARGIN_LEFT = 27; 18 | this.MARGIN_RIGHT = 5; 19 | this.MARGIN_TOP = 15; 20 | this.MARGIN_BOTTOM = 30; 21 | this.HEIGHT_PAD = 10; 22 | this.WIDTH_PAD = 25; 23 | this.MIN_RANGE_C = 0.8; 24 | this.MAX_RANGE_C = 1.2; 25 | this.MOUSE_X_OFFSET = 10; 26 | this.TICKS_NUMBER = 10; 27 | this.FOCUS_RADIUS = 5; 28 | this.DOT_RADIUS = 3; 29 | this.TOOLTIP_OFFSET = 35; 30 | this.HELP_MESSAGE = ( 31 | '

• Scroll on graph to zoom

'+ 32 | '

• Drag to select required area

'); 33 | 34 | this.data_ = data; 35 | this.parent_ = parent; 36 | 37 | // Memory view div size should be specified in CSS to render SVG correctly. 38 | this.memoryView_ = this.parent_.append('div') 39 | .attr('class', 'memory-info-container'); 40 | this.objectsTable_ = this.memoryView_.append('div') 41 | .attr('class', 'memory-table-wrapper') 42 | .append('div') 43 | .attr('class', 'memory-objects-table'); 44 | this.memoryUsageGraph_ = this.memoryView_.append('div') 45 | .attr('class', 'memory-usage-graph'); 46 | 47 | this.TABLE_WIDTH = this.objectsTable_.node().scrollWidth; 48 | this.HEIGHT = this.memoryUsageGraph_.node().scrollHeight - this.HEIGHT_PAD; 49 | this.WIDTH = this.memoryUsageGraph_.node().scrollWidth - this.WIDTH_PAD; 50 | this.GRAPH_HEIGHT = this.HEIGHT - (this.MARGIN_TOP + this.MARGIN_BOTTOM); 51 | this.GRAPH_WIDTH = this.TABLE_WIDTH + this.WIDTH - ( 52 | this.MARGIN_LEFT + this.MARGIN_RIGHT); 53 | this.AXIS_TEXT_X = this.GRAPH_WIDTH - this.TABLE_WIDTH; 54 | this.AXIS_TEXT_Y = 12; 55 | this.AXIS_TEXT_Y_OFFSET = 30; 56 | this.LEGEND_X = this.GRAPH_WIDTH - 450; 57 | this.LEGEND_Y = 100; 58 | this.ZOOM_SCALE_EXTENT = [1, 100]; 59 | this.ZOOM_TRANSLATE_EXTENT = [[0, 0], [this.WIDTH, this.HEIGHT]]; 60 | 61 | this.xScale_ = d3.scaleLinear() 62 | .domain(d3.extent(this.data_.codeEvents, (d) => d[0])) 63 | .range([0, this.GRAPH_WIDTH]); 64 | this.xAxis_ = d3.axisBottom() 65 | .scale(this.xScale_) 66 | .ticks(this.TICKS_NUMBER) 67 | .tickFormat(d3.format(',.0f')); 68 | 69 | // Set tick values explicitly when number of events is small. 70 | if (this.data_.codeEvents.length < this.TICKS_NUMBER) { 71 | let tickValues = []; 72 | for (let i = 0; i < this.data_.codeEvents.length; i++) { 73 | tickValues.push(i); 74 | } 75 | this.xAxis_.tickValues(tickValues); 76 | } else { 77 | this.xAxis_.ticks(this.TICKS_NUMBER); 78 | } 79 | 80 | this.yRange_ = d3.extent(this.data_.codeEvents, (d) => d[2]); 81 | this.yScale_ = d3.scaleLinear() 82 | .domain([ 83 | this.MIN_RANGE_C * this.yRange_[0], this.MAX_RANGE_C * this.yRange_[1]]) 84 | .range([this.GRAPH_HEIGHT, 0]); 85 | this.yAxis_ = d3.axisLeft() 86 | .scale(this.yScale_); 87 | 88 | this.memoryGraph_ = d3.area() 89 | .x((d) => this.xScale_(d[0])) 90 | .y0(this.GRAPH_HEIGHT) 91 | .y1((d) => this.yScale_(d[2])); 92 | } 93 | 94 | /** Renders memory chart. */ 95 | render() { 96 | let canvas = this.memoryUsageGraph_.append('svg') 97 | .attr('width', this.WIDTH) 98 | .attr('height', this.HEIGHT) 99 | .append('g') 100 | .attr('transform', 101 | 'translate(' + this.MARGIN_LEFT + ',' + this.MARGIN_TOP + ')'); 102 | 103 | let tooltip = this.memoryUsageGraph_.append('div') 104 | .attr('class', 'content-tooltip content-tooltip-invisible'); 105 | 106 | this.renderObjectsTable_(); 107 | this.renderLegend_(); 108 | this.renderHelp_(); 109 | 110 | let path = canvas.append('path') 111 | .attr('class', 'memory-graph') 112 | .attr('d', this.memoryGraph_(this.data_.codeEvents)); 113 | 114 | let focus = canvas.append('circle') 115 | .style('display', 'none') 116 | .attr('class', 'memory-graph-focus') 117 | .attr('r', this.FOCUS_RADIUS) 118 | .attr('transform', 'translate(' + (-100) + ', ' + (-100) + ')'); 119 | let focusXLine = canvas.append('line') 120 | .attr('class', 'memory-graph-focus-line') 121 | .attr('y1', this.GRAPH_HEIGHT); 122 | let focusYLine = canvas.append('line') 123 | .attr('class', 'memory-graph-focus-line') 124 | .attr('x1', 0); 125 | 126 | let xGroup = canvas.append('g') 127 | .attr('class', 'x memory-graph-axis') 128 | .attr('transform', 'translate(0,' + this.GRAPH_HEIGHT + ')') 129 | .call(this.xAxis_); 130 | xGroup.append('text') 131 | .attr('x', this.AXIS_TEXT_X) 132 | .attr('y', this.AXIS_TEXT_Y - this.AXIS_TEXT_Y_OFFSET) 133 | .attr('dy', '.71em') 134 | .text('Executed lines'); 135 | 136 | let yGroup = canvas.append('g') 137 | .attr('class', 'y memory-graph-axis') 138 | .call(this.yAxis_); 139 | yGroup.append('text') 140 | .attr('transform', 'rotate(-90)') 141 | .attr('y', this.AXIS_TEXT_Y) 142 | .attr('dy', '.71em') 143 | .text('Memory usage, MB'); 144 | 145 | let zoom = d3.zoom() 146 | .scaleExtent(this.ZOOM_SCALE_EXTENT) 147 | .translateExtent(this.ZOOM_TRANSLATE_EXTENT) 148 | .on('zoom', () => { 149 | let t = d3.event.transform; 150 | xGroup.call(this.xAxis_.scale(t.rescaleX(this.xScale_))); 151 | path.attr( 152 | 'transform', 'translate(' + t.x + ' 0) ' + 'scale(' + t.k + ' 1)'); 153 | }); 154 | 155 | canvas.call(zoom); 156 | canvas.style('pointer-events', 'all') 157 | .on('mouseover', () => this.showFocus_(focus, focusXLine, focusYLine)) 158 | .on('mouseout', () => this.hideFocus_( 159 | focus, tooltip, focusXLine, focusYLine)) 160 | .on('mousemove', () => this.redrawFocus_( 161 | canvas, focus, tooltip, focusXLine, focusYLine)); 162 | } 163 | 164 | /** Renders memory chart legend. */ 165 | renderLegend_() { 166 | let launchTime = common.formatTimestamp(this.data_.timestamp); 167 | this.parent_.append('div') 168 | .attr('class', 'content-legend') 169 | .html('

Object name: ' + this.data_.objectName + '

' + 170 | '

Total lines executed: ' + this.data_.totalEvents + 171 | '

' + '

Timestamp: ' + launchTime +'

') 172 | .style('left', this.LEGEND_X) 173 | .style('top', this.LEGEND_Y); 174 | } 175 | 176 | /** 177 | * Hides graph focus, guiding lines and tooltip. 178 | * @param {Object} focus - Focus circle. 179 | * @param {Object} tooltip - Graph tooltip. 180 | * @param {Object} focusXLine - Guiding line parallel to OY. 181 | * @param {Object} focusYLine - Guiding line parallel to OX. 182 | */ 183 | hideFocus_(focus, tooltip, focusXLine, focusYLine) { 184 | focus.style('display', 'none'); 185 | tooltip.attr('class', 'content-tooltip content-tooltip-invisible'); 186 | focusXLine.style('display', 'none'); 187 | focusYLine.style('display', 'none'); 188 | } 189 | 190 | /** 191 | * Shows graph focus, guiding lines and tooltip. 192 | * @param {Object} focus - Focus circle. 193 | * @param {Object} tooltip - Graph tooltip. 194 | * @param {Object} focusXLine - Guiding line parallel to OY. 195 | * @param {Object} focusYLine - Guiding line parallel to OX. 196 | */ 197 | showFocus_(focus, focusXLine, focusYLine) { 198 | focus.style('display', null); 199 | focusXLine.style('display', null); 200 | focusYLine.style('display', null); 201 | } 202 | 203 | /** 204 | * Redraws graph focus, guiding lines and tooltip. 205 | * @param {Object} canvas - Graph canvas. 206 | * @param {Object} focus - Focus circle. 207 | * @param {Object} tooltip - Graph tooltip. 208 | * @param {Object} focusXLine - Guiding line parallel to OY. 209 | * @param {Object} focusYLine - Guiding line parallel to OX. 210 | */ 211 | redrawFocus_(canvas, focus, tooltip, 212 | focusXLine, focusYLine) { 213 | let t = d3.zoomTransform(canvas.node()); 214 | let crds = d3.mouse(canvas.node()); 215 | let xCoord = (crds[0] - t.x) / t.k; 216 | let closestIndex = Math.round(this.xScale_.invert(xCoord)) - 1; 217 | let closestY = this.yScale_(this.data_.codeEvents[closestIndex][2]); 218 | let closestX = t.k * this.xScale_( 219 | this.data_.codeEvents[closestIndex][0]) + t.x; 220 | 221 | focus.attr('transform', 'translate(' + closestX + ', ' + 222 | closestY + ')'); 223 | focusXLine.attr('x1', closestX) 224 | .attr('x2', closestX) 225 | .attr('y2', closestY); 226 | focusYLine.attr('y1', closestY) 227 | .attr('x2', closestX) 228 | .attr('y2', closestY); 229 | let tooltipText = MemoryChart.generateTooltipText_( 230 | this.data_.codeEvents[closestIndex]); 231 | tooltip.attr('class', 'content-tooltip content-tooltip-visible') 232 | .html(tooltipText) 233 | .style('left', this.TABLE_WIDTH + closestX) 234 | .style('top', closestY - this.TOOLTIP_OFFSET); 235 | } 236 | 237 | /** 238 | * Generates tooltip text from line stats. 239 | * @static 240 | * @param {Object[]} stats - Memory stats of line of code. 241 | * @returns {string} 242 | */ 243 | static generateTooltipText_(stats) { 244 | let result = ''; 245 | if (stats) { 246 | let functionName = stats[3].replace('<', '[').replace('>', ']'); 247 | result = ('

Executed line: ' + stats[0] + '

' + 248 | '

Line number: ' + stats[1] + '

' + 249 | '

Function name: ' + functionName + '

' + 250 | '

Filename: ' + stats[4] + '

' + 251 | '

Memory usage: ' + stats[2] + ' MB

'); 252 | } 253 | return result; 254 | } 255 | 256 | /** Renders memory chart help. */ 257 | renderHelp_() { 258 | this.parent_.append('div') 259 | .attr('class', 'tabhelp inactive-tabhelp') 260 | .html(this.HELP_MESSAGE); 261 | } 262 | 263 | 264 | /** Renders object count table. */ 265 | renderObjectsTable_() { 266 | let tableName = this.objectsTable_.append('tr') 267 | .attr('class', 'memory-table-name'); 268 | 269 | tableName.append('td') 270 | .text('Objects in memory'); 271 | tableName.append('td') 272 | .text(''); 273 | 274 | let tableHeader = this.objectsTable_.append('tr') 275 | .attr('class', 'memory-table-header'); 276 | 277 | tableHeader.append('td') 278 | .text('Objects'); 279 | tableHeader.append('td') 280 | .text('Count'); 281 | 282 | let countRows = this.objectsTable_.selectAll('.memory-table-row') 283 | .data(this.data_.objectsCount) 284 | .enter() 285 | .append('tr') 286 | .attr('class', 'memory-table-row'); 287 | 288 | countRows.append('td') 289 | .text((d) => d[0]); 290 | countRows.append('td') 291 | .text((d) => d[1]); 292 | } 293 | } 294 | 295 | /** 296 | * Renders memory chart and attaches it to the parent. 297 | * @param {Object} parent - Memory chart parent element. 298 | * @param {Object} data - Data for memory chart rendering. 299 | */ 300 | function renderMemoryStats(data, parent) { 301 | let memoryChart = new MemoryChart(parent, data); 302 | memoryChart.render(); 303 | } 304 | 305 | module.exports = { 306 | 'MemoryChart': MemoryChart, 307 | 'renderMemoryStats': renderMemoryStats, 308 | }; 309 | -------------------------------------------------------------------------------- /vprof/ui/profile.html: -------------------------------------------------------------------------------- 1 | 2 | vprof 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vprof/ui/profiler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Profiler wrapper UI module. 3 | */ 4 | 5 | 'use strict'; 6 | const color = require('./color'); 7 | const common = require('./common'); 8 | const d3 = require('d3'); 9 | 10 | /** 11 | * Represents Python profiler UI. 12 | * @constructor 13 | * @param {Object} parent - Parent element for profiler output. 14 | * @param {Object} data - Data for output rendering. 15 | */ 16 | class Profiler { 17 | constructor(parent, data) { 18 | this.PATH_CHAR_COUNT = 70; 19 | this.HELP_MESSAGE = ( 20 | '

• Hover over record to see detailed stats

'); 21 | 22 | this.data_ = data; 23 | this.parent_ = parent; 24 | this.color_ = color.createColorScale(); 25 | } 26 | 27 | /** Renders profiler output */ 28 | render() { 29 | let content = this.parent_.append('div') 30 | .attr('class', 'profiler-content'); 31 | 32 | let tooltip = this.parent_.append('div') 33 | .attr('class', 'content-tooltip content-tooltip-invisible'); 34 | 35 | let recordsTable = content.append('div') 36 | .attr('class', 'profiler-record-table-wrapper') 37 | .append('div') 38 | .attr('class', 'profiler-record-table'); 39 | 40 | recordsTable.append('tr') 41 | .attr('class', 'profiler-record-table-header') 42 | .html( 43 | 'Color' + 44 | '%' + 45 | 'Function name' + 46 | 'Filename' + 47 | 'Line' + 48 | 'Time'); 49 | 50 | this.renderLegend_(content); 51 | this.renderHelp_(); 52 | 53 | let records = recordsTable.selectAll('.profiler-record-normal') 54 | .data(this.data_.callStats) 55 | .enter() 56 | .append('tr') 57 | .attr('class', 'profiler-record-normal') 58 | .on('mouseover', (d, i, n) => this.showTooltip_(n[i], tooltip, d)) 59 | .on('mouseout', (d, i, n) => this.hideTooltip_(n[i], tooltip)); 60 | 61 | records.append('td') 62 | .attr('class', 'profiler-record-color') 63 | .style('background', (d) => this.color_(d[9])); 64 | 65 | records.append('td') 66 | .attr('class', 'profiler-record-percentage') 67 | .html((d) => d[4] + '%'); 68 | 69 | records.append('td') 70 | .attr('class', 'profiler-record-funcname') 71 | .html((d) => d[2].replace(//g, ">")); 72 | 73 | records.append('td') 74 | .attr('class', 'profiler-record-filename') 75 | .html((d) => common.shortenString(d[0], 70, false)); 76 | 77 | records.append('td') 78 | .attr('class', 'profiler-record-lineno') 79 | .html((d) => d[1]); 80 | 81 | records.append('td') 82 | .attr('class', 'profiler-record-cumtime') 83 | .html((d) => d[3] + 's'); 84 | } 85 | 86 | /** 87 | * Shows record tooltip. 88 | * @param {Object} element - Profiler record element. 89 | * @param {Object} tooltip - Tooltip element. 90 | * @param {Object} node - Profiler record info 91 | */ 92 | showTooltip_(element, tooltip, node) { 93 | d3.select(element).attr('class', 'profiler-record-highlight'); 94 | let funcName = node[2].replace(//g, ">"); 95 | tooltip.attr('class', 'content-tooltip content-tooltip-visible') 96 | .html('

Function name: ' + funcName + '

' + 97 | '

Line number: ' + node[1] +'

' + 98 | '

Filename: ' + node[0] +'

' + 99 | '

Cumulative time: ' + node[3] +'s

' + 100 | '

Number of calls: ' + node[5] +'

' + 101 | '

Cumulative calls: ' + node[6] +'

' + 102 | '

Time per call: ' + node[7] +'s

') 103 | .style('left', d3.event.pageX) 104 | .style('top', d3.event.pageY); 105 | } 106 | 107 | /** 108 | * Hides record tooltip. 109 | * @param {Object} element - Profiler record element. 110 | * @param {Object} tooltip - Tooltip element. 111 | */ 112 | hideTooltip_(element, tooltip) { 113 | d3.select(element).attr('class', 'profiler-record-normal'); 114 | tooltip.attr('class', 'content-tooltip content-tooltip-invisible'); 115 | } 116 | 117 | /** Renders profiler tab legend. */ 118 | renderLegend_(parent) { 119 | let launchTime = common.formatTimestamp(this.data_.timestamp); 120 | parent.append('div') 121 | .attr('class', 'profiler-legend') 122 | .append('div') 123 | .attr('class', 'content-legend') 124 | .append('text') 125 | .html('

Object name: ' + this.data_.objectName + '

' + 126 | '

Total time: ' + this.data_.totalTime + 's

' + 127 | '

Primitive calls: ' + this.data_.primitiveCalls + '

' + 128 | '

Total calls: ' + this.data_.totalCalls + '

' + 129 | '

Timestamp: ' + launchTime +'

'); 130 | } 131 | 132 | /** Renders profiler tab help. */ 133 | renderHelp_() { 134 | this.parent_.append('div') 135 | .attr('class', 'tabhelp inactive-tabhelp') 136 | .html(this.HELP_MESSAGE); 137 | }; 138 | } 139 | 140 | /** 141 | * Renders profiler output and attaches it to the parent. 142 | * @param {Object} parent - Profiler output parent element. 143 | * @param {Object} data - Data for profiler output. 144 | */ 145 | function renderProfilerOutput(data, parent) { 146 | let profilerOutput = new Profiler(parent, data); 147 | profilerOutput.render(); 148 | } 149 | 150 | module.exports = { 151 | 'Profiler': Profiler, 152 | 'renderProfilerOutput': renderProfilerOutput, 153 | }; 154 | --------------------------------------------------------------------------------