jsExecutionTransformer.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. var debug = require('debug')('ylt:jsExecutionTransformer');
  2. var offendersHelpers = require('../offendersHelpers');
  3. var Collection = require('./phantomas/custom_modules/util/collection');
  4. var jsExecutionTransformer = function() {
  5. this.transform = function(data) {
  6. var javascriptExecutionTree = {};
  7. var jQueryFunctionsCollection = new Collection();
  8. var metrics = {
  9. domInteractive: 0,
  10. domContentLoaded: 0,
  11. domContentLoadedEnd: 0,
  12. domComplete: 0,
  13. DOMaccesses: 0,
  14. DOMaccessesOnScroll: 0,
  15. queriesWithoutResults: 0
  16. };
  17. var offenders = {};
  18. var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
  19. if (hasjQuery) {
  20. metrics.jQueryCalls = 0;
  21. metrics.jQueryFunctionsUsed = 0;
  22. metrics.jQueryCallsOnEmptyObject = 0;
  23. metrics.jQueryNotDelegatedEvents = 0;
  24. offenders.jQueryFunctionsUsed = [];
  25. }
  26. try {
  27. debug('Starting JS execution transformation');
  28. javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
  29. if (javascriptExecutionTree.children) {
  30. javascriptExecutionTree.children.forEach(function(node, index) {
  31. var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
  32. if (isABindWithoutEventDelegation(node, contextLength)) {
  33. metrics.jQueryNotDelegatedEvents += contextLength;
  34. node.warning = true;
  35. node.eventNotDelegated = true;
  36. }
  37. if (node.data.resultsNumber === 0) {
  38. metrics.queriesWithoutResults ++;
  39. node.queryWithoutResults = true;
  40. node.warning = true;
  41. }
  42. if (contextLength === 0) {
  43. metrics.jQueryCallsOnEmptyObject ++;
  44. node.jQueryCallOnEmptyObject = true;
  45. node.warning = true;
  46. }
  47. if (node.data.type.indexOf('jQuery - ') === 0) {
  48. metrics.jQueryCalls ++;
  49. jQueryFunctionsCollection.push(node.data.type);
  50. }
  51. // Mark errors with an error flag
  52. if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
  53. node.error = true;
  54. }
  55. // Mark a performance flag
  56. if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
  57. node.windowPerformance = true;
  58. // Adjust the navigation timings (cause their not very well synchronised)
  59. switch(node.data.type) {
  60. case 'domInteractive':
  61. javascriptExecutionTree.data.domInteractive = node.data.timestamp;
  62. break;
  63. case 'domContentLoaded':
  64. javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
  65. break;
  66. case 'domContentLoadedEnd':
  67. javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
  68. break;
  69. case 'domComplete':
  70. javascriptExecutionTree.data.domComplete = node.data.timestamp;
  71. break;
  72. }
  73. }
  74. // Fix rare bug when domComplete was never triggered
  75. if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
  76. javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
  77. }
  78. // Transform domPaths into objects
  79. changeListOfDomPaths(node);
  80. // Count the number of DOM accesses, by counting the tree leafs
  81. metrics.DOMaccesses += countTreeLeafs(node);
  82. });
  83. // Count the number of different jQuery functions called
  84. if (hasjQuery) {
  85. jQueryFunctionsCollection.sort().forEach(function(fnName, cnt) {
  86. if (fnName === 'jQuery - find') {
  87. fnName = 'jQuery - $';
  88. }
  89. metrics.jQueryFunctionsUsed ++;
  90. offenders.jQueryFunctionsUsed.push({
  91. functionName: fnName.substring(9),
  92. count: cnt
  93. });
  94. });
  95. }
  96. }
  97. debug('JS execution transformation complete');
  98. if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
  99. debug('Starting scroll execution transformation');
  100. offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
  101. if (offenders.DOMaccessesOnScroll.children) {
  102. offenders.DOMaccessesOnScroll.children.forEach(function(node) {
  103. // Mark a event flag
  104. if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
  105. node.windowPerformance = true;
  106. }
  107. // Transform domPaths into objects
  108. changeListOfDomPaths(node);
  109. // Count the number of DOM accesses, by counting the tree leafs
  110. metrics.DOMaccessesOnScroll += countTreeLeafs(node);
  111. });
  112. }
  113. debug('Scroll execution transformation complete');
  114. } else {
  115. debug('Could not parse scrollExecutionTree');
  116. }
  117. } catch(err) {
  118. throw err;
  119. }
  120. data.javascriptExecutionTree = javascriptExecutionTree;
  121. data.toolsResults.jsExecutionTransformer = {
  122. metrics: metrics,
  123. offenders: offenders
  124. };
  125. return data;
  126. };
  127. function treeRecursiveParser(node, fn) {
  128. if (node.children) {
  129. node.children.forEach(function(child) {
  130. treeRecursiveParser(child, fn);
  131. });
  132. }
  133. fn(node);
  134. }
  135. function changeListOfDomPaths(rootNode) {
  136. treeRecursiveParser(rootNode, function(node) {
  137. if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
  138. node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
  139. }
  140. if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
  141. node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
  142. }
  143. if (node.data.type === 'insertBefore') {
  144. node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
  145. }
  146. });
  147. }
  148. // Returns the number of leafs (nodes without children)
  149. function countTreeLeafs(rootNode) {
  150. var count = 0;
  151. treeRecursiveParser(rootNode, function(node) {
  152. if (!node.children &&
  153. !node.error &&
  154. !node.windowPerformance &&
  155. node.data.type !== 'jQuery loaded') {
  156. count ++;
  157. }
  158. });
  159. return count;
  160. }
  161. function isPureString(str) {
  162. return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
  163. }
  164. function isABindWithoutEventDelegation(node, contextLength) {
  165. // Count only on larger bindings
  166. if (contextLength <= 3) {
  167. return false;
  168. }
  169. if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
  170. return true;
  171. }
  172. if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
  173. var child = node.children[0];
  174. if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
  175. return true;
  176. }
  177. }
  178. return false;
  179. }
  180. };
  181. module.exports = new jsExecutionTransformer();