Sfoglia il codice sorgente

New rules: eventsScrollBound & DOMaccessesOnScroll

Gaël Métais 10 anni fa
parent
commit
f7da653e87

+ 25 - 11
front/src/css/rule.css

@@ -82,7 +82,11 @@
   font-size: 3em;
   font-size: 3em;
   margin-bottom: 1em;
   margin-bottom: 1em;
 }
 }
-.offenders .offenderButton {
+.rule .startTime {
+  display: none;
+}
+.offendersTable .offenderButton,
+.value .offenderButton {
   display: inline-block;
   display: inline-block;
   position: relative;
   position: relative;
   background: #efe;
   background: #efe;
@@ -91,16 +95,19 @@
   border-radius: 0.4em;
   border-radius: 0.4em;
   z-index: 1;
   z-index: 1;
 }
 }
-.offenders .offenderButton.opens {
+.offendersTable .offenderButton.opens,
+.value .offenderButton.opens {
   padding-right: 0.75em;
   padding-right: 0.75em;
 }
 }
-.offenders .offenderButton.opens:after {
+.offendersTable .offenderButton.opens:after,
+.value .offenderButton.opens:after {
   position: relative;
   position: relative;
   left: 0.5em;
   left: 0.5em;
   content: '\25BC';
   content: '\25BC';
   font-size: 0.8em;
   font-size: 0.8em;
 }
 }
-.offenders .offenderButton > div {
+.offendersTable .offenderButton > div,
+.value .offenderButton > div {
   display: none;
   display: none;
   position: absolute;
   position: absolute;
   right: 0;
   right: 0;
@@ -111,28 +118,35 @@
   border-top: 1px solid #999;
   border-top: 1px solid #999;
   z-index: 2;
   z-index: 2;
 }
 }
-.offenders .offenderButton .domTree {
+.offendersTable .offenderButton .domTree,
+.value .offenderButton .domTree {
   text-align: left;
   text-align: left;
   white-space: nowrap;
   white-space: nowrap;
 }
 }
-.offenders .offenderButton .domTree > div {
+.offendersTable .offenderButton .domTree > div,
+.value .offenderButton .domTree > div {
   margin: 0.5em;
   margin: 0.5em;
 }
 }
-.offenders .offenderButton .domTree > div div {
+.offendersTable .offenderButton .domTree > div div,
+.value .offenderButton .domTree > div div {
   margin-left: 1em;
   margin-left: 1em;
 }
 }
-.offenders .offenderButton .backtrace,
-.offenders .offenderButton .cssFileAndLine {
+.offendersTable .offenderButton .backtrace,
+.value .offenderButton .backtrace,
+.offendersTable .offenderButton .cssFileAndLine,
+.value .offenderButton .cssFileAndLine {
   white-space: nowrap;
   white-space: nowrap;
   padding: 0.5em;
   padding: 0.5em;
 }
 }
-.offenders .offenderButton.opens:hover {
+.offendersTable .offenderButton.opens:hover,
+.value .offenderButton.opens:hover {
   border-bottom-left-radius: 0;
   border-bottom-left-radius: 0;
   border-bottom-right-radius: 0;
   border-bottom-right-radius: 0;
   background: #ffe0cc;
   background: #ffe0cc;
   z-index: 2;
   z-index: 2;
 }
 }
-.offenders .offenderButton.opens:hover > div {
+.offendersTable .offenderButton.opens:hover > div,
+.value .offenderButton.opens:hover > div {
   display: block;
   display: block;
   background: #ffe0cc;
   background: #ffe0cc;
 }
 }

+ 29 - 4
front/src/js/directives/offendersDirectives.js

@@ -119,6 +119,22 @@
     function getNonJQueryHTML(node, onASingleLine) {
     function getNonJQueryHTML(node, onASingleLine) {
         var type = node.data.type;
         var type = node.data.type;
 
 
+        if (node.windowPerformance) {
+            switch (type) {
+                case 'documentScroll':
+                    return '(triggering the scroll event on <b>document</b>)';
+
+                case 'windowScroll':
+                    return '(triggering the scroll event on <b>window</b>)';
+
+                case 'window.onscroll':
+                    return '(calling the <b>window.onscroll</b> function)';
+
+                default:
+                    return '';
+            }
+        }
+
         if (!node.data.callDetails) {
         if (!node.data.callDetails) {
             return '';
             return '';
         }
         }
@@ -153,6 +169,18 @@
             case 'error':
             case 'error':
                 return args[0];
                 return args[0];
 
 
+            case 'jQuery - onDOMReady':
+                return '(function)';
+
+            case 'documentScroll':
+                return 'The scroll event just triggered on document';
+
+            case 'windowScroll':
+                return 'The scroll event just triggered on window';
+
+            case 'window.onscroll':
+                return 'The window.onscroll function just got called';
+
             default:
             default:
                 return '';
                 return '';
         }
         }
@@ -518,9 +546,6 @@
                 }
                 }
                 break;
                 break;
 
 
-            case 'jQuery - onDOMReady':
-                return '(function)';
-
             default:
             default:
                 return '';
                 return '';
         }
         }
@@ -674,7 +699,7 @@
         function getProfilerLineHTML(index, node) {
         function getProfilerLineHTML(index, node) {
             return  '<div class="index">' + (index + 1) + '</div>' +
             return  '<div class="index">' + (index + 1) + '</div>' +
                     '<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
                     '<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
-                    '<div class="value offenders">' + getTimelineParamsHTML(node, false) + '</div>' +
+                    '<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
                     '<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
                     '<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
                     '<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
                     '<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
         }
         }

+ 5 - 1
front/src/less/rule.less

@@ -90,7 +90,11 @@
     }
     }
 }
 }
 
 
-.offenders {
+.rule .startTime {
+    display: none;
+}
+
+.offendersTable, .value {
     .offenderButton {
     .offenderButton {
         display: inline-block;
         display: inline-block;
         position: relative;
         position: relative;

+ 27 - 0
front/src/views/rule.html

@@ -47,6 +47,20 @@
                         <b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
                         <b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
                     </div>
                     </div>
 
 
+                    <div ng-if="policyName === 'eventsScrollBound'">
+                        <div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
+                        <div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
+                            backtrace
+                            <div class="backtrace">
+                                <div ng-repeat="obj in offender.backtrace track by $index">
+                                    <span ng-if="obj.functionName">{{obj.functionName}}()</span>
+                                    <url-link url="obj.file" max-length="60"></url-link>
+                                    line {{obj.line}}
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
                     <div ng-if="policyName === 'jsErrors'">
                     <div ng-if="policyName === 'jsErrors'">
                         <b>{{offender.error}}</b>
                         <b>{{offender.error}}</b>
                         <div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
                         <div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
@@ -158,6 +172,19 @@
                 <dom-tree tree="rule.offendersObj.tree"></dom-tree>
                 <dom-tree tree="rule.offendersObj.tree"></dom-tree>
             </div>
             </div>
 
 
+            <div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
+                <p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
+                <div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
+                    <div class="headers">
+                        <div><!-- index --></div>
+                        <div>Type</div>
+                        <div>Params</div>
+                        <div><!-- details --></div>
+                    </div>
+                    <profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
+                </div>
+            </div>
+
             <div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
             <div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
                 <p>This is the colors palette, sized by total occurrences:</p>
                 <p>This is the colors palette, sized by total occurrences:</p>
                 <div class="colorPalette">
                 <div class="colorPalette">

+ 5 - 5
front/src/views/timeline.html

@@ -7,8 +7,8 @@
         </select>
         </select>
     </div>
     </div>
 
 
-    <h2>Javascript Timeline</h2>
-    <p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p>
+    <h2>JavaScript Timeline</h2>
+    <p>This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.</p>
 
 
     <div class="timeline">
     <div class="timeline">
         <div class="chart">
         <div class="chart">
@@ -45,7 +45,7 @@
                 <div class="domComplete"><div class="color"></div>Page is complete</div>
                 <div class="domComplete"><div class="color"></div>Page is complete</div>
             </div>
             </div>
             <div class="tips">
             <div class="tips">
-                <div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
+                <div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
                 <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
                 <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
                 <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
                 <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
                 <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
                 <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
@@ -54,9 +54,9 @@
         </div>
         </div>
     </div>
     </div>
 
 
-    <h2>Javascript Profiler</h2>
+    <h2>JavaScript Profiler</h2>
     <p>
     <p>
-        The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
+        The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
     </p>
     </p>
     <div class="filters">
     <div class="filters">
         <div>
         <div>

+ 51 - 0
lib/metadata/policies.js

@@ -66,6 +66,15 @@ var policies = {
             };
             };
         }
         }
     },
     },
+    "DOMaccesses": {
+        "tool": "jsExecutionTransformer",
+        "label": "DOM access",
+        "message": "<p>TODO</p><p>TODO</p>",
+        "isOkThreshold": 50,
+        "isBadThreshold": 2000,
+        "isAbnormalThreshold": 3000,
+        "hasOffenders": false
+    },
     "DOMinserts": {
     "DOMinserts": {
         "tool": "phantomas",
         "tool": "phantomas",
         "label": "DOM inserts",
         "label": "DOM inserts",
@@ -195,6 +204,48 @@ var policies = {
             };
             };
         }
         }
     },
     },
+    "eventsScrollBound": {
+        "tool": "phantomas",
+        "label": "Scroll events bound",
+        "message": "<p>Number of 'scroll' event listeners binded to 'window' or 'document'.</p><p>Asking too much work to the browser on scroll hurts the smoothness of the scroll. Merging all your event listeners into an unique listener can help you factorize their code and reduce their footprint on scroll.</p>",
+        "isOkThreshold": 1,
+        "isBadThreshold": 7,
+        "isAbnormalThreshold": 12,
+        "hasOffenders": true,
+        "offendersTransformFn": function(offenders) {
+            return {
+                count: offenders.length,
+                list: offenders.map(function(offender) {
+                    var parts = /^bound by (.*)$/.exec(offender);
+
+                    if (!parts) {
+                        debug('eventsScrollBound offenders transform function error with "%s"', offender);
+                        return {
+                            parseError: offender
+                        };
+                    }
+
+                    var backtraceArray = offendersHelpers.backtraceToArray(parts[1]);
+                    
+                    return {
+                        backtrace: backtraceArray || []
+                    };
+                })
+            };
+        }
+    },
+    "DOMaccessesOnScroll": {
+        "tool": "jsExecutionTransformer",
+        "label": "DOM access on scroll",
+        "message": "<p>This rule counts the number of DOM-accessing functions calls, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or deboucing</a>.</p>",
+        "isOkThreshold": 1,
+        "isBadThreshold": 10,
+        "isAbnormalThreshold": 20,
+        "hasOffenders": true,
+        "offendersTransformFn": function(offenders) {
+            return offenders;
+        }
+    },
     "jsErrors": {
     "jsErrors": {
         "tool": "phantomas",
         "tool": "phantomas",
         "label": "JavaScript errors",
         "label": "JavaScript errors",

+ 9 - 0
lib/metadata/scoreProfileGeneric.json

@@ -12,6 +12,7 @@
         "domManipulations": {
         "domManipulations": {
             "label": "DOM manipulations",
             "label": "DOM manipulations",
             "policies": {
             "policies": {
+                "DOMaccesses": 3,
                 "DOMinserts": 2,
                 "DOMinserts": 2,
                 "DOMqueries": 1,
                 "DOMqueries": 1,
                 "DOMqueriesWithoutResults": 2,
                 "DOMqueriesWithoutResults": 2,
@@ -19,6 +20,13 @@
                 "eventsBound": 1
                 "eventsBound": 1
             }
             }
         },
         },
+        "scroll": {
+            "label": "Scroll bottlenecks",
+            "policies": {
+                "eventsScrollBound": 1,
+                "DOMaccessesOnScroll": 4
+            }
+        },
         "badJavascript": {
         "badJavascript": {
             "label": "Bad JavaScript",
             "label": "Bad JavaScript",
             "policies": {
             "policies": {
@@ -99,6 +107,7 @@
     "globalScore": {
     "globalScore": {
         "domComplexity": 1,
         "domComplexity": 1,
         "domManipulations": 2,
         "domManipulations": 2,
+        "scroll": 1,
         "badJavascript": 1,
         "badJavascript": 1,
         "jQueryVersion": 1,
         "jQueryVersion": 1,
         "cssSyntaxError": 1,
         "cssSyntaxError": 1,

+ 1 - 1
lib/offendersHelpers.js

@@ -115,7 +115,7 @@ var OffendersHelpers = function() {
             var parts = null;
             var parts = null;
 
 
             for (var i=0 ; i<traceArray.length ; i++) {
             for (var i=0 ; i<traceArray.length ; i++) {
-                parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
+                parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
 
 
                 if (parts) {
                 if (parts) {
                     var obj = {
                     var obj = {

+ 2 - 0
lib/runner.js

@@ -42,6 +42,8 @@ var Runner = function(params) {
         
         
         delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
         delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
         delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
         delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
+        delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
+        delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
 
 
         return data;
         return data;
 
 

+ 1 - 0
lib/server/controllers/apiController.js

@@ -125,6 +125,7 @@ var ApiController = function(app) {
                     // Empty javascriptExecutionTree if not needed
                     // Empty javascriptExecutionTree if not needed
                     if (!run.params.jsTimeline) {
                     if (!run.params.jsTimeline) {
                         data.javascriptExecutionTree = {};
                         data.javascriptExecutionTree = {};
+                        data.scrollExecutionTree = {};
                     }
                     }
 
 
                     // Remove tools results if not needed
                     // Remove tools results if not needed

+ 69 - 22
lib/tools/jsExecutionTransformer.js

@@ -6,18 +6,19 @@ var jsExecutionTransformer = function() {
 
 
     this.transform = function(data) {
     this.transform = function(data) {
         var javascriptExecutionTree = {};
         var javascriptExecutionTree = {};
+        var scrollExecutionTree = {};
         
         
         var metrics = {
         var metrics = {
-            domManipulations: 0,
+            DOMaccesses: 0,
             queriesWithoutResults: 0,
             queriesWithoutResults: 0,
             jQueryCalls: 0,
             jQueryCalls: 0,
-            jQueryCallsOnEmptyObject: 0
-            
+            jQueryCallsOnEmptyObject: 0,
+            DOMaccessesOnScroll: 0
         };
         };
 
 
-        debug('Starting JS execution transformation');
-
         try {
         try {
+
+            debug('Starting JS execution transformation');
             javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
             javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
         
         
             if (javascriptExecutionTree.children) {
             if (javascriptExecutionTree.children) {
@@ -57,33 +58,46 @@ var jsExecutionTransformer = function() {
                             break;
                             break;
                     }
                     }
 
 
-                    // Change the list of dom paths into a tree
-                    treeRecursiveParser(node, function(node) {
-                        
-                        if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
-                            node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
-                        }
-
-                        if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
-                            node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
-                        }
-
-                        if (node.data.type === 'insertBefore') {
-                            node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
-                        }
-                    });
+                    // Transform domPaths into objects
+                    changeListOfDomPaths(node);
+
+                    // Count the number of DOM accesses, by counting the tree leafs
+                    metrics.DOMaccesses += countTreeLeafs(node);
                 });
                 });
             }
             }
-
             debug('JS execution transformation complete');
             debug('JS execution transformation complete');
 
 
+
+            debug('Starting scroll execution transformation');
+            scrollExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
+            if (scrollExecutionTree.children) {
+                scrollExecutionTree.children.forEach(function(node) {
+                    
+                    // Mark a event flag
+                    if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
+                        node.windowPerformance = true;
+                    }
+
+                    // Transform domPaths into objects
+                    changeListOfDomPaths(node);
+                    
+                    // Count the number of DOM accesses, by counting the tree leafs
+                    metrics.DOMaccessesOnScroll += countTreeLeafs(node);
+                });
+            }
+            debug('Scroll execution transformation complete');
+
         } catch(err) {
         } catch(err) {
             throw err;
             throw err;
         }
         }
 
 
         data.javascriptExecutionTree = javascriptExecutionTree;
         data.javascriptExecutionTree = javascriptExecutionTree;
+        
         data.toolsResults.jsExecutionTransformer = {
         data.toolsResults.jsExecutionTransformer = {
-            metrics: metrics
+            metrics: metrics,
+            offenders: {
+                DOMaccessesOnScroll: scrollExecutionTree
+            }
         };
         };
 
 
         return data;
         return data;
@@ -97,6 +111,39 @@ var jsExecutionTransformer = function() {
         }
         }
         fn(node);
         fn(node);
     }
     }
+
+    function changeListOfDomPaths(rootNode) {
+        treeRecursiveParser(rootNode, function(node) {
+            
+            if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
+                node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
+            }
+
+            if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
+                node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
+            }
+
+            if (node.data.type === 'insertBefore') {
+                node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
+            }
+        });
+    }
+
+    // Returns the number of leafs (nodes without children)
+    function countTreeLeafs(rootNode) {
+        var count = 0;
+
+        treeRecursiveParser(rootNode, function(node) {
+            if (!node.children &&
+                !node.error &&
+                !node.windowPerformance &&
+                node.data.type !== 'jQuery loaded') {
+                count ++;
+            }
+        });
+
+        return count;
+    }
 };
 };
 
 
 module.exports = new jsExecutionTransformer();
 module.exports = new jsExecutionTransformer();

+ 10 - 1
lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js

@@ -104,10 +104,11 @@ exports.module = function(phantomas) {
 
 
                     var root = new ContextTreeNode(null, {type: 'main'});
                     var root = new ContextTreeNode(null, {type: 'main'});
                     var currentContext = root;
                     var currentContext = root;
+                    var depth = 0;
+
                     if (deepAnalysis) {
                     if (deepAnalysis) {
                         phantomas.log('Entering deep Javascript analysis mode');
                         phantomas.log('Entering deep Javascript analysis mode');
                     }
                     }
-                    var depth = 0;
 
 
                     // Add a child but don't enter his context
                     // Add a child but don't enter his context
                     function pushContext(data) {
                     function pushContext(data) {
@@ -176,6 +177,13 @@ exports.module = function(phantomas) {
 
 
                         return root;
                         return root;
                     }
                     }
+
+                    // Empty the tree
+                    function resetTree() {
+                        root = new ContextTreeNode(null, {type: 'main'});
+                        currentContext = root;
+                        depth = 0;
+                    }
                     
                     
 
 
                     function ContextTreeNode(parent, data) {
                     function ContextTreeNode(parent, data) {
@@ -196,6 +204,7 @@ exports.module = function(phantomas) {
                     phantomas.leaveContext = leaveContext;
                     phantomas.leaveContext = leaveContext;
                     phantomas.getContextData = getContextData;
                     phantomas.getContextData = getContextData;
                     phantomas.readFullTree = readFullTree;
                     phantomas.readFullTree = readFullTree;
+                    phantomas.resetTree = resetTree;
 
 
                 })();
                 })();
 
 

+ 2 - 2
lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js

@@ -9,11 +9,11 @@ exports.version = '0.1';
 exports.module = function(phantomas) {
 exports.module = function(phantomas) {
     'use strict';
     'use strict';
 
 
-    phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
+    phantomas.setMetric('javascriptExecutionTree');
 
 
     // save data
     // save data
     phantomas.on('report', function() {
     phantomas.on('report', function() {
-        phantomas.log('Reading execution tree JSON');
+        phantomas.log('JS execution tree: Reading execution tree JSON');
 
 
         phantomas.evaluate(function() {(function(phantomas) {
         phantomas.evaluate(function() {(function(phantomas) {
             var fullTree = phantomas.readFullTree();
             var fullTree = phantomas.readFullTree();

+ 75 - 0
lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js

@@ -0,0 +1,75 @@
+exports.version = '0.1';
+
+exports.module = function(phantomas) {
+    'use strict';
+
+    phantomas.setMetric('scrollExecutionTree');
+
+    phantomas.on('report', function() {
+
+        phantomas.evaluate(function() {
+            (function(phantomas) {
+
+                var evt = document.createEvent('CustomEvent');
+                evt.initCustomEvent('scroll', false, false, null);
+
+                function triggerScrollEvent() {
+                    phantomas.resetTree();
+
+                    try {
+
+                        // Chrome triggers them in this order:
+
+                        // 1. document
+                        phantomas.pushContext({
+                            type: 'documentScroll'
+                        });
+                        document.dispatchEvent(evt);
+
+                        // 2. window
+                        phantomas.pushContext({
+                            type: 'windowScroll'
+                        });
+                        window.dispatchEvent(evt);
+
+                        // 3. onscroll()
+                        if (window.onscroll) {
+                            phantomas.pushContext({
+                                type: 'window.onscroll'
+                            });
+                            window.onscroll();
+                        }
+
+                    } catch(e) {
+                        phantomas.log('ScrollListener error: %s', e);
+                    }
+                }
+
+                var firstScrollTime = Date.now();
+                phantomas.log('ScrollListener: triggering a first scroll event...');
+                triggerScrollEvent();
+
+
+                // Ignore the first scroll event and only save the second one,
+                // because we want to detect un-throttled things, throttled ones are ok.
+                var secondScrollTime = Date.now();
+                phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
+                triggerScrollEvent();
+
+
+                var fullTree = phantomas.readFullTree();
+                if (fullTree !== null) {
+                    phantomas.setMetric('scrollExecutionTree', true, true);
+                    phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
+                    phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
+                } else {
+                    phantomas.log('Error: scrollExecutionTree could not be extracted');
+                }
+
+
+                phantomas.log('ScrollListener: end of scroll triggering');
+
+            })(window.__phantomas);
+        });
+    });
+};

+ 34 - 0
test/core/offendersHelpersTest.js

@@ -166,6 +166,40 @@ describe('offendersHelpers', function() {
             ]);
             ]);
         });
         });
 
 
+        it('should transform another backtrace syntax into an array', function() {
+            var result = offendersHelpers.backtraceToArray('phantomjs://webpage.evaluate():38 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / a (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:3 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:8');
+
+            result.should.deep.equal([
+                {
+                    file: 'phantomjs://webpage.evaluate()',
+                    line: 38
+                },
+                {
+                    functionName: 'e',
+                    file: 'http://s7.addthis.com/js/300/addthis_widget.js',
+                    line: 1
+                },
+                {
+                    functionName: 'a',
+                    file: 'http://s7.addthis.com/js/300/addthis_widget.js',
+                    line: 1
+                },
+                {
+                    file: 'http://s7.addthis.com/js/300/addthis_widget.js',
+                    line: 3
+                },
+                {
+                    functionName: 'e',
+                    file: 'http://s7.addthis.com/js/300/addthis_widget.js',
+                    line: 1
+                },
+                {
+                    file: 'http://s7.addthis.com/js/300/addthis_widget.js',
+                    line: 8
+                }
+            ]);
+        });
+
         it('should return null if it fails', function() {
         it('should return null if it fails', function() {
             var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');
             var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');