346e783aff952316b0ec81c4875d43cfe7370863
[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 sheet, rules;
70
71 style.textContent = css;
72 document.body.appendChild( style );
73 // Standards-compliant browsers use .sheet.cssRules, IE8 uses .styleSheet.rules…
74 sheet = style.sheet || style.styleSheet;
75 rules = sheet.cssRules || sheet.rules;
76 $.each( rules, function ( index, rule ) {
77 selectors.total++;
78 if ( document.querySelector( rule.selectorText ) !== null ) {
79 selectors.matched++;
80 }
81 } );
82 document.body.removeChild( style );
83 return selectors;
84 },
85
86 /**
87 * Get a list of all loaded ResourceLoader modules.
88 *
89 * @return {Array} List of module names
90 */
91 getLoadedModules: function () {
92 return $.grep( mw.loader.getModuleNames(), function ( module ) {
93 return mw.loader.getState( module ) === 'ready';
94 } );
95 },
96
97 /**
98 * Print tabular data to the console, using console.table, console.log,
99 * or mw.log (in declining order of preference).
100 *
101 * @param {Array} data Tabular data represented as an array of objects
102 * with common properties.
103 */
104 dumpTable: function ( data ) {
105 try {
106 // Bartosz made me put this here.
107 if ( window.opera ) { throw window.opera; }
108 // Use Function.prototype#call to force an exception on Firefox,
109 // which doesn't define console#table but doesn't complain if you
110 // try to invoke it.
111 console.table.call( console.table, data );
112 return;
113 } catch (e) {}
114 try {
115 console.log( $.toJSON( data, null, 2 ) );
116 return;
117 } catch (e) {}
118 mw.log( data );
119 },
120
121 /**
122 * Generate and print one more reports. When invoked with no arguments,
123 * print all reports.
124 *
125 * @param {string...} [reports] Report names to run, or unset to print
126 * all available reports.
127 */
128 runReports: function () {
129 var reports = arguments.length > 0 ?
130 Array.prototype.slice.call( arguments ) :
131 $.map( inspect.reports, function ( v, k ) { return k; } );
132
133 $.each( reports, function ( index, name ) {
134 inspect.dumpTable( inspect.reports[name]() );
135 } );
136 },
137
138 /**
139 * @class mw.inspect.reports
140 * @singleton
141 */
142 reports: {
143 /**
144 * Generate a breakdown of all loaded modules and their size in
145 * kilobytes. Modules are ordered from largest to smallest.
146 */
147 size: function () {
148 // Map each module to a descriptor object.
149 var modules = $.map( inspect.getLoadedModules(), function ( module ) {
150 return {
151 name: module,
152 size: inspect.getModuleSize( module )
153 };
154 } );
155
156 // Sort module descriptors by size, largest first.
157 sortByProperty( modules, 'size', true );
158
159 // Convert size to human-readable string.
160 $.each( modules, function ( i, module ) {
161 module.size = module.size > 1024 ?
162 ( module.size / 1024 ).toFixed( 2 ) + ' KB' :
163 ( module.size !== null ? module.size + ' B' : null );
164 } );
165
166 return modules;
167 },
168
169 /**
170 * For each module with styles, count the number of selectors, and
171 * count how many match against some element currently in the DOM.
172 */
173 css: function () {
174 var modules = [];
175
176 $.each( inspect.getLoadedModules(), function ( index, name ) {
177 var css, stats, module = mw.loader.moduleRegistry[name];
178
179 try {
180 css = module.style.css.join();
181 } catch (e) { return; } // skip
182
183 stats = inspect.auditSelectors( css );
184 modules.push( {
185 module: name,
186 allSelectors: stats.total,
187 matchedSelectors: stats.matched,
188 percentMatched: stats.total !== 0 ?
189 ( stats.matched / stats.total * 100 ).toFixed( 2 ) + '%' : null
190 } );
191 } );
192 sortByProperty( modules, 'allSelectors', true );
193 return modules;
194 },
195 }
196 };
197
198 if ( mw.config.get( 'debug' ) ) {
199 mw.log( 'mw.inspect: reports are not available in debug mode.' );
200 }
201
202 mw.inspect = inspect;
203
204 }( mediaWiki, jQuery ) );