mediawiki.inspect.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. /*!
  2. * The mediawiki.inspect module.
  3. *
  4. * @author Ori Livneh
  5. * @since 1.22
  6. */
  7. /* eslint-disable no-console */
  8. ( function () {
  9. // mw.inspect is a singleton class with static methods
  10. // that itself can also be invoked as a function (mediawiki.base/mw#inspect).
  11. // In JavaScript, that is implemented by starting with a function,
  12. // and subsequently setting additional properties on the function object.
  13. /**
  14. * @classdesc Tools for inspecting page composition and performance.
  15. *
  16. * @class mediawiki.inspect
  17. * @hideconstructor
  18. */
  19. var inspect = mw.inspect,
  20. byteLength = require( 'mediawiki.String' ).byteLength,
  21. hasOwn = Object.prototype.hasOwnProperty;
  22. function sortByProperty( array, prop, descending ) {
  23. var order = descending ? -1 : 1;
  24. return array.sort( function ( a, b ) {
  25. if ( a[ prop ] === undefined || b[ prop ] === undefined ) {
  26. // Sort undefined to the end, regardless of direction
  27. return a[ prop ] !== undefined ? -1 : b[ prop ] !== undefined ? 1 : 0;
  28. }
  29. return a[ prop ] > b[ prop ] ? order : a[ prop ] < b[ prop ] ? -order : 0;
  30. } );
  31. }
  32. function humanSize( bytesInput ) {
  33. var i,
  34. bytes = +bytesInput,
  35. units = [ '', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB' ];
  36. if ( bytes === 0 || isNaN( bytes ) ) {
  37. return bytesInput;
  38. }
  39. for ( i = 0; bytes >= 1024; bytes /= 1024 ) {
  40. i++;
  41. }
  42. // Maintain one decimal for KiB and above, but don't
  43. // add ".0" for bytes.
  44. return bytes.toFixed( i > 0 ? 1 : 0 ) + units[ i ];
  45. }
  46. /**
  47. * Return a map of all dependency relationships between loaded modules.
  48. *
  49. * @return {Object} Maps module names to objects. Each sub-object has
  50. * two properties, 'requires' and 'requiredBy'.
  51. * @memberof mediawiki.inspect
  52. * @method mediawiki.inspect.getDependencyGraph
  53. */
  54. inspect.getDependencyGraph = function () {
  55. var modules = inspect.getLoadedModules(),
  56. graph = {};
  57. modules.forEach( function ( moduleName ) {
  58. var dependencies = mw.loader.moduleRegistry[ moduleName ].dependencies || [];
  59. if ( !hasOwn.call( graph, moduleName ) ) {
  60. graph[ moduleName ] = { requiredBy: [] };
  61. }
  62. graph[ moduleName ].requires = dependencies;
  63. dependencies.forEach( function ( depName ) {
  64. if ( !hasOwn.call( graph, depName ) ) {
  65. graph[ depName ] = { requiredBy: [] };
  66. }
  67. graph[ depName ].requiredBy.push( moduleName );
  68. } );
  69. } );
  70. return graph;
  71. };
  72. /**
  73. * Calculate the byte size of a ResourceLoader module.
  74. *
  75. * @param {string} moduleName The name of the module
  76. * @return {number|null} Module size in bytes or null
  77. * @memberof mediawiki.inspect
  78. * @method mediawiki.inspect.getModuleSize
  79. */
  80. inspect.getModuleSize = function ( moduleName ) {
  81. // We typically receive them from the server through batches from load.php,
  82. // or embedded as inline scripts (handled in PHP by ResourceLoader::makeModuleResponse
  83. // and ResourceLoader\ClientHtml respectively).
  84. //
  85. // The module declarator function is stored by mw.loader.implement(), allowing easy
  86. // computation of the exact size.
  87. var module = mw.loader.moduleRegistry[ moduleName ];
  88. if ( module.state !== 'ready' ) {
  89. return null;
  90. }
  91. if ( !module.declarator ) {
  92. return 0;
  93. }
  94. return byteLength( module.declarator.toString() );
  95. };
  96. /**
  97. * Given CSS source, count both the total number of selectors it
  98. * contains and the number which match some element in the current
  99. * document.
  100. *
  101. * @param {string} css CSS source
  102. * @return {Object} Selector counts
  103. * @return {number} return.selectors Total number of selectors
  104. * @return {number} return.matched Number of matched selectors
  105. * @memberof mediawiki.inspect
  106. * @method mediawiki.inspect.auditSelectors
  107. */
  108. inspect.auditSelectors = function ( css ) {
  109. var selectors = { total: 0, matched: 0 },
  110. style = document.createElement( 'style' );
  111. style.textContent = css;
  112. document.body.appendChild( style );
  113. var cssRules = style.sheet.cssRules;
  114. for ( var index in cssRules ) {
  115. const rule = cssRules[ index ];
  116. selectors.total++;
  117. // document.querySelector() on prefixed pseudo-elements can throw exceptions
  118. // in Firefox and Safari. Ignore these exceptions.
  119. // https://bugs.webkit.org/show_bug.cgi?id=149160
  120. // https://bugzilla.mozilla.org/show_bug.cgi?id=1204880
  121. try {
  122. if ( document.querySelector( rule.selectorText ) !== null ) {
  123. selectors.matched++;
  124. }
  125. } catch ( e ) {}
  126. }
  127. document.body.removeChild( style );
  128. return selectors;
  129. };
  130. /**
  131. * Get a list of all loaded ResourceLoader modules.
  132. *
  133. * @return {Array} List of module names
  134. * @memberof mediawiki.inspect
  135. * @method mediawiki.inspect.getLoadedModules
  136. */
  137. inspect.getLoadedModules = function () {
  138. return mw.loader.getModuleNames().filter( function ( module ) {
  139. return mw.loader.getState( module ) === 'ready';
  140. } );
  141. };
  142. /**
  143. * Print tabular data to the console using console.table.
  144. *
  145. * @param {Array} data Tabular data represented as an array of objects
  146. * with common properties.
  147. * @memberof mediawiki.inspect
  148. * @method mediawiki.inspect.dumpTable
  149. */
  150. inspect.dumpTable = console.table;
  151. /**
  152. * Generate and print reports.
  153. *
  154. * When invoked without arguments, prints all available reports.
  155. *
  156. * @param {...string} [reports] One or more of "size", "css", "store", or "time".
  157. * @memberof mediawiki.inspect
  158. * @method mediawiki.inspect.runReports
  159. */
  160. inspect.runReports = function () {
  161. var reports = arguments.length > 0 ?
  162. Array.prototype.slice.call( arguments ) :
  163. Object.keys( inspect.reports );
  164. reports.forEach( function ( name ) {
  165. if ( console.group ) {
  166. console.group( 'mw.inspect ' + name + ' report' );
  167. } else {
  168. console.log( 'mw.inspect ' + name + ' report' );
  169. }
  170. inspect.dumpTable( inspect.reports[ name ]() );
  171. if ( console.group ) {
  172. console.groupEnd( 'mw.inspect ' + name + ' report' );
  173. }
  174. } );
  175. };
  176. /**
  177. * Perform a string search across the JavaScript and CSS source code
  178. * of all loaded modules and return an array of the names of the
  179. * modules that matched.
  180. *
  181. * @param {string|RegExp} pattern String or regexp to match.
  182. * @return {Array} Array of the names of modules that matched.
  183. * @memberof mediawiki.inspect
  184. * @method mediawiki.inspect.grep
  185. */
  186. inspect.grep = function ( pattern ) {
  187. if ( typeof pattern.test !== 'function' ) {
  188. // eslint-disable-next-line security/detect-non-literal-regexp
  189. pattern = new RegExp( mw.util.escapeRegExp( pattern ), 'g' );
  190. }
  191. return inspect.getLoadedModules().filter( function ( moduleName ) {
  192. var module = mw.loader.moduleRegistry[ moduleName ];
  193. // Grep module's JavaScript
  194. if ( typeof module.script === 'function' && pattern.test( module.script.toString() ) ) {
  195. return true;
  196. }
  197. // Grep module's CSS
  198. if (
  199. $.isPlainObject( module.style ) && Array.isArray( module.style.css ) &&
  200. pattern.test( module.style.css.join( '' ) )
  201. ) {
  202. // Module's CSS source matches
  203. return true;
  204. }
  205. return false;
  206. } );
  207. };
  208. /**
  209. * @private
  210. * @class mw.inspect.reports
  211. * @singleton
  212. */
  213. inspect.reports = {
  214. /**
  215. * Generate a breakdown of all loaded modules and their size in
  216. * kibibytes. Modules are ordered from largest to smallest.
  217. *
  218. * @return {Object[]} Size reports
  219. */
  220. size: function () {
  221. // Map each module to a descriptor object.
  222. var modules = inspect.getLoadedModules().map( function ( module ) {
  223. return {
  224. name: module,
  225. size: inspect.getModuleSize( module )
  226. };
  227. } );
  228. // Sort module descriptors by size, largest first.
  229. sortByProperty( modules, 'size', true );
  230. // Convert size to human-readable string.
  231. modules.forEach( function ( module ) {
  232. module.sizeInBytes = module.size;
  233. module.size = humanSize( module.size );
  234. } );
  235. return modules;
  236. },
  237. /**
  238. * For each module with styles, count the number of selectors, and
  239. * count how many match against some element currently in the DOM.
  240. *
  241. * @return {Object[]} CSS reports
  242. */
  243. css: function () {
  244. var modules = [];
  245. inspect.getLoadedModules().forEach( function ( name ) {
  246. var css, stats, module = mw.loader.moduleRegistry[ name ];
  247. try {
  248. css = module.style.css.join();
  249. } catch ( e ) {
  250. // skip
  251. return;
  252. }
  253. stats = inspect.auditSelectors( css );
  254. modules.push( {
  255. module: name,
  256. allSelectors: stats.total,
  257. matchedSelectors: stats.matched,
  258. percentMatched: stats.total !== 0 ?
  259. ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
  260. } );
  261. } );
  262. sortByProperty( modules, 'allSelectors', true );
  263. return modules;
  264. },
  265. /**
  266. * Report stats on mw.loader.store: the number of localStorage
  267. * cache hits and misses, the number of items purged from the
  268. * cache, and the total size of the module blob in localStorage.
  269. *
  270. * @return {Object[]} Store stats
  271. */
  272. store: function () {
  273. var raw, stats = { enabled: mw.loader.store.enabled };
  274. if ( stats.enabled ) {
  275. $.extend( stats, mw.loader.store.stats );
  276. try {
  277. raw = localStorage.getItem( mw.loader.store.key );
  278. stats.totalSizeInBytes = byteLength( raw );
  279. stats.totalSize = humanSize( byteLength( raw ) );
  280. } catch ( e ) {}
  281. }
  282. return [ stats ];
  283. },
  284. /**
  285. * Generate a breakdown of all loaded modules and their time
  286. * spent during initialisation (measured in milliseconds).
  287. *
  288. * This timing data is collected by mw.loader.profiler.
  289. *
  290. * @return {Object[]} Table rows
  291. */
  292. time: function () {
  293. var modules;
  294. if ( !mw.loader.profiler ) {
  295. mw.log.warn( 'mw.inspect: The time report requires $wgResourceLoaderEnableJSProfiler.' );
  296. return [];
  297. }
  298. modules = inspect.getLoadedModules()
  299. .map( function ( moduleName ) {
  300. return mw.loader.profiler.getProfile( moduleName );
  301. } )
  302. .filter( function ( perf ) {
  303. // Exclude modules that reached "ready" state without involvement from mw.loader.
  304. // This is primarily styles-only as loaded via <link rel="stylesheet">.
  305. return perf !== null;
  306. } );
  307. // Sort by total time spent, highest first.
  308. sortByProperty( modules, 'total', true );
  309. // Add human-readable strings
  310. modules.forEach( function ( module ) {
  311. module.totalInMs = module.total;
  312. module.total = module.totalInMs.toLocaleString() + ' ms';
  313. } );
  314. return modules;
  315. }
  316. };
  317. if ( mw.config.get( 'debug' ) ) {
  318. mw.log( 'mw.inspect: reports are not available in debug mode.' );
  319. }
  320. }() );