resultsCtrl.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. var app = angular.module('Results', []);
  2. app.controller('ResultsCtrl', function ($scope) {
  3. // Grab results from nodeJS served page
  4. $scope.phantomasResults = window._phantomas_results;
  5. $scope.view = 'execution';
  6. if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
  7. // Get the execution tree from the offenders
  8. $scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
  9. // Sort globalVariables offenders alphabetically
  10. if ($scope.phantomasResults.offenders.globalVariables) {
  11. $scope.phantomasResults.offenders.globalVariables.sort();
  12. }
  13. initSummaryView();
  14. initJSTimelineView();
  15. }
  16. $scope.setView = function(viewName) {
  17. $scope.view = viewName;
  18. };
  19. $scope.onNodeDetailsClick = function(node) {
  20. var isOpen = node.data.showDetails;
  21. if (!isOpen) {
  22. // Close all other nodes
  23. $scope.javascript.children.forEach(function(currentNode) {
  24. currentNode.data.showDetails = false;
  25. });
  26. // Parse the backtrace
  27. if (!node.data.parsedBacktrace) {
  28. node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
  29. }
  30. }
  31. node.data.showDetails = !isOpen;
  32. };
  33. function initSummaryView() {
  34. // Read the main elements of the tree and sum the total time
  35. $scope.totalJSTime = 0;
  36. $scope.inBodyDomManipulations = 0;
  37. treeRunner($scope.javascript, function(node) {
  38. if (node.data.time) {
  39. $scope.totalJSTime += node.data.time;
  40. }
  41. if (node.data.timestamp < $scope.phantomasResults.metrics.domInteractive &&
  42. node.data.type !== 'jQuery - onDOMReady') {
  43. $scope.inBodyDomManipulations ++;
  44. }
  45. if (node.data.type !== 'main') {
  46. // Don't check the children
  47. return false;
  48. }
  49. });
  50. // If there are some CSS parsing errors, prepare the W3C CSS Validator direct URLs
  51. if ($scope.phantomasResults.offenders.cssParsingErrors) {
  52. $scope.cssW3cDirectUrls = [];
  53. $scope.phantomasResults.offenders.cssParsingErrors.forEach(function(errorString, index) {
  54. var stylesheet = errorString.split(' ')[0];
  55. var w3cUrl = 'http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&vextwarning=true&lang=en&uri=' + encodeURIComponent(stylesheet);
  56. $scope.cssW3cDirectUrls.push({
  57. url: stylesheet,
  58. w3c: w3cUrl
  59. });
  60. });
  61. }
  62. // Grab the notes
  63. $scope.notations = {
  64. domComplexity: getDomComplexityScore(),
  65. jsDomManipulations: getJsDomManipulationsScore(),
  66. jsBadPractices: getJSBadPracticesScore(),
  67. jQueryLoading: getJQueryLoadingScore(),
  68. cssComplexity: getCSSComplexityScore(),
  69. badCss: getBadCssScore(),
  70. requests: requestsScore(),
  71. network: networkScore()
  72. };
  73. }
  74. function initJSTimelineView() {
  75. if (!$scope.javascript.children) {
  76. return;
  77. }
  78. // Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
  79. treeRunner($scope.javascript, function(node) {
  80. switch(node.data.type) {
  81. case 'domInteractive':
  82. $scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
  83. break;
  84. case 'domContentLoaded':
  85. $scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
  86. break;
  87. case 'domContentLoadedEnd':
  88. $scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
  89. break;
  90. case 'domComplete':
  91. $scope.phantomasResults.metrics.domComplete = node.data.timestamp;
  92. break;
  93. }
  94. if (node.data.type !== 'main') {
  95. // Don't check the children
  96. return false;
  97. }
  98. });
  99. // Now read the tree and display it on a timeline
  100. // Split the timeline into 200 intervals
  101. var numberOfIntervals = 199;
  102. var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
  103. $scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
  104. $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
  105. // Pre-fill array of as many elements as there are milleseconds
  106. var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
  107. // Create the milliseconds array from the execution tree
  108. treeRunner($scope.javascript, function(node) {
  109. if (node.data.time !== undefined) {
  110. // Ignore artefacts (durations > 100ms)
  111. var time = Math.min(node.data.time, 100) || 1;
  112. for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
  113. millisecondsArray[i] |= 1;
  114. }
  115. }
  116. if (node.data.type !== 'main') {
  117. // Don't check the children
  118. return false;
  119. }
  120. });
  121. // Pre-fill array of 200 elements
  122. $scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
  123. // Create the timeline from the milliseconds array
  124. millisecondsArray.forEach(function(value, timestamp) {
  125. if (value === 1) {
  126. $scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
  127. }
  128. });
  129. // Get the maximum value of the array (needed for display)
  130. $scope.timelineMax = Math.max.apply(Math, $scope.timeline);
  131. }
  132. function getDomComplexityScore() {
  133. var note = 'A';
  134. var score = $scope.phantomasResults.metrics.DOMelementsCount +
  135. Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
  136. $scope.phantomasResults.metrics.iframesCount * 50 +
  137. $scope.phantomasResults.metrics.DOMidDuplicated * 25;
  138. if (score > 1000) {
  139. note = 'B';
  140. }
  141. if (score > 1500) {
  142. note = 'C';
  143. }
  144. if (score > 2000) {
  145. note = 'D';
  146. }
  147. if (score > 3000) {
  148. note = 'E';
  149. }
  150. if (score > 4000) {
  151. note = 'F';
  152. }
  153. return note;
  154. }
  155. function getJsDomManipulationsScore() {
  156. var note = 'A';
  157. var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
  158. $scope.phantomasResults.metrics.DOMqueries +
  159. $scope.phantomasResults.metrics.DOMqueriesAvoidable * 2 +
  160. $scope.phantomasResults.metrics.eventsBound;
  161. if (score > 300) {
  162. note = 'B';
  163. }
  164. if (score > 500) {
  165. note = 'C';
  166. }
  167. if (score > 700) {
  168. note = 'D';
  169. }
  170. if (score > 1000) {
  171. note = 'E';
  172. }
  173. if (score > 1400) {
  174. note = 'F';
  175. }
  176. return note;
  177. }
  178. function getJSBadPracticesScore() {
  179. var note = 'A';
  180. var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
  181. $scope.phantomasResults.metrics.evalCalls * 2 +
  182. $scope.phantomasResults.metrics.jsErrors * 10 +
  183. $scope.phantomasResults.metrics.consoleMessages / 2 +
  184. $scope.phantomasResults.metrics.globalVariables / 20 +
  185. Math.sqrt($scope.inBodyDomManipulations);
  186. if (score > 10) {
  187. note = 'B';
  188. }
  189. if (score > 15) {
  190. note = 'C';
  191. }
  192. if (score > 20) {
  193. note = 'D';
  194. }
  195. if (score > 30) {
  196. note = 'E';
  197. }
  198. if (score > 45) {
  199. note = 'F';
  200. }
  201. return note;
  202. }
  203. function getJQueryLoadingScore() {
  204. var note = 'NA';
  205. if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
  206. note = 'F';
  207. } else if ($scope.phantomasResults.metrics.jQueryVersion) {
  208. if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
  209. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
  210. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
  211. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
  212. note = 'A';
  213. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
  214. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
  215. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
  216. note = 'B';
  217. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
  218. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
  219. note = 'C';
  220. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
  221. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
  222. note = 'D';
  223. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
  224. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
  225. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
  226. note = 'E';
  227. }
  228. }
  229. return note;
  230. }
  231. function getCSSComplexityScore() {
  232. if (!$scope.phantomasResults.metrics.cssRules) {
  233. return 'NA';
  234. }
  235. var note = 'A';
  236. var score = $scope.phantomasResults.metrics.cssRules +
  237. $scope.phantomasResults.metrics.cssComplexSelectors * 5 +
  238. $scope.phantomasResults.metrics.cssComplexSelectorsByAttribute * 10;
  239. if (score > 800) {
  240. note = 'B';
  241. }
  242. if (score > 1200) {
  243. note = 'C';
  244. }
  245. if (score > 2500) {
  246. note = 'D';
  247. }
  248. if (score > 4000) {
  249. note = 'E';
  250. }
  251. if (score > 6000) {
  252. note = 'F';
  253. }
  254. return note;
  255. }
  256. function getBadCssScore() {
  257. if (!$scope.phantomasResults.metrics.cssRules) {
  258. return 'NA';
  259. }
  260. var note = 'A';
  261. var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
  262. $scope.phantomasResults.metrics.cssDuplicatedProperties +
  263. $scope.phantomasResults.metrics.cssEmptyRules +
  264. $scope.phantomasResults.metrics.cssExpressions * 10 +
  265. $scope.phantomasResults.metrics.cssImportants * 2 +
  266. $scope.phantomasResults.metrics.cssOldIEFixes * 10 +
  267. $scope.phantomasResults.metrics.cssOldPropertyPrefixes +
  268. $scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
  269. $scope.phantomasResults.metrics.cssRedundantBodySelectors * 0.5 +
  270. $scope.phantomasResults.metrics.cssRedundantChildNodesSelectors * 0.5 +
  271. $scope.phantomasResults.metrics.cssImports * 50;
  272. if (score > 50) {
  273. note = 'B';
  274. }
  275. if (score > 100) {
  276. note = 'C';
  277. }
  278. if (score > 200) {
  279. note = 'D';
  280. }
  281. if (score > 500) {
  282. note = 'E';
  283. }
  284. if (score > 1000) {
  285. note = 'F';
  286. }
  287. return note;
  288. }
  289. function requestsScore() {
  290. var note = 'A';
  291. var score = $scope.phantomasResults.metrics.requests;
  292. if (score > 30) {
  293. note = 'B';
  294. }
  295. if (score > 45) {
  296. note = 'C';
  297. }
  298. if (score > 60) {
  299. note = 'D';
  300. }
  301. if (score > 80) {
  302. note = 'E';
  303. }
  304. if (score > 100) {
  305. note = 'F';
  306. }
  307. return note;
  308. }
  309. function networkScore() {
  310. var note = 'A';
  311. var score = $scope.phantomasResults.metrics.notFound * 25 +
  312. $scope.phantomasResults.metrics.closedConnections * 10 +
  313. $scope.phantomasResults.metrics.multipleRequests * 10 +
  314. $scope.phantomasResults.metrics.cachingDisabled * 2 +
  315. $scope.phantomasResults.metrics.cachingNotSpecified +
  316. $scope.phantomasResults.metrics.cachingTooShort / 2 +
  317. $scope.phantomasResults.metrics.domains;
  318. if (score > 20) {
  319. note = 'B';
  320. }
  321. if (score > 40) {
  322. note = 'C';
  323. }
  324. if (score > 60) {
  325. note = 'D';
  326. }
  327. if (score > 80) {
  328. note = 'E';
  329. }
  330. if (score > 100) {
  331. note = 'F';
  332. }
  333. return note;
  334. }
  335. function parseBacktrace(str) {
  336. if (!str) {
  337. return null;
  338. }
  339. var out = [];
  340. var splited = str.split(' / ');
  341. splited.forEach(function(trace) {
  342. var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
  343. if (result && result[2].length > 0) {
  344. var filePath = result[2];
  345. var chunks = filePath.split('/');
  346. var fileName = chunks[chunks.length - 1];
  347. out.push({
  348. fnName: result[1],
  349. fileName: fileName,
  350. filePath: filePath,
  351. line: result[3]
  352. });
  353. }
  354. });
  355. return out;
  356. }
  357. // Goes on every node of the tree and calls the function fn. If fn returns false on a node, its children won't be checked.
  358. function treeRunner(node, fn) {
  359. if (fn(node) !== false && node.children) {
  360. node.children.forEach(function(child) {
  361. treeRunner(child, fn);
  362. });
  363. }
  364. }
  365. });