mediawiki.inspect: use $.toJSON & add workaround for FF oddity
[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 // Use Function.prototype#call to force an exception on Firefox,
105 // which doesn't define console#table but doesn't complain if you
106 // try to invoke it.
107 console.table.call( console.table, data );
108 return;
109 } catch (e) {}
110 try {
111 console.log( $.toJSON( data, null, 2 ) );
112 return;
113 } catch (e) {}
114 mw.log( data );
115 },
116
117 /**
118 * Generate and print one more reports. When invoked with no arguments,
119 * print all reports.
120 *
121 * @param {string...} [reports] Report names to run, or unset to print
122 * all available reports.
123 */
124 runReports: function () {
125 var reports = arguments.length > 0 ?
126 Array.prototype.slice.call( arguments ) :
127 $.map( inspect.reports, function ( v, k ) { return k; } );
128
129 $.each( reports, function ( index, name ) {
130 inspect.dumpTable( inspect.reports[name]() );
131 } );
132 },
133
134 /**
135 * @class mw.inspect.reports
136 * @singleton
137 */
138 reports: {
139 /**
140 * Generate a breakdown of all loaded modules and their size in
141 * kilobytes. Modules are ordered from largest to smallest.
142 */
143 size: function () {
144 // Map each module to a descriptor object.
145 var modules = $.map( inspect.getLoadedModules(), function ( module ) {
146 return {
147 name: module,
148 size: inspect.getModuleSize( module )
149 };
150 } );
151
152 // Sort module descriptors by size, largest first.
153 sortByProperty( modules, 'size', true );
154
155 // Convert size to human-readable string.
156 $.each( modules, function ( i, module ) {
157 module.size = module.size > 1024 ?
158 ( module.size / 1024 ).toFixed( 2 ) + ' KB' :
159 ( module.size !== null ? module.size + ' B' : null );
160 } );
161
162 return modules;
163 },
164
165 /**
166 * For each module with styles, count the number of selectors, and
167 * count how many match against some element currently in the DOM.
168 */
169 css: function () {
170 var modules = [];
171
172 $.each( inspect.getLoadedModules(), function ( index, name ) {
173 var css, stats, module = mw.loader.moduleRegistry[name];
174
175 try {
176 css = module.style.css.join();
177 } catch (e) { return; } // skip
178
179 stats = inspect.auditSelectors( css );
180 modules.push( {
181 module: name,
182 allSelectors: stats.total,
183 matchedSelectors: stats.matched,
184 percentMatched: stats.total !== 0 ?
185 ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
186 } );
187 } );
188 sortByProperty( modules, 'allSelectors', true );
189 return modules;
190 },
191 }
192 };
193
194 if ( mw.config.get( 'debug' ) ) {
195 mw.log( 'mw.inspect: reports are not available in debug mode.' );
196 }
197
198 mw.inspect = inspect;
199
200 }( mediaWiki, jQuery ) );