diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py index 24c942de7..20b2faa9b 100644 --- a/debug_toolbar/panels/cache.py +++ b/debug_toolbar/panels/cache.py @@ -116,7 +116,13 @@ def _store_call_info( template_info, backend, ): - if name == "get" or name == "get_or_set": + if name == "get": + default = kwargs.get("default", args[1] if len(args) > 1 else None) + if return_value == default: + self.misses += 1 + else: + self.hits += 1 + elif name == "get_or_set": if return_value is None: self.misses += 1 else: @@ -143,7 +149,7 @@ def _store_call_info( def _record_call(self, cache, alias, name, original_method, args, kwargs): # Some cache backends implement certain cache methods in terms of other cache - # methods (e.g. get_or_set() in terms of get() and add()). In order to only + # methods (e.g. get_or_set() in terms of get() and add()). In order to only # record the calls made directly by the user code, set the cache's _djdt_panel # attribute to None before invoking the original method, which will cause the # monkey-patched cache methods to skip recording additional calls made during diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css index ad0d58df9..a7000a95a 100644 --- a/debug_toolbar/static/debug_toolbar/css/toolbar.css +++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css @@ -348,7 +348,14 @@ height: 50px; } -#djDebug .djDebugPanelTitle code { +#djDebug .djdt-cache-note { + margin: 1em 0; + padding: 8px 10px; + border: 1px solid var(--djdt-table-border-color); + background-color: var(--djdt-panel-content-table-strip-background-color); +} +#djDebug .djDebugPanelTitle code, +#djDebug .djdt-cache-note code { display: inline; font-size: inherit; } diff --git a/debug_toolbar/templates/debug_toolbar/panels/cache.html b/debug_toolbar/templates/debug_toolbar/panels/cache.html index 04c7d37a1..f2ebe8c3d 100644 --- a/debug_toolbar/templates/debug_toolbar/panels/cache.html +++ b/debug_toolbar/templates/debug_toolbar/panels/cache.html @@ -18,6 +18,11 @@

{% translate "Summary" %}

+

+ Cache hit/miss statistics for cache.get() calls may not always be accurate. + See the discussion + for details and to share feedback about improving cache hit/miss tracking. +

{% translate "Commands" %}

diff --git a/docs/changes.rst b/docs/changes.rst index 5b3156bbc..f2c9c89b8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,9 @@ Pending * Stopped the history panel buttons from submitting their form when clicked before the panel script has loaded, which navigated away from the page. * Added support for Django 6.1. +* Improved cache hit/miss reporting in the Cache panel for ``cache.get()`` + calls with a supplied default value, while documenting the remaining + ambiguity when a cached value equals the supplied default. 7.0.0 (2026-06-17) ------------------ diff --git a/docs/panels.rst b/docs/panels.rst index c9ce5ab63..e6d0e5f26 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -101,6 +101,12 @@ Cache Cache queries. Is incompatible with Django's per-site caching. +Cache hit/miss statistics for ``cache.get()`` calls may not always be +accurate. See the `discussion`_ for details and to share feedback about +improving cache hit/miss tracking. + +.. _discussion: https://github.com/django-commons/django-debug-toolbar/discussions/2441 + Signals ~~~~~~~ diff --git a/tests/panels/test_cache.py b/tests/panels/test_cache.py index fff28280b..66f86d384 100644 --- a/tests/panels/test_cache.py +++ b/tests/panels/test_cache.py @@ -45,6 +45,58 @@ def test_hits_and_misses(self): self.assertEqual(self.panel.hits, 4) self.assertEqual(self.panel.misses, 2) + def test_cached_none_with_default_none_is_ambiguous(self): + cache.cache.clear() + + cache.cache.set("foo", None) + self.assertIsNone(cache.cache.get("foo")) + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + + def test_cached_value_equal_to_default_is_ambiguous(self): + cache.cache.clear() + + cache.cache.set("foo", "bar") + self.assertEqual(cache.cache.get("foo", "bar"), "bar") + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + + def test_missing_key_with_default_counts_as_miss(self): + cache.cache.clear() + + self.assertIsNone(cache.cache.get("foo", None)) + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + + def test_missing_key_returns_supplied_default(self): + cache.cache.clear() + + self.assertEqual(cache.cache.get("foo", "bar"), "bar") + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + + def test_get_with_keyword_default_is_cache_miss(self): + cache.cache.clear() + + self.assertEqual(cache.cache.get("foo", default="default"), "default") + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + + def test_get_with_keyword_key_and_default_is_cache_miss(self): + cache.cache.clear() + + self.assertEqual(cache.cache.get(key="foo", default="default"), "default") + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + + def test_get_with_keyword_key_and_version(self): + cache.cache.clear() + + cache.cache.set("foo", "bar", version=1) + self.assertEqual(cache.cache.get(key="foo", version=1), "bar") + self.assertEqual(self.panel.hits, 1) + self.assertEqual(self.panel.misses, 0) + def test_get_or_set_value(self): cache.cache.get_or_set("baz", "val") self.assertEqual(cache.cache.get("baz"), "val") @@ -114,6 +166,13 @@ def test_get_or_set_does_not_override_existing_value(self): }, ) + def test_get_or_set_none_counts_as_miss(self): + cache.cache.clear() + + self.assertIsNone(cache.cache.get_or_set("foo", None)) + self.assertEqual(self.panel.hits, 0) + self.assertEqual(self.panel.misses, 1) + def test_insert_content(self): """ Test that the panel only inserts content after generate_stats and