Skip to content

Commit 795e3a5

Browse files
Andrew Bakerclaude
andcommitted
v1.7.37 — CS Monitor: Assets tab, Hooks tab, object cache stats
- Assets tab: all enqueued JS/CSS with plugin attribution, type filter, search - Hooks tab: top 50 hooks by cumulative time, sortable, searchable - Summary: object cache hit-rate card, slowest hooks bar chart, assets card - PHP: perf_hook_tracker(), perf_build_assets_data(), perf_build_cache_data(), perf_build_hooks_data() - Fixes str_contains → strpos for PHP 7.4 compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e3c482d commit 795e3a5

5 files changed

Lines changed: 553 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
## [1.7.36] - 2026-04-05
10+
11+
### Added
12+
- CS Monitor: **Assets tab** — all enqueued JS and CSS files, attributed to plugin/theme/wp-core, with type filter and search
13+
- CS Monitor: **Hooks tab** — top 50 WordPress hooks by cumulative execution time; sortable by count, total, or max time; search filter
14+
- CS Monitor: **Object cache stats** card in Summary — hit rate, hit/miss counts, persistent cache detection (Redis/Memcache)
15+
- CS Monitor: **Slowest Hooks** section in Summary — top 8 hooks by total time with bar chart
16+
- CS Monitor: **Assets** summary card showing JS + CSS counts
17+
918
## [1.7.25] - 2026-03-23
1019

1120
### Fixed

assets/cs-perf-monitor.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,55 @@ td.c-n { color: #555; font-size: 10px; text-align: right; padding-right: 6px; }
10851085
flex-shrink: 0;
10861086
}
10871087

1088+
/* ── Assets tab ──────────────────────────────────────────────────────────── */
1089+
.cs-assets-filters,
1090+
.cs-hooks-filters {
1091+
display: flex;
1092+
align-items: center;
1093+
gap: 6px;
1094+
padding: 5px 8px;
1095+
background: #1e1e1e;
1096+
border-bottom: 1px solid #333;
1097+
flex-shrink: 0;
1098+
}
1099+
.cs-assets-filters input,
1100+
.cs-assets-filters select,
1101+
.cs-hooks-filters input {
1102+
background: #2a2a2a;
1103+
color: #ccc;
1104+
border: 1px solid #444;
1105+
border-radius: 3px;
1106+
padding: 2px 6px;
1107+
font-size: 11px;
1108+
outline: none;
1109+
}
1110+
.cs-assets-filters input { flex: 1; min-width: 120px; }
1111+
.cs-hooks-filters input { flex: 1; min-width: 160px; }
1112+
.cs-asset-type-js { background: #264f78; color: #9cdcfe; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; }
1113+
.cs-asset-type-css { background: #3a2a00; color: #ce9178; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; }
1114+
.cs-asset-ver { color: #888; font-size: 9px; margin-left: 4px; }
1115+
.cs-asset-src { color: #888; font-size: 10px; font-family: "Cascadia Code", "Fira Code", Consolas, "Courier New", monospace; }
1116+
.c-at { width: 36px; flex-shrink: 0; }
1117+
.c-ah { flex: 1; min-width: 120px; }
1118+
.c-ap { width: 130px; }
1119+
.c-au { flex: 2; min-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1120+
1121+
/* ── Hooks tab ────────────────────────────────────────────────────────────── */
1122+
.c-hk { flex: 1; min-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1123+
.c-hc { width: 50px; text-align: right; }
1124+
.c-ht { width: 110px; }
1125+
.c-hm { width: 70px; text-align: right; }
1126+
.c-ha { width: 60px; text-align: right; }
1127+
.cs-sum-lb-bar-hook { background: #4ec9b0; }
1128+
1129+
/* ── Summary: cache + assets cards ───────────────────────────────────────── */
1130+
.cs-sum-card-cache { border-left: 3px solid #4ec9b0; }
1131+
.cs-sum-card-assets { border-left: 3px solid #9cdcfe; }
1132+
1133+
/* ── Help cards: new types ────────────────────────────────────────────────── */
1134+
.cs-help-assets { color: #9cdcfe; }
1135+
.cs-help-hooks { color: #4ec9b0; }
1136+
10881137
/* ── Mobile / narrow viewport ────────────────────────────────────────────── */
10891138
@media (max-width: 600px) {
10901139
/* Header: hide total time text to save space */

assets/cs-perf-monitor.js

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
var N1_THRESH = 3;
2424

2525
// ── State ─────────────────────────────────────────────────────────────────
26-
var data = window.csPerfData || { queries: [], http: [], errors: [], logs: [], meta: {} };
26+
var data = window.csPerfData || { queries: [], http: [], errors: [], logs: [], assets: { scripts: [], styles: [] }, cache: {}, hooks: [], meta: {} };
2727
var meta = data.meta || {};
2828
var sortCol = 'time';
2929
var sortDir = 'desc';
@@ -33,6 +33,10 @@
3333
var filteredHTTP = [];
3434
var n1Patterns = {};
3535

36+
// Hooks sort state
37+
var hookSortCol = 'total_ms';
38+
var hookSortDir = 'desc';
39+
3640
// ── DOM refs ──────────────────────────────────────────────────────────────
3741
var panel, toggleBtn, exportBtn, resizeHandle, footTxt, totalTxt, ctxStrip;
3842
var tabBtns, panes, filterBar;
@@ -41,6 +45,8 @@
4145
var dbCount, httpCount, logCount;
4246
var badgeDB, badgeHTTP, badgeLOG;
4347
var logSearch, logLevel, logSource;
48+
var assetsTbody, assetsCount, assetSearch, assetType, assetPlugin;
49+
var hooksTbody, hooksCount, hookSearch;
4450

4551
// ── Bootstrap ─────────────────────────────────────────────────────────────
4652
document.addEventListener('DOMContentLoaded', function () {
@@ -71,16 +77,27 @@
7177
logSearch = document.getElementById('cs-lf-search');
7278
logLevel = document.getElementById('cs-lf-level');
7379
logSource = document.getElementById('cs-lf-source');
80+
assetsTbody = document.getElementById('cs-assets-rows');
81+
assetsCount = document.getElementById('cs-ptc-assets');
82+
assetSearch = document.getElementById('cs-af-search');
83+
assetType = document.getElementById('cs-af-type');
84+
assetPlugin = document.getElementById('cs-af-plugin');
85+
hooksTbody = document.getElementById('cs-hooks-rows');
86+
hooksCount = document.getElementById('cs-ptc-hooks');
87+
hookSearch = document.getElementById('cs-hkf-search');
7488

7589
if (!panel) return;
7690

7791
computeN1Patterns();
7892
populatePluginFilter();
93+
populateAssetPluginFilter();
7994
updateBadges();
8095
updateTotalTime();
8196
renderPageContext();
8297
applyFilters();
8398
renderLogs();
99+
renderAssets();
100+
renderHooks();
84101
renderSummary();
85102
restoreState();
86103
bindEvents();
@@ -171,8 +188,12 @@
171188
var showFilters = tab === 'db' || tab === 'http';
172189
filterBar.style.display = showFilters ? '' : 'none';
173190
if (dupeChk) dupeChk.parentElement.style.display = tab === 'db' ? '' : 'none';
174-
var logFiltersEl = document.querySelector('.cs-log-filters');
175-
if (logFiltersEl) logFiltersEl.style.display = tab === 'logs' ? '' : 'none';
191+
var logFiltersEl = document.querySelector('.cs-log-filters');
192+
var assetsFiltersEl = document.querySelector('.cs-assets-filters');
193+
var hooksFiltersEl = document.querySelector('.cs-hooks-filters');
194+
if (logFiltersEl) logFiltersEl.style.display = tab === 'logs' ? '' : 'none';
195+
if (assetsFiltersEl) assetsFiltersEl.style.display = tab === 'assets' ? '' : 'none';
196+
if (hooksFiltersEl) hooksFiltersEl.style.display = tab === 'hooks' ? '' : 'none';
176197
}
177198

178199
// ── Plugin filter dropdown ────────────────────────────────────────────────
@@ -187,6 +208,122 @@
187208
});
188209
}
189210

211+
function populateAssetPluginFilter() {
212+
if (!assetPlugin) return;
213+
var seen = {};
214+
var assets = data.assets || {};
215+
(assets.scripts || []).forEach(function (a) { seen[a.plugin] = 1; });
216+
(assets.styles || []).forEach(function (a) { seen[a.plugin] = 1; });
217+
Object.keys(seen).sort().forEach(function (name) {
218+
var opt = document.createElement('option');
219+
opt.value = name; opt.text = name;
220+
assetPlugin.appendChild(opt);
221+
});
222+
}
223+
224+
// ── Assets tab ────────────────────────────────────────────────────────────
225+
function renderAssets() {
226+
if (!assetsTbody) return;
227+
var assets = data.assets || {};
228+
var scripts = assets.scripts || [];
229+
var styles = assets.styles || [];
230+
231+
var typeFilter = assetType ? assetType.value : '';
232+
var pluginFilter = assetPlugin ? assetPlugin.value : '';
233+
var search = assetSearch ? assetSearch.value.toLowerCase().trim() : '';
234+
235+
var rows = [];
236+
if (!typeFilter || typeFilter === 'scripts') {
237+
scripts.forEach(function (s) { rows.push({ type: 'JS', handle: s.handle, src: s.src, plugin: s.plugin, ver: s.ver }); });
238+
}
239+
if (!typeFilter || typeFilter === 'styles') {
240+
styles.forEach(function (s) { rows.push({ type: 'CSS', handle: s.handle, src: s.src, plugin: s.plugin, ver: s.ver }); });
241+
}
242+
243+
rows = rows.filter(function (r) {
244+
if (pluginFilter && r.plugin !== pluginFilter) return false;
245+
if (search && r.handle.toLowerCase().indexOf(search) === -1
246+
&& r.src.toLowerCase().indexOf(search) === -1
247+
&& r.plugin.toLowerCase().indexOf(search) === -1) return false;
248+
return true;
249+
});
250+
251+
if (rows.length === 0) {
252+
assetsTbody.innerHTML = '<tr><td colspan="4" class="cs-empty">'
253+
+ '<span class="cs-empty-icon">&#128190;</span>No assets match the filters.'
254+
+ '</td></tr>';
255+
return;
256+
}
257+
258+
// Sort: plugin then type then handle
259+
rows.sort(function (a, b) {
260+
var pc = a.plugin.localeCompare(b.plugin);
261+
if (pc !== 0) return pc;
262+
var tc = a.type.localeCompare(b.type);
263+
return tc !== 0 ? tc : a.handle.localeCompare(b.handle);
264+
});
265+
266+
var html = '';
267+
rows.forEach(function (r) {
268+
var srcShort = r.src ? truncateUrl(r.src, 55) : '—';
269+
html += '<tr>'
270+
+ '<td class="c-at"><span class="cs-asset-type-' + r.type.toLowerCase() + '">' + r.type + '</span></td>'
271+
+ '<td class="c-ah" title="' + esc(r.handle) + '">' + esc(r.handle) + (r.ver ? '<span class="cs-asset-ver"> v' + esc(r.ver) + '</span>' : '') + '</td>'
272+
+ '<td class="c-ap">' + pluginChip(r.plugin) + '</td>'
273+
+ '<td class="c-au" title="' + esc(r.src) + '"><span class="cs-asset-src">' + esc(srcShort) + '</span></td>'
274+
+ '</tr>';
275+
});
276+
assetsTbody.innerHTML = html;
277+
}
278+
279+
// ── Hooks tab ─────────────────────────────────────────────────────────────
280+
function renderHooks() {
281+
if (!hooksTbody) return;
282+
var hooks = data.hooks || [];
283+
var search = hookSearch ? hookSearch.value.toLowerCase().trim() : '';
284+
285+
var filtered = hooks.filter(function (h) {
286+
return !search || h.hook.toLowerCase().indexOf(search) !== -1;
287+
});
288+
289+
// Sort
290+
filtered = filtered.slice().sort(function (a, b) {
291+
var aVal = a[hookSortCol] !== undefined ? a[hookSortCol] : 0;
292+
var bVal = b[hookSortCol] !== undefined ? b[hookSortCol] : 0;
293+
if (typeof aVal === 'string') {
294+
var cmp = aVal.localeCompare(bVal);
295+
return hookSortDir === 'asc' ? cmp : -cmp;
296+
}
297+
return hookSortDir === 'desc' ? bVal - aVal : aVal - bVal;
298+
});
299+
300+
if (filtered.length === 0) {
301+
hooksTbody.innerHTML = '<tr><td colspan="5" class="cs-empty">'
302+
+ '<span class="cs-empty-icon">&#128279;</span>'
303+
+ (hooks.length === 0 ? 'No hooks captured.' : 'No hooks match the filter.')
304+
+ '</td></tr>';
305+
return;
306+
}
307+
308+
var maxMs = filtered.length > 0 ? filtered[0].total_ms : 1;
309+
var html = '';
310+
filtered.forEach(function (h) {
311+
var barW = maxMs > 0 ? Math.max(2, Math.round((h.total_ms / maxMs) * 60)) : 2;
312+
var cls = speedClass(h.max_ms);
313+
html += '<tr>'
314+
+ '<td class="c-hk" title="' + esc(h.hook) + '">' + esc(h.hook) + '</td>'
315+
+ '<td class="c-hc" style="color:#888">' + h.count + '</td>'
316+
+ '<td class="c-ht"><div class="cs-time-cell">'
317+
+ '<span class="cs-lat-bar cs-lat-' + cls + '" style="width:' + barW + 'px"></span>'
318+
+ '<span class="cs-time-val cs-tv-' + cls + '">' + fmtMs(h.total_ms) + '</span>'
319+
+ '</div></td>'
320+
+ '<td class="c-hm cs-tv-' + speedClass(h.max_ms) + '">' + fmtMs(h.max_ms) + '</td>'
321+
+ '<td class="c-ha" style="color:#888">' + fmtMs(h.avg_ms) + '</td>'
322+
+ '</tr>';
323+
});
324+
hooksTbody.innerHTML = html;
325+
}
326+
190327
// ── Badges ────────────────────────────────────────────────────────────────
191328
function updateBadges() {
192329
badgeDB.querySelector('em').textContent = meta.query_count || 0;
@@ -237,7 +374,12 @@
237374
function updateTabCounts() {
238375
dbCount.textContent = filteredDB.length;
239376
httpCount.textContent = filteredHTTP.length;
240-
if (logCount) logCount.textContent = (data.logs || []).length;
377+
if (logCount) logCount.textContent = (data.logs || []).length;
378+
if (assetsCount) {
379+
var assets = data.assets || {};
380+
assetsCount.textContent = ((assets.scripts || []).length + (assets.styles || []).length);
381+
}
382+
if (hooksCount) hooksCount.textContent = (data.hooks || []).length;
241383
}
242384

243385
// ── Multi-column sort ─────────────────────────────────────────────────────
@@ -619,6 +761,30 @@
619761
+ (logs.length === 0 ? '<span class="cs-s-ok">&#10003; No log entries</span>' : '')
620762
+ '</div></div>';
621763

764+
// Cache card
765+
var cache = data.cache || {};
766+
if (cache.available) {
767+
var hitRateStr = cache.hit_rate !== null ? cache.hit_rate + '%' : '–';
768+
var cacheClass = cache.hit_rate !== null ? (cache.hit_rate >= 80 ? 'cs-s-ok' : cache.hit_rate >= 50 ? 'cs-s-warn' : 'cs-s-crit') : '';
769+
html += '<div class="cs-sum-card cs-sum-card-cache"><div class="cs-sum-card-title">&#9889; Object Cache</div>'
770+
+ '<div class="cs-sum-card-stat">' + hitRateStr + '</div>'
771+
+ '<div class="cs-sum-card-sub">'
772+
+ (cache.hit_rate !== null ? '<span class="' + cacheClass + '">&#9679; ' + hitRateStr + ' hit rate</span>' : '')
773+
+ '<span>' + (cache.hits || 0) + ' hits &middot; ' + (cache.misses || 0) + ' misses</span>'
774+
+ (cache.persistent ? '<span class="cs-s-ok">Persistent cache active</span>' : '<span style="color:#888">Non-persistent (no Redis/Memcache)</span>')
775+
+ '</div></div>';
776+
}
777+
778+
// Assets card
779+
var assets = data.assets || {};
780+
var jsCount = (assets.scripts || []).length;
781+
var cssCount = (assets.styles || []).length;
782+
html += '<div class="cs-sum-card cs-sum-card-assets"><div class="cs-sum-card-title">&#128190; Assets</div>'
783+
+ '<div class="cs-sum-card-stat">' + (jsCount + cssCount) + '</div>'
784+
+ '<div class="cs-sum-card-sub">'
785+
+ '<span>' + jsCount + ' JS &middot; ' + cssCount + ' CSS</span>'
786+
+ '</div></div>';
787+
622788
html += '</div>'; // cards
623789

624790
if (pluginList.length > 0) {
@@ -688,6 +854,22 @@
688854
html += '</div>';
689855
}
690856

857+
// Top hooks
858+
var topHooks = (data.hooks || []).slice(0, 8);
859+
if (topHooks.length > 0) {
860+
var maxHookMs = topHooks[0].total_ms || 1;
861+
html += '<div><div class="cs-sum-section-title">Slowest Hooks (top 8 by total time)</div>';
862+
topHooks.forEach(function (h) {
863+
var bar = maxHookMs > 0 ? Math.max(2, Math.round((h.total_ms / maxHookMs) * 100)) : 2;
864+
html += '<div class="cs-sum-lb-row">'
865+
+ '<span class="cs-sum-lb-name" title="' + esc(h.hook) + '">' + esc(h.hook) + '</span>'
866+
+ '<div class="cs-sum-lb-bar-wrap"><div class="cs-sum-lb-bar cs-sum-lb-bar-hook" style="width:' + bar + '%"></div></div>'
867+
+ '<span class="cs-sum-lb-val">' + h.count + '× &middot; ' + fmtMs(h.total_ms) + ' total &middot; max ' + fmtMs(h.max_ms) + '</span>'
868+
+ '</div>';
869+
});
870+
html += '</div>';
871+
}
872+
691873
summaryWrap.innerHTML = html;
692874
}
693875

@@ -853,6 +1035,42 @@
8531035
});
8541036
}
8551037

1038+
// Assets filters
1039+
[assetSearch, assetType, assetPlugin].forEach(function (el) {
1040+
if (!el) return;
1041+
el.addEventListener('input', renderAssets);
1042+
el.addEventListener('change', renderAssets);
1043+
el.addEventListener('click', function (e) { e.stopPropagation(); });
1044+
});
1045+
var assetsFiltersEl = document.querySelector('.cs-assets-filters');
1046+
if (assetsFiltersEl) assetsFiltersEl.addEventListener('click', function (e) { e.stopPropagation(); });
1047+
1048+
// Hooks filter
1049+
if (hookSearch) {
1050+
hookSearch.addEventListener('input', renderHooks);
1051+
hookSearch.addEventListener('click', function (e) { e.stopPropagation(); });
1052+
}
1053+
var hooksFiltersEl = document.querySelector('.cs-hooks-filters');
1054+
if (hooksFiltersEl) hooksFiltersEl.addEventListener('click', function (e) { e.stopPropagation(); });
1055+
1056+
// Hooks sort headers
1057+
Array.prototype.forEach.call(document.querySelectorAll('#cs-pp-hooks .cs-sortable'), function (th) {
1058+
th.addEventListener('click', function (e) {
1059+
e.stopPropagation();
1060+
var col = th.dataset.sort;
1061+
if (hookSortCol === col) hookSortDir = hookSortDir === 'desc' ? 'asc' : 'desc';
1062+
else { hookSortCol = col; hookSortDir = 'desc'; }
1063+
// Update hook sort header arrows
1064+
Array.prototype.forEach.call(document.querySelectorAll('#cs-pp-hooks .cs-sortable'), function (h) {
1065+
var hCol = h.dataset.sort;
1066+
var labels = { total_ms: 'Total', count: 'Count', max_ms: 'Max' };
1067+
var arrow = hCol !== hookSortCol ? '&#8597;' : (hookSortDir === 'desc' ? '&#8595;' : '&#8593;');
1068+
h.innerHTML = (labels[hCol] || hCol) + '&nbsp;' + arrow;
1069+
});
1070+
renderHooks();
1071+
});
1072+
});
1073+
8561074
bindResizeHandle();
8571075
bindSortHeaders();
8581076

0 commit comments

Comments
 (0)