Skip to content

perf: Memoize is_user_admin, is_preview_request and viewing_as_spec per request#1553

Open
jhalitschke wants to merge 2 commits into
Automattic:trunkfrom
jhalitschke:perf/memoize-checks-per-request
Open

perf: Memoize is_user_admin, is_preview_request and viewing_as_spec per request#1553
jhalitschke wants to merge 2 commits into
Automattic:trunkfrom
jhalitschke:perf/memoize-checks-per-request

Conversation

@jhalitschke
Copy link
Copy Markdown

@jhalitschke jhalitschke commented Apr 28, 2026

All Submissions:

Changes proposed in this Pull Request:

Summary

  • Memoize Newspack_Popups::is_user_admin(), Newspack_Popups::is_preview_request() and Newspack_Popups_View_As::viewing_as_spec() using static variables so each method computes its result only
    once per request.

Problem

On archive pages with many posts, these methods are called repeatedly per popup x post combination:

Method Calls per request Cost
is_user_admin() 132 1.6ms
is_preview_request() 120 3.5ms
viewing_as_spec() 132 2.4ms

The call chain is:
assess_taxonomy_filter (96x) -> is_preview_request -> viewing_as_spec -> is_user_admin
show_admin_bar (22x) -> is_preview_request -> viewing_as_spec -> is_user_admin
parse_view_as (12x) -> viewing_as_spec -> is_user_admin

is_user_admin() calls apply_filters() + current_user_can() every time. is_preview_request() calls is_customize_preview(), previewed_popup_id(), preset_popup_id() and
viewing_as_spec() every time. None of these results can change within a single HTTP request.

On a test archive page with 48 posts and ~91 evaluated popups, the total overhead is ~5ms. The cost scales linearly with post count x popup count.

Fix

Add static $result memoization to all three methods. The first call computes the result; subsequent calls return the cached value immediately.

This pattern is safe because:

  • User capabilities don't change within a request
  • $_GET['view_as'] is constant within a request
  • previewed_popup_id() / preset_popup_id() read from $_GET which is constant
  • is_customize_preview() is constant within a request

Test plan

  • Verify prompts display correctly on archive pages (logged out)
  • Verify prompts display correctly for admin users (logged in)
  • Verify "View As" feature still works (?view_as=... parameter)
  • Verify single popup preview still works
  • Verify Customizer preview still works

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to improve runtime performance on high-post-count archive pages by memoizing a few frequently-invoked “request-constant” methods in the Newspack Popups plugin.

Changes:

  • Added local-static memoization to Newspack_Popups::is_preview_request() and Newspack_Popups::is_user_admin().
  • Added local-static memoization to Newspack_Popups_View_As::viewing_as_spec().
  • Updated (but introduced issues in) the is_preview_request() docblock sentence.

Reviewed changes

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

File Description
includes/class-newspack-popups.php Memoizes preview/admin checks to reduce repeated capability/query evaluation work.
includes/class-newspack-popups-view-as.php Memoizes “view as” spec resolution to avoid repeated admin + query param parsing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


/**
* Is it a preview request a single popup preview or using "view as" feature.
* Is it a preview request ? a single popup preview or using "view as" feature.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The docblock punctuation/grammar was changed to "Is it a preview request ? ..." which reads incorrectly (space before "?" and uses "?" where the sentence continues). Please update it to a grammatically correct form (e.g., "Is it a preview request? ..." or "Is it a preview request — ...").

Suggested change
* Is it a preview request ? a single popup preview or using "view as" feature.
* Is it a preview request? A single popup preview or using the "view as" feature.

Copilot uses AI. Check for mistakes.
Comment on lines 833 to +842
public static function is_preview_request() {
static $result = null;
if ( null !== $result ) {
return $result;
}
$is_customizer_preview = is_customize_preview();
// Used by the Newspack Plugin's Campaigns Wizard.
$is_view_as_preview = false != Newspack_Popups_View_As::viewing_as_spec();
return ! empty( self::previewed_popup_id() ) || ! empty( self::preset_popup_id() ) || $is_view_as_preview || $is_customizer_preview;
$result = ! empty( self::previewed_popup_id() ) || ! empty( self::preset_popup_id() ) || $is_view_as_preview || $is_customizer_preview;
return $result;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Memoizing the preview-request result in a local static can leak across multiple simulated requests in the PHPUnit suite (tests call go_to() repeatedly with different query strings). Once cached, later renders in the same PHP process may get a stale preview/non-preview value. Consider caching per request context (e.g., keyed by relevant $_GET values / request URI) or adding an explicit reset hook used by tests before each go_to()/render.

Copilot uses AI. Check for mistakes.
Comment on lines 970 to +984
@@ -971,7 +980,8 @@ public static function is_user_admin() {
* @return string Filtered capability string.
*/
$capability = apply_filters( 'newspack_popups_admin_user_capability', 'edit_others_pages' );
return is_user_logged_in() && current_user_can( $capability );
$result = is_user_logged_in() && current_user_can( $capability );
return $result;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

is_user_admin() is now memoized with a single static $result, which becomes stale if the current user changes within the same PHP process (the test suite calls wp_set_current_user() mid-test). This will cause admin checks to keep returning the first computed value. Cache should be keyed by get_current_user_id() (or invalidated on the set_current_user action) so it remains correct when user context changes.

Copilot uses AI. Check for mistakes.
Comment on lines 35 to +41
* Get the "view as" feature specification.
*
* @return string "View as" specification.
*/
public static function viewing_as_spec() {
static $result = null;
if ( null !== $result ) {
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The @return docblock for viewing_as_spec() says it returns a string, but the function can return false. Please update the docblock to reflect the actual return type (e.g., string|false) so callers and static analysis don’t get incorrect type information.

Copilot uses AI. Check for mistakes.
Comment on lines 39 to +53
public static function viewing_as_spec() {
static $result = null;
if ( null !== $result ) {
return $result;
}
if ( ! Newspack_Popups::is_user_admin() ) {
return false;
$result = false;
return $result;
}
if ( isset( $_GET['view_as'] ) && $_GET['view_as'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return sanitize_text_field( $_GET['view_as'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$result = sanitize_text_field( $_GET['view_as'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $result;
}
$result = false;
return $result;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

viewing_as_spec() now memoizes into a single static $result. In PHPUnit (and in any long-running PHP process), user context and $_GET['view_as'] can change between calls; once cached, this method can keep returning a stale spec/false even after wp_set_current_user() or go_to() updates the request. Prefer caching keyed by current user ID and the raw view_as value, or provide a reset mechanism for tests.

Copilot uses AI. Check for mistakes.
@dkoo
Copy link
Copy Markdown
Contributor

dkoo commented May 29, 2026

@jhalitschke can you please address the feedback from the Copilot code review? If you disagree with any feedback that's no problem, just respond to the thread with an explanation before you resolve it. We'll do a full code review after all Copilot feedback is resolved.

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.

3 participants