├── dozer ├── tests │ ├── __init__.py │ ├── test_util.py │ ├── test_dozer.py │ ├── test_logview.py │ ├── test_reftree.py │ ├── test_profile.py │ └── test_leak.py ├── media │ ├── images │ │ ├── fade.png │ │ ├── pip.gif │ │ └── arrows.gif │ ├── tree.html │ ├── trace.html │ ├── css │ │ ├── main.css │ │ ├── canviz.css │ │ └── profile.css │ ├── graphs.html │ └── javascript │ │ ├── path.js │ │ ├── canviz.js │ │ ├── gvcolors.js │ │ └── excanvas.js ├── templates │ ├── layout.mako │ ├── list_profiles.mako │ ├── show_profile.mako │ └── logbar.mako ├── __init__.py ├── util.py ├── reftree.py ├── logview.py ├── profile.py └── leak.py ├── pytest.ini ├── .gitignore ├── .coveragerc ├── MANIFEST.in ├── LICENSE ├── Makefile ├── setup.cfg ├── tox.ini ├── setup.py ├── README.rst ├── .github └── workflows │ └── build.yml ├── CHANGELOG.rst └── release.mk /dozer/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = dozer 3 | -------------------------------------------------------------------------------- /dozer/media/images/fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgedmin/dozer/HEAD/dozer/media/images/fade.png -------------------------------------------------------------------------------- /dozer/media/images/pip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgedmin/dozer/HEAD/dozer/media/images/pip.gif -------------------------------------------------------------------------------- /dozer/media/images/arrows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgedmin/dozer/HEAD/dozer/media/images/arrows.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__/ 3 | build/ 4 | dist/ 5 | *.egg-info/ 6 | .tox/ 7 | .venv/ 8 | bin/ 9 | .noseids 10 | .coverage 11 | tmp/ 12 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = dozer 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: nocover 7 | pragma: PY2 8 | except ImportError: 9 | except NameError: 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include dozer/media * 2 | recursive-include dozer/templates * 3 | include README.rst 4 | include CHANGELOG.rst 5 | include LICENSE 6 | include Makefile 7 | include tox.ini 8 | include .coveragerc 9 | include .gitignore 10 | 11 | # added by check_manifest.py 12 | include *.yml 13 | 14 | # added by check_manifest.py 15 | include *.mk 16 | 17 | # added by check_manifest.py 18 | include pytest.ini 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE 2 | ------- 3 | This work, including the source code, documentation 4 | and related data, is placed into the public domain. 5 | 6 | The original author is Robert Brewer, with WSGI 7 | modifications by Ben Bangert. 8 | fumanchu@aminus.org 9 | 10 | THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY 11 | OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF 12 | MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE 13 | ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE 14 | RESULTING FROM THE USE, MODIFICATION, OR 15 | REDISTRIBUTION OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /dozer/media/tree.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Dozer: Tree 6 | 7 | 8 | 17 | 18 | 19 | 20 | 23 | 24 |

%(typename)s %(objid)s

25 | 26 |
27 | %(output)s 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /dozer/templates/layout.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ${self.title()} 5 | 6 | ${self.styles()} 7 | 8 | 9 | ${next.body()} 10 | ${self.javascript()} 11 | 12 | 13 | <%def name="title()">Dozer - Profiler 14 | ## 15 | <%def name="styles()"> 16 | 17 | 18 | <%def name="javascript()"> 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python3 2 | 3 | # Used by release.mk 4 | FILE_WITH_VERSION = setup.py 5 | FILE_WITH_CHANGELOG = CHANGELOG.rst 6 | CHANGELOG_DATE_FORMAT = %B %e, %Y 7 | 8 | 9 | .PHONY: all 10 | all: ##: pre-build tox environments 11 | tox -p auto --notest 12 | tox -p auto --notest -e coverage,flake8 13 | 14 | .PHONY: test 15 | test: ##: run tests 16 | tox -p auto 17 | 18 | .PHONY: coverage 19 | coverage: ##: measure test coverage 20 | tox -e coverage 21 | 22 | .PHONY: flake8 23 | flake8: ##: check for style problems 24 | tox -e flake8 25 | 26 | .PHONY: clean 27 | clean: ##: remove build artifacts 28 | rm -rf .venv bin .tox 29 | find -name '*.pyc' -delete 30 | 31 | 32 | include release.mk 33 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [zest.releaser] 2 | date-format = %%B %%e, %%Y 3 | 4 | [flake8] 5 | doctests = yes 6 | max-line-length = 102 7 | extend-ignore = E261,E114,E116,E128,E741,F841 8 | # https://pep8.readthedocs.org/en/latest/intro.html#error-codes 9 | # E261: at least two spaces before inline comment 10 | # E114: indentation is not a multiple of four (comment) 11 | # E116: unexpected indentation (comment) 12 | # E128: continuation line under-indented for visual indent 13 | # E741: do not use variables named 'l', 'O', or 'I' 14 | # F841: local variable ``name`` is assigned to but never used 15 | 16 | [isort] 17 | # from X import ( 18 | # a, 19 | # b, 20 | # ) 21 | multi_line_output = 3 22 | include_trailing_comma = true 23 | lines_after_imports = 2 24 | reverse_relative = true 25 | default_section = THIRDPARTY 26 | known_first_party = dozer 27 | # known_third_party = pytest, ... 28 | # skip = filename... 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310, py311, py312, py313, py314 3 | minversion = 2.4 4 | 5 | [testenv] 6 | extras = test 7 | commands = 8 | pytest {posargs} 9 | 10 | [testenv:py] 11 | commands = 12 | python --version 13 | pytest {posargs} 14 | 15 | [testenv:coverage] 16 | basepython = python3 17 | deps = 18 | coverage 19 | commands = 20 | coverage run -m pytest {posargs} 21 | coverage report -m --fail-under=100 22 | 23 | [testenv:flake8] 24 | deps = flake8 25 | skip_install = true 26 | commands = flake8 setup.py dozer 27 | 28 | [testenv:isort] 29 | deps = isort 30 | skip_install = true 31 | commands = isort {posargs: -c --diff setup.py dozer} 32 | 33 | [testenv:check-manifest] 34 | deps = check-manifest 35 | skip_install = true 36 | commands = check-manifest {posargs} 37 | 38 | [testenv:check-python-versions] 39 | deps = check-python-versions 40 | skip_install = true 41 | commands = check-python-versions {posargs} 42 | -------------------------------------------------------------------------------- /dozer/media/trace.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Dozer: Trace 6 | 7 | 8 | 25 | 26 | 27 | 28 | 31 | 32 |

%(typename)s %(objid)s

33 | 34 |
35 | %(output)s 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /dozer/__init__.py: -------------------------------------------------------------------------------- 1 | from dozer.leak import Dozer 2 | from dozer.logview import Logview 3 | from dozer.profile import Profiler 4 | 5 | 6 | def profile_filter_factory(global_conf, **kwargs): 7 | def filter(app): 8 | return Profiler(app, global_conf, **kwargs) 9 | return filter 10 | 11 | 12 | def profile_filter_app_factory(app, global_conf, **kwargs): 13 | return Profiler(app, global_conf, **kwargs) 14 | 15 | 16 | def dozer_filter_factory(global_conf, **kwargs): 17 | def filter(app): 18 | return Dozer(app, global_conf, **kwargs) 19 | return filter 20 | 21 | 22 | def dozer_filter_app_factory(app, global_conf, **kwargs): 23 | return Dozer(app, global_conf, **kwargs) 24 | 25 | 26 | def logview_filter_factory(global_conf, **kwargs): 27 | def filter(app): 28 | return Logview(app, global_conf, **kwargs) 29 | return filter 30 | 31 | 32 | def logview_filter_app_factory(app, global_conf, **kwargs): 33 | return Logview(app, global_conf, **kwargs) 34 | -------------------------------------------------------------------------------- /dozer/media/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #header { 7 | text-align: center; 8 | background-color: #CCCCCC; 9 | margin: 0; 10 | padding: 0.25em; 11 | } 12 | 13 | #header p { 14 | margin: 0; 15 | padding: 0.25em; 16 | } 17 | 18 | #header h1 a { 19 | text-decoration: none; 20 | } 21 | 22 | #header h1 a:visited { 23 | color: black; 24 | } 25 | 26 | h1 { 27 | font: 900 20pt Verdana, sans-serif; 28 | text-align: center; 29 | margin: 0; 30 | padding: 0.25em; 31 | } 32 | 33 | h2 { 34 | text-align: center; 35 | } 36 | 37 | .obj { 38 | border: 1px dashed #CCCCCC; 39 | padding: 0.5em; 40 | margin: 2px; 41 | font: 10pt Arial, sans-serif; 42 | vertical-align: middle; 43 | } 44 | 45 | .typename { 46 | font-weight: bold; 47 | } 48 | 49 | .refs { 50 | border: 1px solid #CCCCCC; 51 | padding: 0.25em; 52 | margin: 0; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /dozer/util.py: -------------------------------------------------------------------------------- 1 | def asbool(obj): 2 | # Copied from paste.util.converters 3 | # (c) 2005 Ian Bicking and contributors; written for Paste 4 | # (http://pythonpaste.org). Licensed under the MIT license: 5 | # https://www.opensource.org/licenses/mit-license.php 6 | if isinstance(obj, str): 7 | obj = obj.strip().lower() 8 | if obj in ['true', 'yes', 'on', 'y', 't', '1']: 9 | return True 10 | elif obj in ['false', 'no', 'off', 'n', 'f', '0']: 11 | return False 12 | else: 13 | raise ValueError("String is not true/false: %r" % obj) 14 | return bool(obj) 15 | 16 | 17 | def monotonicity(objs): 18 | # Monotonicity is a measurement of value increment over time 19 | # Large monotonicity indicates that number of objects 20 | # has been increased for a while, where leakage is likely to happen 21 | 22 | inc_cnt = 0.0 23 | dec_cnt = 1.0 24 | for i in range(len(objs) - 1): 25 | if objs[i+1] > objs[i]: 26 | inc_cnt += 1 27 | else: 28 | dec_cnt += 1 29 | return inc_cnt / (inc_cnt + dec_cnt) 30 | 31 | 32 | def sort_dict_by_val(d, sort_key, reversed=False): 33 | # Sort a dictionary on its key 34 | return sorted(d.items(), key=lambda x: sort_key(x[1]), reverse=reversed) 35 | -------------------------------------------------------------------------------- /dozer/media/css/canviz.css: -------------------------------------------------------------------------------- 1 | /* $Id: canviz.css 367 2007-03-13 08:57:11Z rschmidt $ */ 2 | 3 | body { 4 | background: #eee; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | #busy { 9 | position: fixed; 10 | z-index: 1; 11 | left: 50%; 12 | top: 50%; 13 | width: 10em; 14 | height: 2em; 15 | margin: -1em 0 0 -5em; 16 | line-height: 2em; 17 | text-align: center; 18 | background: #333; 19 | color: #fff; 20 | opacity: 0.95; 21 | } 22 | #graph_form { 23 | position: fixed; 24 | z-index: 2; 25 | left: 0; 26 | top: 0; 27 | background: #eee; 28 | border: solid #ccc; 29 | border-width: 0 1px 1px 0; 30 | opacity: 0.95; 31 | } 32 | #graph_form, 33 | #graph_form input, 34 | #graph_form select { 35 | font: 12px "Lucida Grande", Arial, Helvetica, sans-serif; 36 | } 37 | #graph_form fieldset { 38 | margin: 0.5em; 39 | padding: 0.5em 0; 40 | text-align: center; 41 | border: solid #ccc; 42 | border-width: 1px 0 0 0; 43 | } 44 | #graph_form legend { 45 | padding: 0 0.5em 0 0; 46 | } 47 | #graph_form input.little_button { 48 | width: 3em; 49 | } 50 | #graph_form select, 51 | #graph_form input.big_button { 52 | width: 15em; 53 | } 54 | #graph_container { 55 | background: #fff; 56 | margin: 0 auto; 57 | } 58 | #graph_texts { 59 | position: relative; 60 | } 61 | #graph_texts div div { 62 | position: absolute; 63 | } 64 | #debug_output { 65 | margin: 1em; 66 | } 67 | -------------------------------------------------------------------------------- /dozer/tests/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from operator import itemgetter 3 | 4 | from dozer.util import asbool, monotonicity, sort_dict_by_val 5 | 6 | 7 | class TestGlobals(unittest.TestCase): 8 | 9 | def check_true(self, value): 10 | self.assertTrue(asbool(value), repr(value)) 11 | 12 | def check_false(self, value): 13 | self.assertFalse(asbool(value), repr(value)) 14 | 15 | def test_asbool(self): 16 | true_values = ['true', 'yes', 'on', 'y', 't', '1'] 17 | for v in true_values: 18 | self.check_true(v) 19 | self.check_true(v.upper()) 20 | self.check_true(' %s ' % v.title()) 21 | self.assertTrue(asbool(True)) 22 | self.assertTrue(asbool(1)) 23 | false_values = ['false', 'no', 'off', 'n', 'f', '0'] 24 | for v in false_values: 25 | self.check_false(v) 26 | self.check_false(v.upper()) 27 | self.check_false(' %s ' % v.title()) 28 | self.assertFalse(asbool(False)) 29 | self.assertFalse(asbool(0)) 30 | self.assertRaises(ValueError, asbool, 'maybe') 31 | 32 | def test_cumulative_derivative(self): 33 | array_1 = [1, 2, 1, 2, 3] 34 | array_2 = [0, 0, 0, 0, 0] 35 | array_empty = [] 36 | self.assertAlmostEqual(0.6, monotonicity(array_1)) 37 | self.assertEqual(0, monotonicity(array_2)) 38 | self.assertEqual(0, monotonicity(array_empty)) 39 | 40 | def test_sort_dict_by_val(self): 41 | d = { 42 | 'a': (5, 9), 43 | 'b': (4, 2), 44 | 'c': (6, 8) 45 | } 46 | key1 = itemgetter(0) 47 | key2 = itemgetter(1) 48 | 49 | s1 = sort_dict_by_val(d, key1) 50 | s2 = sort_dict_by_val(d, key2) 51 | 52 | self.assertEqual(s1[0][0], 'b') 53 | self.assertEqual(s1[1][0], 'a') 54 | self.assertEqual(s2[0][0], 'b') 55 | self.assertEqual(s2[1][0], 'c') 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def read(filename): 7 | return pathlib.Path(__file__).parent.joinpath(filename).read_text() 8 | 9 | 10 | version = '0.10.dev0' 11 | 12 | setup( 13 | name="Dozer", 14 | version=version, 15 | description="WSGI Middleware version of the CherryPy memory leak debugger", 16 | long_description=read('README.rst') + '\n\n' + read('CHANGELOG.rst'), 17 | long_description_content_type='text/x-rst', 18 | keywords='web wsgi memory profiler', 19 | license='CC-PDM-1.0', # i.e. Public Domain 20 | author='Ben Bangert', 21 | author_email='ben@groovie.org', 22 | maintainer='Marius Gedminas', 23 | maintainer_email='marius@gedmin.as', 24 | url='https://github.com/mgedmin/dozer', 25 | packages=find_packages(), 26 | zip_safe=False, 27 | include_package_data=True, 28 | python_requires=">=3.10", 29 | install_requires=[ 30 | "WebOb>=1.2", "Mako", 31 | ], 32 | extras_require={ 33 | 'test': ['pytest', 'WebTest', 'Pillow'], 34 | }, 35 | classifiers=[ 36 | "Development Status :: 3 - Alpha", 37 | "Intended Audience :: Developers", 38 | "Programming Language :: Python :: 3.10", 39 | "Programming Language :: Python :: 3.11", 40 | "Programming Language :: Python :: 3.12", 41 | "Programming Language :: Python :: 3.13", 42 | "Programming Language :: Python :: 3.14", 43 | "Topic :: Internet :: WWW/HTTP", 44 | "Topic :: Internet :: WWW/HTTP :: WSGI", 45 | "Topic :: Software Development :: Libraries :: Python Modules", 46 | ], 47 | entry_points=""" 48 | [paste.filter_factory] 49 | dozer = dozer:dozer_filter_factory 50 | profile = dozer:profile_filter_factory 51 | logview = dozer:logview_filter_factory 52 | [paste.filter_app_factory] 53 | dozer = dozer:dozer_filter_app_factory 54 | profile = dozer:profile_filter_app_factory 55 | logview = dozer:logview_filter_app_factory 56 | """, 57 | ) 58 | -------------------------------------------------------------------------------- /dozer/media/graphs.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Dozer: Types 6 | 7 | 8 | 9 | 29 | 30 | 31 | 32 | 52 | 53 |
54 | %(output)s 55 |
56 | 57 | 58 | 68 | -------------------------------------------------------------------------------- /dozer/tests/test_dozer.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | import unittest 4 | 5 | from dozer import ( 6 | Dozer, 7 | Logview, 8 | Profiler, 9 | dozer_filter_app_factory, 10 | dozer_filter_factory, 11 | logview_filter_app_factory, 12 | logview_filter_factory, 13 | profile_filter_app_factory, 14 | profile_filter_factory, 15 | ) 16 | 17 | 18 | class TestFactories(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.tmpdir = None 22 | 23 | def tearDown(self): 24 | if self.tmpdir: 25 | shutil.rmtree(self.tmpdir) 26 | 27 | def make_tmp_dir(self): 28 | if not self.tmpdir: 29 | self.tmpdir = tempfile.mkdtemp('dozer-tests-') 30 | return self.tmpdir 31 | 32 | def test_profile_filter_factory(self): 33 | self._test_filter_factory(profile_filter_factory, Profiler, 34 | profile_path=self.make_tmp_dir()) 35 | 36 | def test_dozer_filter_factory(self): 37 | self._test_filter_factory(dozer_filter_factory, Dozer) 38 | 39 | def test_logview_filter_factory(self): 40 | self._test_filter_factory(logview_filter_factory, Logview) 41 | 42 | def _test_filter_factory(self, factory, expect, global_conf={}, **kwargs): 43 | app = object() 44 | filter = factory(global_conf, **kwargs) 45 | wrapped_app = filter(app) 46 | self.assertIsInstance(wrapped_app, expect) 47 | 48 | def test_profile_filter_app_factory(self): 49 | self._test_filter_app_factory(profile_filter_app_factory, Profiler, 50 | profile_path=self.make_tmp_dir()) 51 | 52 | def test_dozer_filter_app_factory(self): 53 | self._test_filter_app_factory(dozer_filter_app_factory, Dozer) 54 | 55 | def test_logview_filter_app_factory(self): 56 | self._test_filter_app_factory(logview_filter_app_factory, Logview) 57 | 58 | def _test_filter_app_factory(self, factory, expect, global_conf={}, 59 | **kwargs): 60 | app = object() 61 | wrapped_app = factory(app, global_conf, **kwargs) 62 | self.assertIsInstance(wrapped_app, expect) 63 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Dozer 2 | ===== 3 | 4 | .. image:: https://github.com/mgedmin/dozer/actions/workflows/build.yml/badge.svg?branch=master 5 | :target: https://github.com/mgedmin/dozer/actions 6 | 7 | .. image:: https://coveralls.io/repos/mgedmin/dozer/badge.svg?branch=master 8 | :target: https://coveralls.io/r/mgedmin/dozer 9 | 10 | Dozer was originally a WSGI middleware version of Robert Brewer's 11 | Dowser CherryPy tool that 12 | displays information as collected by the gc module to assist in 13 | tracking down memory leaks. It now also has middleware for profiling 14 | and for looking at logged messages. 15 | 16 | 17 | Tracking down memory leaks 18 | -------------------------- 19 | 20 | Usage:: 21 | 22 | from dozer import Dozer 23 | 24 | # my_wsgi_app is a WSGI application 25 | wsgi_app = Dozer(my_wsgi_app) 26 | 27 | Assuming you're serving your application on the localhost at port 5000, 28 | you can then load up ``http://localhost:5000/_dozer/index`` to view the 29 | gc info. 30 | 31 | 32 | Profiling requests 33 | ------------------ 34 | 35 | Usage:: 36 | 37 | from tempfile import mkdtemp 38 | from dozer import Profiler 39 | 40 | # my_wsgi_app is a WSGI application 41 | wsgi_app = Profiler(my_wsgi_app, profile_path=mkdtemp(prefix='dozer-')) 42 | 43 | Assuming you're serving your application on the localhost at port 5000, 44 | you can then load up ``http://localhost:5000/_profiler`` to view the 45 | list of recorded request profiles. 46 | 47 | The profiles are stored in the directory specified via ``profile_path``. If 48 | you want to keep seeing older profiles after restarting the web app, specify a 49 | fixed directory instead of generating a fresh empty new one with 50 | tempfile.mkdtemp. Make sure the directory is not world-writable, as the 51 | profiles are stored as `insecure Python pickles, which allow arbitrary command 52 | execution during load 53 | `_. 54 | 55 | Here's a blog post by Marius Gedminas that contains `a longer description 56 | of Dozer's profiler `_. 57 | 58 | 59 | Inspecting log messages 60 | ----------------------- 61 | 62 | Usage:: 63 | 64 | from dozer import Logview 65 | 66 | # my_wsgi_app is a WSGI application 67 | wsgi_app = Logview(my_wsgi_app) 68 | 69 | Every text/html page served by your application will get some HTML and 70 | Javascript injected into the response body listing all logging messages 71 | produced by the thread that generated this response. 72 | 73 | Here's a blog post by Marius Gedminas that contains `a longer description 74 | of Dozer's logview `_. 75 | -------------------------------------------------------------------------------- /dozer/templates/list_profiles.mako: -------------------------------------------------------------------------------- 1 | <%inherit file="layout.mako"/> 2 |

All Profiles

3 |

Delete all

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | % for created_time, environ, total_cost, profile_id in profiles: 13 | <% 14 | width = round(400.0 * total_cost / max_cost) 15 | if now == earliest: # I've seen this happen in tests 16 | w = 1 17 | else: 18 | w = 1 - (now-created_time) / (now-earliest) # 0 .. 1 19 | w = round(w * 255) 20 | bg = '#%02x%02x%02x' % (w, w, w) 21 | if w > 128: 22 | fg = 'black' 23 | else: 24 | fg = 'white' 25 | %> 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | % endfor 39 | % if errors: 40 | 41 | 42 | 43 | 44 | 45 | 46 | % for created_time, error, profile_id in errors: 47 | 48 | 49 | 50 | 51 | 52 | 53 | % endfor 54 | % endif 55 |
URLCostTimeProfile ID
${environ['SCRIPT_NAME'] + environ['PATH_INFO'] + environ['QUERY_STRING']|h}\ 29 |
\ 30 | ${total_cost} ms\ 31 |  \ 32 |
\ 33 |
${'%i' % int(now-created_time)} seconds ago${profile_id}delete
ErrorTimeProfile ID
${error|h}${'%i' % int(now-created_time)} seconds ago${profile_id}delete
56 | 57 | <%def name="javascript()"> 58 | ${parent.javascript()} 59 | 73 | 74 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # NB: this name is used in the status badge 2 | name: build 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | workflow_dispatch: 12 | schedule: 13 | - cron: "0 5 * * 6" # 5:00 UTC every Saturday 14 | 15 | jobs: 16 | build: 17 | name: Python ${{ matrix.python-version }} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | 20 | strategy: 21 | matrix: 22 | python-version: 23 | - "3.10" 24 | - "3.11" 25 | - "3.12" 26 | - "3.13" 27 | - "3.14" 28 | os: 29 | - ubuntu-latest 30 | - windows-latest 31 | 32 | steps: 33 | - name: Git clone 34 | uses: actions/checkout@v4 35 | 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: "${{ matrix.python-version }}" 40 | cache: pip 41 | cache-dependency-path: | 42 | setup.py 43 | tox.ini 44 | 45 | - name: Install dependencies 46 | run: | 47 | python -m pip install -U pip 48 | python -m pip install -U setuptools wheel 49 | python -m pip install -U coverage coveralls 50 | python -m pip install -e '.[test]' 51 | 52 | - name: Run tests 53 | run: coverage run -m pytest 54 | 55 | - name: Check test coverage 56 | run: | 57 | coverage report -m --fail-under=100 58 | coverage xml 59 | 60 | - name: Report to coveralls 61 | uses: coverallsapp/github-action@v2 62 | with: 63 | file: coverage.xml 64 | parallel: true 65 | flag-name: run-py${{ matrix.python-version }}-on-${{ matrix.os }} 66 | 67 | finish: 68 | needs: build 69 | if: ${{ always() }} 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Close parallel build 73 | uses: coverallsapp/github-action@v2 74 | with: 75 | parallel-finished: true 76 | 77 | lint: 78 | name: ${{ matrix.toxenv }} 79 | runs-on: ubuntu-latest 80 | 81 | strategy: 82 | matrix: 83 | toxenv: 84 | - flake8 85 | - isort 86 | - check-manifest 87 | - check-python-versions 88 | 89 | steps: 90 | - name: Git clone 91 | uses: actions/checkout@v4 92 | 93 | - name: Set up Python ${{ env.default_python || '3.12' }} 94 | uses: actions/setup-python@v5 95 | with: 96 | python-version: "${{ env.default_python || '3.12' }}" 97 | 98 | - name: Pip cache 99 | uses: actions/cache@v4 100 | with: 101 | path: ~/.cache/pip 102 | key: ${{ runner.os }}-pip-${{ matrix.toxenv }}-${{ hashFiles('tox.ini') }} 103 | restore-keys: | 104 | ${{ runner.os }}-pip-${{ matrix.toxenv }}- 105 | ${{ runner.os }}-pip- 106 | 107 | - name: Install dependencies 108 | run: | 109 | python -m pip install -U pip 110 | python -m pip install -U setuptools wheel 111 | python -m pip install -U tox 112 | 113 | - name: Run ${{ matrix.toxenv }} 114 | run: python -m tox -e ${{ matrix.toxenv }} 115 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Dozer Changelog 2 | =============== 3 | 4 | 0.10 (unreleased) 5 | ----------------- 6 | 7 | - Nothing changed yet. 8 | 9 | 10 | 0.9 (November 7, 2025) 11 | ----------------------- 12 | 13 | - Add support for Python 3.10, 3.11, 3.12, 3.13, and 3.14. 14 | - Drop support for Python 2.7, 3.6, 3.7, 3.8, and 3.9. 15 | - Stop using the cgi module (which wasn't really being used). 16 | - Possibly fix a bug where unbound methods were not being filtered out properly 17 | in memory leak reports. 18 | 19 | 20 | 0.8 (November 13, 2020) 21 | ----------------------- 22 | 23 | - Add support for Python 3.8 and 3.9. 24 | - Drop support for Python 3.5. 25 | - Add UI input for existing "floor" query string parameter 26 | (https://github.com/mgedmin/dozer/issues/2) 27 | - Add UI input to filter type charts by a regular expression 28 | - Add sorting option: monotonicity 29 | - Display traceback on 500 Internal Server Error 30 | - Dicts and sets with unsortable keys are no longer unrepresentable 31 | - Aggregate dynamically-created types with the same ``__name__`` and 32 | ``__module__`` (`issue 9 `_). 33 | 34 | 35 | 0.7 (April 23, 2019) 36 | -------------------- 37 | 38 | * Add support for Python 3.7. 39 | * Drop support for Python 3.3 and 3.4. 40 | * Stop using cgi.escape on Python 3, which is especially important now that 41 | it's been removed from Python 3.8. 42 | 43 | 44 | 0.6 (May 18, 2017) 45 | ------------------ 46 | 47 | * Add support for Python 3.6. 48 | * Drop support for Python 2.6. 49 | * Fix rare TypeError when listing profiles, if two profiles happen to have 50 | the exact same timestamp (https://github.com/mgedmin/dozer/pull/4). 51 | 52 | 0.5 (December 2, 2015) 53 | ---------------------- 54 | * Make /_dozer show the index page (instead of an internal server 55 | error). 56 | * Add support for Python 3.4 and 3.5. 57 | * Drop support for Python 2.5. 58 | * Move to GitHub. 59 | 60 | 0.4 (March 21, 2013) 61 | -------------------- 62 | * 100% test coverage. 63 | * Add support for Python 3.2 or newer. 64 | * Drop dependency on Paste. 65 | 66 | 0.3.2 (February 10, 2013) 67 | -------------------------- 68 | * More comprehensive fixes for issue #5 by Mitchell Peabody. 69 | * Fix TypeError: unsupported operand type(s) for +: 'property' and 'str' 70 | (https://bitbucket.org/bbangert/dozer/issue/3). 71 | * Add a small test suite. 72 | 73 | 0.3.1 (February 6, 2013) 74 | ------------------------ 75 | * Fix TypeError: You cannot set Response.body to a text object 76 | (https://bitbucket.org/bbangert/dozer/issue/5). Patch by Mitchell Peabody. 77 | 78 | 0.3 (December 13, 2012) 79 | ----------------------- 80 | * Emit the "PIL is not installed" only if the Dozer middleware is 81 | actually used. 82 | * Give a name to the Dozer memleak thread. 83 | * You can now supply a function directly to Logview(stack_formatter=fn) 84 | * New configuration option for Logview middleware: tb_formatter, similar 85 | to stack_formatter, but for exception tracebacks. 86 | 87 | 0.2 (December 5, 2012) 88 | ---------------------- 89 | * Adding logview that appends log events for the current request to the bottom 90 | of the html output for html requests. 91 | * Adding profiler for request profiling and call tree viewing. 92 | * Refactored Dozer into its own leak package. 93 | * New maintainer: Marius Gedminas. 94 | 95 | 0.1 (June 14, 2008) 96 | ------------------- 97 | * Initial public release, port from Dowser, a CherryPy tool. 98 | -------------------------------------------------------------------------------- /dozer/templates/show_profile.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | import sys 3 | sys.setrecursionlimit(450) 4 | %> 5 |
6 |

Viewing profile ID: ${id}

7 | 8 |

URL

9 | 10 |
${environ['SCRIPT_NAME'] + environ['PATH_INFO'] + environ['QUERY_STRING']|h}
11 | 12 |

Environment

13 | 14 | 24 | 25 |

Profile

26 |
\ 27 |
    28 | % for node in profile: 29 | <% 30 | if "disable' of '_lsprof.Profiler" in node['function']: 31 | continue 32 | if '' in node['function'] and ':1' in node['function']: 33 | node = profile_data[node['calls'][0]['function']] 34 | %> 35 | ${show_node(node, 0, node['cost'])}\ 36 | % endfor 37 |
38 |
39 |
40 | 41 | <%def name="show_node(node, depth, tottime, callcount=1)"> 42 | <% 43 | import random 44 | parent_id = ''.join([str(random.randrange(0,10)) for x in range(0,9)]) 45 | child_nodes = [x for x in node['calls'] if not x['builtin']] 46 | has_children = len(child_nodes) > 0 47 | if int(float(tottime)) == 0: 48 | proj_width = 1 49 | else: 50 | factor = float(400) / float(tottime) 51 | proj_width = int(float(factor) * float(node['cost'])) 52 | %> 53 | % if has_children: 54 |
  • \ 55 | % else: 56 |
  • \ 57 | % endif 58 |
      59 |
    • ${node['cost']}ms 60 | % if callcount > 1: 61 | ✕${callcount} 62 | % endif 63 | % if has_children: 64 | \ 65 | ${node['func_name']|h}\ 66 | % else: 67 | ${node['func_name']|h}\ 68 | % endif 69 |

    • 70 |
    • 71 |
        72 |
      •  
      • 73 |
      74 |
      75 |
    • 76 |
    • 77 |
    \ 78 | % if has_children: 79 | <% depth = depth + 1 %> 80 | 95 |
  • 96 | % endif 97 | 98 | <%inherit file="layout.mako"/> 99 | <%def name="javascript()"> 100 | ${parent.javascript()} 101 | 104 | 105 | -------------------------------------------------------------------------------- /dozer/tests/test_logview.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | import unittest 4 | 5 | import webtest 6 | 7 | from dozer.logview import Logview, RequestHandler 8 | 9 | 10 | class TestLogview(unittest.TestCase): 11 | 12 | def test_configuration(self): 13 | logview = Logview(None, config={'logview.foo': 'red', 14 | 'traceback.bar': 'green'}, 15 | stack_formatter=traceback.format_stack, 16 | tb_formatter='traceback.format_tb') 17 | self.assertEqual(logview.log_colors, {'foo': 'red'}) 18 | self.assertEqual(logview.traceback_colors, {'bar': 'green'}) 19 | self.assertEqual(logview.reqhandler.stack_formatter, 20 | traceback.format_stack) 21 | self.assertEqual(logview.reqhandler.tb_formatter, traceback.format_tb) 22 | 23 | def test_splice(self): 24 | logview = Logview(None) 25 | testcases = [ 26 | b'[logbar]no body tag', 27 | b'[logbar]text', 28 | b'[logbar]text', 29 | b'[logbar]texthaha invalid markup', 30 | ] 31 | for expected in testcases: 32 | orig_body = expected.replace(b'[logbar]', b'') 33 | self.assertEqual(logview.splice(orig_body, b'[logbar]'), expected) 34 | 35 | 36 | class TestRequestHandler(unittest.TestCase): 37 | 38 | def test_flush(self): 39 | handler = RequestHandler() 40 | handler.buffer[124] = ['pretend record'] 41 | handler.flush() 42 | self.assertEqual(handler.buffer, {}) 43 | 44 | def test_close(self): 45 | handler = RequestHandler() 46 | handler.close() 47 | 48 | 49 | test_log = logging.getLogger(__name__) 50 | 51 | 52 | def hello_world(environ, start_response): 53 | path_info = environ['PATH_INFO'] 54 | if path_info == '/image.png': 55 | content_type = 'image/png' 56 | body = b'[image data]' 57 | else: 58 | content_type = 'text/html; charset=utf-8' 59 | body = b'hello, world!' 60 | if path_info == '/error': 61 | try: 62 | raise Exception('just testing') 63 | except Exception: 64 | test_log.exception('caught exception') 65 | headers = [('Content-Type', content_type), 66 | ('Content-Length', str(len(body)))] 67 | start_response('200 Ok', headers) 68 | return [body] 69 | 70 | 71 | class TestEntireStack(unittest.TestCase): 72 | 73 | def make_wsgi_app(self, **kw): 74 | logview = Logview(hello_world, keep_tracebacks=True, **kw) 75 | return logview 76 | 77 | def make_test_app(self, **kw): 78 | return webtest.TestApp(self.make_wsgi_app(**kw)) 79 | 80 | def test_call(self): 81 | app = self.make_test_app() 82 | resp = app.get('/') 83 | self.assertIn('hello, world!', resp) 84 | self.assertIn('
    ', 105 | tb_formatter=lambda tb: '') 106 | resp = app.get('/error') 107 | print(resp) # for debugging 108 | self.assertIn('hello, world!', resp) 109 | self.assertIn('
    &1`" || { echo; echo "Your working tree is not clean:" 1>&2; $(VCS_STATUS) 1>&2; exit 1; } 72 | endif 73 | 74 | # NB: do not use $(MAKE) in rules with multiple shell commands joined by && 75 | # because then make -n distcheck will actually run those instead of just 76 | # printing what it does 77 | 78 | # TBH this could (and probably should) be replaced by check-manifest 79 | 80 | .PHONY: distcheck-sdist 81 | distcheck-sdist: dist 82 | pkg_and_version=`$(PYTHON) setup.py --name|tr A-Z.- a-z__`-`$(PYTHON) setup.py --version` && \ 83 | rm -rf tmp && \ 84 | mkdir tmp && \ 85 | $(VCS_EXPORT) && \ 86 | cd tmp && \ 87 | tar -xzf ../dist/$$pkg_and_version.tar.gz && \ 88 | diff -ur $$pkg_and_version tree $(DISTCHECK_DIFF_OPTS) && \ 89 | cd $$pkg_and_version && \ 90 | make dist check && \ 91 | cd .. && \ 92 | mkdir one two && \ 93 | cd one && \ 94 | tar -xzf ../../dist/$$pkg_and_version.tar.gz && \ 95 | cd ../two/ && \ 96 | tar -xzf ../$$pkg_and_version/dist/$$pkg_and_version.tar.gz && \ 97 | cd .. && \ 98 | diff -ur one two -x SOURCES.txt -I'^#:' && \ 99 | cd .. && \ 100 | rm -rf tmp && \ 101 | echo "sdist seems to be ok" 102 | 103 | .PHONY: check-latest-rules 104 | check-latest-rules: 105 | ifndef FORCE 106 | @curl -s $(LATEST_RELEASE_MK_URL) | cmp -s release.mk || { printf "\nYour release.mk does not match the latest version at\n$(LATEST_RELEASE_MK_URL)\n\n" 1>&2; exit 1; } 107 | endif 108 | 109 | .PHONY: check-latest-version 110 | check-latest-version: 111 | $(VCS_GET_LATEST) 112 | 113 | .PHONY: check-version-number 114 | check-version-number: 115 | @$(PYTHON) setup.py --version | grep -qv dev || { \ 116 | echo "Please remove the 'dev' suffix from the version number in $(FILE_WITH_VERSION)"; exit 1; } 117 | 118 | .PHONY: check-long-description 119 | check-long-description: 120 | @$(PYTHON) setup.py --long-description | rst2html --exit-status=2 > /dev/null 121 | 122 | .PHONY: check-changelog 123 | check-changelog: 124 | @ver_and_date="$(CHANGELOG_FORMAT)" && \ 125 | grep -q "^$$ver_and_date$$" $(FILE_WITH_CHANGELOG) || { \ 126 | echo "$(FILE_WITH_CHANGELOG) has no entry for $$ver_and_date"; exit 1; } 127 | 128 | 129 | # NB: the Makefile that includes release.mk may want to add additional 130 | # dependencies to the releasechecklist target, but I want 'make distcheck' to 131 | # happen last, so that's why I put it into the recipe and not at the end of the 132 | # list of dependencies. 133 | 134 | .PHONY: releasechecklist 135 | releasechecklist: check-latest-rules check-latest-version check-version-number check-long-description check-changelog 136 | $(MAKE) distcheck 137 | 138 | .PHONY: release 139 | release: releasechecklist do-release ##: prepare a new PyPI release 140 | 141 | .PHONY: do-release 142 | do-release: 143 | $(release_recipe) 144 | 145 | define default_release_recipe_publish_and_tag = 146 | # I'm chicken so I won't actually do these things yet 147 | @echo "Please run" 148 | @echo 149 | @echo " $(PYPI_PUBLISH)" 150 | @echo " $(VCS_TAG)" 151 | @echo 152 | endef 153 | define default_release_recipe_increment_and_push = 154 | @echo "Please increment the version number in $(FILE_WITH_VERSION)" 155 | @echo "and add a new empty entry at the top of the changelog in $(FILE_WITH_CHANGELOG), then" 156 | @echo 157 | @echo ' $(VCS_COMMIT_AND_PUSH)' 158 | @echo 159 | endef 160 | ifndef release_recipe 161 | define release_recipe = 162 | $(default_release_recipe_publish_and_tag) 163 | $(default_release_recipe_increment_and_push) 164 | endef 165 | endif 166 | -------------------------------------------------------------------------------- /dozer/tests/test_reftree.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import unittest 3 | from io import StringIO 4 | from unittest.mock import patch 5 | 6 | from dozer.reftree import ( 7 | CircularReferents, 8 | ReferentTree, 9 | ReferrerTree, 10 | Tree, 11 | count_objects, 12 | get_repr, 13 | repr_dict, 14 | repr_set, 15 | ) 16 | 17 | 18 | class TestTree(unittest.TestCase): 19 | 20 | def test_walk_max_results(self): 21 | tree = Tree(None, None) 22 | tree._gen = lambda x: list(range(20)) 23 | res = list(tree.walk(maxresults=3)) 24 | self.assertEqual(res, 25 | [0, 1, 2, (0, 0, "==== Max results reached ====")]) 26 | 27 | def test_print_tree(self): 28 | tree = Tree(None, None) 29 | tree._gen = lambda x: [(0, 12345, 'ref1'), (1, 23456, 'ref2')] 30 | with patch('sys.stdout', StringIO()) as stdout: 31 | tree.print_tree() 32 | self.assertEqual(stdout.getvalue(), 33 | " 12345 ref1\n" 34 | " 23456 ref2\n") 35 | 36 | 37 | class MyObj(object): 38 | 39 | def __init__(self, **kw): 40 | self.__dict__.update(kw) 41 | 42 | def __repr__(self): 43 | return getattr(self, 'name', 'unnamed-MyObj') 44 | 45 | 46 | class Unrepresentable(object): 47 | def __repr__(self): 48 | raise Exception('haha you cannot represent me') 49 | 50 | 51 | class TestGlobals(unittest.TestCase): 52 | 53 | def test_repr_dict_unsortable(self): 54 | repr_dict({int: 'int', str: 'str'}) 55 | 56 | def test_repr_set_unsortable(self): 57 | repr_set({int, str}) 58 | 59 | def test_get_repr_unrepresentable(self): 60 | # repr(Exception('foo')) is "Exception('foo',)" on Python < 3.7 61 | # repr(Exception('foo')) is "Exception('foo')" on Python >= 3.7 62 | self.assertEqual( 63 | get_repr(Unrepresentable()).replace(',)', ')'), 64 | "unrepresentable object: Exception('haha you cannot represent me')") 65 | 66 | def test_count_objects(self): 67 | gc.collect() 68 | obj = MyObj() 69 | res = count_objects() 70 | self.assertIn((1, MyObj), [(n, c) for (n, c) in res if c is MyObj]) 71 | 72 | 73 | class TestReferentTree(unittest.TestCase): 74 | 75 | def make_tree(self): 76 | tree = ReferentTree(None, None) 77 | tree.maxdepth = 10 78 | tree.seen = {} 79 | return tree 80 | 81 | def test_gen(self): 82 | tree = self.make_tree() 83 | ref = MyObj(name='b') 84 | other = MyObj(name='c') 85 | obj = MyObj(name='a', ref=ref, other=other, again=other) 86 | tree.ignore(ref) 87 | res = list(tree._gen(obj)) 88 | # ref is found either at depth 0 or depth 1, depending on Python 89 | # version, because Pyton 3.13 optimizes __dict__ away and a direct 90 | # reference 91 | self.assertTrue( 92 | (0, id(other), 'c') in res or (1, id(other), 'c') in res 93 | ) 94 | self.assertTrue( 95 | (0, id(other), '!c') in res or (1, id(other), '!c') in res 96 | ) 97 | 98 | 99 | class TestReferrerTree(unittest.TestCase): 100 | 101 | def make_tree(self, maxdepth=10): 102 | tree = ReferrerTree(None, None) 103 | tree.maxdepth = maxdepth 104 | tree.seen = {} 105 | return tree 106 | 107 | def test_gen(self): 108 | tree = self.make_tree() 109 | obj = MyObj() 110 | ref = MyObj(name='a', obj=obj) 111 | res = list(tree._gen(obj)) 112 | # ref is found either at depth 0 or depth 1, depending on Python 113 | # version, because Pyton 3.13 optimizes __dict__ away and a direct 114 | # reference 115 | self.assertTrue((0, id(ref), 'a') in res or (1, id(ref), 'a') in res) 116 | 117 | def test_gen_maxdepth(self): 118 | tree = self.make_tree(maxdepth=1) 119 | obj = MyObj() 120 | # On Python 3.7 the local code frame is somehow not counted 121 | # as a referer! So we need to create at least one more reference. 122 | ref = MyObj(name='a', obj=obj) # noqa 123 | res = list(tree._gen(obj)) 124 | self.assertIn((1, 0, "---- Max depth reached ----"), res) 125 | 126 | 127 | class TestCircularReferents(unittest.TestCase): 128 | 129 | def make_tree(self, obj=None): 130 | tree = CircularReferents(obj, None) 131 | return tree 132 | 133 | def make_cycle(self): 134 | obj = MyObj(name='obj', 135 | a=MyObj(name='a', c=MyObj(name='c')), 136 | b=MyObj(name='b')) 137 | obj.b.obj = obj # make a cycle 138 | return obj 139 | 140 | def test_walk(self): 141 | obj = self.make_cycle() 142 | tree = self.make_tree(obj) 143 | res = list(tree.walk()) 144 | self.assertEqual(len(res), 1) # one cycle 145 | # Python 3.11 introduced lazy object namespaces where obj.__dict__ 146 | # gets created only on access, so the cycle might be MyObj -> MyObj 147 | # or it might be MyObj -> __dict__ -> MyObj -> __dict__, depending 148 | # on Python version and on whether MyObj.__dict__ was ever accessed. 149 | # Python 3.13 never actually treats the __dict__ as a separate object. 150 | self.assertIn(len(res[0]), (2, 4)) 151 | 152 | def test_walk_maxresults(self): 153 | obj = self.make_cycle() 154 | tree = self.make_tree(obj) 155 | res = list(tree.walk(maxresults=1)) 156 | self.assertIn((0, 0, "==== Max results reached ===="), res) 157 | 158 | def test_walk_maxdepth(self): 159 | obj = self.make_cycle() 160 | tree = self.make_tree(obj) 161 | res = list(tree.walk(maxdepth=2)) 162 | self.assertGreater(tree.stops, 0) 163 | 164 | def test_print_tree(self): 165 | obj = self.make_cycle() 166 | tree = self.make_tree(obj) 167 | with patch('sys.stdout', StringIO()) as stdout: 168 | tree.print_tree(maxdepth=5) 169 | self.assertIn( 170 | stdout.getvalue(), 171 | ( 172 | # Python 3.10--3.12: each __dict__ shown separately 173 | '''["dict of len 3: {'a': a, 'b': b, 'name': 'obj'}", 'b',''' 174 | ''' "dict of len 2: {'name': 'b', 'obj': obj}", 'obj']\n''' 175 | '''2 paths stopped because max depth reached\n''', 176 | # Python 3.13: the __dict__s are optimized away 177 | '''['b', 'obj']\n''' 178 | '''9 paths stopped because max depth reached\n''', 179 | ), 180 | ) 181 | -------------------------------------------------------------------------------- /dozer/templates/logbar.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | import time 3 | converter = time.localtime 4 | def format_time(record, start, prev_record=None): 5 | if prev_record: 6 | delta_from_prev = (record.created - prev_record.created) * 1000 7 | return '%+dms' % delta_from_prev 8 | else: 9 | time_from_start = (record.created - start) * 1000 10 | return '%+dms' % time_from_start 11 | 12 | def bg_color(event, log_colors): 13 | if event.name in log_colors: 14 | return log_colors[event.name] 15 | for key in log_colors: 16 | if event.name.startswith(key): 17 | return log_colors[key] 18 | return '#fff' 19 | 20 | def fg_color(frame, traceback_colors): 21 | for key in traceback_colors: 22 | if key in frame: 23 | return traceback_colors[key] 24 | return None 25 | %> 26 | 27 |
    28 |
    \ 29 | View log events for this request 30 | ${'%d' % (1000*tottime)}ms 31 |
    32 | 113 |
    114 | 153 | -------------------------------------------------------------------------------- /dozer/reftree.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import sys 3 | from operator import itemgetter 4 | from types import FrameType 5 | 6 | 7 | class Tree(object): 8 | 9 | def __init__(self, obj, req): 10 | self.req = req 11 | self.obj = obj 12 | self.filename = sys._getframe().f_code.co_filename 13 | self._ignore = {} 14 | 15 | def ignore(self, *objects): 16 | for obj in objects: 17 | self._ignore[id(obj)] = None 18 | 19 | def ignore_caller(self): 20 | f = sys._getframe() # = this function 21 | cur = f.f_back # = the function that called us (probably 'walk') 22 | self.ignore(cur, cur.f_builtins, cur.f_locals, cur.f_globals) 23 | caller = f.f_back # = the 'real' caller 24 | self.ignore(caller, caller.f_builtins, caller.f_locals, caller.f_globals) 25 | 26 | def walk(self, maxresults=100, maxdepth=None): 27 | """Walk the object tree, ignoring duplicates and circular refs.""" 28 | self.seen = {} 29 | self.ignore(self, self.__dict__, self.obj, self.seen, self._ignore) 30 | 31 | # Ignore the calling frame, its builtins, globals and locals 32 | self.ignore_caller() 33 | 34 | self.maxdepth = maxdepth 35 | count = 0 36 | for result in self._gen(self.obj): 37 | yield result 38 | count += 1 39 | if maxresults and count >= maxresults: 40 | yield 0, 0, "==== Max results reached ====" 41 | return 42 | 43 | def print_tree(self, maxresults=100, maxdepth=None): 44 | """Walk the object tree, pretty-printing each branch.""" 45 | self.ignore_caller() 46 | for depth, refid, rep in self.walk(maxresults, maxdepth): 47 | print("%9d %s %s" % (refid, " " * (depth * 2), rep)) 48 | 49 | 50 | def try_sorted(thing): 51 | try: 52 | return sorted(thing) 53 | except TypeError: # pragma: PY3 54 | return thing 55 | 56 | 57 | def repr_dict(obj): 58 | return "dict of len %s: {%s}" % (len(obj), ", ".join( 59 | "%s: %s" % (repr(k), repr(v)) for k, v in try_sorted(obj.items()))) 60 | 61 | 62 | def repr_set(obj): 63 | return "set of len %s: set([%s])" % (len(obj), ", ".join( 64 | map(repr, try_sorted(obj)))) 65 | 66 | 67 | def _repr_container(obj): 68 | return "%s of len %s: %r" % (type(obj).__name__, len(obj), obj) 69 | 70 | 71 | repr_list = _repr_container 72 | repr_tuple = _repr_container 73 | 74 | 75 | def repr_str(obj): 76 | return "%s of len %s: %r" % (type(obj).__name__, len(obj), obj) 77 | 78 | 79 | repr_unicode = repr_str 80 | 81 | 82 | def repr_frame(obj): 83 | return "frame from %s line %s" % (obj.f_code.co_filename, obj.f_lineno) 84 | 85 | 86 | def get_repr(obj, limit=250): 87 | typename = getattr(type(obj), "__name__", None) 88 | handler = globals().get("repr_%s" % typename, repr) 89 | 90 | try: 91 | result = handler(obj) 92 | except Exception as e: 93 | result = "unrepresentable object: %r" % e 94 | 95 | if len(result) > limit: 96 | result = result[:limit] + "..." 97 | 98 | return result 99 | 100 | 101 | class ReferentTree(Tree): 102 | 103 | def _gen(self, obj, depth=0): 104 | if self.maxdepth and depth >= self.maxdepth: 105 | yield depth, 0, "---- Max depth reached ----" 106 | return 107 | 108 | for ref in gc.get_referents(obj): 109 | if id(ref) in self._ignore: 110 | continue 111 | elif id(ref) in self.seen: 112 | yield depth, id(ref), "!" + get_repr(ref) 113 | continue 114 | else: 115 | self.seen[id(ref)] = None 116 | yield depth, id(ref), get_repr(ref) 117 | 118 | for child in self._gen(ref, depth + 1): 119 | yield child 120 | 121 | 122 | class ReferrerTree(Tree): 123 | 124 | def _gen(self, obj, depth=0): 125 | if self.maxdepth and depth >= self.maxdepth: 126 | yield depth, 0, "---- Max depth reached ----" 127 | return 128 | 129 | refs = gc.get_referrers(obj) 130 | refiter = iter(refs) 131 | self.ignore(refs, refiter) 132 | for ref in refiter: 133 | # Exclude all frames that are from this module. 134 | if isinstance(ref, FrameType): # pragma: nocover on Python 3.11 135 | if ref.f_code.co_filename == self.filename: 136 | continue 137 | 138 | if id(ref) in self._ignore: 139 | continue 140 | elif id(ref) in self.seen: # pragma: nocover 141 | yield depth, id(ref), "!" + get_repr(ref) 142 | continue 143 | else: 144 | self.seen[id(ref)] = None 145 | yield depth, id(ref), get_repr(ref) 146 | 147 | for parent in self._gen(ref, depth + 1): 148 | yield parent 149 | 150 | 151 | class CircularReferents(Tree): 152 | 153 | def walk(self, maxresults=100, maxdepth=None): 154 | """Walk the object tree, showing circular referents.""" 155 | self.stops = 0 156 | self.seen = {} 157 | self.ignore(self, self.__dict__, self.seen, self._ignore) 158 | 159 | # Ignore the calling frame, its builtins, globals and locals 160 | self.ignore_caller() 161 | 162 | self.maxdepth = maxdepth 163 | count = 0 164 | for result in self._gen(self.obj): 165 | yield result 166 | count += 1 167 | if maxresults and count >= maxresults: 168 | yield 0, 0, "==== Max results reached ====" 169 | return 170 | 171 | def _gen(self, obj, depth=0, trail=None): 172 | if self.maxdepth and depth >= self.maxdepth: 173 | self.stops += 1 174 | return 175 | 176 | if trail is None: 177 | trail = [] 178 | 179 | for ref in gc.get_referents(obj): 180 | if id(ref) in self._ignore: 181 | continue 182 | elif id(ref) in self.seen: 183 | continue 184 | else: 185 | self.seen[id(ref)] = None 186 | 187 | refrepr = get_repr(ref) 188 | if id(ref) == id(self.obj): 189 | yield trail + [refrepr] 190 | 191 | for child in self._gen(ref, depth + 1, trail + [refrepr]): 192 | yield child 193 | 194 | def print_tree(self, maxresults=100, maxdepth=None): 195 | """Walk the object tree, pretty-printing each branch.""" 196 | self.ignore_caller() 197 | for trail in self.walk(maxresults, maxdepth): 198 | print(trail) 199 | if self.stops: 200 | print("%s paths stopped because max depth reached" % self.stops) 201 | 202 | 203 | def count_objects(): 204 | d = {} 205 | for obj in gc.get_objects(): 206 | objtype = type(obj) 207 | d[objtype] = d.get(objtype, 0) + 1 208 | d = [(v, k) for k, v in d.items()] 209 | d.sort(key=itemgetter(0)) 210 | return d 211 | -------------------------------------------------------------------------------- /dozer/tests/test_profile.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import os 3 | import pickle 4 | import shutil 5 | import sys 6 | import tempfile 7 | import textwrap 8 | import unittest 9 | from collections import namedtuple 10 | from io import StringIO 11 | from unittest.mock import patch 12 | 13 | import webtest 14 | 15 | from dozer.profile import ( 16 | Profiler, 17 | color, 18 | graphlabel, 19 | label, 20 | setup_time, 21 | write_dot_graph, 22 | ) 23 | 24 | 25 | skip_on_windows = unittest.skipIf(sys.platform == 'win32', 26 | 'Windows has a different permissions model') 27 | 28 | 29 | class TestGlobals(unittest.TestCase): 30 | 31 | def make_code_object(self): 32 | d = {} 33 | exec(compile(textwrap.dedent('''\ 34 | # skip a line to get a more interesting line number 35 | def somefunc(): 36 | pass 37 | '''), 'sourcefile.py', 'single'), d) 38 | return d['somefunc'].__code__ 39 | 40 | def test_label(self): 41 | self.assertEqual(label('somefunc'), 'somefunc') 42 | 43 | def test_label_with_code_object(self): 44 | code = self.make_code_object() 45 | self.assertEqual(label(code), 'somefunc sourcefile.py:2') 46 | 47 | def test_graphlabel(self): 48 | # I don't know if this is a plausible example of things that can be 49 | # passed in, I just know the existing code gets rid of double quotes 50 | self.assertEqual(graphlabel('somefunc "dir with spaces/file.py":42'), 51 | "somefunc 'dir with spaces/file.py':42") 52 | 53 | def test_setup_time(self): 54 | self.assertEqual(setup_time(0.004), '4.00') 55 | self.assertEqual(setup_time(0.00025), '0.25') 56 | 57 | def test_color(self): 58 | self.assertEqual(color(0), '#00002C') # cold: darkish blue 59 | self.assertEqual(color(0.25), '#004C4C') # cool: cyanish 60 | self.assertEqual(color(0.5), '#007900') # medium: green 61 | self.assertEqual(color(0.75), '#B4B400') # warm: very dark yellow 62 | self.assertEqual(color(1), '#FF0000') # hot: red 63 | 64 | def test_write_dot_graph_very_very_fast_function(self): 65 | code = self.make_code_object() 66 | profile_entry = namedtuple('profile_entry', 'code totaltime calls') 67 | with patch.object(builtins, 'open', lambda *a: StringIO()): 68 | data = [profile_entry(code=code, totaltime=1, calls=[])] 69 | tree = {'somefunc sourcefile.py:2': dict(cost=0)} 70 | write_dot_graph(data, tree, 'filename.gv') 71 | 72 | 73 | class AppIter(list): 74 | def close(self): 75 | pass 76 | 77 | 78 | def hello_world(environ, start_response): 79 | body = b'hello, world!' 80 | headers = [('Content-Type', 'text/html; charset=utf8'), 81 | ('Content-Length', str(len(body)))] 82 | start_response('200 Ok', headers) 83 | return AppIter([body]) 84 | 85 | 86 | class TestEntireStack(unittest.TestCase): 87 | 88 | def setUp(self): 89 | self.tmpdir = tempfile.mkdtemp('dozer-tests-') 90 | 91 | def tearDown(self): 92 | os.chmod(self.tmpdir, 0o700) 93 | shutil.rmtree(self.tmpdir) 94 | 95 | def make_wsgi_app(self): 96 | profiler = Profiler(hello_world, profile_path=self.tmpdir) 97 | return profiler 98 | 99 | def make_test_app(self): 100 | return webtest.TestApp(self.make_wsgi_app()) 101 | 102 | def list_profiles(self, suffix='.pkl'): 103 | return [fn[:-len(suffix)] for fn in os.listdir(self.tmpdir) 104 | if fn.endswith(suffix)] 105 | 106 | def record_profile(self, app): 107 | before = set(self.list_profiles()) 108 | app.get('/') 109 | after = set(self.list_profiles()) 110 | new = after - before 111 | self.assertEqual(len(new), 1) 112 | return next(iter(new)) 113 | 114 | def save_fake_profile(self, prof_id, data): 115 | with open(os.path.join(self.tmpdir, '%s.pkl' % prof_id), 'wb') as f: 116 | f.write(data) 117 | 118 | def test_application_pass_through(self): 119 | app = self.make_test_app() 120 | resp = app.get('/') 121 | self.assertIn('hello, world!', resp) 122 | # a profile is created 123 | self.assertNotEqual(os.listdir(self.tmpdir), []) 124 | 125 | @skip_on_windows 126 | def test_cannot_save_profile(self): 127 | app = self.make_test_app() 128 | os.chmod(self.tmpdir, 0o500) 129 | self.assertRaises(OSError, self.record_profile, app) 130 | 131 | def test_appiter_close_is_called(self): 132 | app = self.make_test_app() 133 | with patch.object(AppIter, 'close') as close: 134 | app.get('/') 135 | self.assertEqual(close.call_count, 1) 136 | 137 | def test_application_ignore_some_paths(self): 138 | app = self.make_test_app() 139 | resp = app.get('/favicon.ico') 140 | self.assertIn('hello, world!', resp) 141 | self.assertEqual(os.listdir(self.tmpdir), []) 142 | 143 | def test_profiler_index(self): 144 | app = self.make_test_app() 145 | self.record_profile(app) # so the list is not empty 146 | self.record_profile(app) # twice so we have someting to sort 147 | resp = app.get('/_profiler') 148 | self.assertEqual(resp.status_int, 200) 149 | self.assertIn('', resp) 150 | 151 | def test_profiler_index_empty(self): 152 | app = self.make_test_app() 153 | resp = app.get('/_profiler') 154 | self.assertEqual(resp.status_int, 200) 155 | self.assertIn('
    ', resp) 156 | 157 | def test_profiler_broken_pickle(self): 158 | app = self.make_test_app() 159 | self.save_fake_profile(42, b'not a pickle at all') 160 | resp = app.get('/_profiler') 161 | self.assertEqual(resp.status_int, 200) 162 | self.assertIn('
    ', resp) 163 | 164 | def test_profiler_empty_profile(self): 165 | app = self.make_test_app() 166 | self.save_fake_profile(42, pickle.dumps(dict( 167 | environ={'SCRIPT_NAME': '', 'PATH_INFO': '/', 'QUERY_STRING': ''}, 168 | profile={}, 169 | ))) 170 | resp = app.get('/_profiler') 171 | self.assertEqual(resp.status_int, 200) 172 | self.assertIn('
    ', resp) 173 | 174 | def test_profiler_not_found(self): 175 | app = self.make_test_app() 176 | app.get('/_profiler/nosuchpage', status=404) 177 | 178 | def test_profiler_forbidden(self): 179 | app = self.make_test_app() 180 | app.get('/_profiler/profiler', status=403) 181 | 182 | def test_profiler_media(self): 183 | app = self.make_test_app() 184 | resp = app.get('/_profiler/media/css/profile.css') 185 | self.assertEqual(resp.status_int, 200) 186 | self.assertEqual(resp.content_type, 'text/css') 187 | self.assertIn('#profile {', resp) 188 | 189 | def test_profiler_show_no_id(self): 190 | app = self.make_test_app() 191 | app.get('/_profiler/show', status=404) 192 | 193 | def test_profiler_show(self): 194 | app = self.make_test_app() 195 | prof_id = self.record_profile(app) 196 | resp = app.get('/_profiler/show/%s' % prof_id) 197 | self.assertIn('
    ', resp) 198 | 199 | def test_profiler_delete(self): 200 | app = self.make_test_app() 201 | prof_id = self.record_profile(app) 202 | resp = app.get('/_profiler/delete/%s' % prof_id) 203 | self.assertEqual(os.listdir(self.tmpdir), []) 204 | self.assertIn('deleted', resp) 205 | 206 | def test_profiler_delete_nonexistent(self): 207 | app = self.make_test_app() 208 | resp = app.get('/_profiler/delete/42') 209 | self.assertIn('deleted', resp) 210 | 211 | @skip_on_windows 212 | def test_profiler_delete_fails(self): 213 | app = self.make_test_app() 214 | prof_id = self.record_profile(app) 215 | os.chmod(self.tmpdir, 0o500) 216 | self.assertRaises(OSError, app.get, '/_profiler/delete/%s' % prof_id) 217 | 218 | def test_profiler_delete_all(self): 219 | app = self.make_test_app() 220 | self.record_profile(app) 221 | self.record_profile(app) # do it twice 222 | resp = app.get('/_profiler/delete', status=302) 223 | self.assertEqual(os.listdir(self.tmpdir), []) 224 | self.assertEqual(resp.location, 'http://localhost/_profiler/showall') 225 | -------------------------------------------------------------------------------- /dozer/logview.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import logging 3 | import pathlib 4 | import re 5 | import sys 6 | import threading 7 | import time 8 | import traceback 9 | 10 | from mako.lookup import TemplateLookup 11 | from webob import Request 12 | 13 | from dozer.util import asbool 14 | 15 | 16 | here_dir = pathlib.Path(__file__).parent.resolve() 17 | 18 | 19 | class Logview(object): 20 | def __init__(self, app, config=None, loglevel='DEBUG', **kwargs): 21 | """Stores logging statements per request, and includes a bar on 22 | the page that shows the logging statements 23 | 24 | ''loglevel'' 25 | Default log level for messages that should be caught. 26 | 27 | Note: the root logger's log level also matters! If you do 28 | logging.getLogger('').setLevel(logging.INFO), no DEBUG messages 29 | will make it to Logview's handler anyway. 30 | 31 | Config can contain optional additional loggers and the colors 32 | they should be highlighted (in an ini file):: 33 | 34 | logview.sqlalchemy = #ff0000 35 | 36 | Or if passing a dict:: 37 | 38 | app = Logview(app, {'logview.sqlalchemy':'#ff0000'}) 39 | 40 | """ 41 | if config is None: 42 | config = {} 43 | self.app = app 44 | tmpl_dir = here_dir.joinpath('templates') 45 | self.mako = TemplateLookup(directories=[tmpl_dir], 46 | default_filters=['h']) 47 | 48 | self.log_colors = {} 49 | for key, val in itertools.chain(iter(config.items()), 50 | iter(kwargs.items())): 51 | if key.startswith('logview.'): 52 | self.log_colors[key[len('logview.'):]] = val 53 | 54 | self.traceback_colors = {} 55 | for key, val in itertools.chain(iter(config.items()), 56 | iter(kwargs.items())): 57 | if key.startswith('traceback.'): 58 | self.traceback_colors[key[len('traceback.'):]] = val 59 | 60 | self.logger = logging.getLogger(__name__) 61 | self.loglevel = getattr(logging, loglevel) 62 | 63 | self.keep_tracebacks = asbool(kwargs.get( 64 | 'keep_tracebacks', config.get( 65 | 'keep_tracebacks', RequestHandler.keep_tracebacks))) 66 | self.keep_tracebacks_limit = int(kwargs.get( 67 | 'keep_tracebacks_limit', config.get( 68 | 'keep_tracebacks_limit', RequestHandler.keep_tracebacks_limit))) 69 | self.skip_first_n_frames = int(kwargs.get( 70 | 'skip_first_n_frames', config.get( 71 | 'skip_first_n_frames', RequestHandler.skip_first_n_frames))) 72 | self.skip_last_n_frames = int(kwargs.get( 73 | 'skip_last_n_frames', config.get( 74 | 'skip_last_n_frames', RequestHandler.skip_last_n_frames))) 75 | self.stack_formatter = kwargs.get( 76 | 'stack_formatter', config.get( 77 | 'stack_formatter', RequestHandler.stack_formatter)) 78 | self.tb_formatter = kwargs.get( 79 | 'tb_formatter', config.get( 80 | 'tb_formatter', RequestHandler.tb_formatter)) 81 | 82 | reqhandler = RequestHandler() 83 | reqhandler.setLevel(self.loglevel) 84 | reqhandler.keep_tracebacks = self.keep_tracebacks 85 | reqhandler.keep_tracebacks_limit = self.keep_tracebacks_limit 86 | reqhandler.skip_first_n_frames = self.skip_first_n_frames 87 | reqhandler.skip_last_n_frames = self.skip_last_n_frames 88 | if self.stack_formatter: 89 | reqhandler.stack_formatter = self._resolve(self.stack_formatter) 90 | if self.tb_formatter: 91 | reqhandler.tb_formatter = self._resolve(self.tb_formatter) 92 | logging.getLogger('').addHandler(reqhandler) 93 | self.reqhandler = reqhandler 94 | 95 | def _resolve(self, dotted_name): 96 | if callable(dotted_name): 97 | # let's let people supply the function directly 98 | return dotted_name 99 | modname, fn = dotted_name.rsplit('.', 1) 100 | mod = __import__(modname, {}, {}, ['*']) 101 | return getattr(mod, fn) 102 | 103 | def __call__(self, environ, start_response): 104 | tok = threading.get_ident() 105 | 106 | req = Request(environ) 107 | start = time.time() 108 | self.logger.log(self.loglevel, 'request started') 109 | response = req.get_response(self.app) 110 | self.logger.log(self.loglevel, 'request finished') 111 | tottime = time.time() - start 112 | reqlogs = self.reqhandler.pop_events(tok) 113 | if 'content-type' in response.headers and \ 114 | response.headers['content-type'].startswith('text/html'): 115 | logbar = self.render('/logbar.mako', events=reqlogs, 116 | logcolors=self.log_colors, 117 | traceback_colors=self.traceback_colors, 118 | tottime=tottime, start=start) 119 | logbar = logbar.encode('ascii', 'xmlcharrefreplace') 120 | response.body = self.splice(response.body, logbar) 121 | return response(environ, start_response) 122 | 123 | def splice(self, body, logbar): 124 | assert isinstance(body, bytes) 125 | assert isinstance(logbar, bytes) 126 | parts = re.split(b'(]*>)', body, maxsplit=1) 127 | # parts = ['preamble', '', 'text'] or just ['text'] 128 | # we want to insert our logbar after (if it exists) and 129 | # in front of text 130 | return b''.join(parts[:-1] + [logbar] + parts[-1:]) 131 | 132 | def render(self, name, **vars): 133 | tmpl = self.mako.get_template(name) 134 | return tmpl.render(**vars) 135 | 136 | 137 | class RequestHandler(logging.Handler): 138 | """ 139 | A handler class which only records events if its set as active for 140 | a given thread/process (request). Log history per thread must be 141 | removed manually, preferably at the end of the request. A reference 142 | to the RequestHandler instance should be retained for this access. 143 | 144 | This handler otherwise works identically to a request-handler, 145 | except that events are logged to specific 'channels' based on 146 | thread id when available. 147 | 148 | """ 149 | 150 | keep_tracebacks = False 151 | keep_tracebacks_limit = 0 152 | skip_first_n_frames = 0 153 | skip_last_n_frames = 6 # number of frames beween logger.log() and our emit() 154 | # determined empirically on Python 2.6 155 | stack_formatter = None # 'package.module.function' 156 | # e.g. 'traceback.format_stack' 157 | # note: disables skip_first_n_frames 158 | tb_formatter = None # 'package.module.function' 159 | # e.g. 'traceback.format_tb' 160 | 161 | def __init__(self): 162 | """Initialize the handler.""" 163 | logging.Handler.__init__(self) 164 | self.buffer = {} 165 | 166 | def emit(self, record): 167 | """Emit a record. 168 | 169 | Append the record. If shouldFlush() tells us to, call flush() to process 170 | the buffer. 171 | """ 172 | self.buffer.setdefault(record.thread, []).append(record) 173 | if self.keep_tracebacks and (not self.keep_tracebacks_limit or 174 | len(self.buffer[record.thread]) < self.keep_tracebacks_limit): 175 | f = sys._getframe(self.skip_last_n_frames) 176 | if self.stack_formatter: 177 | record.traceback = self.stack_formatter(f) 178 | else: 179 | record.traceback = traceback.format_list( 180 | traceback.extract_stack(f)[self.skip_first_n_frames:]) 181 | if record.exc_info and record.exc_info != (None, None, None): 182 | # When you do log.exception() when there's no exception, you get 183 | # record.exc_info == (None, None, None) 184 | exc_type, exc_value, exc_tb = record.exc_info 185 | if self.tb_formatter: 186 | record.exc_traceback = self.tb_formatter(exc_tb) 187 | else: 188 | record.exc_traceback = traceback.format_tb(exc_tb) 189 | # Make sure we interpolate the message early. Consider this code: 190 | # a_list = [1, 2, 3] 191 | # log.debug('a_list = %r', a_list) 192 | # del a_list[:] 193 | # if we call getMessage() only when we're rendering all the messages 194 | # at the end of request processing, we will see `a_list = []` instead 195 | # of `a_list = [1, 2, 3]` 196 | record.full_message = record.getMessage() 197 | 198 | def pop_events(self, thread_id): 199 | """Return all the events logged for particular thread""" 200 | if thread_id in self.buffer: 201 | return self.buffer.pop(thread_id) 202 | else: 203 | return [] 204 | 205 | def flush(self): 206 | """Kills all data in the buffer""" 207 | self.buffer = {} 208 | 209 | def close(self): 210 | """Close the handler. 211 | 212 | This version just flushes and chains to the parent class' close(). 213 | """ 214 | self.flush() 215 | logging.Handler.close(self) 216 | -------------------------------------------------------------------------------- /dozer/media/css/profile.css: -------------------------------------------------------------------------------- 1 | /* @group Reset */ 2 | /* Reset CSS v1.0 | 20080212 */ 3 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, button, table, caption, tbody, tfoot, thead, tr, th, td {margin: 0; padding: 0; border: 0; outline: 0; font-size: 100%; background: transparent;} 4 | 5 | body {line-height: 1;} 6 | ol, ul {list-style: none;} 7 | blockquote, q {quotes: none;} 8 | blockquote:before, blockquote:after,q:before, q:after {content: ''; content: none;} 9 | 10 | :focus {outline: 0;} /* TODO: define focus styles */ 11 | 12 | ins {text-decoration: none;} /* TODO: highlight inserts */ 13 | del {text-decoration: line-through;} 14 | 15 | /* tables still need 'cellspacing="0"' in the markup */ 16 | table {border: 0; border-collapse: collapse; border-spacing: 0;} 17 | /* @end */ 18 | 19 | /* @group Defaults */ 20 | body {background: #0d0d0d 0 0 repeat-x; color: #fff; font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Verdana, Arial, sans-serif; font-size: 12px; text-align: center;} 21 | h1, h2, h3, h4, h5, h6, p, img, ul, ol, li, dl, dt, dd, blockquote, pre, code, div, table, tr, th, td, tbody, tfoot, fieldset, legend, input, textarea, select, button {line-height: 18px;} 22 | 23 | h1, h2, h3, h4, h5, h6, dt, legend {background: transparent; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;} 24 | h1 {color: #fff; font-size: 18px;} 25 | h2 {background: #939393 0 -9px repeat-x; border-bottom: 1px solid #666; clear: both; color: #333; font-weight: normal; padding: 3px 18px;} 26 | h2 span {font-weight: bold;} 27 | h3 {} 28 | h4 {background: #e6e6e6 0 100% repeat-x; color: #666; font-size: 12px; letter-spacing: 1px; padding: 3px 18px;} 29 | h4 img {padding-left: 10px; margin-bottom: -4px; padding-top: 0px;} 30 | h4 span {font-weight: normal; text-transform: uppercase; font-size: 11px;} 31 | h5 {font-size: 14px;} 32 | h6 {font-size: 12px;} 33 | 34 | samp {color: #545454;} 35 | cite {font-size: 11px; font-style: normal; font-weight: bold;} 36 | blockquote, .note {font-family: Georgia, "Times Roman", "Times New Roman", serif;} 37 | blockquote {font-style: italic;} 38 | 39 | p, ul, ol, dl, blockquote, pre, table {margin-bottom: 18px;} 40 | 41 | dt {font-size: 11px; font-weight: bold; letter-spacing: 1px; margin-bottom: 9px; text-transform: uppercase;} 42 | 43 | a {border: 0; outline: none;} 44 | a:link, a:visited {color: #001999; text-decoration: underline;} 45 | a:hover {color: #001999; text-decoration: none;} 46 | a:active {color: #343434; text-decoration: none;} 47 | 48 | table {empty-cells: show; margin-bottom: 18px;} 49 | th {background: #eee 0 100% repeat-x; color: #000;} 50 | td {background: transparent 0 100% no-repeat; color: #fff;} 51 | th, td {padding: 3px 9px; vertical-align: middle;} 52 | img {background: transparent; border: 0;} 53 | legend {} 54 | 55 | label, .bold {color: #535353; display: block; font-weight: bold; line-height: 18px;} 56 | input, select, textarea {line-height: 18px; margin: 0; margin-bottom: 18px;} 57 | input, textarea {border: 1px solid #999; outline: none;} 58 | button {background: none; cursor: pointer; margin-right: 9px; vertical-align: middle;} 59 | table select, table input, table textarea {margin: 3px 0;} 60 | /* @end */ 61 | 62 | 63 | /* 64 | Copyright (c) 2008, Yahoo! Inc. All rights reserved. 65 | Code licensed under the BSD License: 66 | http://developer.yahoo.net/yui/license.txt 67 | version: 2.5.1 68 | */ 69 | /* Modified to target #profile div */ 70 | 71 | /* Styling */ 72 | #profile { margin-top: 10px; color: #ddd; background-color: #000; padding: 9px; font-family: "Helvetica Neue", "Lucida Grande", Calibri, Helvetica, Verdana, sans-serif; font-size: 12px; overflow: hidden; } 73 | #profile #profile-content { margin-left: 170px; color: #000; height: 61px; vertical-align: middle; text-align: left; } 74 | #profile-root-bar { float: right; } 75 | #profile-root-bar li { float: left; border: 1px solid white; } 76 | #profile a, #profile a:visited { color: #fff; text-decoration: underline; font-size: 12px; border: 0; } 77 | #profile #profile-content p.full { height: 61px; line-height: 61px; vertical-align: middle; padding: 0; } 78 | #profile .small { font-size: 10px; color: #666; padding: 0 8px 0 8px;} 79 | 80 | .profile_bar { margin: 0 4px; margin-left: 10px; list-style: none; } 81 | .profile_bar li { height: 20px; color: #fff; vertical-align: middle; font-size: 11px; padding: 0 4px; 82 | background-image: url(../images/fade.png); 83 | background-position: bottom left; 84 | background-repeat: repeat-x; 85 | } 86 | .layer { background-color: #FF9800; border-right: none; } 87 | 88 | #profile ul.step-info { clear: left; margin: 0; padding: 1px 0; height: 20px;} 89 | #profile ul .title { float: left; overflow: hidden; width: 400px; margin: 0 10px 0 0; padding: 0; color: #ddd; font-size: 12px; text-transform: none; font-weight: inherit; height: 20px;} 90 | #profile ul .title p { text-align: left;} 91 | #profile ul .bar {float: left; height: 20px;} 92 | #profile ul .title strong { color: #fff; font-weight: bold; } 93 | #profile ul .title span.time { float: right; text-transform: none; font-size: 11px; font-weight: normal; margin-right: 0px; min-width: 64px; text-align: right; } 94 | #profile ul .title span.callcount { float: right; text-transform: none; font-size: 11px; font-weight: normal; margin-right: 24px; } 95 | 96 | #profile ul .title a, ul .title a:active, ul .title a:visited { 97 | text-decoration: none; 98 | color: inherit; 99 | font-size: inherit; 100 | font-weight: inherit; 101 | } 102 | 103 | #profile .profile_bar li { text-align: center; } 104 | 105 | #profile li.step { float: left; padding-left: 4px; background: url(../images/pip.gif) 4px 0 no-repeat; clear: left; } 106 | #profile li.with-children { background: url(../images/arrows.gif) 4px 0 no-repeat;} 107 | #profile li.opened { background: url(../images/arrows.gif) 4px -20px no-repeat; } 108 | 109 | #profile ul { margin: 0; padding: 0; } 110 | #profile ul span.time { margin-right: 12px; } 111 | 112 | #profile li .children { display: none; } 113 | 114 | #profile ul .title { padding-left: 16px;} 115 | 116 | #profile ul ul.profile_children { background: #111; } 117 | #profile ul ul .bar { } 118 | #profile ul ul .title { width: 496px; } 119 | 120 | #profile ul ul ul.profile_children { background: #222;} 121 | #profile ul ul ul .title { width: 492px; } 122 | 123 | 124 | #profile ul ul ul ul.children { background: #333;} 125 | #profile ul ul ul ul .title { width: 488px; } 126 | 127 | 128 | #profile ul ul ul ul ul.profile_children { background: #444;} 129 | #profile ul ul ul ul ul .title { width: 484px; } 130 | 131 | 132 | #profile ul ul ul ul ul ul.profile_children { background: #555;} 133 | #profile ul ul ul ul ul ul .title { width: 480px; } 134 | 135 | #profile ul ul ul ul ul ul ul.profile_children { background: #666;} 136 | #profile ul ul ul ul ul ul ul .title { width: 476px; } 137 | 138 | #profile ul ul ul ul ul ul ul ul .title { width: 472px; } 139 | #profile ul ul ul ul ul ul ul ul ul .title { width: 468px; } 140 | #profile ul ul ul ul ul ul ul ul ul ul .title { width: 464px; } 141 | #profile ul ul ul ul ul ul ul ul ul ul ul .title { width: 460px; } 142 | #profile ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 456px; } 143 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 452px; } 144 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 448px; } 145 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 444px; } 146 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 440px; } 147 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 436px; } 148 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 432px; } 149 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 428px; } 150 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 424px; } 151 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 420px; } 152 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 416px; } 153 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 412px; } 154 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 408px; } 155 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 404px; } 156 | #profile ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul ul .title { width: 400px; } 157 | 158 | 159 | #profile-flash { display: none; text-align: left; font-size: 12px; padding: 5px; color: #000; border-bottom: 1px #000 solid; } 160 | #profile-flash emph { color: #666; font-style: italic; } 161 | #profile-flash p { font-family: sans-serif; margin: 0; } 162 | #profile-flash p { padding: 0; padding-left: 20px; font-family: sans-serif; background: none; color: #000; border: 0; } 163 | #profile-flash.tuneup-show { display: block; } 164 | #profile-flash.tuneup-error { background: #FDFCDB; } 165 | #profile-flash.tuneup-error p { background: url(../images/warning.gif) 0 50% no-repeat; } 166 | #profile-flash.tuneup-notice { padding-left: 0; background: #E4FFDE; } 167 | 168 | #profile .profile_bar { float: left; width: 400px; padding: 0;} 169 | #profile .profile_bar li { float: left; height: 18px; padding: 0;} 170 | 171 | #profile a.tuneup-sql { color: 001999; margin-left: 10px; } 172 | #profile a img { border: 0; } 173 | 174 | #profile .tuneup-halo { display: none; } 175 | #profile .step-info:hover .tuneup-halo { display: inline; } 176 | 177 | #profile textarea { background: none; overflow: hidden; color: #fff; font-size: 12px; font-family: sans-serif; padding: 4px; border: 0; font-family: monospace; } 178 | #profile table { border: 0; margin-top: 4px; background: #111; } /* sql */ 179 | #profile th { background: #000; padding: 4px; color: #fff; font-size: 12px; text-align: left; border: 0; font-weight: bold; border: 1px #333 solid; border-left: 0; border-top: 0; } 180 | #profile td { background: #111; padding: 4px; color: #ddd; font-size: 12px; text-align: left; border: 0; } 181 | 182 | #environment th { text-align: right; } 183 | #environment td { text-align: left; } 184 | #profile-list { width: 100%; } 185 | #profile-list td.time, #profile-list td.cost { text-align: right; } 186 | #profile-list td.cost, #profile-list td.pid { width: 100px; } 187 | #profile-list td.time { width: 120px; } 188 | #profile-list td.delete { width: 48px; } 189 | #profile-list td { text-align: left; } 190 | #profile-list tr:hover { background: #444; } 191 | #profile-list tr:hover td { background: #444 !important; } 192 | #profile-list div.box { position: relative; } 193 | #profile-list span.bar { background: #774b00; position: absolute; right: -5px; top: 0; z-index: -1; } 194 | -------------------------------------------------------------------------------- /dozer/media/javascript/path.js: -------------------------------------------------------------------------------- 1 | // $Id: path.js 375 2007-03-14 10:01:48Z rschmidt $ 2 | 3 | var Point = Class.create(); 4 | Point.prototype = { 5 | initialize: function(x, y) { 6 | this.x = x; 7 | this.y = y; 8 | }, 9 | offset: function(dx, dy) { 10 | this.x += dx; 11 | this.y += dy; 12 | }, 13 | distanceFrom: function(point) { 14 | var dx = this.x - point.x; 15 | var dy = this.y - point.y; 16 | return Math.sqrt(dx * dx + dy * dy); 17 | }, 18 | draw: function(ctx) { 19 | ctx.moveTo(this.x, this.y); 20 | ctx.lineTo(this.x + 0.001, this.y); 21 | } 22 | } 23 | 24 | var Bezier = Class.create(); 25 | Bezier.prototype = { 26 | initialize: function(points) { 27 | this.points = points; 28 | this.order = points.length; 29 | }, 30 | reset: function() { 31 | with (Bezier.prototype) { 32 | this.controlPolygonLength = controlPolygonLength; 33 | this.chordLength = chordLength; 34 | this.triangle = triangle; 35 | this.chordPoints = chordPoints; 36 | this.coefficients = coefficients; 37 | } 38 | }, 39 | offset: function(dx, dy) { 40 | this.points.each(function(point) { 41 | point.offset(dx, dy); 42 | }); 43 | this.reset(); 44 | }, 45 | // Based on Oliver Steele's bezier.js library. 46 | controlPolygonLength: function() { 47 | var len = 0; 48 | for (var i = 1; i < this.order; ++i) { 49 | len += this.points[i - 1].distanceFrom(this.points[i]); 50 | } 51 | return (this.controlPolygonLength = function() {return len;})(); 52 | }, 53 | // Based on Oliver Steele's bezier.js library. 54 | chordLength: function() { 55 | var len = this.points[0].distanceFrom(this.points[this.order - 1]); 56 | return (this.chordLength = function() {return len;})(); 57 | }, 58 | // From Oliver Steele's bezier.js library. 59 | triangle: function() { 60 | var upper = this.points; 61 | var m = [upper] 62 | for (var i = 1; i < this.order; ++i) { 63 | var lower = []; 64 | for (var j = 0; j < this.order - i; ++j) { 65 | var c0 = upper[j]; 66 | var c1 = upper[j + 1]; 67 | lower[j] = new Point((c0.x + c1.x) / 2, (c0.y + c1.y) / 2); 68 | } 69 | m.push(lower); 70 | upper = lower; 71 | } 72 | return (this.triangle = function() {return m;})(); 73 | }, 74 | // Based on Oliver Steele's bezier.js library. 75 | triangleAtT: function(t) { 76 | var s = 1 - t; 77 | var upper = this.points; 78 | var m = [upper] 79 | for (var i = 1; i < this.order; ++i) { 80 | var lower = []; 81 | for (var j = 0; j < this.order - i; ++j) { 82 | var c0 = upper[j]; 83 | var c1 = upper[j + 1]; 84 | lower[j] = new Point(c0.x * s + c1.x * t, c0.y * s + c1.y * t); 85 | } 86 | m.push(lower); 87 | upper = lower; 88 | } 89 | return m; 90 | }, 91 | // Returns two beziers resulting from splitting this bezier at t=0.5. 92 | // Based on Oliver Steele's bezier.js library. 93 | split: function(t) { 94 | if ('undefined' == typeof t) t = 0.5; 95 | var m = (0.5 == t) ? this.triangle() : this.triangleAtT(t); 96 | var leftPoints = new Array(this.order); 97 | var rightPoints = new Array(this.order); 98 | for (var i = 0; i < this.order; ++i) { 99 | leftPoints[i] = m[i][0]; 100 | rightPoints[i] = m[this.order - 1 - i][i]; 101 | } 102 | return {left: new Bezier(leftPoints), right: new Bezier(rightPoints)}; 103 | }, 104 | // Returns a bezier which is the portion of this bezier from t1 to t2. 105 | // Thanks to Peter Zin on comp.graphics.algorithms. 106 | mid: function(t1, t2) { 107 | return this.split(t2).left.split(t1 / t2).right; 108 | }, 109 | // Returns points (and their corresponding times in the bezier) that form 110 | // an approximate polygonal representation of the bezier. 111 | // Based on the algorithm described in Jeremy Gibbons' dashed.ps.gz 112 | chordPoints: function() { 113 | var p = [{tStart: 0, tEnd: 0, dt: 0, p: this.points[0]}].concat(this._chordPoints(0, 1)); 114 | return (this.chordPoints = function() {return p;})(); 115 | }, 116 | _chordPoints: function(tStart, tEnd) { 117 | var tolerance = 0.001; 118 | var dt = tEnd - tStart; 119 | if (this.controlPolygonLength() <= (1 + tolerance) * this.chordLength()) { 120 | return [{tStart: tStart, tEnd: tEnd, dt: dt, p: this.points[this.order - 1]}]; 121 | } else { 122 | var tMid = tStart + dt / 2; 123 | var halves = this.split(); 124 | return halves.left._chordPoints(tStart, tMid).concat(halves.right._chordPoints(tMid, tEnd)); 125 | } 126 | }, 127 | // Returns an array of times between 0 and 1 that mark the bezier evenly 128 | // in space. 129 | // Based in part on the algorithm described in Jeremy Gibbons' dashed.ps.gz 130 | markedEvery: function(distance, firstDistance) { 131 | var nextDistance = firstDistance || distance; 132 | var segments = this.chordPoints(); 133 | var times = []; 134 | var t = 0; // time 135 | var dt; // delta t 136 | var segment; 137 | var remainingDistance; 138 | for (var i = 1; i < segments.length; ++i) { 139 | segment = segments[i]; 140 | segment.length = segment.p.distanceFrom(segments[i - 1].p); 141 | if (0 == segment.length) { 142 | t += segment.dt; 143 | } else { 144 | dt = nextDistance / segment.length * segment.dt; 145 | segment.remainingLength = segment.length; 146 | while (segment.remainingLength >= nextDistance) { 147 | segment.remainingLength -= nextDistance; 148 | t += dt; 149 | times.push(t); 150 | if (distance != nextDistance) { 151 | nextDistance = distance; 152 | dt = nextDistance / segment.length * segment.dt; 153 | } 154 | } 155 | nextDistance -= segment.remainingLength; 156 | t = segment.tEnd; 157 | } 158 | } 159 | return {times: times, nextDistance: nextDistance}; 160 | }, 161 | // Return the coefficients of the polynomials for x and y in t. 162 | // From Oliver Steele's bezier.js library. 163 | coefficients: function() { 164 | // This function deals with polynomials, represented as 165 | // arrays of coefficients. p[i] is the coefficient of n^i. 166 | 167 | // p0, p1 => p0 + (p1 - p0) * n 168 | // side-effects (denormalizes) p0, for convienence 169 | function interpolate(p0, p1) { 170 | p0.push(0); 171 | var p = new Array(p0.length); 172 | p[0] = p0[0]; 173 | for (var i = 0; i < p1.length; ++i) { 174 | p[i + 1] = p0[i + 1] + p1[i] - p0[i]; 175 | } 176 | return p; 177 | } 178 | // folds +interpolate+ across a graph whose fringe is 179 | // the polynomial elements of +ns+, and returns its TOP 180 | function collapse(ns) { 181 | while (ns.length > 1) { 182 | var ps = new Array(ns.length-1); 183 | for (var i = 0; i < ns.length - 1; ++i) { 184 | ps[i] = interpolate(ns[i], ns[i + 1]); 185 | } 186 | ns = ps; 187 | } 188 | return ns[0]; 189 | } 190 | // xps and yps are arrays of polynomials --- concretely realized 191 | // as arrays of arrays 192 | var xps = []; 193 | var yps = []; 194 | for (var i = 0, pt; pt = this.points[i++]; ) { 195 | xps.push([pt.x]); 196 | yps.push([pt.y]); 197 | } 198 | var result = {xs: collapse(xps), ys: collapse(yps)}; 199 | return (this.coefficients = function() {return result;})(); 200 | }, 201 | // Return the point at time t. 202 | // From Oliver Steele's bezier.js library. 203 | pointAtT: function(t) { 204 | var c = this.coefficients(); 205 | var cx = c.xs, cy = c.ys; 206 | // evaluate cx[0] + cx[1]t +cx[2]t^2 .... 207 | 208 | // optimization: start from the end, to save one 209 | // muliplicate per order (we never need an explicit t^n) 210 | 211 | // optimization: special-case the last element 212 | // to save a multiply-add 213 | var x = cx[cx.length - 1], y = cy[cy.length - 1]; 214 | 215 | for (var i = cx.length - 1; --i >= 0; ) { 216 | x = x * t + cx[i]; 217 | y = y * t + cy[i]; 218 | } 219 | return new Point(x, y); 220 | }, 221 | // Render the Bezier to a WHATWG 2D canvas context. 222 | // Based on Oliver Steele's bezier.js library. 223 | draw: function (ctx, moveTo) { 224 | if ('undefined' == typeof moveTo) moveTo = true; 225 | if (moveTo) ctx.moveTo(this.points[0].x, this.points[0].y); 226 | var fn = this.drawCommands[this.order]; 227 | if (fn) { 228 | var coords = []; 229 | for (var i = 1 == this.order ? 0 : 1; i < this.points.length; ++i) { 230 | coords.push(this.points[i].x); 231 | coords.push(this.points[i].y); 232 | } 233 | fn.apply(ctx, coords); 234 | } 235 | }, 236 | // Wrapper functions to work around Safari, in which, up to at least 2.0.3, 237 | // fn.apply isn't defined on the context primitives. 238 | // Based on Oliver Steele's bezier.js library. 239 | drawCommands: [ 240 | null, 241 | // This will have an effect if there's a line thickness or end cap. 242 | function(x, y) { 243 | this.lineTo(x + 0.001, y); 244 | }, 245 | function(x, y) { 246 | this.lineTo(x, y); 247 | }, 248 | function(x1, y1, x2, y2) { 249 | this.quadraticCurveTo(x1, y1, x2, y2); 250 | }, 251 | function(x1, y1, x2, y2, x3, y3) { 252 | this.bezierCurveTo(x1, y1, x2, y2, x3, y3); 253 | } 254 | ], 255 | drawDashed: function(ctx, dashLength, firstDistance, drawFirst) { 256 | if (!firstDistance) firstDistance = dashLength; 257 | if ('undefined' == typeof drawFirst) drawFirst = true; 258 | var markedEvery = this.markedEvery(dashLength, firstDistance); 259 | if (drawFirst) markedEvery.times.unshift(0); 260 | var drawLast = (markedEvery.times.length % 2); 261 | if (drawLast) markedEvery.times.push(1); 262 | for (var i = 1; i < markedEvery.times.length; i += 2) { 263 | this.mid(markedEvery.times[i - 1], markedEvery.times[i]).draw(ctx); 264 | } 265 | return {firstDistance: markedEvery.nextDistance, drawFirst: drawLast}; 266 | }, 267 | drawDotted: function(ctx, dotSpacing, firstDistance) { 268 | if (!firstDistance) firstDistance = dotSpacing; 269 | var markedEvery = this.markedEvery(dotSpacing, firstDistance); 270 | if (dotSpacing == firstDistance) markedEvery.times.unshift(0); 271 | markedEvery.times.each(function(t) { 272 | this.pointAtT(t).draw(ctx); 273 | }.bind(this)); 274 | return markedEvery.nextDistance; 275 | } 276 | } 277 | 278 | var Path = Class.create(); 279 | Path.prototype = { 280 | initialize: function(segments) { 281 | this.segments = segments || []; 282 | }, 283 | // Based on Oliver Steele's bezier.js library. 284 | addBezier: function(pointsOrBezier) { 285 | this.segments.push(pointsOrBezier instanceof Array ? new Bezier(pointsOrBezier) : pointsOrBezier); 286 | }, 287 | offset: function(dx, dy) { 288 | this.segments.each(function(segment) { 289 | segment.offset(dx, dy); 290 | }); 291 | }, 292 | // Based on Oliver Steele's bezier.js library. 293 | draw: function(ctx) { 294 | var moveTo = true; 295 | this.segments.each(function(segment) { 296 | segment.draw(ctx, moveTo); 297 | moveTo = false; 298 | }); 299 | }, 300 | drawDashed: function(ctx, dashLength, firstDistance, drawFirst) { 301 | var info = { 302 | drawFirst: ('undefined' == typeof drawFirst) ? true : drawFirst, 303 | firstDistance: firstDistance || dashLength 304 | }; 305 | this.segments.each(function(segment) { 306 | info = segment.drawDashed(ctx, dashLength, info.firstDistance, info.drawFirst); 307 | }); 308 | }, 309 | drawDotted: function(ctx, dotSpacing, firstDistance) { 310 | if (!firstDistance) firstDistance = dotSpacing; 311 | this.segments.each(function(segment) { 312 | firstDistance = segment.drawDotted(ctx, dotSpacing, firstDistance); 313 | }); 314 | } 315 | } 316 | 317 | var Ellipse = Class.create(); 318 | Ellipse.prototype = new Path(); 319 | Object.extend(Ellipse.prototype, { 320 | KAPPA: 0.5522847498, 321 | initialize: function(cx, cy, rx, ry) { 322 | this.cx = cx; // center x 323 | this.cy = cy; // center y 324 | this.rx = rx; // radius x 325 | this.ry = ry; // radius y 326 | this.segments = [ 327 | new Bezier([ 328 | new Point(cx, cy - ry), 329 | new Point(cx + this.KAPPA * rx, cy - ry), 330 | new Point(cx + rx, cy - this.KAPPA * ry), 331 | new Point(cx + rx, cy) 332 | ]), 333 | new Bezier([ 334 | new Point(cx + rx, cy), 335 | new Point(cx + rx, cy + this.KAPPA * ry), 336 | new Point(cx + this.KAPPA * rx, cy + ry), 337 | new Point(cx, cy + ry) 338 | ]), 339 | new Bezier([ 340 | new Point(cx, cy + ry), 341 | new Point(cx - this.KAPPA * rx, cy + ry), 342 | new Point(cx - rx, cy + this.KAPPA * ry), 343 | new Point(cx - rx, cy) 344 | ]), 345 | new Bezier([ 346 | new Point(cx - rx, cy), 347 | new Point(cx - rx, cy - this.KAPPA * ry), 348 | new Point(cx - this.KAPPA * rx, cy - ry), 349 | new Point(cx, cy - ry) 350 | ]) 351 | ]; 352 | } 353 | }); 354 | -------------------------------------------------------------------------------- /dozer/profile.py: -------------------------------------------------------------------------------- 1 | import cProfile 2 | import errno 3 | import os 4 | import pathlib 5 | import pickle 6 | import re 7 | import threading 8 | import time 9 | from datetime import datetime 10 | from operator import itemgetter 11 | 12 | from mako.lookup import TemplateLookup 13 | from webob import Request, Response, exc, static 14 | 15 | 16 | here_dir = pathlib.Path(__file__).parent.resolve() 17 | 18 | DEFAULT_IGNORED_PATHS = [r'/favicon\.ico$', r'^/error/document'] 19 | 20 | 21 | class Profiler(object): 22 | def __init__(self, app, global_conf=None, profile_path=None, 23 | ignored_paths=DEFAULT_IGNORED_PATHS, 24 | dot_graph_cutoff=0.2, **kwargs): 25 | """Profiles an application and saves the pickled version to a 26 | file 27 | """ 28 | assert profile_path, "need profile_path" 29 | assert os.path.isdir(profile_path), "%r: no such directory" % profile_path 30 | self.app = app 31 | self.conf = global_conf 32 | self.dot_graph_cutoff = float(dot_graph_cutoff) 33 | self.profile_path = profile_path 34 | self.ignored_paths = list(map(re.compile, ignored_paths)) 35 | tmpl_dir = here_dir.joinpath('templates') 36 | self.mako = TemplateLookup(directories=[tmpl_dir]) 37 | 38 | def __call__(self, environ, start_response): 39 | assert not environ['wsgi.multiprocess'], ( 40 | "Dozer middleware is not usable in a " 41 | "multi-process environment") 42 | req = Request(environ) 43 | req.base_path = req.application_url + '/_profiler' 44 | if req.path_info_peek() == '_profiler': 45 | return self.profiler(req)(environ, start_response) 46 | for regex in self.ignored_paths: 47 | if regex.match(environ['PATH_INFO']) is not None: 48 | return self.app(environ, start_response) 49 | return self.run_profile(environ, start_response) 50 | 51 | def profiler(self, req): 52 | assert req.path_info_pop() == '_profiler' 53 | next_part = req.path_info_pop() or 'showall' 54 | method = getattr(self, next_part, None) 55 | if method is None: 56 | return exc.HTTPNotFound('Nothing could be found to match %r' % next_part) 57 | if not getattr(method, 'exposed', False): 58 | return exc.HTTPForbidden('Access to %r is forbidden' % next_part) 59 | return method(req) 60 | 61 | def media(self, req): 62 | """Static path where images and other files live""" 63 | path = here_dir.joinpath('media') 64 | app = static.DirectoryApp(path) 65 | return app 66 | media.exposed = True 67 | 68 | def show(self, req): 69 | profile_id = req.path_info_pop() 70 | if not profile_id: 71 | return exc.HTTPNotFound('Missing profile id to view') 72 | dir_name = self.profile_path or '' 73 | fname = os.path.join(dir_name, profile_id) + '.pkl' 74 | with open(fname, 'rb') as f: 75 | data = pickle.load(f) 76 | top = [x for x in data['profile'].values() if not x.get('callers')] 77 | res = Response() 78 | res.body = self.render('/show_profile.mako', time=data['time'], 79 | profile=top, profile_data=data['profile'], 80 | environ=data['environ'], id=profile_id) 81 | return res 82 | show.exposed = True 83 | 84 | def showall(self, req): 85 | dir_name = self.profile_path 86 | profiles = [] 87 | errors = [] 88 | max_cost = 1 # avoid division by zero 89 | for profile_file in os.listdir(dir_name): 90 | if profile_file.endswith('.pkl'): 91 | path = os.path.join(self.profile_path, profile_file) 92 | modified = os.stat(path).st_mtime 93 | try: 94 | with open(path, 'rb') as f: 95 | data = pickle.load(f) 96 | except Exception as e: 97 | errors.append((modified, '%s: %s' % (e.__class__.__name__, e), profile_file[:-4])) 98 | else: 99 | environ = data['environ'] 100 | top = [x for x in data['profile'].values() if not x.get('callers')] 101 | if top: 102 | total_cost = max(float(x['cost']) for x in top) 103 | else: 104 | total_cost = 0 105 | max_cost = max(max_cost, total_cost) 106 | profiles.append((modified, environ, total_cost, profile_file[:-4])) 107 | 108 | profiles.sort(reverse=True, key=itemgetter(0)) 109 | errors.sort(reverse=True) 110 | res = Response() 111 | if profiles: 112 | earliest = profiles[-1][0] 113 | else: 114 | earliest = None 115 | res.body = self.render('/list_profiles.mako', profiles=profiles, 116 | errors=errors, now=time.time(), 117 | earliest=earliest, max_cost=max_cost) 118 | return res 119 | showall.exposed = True 120 | 121 | def delete(self, req): 122 | profile_id = req.path_info_pop() 123 | if profile_id: # this prob a security risk 124 | try: 125 | for ext in ('.gv', '.pkl'): 126 | os.unlink(os.path.join(self.profile_path, profile_id + ext)) 127 | except OSError as e: 128 | if e.errno == errno.ENOENT: 129 | pass # allow a file not found exception 130 | else: 131 | raise 132 | return Response('deleted %s' % profile_id) 133 | 134 | for filename in os.listdir(self.profile_path): 135 | if filename.endswith('.pkl') or filename.endswith('.gv'): 136 | os.unlink(os.path.join(self.profile_path, filename)) 137 | res = Response() 138 | res.location = '/_profiler/showall' 139 | res.status_int = 302 140 | return res 141 | delete.exposed = True 142 | 143 | def render(self, name, **vars): 144 | tmpl = self.mako.get_template(name) 145 | return tmpl.render(**vars).encode('ascii', 'xmlcharrefreplace') 146 | 147 | def run_profile(self, environ, start_response): 148 | """Run the profile over the request and save it""" 149 | prof = cProfile.Profile() 150 | response_body = [] 151 | 152 | def catching_start_response(status, headers, exc_info=None): 153 | start_response(status, headers, exc_info) 154 | return response_body.append 155 | 156 | def runapp(): 157 | appiter = self.app(environ, catching_start_response) 158 | try: 159 | response_body.extend(appiter) 160 | finally: 161 | if hasattr(appiter, 'close'): 162 | appiter.close() 163 | 164 | prof.runcall(runapp) 165 | body = b''.join(response_body) 166 | results = prof.getstats() 167 | tree = buildtree(results) 168 | 169 | # Pull out 'safe' bits from environ 170 | safe_environ = {} 171 | for k, v in environ.items(): 172 | if k.startswith('HTTP_'): 173 | safe_environ[k] = v 174 | elif k in ['REQUEST_METHOD', 'SCRIPT_NAME', 'PATH_INFO', 175 | 'QUERY_STRING', 'CONTENT_TYPE', 'CONTENT_LENGTH', 176 | 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL']: 177 | safe_environ[k] = v 178 | safe_environ['thread_id'] = str(threading.get_ident()) 179 | profile_run = dict(time=datetime.now(), profile=tree, 180 | environ=safe_environ) 181 | dir_name = self.profile_path or '' 182 | 183 | openflags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0) 184 | while True: 185 | fname_base = str(time.time()).replace('.', '_') 186 | prof_file = fname_base + '.pkl' 187 | try: 188 | fd = os.open(os.path.join(dir_name, prof_file), openflags) 189 | except OSError as e: 190 | if e.errno == errno.EEXIST: 191 | # file already exists, try again with a slightly different 192 | # timestamp hopefully 193 | pass # pragma: nocover 194 | else: 195 | raise 196 | else: 197 | break 198 | 199 | with os.fdopen(fd, 'wb') as f: 200 | pickle.dump(profile_run, f) 201 | write_dot_graph(results, tree, os.path.join(dir_name, fname_base+'.gv'), 202 | cutoff=self.dot_graph_cutoff) 203 | del results, tree, profile_run 204 | return [body] 205 | 206 | 207 | def label(code): 208 | """Generate a friendlier version of the code function called""" 209 | if isinstance(code, str): 210 | return code 211 | else: 212 | return '%s %s:%d' % (code.co_name, 213 | code.co_filename, 214 | code.co_firstlineno) 215 | 216 | 217 | def graphlabel(code): 218 | lb = label(code) 219 | return lb.replace('"', "'").strip() 220 | 221 | 222 | def setup_time(t): 223 | """Takes a time generally assumed to be quite small and blows it 224 | up into millisecond time. 225 | 226 | For example: 227 | 0.004 seconds -> 4 ms 228 | 0.00025 seconds -> 0.25 ms 229 | 230 | The result is returned as a string. 231 | 232 | """ 233 | t = t*1000 234 | t = '%0.2f' % t 235 | return t 236 | 237 | 238 | def color(w): 239 | # color scheme borrowed from 240 | # https://github.com/jrfonseca/gprof2dot 241 | hmin, smin, lmin = 2/3., 0.8, .25 242 | hmax, smax, lmax = 0, 1, .5 243 | gamma = 2.2 244 | h = hmin + w * (hmax - hmin) 245 | s = smin + w * (smax - smin) 246 | l = lmin + w * (lmax - lmin) 247 | # https://www.w3.org/TR/css3-color/#hsl-color 248 | if l <= 0.5: 249 | m2 = l * (s + 1) 250 | else: # pragma: nocover -- because our lmax is <= .5! 251 | m2 = l + s - l * s 252 | m1 = l * 2 - m2 253 | 254 | def h2rgb(m1, m2, h): 255 | if h < 0: 256 | h += 1.0 257 | elif h > 1: # pragma: nocover -- our hmax is 2/3, and we add 1/3 to that 258 | h -= 1.0 259 | if h * 6 < 1.0: 260 | return m1 + (m2 - m1) * h * 6 261 | elif h * 2 < 1: 262 | return m2 263 | elif h * 3 < 2: 264 | return m1 + (m2 - m1) * (2/3.0 - h) * 6 265 | else: 266 | return m1 267 | 268 | r = h2rgb(m1, m2, h + 1/3.0) 269 | g = h2rgb(m1, m2, h) 270 | b = h2rgb(m1, m2, h - 1/3.0) 271 | # gamma correction 272 | r **= gamma 273 | g **= gamma 274 | b **= gamma 275 | # graphvizification 276 | r = min(max(0, round(r * 0xff)), 0xff) 277 | g = min(max(0, round(g * 0xff)), 0xff) 278 | b = min(max(0, round(b * 0xff)), 0xff) 279 | return "#%02X%02X%02X" % (r, g, b) 280 | 281 | 282 | def write_dot_graph(data, tree, filename, cutoff=0.2): 283 | f = open(filename, 'w') 284 | f.write('digraph prof {\n') 285 | f.write('\tsize="11,9"; ratio = fill;\n') 286 | f.write('\tnode [style=filled];\n') 287 | 288 | # Find the largest time 289 | highest = 0.00 290 | for entry in tree.values(): 291 | if float(entry['cost']) > highest: 292 | highest = float(entry['cost']) 293 | if highest == 0: 294 | highest = 1 # avoid division by zero 295 | 296 | for entry in data: 297 | code = entry.code 298 | entry_name = graphlabel(code) 299 | skip = float(setup_time(entry.totaltime)) < cutoff 300 | if isinstance(code, str) or skip: 301 | continue 302 | else: 303 | t = tree[label(code)]['cost'] 304 | c = color(float(t) / highest) 305 | f.write('\t"%s" [label="%s\\n%sms",color="%s",fontcolor="white"]\n' 306 | % (entry_name, code.co_name, t, c)) 307 | if entry.calls: 308 | for subentry in entry.calls: 309 | subcode = subentry.code 310 | skip = float(setup_time(subentry.totaltime)) < cutoff 311 | if isinstance(subcode, str) or skip: 312 | continue 313 | sub_name = graphlabel(subcode) 314 | f.write('\t"%s" -> "%s" [label="%s"]\n' % 315 | (entry_name, sub_name, subentry.callcount)) 316 | f.write('}\n') 317 | f.close() 318 | 319 | 320 | def buildtree(data): 321 | """Takes a pmstats object as returned by cProfile and constructs 322 | a call tree out of it""" 323 | functree = {} 324 | callregistry = {} 325 | for entry in data: 326 | node = {} 327 | code = entry.code 328 | # If its a builtin 329 | if isinstance(code, str): 330 | node['filename'] = '~' 331 | node['source_position'] = 0 332 | node['func_name'] = code 333 | else: 334 | node['filename'] = code.co_filename 335 | node['source_position'] = code.co_firstlineno 336 | node['func_name'] = code.co_name 337 | node['line_no'] = code.co_firstlineno 338 | node['cost'] = setup_time(entry.totaltime) 339 | node['function'] = label(code) 340 | 341 | if entry.calls: 342 | for subentry in entry.calls: 343 | subnode = {} 344 | subnode['builtin'] = isinstance(subentry.code, str) 345 | subnode['cost'] = setup_time(subentry.totaltime) 346 | subnode['function'] = label(subentry.code) 347 | subnode['callcount'] = subentry.callcount 348 | node.setdefault('calls', []).append(subnode) 349 | callregistry.setdefault(subnode['function'], []).append(node['function']) 350 | else: 351 | node['calls'] = [] 352 | functree[node['function']] = node 353 | for key in callregistry: 354 | node = functree[key] 355 | node['callers'] = callregistry[key] 356 | return functree 357 | -------------------------------------------------------------------------------- /dozer/tests/test_leak.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import unittest 3 | from unittest.mock import patch 4 | 5 | import webtest 6 | from webob import Request 7 | 8 | from dozer.leak import Dozer, ReferrerTree, get_sort_key, url 9 | from dozer.util import monotonicity 10 | 11 | 12 | class DozerForTests(Dozer): 13 | def __init__(self, app=None, *args, **kw): 14 | super(DozerForTests, self).__init__(app, *args, **kw) 15 | 16 | def _start_thread(self): 17 | pass 18 | 19 | def _maybe_warn_about_PIL(self): 20 | pass 21 | 22 | 23 | class EvilProxyClass(object): 24 | 25 | some_constant = object() 26 | 27 | def __init__(self, obj): 28 | self.obj = obj 29 | 30 | @property 31 | def __module__(self): # pragma: nocover 32 | return self.obj.__module__ 33 | 34 | @property 35 | def __name__(self): 36 | return self.obj.__name__ 37 | 38 | 39 | class MyObj(object): 40 | # Oof. ReferrerTree ignores all objects that have a __module__ containing 41 | # 'dozer'. In Python 3.12 and older, gc.get_referrers() would find the 42 | # __dict__ of MyObj that was a plain dict with no __module__, but in 3.13 43 | # gc.get_referrers() returns the instance directly. The instance has a 44 | # __module__ (inherited from its class), and thus gets filtered out. 45 | __module__ = None 46 | 47 | def __init__(self, **kw): 48 | self.__dict__.update(kw) 49 | 50 | def __repr__(self): 51 | return getattr(self, 'name', 'unnamed-MyObj') 52 | 53 | 54 | class TestDozer(unittest.TestCase): 55 | 56 | def make_request(self, subpath='/', base_path='/_dozer'): 57 | req = Request(dict(PATH_INFO=subpath)) 58 | req.base_path = base_path 59 | return req 60 | 61 | def test_get_sortkey(self): 62 | self.assertEqual((monotonicity, False), get_sort_key("monotonicity")) 63 | self.assertEqual((monotonicity, True), get_sort_key("-monotonicity")) 64 | k, rev = get_sort_key("") 65 | self.assertFalse(rev) 66 | self.assertIsNone(k) 67 | 68 | def test_url(self): 69 | req = self.make_request('/somewhere') 70 | self.assertEqual(url(req, 'foo'), '/_dozer/foo') 71 | self.assertEqual(url(req, '/foo'), '/_dozer/foo') 72 | req = self.make_request('/somewhere', base_path='/_dozer/') 73 | self.assertEqual(url(req, 'bar'), '/_dozer/bar') 74 | self.assertEqual(url(req, '/bar'), '/_dozer/bar') 75 | 76 | def test_path_normalization(self): 77 | dozer = DozerForTests(path='/altpath/') 78 | self.assertEqual(dozer.path, '/altpath') 79 | 80 | def test_start_thread(self): 81 | with patch('threading.Thread.start'): 82 | dozer = Dozer(None) 83 | self.assertEqual(dozer.runthread.name, 'Dozer') 84 | self.assertTrue(dozer.runthread.daemon) 85 | # Python 2.x has __target (mangled), Python 3.x has _target 86 | target = getattr(dozer.runthread, '_Thread__target', 87 | getattr(dozer.runthread, '_target', '???')) 88 | self.assertEqual(target, dozer.start) 89 | self.assertEqual(dozer.runthread.start.call_count, 1) 90 | 91 | def test_maybe_warn_about_PIL(self): 92 | with patch('dozer.leak.Dozer._start_thread'): 93 | with patch('dozer.leak.Image', None): 94 | with patch('warnings.warn') as warn: 95 | dozer = Dozer(None) 96 | warn.assert_called_once_with('PIL is not installed, cannot show charts in Dozer') 97 | 98 | def test_start(self): 99 | with patch('time.sleep') as sleep: 100 | dozer = DozerForTests() 101 | dozer.tick = dozer.stop 102 | dozer.start() 103 | sleep.assert_called_once_with(dozer.period) 104 | 105 | def test_tick(self): 106 | dozer = DozerForTests() 107 | dozer.maxhistory = 10 108 | for n in range(dozer.maxhistory * 2): 109 | dozer.tick() 110 | 111 | def test_tick_handles_disappearing_types(self): 112 | dozer = DozerForTests() 113 | obj = MyType() 114 | dozer.tick() 115 | del obj 116 | dozer.tick() 117 | self.assertEqual(dozer.history['mymodule.MyType'], [1, 0]) 118 | 119 | def test_tick_handles_types_with_broken_module(self): 120 | # https://bitbucket.org/bbangert/dozer/issue/3/cannot-user-operator-between-property-and 121 | dozer = DozerForTests() 122 | evil_proxy = EvilProxyClass(object) # keep a reference to it 123 | dozer.tick() 124 | 125 | def test_trace_all_handles_types_with_broken_module(self): 126 | dozer = DozerForTests() 127 | evil_proxy = EvilProxyClass(object) # keep a reference to it 128 | dozer.trace_all(None, 'no-such-module.No-Such-Type') 129 | 130 | def test_trace_one_handles_types_with_broken_module(self): 131 | dozer = DozerForTests() 132 | evil_proxy = EvilProxyClass(object) # keep a reference to it 133 | dozer.trace_one(None, 'no-such-module.No-Such-Type', id(evil_proxy)) 134 | 135 | def test_trace_one_shows_referers(self): 136 | dozer = DozerForTests() 137 | a = MyObj(name='the-thing') 138 | b = MyObj(name='the-referrer', has=a) 139 | req = self.make_request('/blah/blah') 140 | rows = dozer.trace_one(req, '{0.__module__}.{0.__name__}'.format(MyObj), id(a)) 141 | self.assertIn('the-referrer', '\n'.join(rows)) 142 | 143 | def test_tree_handles_types_with_broken_module(self): 144 | dozer = DozerForTests() 145 | evil_proxy = EvilProxyClass(object) # keep a reference to it 146 | req = self.make_request('/nosuchtype/%d' % id(evil_proxy)) 147 | dozer.tree(req) 148 | 149 | def test_tree_shows_referers(self): 150 | dozer = DozerForTests() 151 | a = MyObj(name='the-thing') 152 | b = MyObj(name='the-referrer', has=a) 153 | req = self.make_request('/{0.__module__}.{0.__name__}/{1}'.format(type(a), id(a))) 154 | resp = dozer.tree(req) 155 | self.assertIn('the-referrer', resp.text) 156 | 157 | 158 | class TestReferrerTree(unittest.TestCase): 159 | 160 | def make_request(self): 161 | req = Request({'PATH_INFO': '/whatevs', 'wsgi.url_scheme': 'http', 162 | 'HTTP_HOST': 'localhost'}) 163 | req.base_path = '/_dozer' 164 | return req 165 | 166 | def make_tree(self, maxdepth=10): 167 | req = self.make_request() 168 | tree = ReferrerTree(None, req) 169 | tree.maxdepth = maxdepth 170 | tree.seen = {} 171 | return tree 172 | 173 | def test_get_repr_handles_types_with_broken_module(self): 174 | tree = self.make_tree() 175 | evil_proxy = EvilProxyClass(object) 176 | tree.get_repr(evil_proxy) 177 | 178 | def test_gen_handles_types_with_broken_module(self): 179 | tree = self.make_tree() 180 | list(tree._gen(EvilProxyClass.some_constant)) 181 | 182 | def test_gen_skips_itself(self): 183 | tree = self.make_tree() 184 | list(tree._gen(ReferrerTree)) 185 | 186 | def test_gen_maxdepth(self): 187 | tree = self.make_tree(maxdepth=1) 188 | obj = object() 189 | ref = [obj] 190 | res = list(tree._gen(obj)) 191 | self.assertIn((1, 0, "---- Max depth reached ----"), res) 192 | 193 | 194 | def hello_world(environ, start_response): 195 | body = b'hello, world!' 196 | headers = [('Content-Type', 'text/html; charset=utf8'), 197 | ('Content-Length', str(len(body)))] 198 | start_response('200 Ok', headers) 199 | return [body] 200 | 201 | 202 | class MyType(object): 203 | __module__ = 'mymodule' 204 | 205 | 206 | class MyFaultyType(object): 207 | __module__ = 'mymodule' 208 | 209 | foo = 42 210 | 211 | @property 212 | def bar(self): 213 | raise KeyError 214 | 215 | 216 | class TestEntireStack(unittest.TestCase): 217 | 218 | def make_wsgi_app(self): 219 | dozer = DozerForTests(hello_world) 220 | dozer.history['mymodule.MyType'] = [1, 2, 3, 4, 5] 221 | return dozer 222 | 223 | def make_test_app(self): 224 | return webtest.TestApp(self.make_wsgi_app()) 225 | 226 | def test_application_pass_through(self): 227 | app = self.make_test_app() 228 | resp = app.get('/') 229 | self.assertIn('hello, world!', resp) 230 | 231 | def test_dozer(self): 232 | app = self.make_test_app() 233 | resp = app.get('/_dozer') 234 | self.assertEqual(resp.status_int, 200) 235 | self.assertIn('
    ', resp) 236 | 237 | def test_dozer_index(self): 238 | app = self.make_test_app() 239 | resp = app.get('/_dozer/index') 240 | self.assertEqual(resp.status_int, 200) 241 | self.assertIn('
    ', resp) 242 | 243 | def test_dozer_chart(self): 244 | app = self.make_test_app() 245 | resp = app.get('/_dozer/chart/mymodule.MyType') 246 | self.assertEqual(resp.status_int, 200) 247 | self.assertEqual(resp.content_type, 'image/png') 248 | 249 | def test_dozer_chart_no_PIL(self): 250 | app = self.make_test_app() 251 | with patch('dozer.leak.Image', None): 252 | resp = app.get('/_dozer/chart/mymodule.MyType', status=404) 253 | 254 | def test_dozer_trace_all(self): 255 | app = self.make_test_app() 256 | resp = app.get('/_dozer/trace/mymodule.MyType') 257 | self.assertEqual(resp.status_int, 200) 258 | self.assertIn('
    ', resp) 259 | 260 | def test_dozer_trace_all_not_empty(self): 261 | app = self.make_test_app() 262 | obj = MyType() # keep a reference so it's not gc'ed 263 | resp = app.get('/_dozer/trace/mymodule.MyType') 264 | self.assertEqual(resp.status_int, 200) 265 | self.assertIn('
    ', resp) 266 | self.assertIn("

    ", resp) 267 | 268 | def test_dozer_trace_one(self): 269 | app = self.make_test_app() 270 | resp = app.get('/_dozer/trace/mymodule.MyType/1234') 271 | self.assertEqual(resp.status_int, 200) 272 | self.assertIn('

    ', resp) 273 | 274 | def test_dozer_trace_one_raising_property(self): 275 | app = self.make_test_app() 276 | obj = MyFaultyType() # keep a reference so it's not gc'ed 277 | resp = app.get('/_dozer/trace/mymodule.MyFaultyType/{}'.format(id(obj))) 278 | self.assertEqual(resp.status_int, 200) 279 | self.assertIn('

    bar: KeyError()

    ', resp) 280 | self.assertIn('

    foo: 42

    ', resp) 281 | 282 | def test_dozer_trace_one_not_empty(self): 283 | app = self.make_test_app() 284 | obj = MyType() 285 | resp = app.get('/_dozer/trace/mymodule.MyType/%d' % id(obj)) 286 | self.assertEqual(resp.status_int, 200) 287 | self.assertIn('
    ', resp) 288 | self.assertIn('
    ', resp) 289 | 290 | def test_dozer_tree(self): 291 | app = self.make_test_app() 292 | resp = app.get('/_dozer/tree/mymodule.MyType/1234') 293 | self.assertEqual(resp.status_int, 200) 294 | self.assertIn('
    ', resp) 295 | 296 | def test_dozer_tree_not_empty(self): 297 | app = self.make_test_app() 298 | obj = MyType() 299 | resp = app.get('/_dozer/tree/mymodule.MyType/%d' % id(obj)) 300 | self.assertEqual(resp.status_int, 200) 301 | self.assertIn('
    ', resp) 302 | self.assertIn('
    ', resp) 303 | # this removes a 3-second pause in the next test 304 | gc.collect() 305 | 306 | def test_dozer_media(self): 307 | app = self.make_test_app() 308 | resp = app.get('/_dozer/media/css/main.css') 309 | self.assertEqual(resp.status_int, 200) 310 | self.assertEqual(resp.content_type, 'text/css') 311 | self.assertIn('.typename {', resp) 312 | 313 | def test_dozer_not_found(self): 314 | app = self.make_test_app() 315 | resp = app.get('/_dozer/nosuchpage', status=404) 316 | 317 | def test_dozer_forbidden(self): 318 | app = self.make_test_app() 319 | resp = app.get('/_dozer/dowse', status=403) 320 | 321 | def test_dozer_sortby(self): 322 | app = self.make_test_app() 323 | app.app.history['mymodule.AnotherType'] = [10, 20, 30, 40, 50] 324 | resp = app.get('/_dozer/?sortby=-monotonicity') 325 | self.assertEqual(resp.status_int, 200) 326 | 327 | def test_dozer_floor(self): 328 | app = self.make_test_app() 329 | app.app.history['mymodule.AnotherType'] = [10, 20, 30, 40, 50] 330 | resp = app.get('/_dozer/?floor=4') 331 | self.assertEqual(resp.status_int, 200) 332 | self.assertIn('', resp) 333 | self.assertIn('mymodule.AnotherType', resp) 334 | self.assertIn('mymodule.MyType', resp) 335 | 336 | resp = app.get('/_dozer/?floor=10') 337 | self.assertEqual(resp.status_int, 200) 338 | self.assertIn('', resp) 339 | self.assertIn('mymodule.AnotherType', resp) 340 | self.assertNotIn('mymodule.MyType', resp) 341 | 342 | def test_dozer_filter(self): 343 | app = self.make_test_app() 344 | app.app.history['mymodule.AnotherType'] = [10, 20, 30, 40, 50] 345 | resp = app.get('/_dozer/?filter=type') 346 | self.assertEqual(resp.status_int, 200) 347 | self.assertIn('', resp) 348 | self.assertIn('mymodule.AnotherType', resp) 349 | self.assertIn('mymodule.MyType', resp) 350 | 351 | resp = app.get('/_dozer/?filter=another') 352 | self.assertEqual(resp.status_int, 200) 353 | self.assertIn('', resp) 354 | self.assertIn('mymodule.AnotherType', resp) 355 | self.assertNotIn('mymodule.MyType', resp) 356 | 357 | def test_dozer_filter_broken_re_500_with_traceback(self): 358 | app = self.make_test_app() 359 | resp = app.get('/_dozer/?filter=(', status=500) 360 | self.assertEqual(resp.status_int, 500) 361 | self.assertIn('500 Internal Server Error', resp) 362 | self.assertIn('Traceback (most recent call last)', resp) 363 | self.assertIn( 364 | # Python 3.12 and older raise re.error: ... 365 | # Python 3.13 raises re.PatternError: ... 366 | 'rror: missing ), unterminated subpattern at position 0', resp 367 | ) 368 | -------------------------------------------------------------------------------- /dozer/leak.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import gc 3 | import pathlib 4 | import re 5 | import sys 6 | import threading 7 | import time 8 | import traceback 9 | import types 10 | import warnings 11 | from html import escape 12 | from io import BytesIO 13 | from types import FrameType, GeneratorType, ModuleType 14 | 15 | from webob import Request, Response, exc, static 16 | 17 | from dozer import reftree 18 | from dozer.util import monotonicity, sort_dict_by_val 19 | 20 | 21 | try: 22 | from PIL import Image, ImageDraw 23 | except ImportError: 24 | Image = ImageDraw = None 25 | 26 | 27 | localDir = pathlib.Path(__file__).parent.resolve() 28 | 29 | 30 | def get_repr(obj, limit=250): 31 | return escape(reftree.get_repr(obj, limit)) 32 | 33 | 34 | method_types = [ 35 | types.BuiltinFunctionType, # 'builtin_function_or_method' 36 | types.BuiltinMethodType, # 'builtin_function_or_method' 37 | types.MethodWrapperType, # 'method-wrapper' 38 | types.WrapperDescriptorType, # 'wrapper_descriptor' 39 | types.MethodType, # 'method' aka bound method 40 | types.FunctionType, # 'function' and also unbound method 41 | # should GeneratorType and AsyncGeneratorType also be here? 42 | ] 43 | 44 | 45 | sort_keys = { 46 | "monotonicity": monotonicity 47 | } 48 | 49 | 50 | def url(req, path): 51 | if path.startswith('/'): 52 | path = path[1:] 53 | base_path = req.base_path 54 | if base_path.endswith('/'): 55 | return base_path + path 56 | else: 57 | return base_path + '/' + path 58 | 59 | 60 | def template(req, name, **params): 61 | p = { 62 | 'maincss': url(req, "/media/css/main.css"), 63 | 'home': url(req, "/index"), 64 | } 65 | p.update(params) 66 | return localDir.joinpath('media', name).read_text() % p 67 | 68 | 69 | def get_sort_key(sortby): 70 | if len(sortby) < 1: 71 | return None, False 72 | if sortby[0] == '-': 73 | return sort_keys.get(sortby[1:], None), True 74 | else: 75 | return sort_keys.get(sortby, None), False 76 | 77 | 78 | class Dozer(object): 79 | """Sets up a page that displays object information to help 80 | troubleshoot memory leaks""" 81 | period = 5 82 | maxhistory = 300 83 | 84 | def __init__(self, app, global_conf=None, media_paths=None, path='/_dozer', 85 | **kwargs): 86 | self.app = app 87 | self.media_paths = media_paths or {} 88 | if path.endswith('/'): 89 | path = path[:-1] 90 | self.path = path 91 | self.history = {} 92 | self.samples = 0 93 | self._start_thread() 94 | self._maybe_warn_about_PIL() 95 | 96 | def _start_thread(self): 97 | self.runthread = threading.Thread(name='Dozer', target=self.start) 98 | self.runthread.daemon = True 99 | self.runthread.start() 100 | 101 | def _maybe_warn_about_PIL(self): 102 | if Image is None or ImageDraw is None: 103 | warnings.warn('PIL is not installed, cannot show charts in Dozer') 104 | 105 | def __call__(self, environ, start_response): 106 | assert not environ['wsgi.multiprocess'], ( 107 | "Dozer middleware is not usable in a " 108 | "multi-process environment") 109 | req = Request(environ) 110 | req.base_path = req.application_url + self.path 111 | if (req.path_info.startswith(self.path+'/') 112 | or req.path_info == self.path): 113 | req.script_name += self.path 114 | req.path_info = req.path_info[len(self.path):] 115 | try: 116 | return self.dowse(req)(environ, start_response) 117 | except Exception as ex: 118 | error_text = traceback.format_exc() 119 | 120 | acceptable_offers = req.accept.acceptable_offers( 121 | offers=['text/html', 'application/json'] 122 | ) 123 | match = acceptable_offers[0][0] if acceptable_offers else None 124 | if match != 'application/json': 125 | # Strangely, webob.exc.WSGIHTTPException.plain_body replaces newlines 126 | # to spaces for plain/text, but replaces "
    " tags to newlines. 127 | error_text = error_text.replace('\n', '
    ') 128 | 129 | return exc.HTTPInternalServerError( 130 | str(ex), body_template=error_text 131 | )(environ, start_response) 132 | else: 133 | return self.app(environ, start_response) 134 | 135 | def dowse(self, req): 136 | next_part = req.path_info_pop() or 'index' 137 | method = getattr(self, next_part, None) 138 | if method is None: 139 | return exc.HTTPNotFound('Nothing could be found to match %r' % next_part) 140 | if not getattr(method, 'exposed', False): 141 | return exc.HTTPForbidden('Access to %r is forbidden' % next_part) 142 | return method(req) 143 | 144 | def media(self, req): 145 | """Static path where images and other files live""" 146 | path = localDir.joinpath('media') 147 | return static.DirectoryApp(path) 148 | media.exposed = True 149 | 150 | def start(self): 151 | self.running = True 152 | while self.running: 153 | self.tick() 154 | time.sleep(self.period) 155 | 156 | def tick(self): 157 | gc.collect() 158 | 159 | typenamecounts = collections.defaultdict(int) 160 | for obj in gc.get_objects(): 161 | objtype = type(obj) 162 | typename = "%s.%s" % (objtype.__module__, objtype.__name__) 163 | typenamecounts[typename] += 1 164 | 165 | for typename, count in typenamecounts.items(): 166 | if typename not in self.history: 167 | self.history[typename] = [0] * self.samples 168 | self.history[typename].append(count) 169 | 170 | samples = self.samples + 1 171 | 172 | # Add dummy entries for any types which no longer exist 173 | for typename, hist in self.history.items(): 174 | diff = samples - len(hist) 175 | if diff > 0: 176 | hist.extend([0] * diff) 177 | 178 | # Truncate history to self.maxhistory 179 | if samples > self.maxhistory: 180 | for typename, hist in self.history.items(): 181 | hist.pop(0) 182 | else: 183 | self.samples = samples 184 | 185 | def stop(self): 186 | self.running = False 187 | 188 | def index(self, req): 189 | floor = req.GET.get('floor', 0) or 0 190 | filtertext = req.GET.get('filter', '') 191 | sortby = req.GET.get('sortby', '') 192 | filterre = re.compile(filtertext, re.IGNORECASE) if filtertext else None 193 | rows = [] 194 | typenames = sorted(self.history) 195 | sort_key, reversed = get_sort_key(sortby) 196 | if sort_key is not None: 197 | sorted_history = sort_dict_by_val(self.history, sort_key, reversed) 198 | else: 199 | sorted_history = sorted(self.history.items()) 200 | 201 | for typename, hist in sorted_history: 202 | maxhist = max(hist) 203 | if ( 204 | maxhist > int(floor) 205 | and (not filterre or filterre.search(typename)) 206 | ): 207 | row = ('
    %s
    ' 208 | '
    ' 209 | 'Min: %s Cur: %s Max: %s TRACE
    ' 210 | % (escape(typename), 211 | url(req, "chart/%s" % typename), 212 | min(hist), hist[-1], maxhist, 213 | url(req, "trace/%s" % typename), 214 | ) 215 | ) 216 | rows.append(row) 217 | res = Response() 218 | res.text = template( 219 | req, 220 | "graphs.html", 221 | output="\n".join(rows), 222 | floor=floor, 223 | filter=escape(filtertext), 224 | sortby=sortby, 225 | jquery=url(req, "media/javascript/jquery-1.2.6.min.js") 226 | ) 227 | return res 228 | index.exposed = True 229 | 230 | def chart(self, req): 231 | """Return a sparkline chart of the given type.""" 232 | if Image is None or ImageDraw is None: 233 | # Cannot render 234 | return Response('Cannot render; PIL/Pillow is not installed', 235 | status='404 Not Found') 236 | typename = req.path_info_pop() 237 | data = self.history[typename] 238 | height = 20.0 239 | scale = height / max(data) 240 | im = Image.new("RGB", (len(data), int(height)), 'white') 241 | draw = ImageDraw.Draw(im) 242 | draw.line([(i, int(height - (v * scale))) for i, v in enumerate(data)], 243 | fill="#009900") 244 | del draw 245 | 246 | f = BytesIO() 247 | im.save(f, "PNG") 248 | result = f.getvalue() 249 | 250 | res = Response() 251 | res.headers["Content-Type"] = "image/png" 252 | res.body = result 253 | return res 254 | chart.exposed = True 255 | 256 | def trace(self, req): 257 | typename = req.path_info_pop() 258 | objid = req.path_info_pop() 259 | gc.collect() 260 | 261 | if objid is None: 262 | rows = self.trace_all(req, typename) 263 | else: 264 | rows = self.trace_one(req, typename, objid) 265 | 266 | res = Response() 267 | res.text = template(req, "trace.html", output="\n".join(rows), 268 | typename=escape(typename), 269 | objid=str(objid or '')) 270 | return res 271 | trace.exposed = True 272 | 273 | def trace_all(self, req, typename): 274 | rows = [] 275 | for obj in gc.get_objects(): 276 | objtype = type(obj) 277 | if "%s.%s" % (objtype.__module__, objtype.__name__) == typename: 278 | rows.append("

    %s

    " 279 | % ReferrerTree(obj, req).get_repr(obj)) 280 | if not rows: 281 | rows = ["

    The type you requested was not found.

    "] 282 | return rows 283 | 284 | def trace_one(self, req, typename, objid): 285 | rows = [] 286 | objid = int(objid) 287 | all_objs = gc.get_objects() 288 | for obj in all_objs: 289 | if id(obj) == objid: 290 | objtype = type(obj) 291 | if "%s.%s" % (objtype.__module__, objtype.__name__) != typename: 292 | rows = ["

    The object you requested is no longer " 293 | "of the correct type.

    "] 294 | else: 295 | # Attributes 296 | rows.append('

    Attributes

    ') 297 | for k in dir(obj): 298 | try: 299 | v = getattr(obj, k, AttributeError) 300 | except Exception as ex: 301 | v = ex 302 | if type(v) not in method_types: 303 | rows.append('

    %s: %s

    ' % 304 | (k, get_repr(v))) 305 | del v 306 | rows.append('
    ') 307 | 308 | # Referrers 309 | rows.append('

    Referrers (Parents)

    ') 310 | rows.append('

    Show the ' 311 | 'entire tree of reachable objects

    ' 312 | % url(req, "/tree/%s/%s" % (typename, objid))) 313 | tree = ReferrerTree(obj, req) 314 | tree.ignore(all_objs) 315 | for depth, parentid, parentrepr in tree.walk(maxdepth=1): 316 | if parentid: 317 | rows.append("

    %s

    " % parentrepr) 318 | rows.append('
    ') 319 | 320 | # Referents 321 | rows.append('

    Referents (Children)

    ') 322 | for child in gc.get_referents(obj): 323 | rows.append("

    %s

    " % tree.get_repr(child)) 324 | rows.append('
    ') 325 | break 326 | if not rows: 327 | rows = ["

    The object you requested was not found.

    "] 328 | return rows 329 | 330 | def tree(self, req): 331 | typename = req.path_info_pop() 332 | objid = req.path_info_pop() 333 | gc.collect() 334 | 335 | rows = [] 336 | objid = int(objid) 337 | all_objs = gc.get_objects() 338 | for obj in all_objs: 339 | if id(obj) == objid: 340 | objtype = type(obj) 341 | if "%s.%s" % (objtype.__module__, objtype.__name__) != typename: 342 | rows = ["

    The object you requested is no longer " 343 | "of the correct type.

    "] 344 | else: 345 | rows.append('
    ') 346 | 347 | tree = ReferrerTree(obj, req) 348 | tree.ignore(all_objs) 349 | for depth, parentid, parentrepr in tree.walk(maxresults=1000): 350 | rows.append(parentrepr) 351 | 352 | rows.append('
    ') 353 | break 354 | if not rows: 355 | rows = ["

    The object you requested was not found.

    "] 356 | 357 | params = {'output': "\n".join(rows), 358 | 'typename': escape(typename), 359 | 'objid': str(objid), 360 | } 361 | res = Response() 362 | res.text = template(req, "tree.html", **params) 363 | return res 364 | tree.exposed = True 365 | 366 | 367 | class ReferrerTree(reftree.Tree): 368 | 369 | ignore_modules = True 370 | 371 | def _gen(self, obj, depth=0): 372 | if self.maxdepth and depth >= self.maxdepth: 373 | yield depth, 0, "---- Max depth reached ----" 374 | return 375 | 376 | if isinstance(obj, ModuleType) and self.ignore_modules: 377 | return 378 | 379 | refs = gc.get_referrers(obj) 380 | refiter = iter(refs) 381 | self.ignore(refs, refiter) 382 | thisfile = sys._getframe().f_code.co_filename 383 | for ref in refiter: 384 | # Exclude all frames that are from this module or reftree. 385 | if (isinstance(ref, FrameType) 386 | and ref.f_code.co_filename in (thisfile, self.filename)): 387 | continue # pragma: nocover -- on Python 3.11 this is never hit? 388 | if (isinstance(ref, GeneratorType) 389 | and ref.gi_code.co_filename in (thisfile, self.filename)): 390 | continue # pragma: nocover -- this is only hit on Python 3.14 391 | 392 | # Exclude all functions and classes from this module or reftree. 393 | mod = str(getattr(ref, "__module__", "")) 394 | if "dozer" in mod or "reftree" in mod or mod == '__main__': 395 | continue # pragma: nocover -- avoid bug in coverage due to Python's peephole optimizer 396 | 397 | # Exclude all parents in our ignore list. 398 | if id(ref) in self._ignore: 399 | continue 400 | 401 | # Yield the (depth, id, repr) of our object. 402 | yield depth, 0, '%s
    ' % (" " * depth) 403 | if id(ref) in self.seen: 404 | yield depth, id(ref), "see %s above" % id(ref) 405 | else: 406 | self.seen[id(ref)] = None 407 | yield depth, id(ref), self.get_repr(ref, obj) 408 | 409 | for parent in self._gen(ref, depth + 1): 410 | yield parent 411 | yield depth, 0, '%s
    ' % (" " * depth) 412 | 413 | def get_repr(self, obj, referent=None): 414 | """Return an HTML tree block describing the given object.""" 415 | objtype = type(obj) 416 | typename = "%s.%s" % (objtype.__module__, objtype.__name__) 417 | prettytype = typename.replace("__builtin__.", "") 418 | 419 | name = getattr(obj, "__name__", "") 420 | if name: 421 | prettytype = "%s %r" % (prettytype, name) 422 | 423 | key = "" 424 | if referent: 425 | key = self.get_refkey(obj, referent) 426 | return ('%s ' 427 | '%s%s
    ' 428 | '%s' 429 | % (url(self.req, "/trace/%s/%s" % (typename, id(obj))), 430 | id(obj), prettytype, key, get_repr(obj, 100)) 431 | ) 432 | 433 | def get_refkey(self, obj, referent): 434 | """Return the dict key or attribute name of obj which refers to referent.""" 435 | if isinstance(obj, dict): 436 | for k, v in obj.items(): 437 | if v is referent: 438 | return " (via its %r key)" % repr(k) 439 | 440 | for k in dir(obj) + ['__dict__']: 441 | if getattr(obj, k, None) is referent: 442 | return " (via its %r attribute)" % repr(k) 443 | return "" 444 | -------------------------------------------------------------------------------- /dozer/media/javascript/canviz.js: -------------------------------------------------------------------------------- 1 | // $Id: canviz.js 390 2007-03-29 10:45:46Z rschmidt $ 2 | 3 | var Tokenizer = Class.create(); 4 | Tokenizer.prototype = { 5 | initialize: function(str) { 6 | this.str = str; 7 | }, 8 | takeChars: function(num) { 9 | if (!num) { 10 | num = 1; 11 | } 12 | var tokens = new Array(); 13 | while (num--) { 14 | var matches = this.str.match(/^(\S+)\s*/); 15 | if (matches) { 16 | this.str = this.str.substr(matches[0].length); 17 | tokens.push(matches[1]); 18 | } else { 19 | tokens.push(false); 20 | } 21 | } 22 | if (1 == tokens.length) { 23 | return tokens[0]; 24 | } else { 25 | return tokens; 26 | } 27 | }, 28 | takeNumber: function(num) { 29 | if (!num) { 30 | num = 1; 31 | } 32 | if (1 == num) { 33 | return Number(this.takeChars()) 34 | } else { 35 | var tokens = this.takeChars(num); 36 | while (num--) { 37 | tokens[num] = Number(tokens[num]); 38 | } 39 | return tokens; 40 | } 41 | }, 42 | takeString: function() { 43 | var chars = Number(this.takeChars()); 44 | if ('-' != this.str.charAt(0)) { 45 | return false; 46 | } 47 | var str = this.str.substr(1, chars); 48 | this.str = this.str.substr(1 + chars).replace(/^\s+/, ''); 49 | return str; 50 | } 51 | } 52 | 53 | var Graph = Class.create(); 54 | Graph.prototype = { 55 | initialize: function(ctx, file, engine) { 56 | this.maxXdotVersion = 1.2; 57 | this.systemScale = 4/3; 58 | this.scale = 1; 59 | this.padding = 8; 60 | this.ctx = ctx; 61 | this.images = new Hash(); 62 | this.numImages = 0; 63 | this.numImagesFinished = 0; 64 | if (file) { 65 | this.load(file, engine); 66 | } 67 | }, 68 | setImagePath: function(imagePath) { 69 | this.imagePath = imagePath; 70 | }, 71 | load: function(file, engine) { 72 | $('debug_output').innerHTML = ''; 73 | var url = 'graph.php'; 74 | var params = 'file=' + file; 75 | if (engine) { 76 | params += '&engine=' + engine; 77 | } 78 | new Ajax.Request(url, { 79 | method: 'get', 80 | parameters: params, 81 | onComplete: this.parse.bind(this) 82 | }); 83 | }, 84 | parse: function(request) { 85 | this.xdotversion = false; 86 | this.commands = new Array(); 87 | this.width = 0; 88 | this.height = 0; 89 | this.maxWidth = false; 90 | this.maxHeight = false; 91 | this.bbEnlarge = false; 92 | this.bbScale = 1; 93 | this.orientation = 'portrait'; 94 | this.bgcolor = '#ffffff'; 95 | this.dashLength = 6; 96 | this.dotSpacing = 4; 97 | this.fontName = 'Times New Roman'; 98 | this.fontSize = 14; 99 | var graph_src = request.responseText; 100 | var lines = graph_src.split('\n'); 101 | var i = 0; 102 | var line, lastchar, matches, is_graph, entity, params, param_name, param_value; 103 | var container_stack = new Array(); 104 | while (i < lines.length) { 105 | line = lines[i++].replace(/^\s+/, ''); 106 | if ('' != line && '#' != line.substr(0, 1)) { 107 | while (i < lines.length && ';' != (lastchar = line.substr(line.length - 1, line.length)) && '{' != lastchar && '}' != lastchar) { 108 | if ('\\' == lastchar) { 109 | line = line.substr(0, line.length - 1); 110 | } 111 | line += lines[i++]; 112 | } 113 | // debug(line); 114 | matches = line.match(/^(.*?)\s*{$/); 115 | if (matches) { 116 | container_stack.push(matches[1]); 117 | // debug('begin container ' + container_stack.last()); 118 | } else if ('}' == line) { 119 | // debug('end container ' + container_stack.last()); 120 | container_stack.pop(); 121 | } else { 122 | // matches = line.match(/^(".*?[^\\]"|\S+?)\s+\[(.+)\];$/); 123 | matches = line.match(/^(.*?)\s+\[(.+)\];$/); 124 | if (matches) { 125 | is_graph = ('graph' == matches[1]); 126 | // entity = this.unescape(matches[1]); 127 | entity = matches[1]; 128 | params = matches[2]; 129 | do { 130 | matches = params.match(/^(\S+?)=(""|".*?[^\\]"|<(<[^>]+>|[^<>]+?)+>|\S+?)(?:,\s*|$)/); 131 | if (matches) { 132 | params = params.substr(matches[0].length); 133 | param_name = matches[1]; 134 | param_value = this.unescape(matches[2]); 135 | // debug(param_name + ' ' + param_value); 136 | if (is_graph && 1 == container_stack.length) { 137 | switch (param_name) { 138 | case 'bb': 139 | var bb = param_value.split(/,/); 140 | this.width = Number(bb[2]); 141 | this.height = Number(bb[3]); 142 | break; 143 | case 'bgcolor': 144 | this.bgcolor = this.parseColor(param_value); 145 | break; 146 | case 'size': 147 | var size = param_value.match(/^(\d+|\d*(?:\.\d+)),\s*(\d+|\d*(?:\.\d+))(!?)$/); 148 | if (size) { 149 | this.maxWidth = 72 * Number(size[1]); 150 | this.maxHeight = 72 * Number(size[2]); 151 | this.bbEnlarge = ('!' == size[3]); 152 | } else { 153 | debug('can\'t parse size'); 154 | } 155 | break; 156 | case 'orientation': 157 | if (param_value.match(/^l/i)) { 158 | this.orientation = 'landscape'; 159 | } 160 | break; 161 | case 'rotate': 162 | if (90 == param_value) { 163 | this.orientation = 'landscape'; 164 | } 165 | break; 166 | case 'xdotversion': 167 | this.xdotversion = parseFloat(param_value); 168 | if (this.maxXdotVersion < this.xdotversion) { 169 | debug('unsupported xdotversion ' + this.xdotversion + '; this script currently supports up to xdotversion ' + this.maxXdotVersion); 170 | } 171 | break; 172 | } 173 | } 174 | switch (param_name) { 175 | case '_draw_': 176 | case '_ldraw_': 177 | case '_hdraw_': 178 | case '_tdraw_': 179 | case '_hldraw_': 180 | case '_tldraw_': 181 | // debug(entity + ': ' + param_value); 182 | this.commands.push(param_value); 183 | break; 184 | } 185 | } 186 | } while (matches); 187 | } 188 | } 189 | } 190 | } 191 | if (!this.xdotversion) { 192 | this.xdotversion = 1.0; 193 | } 194 | /* 195 | if (this.maxWidth && this.maxHeight) { 196 | if (this.width > this.maxWidth || this.height > this.maxHeight || this.bbEnlarge) { 197 | this.bbScale = Math.min(this.maxWidth / this.width, this.maxHeight / this.height); 198 | this.width = Math.round(this.width * this.bbScale); 199 | this.height = Math.round(this.height * this.bbScale); 200 | } 201 | if ('landscape' == this.orientation) { 202 | var temp = this.width; 203 | this.width = this.height; 204 | this.height = temp; 205 | } 206 | } 207 | */ 208 | // debug('done'); 209 | this.draw(); 210 | }, 211 | draw: function(redraw_canvas) { 212 | if (!redraw_canvas) redraw_canvas = false; 213 | var width = Math.round(this.scale * this.systemScale * this.width + 2 * this.padding); 214 | var height = Math.round(this.scale * this.systemScale * this.height + 2 * this.padding); 215 | if (!redraw_canvas) { 216 | canvas.width = width; 217 | canvas.height = height; 218 | Element.setStyle(canvas, { 219 | width: width + 'px', 220 | height: height + 'px' 221 | }); 222 | Element.setStyle('graph_container', { 223 | width: width + 'px' 224 | }); 225 | $('graph_texts').innerHTML = ''; 226 | } 227 | this.ctx.save(); 228 | this.ctx.lineCap = 'round'; 229 | this.ctx.fillStyle = this.bgcolor; 230 | this.ctx.fillRect(0, 0, width, height); 231 | this.ctx.translate(this.padding, this.padding); 232 | this.ctx.scale(this.scale * this.systemScale, this.scale * this.systemScale); 233 | this.ctx.lineWidth = 1 / this.systemScale; 234 | var i, tokens; 235 | var entity_id = 0; 236 | var text_divs = ''; 237 | for (var command_index = 0; command_index < this.commands.length; command_index++) { 238 | var command = this.commands[command_index]; 239 | // debug(command); 240 | var tokenizer = new Tokenizer(command); 241 | var token = tokenizer.takeChars(); 242 | if (token) { 243 | ++entity_id; 244 | var entity_text_divs = ''; 245 | this.dashStyle = 'solid'; 246 | this.ctx.save(); 247 | while (token) { 248 | // debug('processing token ' + token); 249 | switch (token) { 250 | case 'E': // filled ellipse 251 | case 'e': // unfilled ellipse 252 | var filled = ('E' == token); 253 | var cx = tokenizer.takeNumber(); 254 | var cy = this.height - tokenizer.takeNumber(); 255 | var rx = tokenizer.takeNumber(); 256 | var ry = tokenizer.takeNumber(); 257 | this.render(new Ellipse(cx, cy, rx, ry), filled); 258 | break; 259 | case 'P': // filled polygon 260 | case 'p': // unfilled polygon 261 | case 'L': // polyline 262 | var filled = ('P' == token); 263 | var closed = ('L' != token); 264 | var num_points = tokenizer.takeNumber(); 265 | tokens = tokenizer.takeNumber(2 * num_points); // points 266 | var path = new Path(); 267 | for (i = 2; i < 2 * num_points; i += 2) { 268 | path.addBezier([ 269 | new Point(tokens[i - 2], this.height - tokens[i - 1]), 270 | new Point(tokens[i], this.height - tokens[i + 1]) 271 | ]); 272 | } 273 | if (closed) { 274 | path.addBezier([ 275 | new Point(tokens[2 * num_points - 2], this.height - tokens[2 * num_points - 1]), 276 | new Point(tokens[0], this.height - tokens[1]) 277 | ]); 278 | } 279 | this.render(path, filled); 280 | break; 281 | case 'B': // unfilled b-spline 282 | case 'b': // filled b-spline 283 | var filled = ('b' == token); 284 | var num_points = tokenizer.takeNumber(); 285 | tokens = tokenizer.takeNumber(2 * num_points); // points 286 | var path = new Path(); 287 | for (i = 2; i < 2 * num_points; i += 6) { 288 | path.addBezier([ 289 | new Point(tokens[i - 2], this.height - tokens[i - 1]), 290 | new Point(tokens[i], this.height - tokens[i + 1]), 291 | new Point(tokens[i + 2], this.height - tokens[i + 3]), 292 | new Point(tokens[i + 4], this.height - tokens[i + 5]) 293 | ]); 294 | } 295 | this.render(path, filled); 296 | break; 297 | case 'I': // image 298 | var x = tokenizer.takeNumber(); 299 | var y = this.height - tokenizer.takeNumber(); 300 | var w = tokenizer.takeNumber(); 301 | var h = tokenizer.takeNumber(); 302 | var src = tokenizer.takeString(); 303 | if (!this.images[src]) { 304 | y -= h; 305 | this.images[src] = new GraphImage(this, src, x, y, w, h); 306 | } 307 | this.images[src].draw(); 308 | break; 309 | case 'T': // text 310 | var x = Math.round(this.scale * this.systemScale * tokenizer.takeNumber() + this.padding); 311 | var y = Math.round(height - (this.scale * this.systemScale * (tokenizer.takeNumber() + this.bbScale * this.fontSize) + this.padding)); 312 | var text_align = tokenizer.takeNumber(); 313 | var text_width = Math.round(this.scale * this.systemScale * tokenizer.takeNumber()); 314 | var str = tokenizer.takeString(); 315 | if (!redraw_canvas && !str.match(/^\s*$/)) { 316 | // debug('draw text ' + str + ' ' + x + ' ' + y + ' ' + text_align + ' ' + text_width); 317 | str = str.escapeHTML(); 318 | do { 319 | matches = str.match(/ ( +)/); 320 | if (matches) { 321 | var spaces = ' '; 322 | matches[1].length.times(function() { 323 | spaces += ' '; 324 | }); 325 | str = str.replace(/ +/, spaces); 326 | } 327 | } while (matches); 328 | entity_text_divs += '
    ' + str + '
    '; 342 | } 343 | break; 344 | case 'C': // set fill color 345 | case 'c': // set pen color 346 | var fill = ('C' == token); 347 | var color = this.parseColor(tokenizer.takeString()); 348 | if (fill) { 349 | this.ctx.fillStyle = color; 350 | } else { 351 | this.ctx.strokeStyle = color; 352 | } 353 | break; 354 | case 'F': // set font 355 | this.fontSize = tokenizer.takeNumber(); 356 | this.fontName = tokenizer.takeString(); 357 | switch (this.fontName) { 358 | case 'Times-Roman': 359 | this.fontName = 'Times New Roman'; 360 | break; 361 | case 'Courier': 362 | this.fontName = 'Courier New'; 363 | break; 364 | case 'Helvetica': 365 | this.fontName = 'Arial'; 366 | break; 367 | default: 368 | // nothing 369 | } 370 | // debug('set font ' + this.fontSize + 'pt ' + this.fontName); 371 | break; 372 | case 'S': // set style 373 | var style = tokenizer.takeString(); 374 | switch (style) { 375 | case 'solid': 376 | case 'filled': 377 | // nothing 378 | break; 379 | case 'dashed': 380 | case 'dotted': 381 | this.dashStyle = style; 382 | break; 383 | case 'bold': 384 | this.ctx.lineWidth = 2 / this.systemScale; 385 | break; 386 | default: 387 | matches = style.match(/^setlinewidth\((.*)\)$/); 388 | if (matches) { 389 | this.ctx.lineWidth = Number(matches[1]) / this.systemScale; 390 | } else { 391 | debug('unknown style ' + style); 392 | } 393 | } 394 | break; 395 | default: 396 | debug('unknown token ' + token); 397 | return; 398 | } 399 | token = tokenizer.takeChars(); 400 | } 401 | this.ctx.restore(); 402 | if (entity_text_divs) { 403 | text_divs += '
    ' + entity_text_divs + '
    '; 404 | } 405 | } 406 | }; 407 | this.ctx.restore(); 408 | if (!redraw_canvas) $('graph_texts').innerHTML = text_divs; 409 | }, 410 | render: function(path, filled) { 411 | if (filled) { 412 | this.ctx.beginPath(); 413 | path.draw(this.ctx); 414 | this.ctx.fill(); 415 | } 416 | if (this.ctx.fillStyle != this.ctx.strokeStyle || !filled) { 417 | switch (this.dashStyle) { 418 | case 'dashed': 419 | this.ctx.beginPath(); 420 | path.drawDashed(this.ctx, this.dashLength); 421 | break; 422 | case 'dotted': 423 | var oldLineWidth = this.ctx.lineWidth; 424 | this.ctx.lineWidth *= 2; 425 | this.ctx.beginPath(); 426 | path.drawDotted(this.ctx, this.dotSpacing); 427 | break; 428 | case 'solid': 429 | default: 430 | if (!filled) { 431 | this.ctx.beginPath(); 432 | path.draw(this.ctx); 433 | } 434 | } 435 | this.ctx.stroke(); 436 | if (oldLineWidth) this.ctx.lineWidth = oldLineWidth; 437 | } 438 | }, 439 | unescape: function(str) { 440 | var matches = str.match(/^"(.*)"$/); 441 | if (matches) { 442 | return matches[1].replace(/\\"/g, '"'); 443 | } else { 444 | return str; 445 | } 446 | }, 447 | parseColor: function(color) { 448 | if (gvcolors[color]) { // named color 449 | return 'rgb(' + gvcolors[color][0] + ',' + gvcolors[color][1] + ',' + gvcolors[color][2] + ')'; 450 | } else { 451 | var matches = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i); 452 | if (matches) { // rgba 453 | return 'rgba(' + parseInt(matches[1], 16) + ',' + parseInt(matches[2], 16) + ',' + parseInt(matches[3], 16) + ',' + (parseInt(matches[4], 16) / 255) + ')'; 454 | } else { 455 | matches = color.match(/(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)/); 456 | if (matches) { // hsv 457 | return this.hsvToRgbColor(matches[1], matches[2], matches[3]); 458 | } else if (color.match(/^#[0-9a-f]{6}$/i)) { 459 | return color; 460 | } 461 | } 462 | } 463 | debug('unknown color ' + color); 464 | return '#000000'; 465 | }, 466 | hsvToRgbColor: function(h, s, v) { 467 | var i, f, p, q, t, r, g, b; 468 | h *= 360; 469 | i = Math.floor(h / 60) % 6; 470 | f = h / 60 - i; 471 | p = v * (1 - s); 472 | q = v * (1 - f * s); 473 | t = v * (1 - (1 - f) * s) 474 | switch (i) { 475 | case 0: r = v; g = t; b = p; break; 476 | case 1: r = q; g = v; b = p; break; 477 | case 2: r = p; g = v; b = t; break; 478 | case 3: r = p; g = q; b = v; break; 479 | case 4: r = t; g = p; b = v; break; 480 | case 5: r = v; g = p; b = q; break; 481 | } 482 | return 'rgb(' + Math.round(255 * r) + ',' + Math.round(255 * g) + ',' + Math.round(255 * b) + ')'; 483 | } 484 | } 485 | 486 | var GraphImage = Class.create(); 487 | GraphImage.prototype = { 488 | initialize: function(graph, src, x, y, w, h) { 489 | this.graph = graph; 490 | ++this.graph.numImages; 491 | this.src = this.graph.imagePath + '/' + src; 492 | this.x = x; 493 | this.y = y; 494 | this.w = w; 495 | this.h = h; 496 | this.loaded = false; 497 | this.img = new Image(); 498 | this.img.onload = this.succeeded.bind(this); 499 | this.img.onerror = this.finished.bind(this); 500 | this.img.onabort = this.finished.bind(this); 501 | this.img.src = this.src; 502 | }, 503 | succeeded: function() { 504 | this.loaded = true; 505 | this.finished(); 506 | }, 507 | finished: function() { 508 | ++this.graph.numImagesFinished; 509 | if (this.graph.numImages == this.graph.numImagesFinished) { 510 | this.graph.draw(true); 511 | } 512 | }, 513 | draw: function() { 514 | if (this.loaded) { 515 | this.graph.ctx.drawImage(this.img, this.x, this.y, this.w, this.h); 516 | } 517 | } 518 | } 519 | 520 | function debug(str) { 521 | $('debug_output').innerHTML += '»' + String(str).escapeHTML() + '«
    '; 522 | } 523 | -------------------------------------------------------------------------------- /dozer/media/javascript/gvcolors.js: -------------------------------------------------------------------------------- 1 | // $Id: gvcolors.js 367 2007-03-13 08:57:11Z rschmidt $ 2 | 3 | gvcolors={ 4 | aliceblue:[240,248,255], 5 | antiquewhite:[250,235,215], 6 | antiquewhite1:[255,239,219], 7 | antiquewhite2:[238,223,204], 8 | antiquewhite3:[205,192,176], 9 | antiquewhite4:[139,131,120], 10 | aquamarine:[127,255,212], 11 | aquamarine1:[127,255,212], 12 | aquamarine2:[118,238,198], 13 | aquamarine3:[102,205,170], 14 | aquamarine4:[69,139,116], 15 | azure:[240,255,255], 16 | azure1:[240,255,255], 17 | azure2:[224,238,238], 18 | azure3:[193,205,205], 19 | azure4:[131,139,139], 20 | beige:[245,245,220], 21 | bisque:[255,228,196], 22 | bisque1:[255,228,196], 23 | bisque2:[238,213,183], 24 | bisque3:[205,183,158], 25 | bisque4:[139,125,107], 26 | black:[0,0,0], 27 | blanchedalmond:[255,235,205], 28 | blue:[0,0,255], 29 | blue1:[0,0,255], 30 | blue2:[0,0,238], 31 | blue3:[0,0,205], 32 | blue4:[0,0,139], 33 | blueviolet:[138,43,226], 34 | brown:[165,42,42], 35 | brown1:[255,64,64], 36 | brown2:[238,59,59], 37 | brown3:[205,51,51], 38 | brown4:[139,35,35], 39 | burlywood:[222,184,135], 40 | burlywood1:[255,211,155], 41 | burlywood2:[238,197,145], 42 | burlywood3:[205,170,125], 43 | burlywood4:[139,115,85], 44 | cadetblue:[95,158,160], 45 | cadetblue1:[152,245,255], 46 | cadetblue2:[142,229,238], 47 | cadetblue3:[122,197,205], 48 | cadetblue4:[83,134,139], 49 | chartreuse:[127,255,0], 50 | chartreuse1:[127,255,0], 51 | chartreuse2:[118,238,0], 52 | chartreuse3:[102,205,0], 53 | chartreuse4:[69,139,0], 54 | chocolate:[210,105,30], 55 | chocolate1:[255,127,36], 56 | chocolate2:[238,118,33], 57 | chocolate3:[205,102,29], 58 | chocolate4:[139,69,19], 59 | coral:[255,127,80], 60 | coral1:[255,114,86], 61 | coral2:[238,106,80], 62 | coral3:[205,91,69], 63 | coral4:[139,62,47], 64 | cornflowerblue:[100,149,237], 65 | cornsilk:[255,248,220], 66 | cornsilk1:[255,248,220], 67 | cornsilk2:[238,232,205], 68 | cornsilk3:[205,200,177], 69 | cornsilk4:[139,136,120], 70 | crimson:[220,20,60], 71 | cyan:[0,255,255], 72 | cyan1:[0,255,255], 73 | cyan2:[0,238,238], 74 | cyan3:[0,205,205], 75 | cyan4:[0,139,139], 76 | darkgoldenrod:[184,134,11], 77 | darkgoldenrod1:[255,185,15], 78 | darkgoldenrod2:[238,173,14], 79 | darkgoldenrod3:[205,149,12], 80 | darkgoldenrod4:[139,101,8], 81 | darkgreen:[0,100,0], 82 | darkkhaki:[189,183,107], 83 | darkolivegreen:[85,107,47], 84 | darkolivegreen1:[202,255,112], 85 | darkolivegreen2:[188,238,104], 86 | darkolivegreen3:[162,205,90], 87 | darkolivegreen4:[110,139,61], 88 | darkorange:[255,140,0], 89 | darkorange1:[255,127,0], 90 | darkorange2:[238,118,0], 91 | darkorange3:[205,102,0], 92 | darkorange4:[139,69,0], 93 | darkorchid:[153,50,204], 94 | darkorchid1:[191,62,255], 95 | darkorchid2:[178,58,238], 96 | darkorchid3:[154,50,205], 97 | darkorchid4:[104,34,139], 98 | darksalmon:[233,150,122], 99 | darkseagreen:[143,188,143], 100 | darkseagreen1:[193,255,193], 101 | darkseagreen2:[180,238,180], 102 | darkseagreen3:[155,205,155], 103 | darkseagreen4:[105,139,105], 104 | darkslateblue:[72,61,139], 105 | darkslategray:[47,79,79], 106 | darkslategray1:[151,255,255], 107 | darkslategray2:[141,238,238], 108 | darkslategray3:[121,205,205], 109 | darkslategray4:[82,139,139], 110 | darkslategrey:[47,79,79], 111 | darkturquoise:[0,206,209], 112 | darkviolet:[148,0,211], 113 | deeppink:[255,20,147], 114 | deeppink1:[255,20,147], 115 | deeppink2:[238,18,137], 116 | deeppink3:[205,16,118], 117 | deeppink4:[139,10,80], 118 | deepskyblue:[0,191,255], 119 | deepskyblue1:[0,191,255], 120 | deepskyblue2:[0,178,238], 121 | deepskyblue3:[0,154,205], 122 | deepskyblue4:[0,104,139], 123 | dimgray:[105,105,105], 124 | dimgrey:[105,105,105], 125 | dodgerblue:[30,144,255], 126 | dodgerblue1:[30,144,255], 127 | dodgerblue2:[28,134,238], 128 | dodgerblue3:[24,116,205], 129 | dodgerblue4:[16,78,139], 130 | firebrick:[178,34,34], 131 | firebrick1:[255,48,48], 132 | firebrick2:[238,44,44], 133 | firebrick3:[205,38,38], 134 | firebrick4:[139,26,26], 135 | floralwhite:[255,250,240], 136 | forestgreen:[34,139,34], 137 | gainsboro:[220,220,220], 138 | ghostwhite:[248,248,255], 139 | gold:[255,215,0], 140 | gold1:[255,215,0], 141 | gold2:[238,201,0], 142 | gold3:[205,173,0], 143 | gold4:[139,117,0], 144 | goldenrod:[218,165,32], 145 | goldenrod1:[255,193,37], 146 | goldenrod2:[238,180,34], 147 | goldenrod3:[205,155,29], 148 | goldenrod4:[139,105,20], 149 | gray:[192,192,192], 150 | gray0:[0,0,0], 151 | gray1:[3,3,3], 152 | gray10:[26,26,26], 153 | gray100:[255,255,255], 154 | gray11:[28,28,28], 155 | gray12:[31,31,31], 156 | gray13:[33,33,33], 157 | gray14:[36,36,36], 158 | gray15:[38,38,38], 159 | gray16:[41,41,41], 160 | gray17:[43,43,43], 161 | gray18:[46,46,46], 162 | gray19:[48,48,48], 163 | gray2:[5,5,5], 164 | gray20:[51,51,51], 165 | gray21:[54,54,54], 166 | gray22:[56,56,56], 167 | gray23:[59,59,59], 168 | gray24:[61,61,61], 169 | gray25:[64,64,64], 170 | gray26:[66,66,66], 171 | gray27:[69,69,69], 172 | gray28:[71,71,71], 173 | gray29:[74,74,74], 174 | gray3:[8,8,8], 175 | gray30:[77,77,77], 176 | gray31:[79,79,79], 177 | gray32:[82,82,82], 178 | gray33:[84,84,84], 179 | gray34:[87,87,87], 180 | gray35:[89,89,89], 181 | gray36:[92,92,92], 182 | gray37:[94,94,94], 183 | gray38:[97,97,97], 184 | gray39:[99,99,99], 185 | gray4:[10,10,10], 186 | gray40:[102,102,102], 187 | gray41:[105,105,105], 188 | gray42:[107,107,107], 189 | gray43:[110,110,110], 190 | gray44:[112,112,112], 191 | gray45:[115,115,115], 192 | gray46:[117,117,117], 193 | gray47:[120,120,120], 194 | gray48:[122,122,122], 195 | gray49:[125,125,125], 196 | gray5:[13,13,13], 197 | gray50:[127,127,127], 198 | gray51:[130,130,130], 199 | gray52:[133,133,133], 200 | gray53:[135,135,135], 201 | gray54:[138,138,138], 202 | gray55:[140,140,140], 203 | gray56:[143,143,143], 204 | gray57:[145,145,145], 205 | gray58:[148,148,148], 206 | gray59:[150,150,150], 207 | gray6:[15,15,15], 208 | gray60:[153,153,153], 209 | gray61:[156,156,156], 210 | gray62:[158,158,158], 211 | gray63:[161,161,161], 212 | gray64:[163,163,163], 213 | gray65:[166,166,166], 214 | gray66:[168,168,168], 215 | gray67:[171,171,171], 216 | gray68:[173,173,173], 217 | gray69:[176,176,176], 218 | gray7:[18,18,18], 219 | gray70:[179,179,179], 220 | gray71:[181,181,181], 221 | gray72:[184,184,184], 222 | gray73:[186,186,186], 223 | gray74:[189,189,189], 224 | gray75:[191,191,191], 225 | gray76:[194,194,194], 226 | gray77:[196,196,196], 227 | gray78:[199,199,199], 228 | gray79:[201,201,201], 229 | gray8:[20,20,20], 230 | gray80:[204,204,204], 231 | gray81:[207,207,207], 232 | gray82:[209,209,209], 233 | gray83:[212,212,212], 234 | gray84:[214,214,214], 235 | gray85:[217,217,217], 236 | gray86:[219,219,219], 237 | gray87:[222,222,222], 238 | gray88:[224,224,224], 239 | gray89:[227,227,227], 240 | gray9:[23,23,23], 241 | gray90:[229,229,229], 242 | gray91:[232,232,232], 243 | gray92:[235,235,235], 244 | gray93:[237,237,237], 245 | gray94:[240,240,240], 246 | gray95:[242,242,242], 247 | gray96:[245,245,245], 248 | gray97:[247,247,247], 249 | gray98:[250,250,250], 250 | gray99:[252,252,252], 251 | green:[0,255,0], 252 | green1:[0,255,0], 253 | green2:[0,238,0], 254 | green3:[0,205,0], 255 | green4:[0,139,0], 256 | greenyellow:[173,255,47], 257 | grey:[192,192,192], 258 | grey0:[0,0,0], 259 | grey1:[3,3,3], 260 | grey10:[26,26,26], 261 | grey100:[255,255,255], 262 | grey11:[28,28,28], 263 | grey12:[31,31,31], 264 | grey13:[33,33,33], 265 | grey14:[36,36,36], 266 | grey15:[38,38,38], 267 | grey16:[41,41,41], 268 | grey17:[43,43,43], 269 | grey18:[46,46,46], 270 | grey19:[48,48,48], 271 | grey2:[5,5,5], 272 | grey20:[51,51,51], 273 | grey21:[54,54,54], 274 | grey22:[56,56,56], 275 | grey23:[59,59,59], 276 | grey24:[61,61,61], 277 | grey25:[64,64,64], 278 | grey26:[66,66,66], 279 | grey27:[69,69,69], 280 | grey28:[71,71,71], 281 | grey29:[74,74,74], 282 | grey3:[8,8,8], 283 | grey30:[77,77,77], 284 | grey31:[79,79,79], 285 | grey32:[82,82,82], 286 | grey33:[84,84,84], 287 | grey34:[87,87,87], 288 | grey35:[89,89,89], 289 | grey36:[92,92,92], 290 | grey37:[94,94,94], 291 | grey38:[97,97,97], 292 | grey39:[99,99,99], 293 | grey4:[10,10,10], 294 | grey40:[102,102,102], 295 | grey41:[105,105,105], 296 | grey42:[107,107,107], 297 | grey43:[110,110,110], 298 | grey44:[112,112,112], 299 | grey45:[115,115,115], 300 | grey46:[117,117,117], 301 | grey47:[120,120,120], 302 | grey48:[122,122,122], 303 | grey49:[125,125,125], 304 | grey5:[13,13,13], 305 | grey50:[127,127,127], 306 | grey51:[130,130,130], 307 | grey52:[133,133,133], 308 | grey53:[135,135,135], 309 | grey54:[138,138,138], 310 | grey55:[140,140,140], 311 | grey56:[143,143,143], 312 | grey57:[145,145,145], 313 | grey58:[148,148,148], 314 | grey59:[150,150,150], 315 | grey6:[15,15,15], 316 | grey60:[153,153,153], 317 | grey61:[156,156,156], 318 | grey62:[158,158,158], 319 | grey63:[161,161,161], 320 | grey64:[163,163,163], 321 | grey65:[166,166,166], 322 | grey66:[168,168,168], 323 | grey67:[171,171,171], 324 | grey68:[173,173,173], 325 | grey69:[176,176,176], 326 | grey7:[18,18,18], 327 | grey70:[179,179,179], 328 | grey71:[181,181,181], 329 | grey72:[184,184,184], 330 | grey73:[186,186,186], 331 | grey74:[189,189,189], 332 | grey75:[191,191,191], 333 | grey76:[194,194,194], 334 | grey77:[196,196,196], 335 | grey78:[199,199,199], 336 | grey79:[201,201,201], 337 | grey8:[20,20,20], 338 | grey80:[204,204,204], 339 | grey81:[207,207,207], 340 | grey82:[209,209,209], 341 | grey83:[212,212,212], 342 | grey84:[214,214,214], 343 | grey85:[217,217,217], 344 | grey86:[219,219,219], 345 | grey87:[222,222,222], 346 | grey88:[224,224,224], 347 | grey89:[227,227,227], 348 | grey9:[23,23,23], 349 | grey90:[229,229,229], 350 | grey91:[232,232,232], 351 | grey92:[235,235,235], 352 | grey93:[237,237,237], 353 | grey94:[240,240,240], 354 | grey95:[242,242,242], 355 | grey96:[245,245,245], 356 | grey97:[247,247,247], 357 | grey98:[250,250,250], 358 | grey99:[252,252,252], 359 | honeydew:[240,255,240], 360 | honeydew1:[240,255,240], 361 | honeydew2:[224,238,224], 362 | honeydew3:[193,205,193], 363 | honeydew4:[131,139,131], 364 | hotpink:[255,105,180], 365 | hotpink1:[255,110,180], 366 | hotpink2:[238,106,167], 367 | hotpink3:[205,96,144], 368 | hotpink4:[139,58,98], 369 | indianred:[205,92,92], 370 | indianred1:[255,106,106], 371 | indianred2:[238,99,99], 372 | indianred3:[205,85,85], 373 | indianred4:[139,58,58], 374 | indigo:[75,0,130], 375 | ivory:[255,255,240], 376 | ivory1:[255,255,240], 377 | ivory2:[238,238,224], 378 | ivory3:[205,205,193], 379 | ivory4:[139,139,131], 380 | khaki:[240,230,140], 381 | khaki1:[255,246,143], 382 | khaki2:[238,230,133], 383 | khaki3:[205,198,115], 384 | khaki4:[139,134,78], 385 | lavender:[230,230,250], 386 | lavenderblush:[255,240,245], 387 | lavenderblush1:[255,240,245], 388 | lavenderblush2:[238,224,229], 389 | lavenderblush3:[205,193,197], 390 | lavenderblush4:[139,131,134], 391 | lawngreen:[124,252,0], 392 | lemonchiffon:[255,250,205], 393 | lemonchiffon1:[255,250,205], 394 | lemonchiffon2:[238,233,191], 395 | lemonchiffon3:[205,201,165], 396 | lemonchiffon4:[139,137,112], 397 | lightblue:[173,216,230], 398 | lightblue1:[191,239,255], 399 | lightblue2:[178,223,238], 400 | lightblue3:[154,192,205], 401 | lightblue4:[104,131,139], 402 | lightcoral:[240,128,128], 403 | lightcyan:[224,255,255], 404 | lightcyan1:[224,255,255], 405 | lightcyan2:[209,238,238], 406 | lightcyan3:[180,205,205], 407 | lightcyan4:[122,139,139], 408 | lightgoldenrod:[238,221,130], 409 | lightgoldenrod1:[255,236,139], 410 | lightgoldenrod2:[238,220,130], 411 | lightgoldenrod3:[205,190,112], 412 | lightgoldenrod4:[139,129,76], 413 | lightgoldenrodyellow:[250,250,210], 414 | lightgray:[211,211,211], 415 | lightgrey:[211,211,211], 416 | lightpink:[255,182,193], 417 | lightpink1:[255,174,185], 418 | lightpink2:[238,162,173], 419 | lightpink3:[205,140,149], 420 | lightpink4:[139,95,101], 421 | lightsalmon:[255,160,122], 422 | lightsalmon1:[255,160,122], 423 | lightsalmon2:[238,149,114], 424 | lightsalmon3:[205,129,98], 425 | lightsalmon4:[139,87,66], 426 | lightseagreen:[32,178,170], 427 | lightskyblue:[135,206,250], 428 | lightskyblue1:[176,226,255], 429 | lightskyblue2:[164,211,238], 430 | lightskyblue3:[141,182,205], 431 | lightskyblue4:[96,123,139], 432 | lightslateblue:[132,112,255], 433 | lightslategray:[119,136,153], 434 | lightslategrey:[119,136,153], 435 | lightsteelblue:[176,196,222], 436 | lightsteelblue1:[202,225,255], 437 | lightsteelblue2:[188,210,238], 438 | lightsteelblue3:[162,181,205], 439 | lightsteelblue4:[110,123,139], 440 | lightyellow:[255,255,224], 441 | lightyellow1:[255,255,224], 442 | lightyellow2:[238,238,209], 443 | lightyellow3:[205,205,180], 444 | lightyellow4:[139,139,122], 445 | limegreen:[50,205,50], 446 | linen:[250,240,230], 447 | magenta:[255,0,255], 448 | magenta1:[255,0,255], 449 | magenta2:[238,0,238], 450 | magenta3:[205,0,205], 451 | magenta4:[139,0,139], 452 | maroon:[176,48,96], 453 | maroon1:[255,52,179], 454 | maroon2:[238,48,167], 455 | maroon3:[205,41,144], 456 | maroon4:[139,28,98], 457 | mediumaquamarine:[102,205,170], 458 | mediumblue:[0,0,205], 459 | mediumorchid:[186,85,211], 460 | mediumorchid1:[224,102,255], 461 | mediumorchid2:[209,95,238], 462 | mediumorchid3:[180,82,205], 463 | mediumorchid4:[122,55,139], 464 | mediumpurple:[147,112,219], 465 | mediumpurple1:[171,130,255], 466 | mediumpurple2:[159,121,238], 467 | mediumpurple3:[137,104,205], 468 | mediumpurple4:[93,71,139], 469 | mediumseagreen:[60,179,113], 470 | mediumslateblue:[123,104,238], 471 | mediumspringgreen:[0,250,154], 472 | mediumturquoise:[72,209,204], 473 | mediumvioletred:[199,21,133], 474 | midnightblue:[25,25,112], 475 | mintcream:[245,255,250], 476 | mistyrose:[255,228,225], 477 | mistyrose1:[255,228,225], 478 | mistyrose2:[238,213,210], 479 | mistyrose3:[205,183,181], 480 | mistyrose4:[139,125,123], 481 | moccasin:[255,228,181], 482 | navajowhite:[255,222,173], 483 | navajowhite1:[255,222,173], 484 | navajowhite2:[238,207,161], 485 | navajowhite3:[205,179,139], 486 | navajowhite4:[139,121,94], 487 | navy:[0,0,128], 488 | navyblue:[0,0,128], 489 | oldlace:[253,245,230], 490 | olivedrab:[107,142,35], 491 | olivedrab1:[192,255,62], 492 | olivedrab2:[179,238,58], 493 | olivedrab3:[154,205,50], 494 | olivedrab4:[105,139,34], 495 | orange:[255,165,0], 496 | orange1:[255,165,0], 497 | orange2:[238,154,0], 498 | orange3:[205,133,0], 499 | orange4:[139,90,0], 500 | orangered:[255,69,0], 501 | orangered1:[255,69,0], 502 | orangered2:[238,64,0], 503 | orangered3:[205,55,0], 504 | orangered4:[139,37,0], 505 | orchid:[218,112,214], 506 | orchid1:[255,131,250], 507 | orchid2:[238,122,233], 508 | orchid3:[205,105,201], 509 | orchid4:[139,71,137], 510 | palegoldenrod:[238,232,170], 511 | palegreen:[152,251,152], 512 | palegreen1:[154,255,154], 513 | palegreen2:[144,238,144], 514 | palegreen3:[124,205,124], 515 | palegreen4:[84,139,84], 516 | paleturquoise:[175,238,238], 517 | paleturquoise1:[187,255,255], 518 | paleturquoise2:[174,238,238], 519 | paleturquoise3:[150,205,205], 520 | paleturquoise4:[102,139,139], 521 | palevioletred:[219,112,147], 522 | palevioletred1:[255,130,171], 523 | palevioletred2:[238,121,159], 524 | palevioletred3:[205,104,137], 525 | palevioletred4:[139,71,93], 526 | papayawhip:[255,239,213], 527 | peachpuff:[255,218,185], 528 | peachpuff1:[255,218,185], 529 | peachpuff2:[238,203,173], 530 | peachpuff3:[205,175,149], 531 | peachpuff4:[139,119,101], 532 | peru:[205,133,63], 533 | pink:[255,192,203], 534 | pink1:[255,181,197], 535 | pink2:[238,169,184], 536 | pink3:[205,145,158], 537 | pink4:[139,99,108], 538 | plum:[221,160,221], 539 | plum1:[255,187,255], 540 | plum2:[238,174,238], 541 | plum3:[205,150,205], 542 | plum4:[139,102,139], 543 | powderblue:[176,224,230], 544 | purple:[160,32,240], 545 | purple1:[155,48,255], 546 | purple2:[145,44,238], 547 | purple3:[125,38,205], 548 | purple4:[85,26,139], 549 | red:[255,0,0], 550 | red1:[255,0,0], 551 | red2:[238,0,0], 552 | red3:[205,0,0], 553 | red4:[139,0,0], 554 | rosybrown:[188,143,143], 555 | rosybrown1:[255,193,193], 556 | rosybrown2:[238,180,180], 557 | rosybrown3:[205,155,155], 558 | rosybrown4:[139,105,105], 559 | royalblue:[65,105,225], 560 | royalblue1:[72,118,255], 561 | royalblue2:[67,110,238], 562 | royalblue3:[58,95,205], 563 | royalblue4:[39,64,139], 564 | saddlebrown:[139,69,19], 565 | salmon:[250,128,114], 566 | salmon1:[255,140,105], 567 | salmon2:[238,130,98], 568 | salmon3:[205,112,84], 569 | salmon4:[139,76,57], 570 | sandybrown:[244,164,96], 571 | seagreen:[46,139,87], 572 | seagreen1:[84,255,159], 573 | seagreen2:[78,238,148], 574 | seagreen3:[67,205,128], 575 | seagreen4:[46,139,87], 576 | seashell:[255,245,238], 577 | seashell1:[255,245,238], 578 | seashell2:[238,229,222], 579 | seashell3:[205,197,191], 580 | seashell4:[139,134,130], 581 | sienna:[160,82,45], 582 | sienna1:[255,130,71], 583 | sienna2:[238,121,66], 584 | sienna3:[205,104,57], 585 | sienna4:[139,71,38], 586 | skyblue:[135,206,235], 587 | skyblue1:[135,206,255], 588 | skyblue2:[126,192,238], 589 | skyblue3:[108,166,205], 590 | skyblue4:[74,112,139], 591 | slateblue:[106,90,205], 592 | slateblue1:[131,111,255], 593 | slateblue2:[122,103,238], 594 | slateblue3:[105,89,205], 595 | slateblue4:[71,60,139], 596 | slategray:[112,128,144], 597 | slategray1:[198,226,255], 598 | slategray2:[185,211,238], 599 | slategray3:[159,182,205], 600 | slategray4:[108,123,139], 601 | slategrey:[112,128,144], 602 | snow:[255,250,250], 603 | snow1:[255,250,250], 604 | snow2:[238,233,233], 605 | snow3:[205,201,201], 606 | snow4:[139,137,137], 607 | springgreen:[0,255,127], 608 | springgreen1:[0,255,127], 609 | springgreen2:[0,238,118], 610 | springgreen3:[0,205,102], 611 | springgreen4:[0,139,69], 612 | steelblue:[70,130,180], 613 | steelblue1:[99,184,255], 614 | steelblue2:[92,172,238], 615 | steelblue3:[79,148,205], 616 | steelblue4:[54,100,139], 617 | tan:[210,180,140], 618 | tan1:[255,165,79], 619 | tan2:[238,154,73], 620 | tan3:[205,133,63], 621 | tan4:[139,90,43], 622 | thistle:[216,191,216], 623 | thistle1:[255,225,255], 624 | thistle2:[238,210,238], 625 | thistle3:[205,181,205], 626 | thistle4:[139,123,139], 627 | tomato:[255,99,71], 628 | tomato1:[255,99,71], 629 | tomato2:[238,92,66], 630 | tomato3:[205,79,57], 631 | tomato4:[139,54,38], 632 | transparent:[255,255,254], 633 | turquoise:[64,224,208], 634 | turquoise1:[0,245,255], 635 | turquoise2:[0,229,238], 636 | turquoise3:[0,197,205], 637 | turquoise4:[0,134,139], 638 | violet:[238,130,238], 639 | violetred:[208,32,144], 640 | violetred1:[255,62,150], 641 | violetred2:[238,58,140], 642 | violetred3:[205,50,120], 643 | violetred4:[139,34,82], 644 | wheat:[245,222,179], 645 | wheat1:[255,231,186], 646 | wheat2:[238,216,174], 647 | wheat3:[205,186,150], 648 | wheat4:[139,126,102], 649 | white:[255,255,255], 650 | whitesmoke:[245,245,245], 651 | yellow:[255,255,0], 652 | yellow1:[255,255,0], 653 | yellow2:[238,238,0], 654 | yellow3:[205,205,0], 655 | yellow4:[139,139,0], 656 | yellowgreen:[154,205,50] 657 | }; 658 | -------------------------------------------------------------------------------- /dozer/media/javascript/excanvas.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // TODO: Patterns 16 | // TODO: Radial gradient 17 | // TODO: Clipping paths 18 | // TODO: Coordsize 19 | // TODO: Painting mode 20 | // TODO: Optimize 21 | // TODO: canvas width/height sets content size in moz, border size in ie 22 | // TODO: Painting outside the canvas should not be allowed 23 | 24 | // only add this code if we do not already have a canvas implementation 25 | if (!window.CanvasRenderingContext2D) { 26 | 27 | (function () { 28 | 29 | var G_vmlCanvasManager_ = { 30 | init: function (opt_doc) { 31 | var doc = opt_doc || document; 32 | if (/MSIE/.test(navigator.userAgent) && !window.opera) { 33 | var self = this; 34 | doc.attachEvent("onreadystatechange", function () { 35 | self.init_(doc); 36 | }); 37 | } 38 | }, 39 | 40 | init_: function (doc, e) { 41 | if (doc.readyState == "complete") { 42 | // create xmlns 43 | if (!doc.namespaces["g_vml_"]) { 44 | doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); 45 | } 46 | 47 | // setup default css 48 | var ss = doc.createStyleSheet(); 49 | ss.cssText = "canvas{display:inline-block;overflow:hidden;" + 50 | "text-align:left;}" + 51 | "canvas *{behavior:url(#default#VML)}"; 52 | 53 | // find all canvas elements 54 | var els = doc.getElementsByTagName("canvas"); 55 | for (var i = 0; i < els.length; i++) { 56 | if (!els[i].getContext) { 57 | this.initElement(els[i]); 58 | } 59 | } 60 | } 61 | }, 62 | 63 | fixElement_: function (el) { 64 | // in IE before version 5.5 we would need to add HTML: to the tag name 65 | // but we do not care about IE before version 6 66 | var outerHTML = el.outerHTML; 67 | var newEl = document.createElement(outerHTML); 68 | // if the tag is still open IE has created the children as siblings and 69 | // it has also created a tag with the name "/FOO" 70 | if (outerHTML.slice(-2) != "/>") { 71 | var tagName = "/" + el.tagName; 72 | var ns; 73 | // remove content 74 | while ((ns = el.nextSibling) && ns.tagName != tagName) { 75 | ns.removeNode(); 76 | } 77 | // remove the incorrect closing tag 78 | if (ns) { 79 | ns.removeNode(); 80 | } 81 | } 82 | el.parentNode.replaceChild(newEl, el); 83 | return newEl; 84 | }, 85 | 86 | /** 87 | * Public initializes a canvas element so that it can be used as canvas 88 | * element from now on. This is called automatically before the page is 89 | * loaded but if you are creating elements using createElement yuo need to 90 | * make sure this is called on the element. 91 | * @param el {HTMLElement} The canvas element to initialize. 92 | */ 93 | initElement: function (el) { 94 | el = this.fixElement_(el); 95 | el.getContext = function () { 96 | if (this.context_) { 97 | return this.context_; 98 | } 99 | return this.context_ = new CanvasRenderingContext2D_(this); 100 | }; 101 | 102 | var self = this; //bind 103 | el.attachEvent("onpropertychange", function (e) { 104 | // we need to watch changes to width and height 105 | switch (e.propertyName) { 106 | case "width": 107 | case "height": 108 | // coord size changed? 109 | break; 110 | } 111 | }); 112 | 113 | // if style.height is set 114 | 115 | var attrs = el.attributes; 116 | if (attrs.width && attrs.width.specified) { 117 | // TODO: use runtimeStyle and coordsize 118 | // el.getContext().setWidth_(attrs.width.nodeValue); 119 | el.style.width = attrs.width.nodeValue + "px"; 120 | } 121 | if (attrs.height && attrs.height.specified) { 122 | // TODO: use runtimeStyle and coordsize 123 | // el.getContext().setHeight_(attrs.height.nodeValue); 124 | el.style.height = attrs.height.nodeValue + "px"; 125 | } 126 | //el.getContext().setCoordsize_() 127 | } 128 | }; 129 | 130 | G_vmlCanvasManager_.init(); 131 | 132 | // precompute "00" to "FF" 133 | var dec2hex = []; 134 | for (var i = 0; i < 16; i++) { 135 | for (var j = 0; j < 16; j++) { 136 | dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); 137 | } 138 | } 139 | 140 | function createMatrixIdentity() { 141 | return [ 142 | [1, 0, 0], 143 | [0, 1, 0], 144 | [0, 0, 1] 145 | ]; 146 | } 147 | 148 | function matrixMultiply(m1, m2) { 149 | var result = createMatrixIdentity(); 150 | 151 | for (var x = 0; x < 3; x++) { 152 | for (var y = 0; y < 3; y++) { 153 | var sum = 0; 154 | 155 | for (var z = 0; z < 3; z++) { 156 | sum += m1[x][z] * m2[z][y]; 157 | } 158 | 159 | result[x][y] = sum; 160 | } 161 | } 162 | return result; 163 | } 164 | 165 | function copyState(o1, o2) { 166 | o2.fillStyle = o1.fillStyle; 167 | o2.lineCap = o1.lineCap; 168 | o2.lineJoin = o1.lineJoin; 169 | o2.lineWidth = o1.lineWidth; 170 | o2.miterLimit = o1.miterLimit; 171 | o2.shadowBlur = o1.shadowBlur; 172 | o2.shadowColor = o1.shadowColor; 173 | o2.shadowOffsetX = o1.shadowOffsetX; 174 | o2.shadowOffsetY = o1.shadowOffsetY; 175 | o2.strokeStyle = o1.strokeStyle; 176 | } 177 | 178 | function processStyle(styleString) { 179 | var str, alpha = 1; 180 | 181 | styleString = String(styleString); 182 | if (styleString.substring(0, 3) == "rgb") { 183 | var start = styleString.indexOf("(", 3); 184 | var end = styleString.indexOf(")", start + 1); 185 | var guts = styleString.substring(start + 1, end).split(","); 186 | 187 | str = "#"; 188 | for (var i = 0; i < 3; i++) { 189 | str += dec2hex[parseInt(guts[i])]; 190 | } 191 | 192 | if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) { 193 | alpha = guts[3]; 194 | } 195 | } else { 196 | str = styleString; 197 | } 198 | 199 | return [str, alpha]; 200 | } 201 | 202 | function processLineCap(lineCap) { 203 | switch (lineCap) { 204 | case "butt": 205 | return "flat"; 206 | case "round": 207 | return "round"; 208 | case "square": 209 | default: 210 | return "square"; 211 | } 212 | } 213 | 214 | /** 215 | * This class implements CanvasRenderingContext2D interface as described by 216 | * the WHATWG. 217 | * @param surfaceElement {HTMLElement} The element that the 2D context should 218 | * be associated with 219 | */ 220 | function CanvasRenderingContext2D_(surfaceElement) { 221 | this.m_ = createMatrixIdentity(); 222 | this.element_ = surfaceElement; 223 | 224 | this.mStack_ = []; 225 | this.aStack_ = []; 226 | this.currentPath_ = []; 227 | 228 | // Canvas context properties 229 | this.strokeStyle = "#000"; 230 | this.fillStyle = "#ccc"; 231 | 232 | this.lineWidth = 1; 233 | this.lineJoin = "miter"; 234 | this.lineCap = "butt"; 235 | this.miterLimit = 10; 236 | this.globalAlpha = 1; 237 | }; 238 | 239 | var contextPrototype = CanvasRenderingContext2D_.prototype; 240 | contextPrototype.clearRect = function() { 241 | this.element_.innerHTML = ""; 242 | this.currentPath_ = []; 243 | }; 244 | 245 | contextPrototype.beginPath = function() { 246 | // TODO: Branch current matrix so that save/restore has no effect 247 | // as per safari docs. 248 | 249 | this.currentPath_ = []; 250 | }; 251 | 252 | contextPrototype.moveTo = function(aX, aY) { 253 | this.currentPath_.push({type: "moveTo", x: aX, y: aY}); 254 | }; 255 | 256 | contextPrototype.lineTo = function(aX, aY) { 257 | this.currentPath_.push({type: "lineTo", x: aX, y: aY}); 258 | }; 259 | 260 | contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, 261 | aCP2x, aCP2y, 262 | aX, aY) { 263 | this.currentPath_.push({type: "bezierCurveTo", 264 | cp1x: aCP1x, 265 | cp1y: aCP1y, 266 | cp2x: aCP2x, 267 | cp2y: aCP2y, 268 | x: aX, 269 | y: aY}); 270 | }; 271 | 272 | contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { 273 | // VML's qb produces different output to Firefox's 274 | // FF's behaviour seems to have changed in 1.5.0.1, check this 275 | this.bezierCurveTo(aCPx, aCPy, aCPx, aCPy, aX, aY); 276 | }; 277 | 278 | contextPrototype.arc = function(aX, aY, aRadius, 279 | aStartAngle, aEndAngle, aClockwise) { 280 | if (!aClockwise) { 281 | var t = aStartAngle; 282 | aStartAngle = aEndAngle; 283 | aEndAngle = t; 284 | } 285 | 286 | var xStart = aX + (Math.cos(aStartAngle) * aRadius); 287 | var yStart = aY + (Math.sin(aStartAngle) * aRadius); 288 | 289 | var xEnd = aX + (Math.cos(aEndAngle) * aRadius); 290 | var yEnd = aY + (Math.sin(aEndAngle) * aRadius); 291 | 292 | this.currentPath_.push({type: "arc", 293 | x: aX, 294 | y: aY, 295 | radius: aRadius, 296 | xStart: xStart, 297 | yStart: yStart, 298 | xEnd: xEnd, 299 | yEnd: yEnd}); 300 | 301 | }; 302 | 303 | contextPrototype.rect = function(aX, aY, aWidth, aHeight) { 304 | this.moveTo(aX, aY); 305 | this.lineTo(aX + aWidth, aY); 306 | this.lineTo(aX + aWidth, aY + aHeight); 307 | this.lineTo(aX, aY + aHeight); 308 | this.closePath(); 309 | }; 310 | 311 | contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { 312 | // Will destroy any existing path (same as FF behaviour) 313 | this.beginPath(); 314 | this.moveTo(aX, aY); 315 | this.lineTo(aX + aWidth, aY); 316 | this.lineTo(aX + aWidth, aY + aHeight); 317 | this.lineTo(aX, aY + aHeight); 318 | this.closePath(); 319 | this.stroke(); 320 | }; 321 | 322 | contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { 323 | // Will destroy any existing path (same as FF behaviour) 324 | this.beginPath(); 325 | this.moveTo(aX, aY); 326 | this.lineTo(aX + aWidth, aY); 327 | this.lineTo(aX + aWidth, aY + aHeight); 328 | this.lineTo(aX, aY + aHeight); 329 | this.closePath(); 330 | this.fill(); 331 | }; 332 | 333 | contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { 334 | var gradient = new CanvasGradient_("gradient"); 335 | return gradient; 336 | }; 337 | 338 | contextPrototype.createRadialGradient = function(aX0, aY0, 339 | aR0, aX1, 340 | aY1, aR1) { 341 | var gradient = new CanvasGradient_("gradientradial"); 342 | gradient.radius1_ = aR0; 343 | gradient.radius2_ = aR1; 344 | gradient.focus_.x = aX0; 345 | gradient.focus_.y = aY0; 346 | return gradient; 347 | }; 348 | 349 | contextPrototype.drawImage = function (image, var_args) { 350 | var dx, dy, dw, dh, sx, sy, sw, sh; 351 | var w = image.width; 352 | var h = image.height; 353 | 354 | if (arguments.length == 3) { 355 | dx = arguments[1]; 356 | dy = arguments[2]; 357 | sx = sy = 0; 358 | sw = dw = w; 359 | sh = dh = h; 360 | } else if (arguments.length == 5) { 361 | dx = arguments[1]; 362 | dy = arguments[2]; 363 | dw = arguments[3]; 364 | dh = arguments[4]; 365 | sx = sy = 0; 366 | sw = w; 367 | sh = h; 368 | } else if (arguments.length == 9) { 369 | sx = arguments[1]; 370 | sy = arguments[2]; 371 | sw = arguments[3]; 372 | sh = arguments[4]; 373 | dx = arguments[5]; 374 | dy = arguments[6]; 375 | dw = arguments[7]; 376 | dh = arguments[8]; 377 | } else { 378 | throw "Invalid number of arguments"; 379 | } 380 | 381 | var d = this.getCoords_(dx, dy); 382 | 383 | var w2 = (sw / 2); 384 | var h2 = (sh / 2); 385 | 386 | var vmlStr = []; 387 | 388 | // For some reason that I've now forgotten, using divs didn't work 389 | vmlStr.push(' ' , 428 | '', 436 | ''); 437 | 438 | this.element_.insertAdjacentHTML("BeforeEnd", 439 | vmlStr.join("")); 440 | }; 441 | 442 | contextPrototype.stroke = function(aFill) { 443 | var lineStr = []; 444 | var lineOpen = false; 445 | var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); 446 | var color = a[0]; 447 | var opacity = a[1] * this.globalAlpha; 448 | 449 | lineStr.push(' max.x) { 515 | max.x = c.x; 516 | } 517 | if (min.y == null || c.y < min.y) { 518 | min.y = c.y; 519 | } 520 | if (max.y == null || c.y > max.y) { 521 | max.y = c.y; 522 | } 523 | } 524 | } 525 | lineStr.push(' ">'); 526 | 527 | if (typeof this.fillStyle == "object") { 528 | var focus = {x: "50%", y: "50%"}; 529 | var width = (max.x - min.x); 530 | var height = (max.y - min.y); 531 | var dimension = (width > height) ? width : height; 532 | 533 | focus.x = Math.floor((this.fillStyle.focus_.x / width) * 100 + 50) + "%"; 534 | focus.y = Math.floor((this.fillStyle.focus_.y / height) * 100 + 50) + "%"; 535 | 536 | var colors = []; 537 | 538 | // inside radius (%) 539 | if (this.fillStyle.type_ == "gradientradial") { 540 | var inside = (this.fillStyle.radius1_ / dimension * 100); 541 | 542 | // percentage that outside radius exceeds inside radius 543 | var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside; 544 | } else { 545 | var inside = 0; 546 | var expansion = 100; 547 | } 548 | 549 | var insidecolor = {offset: null, color: null}; 550 | var outsidecolor = {offset: null, color: null}; 551 | 552 | // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie 553 | // won't interpret it correctly 554 | this.fillStyle.colors_.sort(function (cs1, cs2) { 555 | return cs1.offset - cs2.offset; 556 | }); 557 | 558 | for (var i = 0; i < this.fillStyle.colors_.length; i++) { 559 | var fs = this.fillStyle.colors_[i]; 560 | 561 | colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ","); 562 | 563 | if (fs.offset > insidecolor.offset || insidecolor.offset == null) { 564 | insidecolor.offset = fs.offset; 565 | insidecolor.color = fs.color; 566 | } 567 | 568 | if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) { 569 | outsidecolor.offset = fs.offset; 570 | outsidecolor.color = fs.color; 571 | } 572 | } 573 | colors.pop(); 574 | 575 | lineStr.push(''); 582 | } else if (aFill) { 583 | lineStr.push(''); 584 | } else { 585 | lineStr.push( 586 | '' 593 | ); 594 | } 595 | 596 | lineStr.push(""); 597 | 598 | this.element_.insertAdjacentHTML("beforeEnd", lineStr.join("")); 599 | 600 | this.currentPath_ = []; 601 | }; 602 | 603 | contextPrototype.fill = function() { 604 | this.stroke(true); 605 | } 606 | 607 | contextPrototype.closePath = function() { 608 | this.currentPath_.push({type: "close"}); 609 | }; 610 | 611 | /** 612 | * @private 613 | */ 614 | contextPrototype.getCoords_ = function(aX, aY) { 615 | return { 616 | x: (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]), 617 | y: (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) 618 | } 619 | }; 620 | 621 | contextPrototype.save = function() { 622 | var o = {}; 623 | copyState(this, o); 624 | this.aStack_.push(o); 625 | this.mStack_.push(this.m_); 626 | this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); 627 | }; 628 | 629 | contextPrototype.restore = function() { 630 | copyState(this.aStack_.pop(), this); 631 | this.m_ = this.mStack_.pop(); 632 | }; 633 | 634 | contextPrototype.translate = function(aX, aY) { 635 | var m1 = [ 636 | [1, 0, 0], 637 | [0, 1, 0], 638 | [aX, aY, 1] 639 | ]; 640 | 641 | this.m_ = matrixMultiply(m1, this.m_); 642 | }; 643 | 644 | contextPrototype.rotate = function(aRot) { 645 | var c = Math.cos(aRot); 646 | var s = Math.sin(aRot); 647 | 648 | var m1 = [ 649 | [c, s, 0], 650 | [-s, c, 0], 651 | [0, 0, 1] 652 | ]; 653 | 654 | this.m_ = matrixMultiply(m1, this.m_); 655 | }; 656 | 657 | contextPrototype.scale = function(aX, aY) { 658 | var m1 = [ 659 | [aX, 0, 0], 660 | [0, aY, 0], 661 | [0, 0, 1] 662 | ]; 663 | 664 | this.m_ = matrixMultiply(m1, this.m_); 665 | }; 666 | 667 | /******** STUBS ********/ 668 | contextPrototype.clip = function() { 669 | // TODO: Implement 670 | }; 671 | 672 | contextPrototype.arcTo = function() { 673 | // TODO: Implement 674 | }; 675 | 676 | contextPrototype.createPattern = function() { 677 | return new CanvasPattern_; 678 | }; 679 | 680 | // Gradient / Pattern Stubs 681 | function CanvasGradient_(aType) { 682 | this.type_ = aType; 683 | this.radius1_ = 0; 684 | this.radius2_ = 0; 685 | this.colors_ = []; 686 | this.focus_ = {x: 0, y: 0}; 687 | } 688 | 689 | CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { 690 | aColor = processStyle(aColor); 691 | this.colors_.push({offset: 1-aOffset, color: aColor}); 692 | }; 693 | 694 | function CanvasPattern_() {} 695 | 696 | // set up externs 697 | G_vmlCanvasManager = G_vmlCanvasManager_; 698 | CanvasRenderingContext2D = CanvasRenderingContext2D_; 699 | CanvasGradient = CanvasGradient_; 700 | CanvasPattern = CanvasPattern_; 701 | 702 | })(); 703 | 704 | } // if --------------------------------------------------------------------------------