Skip to content

Conversation

@joychetry
Copy link

@joychetry joychetry commented Sep 3, 2025

  • Implemented comprehensive hook profiler interface with multi-tab navigation
  • Added plugin loading analysis with timing breakdowns by plugin type (sunrise, mu-plugins, network, regular)
  • Enhanced callback wrapper with detailed timing aggregation and plugin detection
  • Added advanced filtering capabilities for hooks, plugins, and callbacks with real-time search
  • Introduced performance visualization with color-coded timing indicators
  • Integrated sorting and grouping functionality for better data analysis

Summary by CodeRabbit

  • New Features

    • Added per-hook search across hook names, callback names, and meta fields.
    • Plugin filter now applies per-callback within hooks and updates headers with visible/total counts.
    • Displays human-friendly plugin names when available, with graceful fallback.
  • Bug Fixes

    • Robust handling of missing or NaN timing data; times render as 0.000 ms when needed.
    • Hook group sorting now uses total time for more accurate ordering.
    • Consistent plugin name usage and header/meta updates across the profiler UI.

…gin performance analysis UI

- Implemented comprehensive hook profiler interface with multi-tab navigation
- Added plugin loading analysis with timing breakdowns by plugin type (sunrise, mu-plugins, network, regular)
- Enhanced callback wrapper with detailed timing aggregation and plugin detection
- Added advanced filtering capabilities for hooks, plugins, and callbacks with real-time search
- Introduced performance visualization with color-coded timing indicators
- Integrated sorting and grouping functionality for better data analysis
@coderabbitai
Copy link

coderabbitai bot commented Sep 3, 2025

Walkthrough

Updates profiler UI logic to prefer plugin_name, handle NaN-safe times, compute and sort by total_time, and introduce refined per-hook filtering/search (by plugin and search term) with visibility counts. PHP adds plugin_name to callback aggregates. No public API signature changes.

Changes

Cohort / File(s) Summary of Changes
Profiler UI filtering, sorting, and rendering
assets/profiler.js
- Prefer plugin_name over plugin in displays and headers; NaN-safe time rendering
- Group totalTime computed from total_time with safe defaults; sort groups by total_time
- Reworked plugin-filtering to filter callbacks within hooks and show counts (visible/total)
- Added filterHooksBySearch for hook/callback/meta search preserving visibility
- Minor formatting near populatePluginFilter
Callback aggregation data enrichment
inc/class-callback-wrapper.php
- Added 'plugin_name' to per-callback aggregate initialization alongside existing fields; no other logic changes

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Profiler UI
  participant Data as Callback Groups
  participant Filter as filterHooksBySearch / plugin-filter
  participant Render as Renderer

  User->>UI: Set search term / plugin filter
  UI->>Filter: Apply search and/or plugin filter
  alt Plugin filter active
    Filter->>Data: Filter callbacks within each hook
    Filter-->>UI: Matching hooks + visible/total counts
  else Search only
    Filter->>Data: Search by hook/callback/meta
    Filter-->>UI: Matching hooks (preserve callback visibility)
  end
  UI->>Data: Compute total_time per group (safe defaults)
  UI->>Data: Sort groups by total_time
  UI->>Render: Render headers (plugin_name, counts, times)
  Render-->>User: Updated table (NaN-safe times)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my whiskers, sift the logs,
Through hooky thickets and timing fogs.
Plugin names bloom, times don’t NaN,
I sort the heaps like a careful fan.
With filtered hops and counted calls—
Thump! The profiler neatly stalls no stalls. 🐇⏱️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
inc/class-callback-wrapper.php (1)

60-79: Bug: totals updated even when $eta is NaN/invalid.

Plugin totals are incremented outside the finite/positive guard, which can propagate NaN or negative values into profileData.plugins. Move the totals block under the same validation.

Apply this diff:

-        // Update plugin totals
-        $plugin_key = $plugin_info['plugin'];
-        if (!isset($this->engine->timing_data[$plugin_key])) {
+        // Update plugin totals (guarded)
+        if (is_finite($eta) && $eta >= 0) {
+        $plugin_key = $plugin_info['plugin'];
+        if (!isset($this->engine->timing_data[$plugin_key])) {
             $this->engine->timing_data[$plugin_key] = [
                 'total_time' => 0,
                 'hook_count' => 0,
                 'callback_count' => 0,
                 'hooks' => [],
                 'plugin_name' => $plugin_info['plugin_name'],
                 'plugin_file' => $plugin_info['plugin_file']
             ];
-        }
-        
-        $this->engine->timing_data[$plugin_key]['total_time'] += $eta;
-        $this->engine->timing_data[$plugin_key]['callback_count']++;
+        }
+        $this->engine->timing_data[$plugin_key]['total_time'] += $eta;
+        $this->engine->timing_data[$plugin_key]['callback_count']++;
         
         if (!in_array($this->hook_name, $this->engine->timing_data[$plugin_key]['hooks'])) {
             $this->engine->timing_data[$plugin_key]['hooks'][] = $this->hook_name;
             $this->engine->timing_data[$plugin_key]['hook_count']++;
         }
+        }
🧹 Nitpick comments (6)
inc/class-callback-wrapper.php (1)

48-58: PHP compatibility: avoid dependency on is_finite availability.

On older environments, is_finite may be unavailable. Use a fallback check to keep behavior consistent.

Apply this diff:

-        if (is_finite($eta) && $eta >= 0) {
+        $eta_is_finite = function_exists('is_finite') ? is_finite($eta) : (!is_nan($eta) && !is_infinite($eta));
+        if ($eta_is_finite && $eta >= 0) {
assets/profiler.js (5)

190-201: Make plugin filtering robust: add data attribute for exact matching.

Relying on meta text parsing is brittle and not i18n-safe. Store plugin on the item.

Apply this diff:

-                    <div class="wp-hook-profiler-callback-item">
+                    <div class="wp-hook-profiler-callback-item" data-plugin="${escapeHtml(callback.plugin_name || callback.plugin)}">

And adjust the filter (see below).


174-181: Nit: escape hookName in data-hook attribute.

Prevents malformed attributes if hook names contain special chars.

Apply this diff:

-                <div class="wp-hook-profiler-hook-group" data-hook="${hookName}">
+                <div class="wp-hook-profiler-hook-group" data-hook="${escapeHtml(hookName)}">

313-332: Use data-plugin instead of parsing text for filtering.

This avoids false positives and supports future localization.

Apply this diff:

-                const callbackItems = $(this).find('.wp-hook-profiler-callback-item');
-                callbackItems.each(function() {
-                    const callbackMeta = $(this).find('.wp-hook-profiler-callback-meta');
-                    const metaText = callbackMeta.text();
-                    // Check if this callback matches the plugin filter
-                    const matchesPlugin = metaText.includes(`Plugin: ${pluginFilter}`) ||
-                                        metaText.toLowerCase().includes(`plugin: ${pluginFilter.toLowerCase()}`) ||
-                                        metaText.toLowerCase().includes(pluginFilter.toLowerCase());
-                    if (matchesPlugin) {
+                const callbackItems = $(this).find('.wp-hook-profiler-callback-item');
+                callbackItems.each(function() {
+                    const itemPlugin = ($(this).data('plugin') || '').toString();
+                    const matchesPlugin = itemPlugin.toLowerCase() === pluginFilter.toLowerCase();
+                    if (matchesPlugin) {
                         hookHasMatchingCallbacks = true;
                         visibleCallbackCount++;
                         $(this).show();
                     } else {
                         $(this).hide();
                     }
                 });

355-362: Remove commented debug logs.

Dead comments add noise. Consider deleting or gating behind a verbose flag.


98-100: NaN-safe total time in summary.

Guard against undefined/NaN to avoid showing “NaN”.

Apply this diff:

-        $('#wp-hook-profiler-total-time').text((profileData.total_execution_time).toFixed(2));
+        const totalTime = Number(profileData.total_execution_time) || 0;
+        $('#wp-hook-profiler-total-time').text(totalTime.toFixed(2));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 295dfd9 and b18d0ab.

📒 Files selected for processing (2)
  • assets/profiler.js (6 hunks)
  • inc/class-callback-wrapper.php (1 hunks)
🔇 Additional comments (4)
inc/class-callback-wrapper.php (1)

31-31: Good addition: expose human-friendly plugin name.

Including plugin_name in aggregates aligns backend with the UI and filtering needs.

assets/profiler.js (3)

151-152: UI correctness: prefer plugin_name with safe fallback.

Good fallback to callback.plugin when plugin_name is absent.


299-305: Nice: split search-only path for performance.

The early return reduces DOM work when only searching.


408-409: Sorting by total_time makes sense.

Aligns sorting with displayed metrics and avoids undefined penalties via fallback 0.

Comment on lines +170 to 178
const totalTime = callbacks.reduce((sum, cb) => sum + (cb.total_time || 0), 0) * 1000;
const timeClass = getTimeColorClass(totalTime);

const hookGroup = $(`
<div class="wp-hook-profiler-hook-group" data-hook="${hookName}">
<div class="wp-hook-profiler-hook-header">
${escapeHtml(hookName)}
<span class="${timeClass}" style="float: right;">${totalTime.toFixed(3)}ms</span>
<span class="${timeClass}" style="float: right;">${isNaN(totalTime) ? '0.000' : totalTime.toFixed(3)}ms</span>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: unit error inflates hook total time by 1000x.

callback.total_time is already in milliseconds; multiplying by 1000 mislabels values and color classes.

Apply this diff:

-            const totalTime = callbacks.reduce((sum, cb) => sum + (cb.total_time || 0), 0) * 1000;
+            const totalTime = callbacks.reduce((sum, cb) => sum + (cb.total_time || 0), 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalTime = callbacks.reduce((sum, cb) => sum + (cb.total_time || 0), 0) * 1000;
const timeClass = getTimeColorClass(totalTime);
const hookGroup = $(`
<div class="wp-hook-profiler-hook-group" data-hook="${hookName}">
<div class="wp-hook-profiler-hook-header">
${escapeHtml(hookName)}
<span class="${timeClass}" style="float: right;">${totalTime.toFixed(3)}ms</span>
<span class="${timeClass}" style="float: right;">${isNaN(totalTime) ? '0.000' : totalTime.toFixed(3)}ms</span>
</div>
const totalTime = callbacks.reduce((sum, cb) => sum + (cb.total_time || 0), 0);
const timeClass = getTimeColorClass(totalTime);
const hookGroup = $(`
<div class="wp-hook-profiler-hook-group" data-hook="${hookName}">
<div class="wp-hook-profiler-hook-header">
${escapeHtml(hookName)}
<span class="${timeClass}" style="float: right;">${isNaN(totalTime) ? '0.000' : totalTime.toFixed(3)}ms</span>
</div>
🤖 Prompt for AI Agents
In assets/profiler.js around lines 170 to 178, the code multiplies
callback.total_time by 1000 but callback.total_time is already in milliseconds,
inflating displayed totals and affecting color classification; remove the * 1000
multiplier so totalTime is computed as const totalTime = callbacks.reduce((sum,
cb) => sum + (cb.total_time || 0), 0); keep the subsequent
getTimeColorClass(totalTime) and the isNaN/ toFixed formatting unchanged so
values and classes reflect true milliseconds.

Comment on lines +310 to +353
let hookHasMatchingCallbacks = !pluginFilter;
let visibleCallbackCount = 0;

if (pluginFilter) {
const hasMatchingPlugin = $(this).find('.wp-hook-profiler-callback-meta')
.toArray()
.some(el => $(el).text().includes(`Plugin: ${pluginFilter}`));
matchesPlugin = hasMatchingPlugin;
// Filter individual callbacks within this hook group
const callbackItems = $(this).find('.wp-hook-profiler-callback-item');
callbackItems.each(function() {
const callbackMeta = $(this).find('.wp-hook-profiler-callback-meta');
const metaText = callbackMeta.text();

// Check if this callback matches the plugin filter
const matchesPlugin = metaText.includes(`Plugin: ${pluginFilter}`) ||
metaText.toLowerCase().includes(`plugin: ${pluginFilter.toLowerCase()}`) ||
metaText.toLowerCase().includes(pluginFilter.toLowerCase());

if (matchesPlugin) {
hookHasMatchingCallbacks = true;
visibleCallbackCount++;
$(this).show();
} else {
$(this).hide();
}
});
} else {
// No plugin filter - show all callbacks
$(this).find('.wp-hook-profiler-callback-item').show();
hookHasMatchingCallbacks = true;
visibleCallbackCount = $(this).find('.wp-hook-profiler-callback-item').length;
}

// Show/hide the entire hook group based on whether it has matching callbacks
const shouldShowHook = matchesSearch && hookHasMatchingCallbacks && visibleCallbackCount > 0;
$(this).toggle(shouldShowHook);

// Update hook header to reflect filtered callback count
if (shouldShowHook && pluginFilter) {
const totalCallbacks = $(this).find('.wp-hook-profiler-callback-item').length;
const hookHeader = $(this).find('.wp-hook-profiler-hook-header');
const originalText = hookHeader.text().replace(/\(\d+\/\d+\)/, '').trim();
if (visibleCallbackCount !== totalCallbacks) {
hookHeader.html(`${originalText} <small>(${visibleCallbackCount}/${totalCallbacks} callbacks)</small>`);
}
}
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: header rewrite drops the right-aligned time span.

Using hookHeader.text() then .html() nukes existing child nodes (time badge) and may duplicate labels on subsequent filters.

Apply this diff to preserve the time span and only toggle a dedicated count node:

-            // Update hook header to reflect filtered callback count
-            if (shouldShowHook && pluginFilter) {
-                const totalCallbacks = $(this).find('.wp-hook-profiler-callback-item').length;
-                const hookHeader = $(this).find('.wp-hook-profiler-hook-header');
-                const originalText = hookHeader.text().replace(/\(\d+\/\d+\)/, '').trim();
-                if (visibleCallbackCount !== totalCallbacks) {
-                    hookHeader.html(`${originalText} <small>(${visibleCallbackCount}/${totalCallbacks} callbacks)</small>`);
-                }
-            }
+            // Update hook header to reflect filtered callback count (preserve time span)
+            if (shouldShowHook && pluginFilter) {
+                const totalCallbacks = $(this).find('.wp-hook-profiler-callback-item').length;
+                const hookHeader = $(this).find('.wp-hook-profiler-hook-header');
+                let countEl = hookHeader.find('.wp-hook-profiler-hook-count');
+                if (!countEl.length) {
+                    countEl = $('<small class="wp-hook-profiler-hook-count"></small>').appendTo(hookHeader);
+                }
+                if (visibleCallbackCount !== totalCallbacks) {
+                    countEl.text(` (${visibleCallbackCount}/${totalCallbacks} callbacks)`);
+                } else {
+                    countEl.remove();
+                }
+            } else {
+                $(this).find('.wp-hook-profiler-hook-header .wp-hook-profiler-hook-count').remove();
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let hookHasMatchingCallbacks = !pluginFilter;
let visibleCallbackCount = 0;
if (pluginFilter) {
const hasMatchingPlugin = $(this).find('.wp-hook-profiler-callback-meta')
.toArray()
.some(el => $(el).text().includes(`Plugin: ${pluginFilter}`));
matchesPlugin = hasMatchingPlugin;
// Filter individual callbacks within this hook group
const callbackItems = $(this).find('.wp-hook-profiler-callback-item');
callbackItems.each(function() {
const callbackMeta = $(this).find('.wp-hook-profiler-callback-meta');
const metaText = callbackMeta.text();
// Check if this callback matches the plugin filter
const matchesPlugin = metaText.includes(`Plugin: ${pluginFilter}`) ||
metaText.toLowerCase().includes(`plugin: ${pluginFilter.toLowerCase()}`) ||
metaText.toLowerCase().includes(pluginFilter.toLowerCase());
if (matchesPlugin) {
hookHasMatchingCallbacks = true;
visibleCallbackCount++;
$(this).show();
} else {
$(this).hide();
}
});
} else {
// No plugin filter - show all callbacks
$(this).find('.wp-hook-profiler-callback-item').show();
hookHasMatchingCallbacks = true;
visibleCallbackCount = $(this).find('.wp-hook-profiler-callback-item').length;
}
// Show/hide the entire hook group based on whether it has matching callbacks
const shouldShowHook = matchesSearch && hookHasMatchingCallbacks && visibleCallbackCount > 0;
$(this).toggle(shouldShowHook);
// Update hook header to reflect filtered callback count
if (shouldShowHook && pluginFilter) {
const totalCallbacks = $(this).find('.wp-hook-profiler-callback-item').length;
const hookHeader = $(this).find('.wp-hook-profiler-hook-header');
const originalText = hookHeader.text().replace(/\(\d+\/\d+\)/, '').trim();
if (visibleCallbackCount !== totalCallbacks) {
hookHeader.html(`${originalText} <small>(${visibleCallbackCount}/${totalCallbacks} callbacks)</small>`);
}
}
});
let hookHasMatchingCallbacks = !pluginFilter;
let visibleCallbackCount = 0;
if (pluginFilter) {
// Filter individual callbacks within this hook group
const callbackItems = $(this).find('.wp-hook-profiler-callback-item');
callbackItems.each(function() {
const callbackMeta = $(this).find('.wp-hook-profiler-callback-meta');
const metaText = callbackMeta.text();
// Check if this callback matches the plugin filter
const matchesPlugin = metaText.includes(`Plugin: ${pluginFilter}`) ||
metaText.toLowerCase().includes(`plugin: ${pluginFilter.toLowerCase()}`) ||
metaText.toLowerCase().includes(pluginFilter.toLowerCase());
if (matchesPlugin) {
hookHasMatchingCallbacks = true;
visibleCallbackCount++;
$(this).show();
} else {
$(this).hide();
}
});
} else {
// No plugin filter - show all callbacks
$(this).find('.wp-hook-profiler-callback-item').show();
hookHasMatchingCallbacks = true;
visibleCallbackCount = $(this).find('.wp-hook-profiler-callback-item').length;
}
// Show/hide the entire hook group based on whether it has matching callbacks
const shouldShowHook = matchesSearch && hookHasMatchingCallbacks && visibleCallbackCount > 0;
$(this).toggle(shouldShowHook);
// Update hook header to reflect filtered callback count (preserve time span)
if (shouldShowHook && pluginFilter) {
const totalCallbacks = $(this).find('.wp-hook-profiler-callback-item').length;
const hookHeader = $(this).find('.wp-hook-profiler-hook-header');
let countEl = hookHeader.find('.wp-hook-profiler-hook-count');
if (!countEl.length) {
countEl = $('<small class="wp-hook-profiler-hook-count"></small>').appendTo(hookHeader);
}
if (visibleCallbackCount !== totalCallbacks) {
countEl.text(` (${visibleCallbackCount}/${totalCallbacks} callbacks)`);
} else {
countEl.remove();
}
} else {
$(this).find('.wp-hook-profiler-hook-header .wp-hook-profiler-hook-count').remove();
}
});

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.

1 participant