User:Krinkle/Scripts/Perf.js
< User:Krinkle | Scripts
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* Perf utils by Krinkle
*
* This creates a "Perf" portlet menu, and defines mw.loader.findAll() for ad-hoc use via the browser console.
*
* Usage:
*
// [[File:Krinkle_Perf.js]]
mw.loader.load('https://meta.wikimedia.org/w/index.php?title=User:Krinkle/Scripts/Perf.js&action=raw&ctype=text/javascript');
*
* @version 2023-09-19
* @source https://meta.wikimedia.org/wiki/User:Krinkle/Scripts/Perf.js
* @author Timo Tijhof
*/
var domReady = new Promise(function (resolve) { document.readyState === 'complete' ? setTimeout(resolve) : document.onreadystatechange = setTimeout.bind(null, resolve); });
domReady.then(function () {
var prevPortlet;
var perfMenu;
var nt;
var ppr;
var ppr14;
var pprInt14;
var nowInt14;
// https://wikitech.wikimedia.org/wiki/SRE/Infrastructure_naming_conventions#Servers
var dcNumMap = { 1: 'eqiad', 2: 'codfw', 3: 'esams', 4: 'ulsfo', 5: 'eqsin', 6: 'drmrs' };
var browserResp = 'unknown';
var ttfb = null;
var respParts = [];
var cdnCacheStatus = 'unknown';
var cdnHost = 'unknown';
var beParts = [];
var beResp = 0;
var pcParts = [];
var pcResp = 0;
function addItem(texts) {
if (perfMenu) {
var item = document.createElement('li');
item.className = 'perf-textonly';
if (Array.isArray(texts)) {
texts.forEach(function (text, i) {
if (text === null) {
return;
}
if (i !== 0) {
item.append(document.createElement('br'));
}
item.append(text);
});
} else {
item.textContent = texts;
}
perfMenu.append(item);
}
}
function conf(key) {
return window.mw && window.mw.config && window.mw.config.get(key);
}
function tfmt(ms) {
// use milliseconds upto 9000 ms, then use seconds
return ms > 9000 ? (ms / 1000).toFixed(3) + ' s' : Math.round(ms).toLocaleString() + '\u00A0' + 'ms';
}
function percent(total, sub) {
var ratio = (sub / total) * 100;
return Math.floor(ratio) + '%';
}
// Can't use mw.util.addPortlet() since it's not compatible with anything other sidebar portlets outside Vector22 skin.
// Vector skin
prevPortlet = document.querySelector('nav#p-variants.vector-menu');
if (prevPortlet) {
prevPortlet.insertAdjacentHTML('afterend', ''
+ '<style>#p-perf li.perf-textonly {'
+ ' padding: 0.42em 0.625em;'
+ ' line-height: 1.4;'
+ ' font-size: 0.8125em;'
+ ' white-space: nowrap;'
+ '}</style>'
+ '<nav id="p-perf" class="mw-portlet vector-menu vector-menu-dropdown vector-menu-dropdown-noicon" aria-labelledby="p-perf-label" role="navigation">'
+ '<input type="checkbox" class="vector-menu-checkbox" aria-labelledby="p-perf-label"><label class="vector-menu-heading" id="p-perf-label"><span>⏱</span></label>'
+ '<div class="vector-menu-content"><ul class="vector-menu-content-list"></ul></div></nav>'
);
}
// Vector 2022 skin
// * Workaround bug in vector-2022 where menus like #p-variants are twice in the DOM (ID is not unique???)
// * Fix bug in vector-2022 where tall characters in portlet label cause a jarring change in toolbar height
// * Avoid "white-space:nowrap" on items because menus have a fixed max-width and no handling for overflow (inaccessible text).
prevPortlet = document.querySelector('#p-variants.vector-dropdown');
if (prevPortlet) {
prevPortlet.insertAdjacentHTML('afterend', `
<style>
#p-perf label {
line-height: 0.9;
}
#p-perf li.perf-textonly {
padding: 0.42em 0.625em;
line-height: 1.4;
font-size: 0.8125em;
}
#p-perf .vector-dropdown-content {
max-width: 250px;
}
</style>
<div id="p-perf" class="vector-dropdown" role="navigation">
<input id="p-perf-checkbox" type="checkbox" class="vector-dropdown-checkbox" aria-labelledby="p-perf-label"><label class="vector-dropdown-label cdx-button" for="p-perf-checkbox" id="p-perf-label"><span>⏱</span></label>
<div class="vector-dropdown-content vector-menu"><ul class="vector-menu-content-list"></ul></div></div>
`);
}
// Monobook skin
prevPortlet = document.querySelector('#p-tb.portlet');
if (prevPortlet) {
prevPortlet.insertAdjacentHTML('afterend', ''
+ '<div role="navigation" class="portlet" id="p-perf" aria-labelledby="p-perf-label">'
+ '<h3 id="p-perf-label" dir="ltr" lang="en">⏱ Performance</h3>'
+ '<div class="pBody"><ul dir="ltr" lang="en"></ul></div></div>'
);
}
// Timeless skin
prevPortlet = document.querySelector('#p-pagemisc.mw-portlet');
if (prevPortlet) {
prevPortlet.insertAdjacentHTML('afterend', ''
+ '<style>#p-perf ul { color: #555; line-height: 1; font-size: 85%; }</style>'
+ '<div role="navigation" class="mw-portlet" id="p-perf" aria-labelledby="p-perf-label">'
+ '<h3 id="p-perf-label" dir="ltr" lang="en">⏱ Performance</h3>'
+ '<div class="mw-portlet-body"><ul dir="ltr" lang="en"></ul></div></div>'
);
}
// Minerva skin
prevPortlet = document.querySelector('footer.minerva-footer');
if (prevPortlet) {
prevPortlet.insertAdjacentHTML('beforeend', ''
// Use hlist for font style, but use separate lines
+ '<style>#p-perf {'
+ 'margin-top: 1rem;'
+ 'overflow: visible;'
+ '}'
+ '#p-perf li {'
+ 'display: list-item;'
+ 'list-style: circle outside;'
+ 'margin-left: 0.5rem;'
+ '}</style>'
+ '<div id="p-perf" class="post-content footer-content">'
+ '<h2>⏱ Performance</h2>'
+ '<ul class="hlist"></ul></div>');
}
perfMenu = document.querySelector('#p-perf ul');
try {
// Navigation Timing API
nt = performance.getEntriesByType('navigation')[0];
ttfb = nt.responseStart;
// Resource Timing API
if (nt.transferSize === 0) {
browserResp = 'local cache (no network)';
} else if (nt.transferSize > 0 && nt.encodedBodySize > 0 && nt.transferSize < nt.encodedBodySize ) {
browserResp = 'local cache (after HTTP 304)';
} else {
browserResp = 'fresh HTTP 200';
}
respParts.push('Response time: ' + tfmt(ttfb));
// Server Timing API
if (nt.serverTiming[0].name === 'cache') {
// One of "hit", "hit-front", "miss" or "pass"
cdnCacheStatus = nt.serverTiming[0].description;
}
if (nt.serverTiming[1].name === 'host') {
// e.g. cp0000
cdnHost = nt.serverTiming[1].description;
// match will yield null or ['1'], both of which can cast nicely to a string key in dcNumMap
// this avoids complexity around conditionally reading matchResult[0]
cdnHost = cdnHost + '.' + (dcNumMap[cdnHost.match(/\d/)] || 'unknown') + '.wmnet';
}
// MediaWiki-specific: config on all HTML responses
beResp = (cdnCacheStatus.includes('hit') || browserResp.includes('cache')) ? 0 : conf('wgBackendResponseTime');
if (beResp) {
beParts.push('MediaWiki backend: ' + conf('wgHostname'));
} else {
beParts.push('MediaWiki backend: (cache hit)');
beParts.push('(cached) host: ' + conf('wgHostname'));
// Only show dedicated entry here if cached (and thus not shown in respParts)
beParts.push('(cached) duration: ' + tfmt(conf('wgBackendResponseTime')));
}
respParts.push(
'• ' + percent(ttfb, ttfb - beResp) + ' 🌐 Internet connection: ' + tfmt(ttfb - beResp),
Object.assign(document.createElement('small'), { textContent: '\u00A0\u00A0 (time between browser and CDN)' })
);
if (beResp) {
respParts.push('• ' + percent(ttfb, beResp) + ' 🌻 MediaWiki backend: ' + tfmt(beResp));
}
// MediaWiki-specific: on when action=view, on a page that exists, is local, and has wikitext content.
ppr = conf('wgPageParseReport');
if (ppr.cachereport && ppr.limitreport) {
ppr14 = ppr.cachereport.timestamp;
// This is the timestamp after parsing is done when it is about to saved.
// Therefore, below we don't need to account for parse time itself.
pprInt14 = Number(ppr14);
// "2020-10-18T23:50:34.799Z" -> 20201018235034
nowInt14 = Number(new Date(performance.timeOrigin).toISOString().replace(/([-T:]|\..*$)/g, ''));
// Assume cache reuse, unless same host and under 5 seconds ago.
pcResp = (ppr.cachereport.origin === conf('wgHostname') && (nowInt14 - pprInt14) < 5) ? (ppr.limitreport.walltime * 1000) : 0;
if (pcResp) {
respParts.push('\u00A0\u00A0• ' + percent(ttfb, pcResp) + ' 🧮 MediaWiki parser: ' + tfmt(pcResp));
respParts.push('\u00A0\u00A0• ' + percent(ttfb, beResp - pcResp) + ' 🖼 MediaWiki skin: ' + tfmt(beResp - pcResp));
pcParts.push('MediaWiki parser: miss (freshly parsed)');
} else {
respParts.push('\u00A0\u00A0• ' + percent(ttfb, pcResp) + ' 🧮 MediaWiki parser: ' + tfmt(pcResp));
respParts.push('\u00A0\u00A0• ' + percent(ttfb, beResp - pcResp) + ' 🖼 MediaWiki skin: ' + tfmt(beResp - pcResp));
pcParts.push('MediaWiki parser: (cache hit)');
pcParts.push(' (cached) host: ' + ppr.cachereport.origin);
// Only show dedicated entry here if cached (and thus not shown in respParts)
pcParts.push(' (cached) duration: ' + tfmt(ppr.limitreport.walltime * 1000));
// Only show if cached, otherwise uninteresting
pcParts.push(' (cached) timestamp: ' + ppr14.slice(0, 4) + '-' + ppr14.slice(4, 6) + '-' + ppr14.slice(6, 8) +
' ' + ppr14.slice(8, 10) + ':' + ppr14.slice(10, 12) + ':' + ppr14.slice(12, 14) + ' (UTC)'
);
}
}
} catch (e) {
// Ignored
}
addItem('Response: ' + browserResp);
addItem(respParts);
addItem(['CDN response:', '• status: ' + cdnCacheStatus, '• host: ' + cdnHost]);
addItem(beParts);
addItem(pcParts);
mw.hook('krinkle.perf-menu').fire();
});