resultsCtrl.js 14 KB

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