Skip to content

feat: network-wide presence aggregation for multisite - #299

Open
josephfusco wants to merge 5 commits into
mainfrom
feature/network-presence-screen
Open

feat: network-wide presence aggregation for multisite#299
josephfusco wants to merge 5 commits into
mainfrom
feature/network-presence-screen

Conversation

@josephfusco

@josephfusco josephfusco commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Multisite networks had no way to see who's online across sites.

Part of #298

See #310 for an open question on this design worth discussion before merge.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Sonnet 5
Used for: Assisting with design, implementation, and testing

@github-actions

Copy link
Copy Markdown
Contributor

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props joefusco.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

▶ Preview in WordPress Playground

Open in WordPress Playground

Boots a fresh WordPress with this PR's presence-api build, seeds 5 demo users, and drops you on the dashboard.

Stress-test variant: 40 demo users · Built from 91bf7412813b5146aac4a1e67d7ffb2b4c6ecef5. Auto-updates when you push.

@josephfusco josephfusco changed the title Add network-wide presence for multisite feat: add network-wide presence for multisite Aug 18, 2026
@josephfusco
josephfusco marked this pull request as draft August 18, 2026 21:50
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.16%. Comparing base (433f7b6) to head (91bf741).

Additional details and impacted files
@@            Coverage Diff             @@
##               main     #299    +/-   ##
==========================================
  Coverage     97.15%   97.16%            
  Complexity      190      190            
==========================================
  Files            14       15     +1     
  Lines          2076     2220   +144     
==========================================
+ Hits           2017     2157   +140     
- Misses           59       63     +4     
Flag Coverage Δ
multisite 97.16% <96.66%> (+<0.01%) ⬆️
phpunit 90.27% <6.66%> (-5.78%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@josephfusco josephfusco changed the title feat: add network-wide presence for multisite feat: network-wide presence for multisite Aug 21, 2026
@josephfusco
josephfusco force-pushed the feature/network-presence-screen branch from 9086208 to ee17555 Compare August 21, 2026 19:01
@josephfusco
josephfusco force-pushed the feature/network-presence-screen branch from 204605f to ad241c7 Compare August 22, 2026 20:46
@josephfusco josephfusco changed the title feat: network-wide presence for multisite feat: network-wide presence aggregation for multisite Aug 22, 2026
@josephfusco
josephfusco force-pushed the feature/network-presence-screen branch from a43e3be to 9f75e80 Compare August 22, 2026 22:11
@josephfusco
josephfusco requested a balanced review from Copilot August 23, 2026 03:55
@josephfusco
josephfusco marked this pull request as ready for review August 23, 2026 03:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the network-presence aggregation foundation for multisite using a shared push-based summary table.

Changes:

  • Provisions and updates a network-wide presence summary table.
  • Aggregates fresh site snapshots without switching blogs.
  • Adds multisite provisioning, aggregation, freshness, and recovery tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
presence-api.php Registers storage and lifecycle hooks.
includes/network-functions.php Implements provisioning, pushing, and aggregation.
includes/functions.php Emits change events and renders avatar stacks.
tests/test-network-presence.php Tests aggregation and refresh behavior.
tests/test-network-summary-table-creation.php Tests table provisioning and recovery.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

if ( $held_since && $held_since > time() - MINUTE_IN_SECONDS ) {
return;
}
update_site_option( $lock_option, time() );
Comment on lines +94 to +95
dbDelta(
"CREATE TABLE {$wpdb->presence_network_summary} (
Comment on lines +316 to +321
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT blog_id, data FROM {$wpdb->presence_network_summary} WHERE updated_gmt > %s",
$cutoff
)
);
$total_users = 0;

foreach ( $by_site as $blog_id => $entries ) {
$site = get_site( $blog_id );
Comment on lines +394 to +396
// get_site_url()/get_blog_option() switch blogs on every call; the raw
// WP_Site fields don't, at the cost of not reflecting a mapped domain.
'url' => ( is_ssl() ? 'https://' : 'http://' ) . $site->domain . $site->path,
@josephfusco
josephfusco force-pushed the feature/network-presence-screen branch from 20cda43 to 8a3c989 Compare August 23, 2026 13:35
@josephfusco

Copy link
Copy Markdown
Collaborator Author

Went through this against the sharding question in #310, with the numbers measured on a live multisite rather than read off the diff.

Write amplification on every pageview

wp_set_presence() goes from 1 query to 4 with the push hook active:

wp_set_presence queries with push=4 without push=1

This is not just the heartbeat tick. wp_presence_enqueue_heartbeat_ping() writes the admin room on every admin pageview and every front-end view with an admin bar, so every pageview by every edit_posts user on every site now issues a SELECT plus an INSERT ... ON DUPLICATE KEY UPDATE against {base_prefix}presence_network_summary.

Coalescing bounds row churn. It does not bound statement rate, connection count, or row-lock acquisition, and the SELECT that feeds the comparison runs either way. Full answer to #310 over on that issue.

Fix: debounce the push through the object cache so a site pushes at most once per wp_presence_network_summary_refresh_interval(), and default the aggregation off under wp_is_large_network().

Nothing ever removes a row

There is no wp_uninitialize_site or wp_delete_site hook, so a deleted site's row stays forever. The read path skips it because get_site() returns null, so this is a leak rather than a correctness problem.

uninstall.php also never drops {base_prefix}presence_network_summary and never deletes wp_presence_network_summary_db_version or wp_presence_network_summary_table.lock.

The read path is unbounded

wp_presence_compute_network_summary() selects every row over the cutoff with no LIMIT, hydrates every user on every site, then sorts twice. get_site() runs per row with no _prime_site_caches().

At 300 sites averaging 15 users, the avatar hydration alone:

300 sites x 15 users get_avatar_url: 135ms per summary build

Smaller

  • JSON_PRETTY_PRINT inflates the row 2.8x, 241 bytes against 86 for 15 users. That column is the operand of data <> VALUES(data) on a longtext, so the inflation is paid on every push comparison and every read.
  • false !== $result treats a delete matching zero rows as a change, so the pagehide path fires spurious pushes.
  • url is built from is_ssl() on the current request, so a mixed-scheme network gets wrong links for some sites.
  • wp_presence_register_network_summary_table() appends to ms_global_tables with no in_array guard and runs both at require time and on init priority 0, so the entry lands twice.
  • wp_presence_network_summary_table_exists() runs SHOW TABLES LIKE on every admin_init once provisioned. Same shape as the single-site table, but a network with tens of thousands of tables turns it into a data dictionary scan per admin request.

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