|
23 | 23 | var N1_THRESH = 3; |
24 | 24 |
|
25 | 25 | // ── 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: {} }; |
27 | 27 | var meta = data.meta || {}; |
28 | 28 | var sortCol = 'time'; |
29 | 29 | var sortDir = 'desc'; |
|
33 | 33 | var filteredHTTP = []; |
34 | 34 | var n1Patterns = {}; |
35 | 35 |
|
| 36 | + // Hooks sort state |
| 37 | + var hookSortCol = 'total_ms'; |
| 38 | + var hookSortDir = 'desc'; |
| 39 | + |
36 | 40 | // ── DOM refs ────────────────────────────────────────────────────────────── |
37 | 41 | var panel, toggleBtn, exportBtn, resizeHandle, footTxt, totalTxt, ctxStrip; |
38 | 42 | var tabBtns, panes, filterBar; |
|
41 | 45 | var dbCount, httpCount, logCount; |
42 | 46 | var badgeDB, badgeHTTP, badgeLOG; |
43 | 47 | var logSearch, logLevel, logSource; |
| 48 | + var assetsTbody, assetsCount, assetSearch, assetType, assetPlugin; |
| 49 | + var hooksTbody, hooksCount, hookSearch; |
44 | 50 |
|
45 | 51 | // ── Bootstrap ───────────────────────────────────────────────────────────── |
46 | 52 | document.addEventListener('DOMContentLoaded', function () { |
|
71 | 77 | logSearch = document.getElementById('cs-lf-search'); |
72 | 78 | logLevel = document.getElementById('cs-lf-level'); |
73 | 79 | 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'); |
74 | 88 |
|
75 | 89 | if (!panel) return; |
76 | 90 |
|
77 | 91 | computeN1Patterns(); |
78 | 92 | populatePluginFilter(); |
| 93 | + populateAssetPluginFilter(); |
79 | 94 | updateBadges(); |
80 | 95 | updateTotalTime(); |
81 | 96 | renderPageContext(); |
82 | 97 | applyFilters(); |
83 | 98 | renderLogs(); |
| 99 | + renderAssets(); |
| 100 | + renderHooks(); |
84 | 101 | renderSummary(); |
85 | 102 | restoreState(); |
86 | 103 | bindEvents(); |
|
171 | 188 | var showFilters = tab === 'db' || tab === 'http'; |
172 | 189 | filterBar.style.display = showFilters ? '' : 'none'; |
173 | 190 | 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'; |
176 | 197 | } |
177 | 198 |
|
178 | 199 | // ── Plugin filter dropdown ──────────────────────────────────────────────── |
|
187 | 208 | }); |
188 | 209 | } |
189 | 210 |
|
| 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">💾</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">🔗</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 | + |
190 | 327 | // ── Badges ──────────────────────────────────────────────────────────────── |
191 | 328 | function updateBadges() { |
192 | 329 | badgeDB.querySelector('em').textContent = meta.query_count || 0; |
|
237 | 374 | function updateTabCounts() { |
238 | 375 | dbCount.textContent = filteredDB.length; |
239 | 376 | 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; |
241 | 383 | } |
242 | 384 |
|
243 | 385 | // ── Multi-column sort ───────────────────────────────────────────────────── |
|
619 | 761 | + (logs.length === 0 ? '<span class="cs-s-ok">✓ No log entries</span>' : '') |
620 | 762 | + '</div></div>'; |
621 | 763 |
|
| 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">⚡ 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 + '">● ' + hitRateStr + ' hit rate</span>' : '') |
| 773 | + + '<span>' + (cache.hits || 0) + ' hits · ' + (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">💾 Assets</div>' |
| 783 | + + '<div class="cs-sum-card-stat">' + (jsCount + cssCount) + '</div>' |
| 784 | + + '<div class="cs-sum-card-sub">' |
| 785 | + + '<span>' + jsCount + ' JS · ' + cssCount + ' CSS</span>' |
| 786 | + + '</div></div>'; |
| 787 | + |
622 | 788 | html += '</div>'; // cards |
623 | 789 |
|
624 | 790 | if (pluginList.length > 0) { |
|
688 | 854 | html += '</div>'; |
689 | 855 | } |
690 | 856 |
|
| 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 + '× · ' + fmtMs(h.total_ms) + ' total · max ' + fmtMs(h.max_ms) + '</span>' |
| 868 | + + '</div>'; |
| 869 | + }); |
| 870 | + html += '</div>'; |
| 871 | + } |
| 872 | + |
691 | 873 | summaryWrap.innerHTML = html; |
692 | 874 | } |
693 | 875 |
|
|
853 | 1035 | }); |
854 | 1036 | } |
855 | 1037 |
|
| 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 ? '↕' : (hookSortDir === 'desc' ? '↓' : '↑'); |
| 1068 | + h.innerHTML = (labels[hCol] || hCol) + ' ' + arrow; |
| 1069 | + }); |
| 1070 | + renderHooks(); |
| 1071 | + }); |
| 1072 | + }); |
| 1073 | + |
856 | 1074 | bindResizeHandle(); |
857 | 1075 | bindSortHeaders(); |
858 | 1076 |
|
|
0 commit comments