diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36baad66d..6e9ac0b64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,12 +15,12 @@ repos: hooks: - id: doc8 - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/adamchainz/django-upgrade - rev: 1.8.0 + rev: 1.10.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] @@ -38,12 +38,14 @@ repos: - id: rst-backticks - id: rst-directive-colons - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.7.1 + rev: v3.0.0-alpha.0 hooks: - id: prettier types_or: [javascript, css] + args: + - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.22.0 + rev: v8.23.1 hooks: - id: eslint files: \.js?$ @@ -51,7 +53,7 @@ repos: args: - --fix - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black language_version: python3 diff --git a/README.rst b/README.rst index c7ea51bd6..d050f5068 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,7 @@ Here's a screenshot of the toolbar in action: In addition to the built-in panels, a number of third-party panels are contributed by the community. -The current stable version of the Debug Toolbar is 3.6.0. It works on +The current stable version of the Debug Toolbar is 3.7.0. It works on Django ≥ 3.2.4. Documentation, including installation and configuration instructions, is diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py index 17f1f9e69..e9ed74ab1 100644 --- a/debug_toolbar/__init__.py +++ b/debug_toolbar/__init__.py @@ -4,7 +4,7 @@ # Do not use pkg_resources to find the version but set it here directly! # see issue #1446 -VERSION = "3.6.0" +VERSION = "3.7.0" # Code that discovers files or modules in INSTALLED_APPS imports this module. urls = "debug_toolbar.urls", APP_NAME diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 5fd5b3c84..ca32b98c2 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -3,6 +3,7 @@ from colorsys import hsv_to_rgb from pstats import Stats +from django.conf import settings from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ @@ -32,6 +33,22 @@ def background(self): r, g, b = hsv_to_rgb(*self.hsv) return f"rgb({r * 100:f}%,{g * 100:f}%,{b * 100:f}%)" + def is_project_func(self): + """ + Check if the function is from the project code. + + Project code is identified by the BASE_DIR setting + which is used in Django projects by default. + """ + if hasattr(settings, "BASE_DIR"): + file_name, _, _ = self.func + return ( + str(settings.BASE_DIR) in file_name + and "/site-packages/" not in file_name + and "/dist-packages/" not in file_name + ) + return None + def func_std_string(self): # match what old profile produced func_name = self.func if func_name[:2] == ("~", 0): @@ -123,19 +140,25 @@ class ProfilingPanel(Panel): title = _("Profiling") template = "debug_toolbar/panels/profiling.html" + capture_project_code = dt_settings.get_config()["PROFILER_CAPTURE_PROJECT_CODE"] def process_request(self, request): self.profiler = cProfile.Profile() return self.profiler.runcall(super().process_request, request) - def add_node(self, func_list, func, max_depth, cum_time=0.1): + def add_node(self, func_list, func, max_depth, cum_time): func_list.append(func) func.has_subfuncs = False if func.depth < max_depth: for subfunc in func.subfuncs(): - if subfunc.stats[3] >= cum_time: + # Always include the user's code + if subfunc.stats[3] >= cum_time or ( + self.capture_project_code + and subfunc.is_project_func() + and subfunc.stats[3] > 0 + ): func.has_subfuncs = True - self.add_node(func_list, subfunc, max_depth, cum_time=cum_time) + self.add_node(func_list, subfunc, max_depth, cum_time) def generate_stats(self, request, response): if not hasattr(self, "profiler"): @@ -150,10 +173,13 @@ def generate_stats(self, request, response): if root_func in self.stats.stats: root = FunctionCall(self.stats, root_func, depth=0) func_list = [] + cum_time_threshold = ( + root.stats[3] / dt_settings.get_config()["PROFILER_THRESHOLD_RATIO"] + ) self.add_node( func_list, root, dt_settings.get_config()["PROFILER_MAX_DEPTH"], - root.stats[3] / 8, + cum_time_threshold, ) self.record_stats({"func_list": func_list}) diff --git a/debug_toolbar/panels/request.py b/debug_toolbar/panels/request.py index 966301d97..bfb485ae7 100644 --- a/debug_toolbar/panels/request.py +++ b/debug_toolbar/panels/request.py @@ -59,13 +59,12 @@ def generate_stats(self, request, response): self.record_stats(view_info) if hasattr(request, "session"): - self.record_stats( - { - "session": { - "list": [ - (k, request.session.get(k)) - for k in sorted(request.session.keys()) - ] - } - } - ) + try: + session_list = [ + (k, request.session.get(k)) for k in sorted(request.session.keys()) + ] + except TypeError: + session_list = [ + (k, request.session.get(k)) for k in request.session.keys() + ] + self.record_stats({"session": {"list": session_list}}) diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index 5bf9bb09f..2bad251c1 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -33,7 +33,9 @@ "django.utils.functional", ), "PRETTIFY_SQL": True, + "PROFILER_CAPTURE_PROJECT_CODE": True, "PROFILER_MAX_DEPTH": 10, + "PROFILER_THRESHOLD_RATIO": 8, "SHOW_TEMPLATE_CONTEXT": True, "SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"), "SQL_WARNING_THRESHOLD": 500, # milliseconds diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index a105bfd11..f87d7a133 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -64,6 +64,7 @@ #djDebug tr, #djDebug th, #djDebug td, +#djDebug summary, #djDebug button { margin: 0; padding: 0; @@ -76,7 +77,9 @@ color: #000; vertical-align: baseline; background-color: transparent; - font-family: sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, + Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; text-align: left; text-shadow: none; white-space: normal; @@ -157,7 +160,7 @@ text-decoration: none; display: block; font-size: 16px; - padding: 10px 10px 5px 25px; + padding: 7px 10px 8px 25px; color: #fff; } #djDebug #djDebugToolbar li > div.djdt-disabled { @@ -176,6 +179,7 @@ #djDebug #djDebugToolbar li.djdt-active:before { content: "▶"; + font-family: sans-serif; position: absolute; left: 0; top: 50%; @@ -237,13 +241,30 @@ font-size: 16px; } +#djDebug pre, #djDebug code { display: block; - font-family: Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", - monospace; + font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + overflow: auto; +} + +#djDebug code { font-size: 12px; white-space: pre; - overflow: auto; +} + +#djDebug pre { + white-space: pre-wrap; + color: #555; + border: 1px solid #ccc; + border-collapse: collapse; + background-color: #fff; + padding: 2px 3px; + margin-bottom: 3px; } #djDebug .djdt-panelContent { @@ -369,18 +390,19 @@ position: absolute; top: 4px; right: 15px; - height: 16px; - width: 16px; line-height: 16px; - padding: 5px; border: 6px solid #ddd; border-radius: 50%; background: #fff; color: #ddd; - text-align: center; font-weight: 900; font-size: 20px; - box-sizing: content-box; + height: 36px; + width: 36px; + padding: 0 0 5px; + box-sizing: border-box; + display: grid; + place-items: center; } #djDebug .djdt-panelContent .djDebugClose:hover { @@ -562,19 +584,7 @@ #djDebug .djSQLDetailsDiv { margin-top: 0.8em; } -#djDebug pre { - white-space: pre-wrap; - color: #555; - border: 1px solid #ccc; - border-collapse: collapse; - background-color: #fff; - display: block; - overflow: auto; - padding: 2px 3px; - margin-bottom: 3px; - font-family: Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", - monospace; -} + #djDebug .djdt-stack span { color: #000; font-weight: bold; @@ -614,6 +624,12 @@ #djDebug .djdt-highlighted { background-color: lightgrey; } +#djDebug tr.djdt-highlighted.djdt-profile-row { + background-color: #ffc; +} +#djDebug tr.djdt-highlighted.djdt-profile-row:nth-child(2n + 1) { + background-color: #dd9; +} @keyframes djdt-flash-new { from { background-color: green; diff --git a/debug_toolbar/templates/debug_toolbar/panels/profiling.html b/debug_toolbar/templates/debug_toolbar/panels/profiling.html index 837698889..4c1c3acd3 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/profiling.html +++ b/debug_toolbar/templates/debug_toolbar/panels/profiling.html @@ -12,7 +12,7 @@ {% for call in func_list %} - +
{% if call.has_subfuncs %} diff --git a/docs/changes.rst b/docs/changes.rst index df6be99f2..720a8c050 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,24 @@ Change log Pending ------- +3.7.0 (2022-09-25) +------------------ + +* Added Profiling panel setting ``PROFILER_THRESHOLD_RATIO`` to give users + better control over how many function calls are included. A higher value + will include more data, but increase render time. +* Update Profiling panel to include try to always include user code. This + code is more important to developers than dependency code. +* Highlight the project function calls in the profiling panel. +* Added Profiling panel setting ``PROFILER_CAPTURE_PROJECT_CODE`` to allow + users to disable the inclusion of all project code. This will be useful + to project setups that have dependencies installed under + ``settings.BASE_DIR``. +* The toolbar's font stack now prefers system UI fonts. Tweaked paddings, + margins and alignments a bit in the CSS code. +* Only sort the session dictionary when the keys are all strings. Fixes a + bug that causes the toolbar to crash when non-strings are used as keys. + 3.6.0 (2022-08-17) ------------------ diff --git a/docs/conf.py b/docs/conf.py index 476a055de..97bab48c8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ copyright = copyright.format(datetime.date.today().year) # The full version, including alpha/beta/rc tags -release = "3.6.0" +release = "3.7.0" # -- General configuration --------------------------------------------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index 6f4084ad5..07e0a845c 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -250,6 +250,18 @@ Panel options WHERE "auth_user"."username" = '''test_username''' LIMIT 21 +* ``PROFILER_CAPTURE_PROJECT_CODE`` + + Default: ``True`` + + Panel: profiling + + When enabled this setting will include all project function calls in the + panel. Project code is defined as files in the path defined at + ``settings.BASE_DIR``. If you install dependencies under + ``settings.BASE_DIR`` in a directory other than ``sites-packages`` or + ``dist-packages`` you may need to disable this setting. + * ``PROFILER_MAX_DEPTH`` Default: ``10`` @@ -259,6 +271,20 @@ Panel options This setting affects the depth of function calls in the profiler's analysis. +* ``PROFILER_THRESHOLD_RATIO`` + + Default: ``8`` + + Panel: profiling + + This setting affects the which calls are included in the profile. A higher + value will include more function calls. A lower value will result in a faster + render of the profiling panel, but will exclude data. + + This value is used to determine the threshold of cumulative time to include + the nested functions. The threshold is calculated by the root calls' + cumulative time divided by this ratio. + * ``SHOW_TEMPLATE_CONTEXT`` Default: ``True`` diff --git a/docs/panels.rst b/docs/panels.rst index 8e5558aab..09891f2e5 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -130,6 +130,12 @@ Profiling information for the processing of the request. This panel is included but inactive by default. You can activate it by default with the ``DISABLE_PANELS`` configuration option. +The panel will include all function calls made by your project if you're using +the setting ``settings.BASE_DIR`` to point to your project's root directory. +If a function is in a file within that directory and does not include +``"/site-packages/"`` or ``"/dist-packages/"`` in the path, it will be +included. + Third-party panels ------------------ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index ee911963f..5e3a7f64b 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -16,12 +16,14 @@ jQuery jrestclient js Makefile +margins memcache memcached middleware middlewares multi neo +paddings pre profiler psycopg diff --git a/docs/tips.rst b/docs/tips.rst index e6957b0c6..d5d160fb3 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -77,6 +77,8 @@ by disabling some configuration options that are enabled by default: - ``ENABLE_STACKTRACES`` for the SQL and cache panels, - ``SHOW_TEMPLATE_CONTEXT`` for the template panel. +- ``PROFILER_CAPTURE_PROJECT_CODE`` and ``PROFILER_THRESHOLD_RATIO`` for the + profiling panel. Also, check ``SKIP_TEMPLATE_PREFIXES`` when you're using template-based form widgets. diff --git a/setup.cfg b/setup.cfg index 5daaee0bd..1c3c105d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = django-debug-toolbar -version = 3.6.0 +version = 3.7.0 description = A configurable set of panels that display various debug information about the current request/response. long_description = file: README.rst long_description_content_type = text/x-rst diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py index ca5c2463b..2169932b2 100644 --- a/tests/panels/test_profiling.py +++ b/tests/panels/test_profiling.py @@ -33,6 +33,21 @@ def test_insert_content(self): # ensure the panel renders correctly. content = self.panel.content self.assertIn("regular_view", content) + self.assertIn("render", content) + self.assertValidHTML(content) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"PROFILER_THRESHOLD_RATIO": 1}) + def test_cum_time_threshold(self): + """ + Test that cumulative time threshold excludes calls + """ + self._get_response = lambda request: regular_view(request, "profiling") + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + # ensure the panel renders but doesn't include our function. + content = self.panel.content + self.assertIn("regular_view", content) + self.assertNotIn("render", content) self.assertValidHTML(content) def test_listcomp_escaped(self): diff --git a/tests/panels/test_request.py b/tests/panels/test_request.py index 8087203c3..ea7f1681a 100644 --- a/tests/panels/test_request.py +++ b/tests/panels/test_request.py @@ -104,3 +104,32 @@ def test_namespaced_url(self): self.panel.generate_stats(self.request, response) panel_stats = self.panel.get_stats() self.assertEqual(panel_stats["view_urlname"], "admin:login") + + def test_session_list_sorted_or_not(self): + """ + Verify the session is sorted when all keys are strings. + + See https://github.com/jazzband/django-debug-toolbar/issues/1668 + """ + self.request.session = { + 1: "value", + "data": ["foo", "bar", 1], + (2, 3): "tuple_key", + } + data = { + "list": [(1, "value"), ("data", ["foo", "bar", 1]), ((2, 3), "tuple_key")] + } + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + panel_stats = self.panel.get_stats() + self.assertEqual(panel_stats["session"], data) + + self.request.session = { + "b": "b-value", + "a": "a-value", + } + data = {"list": [("a", "a-value"), ("b", "b-value")]} + response = self.panel.process_request(self.request) + self.panel.generate_stats(self.request, response) + panel_stats = self.panel.get_stats() + self.assertEqual(panel_stats["session"], data) diff --git a/tests/test_integration.py b/tests/test_integration.py index e9962b32b..982a2824c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -174,6 +174,12 @@ def test_is_toolbar_request_override_request_urlconf(self): self.request.path = "/__debug__/render_panel/" self.assertTrue(self.toolbar.is_toolbar_request(self.request)) + def test_data_gone(self): + response = self.client.get( + "/__debug__/render_panel/?store_id=GONE&panel_id=RequestPanel" + ) + self.assertIn("Please reload the page and retry.", response.json()["content"]) + @override_settings(DEBUG=True) class DebugToolbarIntegrationTestCase(IntegrationTestCase): diff --git a/tests/urls.py b/tests/urls.py index c12fc744a..6fc8811b7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -9,7 +9,7 @@ re_path( r"^resolving1/(.+)/(.+)/$", views.resolving_view, name="positional-resolving" ), - re_path(r"^resolving2/(?P.+)/(?P.+)/$", views.resolving_view), + path("resolving2///", views.resolving_view), re_path(r"^resolving3/(.+)/$", views.resolving_view, {"arg2": "default"}), re_path(r"^regular/(?P.*)/$", views.regular_view), re_path(r"^template_response/(?P<title>.*)/$", views.template_response_view),