resultsCtrl.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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. initSummaryView();
  11. initExecutionView();
  12. initMetricsView();
  13. }
  14. $scope.setView = function(viewName) {
  15. $scope.view = viewName;
  16. };
  17. $scope.onNodeDetailsClick = function(node) {
  18. var isOpen = node.data.showDetails;
  19. if (!isOpen) {
  20. // Close all other nodes
  21. $scope.javascript.children.forEach(function(currentNode) {
  22. currentNode.data.showDetails = false;
  23. });
  24. // Parse the backtrace
  25. if (!node.data.parsedBacktrace) {
  26. node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
  27. }
  28. }
  29. node.data.showDetails = !isOpen;
  30. };
  31. function initSummaryView() {
  32. // Read the main elements of the tree and sum the total time
  33. $scope.totalJSTime = 0;
  34. treeRunner($scope.javascript, function(node) {
  35. if (node.data.time) {
  36. $scope.totalJSTime += node.data.time;
  37. }
  38. if (node.data.type !== 'main') {
  39. // Don't check the children
  40. return false;
  41. }
  42. });
  43. // Read all the duplicated queries and calculate a more appropriated score
  44. $scope.duplicatedQueriesCountAll = 0;
  45. if ($scope.phantomasResults.offenders.DOMqueriesDuplicated) {
  46. var regex = /\): *(\d+) queries$/;
  47. $scope.phantomasResults.offenders.DOMqueriesDuplicated.forEach(function(query) {
  48. var regexResult = regex.exec(query);
  49. if (regexResult) {
  50. $scope.duplicatedQueriesCountAll += parseInt(regexResult[1], 10) - 1;
  51. }
  52. });
  53. }
  54. $scope.notations = {
  55. domComplexity: getDomComplexityScore(),
  56. domManipulations: getDomManipulationsScore(),
  57. duplicatedDomQueries: getDuplicatedDomQueriesScore(),
  58. eventsBound: getEventsBoundScore(),
  59. badPractices: getBadPracticesScore(),
  60. scripts: getScriptsScore(),
  61. jQueryLoading: getJQueryLoadingScore(),
  62. cssComplexity: getCSSComplexityScore(),
  63. badCss: getBadCssScore()
  64. };
  65. }
  66. function initExecutionView() {
  67. $scope.slowRequestsOn = false;
  68. $scope.slowRequestsLimit = 5;
  69. if (!$scope.javascript.children) {
  70. return;
  71. }
  72. // Now read the tree and display it on a timeline
  73. // Split the timeline into 200 intervals
  74. var numberOfIntervals = 200;
  75. var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
  76. $scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
  77. $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
  78. // Pre-filled array of 100 elements
  79. $scope.timeline = Array.apply(null, new Array(numberOfIntervals)).map(Number.prototype.valueOf,0);
  80. treeRunner($scope.javascript, function(node) {
  81. if (node.data.time) {
  82. // If a node is between two intervals, split it. That's the meaning of the following dirty algorithm.
  83. var startInterval = Math.floor(node.data.timestamp / $scope.timelineIntervalDuration);
  84. var endInterval = Math.floor((node.data.timestamp + node.data.time) / $scope.timelineIntervalDuration);
  85. if (startInterval === endInterval) {
  86. $scope.timeline[startInterval] += node.data.time;
  87. } else {
  88. var timeToDispatch = node.data.time;
  89. var startIntervalPart = ((startInterval + 1) * $scope.timelineIntervalDuration) - node.data.timestamp;
  90. $scope.timeline[startInterval] += startIntervalPart;
  91. timeToDispatch -= startIntervalPart;
  92. var currentInterval = startInterval;
  93. while(currentInterval < endInterval && currentInterval + 1 < numberOfIntervals) {
  94. currentInterval ++;
  95. var currentIntervalPart = Math.min(timeToDispatch, $scope.timelineIntervalDuration);
  96. $scope.timeline[currentInterval] = currentIntervalPart;
  97. timeToDispatch -= currentIntervalPart;
  98. }
  99. }
  100. }
  101. if (node.data.type !== 'main') {
  102. // Don't check the children
  103. return false;
  104. }
  105. });
  106. $scope.timelineMax = Math.max.apply(Math, $scope.timeline);
  107. }
  108. function initMetricsView() {
  109. // Get the Phantomas modules from metadata
  110. $scope.metricsModule = {};
  111. for (var metricName in $scope.phantomasMetadata) {
  112. var metric = $scope.phantomasMetadata[metricName];
  113. if (!$scope.metricsModule[metric.module]) {
  114. $scope.metricsModule[metric.module] = {};
  115. }
  116. $scope.metricsModule[metric.module][metricName] = metric;
  117. }
  118. }
  119. function getDomComplexityScore() {
  120. var note = 'A';
  121. var domComplexityScore = $scope.phantomasResults.metrics.DOMelementsCount +
  122. Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
  123. $scope.phantomasResults.metrics.iframesCount * 50;
  124. if (domComplexityScore > 1000) {
  125. note = 'B';
  126. }
  127. if (domComplexityScore > 1500) {
  128. note = 'C';
  129. }
  130. if (domComplexityScore > 2000) {
  131. note = 'D';
  132. }
  133. if (domComplexityScore > 3000) {
  134. note = 'E';
  135. }
  136. if (domComplexityScore > 4000) {
  137. note = 'F';
  138. }
  139. return note;
  140. }
  141. function getDomManipulationsScore() {
  142. var note = 'A';
  143. var domManipulationsScore = $scope.phantomasResults.metrics.DOMinserts +
  144. $scope.phantomasResults.metrics.DOMqueries * 0.5 +
  145. $scope.totalJSTime;
  146. if (domManipulationsScore > 100) {
  147. note = 'B';
  148. }
  149. if (domManipulationsScore > 200) {
  150. note = 'C';
  151. }
  152. if (domManipulationsScore > 300) {
  153. note = 'D';
  154. }
  155. if (domManipulationsScore > 500) {
  156. note = 'E';
  157. }
  158. if (domManipulationsScore > 800) {
  159. note = 'F';
  160. }
  161. return note;
  162. }
  163. function getDuplicatedDomQueriesScore() {
  164. var note = 'A';
  165. var duplicatedDomQueries = $scope.duplicatedQueriesCountAll;
  166. if (duplicatedDomQueries > 10) {
  167. note = 'B';
  168. }
  169. if (duplicatedDomQueries > 50) {
  170. note = 'C';
  171. }
  172. if (duplicatedDomQueries > 100) {
  173. note = 'D';
  174. }
  175. if (duplicatedDomQueries > 200) {
  176. note = 'E';
  177. }
  178. if (duplicatedDomQueries > 500) {
  179. note = 'F';
  180. }
  181. return note;
  182. }
  183. function getEventsBoundScore() {
  184. var note = 'A';
  185. var eventsBoundScore = $scope.phantomasResults.metrics.eventsBound;
  186. if (eventsBoundScore > 50) {
  187. note = 'B';
  188. }
  189. if (eventsBoundScore > 100) {
  190. note = 'C';
  191. }
  192. if (eventsBoundScore > 200) {
  193. note = 'D';
  194. }
  195. if (eventsBoundScore > 500) {
  196. note = 'E';
  197. }
  198. if (eventsBoundScore > 1000) {
  199. note = 'F';
  200. }
  201. return note;
  202. }
  203. function getBadPracticesScore() {
  204. var note = 'A';
  205. var badPracticesScore = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
  206. $scope.phantomasResults.metrics.evalCalls * 3 +
  207. $scope.phantomasResults.metrics.jsErrors * 10 +
  208. $scope.phantomasResults.metrics.consoleMessages;
  209. if (badPracticesScore > 5) {
  210. note = 'B';
  211. }
  212. if (badPracticesScore > 10) {
  213. note = 'C';
  214. }
  215. if (badPracticesScore > 15) {
  216. note = 'D';
  217. }
  218. if (badPracticesScore > 25) {
  219. note = 'E';
  220. }
  221. if (badPracticesScore > 40) {
  222. note = 'F';
  223. }
  224. return note;
  225. }
  226. function getScriptsScore() {
  227. var note = 'A';
  228. var scriptsScore = $scope.phantomasResults.metrics.jsCount;
  229. if (scriptsScore > 4) {
  230. note = 'B';
  231. }
  232. if (scriptsScore > 8) {
  233. note = 'C';
  234. }
  235. if (scriptsScore > 12) {
  236. note = 'D';
  237. }
  238. if (scriptsScore > 16) {
  239. note = 'E';
  240. }
  241. if (scriptsScore > 20) {
  242. note = 'F';
  243. }
  244. return note;
  245. }
  246. function getJQueryLoadingScore() {
  247. var note = 'NA';
  248. if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
  249. note = 'F';
  250. } else if ($scope.phantomasResults.metrics.jQueryVersion) {
  251. if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
  252. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
  253. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
  254. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0 ||
  255. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
  256. $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
  257. note = 'A';
  258. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0 ||
  259. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0) {
  260. note = 'B';
  261. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0 ||
  262. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0) {
  263. note = 'C';
  264. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0 ||
  265. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0) {
  266. note = 'D';
  267. } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
  268. $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0) {
  269. note = 'E';
  270. }
  271. }
  272. return note;
  273. }
  274. function getCSSComplexityScore() {
  275. if (!$scope.phantomasResults.metrics.cssRules) {
  276. return 'NA';
  277. }
  278. var note = 'A';
  279. var cssScore = $scope.phantomasResults.metrics.cssRules +
  280. $scope.phantomasResults.metrics.cssComplexSelectors * 10;
  281. if (cssScore > 200) {
  282. note = 'B';
  283. }
  284. if (cssScore > 500) {
  285. note = 'C';
  286. }
  287. if (cssScore > 1000) {
  288. note = 'D';
  289. }
  290. if (cssScore > 2000) {
  291. note = 'E';
  292. }
  293. if (cssScore > 5000) {
  294. note = 'F';
  295. }
  296. return note;
  297. }
  298. function getBadCssScore() {
  299. if (!$scope.phantomasResults.metrics.cssRules) {
  300. return 'NA';
  301. }
  302. var note = 'A';
  303. var badCssScore = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
  304. $scope.phantomasResults.metrics.cssEmptyRules +
  305. $scope.phantomasResults.metrics.cssExpressions * 10 +
  306. $scope.phantomasResults.metrics.cssImportants * 2 +
  307. $scope.phantomasResults.metrics.cssOldIEFixes * 10 +
  308. $scope.phantomasResults.metrics.cssOldPropertyPrefixes +
  309. $scope.phantomasResults.metrics.cssUniversalSelectors * 5
  310. $scope.phantomasResults.metrics.cssRedundantBodySelectors
  311. if (badCssScore > 20) {
  312. note = 'B';
  313. }
  314. if (badCssScore > 50) {
  315. note = 'C';
  316. }
  317. if (badCssScore > 100) {
  318. note = 'D';
  319. }
  320. if (badCssScore > 200) {
  321. note = 'E';
  322. }
  323. if (badCssScore > 500) {
  324. note = 'F';
  325. }
  326. return note;
  327. }
  328. function parseBacktrace(str) {
  329. if (!str) {
  330. return null;
  331. }
  332. var out = [];
  333. var splited = str.split(' / ');
  334. splited.forEach(function(trace) {
  335. var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
  336. if (result && result[2].length > 0) {
  337. var filePath = result[2];
  338. var chunks = filePath.split('/');
  339. var fileName = chunks[chunks.length - 1];
  340. out.push({
  341. fnName: result[1],
  342. fileName: fileName,
  343. filePath: filePath,
  344. line: result[3]
  345. });
  346. }
  347. });
  348. return out;
  349. }
  350. // 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.
  351. function treeRunner(node, fn) {
  352. if (fn(node) !== false && node.children) {
  353. node.children.forEach(function(child) {
  354. treeRunner(child, fn);
  355. });
  356. }
  357. }
  358. });