Skip to content

fix: prevent OOM memory exhaustion on sites with many hooks - #6

Merged
superdav42 merged 1 commit into
mainfrom
bugfix/oom-memory-limits
Mar 26, 2026
Merged

fix: prevent OOM memory exhaustion on sites with many hooks#6
superdav42 merged 1 commit into
mainfrom
bugfix/oom-memory-limits

Conversation

@superdav42

Copy link
Copy Markdown
Contributor

Summary

Fixes the PHP Fatal: Allowed memory size exhausted error reported in issue #2.

Five root causes were identified and fixed:

1. Inline JSON dump removed from render_debug_panel()

The full profile dataset was serialised via wp_json_encode() and embedded as a <script> tag on every page load. On sites with many hooks this payload could be several MB, causing the PHP OOM at render time — before the user even opened the panel.

Data is now fetched lazily via AJAX only when the panel is opened. A dataLoaded guard prevents duplicate requests on subsequent opens.

2. callback_aggregates cap enforced at collection time

Previously only the final get_profile_data() return was sliced to 150 items, but the in-memory map grew without bound throughout the request. A new max_callbacks limit (default 500) stops new entries being created once the cap is reached. Existing entries continue to accumulate timing data; total_execution_time is always accurate.

// Raise the cap if needed:
add_filter( 'wp_hook_profiler_max_callbacks', fn() => 1000 );

3. Per-plugin hooks array capped

timing_data[$plugin]['hooks'] was an ever-growing array of every unique hook name seen for that plugin, causing O(hooks × plugins) memory growth. Capped at max_hooks_per_plugin (default 100).

add_filter( 'wp_hook_profiler_max_hooks_per_plugin', fn() => 200 );

4. Proactive memory guard

on_hook_start() now checks memory_get_usage(true) against a configurable fraction of memory_limit (default 80%) and pauses profiling if the threshold is exceeded. A memory_paused flag is returned in get_profile_data() and shown as a warning banner in the UI.

add_filter( 'wp_hook_profiler_memory_threshold', fn() => 0.70 );

5. plugins summary strips hooks array before returning

get_profile_data() now returns a plugins_summary that omits the hooks array from each plugin entry. The UI does not use it and it was the largest contributor to the serialised payload size.

UI changes

  • Warning banners shown when memory_paused or callbacks_capped so users know the data is partial and how to tune the limits.
  • Panel now shows a loading spinner while the AJAX fetch completes (was instant before since data was inline).

Files changed

  • inc/class-hook-profiler-engine.php — memory guard, caps, filter hooks, summary stripping
  • inc/class-callback-wrapper.php — cap enforcement at collection time, hooks-per-plugin cap
  • hook-profiler.php — removed inline JSON dump from render_debug_panel()
  • assets/profiler.js — lazy AJAX load, dataLoaded guard, memory/cap warning banners
  • assets/profiler.css — warning notice styles
  • README.md — troubleshooting section updated with filter hook examples

Closes #2

@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@superdav42 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ad94101-886f-4d97-92da-04d5c6cb4e3f

📥 Commits

Reviewing files that changed from the base of the PR and between faf8067 and 435920c.

📒 Files selected for processing (6)
  • README.md
  • assets/profiler.css
  • assets/profiler.js
  • hook-profiler.php
  • inc/class-callback-wrapper.php
  • inc/class-hook-profiler-engine.php
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/oom-memory-limits

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@superdav42

Copy link
Copy Markdown
Contributor Author

Pulse: PR has merge conflicts (likely due to PR #4 merging the aidevops init files). Please rebase onto main to resolve.

Root causes identified and fixed (issue #2):

1. Inline JSON dump removed from render_debug_panel()
   The full profile dataset was serialised via wp_json_encode() and embedded
   as a <script> tag on every page load. On sites with many hooks this payload
   could be several MB, causing the PHP OOM at render time. Data is now fetched
   lazily via AJAX only when the panel is opened (dataLoaded guard prevents
   duplicate requests).

2. callback_aggregates cap enforced at collection time
   Previously only the final get_profile_data() return was sliced to 150 items,
   but the in-memory map grew without bound throughout the request. A new
   max_callbacks limit (default 500) stops new entries being created once the
   cap is reached. Existing entries continue to accumulate timing data.
   Configurable: add_filter('wp_hook_profiler_max_callbacks', fn() => 1000)

3. Per-plugin hooks array capped
   timing_data[$plugin]['hooks'] was an ever-growing array of every unique hook
   name seen for that plugin, causing O(hooks x plugins) memory growth. Capped
   at max_hooks_per_plugin (default 100).
   Configurable: add_filter('wp_hook_profiler_max_hooks_per_plugin', fn() => 200)

4. Proactive memory guard
   on_hook_start() now checks memory_get_usage(true) against a configurable
   fraction of PHP memory_limit (default 80%) and pauses profiling if the
   threshold is exceeded. A memory_paused flag is returned in get_profile_data()
   and shown as a warning banner in the UI.
   Configurable: add_filter('wp_hook_profiler_memory_threshold', fn() => 0.70)

5. plugins summary strips hooks array before returning
   get_profile_data() now returns a plugins_summary that omits the hooks array
   from each plugin entry — the UI does not use it and it was the largest
   contributor to the serialised payload size.

UI: warning banners shown when memory_paused or callbacks_capped so users
know the data is partial and how to tune the limits.
@superdav42
superdav42 force-pushed the bugfix/oom-memory-limits branch from ca73d70 to 435920c Compare March 26, 2026 04:51
@superdav42
superdav42 merged commit 52210f6 into main Mar 26, 2026
1 check 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.

PHP Fatal error: Allowed memory size exhausted

1 participant