Every page re-reads the package metadata for the footer's version, and the dashboard does it once per widget #560

Closed
opened 2026-10-01 10:47:38 +00:00 by tiagoagueda · 1 comment
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, by counting and timing the ui context processor on the dashboard and the applications page.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at core/updates.py:106-126 (installed_version, moved there by #248 and still uncached; core/context_processors.py:9 imports it and hands it on to healthz), core/context_processors.py:91 (the call in ui) and core/templatetags/postulo.py:492-506 (render_widget); templates/core/dashboard.html:150 and core/views.py:56-58 have not moved.

The ui context processor calls installed_version() on every render with a request, and that reads the installed distribution's metadata from disk each time, although the version cannot change without a restart. The dashboard renders each widget with render_to_string(..., request=...), which builds a fresh RequestContext and runs every context processor again, so the page pays for ui() once per widget.

  • Where:
    • core/context_processors.py:87 ("postulo_version": installed_version() in ui)
    • core/context_processors.py:125-139 (installed_version: importlib.metadata.version("postulo"), uncached)
    • core/templatetags/postulo.py:511-525 (render_widget: render_to_string(rendered.spec.template, …, request=context.get("request")))
    • templates/core/dashboard.html:150 ({% render_widget item %} per widget)
    • core/views.py:56-58 (healthz reads the metadata on every health check as well)
  • What happens: on the dashboard with the seven default widgets, ui() runs 8 times and installed_version() 8 times; on the applications page, once. Measured on Windows, ui() costs 3–4 ms per call, about half of it in installed_version(), so the widgets alone add roughly 25 ms to a dashboard that renders in about 45 ms. Every htmx fragment rendered with a request (each pause while typing in a table filter) pays one metadata read.
  • Why it matters: a fixed cost on every page and a per-widget multiplier on the busiest one, for a value that is constant for the life of the process. The render_widget docstring also says "a widget can only see what it asked for", which is not true: each widget receives the whole ui dict, the messages and the rest of the processors' output.
  • Fix:
    • Decorate installed_version with @functools.cache.
    • Render widgets without re-running the processors: load the template and render it against a plain dict of the widget's context plus widget, request and csrf_token taken from the page's context (widgets/due_reminders.html:20 and widgets/gone_quiet.html:37,44,51 post forms and need the token). That also makes the docstring true.

Related to #231, whose item 4 memoised the site settings per request but left ui itself running once per widget.

Done when

  • A test in tests/test_footer.py renders two pages in a row and asserts importlib.metadata.version was called at most once.
  • A test in tests/test_dashboard_grid.py renders the dashboard with the default widgets and asserts the ui context processor ran once.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, by counting and timing the `ui` context processor on the dashboard and the applications page. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `core/updates.py:106-126` (`installed_version`, moved there by #248 and still uncached; `core/context_processors.py:9` imports it and hands it on to `healthz`), `core/context_processors.py:91` (the call in `ui`) and `core/templatetags/postulo.py:492-506` (`render_widget`); `templates/core/dashboard.html:150` and `core/views.py:56-58` have not moved. The `ui` context processor calls `installed_version()` on every render with a request, and that reads the installed distribution's metadata from disk each time, although the version cannot change without a restart. The dashboard renders each widget with `render_to_string(..., request=...)`, which builds a fresh `RequestContext` and runs every context processor again, so the page pays for `ui()` once per widget. - **Where:** - `core/context_processors.py:87` (`"postulo_version": installed_version()` in `ui`) - `core/context_processors.py:125-139` (`installed_version`: `importlib.metadata.version("postulo")`, uncached) - `core/templatetags/postulo.py:511-525` (`render_widget`: `render_to_string(rendered.spec.template, …, request=context.get("request"))`) - `templates/core/dashboard.html:150` (`{% render_widget item %}` per widget) - `core/views.py:56-58` (`healthz` reads the metadata on every health check as well) - **What happens:** on the dashboard with the seven default widgets, `ui()` runs 8 times and `installed_version()` 8 times; on the applications page, once. Measured on Windows, `ui()` costs 3–4 ms per call, about half of it in `installed_version()`, so the widgets alone add roughly 25 ms to a dashboard that renders in about 45 ms. Every htmx fragment rendered with a request (each pause while typing in a table filter) pays one metadata read. - **Why it matters:** a fixed cost on every page and a per-widget multiplier on the busiest one, for a value that is constant for the life of the process. The `render_widget` docstring also says "a widget can only see what it asked for", which is not true: each widget receives the whole `ui` dict, the messages and the rest of the processors' output. - **Fix:** - Decorate `installed_version` with `@functools.cache`. - Render widgets without re-running the processors: load the template and render it against a plain dict of the widget's context plus `widget`, `request` and `csrf_token` taken from the page's context (`widgets/due_reminders.html:20` and `widgets/gone_quiet.html:37,44,51` post forms and need the token). That also makes the docstring true. Related to #231, whose item 4 memoised the site settings per request but left `ui` itself running once per widget. ## Done when - A test in `tests/test_footer.py` renders two pages in a row and asserts `importlib.metadata.version` was called at most once. - A test in `tests/test_dashboard_grid.py` renders the dashboard with the default widgets and asserts the `ui` context processor ran once.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:47:38 +00:00
Author
Owner

Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#560).

Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#560).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#560
No description provided.