resultsCtrl.js 14 KB

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