jsExecutionTransformer.js 8.7 KB

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