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. * * == How to install == * * Add the following to your [[m:Special:Mypage/global.js]]: // [[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', 7: 'magru' }; 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, #vector-variants-dropdown.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; var expires = new Date(ppr14.slice(0, 4) + '-' + ppr14.slice(4, 6) + '-' + ppr14.slice(6, 8) + 'T' + ppr14.slice(8, 10) + ':' + ppr14.slice(10, 12) + ':' + ppr14.slice(12, 14) + 'Z'); expires.setSeconds(expires.getSeconds() + ppr.cachereport.ttl); expires = expires.toISOString().replace('\.000', ''); 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)'); pcParts.push(' ttl: ' + ppr.cachereport.ttl); pcParts.push(' expires: ' + expires); } 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)' ); pcParts.push(' (cached) ttl: ' + ppr.cachereport.ttl); pcParts.push(' (cached) expires: ' + expires); } } } 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(); });