Skip to content

Screen revision counter uses a read-modify-write on a single shared option #204

Description

@josephfusco

wp_presence_bump_screen_revision() (includes/screen-revisions.php:88-111) does a read-modify-write on a single option: it loads the entire wp_presence_screen_revisions map, mutates one key, and writes the whole map back.

It runs on post_updated, edited_term, edit_comment, profile_update, and updated_option, so it is on the hot path for essentially every content change on the site.

Two problems:

  1. Lost updates. There is no locking or atomicity between the get_option() and the update_option(). Two edits that overlap will drop one of the two revision bumps, and the stale-screen banner that depends on the counter will not fire.
  2. Cache churn. Every bump invalidates the entire option in the object cache, no matter which screen key changed. A post save invalidates the revision data for every settings page, term, and comment along with it.

This diverges from core. Core does not funnel a per-object counter through one shared option, it stores per-object state on the object.

Direction

No new table, and no new writes on the hot path.

Moving the map into postmeta is not the fix on its own. update_metadata() calls wp_cache_delete( $object_id, $meta_type . '_meta' ) (wp-includes/meta.php:145), so a write invalidates every cached meta value for that object, not just ours. On a post carrying a lot of meta that trades one shared invalidation for a per-post one.

Posts need no stored revision at all. Everything the banner reads already exists and is already maintained by core on the same save:

  • Revision: post_modified_gmt, set on every update (wp-includes/post.php:4698).
  • Actor: _edit_last, written by edit_post() (wp-admin/includes/post.php:447).
  • Time: post_modified_gmt.

Both are read through the post and meta caches that the edit screen has already primed, so the heartbeat lookup costs no extra query and the save costs no extra write. post_updated is the overwhelming majority of bumps, so this removes most of the churn rather than relocating it.

The remaining four have no equivalent and do need storage, but none of them are hot:

Key Storage Note
post/{id} none post_modified_gmt + _edit_last
user-edit/{id} usermeta wp_insert_user() already writes user meta on every profile update, so the bucket is dirty regardless
term/{tax}/{id} termmeta new invalidation, but term edits are rare
comment/{id} commentmeta new invalidation, but comment edits are rare
options/{page} one option per page, autoload false own cache key, invalidated only when that page is saved

This also removes the need for WP_PRESENCE_SCREEN_REV_LIMIT and the LRU trim, since storage becomes bounded by the objects themselves and WordPress deletes the meta with the object.

wp_presence is not the right home despite already existing. Its rows are TTL-swept at 60 seconds, and a stale-screen banner has to survive longer than that. A viewer idle for more than a minute would get null back from wp_presence_get_screen_revision() and never see the notice, which is the case the feature exists for.

The counter is what forces the read

The lost update is not fixed by relocating storage, because get_metadata() followed by update_metadata() is the same read-modify-write in a narrower place. The client only asks whether the value changed since page load, so a timestamp works as well as an incrementing counter and needs no prior read. That is also what makes post_modified_gmt usable directly.

Caveat: post_modified_gmt has second granularity, so two saves inside the same second produce the same revision and the second one would not raise a banner. The current counter does not have that gap.

That touches assets/js/stale-screen.js:72-81. The rev <= baselineRev comparison still holds for a monotonic timestamp, but rev === baselineRev + 1 assumes increments of exactly one, and would collapse to checking actor_is_me.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Assisting with investigation and drafting

Metadata

Metadata

Assignees

Labels

PerformanceWork relates to query load, cache behavior, or scaling[Area] DatabaseIssues for the wp_presence table and cron cleanup[Type] BugAn existing feature is brokenphpPull requests that update php code

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions