diff --git a/.env.example b/.env.example index 4962895..dc2fe20 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,13 @@ DASH_DEBUG=true MOCK_PLATFORM_HEADER=82 MOCK_PLATFORM_SPACER=20 + + +# --------------------------------------------------------------------------- +# Development aid: print memory usage to the terminal +# --------------------------------------------------------------------------- +# Prints the app's RSS memory usage every time you click a page link. +# +# Off by default. Needs psutil, which is only in requirements-dev.txt, so +# never turn this on anywhere requirements.txt alone was installed. +LOG_MEMORY=false diff --git a/README.md b/README.md index 7bf68eb..6a2119e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,12 @@ properties on the page root, and `.shell` pads itself by those same variables — that's what actually reserves the space so page content doesn't render underneath the bars. +## Memory logging + +Set `LOG_MEMORY=true` in `.env` to print the app's RSS memory usage to the +terminal every time you click a page link. It's a dev aid only, off by +default. + ## Styling / theming Colours, fonts and spacing live in two places that must be kept in step: diff --git a/requirements-dev.txt b/requirements-dev.txt index 3568291..8627e39 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ pytest>=8.0 black>=24.0 pre-commit>=3.7 +psutil>=5.9 diff --git a/src/app.py b/src/app.py index 7662bf1..26b472e 100644 --- a/src/app.py +++ b/src/app.py @@ -14,6 +14,7 @@ from dash import Dash, Input, Output, callback, dcc, html from dotenv import load_dotenv +import memory_log import theme # registers the "4subsea" Plotly template PAGE_TITLE = "Dashboard Template" @@ -25,6 +26,7 @@ DASH_DEBUG = os.getenv("DASH_DEBUG", "true").strip().lower() == "true" MOCK_PLATFORM_HEADER = int(os.getenv("MOCK_PLATFORM_HEADER", "0") or 0) MOCK_PLATFORM_SPACER = int(os.getenv("MOCK_PLATFORM_SPACER", "20") or 20) +LOG_MEMORY = os.getenv("LOG_MEMORY", "false").strip().lower() == "true" app = Dash( __name__, @@ -126,6 +128,8 @@ def mock_4insight(): @callback(Output("nav-bar", "children"), Input("url", "pathname")) def highlight_active_tab(pathname): """Function to highlight the active tab in the sidebar based on the current URL path.""" + if LOG_MEMORY: + memory_log.log_once(pathname) return nav_bar(pathname) diff --git a/src/memory_log.py b/src/memory_log.py new file mode 100644 index 0000000..be9d4a7 --- /dev/null +++ b/src/memory_log.py @@ -0,0 +1,23 @@ +"""Prints RSS memory usage to the terminal on demand. + +Opt-in dev aid - see LOG_MEMORY in app.py and .env.example. Nothing here +writes to a file or feeds a log collector; it is only for watching a local +`python src/app.py` session by eye. +""" + +_process = None + + +def _rss_mb(): + global _process + import psutil + + if _process is None: + _process = psutil.Process() + return _process.memory_info().rss / (1024 * 1024) + + +def log_once(note=""): + """Print one line right now. `note` names what triggered it, e.g. a path.""" + suffix = f" ({note})" if note else "" + print(f"[memory] {_rss_mb():.1f} MB RSS{suffix}") diff --git a/tests/test_app.py b/tests/test_app.py index 3772939..8dd30a8 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -150,3 +150,24 @@ def test_the_mock_header_carries_the_logo_and_placeholder_title_when_configured( assert bar.style["height"] == "82px" assert bottom_bar.style["height"] == "20px" assert text_of(bottom_bar.children) == "simulated spacer — not part of the app" + + +# --------------------------------------------------------------------------- +# Memory logging +# --------------------------------------------------------------------------- + + +def test_navigation_logs_memory_when_enabled(monkeypatch): + monkeypatch.setattr(app, "LOG_MEMORY", True) + calls = [] + monkeypatch.setattr(app.memory_log, "log_once", lambda note="": calls.append(note)) + app.highlight_active_tab("/analytics") + assert calls == ["/analytics"] + + +def test_navigation_does_not_log_memory_when_disabled(monkeypatch): + monkeypatch.setattr(app, "LOG_MEMORY", False) + calls = [] + monkeypatch.setattr(app.memory_log, "log_once", lambda note="": calls.append(note)) + app.highlight_active_tab("/analytics") + assert calls == []