Skip to content

perf: stop the Overview sizing every relation on each render - #30

Merged
Heyosseus merged 1 commit into
Heyosseus:mainfrom
wit3:perf/cheaper-size-widgets
Oct 1, 2026
Merged

Heyosseus merged 1 commit into
Heyosseus:mainfrom
wit3:perf/cheaper-size-widgets

Conversation

@wit3

@wit3 wit3 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What happened

We run Vacuum in a Filament panel on a 160 GB PostgreSQL (~500 tables). While chasing a CPU-saturated database, pg_stat_statements ranked two Vacuum queries first and second across every role on the cluster, over three weeks:

Query Calls Mean Total
LargestTables — pg_total_relation_size() over pg_stat_user_tables, top 8 299 62 s 5 h 08 m
DatabaseVitals::totalBytes() — sum(pg_total_relation_size()) over pg_stat_user_tables 292 26 s 2 h 06 m

That is ~80% of all the time our application role spent in the database, just by opening the Overview. Both queries have the server stat the files of every table, TOAST table and index one relation at a time, and both go through Eloquent rather than ReadOnlyExecutor, so the executor's statement timeout never applies to them.

The change

  • DatabaseVitals: database size from pg_database_size(current_database()), read through ReadOnlyExecutor. Same database, measured back to back: sum(pg_total_relation_size()) 8.1 s on a quiet server, pg_database_size() 1.0 s. The value differs by 0.03% (the system catalogs and the ignored schemas are counted), which is what "Database size" means to the reader anyway.
  • LargestTables: $pollingInterval = null. CanPoll defaults to '5s', so every five seconds the chart re-sized the whole database. Its answer does not change between polls; a reload redraws it.
  • DatabaseVitals: each figure read once per render. cacheHitRatio() was queried three times (value, description, colour) and activeSessions() twice.

Nothing else changes on the page: same stats, same labels, same chart.

Tests

Three new tests in tests/Filament/OverviewTest.php, each red on main and green here:

  • the size card shows Bytes::human(pg_database_size());
  • one pg_stat_database read, one active-sessions read, no pg_total_relation_size per vitals render;
  • LargestTables does not poll.

Both widgets stay at 100% line coverage (checked with Xdebug). rector --dry-run, pint --test and phpstan are clean. Locally the suite has 37 failures, identical on main: they all come from pg_stat_statements must be loaded via "shared_preload_libraries", because my Postgres is not the one in docker-compose.yml. CI should be unaffected.

Left out on purpose

TableResource::getEloquentQuery() also orders the whole list by pg_total_relation_size(), so every load of the Tables list pays the same cost. I didn't touch it: making it cheap without losing exact ordering is a design choice (a size estimate from pg_class.relpages gets stale enough to misorder real tables, as I found on our data), not a fix. Happy to open an issue for it if useful.

🤖 Generated with Claude Code

On a 160 GB database with ~500 tables the two size queries behind the
Overview ranked first and second in pg_stat_statements across every
role: 62 s a call for the Largest tables chart, 26 s for the Database
size card, both pg_total_relation_size() over all of pg_stat_user_tables
and both outside the read-only executor's statement timeout.

- Database size reads pg_database_size() through the read-only executor
  (1 s against 8 s on the same database, a 0.03% difference in value).
- Largest tables no longer polls every five seconds.
- The vitals card reads the cache hit ratio and active sessions once per
  render instead of three and two times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Heyosseus
Heyosseus merged commit e72937b into Heyosseus:main Oct 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants