|
5 | 5 | * Features: call-chain trace, EXPLAIN on demand, N+1 detection, |
6 | 6 | * multi-column sort, colour-coded severity, export JSON. |
7 | 7 | * |
8 | | - * @since 1.8.0 |
| 8 | + * @since 1.8.6 |
9 | 9 | */ |
10 | 10 | (function () { |
11 | 11 | 'use strict'; |
|
1232 | 1232 | } |
1233 | 1233 | } |
1234 | 1234 |
|
| 1235 | + // ── Copy current tab to clipboard ───────────────────────────────────────── |
| 1236 | + function copyCurrentTab() { |
| 1237 | + var tab = activeTab; |
| 1238 | + var lines = ['=== CS Monitor: ' + tab.toUpperCase() + ' ===', 'URL: ' + (meta.url || window.location.href), '']; |
| 1239 | + |
| 1240 | + switch (tab) { |
| 1241 | + case 'issues': |
| 1242 | + if (issuesList.length === 0) { |
| 1243 | + lines.push('No issues detected.'); |
| 1244 | + } else { |
| 1245 | + issuesList.forEach(function (issue) { |
| 1246 | + lines.push('[' + issue.sev.toUpperCase() + '] ' + issue.title |
| 1247 | + + (issue.detail ? ' — ' + issue.detail : '') |
| 1248 | + + ' (\u2192 ' + issue.tab + ')'); |
| 1249 | + }); |
| 1250 | + } |
| 1251 | + break; |
| 1252 | + case 'db': |
| 1253 | + lines.push('Queries: ' + filteredDB.length + ' / ' + data.queries.length); |
| 1254 | + lines.push(''); |
| 1255 | + filteredDB.forEach(function (q, i) { |
| 1256 | + lines.push((i + 1) + '. [' + q.keyword + '] ' + q.sql.replace(/\s+/g, ' ').trim()); |
| 1257 | + lines.push(' Plugin: ' + q.plugin + ' | Rows: ' + (q.rows >= 0 ? q.rows : '\u2013') + ' | Time: ' + fmtMs(q.time_ms)); |
| 1258 | + if (q.is_dupe) lines.push(' [DUPLICATE]'); |
| 1259 | + if (isN1(q.sql)) lines.push(' [N+1 PATTERN]'); |
| 1260 | + }); |
| 1261 | + break; |
| 1262 | + case 'http': |
| 1263 | + lines.push('HTTP calls: ' + filteredHTTP.length); |
| 1264 | + lines.push(''); |
| 1265 | + filteredHTTP.forEach(function (h, i) { |
| 1266 | + lines.push((i + 1) + '. [' + (h.method || 'GET') + '] ' + h.url); |
| 1267 | + lines.push(' Plugin: ' + h.plugin + ' | Status: ' + (h.status || 'ERR') + ' | Time: ' + fmtMs(h.time_ms)); |
| 1268 | + if (h.error) lines.push(' Error: ' + h.error); |
| 1269 | + }); |
| 1270 | + break; |
| 1271 | + case 'logs': |
| 1272 | + var logs = data.logs || []; |
| 1273 | + lines.push('Log entries: ' + logs.length); |
| 1274 | + lines.push(''); |
| 1275 | + logs.forEach(function (l) { |
| 1276 | + lines.push('[' + (l.level || 'info').toUpperCase() + '] ' + (l.message || '') |
| 1277 | + + (l.file ? ' (' + l.file + (l.line ? ':' + l.line : '') + ')' : '')); |
| 1278 | + }); |
| 1279 | + break; |
| 1280 | + case 'assets': |
| 1281 | + var assets = data.assets || {}; |
| 1282 | + var scripts = assets.scripts || [], styles = assets.styles || []; |
| 1283 | + lines.push('Scripts: ' + scripts.length + ' | Styles: ' + styles.length); |
| 1284 | + lines.push(''); |
| 1285 | + lines.push('--- Scripts ---'); |
| 1286 | + scripts.forEach(function (a) { lines.push(a.handle + ' | ' + a.plugin + ' | ' + (a.src || '')); }); |
| 1287 | + lines.push(''); |
| 1288 | + lines.push('--- Styles ---'); |
| 1289 | + styles.forEach(function (a) { lines.push(a.handle + ' | ' + a.plugin + ' | ' + (a.src || '')); }); |
| 1290 | + break; |
| 1291 | + case 'hooks': |
| 1292 | + var hooks = data.hooks || []; |
| 1293 | + lines.push('Hooks: ' + hooks.length); |
| 1294 | + lines.push(''); |
| 1295 | + hooks.slice(0, 50).forEach(function (h) { |
| 1296 | + lines.push(h.hook + ' | ' + h.count + 'x | ' + fmtMs(h.total_ms) + ' total | max ' + fmtMs(h.max_ms)); |
| 1297 | + }); |
| 1298 | + break; |
| 1299 | + case 'request': |
| 1300 | + var req = data.request || {}; |
| 1301 | + if (req.method) lines.push('Method: ' + req.method); |
| 1302 | + if (req.url) lines.push('Request URL: ' + req.url); |
| 1303 | + if (req.matched_rule) lines.push('Rewrite rule: ' + req.matched_rule); |
| 1304 | + if (req.query_vars && Object.keys(req.query_vars).length) { |
| 1305 | + lines.push('Query vars:'); |
| 1306 | + Object.keys(req.query_vars).forEach(function (k) { lines.push(' ' + k + ': ' + req.query_vars[k]); }); |
| 1307 | + } |
| 1308 | + if (req.get && Object.keys(req.get).length) { |
| 1309 | + lines.push('GET:'); |
| 1310 | + Object.keys(req.get).forEach(function (k) { lines.push(' ' + k + ': ' + req.get[k]); }); |
| 1311 | + } |
| 1312 | + if (req.post && Object.keys(req.post).length) { |
| 1313 | + lines.push('POST:'); |
| 1314 | + Object.keys(req.post).forEach(function (k) { lines.push(' ' + k + ': ' + req.post[k]); }); |
| 1315 | + } |
| 1316 | + if (req.user_roles && req.user_roles.length) lines.push('Roles: ' + req.user_roles.join(', ')); |
| 1317 | + break; |
| 1318 | + case 'template': |
| 1319 | + var tmpl = data.template || {}; |
| 1320 | + lines.push('Active template: ' + (tmpl.final || '(unknown)')); |
| 1321 | + lines.push(''); |
| 1322 | + (tmpl.hierarchy || []).forEach(function (f) { |
| 1323 | + lines.push((f.exists ? '[x] ' : '[ ] ') + f.file); |
| 1324 | + }); |
| 1325 | + break; |
| 1326 | + case 'transients': |
| 1327 | + var trans = data.transients || []; |
| 1328 | + lines.push('Transients: ' + trans.length); |
| 1329 | + lines.push(''); |
| 1330 | + trans.forEach(function (t) { |
| 1331 | + lines.push(t.key + ' | ' + (t.hit ? 'HIT' : 'MISS') |
| 1332 | + + ' | gets: ' + t.gets + ' | sets: ' + t.sets + ' | deletes: ' + t.deletes); |
| 1333 | + }); |
| 1334 | + break; |
| 1335 | + case 'summary': |
| 1336 | + var cQueries = data.queries || [], cHttp = data.http || [], cLogs = data.logs || []; |
| 1337 | + |
| 1338 | + // Environment |
| 1339 | + if (meta.php_version) { |
| 1340 | + lines.push('Environment'); |
| 1341 | + lines.push(' PHP: ' + meta.php_version + ' | WP: ' + (meta.wp_version || '?') + (meta.mysql_version ? ' | MySQL: ' + meta.mysql_version : '')); |
| 1342 | + if (meta.memory_peak_mb) lines.push(' Memory peak: ' + meta.memory_peak_mb + 'MB / ' + (meta.memory_limit || '?')); |
| 1343 | + if (meta.active_theme) lines.push(' Theme: ' + meta.active_theme); |
| 1344 | + if (meta.is_multisite) lines.push(' Multisite: yes'); |
| 1345 | + lines.push(''); |
| 1346 | + } |
| 1347 | + |
| 1348 | + // DB card |
| 1349 | + var cSlowQ = cQueries.filter(function (q) { return q.time_ms >= T_SLOW; }).length; |
| 1350 | + var cCritQ = cQueries.filter(function (q) { return q.time_ms >= T_CRITICAL; }).length; |
| 1351 | + var cDupeQ = cQueries.filter(function (q) { return q.is_dupe; }).length; |
| 1352 | + var cN1Cnt = Object.keys(n1Patterns).length; |
| 1353 | + lines.push('DB Queries: ' + meta.query_count + ' | ' + fmtMs(meta.query_total_ms) + ' total' |
| 1354 | + + (cCritQ ? ' | ' + cCritQ + ' critical' : cSlowQ ? ' | ' + cSlowQ + ' slow' : ' | no slow queries') |
| 1355 | + + (cDupeQ ? ' | ' + cDupeQ + ' dupes' : '') |
| 1356 | + + (cN1Cnt ? ' | ' + cN1Cnt + ' N+1 pattern' + (cN1Cnt > 1 ? 's' : '') : '')); |
| 1357 | + |
| 1358 | + // HTTP card |
| 1359 | + var cSlowH = cHttp.filter(function (h) { return h.time_ms >= T_SLOW; }).length; |
| 1360 | + var cCacH = cHttp.filter(function (h) { return h.cached; }).length; |
| 1361 | + var cErrH = cHttp.filter(function (h) { return !!h.error; }).length; |
| 1362 | + lines.push('HTTP / REST: ' + meta.http_count + ' calls | ' + fmtMs(meta.http_total_ms) + ' total' |
| 1363 | + + (cSlowH ? ' | ' + cSlowH + ' slow' : '') |
| 1364 | + + (cCacH ? ' | ' + cCacH + ' cached' : '') |
| 1365 | + + (cErrH ? ' | ' + cErrH + ' errors' : '') |
| 1366 | + + (cHttp.length === 0 ? ' | no outbound calls' : '')); |
| 1367 | + |
| 1368 | + // Logs card |
| 1369 | + var cErrL = cLogs.filter(function (e) { return (e.level || '').toLowerCase().indexOf('error') !== -1; }).length; |
| 1370 | + var cWarnL = cLogs.filter(function (e) { return (e.level || '').toLowerCase().indexOf('warn') !== -1; }).length; |
| 1371 | + var cDepL = cLogs.filter(function (e) { return (e.level || '').toLowerCase().indexOf('dep') !== -1; }).length; |
| 1372 | + lines.push('Logs: ' + cLogs.length |
| 1373 | + + (cErrL ? ' | ' + cErrL + ' errors' : '') |
| 1374 | + + (cWarnL ? ' | ' + cWarnL + ' warnings' : '') |
| 1375 | + + (cDepL ? ' | ' + cDepL + ' deprecated' : '') |
| 1376 | + + (cLogs.length === 0 ? ' | none' : '')); |
| 1377 | + |
| 1378 | + // Cache card |
| 1379 | + var cCache = data.cache || {}; |
| 1380 | + if (cCache.available) { |
| 1381 | + var cHitStr = cCache.hit_rate !== null ? cCache.hit_rate + '%' : 'n/a'; |
| 1382 | + lines.push('Object Cache: ' + cHitStr + ' hit rate | ' + (cCache.hits || 0) + ' hits, ' + (cCache.misses || 0) + ' misses' |
| 1383 | + + (cCache.persistent ? ' | persistent' : ' | non-persistent')); |
| 1384 | + } |
| 1385 | + |
| 1386 | + // Assets card |
| 1387 | + var cAssets = data.assets || {}; |
| 1388 | + lines.push('Assets: ' + ((cAssets.scripts || []).length + (cAssets.styles || []).length) |
| 1389 | + + ' | ' + (cAssets.scripts || []).length + ' JS, ' + (cAssets.styles || []).length + ' CSS'); |
| 1390 | + |
| 1391 | + lines.push(''); |
| 1392 | + |
| 1393 | + // Plugin leaderboard |
| 1394 | + var cByP = {}; |
| 1395 | + cQueries.forEach(function (q) { |
| 1396 | + if (!cByP[q.plugin]) cByP[q.plugin] = { count: 0, total_ms: 0, slow: 0, n1: 0 }; |
| 1397 | + cByP[q.plugin].count++; cByP[q.plugin].total_ms += q.time_ms; |
| 1398 | + if (q.time_ms >= T_SLOW) cByP[q.plugin].slow++; |
| 1399 | + if (isN1(q.sql)) cByP[q.plugin].n1++; |
| 1400 | + }); |
| 1401 | + var cPluginList = Object.keys(cByP).map(function (p) { |
| 1402 | + return { plugin: p, count: cByP[p].count, total_ms: cByP[p].total_ms, slow: cByP[p].slow, n1: cByP[p].n1 }; |
| 1403 | + }).sort(function (a, b) { return b.total_ms - a.total_ms; }); |
| 1404 | + if (cPluginList.length > 0) { |
| 1405 | + lines.push('Plugin Leaderboard — DB query time'); |
| 1406 | + cPluginList.slice(0, 8).forEach(function (p, i) { |
| 1407 | + lines.push(' ' + (i + 1) + '. ' + p.plugin + ' \u2014 ' + p.count + ' queries, ' + fmtMs(p.total_ms) |
| 1408 | + + (p.slow ? ', ' + p.slow + ' slow' : '') |
| 1409 | + + (p.n1 ? ', ' + p.n1 + ' N+1' : '')); |
| 1410 | + }); |
| 1411 | + lines.push(''); |
| 1412 | + } |
| 1413 | + |
| 1414 | + // Slowest queries (top 5) |
| 1415 | + var cTop5Q = cQueries.slice().sort(function (a, b) { return b.time_ms - a.time_ms; }).slice(0, 5); |
| 1416 | + if (cTop5Q.length > 0) { |
| 1417 | + lines.push('Slowest Queries'); |
| 1418 | + cTop5Q.forEach(function (q) { |
| 1419 | + lines.push(' ' + fmtMs(q.time_ms) + ' ' + q.sql.replace(/\s+/g, ' ').trim().slice(0, 100) + ' [' + q.plugin + ']'); |
| 1420 | + }); |
| 1421 | + lines.push(''); |
| 1422 | + } |
| 1423 | + |
| 1424 | + // N+1 patterns |
| 1425 | + var cN1List = Object.values(n1Patterns).sort(function (a, b) { return b.count - a.count; }); |
| 1426 | + if (cN1List.length > 0) { |
| 1427 | + lines.push('N+1 Query Patterns'); |
| 1428 | + cN1List.forEach(function (p) { |
| 1429 | + lines.push(' x' + p.count + ' ' + normalisePattern(p.example).slice(0, 100) + ' [' + p.plugin + ']'); |
| 1430 | + }); |
| 1431 | + lines.push(''); |
| 1432 | + } |
| 1433 | + |
| 1434 | + // Slowest HTTP (top 5) |
| 1435 | + var cTop5H = cHttp.slice().sort(function (a, b) { return b.time_ms - a.time_ms; }).slice(0, 5); |
| 1436 | + if (cTop5H.length > 0) { |
| 1437 | + lines.push('Slowest HTTP Calls'); |
| 1438 | + cTop5H.forEach(function (h) { |
| 1439 | + lines.push(' ' + fmtMs(h.time_ms) + ' [' + (h.method || 'GET') + '] ' + (h.url || '').slice(0, 100) + ' [' + h.plugin + ']'); |
| 1440 | + }); |
| 1441 | + lines.push(''); |
| 1442 | + } |
| 1443 | + |
| 1444 | + // Duplicate queries |
| 1445 | + var cDupeGroups = {}; |
| 1446 | + cQueries.forEach(function (q) { |
| 1447 | + var fp = q.sql.replace(/\s+/g, ' ').toLowerCase().trim(); |
| 1448 | + if (!cDupeGroups[fp]) cDupeGroups[fp] = { sql: q.sql, count: 0, total_ms: 0 }; |
| 1449 | + cDupeGroups[fp].count++; cDupeGroups[fp].total_ms += q.time_ms; |
| 1450 | + }); |
| 1451 | + var cDupeList = Object.values(cDupeGroups).filter(function (g) { return g.count > 1; }) |
| 1452 | + .sort(function (a, b) { return b.count - a.count; }).slice(0, 8); |
| 1453 | + if (cDupeList.length > 0) { |
| 1454 | + lines.push('Exact Duplicate Queries (' + cDupeList.length + ' groups)'); |
| 1455 | + cDupeList.forEach(function (g) { |
| 1456 | + lines.push(' x' + g.count + ' ' + g.sql.replace(/\s+/g, ' ').trim().slice(0, 100) |
| 1457 | + + ' (' + fmtMs(g.count > 0 ? g.total_ms / g.count : 0) + ' avg)'); |
| 1458 | + }); |
| 1459 | + lines.push(''); |
| 1460 | + } |
| 1461 | + |
| 1462 | + // Slowest hooks (top 8) |
| 1463 | + var cTopHooks = (data.hooks || []).slice(0, 8); |
| 1464 | + if (cTopHooks.length > 0) { |
| 1465 | + lines.push('Slowest Hooks'); |
| 1466 | + cTopHooks.forEach(function (h) { |
| 1467 | + lines.push(' ' + h.hook + ' | ' + h.count + 'x | ' + fmtMs(h.total_ms) + ' total | max ' + fmtMs(h.max_ms)); |
| 1468 | + }); |
| 1469 | + } |
| 1470 | + break; |
| 1471 | + } |
| 1472 | + |
| 1473 | + var text = lines.join('\n'); |
| 1474 | + var copyBtn = document.getElementById('cs-perf-copy'); |
| 1475 | + if (navigator.clipboard && navigator.clipboard.writeText) { |
| 1476 | + navigator.clipboard.writeText(text).then(function () { |
| 1477 | + flashCopyBtn(copyBtn, 'Copied!'); |
| 1478 | + }).catch(function () { fallbackCopy(text, copyBtn); }); |
| 1479 | + } else { |
| 1480 | + fallbackCopy(text, copyBtn); |
| 1481 | + } |
| 1482 | + } |
| 1483 | + |
| 1484 | + function fallbackCopy(text, btn) { |
| 1485 | + var ta = document.createElement('textarea'); |
| 1486 | + ta.value = text; |
| 1487 | + ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0'; |
| 1488 | + document.body.appendChild(ta); |
| 1489 | + ta.focus(); ta.select(); |
| 1490 | + try { document.execCommand('copy'); flashCopyBtn(btn, 'Copied!'); } |
| 1491 | + catch (e) { flashCopyBtn(btn, 'Failed'); } |
| 1492 | + document.body.removeChild(ta); |
| 1493 | + } |
| 1494 | + |
| 1495 | + function flashCopyBtn(btn, msg) { |
| 1496 | + if (!btn) return; |
| 1497 | + var orig = btn.textContent; |
| 1498 | + btn.textContent = msg; |
| 1499 | + setTimeout(function () { btn.textContent = orig; }, 1500); |
| 1500 | + } |
| 1501 | + |
1235 | 1502 | // ── Resize ──────────────────────────────────────────────────────────────── |
1236 | 1503 | function bindResizeHandle() { |
1237 | 1504 | var startY, startH; |
|
1287 | 1554 | togglePanel(); |
1288 | 1555 | }); |
1289 | 1556 | if (exportBtn) exportBtn.addEventListener('click', function (e) { e.stopPropagation(); exportJSON(); }); |
| 1557 | + var copyBtn = document.getElementById('cs-perf-copy'); |
| 1558 | + if (copyBtn) copyBtn.addEventListener('click', function (e) { e.stopPropagation(); copyCurrentTab(); }); |
1290 | 1559 |
|
1291 | 1560 | var helpBtn = document.getElementById('cs-perf-help-btn'); |
1292 | 1561 | var helpPanel = document.getElementById('cs-perf-help'); |
|
1409 | 1678 | if (e.ctrlKey && e.shiftKey && (e.key === 'm' || e.key === 'M')) { |
1410 | 1679 | e.preventDefault(); togglePanel(); |
1411 | 1680 | } |
| 1681 | + if (e.key === 'Escape') { |
| 1682 | + // Close help panel first if open |
| 1683 | + var helpPanelEl = document.getElementById('cs-perf-help'); |
| 1684 | + if (helpPanelEl && helpPanelEl.style.display !== 'none') { |
| 1685 | + helpPanelEl.style.display = 'none'; |
| 1686 | + return; |
| 1687 | + } |
| 1688 | + // Collapse any open EXPLAIN result divs and detail rows |
| 1689 | + var hadOpen = false; |
| 1690 | + Array.prototype.forEach.call(document.querySelectorAll('.cs-explain-result'), function (r) { |
| 1691 | + if (r.innerHTML) { r.innerHTML = ''; hadOpen = true; } |
| 1692 | + }); |
| 1693 | + Array.prototype.forEach.call(document.querySelectorAll('.cs-row-detail'), function (d) { |
| 1694 | + if (d.style.display !== 'none') { d.style.display = 'none'; hadOpen = true; } |
| 1695 | + }); |
| 1696 | + // Reset any disabled EXPLAIN buttons |
| 1697 | + if (hadOpen) { |
| 1698 | + Array.prototype.forEach.call(document.querySelectorAll('.cs-explain-btn'), function (btn) { |
| 1699 | + btn.disabled = false; btn.textContent = 'EXPLAIN'; |
| 1700 | + }); |
| 1701 | + } |
| 1702 | + } |
1412 | 1703 | }); |
1413 | 1704 | } |
1414 | 1705 |
|
|
0 commit comments