Merge "mediawiki.inspect: add CSS report"
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.inspect.js
1 /*!
2 * Tools for inspecting page composition and performance.
3 *
4 * @author Ori Livneh
5 * @since 1.22
6 */
7 /*jshint devel:true */
8 ( function ( mw, $ ) {
9
10 function sortByProperty( array, prop, descending ) {
11 var order = descending ? -1 : 1;
12 return array.sort( function ( a, b ) {
13 return a[prop] > b[prop] ? order : a[prop] < b[prop] ? -order : 0;
14 } );
15 }
16
17 /**
18 * @class mw.inspect
19 * @singleton
20 */
21 var inspect = {
22
23 /**
24 * Calculate the byte size of a ResourceLoader module.
25 *
26 * @param {string} moduleName The name of the module
27 * @return {number|null} Module size in bytes or null
28 */
29 getModuleSize: function ( moduleName ) {
30 var module = mw.loader.moduleRegistry[ moduleName ],
31 payload = 0;
32
33 if ( mw.loader.getState( moduleName ) !== 'ready' ) {
34 return null;
35 }
36
37 if ( !module.style && !module.script ) {
38 return null;
39 }
40
41 // Tally CSS
42 if ( module.style && $.isArray( module.style.css ) ) {
43 $.each( module.style.css, function ( i, stylesheet ) {
44 payload += $.byteLength( stylesheet );
45 } );
46 }
47
48 // Tally JavaScript
49 if ( $.isFunction( module.script ) ) {
50 payload += $.byteLength( module.script.toString() );
51 }
52
53 return payload;
54 },
55
56 /**
57 * Given CSS source, count both the total number of selectors it
58 * contains and the number which match some element in the current
59 * document.
60 *
61 * @param {string} css CSS source
62 * @return Selector counts
63 * @return {number} return.selectors Total number of selectors
64 * @return {number} return.matched Number of matched selectors
65 */
66 auditSelectors: function ( css ) {
67 var selectors = { total: 0, matched: 0 },
68 style = document.createElement( 'style' );
69
70 style.textContent = css;
71 document.body.appendChild( style );
72 $.each( style.sheet.cssRules, function ( index, rule ) {
73 selectors.total++;
74 if ( document.querySelector( rule.selectorText ) !== null ) {
75 selectors.matched++;
76 }
77 } );
78 document.body.removeChild( style );
79 return selectors;
80 },
81
82 /**
83 * Get a list of all loaded ResourceLoader modules.
84 *
85 * @return {Array} List of module names
86 */
87 getLoadedModules: function () {
88 return $.grep( mw.loader.getModuleNames(), function ( module ) {
89 return mw.loader.getState( module ) === 'ready';
90 } );
91 },
92
93 /**
94 * Print tabular data to the console, using console.table, console.log,
95 * or mw.log (in declining order of preference).
96 *
97 * @param {Array} data Tabular data represented as an array of objects
98 * with common properties.
99 */
100 dumpTable: function ( data ) {
101 try {
102 // Bartosz made me put this here.
103 if ( window.opera ) { throw window.opera; }
104 console.table( data );
105 return;
106 } catch (e) {}
107 try {
108 console.log( JSON.stringify( data, null, 2 ) );
109 return;
110 } catch (e) {}
111 mw.log( data );
112 },
113
114 /**
115 * Generate and print one more reports. When invoked with no arguments,
116 * print all reports.
117 *
118 * @param {string...} [reports] Report names to run, or unset to print
119 * all available reports.
120 */
121 runReports: function () {
122 var reports = arguments.length > 0 ?
123 Array.prototype.slice.call( arguments ) :
124 $.map( inspect.reports, function ( v, k ) { return k; } );
125
126 $.each( reports, function ( index, name ) {
127 inspect.dumpTable( inspect.reports[name]() );
128 } );
129 },
130
131 /**
132 * @class mw.inspect.reports
133 * @singleton
134 */
135 reports: {
136 /**
137 * Generate a breakdown of all loaded modules and their size in
138 * kilobytes. Modules are ordered from largest to smallest.
139 */
140 size: function () {
141 // Map each module to a descriptor object.
142 var modules = $.map( inspect.getLoadedModules(), function ( module ) {
143 return {
144 name: module,
145 size: inspect.getModuleSize( module )
146 };
147 } );
148
149 // Sort module descriptors by size, largest first.
150 sortByProperty( modules, 'size', true );
151
152 // Convert size to human-readable string.
153 $.each( modules, function ( i, module ) {
154 module.size = module.size > 1024 ?
155 ( module.size / 1024 ).toFixed( 2 ) + ' KB' :
156 ( module.size !== null ? module.size + ' B' : null );
157 } );
158
159 return modules;
160 },
161
162 /**
163 * For each module with styles, count the number of selectors, and
164 * count how many match against some element currently in the DOM.
165 */
166 css: function () {
167 var modules = [];
168
169 $.each( inspect.getLoadedModules(), function ( index, name ) {
170 var css, stats, module = mw.loader.moduleRegistry[name];
171
172 try {
173 css = module.style.css.join();
174 } catch (e) { return; } // skip
175
176 stats = inspect.auditSelectors( css );
177 modules.push( {
178 module: name,
179 allSelectors: stats.total,
180 matchedSelectors: stats.matched,
181 percentMatched: stats.total !== 0 ?
182 ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
183 } );
184 } );
185 sortByProperty( modules, 'allSelectors', true );
186 return modules;
187 },
188 }
189 };
190
191 if ( mw.config.get( 'debug' ) ) {
192 mw.log( 'mw.inspect: reports are not available in debug mode.' );
193 }
194
195 mw.inspect = inspect;
196
197 }( mediaWiki, jQuery ) );