resultsCtrl.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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.phantomasMetadata = window._phantomas_metadata.metrics;
  6. $scope.view = 'summary';
  7. if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
  8. // Get the execution tree from the offenders
  9. $scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
  10. // Sort globalVariables offenders alphabetically
  11. $scope.phantomasResults.offenders.globalVariables.sort();
  12. initSummaryView();
  13. initJSTimelineView();
  14. initMetricsView();
  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. // Read all the duplicated queries and calculate a more appropriated score
  51. $scope.duplicatedQueriesCountAll = 0;
  52. if ($scope.phantomasResults.offenders.DOMqueriesDuplicated) {
  53. var regex = /\): *(\d+) queries$/;
  54. $scope.phantomasResults.offenders.DOMqueriesDuplicated.forEach(function(query) {
  55. var regexResult = regex.exec(query);
  56. if (regexResult) {
  57. $scope.duplicatedQueriesCountAll += parseInt(regexResult[1], 10) - 1;
  58. }
  59. });
  60. }
  61. // Grab the notes
  62. $scope.notations = {
  63. domComplexity: getDomComplexityScore(),
  64. jsDomManipulations: getJsDomManipulationsScore(),
  65. jsBadPractices: getJSBadPracticesScore(),
  66. jQueryLoading: getJQueryLoadingScore(),
  67. cssComplexity: getCSSComplexityScore(),
  68. badCss: getBadCssScore(),
  69. requests: requestsScore(),
  70. network: networkScore()
  71. };
  72. }
  73. function initJSTimelineView() {
  74. $scope.slowRequestsOn = false;
  75. $scope.slowRequestsLimit = 5;
  76. if (!$scope.javascript.children) {
  77. return;
  78. }
  79. // Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
  80. treeRunner($scope.javascript, function(node) {
  81. switch(node.data.type) {
  82. case 'domInteractive':
  83. $scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
  84. break;
  85. case 'domContentLoaded':
  86. $scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
  87. break;
  88. case 'domContentLoadedEnd':
  89. $scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
  90. break;
  91. case 'domComplete':
  92. $scope.phantomasResults.metrics.domComplete = node.data.timestamp;
  93. break;
  94. }
  95. if (node.data.type !== 'main') {
  96. // Don't check the children
  97. return false;
  98. }
  99. });
  100. // Now read the tree and display it on a timeline
  101. // Split the timeline into 200 intervals
  102. var numberOfIntervals = 200;
  103. var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
  104. $scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
  105. $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
  106. // Pre-filled array of 100 elements
  107. $scope.timeline = Array.apply(null, new Array(numberOfIntervals)).map(Number.prototype.valueOf,0);
  108. treeRunner($scope.javascript, function(node) {
  109. if (node.data.time) {
  110. // If a node is between two intervals, split it. That's the meaning of the following dirty algorithm.
  111. var startInterval = Math.floor(node.data.timestamp / $scope.timelineIntervalDuration);
  112. var endInterval = Math.floor((node.data.timestamp + node.data.time) / $scope.timelineIntervalDuration);
  113. if (startInterval === endInterval) {
  114. $scope.timeline[startInterval] += node.data.time;
  115. } else {
  116. var timeToDispatch = node.data.time;
  117. var startIntervalPart = ((startInterval + 1) * $scope.timelineIntervalDuration) - node.data.timestamp;
  118. $scope.timeline[startInterval] += startIntervalPart;
  119. timeToDispatch -= startIntervalPart;
  120. var currentInterval = startInterval;
  121. while(currentInterval < endInterval && currentInterval + 1 < numberOfIntervals) {
  122. currentInterval ++;
  123. var currentIntervalPart = Math.min(timeToDispatch, $scope.timelineIntervalDuration);
  124. $scope.timeline[currentInterval] = currentIntervalPart;
  125. timeToDispatch -= currentIntervalPart;
  126. }
  127. }
  128. }
  129. if (node.data.type !== 'main') {
  130. // Don't check the children
  131. return false;
  132. }
  133. });
  134. $scope.timelineMax = Math.max.apply(Math, $scope.timeline);
  135. }
  136. function initMetricsView() {
  137. // Get the Phantomas modules from metadata
  138. $scope.metricsModule = {};
  139. for (var metricName in $scope.phantomasMetadata) {
  140. var metric = $scope.phantomasMetadata[metricName];
  141. if (!$scope.metricsModule[metric.module]) {
  142. $scope.metricsModule[metric.module] = {};
  143. }
  144. $scope.metricsModule[metric.module][metricName] = metric;
  145. }
  146. }
  147. function getDomComplexityScore() {
  148. var note = 'A';
  149. var score = $scope.phantomasResults.metrics.DOMelementsCount +
  150. Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
  151. $scope.phantomasResults.metrics.iframesCount * 50 +
  152. $scope.phantomasResults.metrics.DOMidDuplicated * 25;
  153. if (score > 1000) {
  154. note = 'B';
  155. }
  156. if (score > 1500) {
  157. note = 'C';
  158. }
  159. if (score > 2000) {
  160. note = 'D';
  161. }
  162. if (score > 3000) {
  163. note = 'E';
  164. }
  165. if (score > 4000) {
  166. note = 'F';
  167. }
  168. return note;
  169. }
  170. function getJsDomManipulationsScore() {
  171. var note = 'A';
  172. var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
  173. $scope.phantomasResults.metrics.DOMqueries +
  174. $scope.duplicatedQueriesCountAll * 2 +
  175. $scope.phantomasResults.metrics.eventsBound;
  176. if (score > 300) {
  177. note = 'B';
  178. }
  179. if (score > 500) {
  180. note = 'C';
  181. }
  182. if (score > 700) {
  183. note = 'D';
  184. }
  185. if (score > 1000) {
  186. note = 'E';
  187. }
  188. if (score > 1400) {
  189. note = 'F';
  190. }
  191. return note;
  192. }
  193. function getJSBadPracticesScore() {
  194. var note = 'A';
  195. var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
  196. $scope.phantomasResults.metrics.evalCalls * 2 +
  197. $scope.phantomasResults.metrics.jsErrors * 10 +
  198. $scope.phantomasResults.metrics.consoleMessages / 2 +
  199. $scope.phantomasResults.metrics.globalVariables / 20 +
  200. Math.sqrt($scope.inBodyDomManipulations);
  201. if (score > 10) {
  202. note = 'B';
  203. }
  204. if (score > 15) {
  205. note = 'C';
  206. }
  207. if (score > 20) {
  208. note = 'D';
  209. }
  210. if (score > 30) {
  211. note = 'E';
  212. }
  213. if (score > 45) {
  214. note = 'F';
  215. }
  216. return note;
  217. }
  218. function getJQueryLoadingScore() {
  219. var note = 'NA';
  220. if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
  221. note = 'F';
  222. } else if ($scope.phantomasResults.metrics.jQueryVersion) {
  223. if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
  224. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
  225. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
  226. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
  227. note = 'A';
  228. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
  229. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
  230. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
  231. note = 'B';
  232. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
  233. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
  234. note = 'C';
  235. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
  236. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
  237. note = 'D';
  238. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
  239. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
  240. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
  241. note = 'E';
  242. }
  243. }
  244. return note;
  245. }
  246. function getCSSComplexityScore() {
  247. if (!$scope.phantomasResults.metrics.cssRules) {
  248. return 'NA';
  249. }
  250. var note = 'A';
  251. var score = $scope.phantomasResults.metrics.cssRules +
  252. $scope.phantomasResults.metrics.cssComplexSelectors * 10;
  253. if (score > 500) {
  254. note = 'B';
  255. }
  256. if (score > 1000) {
  257. note = 'C';
  258. }
  259. if (score > 2000) {
  260. note = 'D';
  261. }
  262. if (score > 4500) {
  263. note = 'E';
  264. }
  265. if (score > 7000) {
  266. note = 'F';
  267. }
  268. return note;
  269. }
  270. function getBadCssScore() {
  271. if ($scope.phantomasResults.metrics.cssParsingErrors) {
  272. return 'F';
  273. } else if (!$scope.phantomasResults.metrics.cssRules) {
  274. return 'NA';
  275. }
  276. var note = 'A';
  277. var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
  278. $scope.phantomasResults.metrics.cssEmptyRules +
  279. $scope.phantomasResults.metrics.cssExpressions * 10 +
  280. $scope.phantomasResults.metrics.cssImportants * 2 +
  281. $scope.phantomasResults.metrics.cssOldIEFixes * 10 +
  282. $scope.phantomasResults.metrics.cssOldPropertyPrefixes +
  283. $scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
  284. $scope.phantomasResults.metrics.cssRedundantBodySelectors;
  285. if (score > 20) {
  286. note = 'B';
  287. }
  288. if (score > 50) {
  289. note = 'C';
  290. }
  291. if (score > 100) {
  292. note = 'D';
  293. }
  294. if (score > 200) {
  295. note = 'E';
  296. }
  297. if (score > 500) {
  298. note = 'F';
  299. }
  300. return note;
  301. }
  302. function requestsScore() {
  303. var note = 'A';
  304. var score = $scope.phantomasResults.metrics.requests;
  305. if (score > 30) {
  306. note = 'B';
  307. }
  308. if (score > 45) {
  309. note = 'C';
  310. }
  311. if (score > 60) {
  312. note = 'D';
  313. }
  314. if (score > 80) {
  315. note = 'E';
  316. }
  317. if (score > 100) {
  318. note = 'F';
  319. }
  320. return note;
  321. }
  322. function networkScore() {
  323. var note = 'A';
  324. var score = $scope.phantomasResults.metrics.notFound * 25 +
  325. $scope.phantomasResults.metrics.closedConnections * 10 +
  326. $scope.phantomasResults.metrics.multipleRequests * 10 +
  327. $scope.phantomasResults.metrics.cachingDisabled * 2 +
  328. $scope.phantomasResults.metrics.cachingNotSpecified +
  329. $scope.phantomasResults.metrics.cachingTooShort / 2 +
  330. $scope.phantomasResults.metrics.domains;
  331. if (score > 20) {
  332. note = 'B';
  333. }
  334. if (score > 40) {
  335. note = 'C';
  336. }
  337. if (score > 60) {
  338. note = 'D';
  339. }
  340. if (score > 80) {
  341. note = 'E';
  342. }
  343. if (score > 100) {
  344. note = 'F';
  345. }
  346. return note;
  347. }
  348. function parseBacktrace(str) {
  349. if (!str) {
  350. return null;
  351. }
  352. var out = [];
  353. var splited = str.split(' / ');
  354. splited.forEach(function(trace) {
  355. var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
  356. if (result && result[2].length > 0) {
  357. var filePath = result[2];
  358. var chunks = filePath.split('/');
  359. var fileName = chunks[chunks.length - 1];
  360. out.push({
  361. fnName: result[1],
  362. fileName: fileName,
  363. filePath: filePath,
  364. line: result[3]
  365. });
  366. }
  367. });
  368. return out;
  369. }
  370. // 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.
  371. function treeRunner(node, fn) {
  372. if (fn(node) !== false && node.children) {
  373. node.children.forEach(function(child) {
  374. treeRunner(child, fn);
  375. });
  376. }
  377. }
  378. });