Compare commits

...
198 Commits
Author SHA1 Message Date
Gaël Métais 0ba526ebd8 v1.0.0 2014-11-17 19:34:59 +01:00
Gaël Métais 92facbb9ef Remove experimental notice 2014-11-17 19:34:48 +01:00
Gaël Métais bddab92c31 Small text change 2014-11-17 19:33:47 +01:00
Gaël Métais 3b94a4520c Reorganize menu 2014-11-16 20:07:24 +01:00
Gaël Métais 88beb41143 Reorganize menu 2014-11-16 20:07:12 +01:00
Gaël Métais 2f06f4d8d7 Increase warning threshold for cssDuplicatedProperties 2014-11-14 13:49:09 +01:00
Gaël Métais dc5bd270ea Tweek CSS complexity score 2014-11-13 09:07:17 +01:00
Gaël Métais 6bb2be532a Add cssRedundantChildNodesSelectors rule 2014-11-12 11:23:47 +01:00
Gaël Métais ace02138ff Merge branch 'analyze-css-0.7.0' 2014-11-12 09:04:01 +01:00
Gaël Métais c85d5042e0 Merge domComplexity module from Phantomas 1.7.0 2014-11-12 08:17:12 +01:00
Gaël Métais bd66dedf71 Merge CSS parsing errors from phantomas 1.7.0 2014-11-11 23:31:57 +01:00
Gaël Métais aa34f60ce9 Merge phantomas 1.7.0 new functionnalities 2014-11-11 23:29:54 +01:00
Gaël Métais 9249b7ae3e Update phantomas to 1.7.0 2014-11-11 23:28:39 +01:00
Gaël Métais bc3323eb8b Label on warningsFilter checkbox 2014-11-05 19:09:19 +01:00
Gaël Métais 89b45163e9 Warning if css rules nbr > dom elements count 2014-11-05 19:07:36 +01:00
Gaël Métais 1e1ed54750 Change the message of DOM manipulations in body 2014-11-04 19:47:35 +01:00
Gaël Métais 595d0156ea Adjust timeline width 2014-11-03 17:38:09 +01:00
Gaël Métais 9f35d4d3cd Revert input url 2014-11-03 17:36:31 +01:00
Gaël Métais b3ec3512b9 Change main input type from text to url 2014-11-03 17:18:35 +01:00
Gaël Métais 52b77468f4 Update packages 2014-11-03 17:10:57 +01:00
Gaël Métais 5276ad8a26 Version problem with css-analyze 2014-11-03 17:10:08 +01:00
Gaël Métais c3b4b30603 Warn when a jQuery function is called on an empty object 2014-11-03 16:45:10 +01:00
Gaël Métais 3ae318c9bc Warn when a query returns 0 results in timeline 2014-11-03 16:04:36 +01:00
Gaël Métais ddb95b2653 Warn when a query returns 0 results in timeline 2014-11-03 16:00:13 +01:00
Gaël Métais 5e6a7fc6b6 Warning more visible if bind on multiple elements 2014-11-03 11:32:47 +01:00
Gaël Métais a2217a9178 Fix 'undefined' arg in JS execution tree 2014-11-03 11:12:27 +01:00
Gaël Métais 1226b4cb60 No info icon when not needed 2014-11-03 09:47:33 +01:00
Gaël Métais 1b455367de Fixes on duplicated queries 2014-10-29 19:27:29 +01:00
Gaël Métais 35de0bdcf3 Remove durations on profiling table and improve the timeline 2014-10-29 00:25:28 +01:00
Gaël Métais 36375a507d Clean npm and bower repositories on server update 2014-10-28 16:31:32 +01:00
Gaël Métais fc5bf1efc5 update dependencies 2014-10-28 16:30:59 +01:00
Gaël Métais 39a9c04449 Even more precise positionning of navigation timings in timeline 2014-10-28 16:27:02 +01:00
Gaël Métais ecef623cde Move up the timestamp on the timeline hover 2014-10-27 18:43:17 +01:00
Gaël Métais d279959042 Remove the 'All Metrics' tab 2014-10-27 18:07:30 +01:00
Gaël Métais 9937670a2e Add rules for cssComplexSelectorsByAttribute and cssDuplicatedProperties 2014-10-27 16:57:51 +01:00
Gaël Métais ed3df192ab CSS parsing error now included in phantomas 2014-10-27 16:53:20 +01:00
Gaël Métais 13d4b075a8 update dependencies 2014-10-27 09:58:11 +01:00
Gaël Métais 65a4947884 Add rule about CSS @import (ready for analyze-css 0.7.0) 2014-10-23 16:53:32 +02:00
Gaël Métais 0083fcf0fc Fix bug when there are no global variables 2014-10-23 14:36:20 +02:00
Gaël Métais 130ae7d3f8 Better navigation timings precision in JS timeline 2014-10-23 14:23:44 +02:00
Gaël Métais 10cdd9cda4 Avoid problems with string.prototype.replace and 610 2014-10-22 08:59:33 +02:00
Gaël Métais 1887fd2d65 remove unused function 2014-10-21 23:12:26 +02:00
Gaël Métais edbcb4e937 Make it harder to get a good jQuery version note 2014-10-21 23:05:09 +02:00
Gaël Métais aef75751b2 Fix different domains warning 2014-10-20 18:12:59 +02:00
Gaël Métais d6b166cb9a Update dependencies 2014-10-20 10:04:49 +02:00
Gaël Métais 84a5624eb7 Make jsHint happy 2014-09-27 00:52:22 +02:00
Gaël Métais c1992dc340 Add JS execution in before DOM Ready to the bad practices 2014-09-27 00:48:18 +02:00
Gaël Métais 6ca13ae5da Mark in red extreme values 2014-09-26 23:23:17 +02:00
Gaël Métais 6bc9aa2e5d Fix less compilation 2014-09-26 23:22:43 +02:00
Gaël Métais 306d1ee953 Remove font-icon generation from build, timing out too often 2014-09-26 17:06:49 +02:00
Gaël Métais d765925ba3 Update dependencies 2014-09-26 16:53:54 +02:00
Gaël Métais a02fd2496f Better handling of CSS parsing errors 2014-09-26 16:50:42 +02:00
Gaël Métais 06ea90f073 Merge a few criterium into the DOM Manipulations note 2014-09-26 11:00:59 +02:00
Gaël Métais fa120ed43f The avg nbr of global variables is prety high... 2014-09-25 19:05:02 +02:00
Gaël Métais 447cf90887 Sorry, i added a global variable 2014-09-25 19:04:03 +02:00
Gaël Métais a8fda1d0e1 Some comments about modules 2014-09-25 18:30:56 +02:00
Gaël Métais 312e33d15d Handle the case when the CSS parsing fails 2014-09-25 18:15:37 +02:00
Gaël Métais dd9e41252c About Domain Sharding 2014-09-25 16:51:40 +02:00
Gaël Métais 47397b7b57 Remove comments 2014-09-25 16:51:07 +02:00
Gaël Métais db3bafd3d2 Remove the don't spread warning 2014-09-25 16:27:26 +02:00
Gaël Métais 0fc6f85410 Fix some wordings 2014-09-25 16:08:24 +02:00
Gaël Métais 188d3ecaaa Tooltiping complete 2014-09-25 14:52:01 +02:00
Gaël Métais 0e18ec3a7b Add Travis build badge on README 2014-09-24 14:02:57 +02:00
Gaël Métais ad25e33bb3 Try to build with Travis 2014-09-24 13:44:30 +02:00
Gaël Métais f112b3d676 And remove Node v0.8 test in Travis 2014-09-24 13:35:03 +02:00
Gaël Métais ed1dcd55ce Remove production only npm install for Travis 2014-09-24 13:33:43 +02:00
Gaël Métais 12f68312c6 Remove Node 0.6 version in Travis 2014-09-24 13:28:19 +02:00
Gaël Métais 9cf3e2e713 Avoid install of dev-dependencies on Travis 2014-09-24 13:15:03 +02:00
Gaël Métais 8f789181f5 Test on older versions of NodeJS 2014-09-24 13:12:08 +02:00
Gaël Métais 3cf80c4a4a Trying Travis 2014-09-24 13:08:01 +02:00
Gaël Métais ad00e3f794 More tooltips 2014-09-24 13:00:43 +02:00
Gaël Métais 491ee93c69 Make it easier to have a good CSS complexity score 2014-09-24 13:00:22 +02:00
Gaël Métais 87bd8339ee More tooltips 2014-09-24 01:00:00 +02:00
Gaël Métais 63e99b2dc4 Looking for spell check 2014-09-24 00:41:13 +02:00
Gaël Métais 022019bffd Sort global variables alphabetically 2014-09-23 21:58:57 +02:00
Gaël Métais 07083d0098 Bad metric was displayed 2014-09-23 11:02:36 +02:00
Gaël Métais c2abf5a7a4 Increase a little more the test timeout 2014-09-23 10:50:48 +02:00
Gaël Métais fd5078b540 Fix error code replaced by 1002 2014-09-23 10:41:23 +02:00
Gaël Métais 3ff2b36cca Tooltips on summary (to be continued) 2014-09-23 09:45:30 +02:00
Gaël Métais b51149a8a4 New table layout for notations 2014-09-22 18:52:20 +02:00
Gaël Métais 32dc60242f Update dev-dependencies 2014-09-22 15:59:48 +02:00
Gaël Métais c9707502d3 Modal window closes on Esc keypress 2014-09-22 15:52:56 +02:00
Gaël Métais db7d07af7a Add multipleRequests to network problems list 2014-09-22 15:52:10 +02:00
Gaël Métais b2ea3515a5 Update dependencies 2014-09-22 10:20:02 +02:00
Gaël Métais 2865c922d7 Remove unused 'caller' in the JS execution tree 2014-09-20 00:17:55 +02:00
Gaël Métais 46fb4188f0 Show offenders buttons on the summary page 2014-09-20 00:02:19 +02:00
Gaël Métais 564884ca08 Hey Bower, stay in bower_componenets directory please 2014-09-19 18:05:42 +02:00
Gaël Métais 41ddd4961f Added new Network related notations 2014-09-19 17:08:46 +02:00
Gaël Métais e593d80f51 Update dependency 2014-09-18 22:56:08 +02:00
Gaël Métais f122188ed1 Requests notation 2014-09-18 22:29:02 +02:00
Gaël Métais aeb1da74fe Small adjustments in notations 2014-09-18 16:43:11 +02:00
Gaël Métais e3c2f0272f Add DOM IDs duplicated to DOM Complexity notation 2014-09-18 15:50:43 +02:00
Gaël Métais 2714b2bd11 Update Phantomas version 2014-09-17 13:22:05 +02:00
Gaël Métais 135cddb121 Preparation for summary tooltips 2014-09-16 19:15:01 +02:00
Gaël Métais 3f06c28595 Menu wording 2014-09-16 19:08:07 +02:00
Gaël Métais 7c5c4821f2 New CSS Count notation 2014-09-15 18:50:42 +02:00
Gaël Métais ab100010d8 Add new CSS metrics with analyze-css 2014-09-15 18:21:48 +02:00
Gaël Métais 4a3f83730e Introduce DomInteractive in the timeline 2014-09-15 12:35:11 +02:00
Gaël Métais 38520cf14f Wording 2014-09-12 18:35:46 +02:00
Gaël Métais c3653a1725 Package update 2014-09-12 16:38:42 +02:00
Gaël Métais 94f065f4f8 Add background color to the timestamp column in the profiler 2014-09-12 12:42:38 +02:00
Gaël Métais 5b50809291 Timeline: fix subpixel width in Safari 2014-09-12 12:16:42 +02:00
Gaël Métais e1f7ca293c wording 2014-09-12 11:04:20 +02:00
Gaël Métais aa9ba625a1 Improve timeline 2014-09-12 10:19:41 +02:00
Gaël Métais 3d7ac45e28 Fix timeline display of a test where domComplete is 0 (www.promovacances.com) 2014-09-11 19:29:55 +02:00
Gaël Métais d2055287cc Improve timeline legend 2014-09-11 18:59:34 +02:00
Gaël Métais 2074618fd5 Wording 2014-09-11 15:55:19 +02:00
Gaël Métais 8ecfcee83c Better fix for </script> in JSON bug 2014-09-10 18:48:35 +02:00
Gaël Métais 2b86a5fdc6 Fix HTML parsing bug when </script> is in the phantomas results json 2014-09-09 11:19:51 +02:00
Gaël Métais 8f152c11a9 Improve timeline 2014-09-09 10:35:49 +02:00
Gaël Métais 2d00ac81ea Brand new timeline graph 2014-09-08 19:30:57 +02:00
Gaël Métais bf752790f2 Stricter package.json 2014-09-08 11:25:50 +02:00
Gaël Métais 02f2f4d513 Wording 2014-09-06 11:28:01 +02:00
Gaël Métais 69bd752763 Fix [Object] in spaghetti 2014-09-05 23:14:36 +02:00
Gaël Métais a4015aac37 Send the CSS animation to the GPU 2014-09-05 23:01:41 +02:00
Gaël Métais b730249b34 The server auto-kills when a major error occurs 2014-09-05 19:04:25 +02:00
Gaël Métais 839a891e9d Put the official URL on GitHub 2014-09-05 17:08:37 +02:00
Gaël Métais 3058765b48 More readable footer 2014-09-05 16:56:54 +02:00
Gaël Métais 9ccce18e13 Add a favicon 2014-09-05 16:44:25 +02:00
Gaël Métais 831d267106 Add bower and npm update in update script 2014-09-05 16:12:53 +02:00
Gaël Métais 85654aa7a8 Chmod the server_update shell 2014-09-05 16:09:33 +02:00
Gaël Métais 849e5a24a9 Very simple delivery script 2014-09-05 16:01:06 +02:00
Gaël Métais 87717ba448 Catch the JSON undefined error 2014-09-05 15:23:16 +02:00
Gaël Métais 7b6b7bc58d Add a spinner on the loading page 2014-09-05 14:32:49 +02:00
Gaël Métais 9772403609 Increase stack size of NodeJS to avoid some bugs sometime 2014-09-05 13:44:48 +02:00
Gaël Métais 45a9abebe3 Better DOM duplicated queries handling 2014-09-05 12:05:00 +02:00
Gaël Métais 2829c8de38 Fix rare bug on duplicated queries 2014-09-05 11:52:52 +02:00
Gaël Métais 43109a8274 First beta version is 0.1.0 2014-09-05 11:52:18 +02:00
Gaël Métais 58b9a44554 Catch the JSON undefined error 2014-09-05 11:18:55 +02:00
Gaël Métais 6e8c73dfe0 Catch the JSON undefined error 2014-09-05 11:11:13 +02:00
Gaël Métais 2e25cd404b Catch the JSON undefined error 2014-09-05 10:52:57 +02:00
Gaël Métais 21c3a7590e Align timestamp column to center 2014-09-05 10:42:51 +02:00
Gaël Métais e06d7f9798 Add Google Analytics in production 2014-09-04 19:23:28 +02:00
Gaël Métais 20530f2cc0 Fix bug when results folder doesn't exist 2014-09-04 18:06:48 +02:00
Gaël Métais bcbcdb050a Don't install devDependencies in production 2014-09-04 17:33:28 +02:00
Gaël Métais c76711314a Redirect www to non-www for SEO 2014-09-04 17:31:18 +02:00
Gaël Métais 6a212b50e6 Wiki page for private instance 2014-09-04 17:08:00 +02:00
Gaël Métais 73dc5a6332 Same header and same footer on all pages 2014-09-04 16:28:54 +02:00
Gaël Métais 24d9690b09 New icons from IcoMoon 2014-09-04 16:15:24 +02:00
Gaël Métais a8af5915e3 jsHint 2014-09-04 14:15:38 +02:00
Gaël Métais 01f5c52104 Had a problem on npm install 2014-09-04 14:00:32 +02:00
Gaël Métais 4fefb54a13 Improved home page 2014-09-04 13:48:49 +02:00
Gaël Métais 6292bf10b9 Update version number 2014-09-04 11:32:11 +02:00
Gaël Métais 8aa0b9e63d Add explainations on the Spaghetti page 2014-09-03 19:54:17 +02:00
Gaël Métais 3e8d2e25aa Add an 'experimental' flag 2014-09-03 19:27:50 +02:00
Gaël Métais d86b93e646 Add a special line color for Window Performances 2014-09-03 19:26:09 +02:00
Gaël Métais a7493fc4a4 Not needed 2014-09-03 18:44:58 +02:00
Gaël Métais 81416240bc Fix DOM Complexity note 2014-09-03 18:16:16 +02:00
Gaël Métais eb4e8681d6 Fix tests 2014-09-03 17:38:38 +02:00
Gaël Métais 8c260fe5ed Improved Summary page 2014-09-03 17:21:42 +02:00
Gaël Métais 61f97f4856 New notations on Summary : jQuery version and Events bound 2014-09-03 16:19:32 +02:00
Gaël Métais 87ecb47d3d New better duplicated DOM query counter 2014-09-03 14:31:13 +02:00
Gaël Métais 60a30c594e New better duplicated DOM query counter 2014-09-03 14:29:47 +02:00
Gaël Métais 565d1185ab Validate the test even when it's in timeout 2014-09-03 13:38:40 +02:00
Gaël Métais bd3bf06940 Comment wording 2014-09-03 09:07:47 +02:00
Gaël Métais dd9ed8af74 Fix bug test error code always being 1001 2014-09-03 09:07:14 +02:00
Gaël Métais 1d66d9658c First server tests 2014-09-03 09:06:35 +02:00
Gaël Métais 3b365772d2 New JS bad practices notation 2014-09-02 10:20:29 +02:00
Gaël Métais 1570a4ce92 Starting to construct the Summary dashboard 2014-09-01 19:46:05 +02:00
Gaël Métais 2570033791 Excution tree timestamp is now relative to responseEndTime 2014-09-01 19:44:54 +02:00
Gaël Métais a9f6ac36b3 Add window.performance to the execution tree 2014-09-01 19:43:44 +02:00
Gaël Métais 67e501030b Update packages 2014-09-01 19:40:24 +02:00
Gaël Métais 276cc5f479 Add gzip compression to Express 2014-08-29 13:46:28 +02:00
Gaël Métais 1f4d658e09 Tribute to Phantomas 2014-08-25 19:06:31 +02:00
Gaël Métais 3170deef28 Add addEventListeners to the JS execution tree 2014-08-25 18:33:35 +02:00
Gaël Métais 96e5eb72df Improve Phantomas metrics reporting 2014-08-25 17:31:07 +02:00
Gaël Métais fce76be041 Improve Phantomas metrics reporting 2014-08-25 17:30:24 +02:00
Gaël Métais 64488cda02 Remove spy on .trigger() as it hides some real DOM manipulations 2014-08-25 17:29:17 +02:00
Gaël Métais b3980706b6 Fix menu bug 2014-08-25 14:46:59 +02:00
Gaël Métais dcd06a629a Remove some phantomas.log, i want phantomas to be fast 2014-08-25 14:32:39 +02:00
Gaël Métais ab7e95759d app/public/styles/results.css 2014-08-25 14:31:28 +02:00
Gaël Métais c77236b71c Total JS time 2014-08-25 14:30:45 +02:00
Gaël Métais 8c6666a33c Add Phantomas metadata to the page 2014-08-25 14:20:29 +02:00
Gaël Métais fc457926a5 Fix bug on the error reporting 2014-08-25 14:19:35 +02:00
Gaël Métais d8f6780ee4 No angular routes for the moment 2014-08-25 14:18:59 +02:00
Gaël Métais 714d35aa27 New offender for maxDepth 2014-08-20 14:30:49 +02:00
Gaël Métais cefec924ce Fix backtrace not being displayed on errors 2014-08-20 12:07:58 +02:00
Gaël Métais 82a1f4f934 Wording 2014-08-20 10:32:07 +02:00
Gaël Métais 28415a79d5 Outputs the phantomas command in the console 2014-08-20 09:49:55 +02:00
Gaël Métais 6b644e0078 Fix DOMinsert count problem 2014-08-20 09:48:29 +02:00
Gaël Métais c140fea9e9 License and author 2014-08-14 18:11:10 +02:00
Gaël Métais cff3dcdc87 Installation instructions in README 2014-08-14 17:07:44 +02:00
Gaël Métais 5dfa5b645d Add dependecy status badge to REAMDE 2014-08-11 11:34:35 +02:00
Gaël Métais 3001009d3f Auto retry twice if test failed 2014-08-11 11:27:50 +02:00
Gaël Métais 709ee60db3 Add Grunt and JsHint 2014-08-07 19:27:32 +02:00
Gaël Métais 7fcdf2aaa2 Splitted the node server in modules 2014-08-07 19:26:58 +02:00
Gaël Métais 9b6b0f39f0 Add a line when a JS file is loaded 2014-08-06 18:25:52 +02:00
Gaël Métais ec3b77f47d Add warnings and advices on details popin 2014-08-06 18:25:01 +02:00
Gaël Métais e2532ad16e Fix js error 2014-08-06 18:23:46 +02:00
Gaël Métais c83ca394ac It's strange to be wainting behind 0 test 2014-08-06 18:23:04 +02:00
Gaël Métais 27965eb923 Warning when bind on multiple elements 2014-08-06 18:22:26 +02:00
Gaël Métais 5dd0fa59c4 Fix query duration not being displayed 2014-08-05 09:55:25 +02:00
Gaël Métais 3f3e250a49 Not sure it's the best error handling method but it works 2014-08-05 00:26:34 +02:00
Gaël Métais df4c93356e Back to a clean phantomas version + modules 2014-08-05 00:08:22 +02:00
Gaël Métais aba5ef5a51 Server install bash 2014-08-01 23:12:16 +02:00
Gaël Métais 759dc3f8b5 Add angular-route 2014-08-01 23:10:39 +02:00
Gaël Métais 13ddba6457 Merge GitHub auto-generated files 2014-08-01 16:36:41 +02:00
Gaël Métais 8b30366de7 Initial commit 2014-08-01 16:24:43 +02:00
63 changed files with 5163 additions and 550 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"directory" : "bower_components"
}
+5 -1
View File
@@ -1,3 +1,7 @@
node_modules
bower_components
results/*
.vagrant
results/*
coverage
har.json
+8
View File
@@ -0,0 +1,8 @@
language: node_js
node_js:
- "0.10"
before_install:
- "npm install -g grunt-cli"
- "npm install -g phantomjs"
install: npm install
before_script: grunt build
+105
View File
@@ -0,0 +1,105 @@
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
font: {
icons: {
src: ['app/public/fonts/svg-icons/*.svg'],
destCss: 'app/public/styles/less/icons.less',
destFonts: 'app/public/fonts/icons.woff',
// Optional: Custom routing of font filepaths for CSS
cssRouter: function (fontpath) {
var pathArray = fontpath.split('/');
var fileName = pathArray[pathArray.length - 1];
return '/public/fonts/' + fileName;
}
}
},
less: {
all: {
files: {
'app/public/styles/main.css': [ 'app/public/styles/less/main.less' ],
'app/public/styles/index.css': [ 'app/public/styles/less/index.less' ],
'app/public/styles/launchTest.css': [ 'app/public/styles/less/launchTest.less' ],
'app/public/styles/results.css': [ 'app/public/styles/less/results.less' ]
}
}
},
jshint: {
all: [
'*.js',
'app/lib/*',
'app/nodeControllers/*.js',
'app/public/scripts/*.js',
'phantomas_custom/**/*.js'
]
},
clean: {
icons: {
src: ['tmp']
},
coverage: {
src: ['coverage/']
}
},
copy: {
coverage: {
src: ['test/**'],
dest: 'coverage/'
}
},
blanket: {
coverage: {
src: ['app/'],
dest: 'coverage/app/'
}
},
mochaTest: {
test: {
options: {
reporter: 'spec',
},
src: ['coverage/test/server/*.js']
},
coverage: {
options: {
reporter: 'html-cov',
// use the quiet flag to suppress the mocha console output
quiet: true,
// specify a destination file to capture the mocha
// output (the quiet option does not suppress this)
captureFile: 'coverage/coverage.html'
},
src: ['coverage/test/server/*.js']
}
}
});
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
grunt.registerTask('icons', [
'font:icons',
'less',
'clean:icons'
]);
grunt.registerTask('build', [
'jshint',
'less'
]);
grunt.registerTask('hint', [
'jshint'
]);
grunt.registerTask('test', [
'clean:coverage',
'blanket',
'copy:coverage',
'mochaTest'
]);
};
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+37
View File
@@ -0,0 +1,37 @@
# Yellow Lab Tools
Online tool designed to help Front-End developers optimize their website
[![Dependency Status](https://gemnasium.com/gmetais/YellowLabTools.svg)](https://gemnasium.com/gmetais/YellowLabTools) [![Build Status](https://travis-ci.org/gmetais/YellowLabTools.svg?branch=master)](https://travis-ci.org/gmetais/YellowLabTools)
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
## How it works
The tool is based on the fabulous [Phantomas](https://github.com/macbre/phantomas) by Maciej Brencz, that loads a page and collects many web performance metrics.
I rewrote some modules to deeper analyze Javascript interactions with the DOM (especially with jQuery).
And then it is wrapped inside a small NodeJS server to get an interface (inspired by my favorite tool: [WebPageTest](http://www.webpagetest.org/)).
**There are so many things left to do, your help would be greatly appreciated! Please report bugs, ask for evolutions and come code with me.**
## Help needed
I'm currently looking for help in correcting the english wordings in the tool, as it is not my native language.
## Install your own instance
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-own-instance).
## License
Please read the [license](LICENSE).
Icons are the property of [IcoMoon.io](https://icomoon.io/)
## Author
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
Vendored
+17
View File
@@ -0,0 +1,17 @@
Vagrant.configure("2") do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.network :private_network, ip: "10.10.10.10"
config.ssh.forward_agent = true
config.vm.provider :virtualbox do |vb|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--memory", 512]
vb.customize ["modifyvm", :id, "--cpus", 2]
end
config.vm.provision :shell, :path => "server_config/server_install.sh"
end
+104
View File
@@ -0,0 +1,104 @@
/**
* Yellow Lab Tools main file
*/
var async = require('async');
var phantomas = require('phantomas');
var PhantomasWrapper = function() {
'use strict';
/**
* This is the phantomas launcher. It merges user chosen options into the default options
* Available options :
*
* - timeout : in seconds (default 60)
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree (reported durations of main tasks will be slower than usual)
*
*/
this.execute = function(task, callback) {
var options = {
// Cusomizable options
timeout: task.options.timeout || 60,
'js-deep-analysis': task.options.jsDeepAnalysis || false,
// Mandatory
reporter: 'json:pretty',
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'domComplexity', // overriden
'domMutations', // not compatible with webkit
'domQueries', // overriden
'eventListeners', // overridden
'filmStrip', // not needed
'har', // not needed for the moment
'pageSource', // not needed
'screenshot', // not needed for the moment
'waitForSelector', // not needed
'windowPerformance' // overriden
].join(','),
'include-dirs': [
'phantomas_custom/core',
'phantomas_custom/modules'
].join(',')
};
// Output the command line for debugging purpose
console.log('If you want to reproduce the phantomas task only, copy the following command line:');
var optionsString = '';
for (var opt in options) {
optionsString += ' ' + '--' + opt + '=' + options[opt];
}
console.log('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
// Kill the application if nothing happens for 10 minutes
var killer = setTimeout(function() {
console.log('Killing the server because the test ' + task.testId + ' on ' + task.url + ' was launched 10 minutes ago');
// Forever will restart the server
process.exit(1);
}, 600000);
// It's time to launch the test!!!
var triesNumber = 3;
async.retry(triesNumber, function(cb) {
phantomas(task.url, options, function(err, json, results) {
console.log('Returning from Phantomas');
// Adding some YellowLabTools errors here
if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
err = 1001;
}
if (!err && (!json || !json.metrics)) {
err = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (err === 252 && json) {
console.log('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
err = null;
}
if (err) {
console.log('Attempt failed for test id ' + task.testId + '. Error code ' + err);
}
cb(err, {json: json, results: results});
});
}, function(err, data) {
clearTimeout(killer);
if (err) {
console.log('All ' + triesNumber + ' attemps failed for test id ' + task.testId);
}
callback(err, data.json, data.results);
});
};
};
module.exports = new PhantomasWrapper();
+8
View File
@@ -0,0 +1,8 @@
/**
* Alternative to the standard String.prototype.replace function
* Avoids problems with $$, $1, $2, ...
*/
module.exports = function(str, searched, replacement) {
return str.split(searched).join(replacement);
};
+66
View File
@@ -0,0 +1,66 @@
/**
* Creation of a queue and it's worker function
*/
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var phantomasWrapper = require('./phantomasWrapper');
var testQueue = function() {
'use strict';
var currentTask = null;
var self = this;
var queue = async.queue(function(task, callback) {
currentTask = task;
console.log('Starting test ' + task.testId);
phantomasWrapper.execute(task, function(err, json, results) {
console.log('Test ' + task.testId + ' complete');
currentTask = null;
callback(err, json, results);
self.emit('queueMoving');
});
});
// Use this method to add a test to the queue
this.push = queue.push;
// Gives the position of a task in the queue
// Returns 0 if it is the current running task
// Returns -1 if not found
this.indexOf = function(testId) {
if (currentTask && currentTask.testId === testId) {
return 0;
}
var position = -1;
if (queue.length() > 0) {
queue.tasks.forEach(function(task, index) {
if (task.data.testId === testId) {
position = index + 1;
}
});
}
return position;
};
this.testComplete = function(testId) {
self.emit('testComplete', testId);
};
this.testFailed = function(testId) {
self.emit('testFailed', testId);
};
};
// extend the EventEmitter class
util.inherits(testQueue, EventEmitter);
module.exports = new testQueue();
+27
View File
@@ -0,0 +1,27 @@
/**
* Yellow Lab Tools home page controller
*/
var async = require('async');
var fs = require ('fs');
var strReplace = require('../lib/strReplace');
var indexController = function(req, res, googleAnalyticsId) {
'use strict';
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/node_views/index.html', {encoding: 'utf8'}, callback);
}
}, function(err, results) {
var html = results.htmlTemplate;
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
};
module.exports = indexController;
@@ -0,0 +1,85 @@
/**
* Controller for the test launching page (the waiting page, after the user submited a test on the index page)
*/
var async = require('async');
var fs = require ('fs');
var strReplace = require('../lib/strReplace');
var launchTestController = function(req, res, testQueue, googleAnalyticsId) {
'use strict';
// Generate test id
var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
var url = req.body.url;
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
url = 'http://' + url;
}
var options = {};
if (req.body.timeout) {
options.timeout = req.body.timeout;
}
async.waterfall([
function htmlTemplate(callback) {
fs.readFile('./app/node_views/launchTest.html', {encoding: 'utf8'}, callback);
},
function sendResponse(html, callback) {
html = strReplace(html, '%%TEST_URL%%', url);
html = strReplace(html, '%%TEST_ID%%', testId);
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
callback();
},
function createFolder(callback) {
// Create results folder
fs.mkdir(resultsPath, callback);
},
function executePhantomas(callback) {
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
var task = {
testId: testId,
url: url,
options: options
};
testQueue.push(task, callback);
},
function writeResults(json, resultsObject, callback) {
console.log('Saving Phantomas results file to ' + phantomasResultsPath);
fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
}
], function(err) {
if (err) {
console.log('An error occured in the phantomas test: ', err);
fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
if (err) {
console.log('Could not even write an error message on file ' + phantomasResultsPath);
console.log(err);
}
});
testQueue.testFailed(testId);
} else {
testQueue.testComplete(testId);
}
});
};
module.exports = launchTestController;
+47
View File
@@ -0,0 +1,47 @@
/**
* The page that dispays the results
*/
var async = require('async');
var fs = require('fs');
var strReplace = require('../lib/strReplace');
var resultsController = function(req, res, googleAnalyticsId) {
'use strict';
var testId = req.params.testId;
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
console.log('Opening test ' + testId + ' results as HTML');
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/node_views/results.html', {encoding: 'utf8'}, callback);
},
phantomasResults: function(callback) {
fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
}
}, function(err, results) {
if (err) {
console.log(err);
return res.status(404).send('Sorry, test not found...');
}
// Escape "</script>" because it can interfer with the HTML parser
var phantomasResults = results.phantomasResults;
phantomasResults = phantomasResults.replace(/<\/script>/g, '\\u003c/script>');
var html = results.htmlTemplate;
html = strReplace(html, '%%RESULTS%%', phantomasResults);
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
};
module.exports = resultsController;
@@ -0,0 +1,60 @@
/**
* Socket.io handler
*/
var fs = require('fs');
var waitingQueueSocket = function(socket, testQueue) {
socket.on('waiting', function(testId) {
console.log('User waiting for test id ' + testId);
sendTestStatus(testId);
testQueue.on('testComplete', function(id) {
if (testId === id) {
socket.emit('complete');
console.log('Sending complete event to test id ' + testId);
}
});
testQueue.on('testFailed', function(id) {
if (testId === id) {
socket.emit('failed');
console.log('Sending failed event to test id ' + testId);
}
});
testQueue.on('queueMoving', function() {
var positionInQueue = testQueue.indexOf(testId);
if (positionInQueue >= 0) {
socket.emit('position', positionInQueue);
console.log('Sending position to test id ' + testId);
}
});
});
// Finds the status of a test and send it to the client
function sendTestStatus(testId) {
// Check task position in queue
var positionInQueue = testQueue.indexOf(testId);
if (positionInQueue >= 0) {
socket.emit('position', positionInQueue);
} else {
// Find in results files
var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
if (exists) {
// TODO : find a way to make sure the file is completely written
setTimeout(function() {
socket.emit('complete');
}, 4000);
} else {
socket.emit('404');
}
});
}
}
};
module.exports = waitingQueueSocket;
+35
View File
@@ -0,0 +1,35 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools</title>
<link rel="icon" type="image/png" href="/public/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/index.css">
</head>
<body>
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
<form method="post" action="/launchTest">
<input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
</form>
<p>(This is a BETA, your feedback is more than welcome)</p>
<div class="footer">
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
</div>
<script>
var GA_ID = '%%GA_ID%%';
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', GA_ID, 'auto');
ga('send', 'pageview');
}
</script>
</body>
</html>
+97
View File
@@ -0,0 +1,97 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools - Awaiting</title>
<link rel="icon" type="image/png" href="/public/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
<div id="status"></div>
<div>%%TEST_URL%%</div>
<div class="footer">
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
</div>
<script>
var testId = '%%TEST_ID%%';
var statusElement = document.getElementById('status');
var socket = io();
function askStatus() {
socket.emit('waiting', testId);
}
socket.on('position', function(position) {
if (position === 0) {
statusElement.innerHTML = 'Test is running...';
} else {
statusElement.innerHTML = 'Waiting behind ' + (position) + ' other tests';
}
sendPositionInQueueToGA(position);
// Rotate the spinner
document.body.className = 'waiting';
});
socket.on('complete', function() {
statusElement.innerHTML = 'Test complete';
window.location.replace('/results/' + testId);
// Stop the spinner
document.body.className = '';
});
socket.on('failed', function() {
statusElement.innerHTML = 'Test failed';
window.location.replace('/results/' + testId);
// Stop the spinner
document.body.className = '';
});
socket.on('404', function() {
statusElement.innerHTML = 'Test not found';
// Stop the spinner
document.body.className = '';
});
socket.on('reconnect_error', function() {
statusElement.innerHTML = "Server reboot. Please start a new test."
// Stop the spinner
document.body.className = '';
});
askStatus();
var GA_ID = '%%GA_ID%%';
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', GA_ID, 'auto');
}
// Need stats on average queue position
var positionAlreadySent = false;
function sendPositionInQueueToGA(position) {
if (!positionAlreadySent && GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
ga('send', 'pageview', {
queuePosition: position
});
positionAlreadySent = true;
}
}
</script>
</body>
</html>
+933
View File
@@ -0,0 +1,933 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools - Results page</title>
<link rel="icon" type="image/png" href="/public/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/results.css">
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/ngModal/dist/ng-modal.js"></script>
<script src="/public/scripts/app.js"></script>
<script src="/public/scripts/resultsCtrl.js"></script>
<script src="/public/scripts/showOffenders.js"></script>
</head>
<body ng-app="YellowLabTools" ng-controller="ResultsCtrl">
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
<div ng-if="undefined">Untangling and counting the spaghettis...</div>
<div class="ng-cloak">
<div>Tested url: &nbsp; <a class="testedUrl" href="{{phantomasResults.url}}" target="_blank">{{phantomasResults.url}}</a></div>
<div class="resultsMenu">
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'execution'}" ng-click="setView('execution')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'summary'}" ng-click="setView('summary')"><div class="icon-summary"></div><span>Grades</span></div>
</div>
<div ng-if="phantomasResults.error">
<h2>Error: {{phantomasResults.error}}</h2>
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
<div ng-if="phantomasResults.error == 1001">Javascript execution tree error</div>
<div ng-if="phantomasResults.error == 1002">JSON undefined error</div>
</div>
<div ng-show="view == 'summary' && !phantomasResults.error" class="summary">
<h2>Grades</h2>
<div class="notations">
<div>
<div ng-class="notations.domComplexity">{{notations.domComplexity}}</div>
<div class="notation">DOM complexity</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.DOMelementsCount" ng-class="{'warning': phantomasResults.metrics.DOMelementsCount > 5000}">
<div class="label">DOM elements count</div>
<div class="result">
{{phantomasResults.metrics.DOMelementsCount}}
</div>
<div class="info">
<div class="icon-question" ng-click="DOMelementsCountTooltip = true"></div>
<modal-dialog show="DOMelementsCountTooltip" dialog-title="Avoid too many DOM elements">
<p>A high number of DOM elements means a lot of work for the browser to render the page.</p>
<p>It also slows down Javascript DOM queries, as there are more elements to search through.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMelementMaxDepth" ng-class="{'warning': phantomasResults.metrics.DOMelementMaxDepth > 30}">
<div class="label">DOM max depth</div>
<div class="result">
{{phantomasResults.metrics.DOMelementMaxDepth}}
<show-offenders modal-title="DOM max depth" metric-name="DOMelementMaxDepth" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="Reduce DOM depth">
<p>A deep DOM makes the CSS matching with DOM elements difficult.</p>
<p>It also slows down Javascript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for Javascript events, that bubble up to the document root.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.iframesCount" ng-class="{'warning': phantomasResults.metrics.iframesCount > 30}">
<div class="label">Number of iframes</div>
<div class="result">
{{phantomasResults.metrics.iframesCount}}
</div>
<div class="info">
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="iFrames are slow">
<p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMidDuplicated" ng-class="{'warning': phantomasResults.metrics.DOMidDuplicated > 20}">
<div class="label">IDs duplicated</div>
<div class="result">
{{phantomasResults.metrics.DOMidDuplicated}}
<show-offenders modal-title="DOM IDs duplicated" metric-name="DOMidDuplicated" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="DOMidDuplicatedTooltip = true"></div>
<modal-dialog show="DOMidDuplicatedTooltip" dialog-title="Warning: duplicated DOM IDs">
<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.jsDomManipulations">{{notations.jsDomManipulations}}</div>
<div class="notation">DOM manipulations</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.DOMinserts" ng-class="{'warning': phantomasResults.metrics.DOMinserts > 1000}">
<div class="label">DOM inserts</div>
<div class="result">
{{phantomasResults.metrics.DOMinserts}}
<show-offenders modal-title="DOM inserts" metric-name="DOMinserts" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="DOMinsertsTooltip = true"></div>
<modal-dialog show="DOMinsertsTooltip" dialog-title="DOM insertions are slow">
<p>Working with the DOM in Javascript triggers layout calculations and slows down the page.</p>
<p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMqueries" ng-class="{'warning': phantomasResults.metrics.DOMqueries > 2000}">
<div class="label">DOM queries</div>
<div class="result">
{{phantomasResults.metrics.DOMqueries}}
</div>
<div class="info">
<div class="icon-question" ng-click="DOMqueriesTooltip = true"></div>
<modal-dialog show="DOMqueriesTooltip" dialog-title="Avoid having too many DOM queries">
<p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p>
<p>Try to reduce the number of queries by refactoring your Javascript code.</p>
<p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMqueriesAvoidable" ng-class="{'warning': phantomasResults.metrics.DOMqueriesAvoidable > 500}">
<div class="label">Avoidable queries</div>
<div class="result">
{{phantomasResults.metrics.DOMqueriesAvoidable}}
<show-offenders modal-title="Duplicated DOM queries" metric-name="DOMqueriesDuplicated" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="duplicatedQueriesCountAllTooltip = true"></div>
<modal-dialog show="duplicatedQueriesCountAllTooltip" dialog-title="Cache duplicated DOM queries">
<p>This is the number of queries that could be avoided by removing all duplicated queries.</p>
<p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.eventsBound" ng-class="{'warning': phantomasResults.metrics.eventsBound > 2000}">
<div class="label">Events bound</div>
<div class="result">
{{phantomasResults.metrics.eventsBound}}
<show-offenders modal-title="Events bound" metric-name="eventsBound" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="eventsBoundTooltip = true"></div>
<modal-dialog show="eventsBoundTooltip" dialog-title="Reduce the number of events binding">
<p>Binding too many events has a cost.</p>
<p>It can be avoided by using "event delegation". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.jsBadPractices">{{notations.jsBadPractices}}</div>
<div class="notation">Bad Javascript</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.jsErrors" ng-class="{'warning': phantomasResults.metrics.jsErrors > 5}">
<div class="label">Javascript errors</div>
<div class="result">
{{phantomasResults.metrics.jsErrors}}
<show-offenders modal-title="Javascript errors" metric-name="jsErrors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="jsErrorsTooltip = true"></div>
<modal-dialog show="jsErrorsTooltip" dialog-title="Javascript errors">
<p>Just to let you know there are some errors on the page.</p>
<p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.evalCalls">
<div class="label">eval calls</div>
<div class="result">{{phantomasResults.metrics.evalCalls}}</div>
<div class="info">
<div class="icon-question" ng-click="evalCallsTooltip = true"></div>
<modal-dialog show="evalCallsTooltip" dialog-title="Eval is evil">
<p>The 'eval' function is slow and a bad coding practice. Try to get rid of it.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.documentWriteCalls">
<div class="label">document.write calls</div>
<div class="result">{{phantomasResults.metrics.documentWriteCalls}}</div>
<div class="info">
<div class="icon-question" ng-click="documentWriteCallsTooltip = true"></div>
<modal-dialog show="documentWriteCallsTooltip" dialog-title="document.write should have never existed">
<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p>
<p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href="https://github.com/krux/postscribe" target="_blank">PostScribe</a>.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.consoleMessages">
<div class="label">Console messages</div>
<div class="result">{{phantomasResults.metrics.consoleMessages}}</div>
<div class="info">
<div class="icon-question" ng-click="consoleMessagesTooltip = true"></div>
<modal-dialog show="consoleMessagesTooltip" dialog-title="Avoid using console.log()">
<p>Try to keep your console clean when in production. Debugging is good for development only.</p>
<p>Writing in the console has a cost, especially when dumping large object variables.</p>
<p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.globalVariables">
<div class="label">Global variables</div>
<div class="result">
{{phantomasResults.metrics.globalVariables}}
<show-offenders modal-title="Global variables" metric-name="globalVariables" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="globalVariablesTooltip = true"></div>
<modal-dialog show="globalVariablesTooltip" dialog-title="Minimize the use of global variables">
<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p>
<p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>
</modal-dialog>
</div>
</div>
<div ng-if="inBodyDomManipulations > 10" ng-class="{'warning': inBodyDomManipulations > 200}">
<div class="label">DOM manipulations in body</div>
<div class="result">
{{inBodyDomManipulations}}
</div>
<div class="info">
<div class="icon-question" ng-click="inBodyDomManipulationsTooltip = true"></div>
<modal-dialog show="inBodyDomManipulationsTooltip" dialog-title="Javascript executed inside the body">
<p>This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the Javascript before the DOMContentLoaded event.</p>
<p>Wait for this event before manipulating the DOM. Do not execute Javascript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.</p>
<p>The JS Timeline tab can help you identify what's happening.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.jQueryLoading">{{notations.jQueryLoading}}</div>
<div class="notation">jQuery version</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions == 1">
<div class="label">jQuery version</div>
<div class="result">{{phantomasResults.metrics.jQueryVersion}}</div>
<div class="info">
<div class="icon-question" ng-click="jQueryVersionTooltip = true"></div>
<modal-dialog show="jQueryVersionTooltip" dialog-title="Use the latest version of jQuery">
<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p>
<p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions > 1" class="warning">
<div class="label">{{phantomasResults.metrics.jQueryDifferentVersions}} versions loaded</div>
<div class="result">
<span ng-repeat="version in phantomasResults.offenders.jQueryDifferentVersions">{{version}}<span ng-show="!$last"> & </span></span>
</div>
<div class="info">
<div class="icon-question" ng-click="jQueryDifferentVersionsTooltip = true"></div>
<modal-dialog show="jQueryDifferentVersionsTooltip" dialog-title="Several versions of jQuery loaded">
<p>jQuery is a heavy library. You should **never** load jQuery more than one on the same page.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.cssComplexity">{{notations.cssComplexity}}</div>
<div class="notation">CSS complexity</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.cssRules" ng-class="{'warning': phantomasResults.metrics.cssRules > 4000 || (phantomasResults.metrics.cssRules > 1000 && phantomasResults.metrics.cssRules > phantomasResults.metrics.DOMelementsCount)}">
<div class="label">Rules count</div>
<div class="result">
{{phantomasResults.metrics.cssRules}}
</div>
<div class="info">
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
<modal-dialog show="cssRulesTooltip" dialog-title="Clean up the CSS files">
<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p>
<p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom files per page.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssComplexSelectors" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectors > 2000}">
<div class="label">Complex selectors</div>
<div class="result">
{{phantomasResults.metrics.cssComplexSelectors}}
<show-offenders modal-title="Complex selectors" metric-name="cssComplexSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssComplexSelectorsTooltip = true"></div>
<modal-dialog show="cssComplexSelectorsTooltip" dialog-title="Avoid complex selectors">
<p>Complex selectors are CSS selectors with 4 or more expressions, like "#header ul li .foo".</p>
<p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssComplexSelectorsByAttribute" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectorsByAttribute > 100}">
<div class="label">Complex attributes selector</div>
<div class="result">
{{phantomasResults.metrics.cssComplexSelectorsByAttribute}}
<show-offenders modal-title="Complex attributes selector" metric-name="cssComplexSelectorsByAttribute" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssComplexSelectorsByAttributeTooltip = true"></div>
<modal-dialog show="cssComplexSelectorsByAttributeTooltip" dialog-title="Avoid complex attribute expressions">
<p>Complex attributes selectors are one of these:
<ul>
<li>.foo[type*=bar] (contains bar)</li>
<li>.foo[type^=bar] (starts with bar)</li>
<li>.foo[type|=bar] (starts with bar or bar-)</li>
<li>.foo[type$=bar] (ends with bar)</li>
<li>.foo[type~=bar baz] (bar or baz)</li>
</ul>
</p>
<p>Their matching process needs more CPU and it has a cost on performances.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.badCss">{{notations.badCss}}</div>
<div class="notation">Bad CSS</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.cssParsingErrors" class="warning">
<div class="label">CSS syntax error</div>
<div class="result">
{{phantomasResults.metrics.cssParsingErrors}}
<show-offenders modal-title="CSS syntax errors" metric-name="cssParsingErrors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
<modal-dialog show="cssRulesTooltip" dialog-title="Parsing error while analyzing CSS">
<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p>
<p>Maybe a <a href="http://jigsaw.w3.org/css-validator" target="_blank">CSS validator</a> can help you.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssImports" class="warning">
<div class="label">Uses of @import</div>
<div class="result">
{{phantomasResults.metrics.cssImports}}
<show-offenders modal-title="Uses of @import" metric-name="cssImports" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssImportsTooltip = true"></div>
<modal-dialog show="cssImportsTooltip" dialog-title="Don't use @import">
<p>Its bad for performance to use @import because CSS files don't get downloaded in parallel.</p>
<p>You should use &lt;link rel='stylesheet' href='a.css'&gt; instead.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssDuplicatedSelectors" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedSelectors > 80}">
<div class="label">Duplicated selectors</div>
<div class="result">
{{phantomasResults.metrics.cssDuplicatedSelectors}}
<show-offenders modal-title="Duplicated selectors" metric-name="cssDuplicatedSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssDuplicatedSelectorsTooltip = true"></div>
<modal-dialog show="cssDuplicatedSelectorsTooltip" dialog-title="Merge duplicated selectors">
<p>This is when two or more selectors are strictly identical and should be merged.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssDuplicatedProperties" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedProperties > 100}">
<div class="label">Duplicated properties</div>
<div class="result">
{{phantomasResults.metrics.cssDuplicatedProperties}}
<show-offenders modal-title="Duplicated properties" metric-name="cssDuplicatedProperties" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssDuplicatedPropertiesTooltip = true"></div>
<modal-dialog show="cssDuplicatedPropertiesTooltip" dialog-title="Remove duplicated properties">
<p>This is the number of property definitions duplicated within a selector.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssEmptyRules" ng-class="{'warning': phantomasResults.metrics.cssEmptyRules > 100}">
<div class="label">Empty rules</div>
<div class="result">
{{phantomasResults.metrics.cssEmptyRules}}
<show-offenders modal-title="Empty rules" metric-name="cssEmptyRules" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssEmptyRulesTooltip = true"></div>
<modal-dialog show="cssEmptyRulesTooltip" dialog-title="Eliminate empty rules">
<p>Very easy to fix.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssExpressions">
<div class="label">CSS expressions</div>
<div class="result">
{{phantomasResults.metrics.cssExpressions}}
<show-offenders modal-title="CSS expressions" metric-name="cssExpressions" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssExpressionsTooltip = true"></div>
<modal-dialog show="cssExpressionsTooltip" dialog-title="Number of rules with CSS expressions">
<p>Such as: expression( document.body.clientWidth > 600 ? "600px" : "auto" )</p>
<p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssImportants">
<div class="label">Uses of !important</div>
<div class="result">
{{phantomasResults.metrics.cssImportants}}
<show-offenders modal-title="Uses of !important" metric-name="cssImportants" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssImportantsTooltip = true"></div>
<modal-dialog show="cssImportantsTooltip" dialog-title="Don't over-use !important">
<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssOldIEFixes">
<div class="label">Old IE fixes</div>
<div class="result">
{{phantomasResults.metrics.cssOldIEFixes}}
<show-offenders modal-title="Old IE fixes" metric-name="cssOldIEFixes" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssOldIEFixesTooltip = true"></div>
<modal-dialog show="cssOldIEFixesTooltip" dialog-title="Clean up old IE fixes">
<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p>
<p>IE6:
<ul>
<li>* html</li>
<li>html > body (everything but IE6)</li>
</ul>
<p>
<p>IE7:
<ul>
<li><b>*</b>height: 123px;</li>
<li>height: 123px <b>!ie</b>;</li>
</ul>
<p>
<p>IE9:
<ul>
<li>-ms-filter</li>
<li>progid:DXImageTransform.Microsoft</li>
</ul>
</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssOldPropertyPrefixes">
<div class="label">Old prefixes</div>
<div class="result">
{{phantomasResults.metrics.cssOldPropertyPrefixes}}
<show-offenders modal-title="Old prefixes" metric-name="cssOldPropertyPrefixes" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssOldPropertyPrefixesTooltip = true"></div>
<modal-dialog show="cssOldPropertyPrefixesTooltip" dialog-title="Clean up old CSS3 prefixes">
<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssUniversalSelectors">
<div class="label">Universal selectors</div>
<div class="result">
{{phantomasResults.metrics.cssUniversalSelectors}}
<show-offenders modal-title="Universal selectors" metric-name="cssUniversalSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssUniversalSelectorsTooltip = true"></div>
<modal-dialog show="cssUniversalSelectorsTooltip" dialog-title="Avoid universal selectors">
<p>Universal selectors are the most expensive CSS selectors.</p>
<p>More informations <a href="http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/" target="_blank">here</a>.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssRedundantBodySelectors">
<div class="label">Redundant body selectors</div>
<div class="result">
{{phantomasResults.metrics.cssRedundantBodySelectors}}
<show-offenders modal-title="Redundant body selectors" metric-name="cssRedundantBodySelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssRedundantBodySelectorsTooltip = true"></div>
<modal-dialog show="cssRedundantBodySelectorsTooltip" dialog-title="Useless redundant selectors">
<p>This is one way to remove complexity from a CSS rule. Generally, when "body" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssRedundantChildNodesSelectors">
<div class="label">Redundant tags selectors</div>
<div class="result">
{{phantomasResults.metrics.cssRedundantChildNodesSelectors}}
<show-offenders modal-title="Redundant tags selectors" metric-name="cssRedundantChildNodesSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssRedundantChildNodesSelectorsTooltip = true"></div>
<modal-dialog show="cssRedundantChildNodesSelectorsTooltip" dialog-title="Useless redundant selectors">
<p>Some tags included inside other tags are obvious. For example, when "ul li" is specified in a rule, "ul" can be removed because the "li" element is <b>always</b> inside a "ul". Same thing for "tr td", "select option", ...</p>
<p>Lowering compexity in CSS selectors can make the page load a little faster.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.requests">{{notations.requests}}</div>
<div class="notation">Requests number</div>
<div class="criteria">
<div class="table">
<div ng-class="{'warning': phantomasResults.metrics.requests > 200}">
<div class="label">Total requests</div>
<div class="result">
{{phantomasResults.metrics.requests}}
</div>
<div class="info">
<div class="icon-question" ng-click="requestsTooltip = true"></div>
<modal-dialog show="requestsTooltip" dialog-title="Minimize the number of requests">
<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p>
<p>There are several technics to reduce their number:
<ul>
<li>Concatenate JS files</li>
<li>Concatenate CSS files</li>
<li>Embed or inline small JS or CSS files in the HTML</li>
<li>Create sprites or icon fonts</li>
<li>Base64 encode small images in HTML or stylesheets</li>
<li>Use lazyloading for images</li>
</ul>
</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.htmlCount">
<div class="label">Documents</div>
<div class="result">
{{phantomasResults.metrics.htmlCount}}
<show-offenders modal-title="HTML count" metric-name="htmlCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.jsCount" ng-class="{'warning': phantomasResults.metrics.jsCount > 40}">
<div class="label">Scripts</div>
<div class="result">
{{phantomasResults.metrics.jsCount}}
<show-offenders modal-title="JS count" metric-name="jsCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.cssCount" ng-class="{'warning': phantomasResults.metrics.cssCount > 25}">
<div class="label">Stylesheets</div>
<div class="result">
{{phantomasResults.metrics.cssCount}}
<show-offenders modal-title="CSS count" metric-name="cssCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.imageCount" ng-class="{'warning': phantomasResults.metrics.imageCount > 80}">
<div class="label">Images</div>
<div class="result">
{{phantomasResults.metrics.imageCount}}
<show-offenders modal-title="Image count" metric-name="imageCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.webfontCount" ng-class="{'warning': phantomasResults.metrics.webfontCount > 4}">
<div class="label">Fonts</div>
<div class="result">
{{phantomasResults.metrics.webfontCount}}
<show-offenders modal-title="Webfont count" metric-name="webfontCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.videoCount">
<div class="label">Videos</div>
<div class="result">
{{phantomasResults.metrics.videoCount}}
<show-offenders modal-title="Video count" metric-name="videoCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.jsonCount">
<div class="label">JSON</div>
<div class="result">
{{phantomasResults.metrics.jsonCount}}
<show-offenders modal-title="JSON count" metric-name="jsonCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.otherCount">
<div class="label">Other</div>
<div class="result">
{{phantomasResults.metrics.otherCount}}
<show-offenders modal-title="Other count" metric-name="otherCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.network">{{notations.network}}</div>
<div class="notation">Network</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.notFound" class="warning">
<div class="label">404 not found</div>
<div class="result">
{{phantomasResults.metrics.notFound}}
<show-offenders modal-title="404 not found" metric-name="notFound" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="notFoundTooltip = true"></div>
<modal-dialog show="notFoundTooltip" dialog-title="404 errors">
<p>404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.closedConnections" ng-class="{'warning': phantomasResults.metrics.closedConnections > 20}">
<div class="label">Connections closed</div>
<div class="result">
{{phantomasResults.metrics.closedConnections}}
<show-offenders modal-title="Connections closed" metric-name="closedConnections" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="closedConnectionsTooltip = true"></div>
<modal-dialog show="closedConnectionsTooltip" dialog-title="Connection keep-alive is important">
<p>This counts the number of requests not keeping the connection alive (specifying "Connection: close" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p>
<p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.</p>
<p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.multipleRequests" ng-class="{'warning': phantomasResults.metrics.multipleRequests > 10}">
<div class="label">Duplicated requests</div>
<div class="result">
{{phantomasResults.metrics.multipleRequests}}
<show-offenders modal-title="Static assets requested more than once" metric-name="multipleRequests" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="multipleRequestsTooltip = true"></div>
<modal-dialog show="multipleRequestsTooltip" dialog-title="The same file is requested twice">
<p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cachingDisabled" ng-class="{'warning': phantomasResults.metrics.cachingDisabled > 25}">
<div class="label">Caching disabled</div>
<div class="result">
{{phantomasResults.metrics.cachingDisabled}}
<show-offenders modal-title="Caching disabled" metric-name="cachingDisabled" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cachingDisabledTooltip = true"></div>
<modal-dialog show="cachingDisabledTooltip" dialog-title="Caching disabled">
<p>Counts responses with caching disabled (max-age=0)</p>
<p>Fix immediatly if on static assets.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cachingNotSpecified" ng-class="{'warning': phantomasResults.metrics.cachingNotSpecified > 50}">
<div class="label">Caching not specified</div>
<div class="result">
{{phantomasResults.metrics.cachingNotSpecified}}
<show-offenders modal-title="Caching not specified" metric-name="cachingNotSpecified" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cachingNotSpecifiedTooltip = true"></div>
<modal-dialog show="cachingNotSpecifiedTooltip" dialog-title="No caching header">
<p>Responses with no caching header sent (either Cache-Control or Expires).</p>
<p>Every request should have a cache time specified. If you really don't want cache, specify "max-age=0", otherwise some browsers will try to cache.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cachingTooShort">
<div class="label">Caching too short</div>
<div class="result">
{{phantomasResults.metrics.cachingTooShort}}
<show-offenders modal-title="Caching too short" metric-name="cachingTooShort" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cachingTooShortTooltip = true"></div>
<modal-dialog show="cachingTooShortTooltip" dialog-title="Increase cache time">
<p>Responses with too short caching time (less than a week).</p>
<p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.domains" ng-class="{'warning': phantomasResults.metrics.domains > 40}">
<div class="label">Different domains</div>
<div class="result">
{{phantomasResults.metrics.domains}}
<show-offenders modal-title="Different domains" metric-name="domains" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="domainsTooltip = true"></div>
<modal-dialog show="domainsTooltip" dialog-title="Reduce the number of domains">
<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p>
<p>By the way, domain sharding is not a good practice anymore.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-show="view == 'execution' && !phantomasResults.error" class="execution">
<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="chart">
<div class="chartPoints">
<div ng-repeat="duration in timeline track by $index"
class="interval"
ng-class="{
domCreation: $index * timelineIntervalDuration < phantomasResults.metrics.domInteractive,
domInteractive: $index * timelineIntervalDuration >= phantomasResults.metrics.domInteractive
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoaded
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoadedEnd
&& $index * timelineIntervalDuration < phantomasResults.metrics.domComplete,
domComplete: $index * timelineIntervalDuration >= phantomasResults.metrics.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
<div class="tooltip detailsOverlay">
<div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
</div>
</div>
</div>
<div class="startTime">0 ms</div>
<div class="endTime">{{endTime | number: 0}} ms</div>
</div>
<div class="legend">
<div class="titles">
<div class="domCreation"><div class="color"></div>DOM creation</div>
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
<div class="domComplete"><div class="color"></div>Page is complete</div>
</div>
<div class="tips">
<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>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>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
</div>
</div>
</div>
<h2>Javascript Profiler</h2>
<p>
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
</p>
<div class="filters">
<div>
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Show warnings only</label>
</div>
<div>
<input type="checkbox" ng-model="textFilterOn" />
Filter by
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
</div>
</div>
<div class="table">
<toto data-info="mavariable"></toto>
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<div ng-repeat="node in javascript.children"
ng-if="(!warningsFilterOn
|| (node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5)
|| node.data.resultsNumber === 0
|| node.data.callDetails.context.length === 0
|| node.data.type == 'error'
|| node.data.type == 'jQuery version change')
&& (!textFilterOn
|| !textFilter.length
|| node.data.type.indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[0].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[1].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[2].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[3].indexOf(textFilter) >= 0)"
ng-class="{
showingDetails: node.data.showDetails,
jsError: node.data.type == 'error' || node.data.type == 'jQuery version change',
windowPerformance: node.data.type == 'domInteractive' || node.data.type == 'domContentLoaded' || node.data.type == 'domContentLoadedEnd' || node.data.type == 'domComplete'
}">
<div class="index">{{$index + 1}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">
{{node.data.callDetails.arguments[0]}}
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
</div>
<div class="details">
<div ng-class="{
'icon-question': !(node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5) && node.data.resultsNumber !== 0 && node.data.callDetails.context.length !== 0,
'icon-warning': node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5 || node.data.resultsNumber === 0 || node.data.callDetails.context.length === 0
}"
ng-click="onNodeDetailsClick(node)"
ng-if="node.data.type != 'jQuery loaded'
&& node.data.type != 'jQuery version change'
&& node.data.type != 'domInteractive'
&& node.data.type != 'domContentLoaded'
&& node.data.type != 'domContentLoadedEnd'
&& node.data.type != 'domComplete'"></div>
<div class="detailsOverlay" ng-show="node.data.showDetails">
<div class="closeBtn" ng-click="onNodeDetailsClick(node)"></div>
<div ng-if="node.data.callDetails.context.domElement">
<h4>Called on DOM element</h4>
<div>{{node.data.callDetails.context.domElement}}</div>
</div>
<div ng-if="node.data.callDetails.context.length === 0">
<h4>Called on 0 jQuery element</h4>
<p class="advice">Useless function call, as the jQuery object is empty.</p>
</div>
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
<h4>Called on 1 jQuery element</h4>
<div>{{node.data.callDetails.context.firstElementPath}}</div>
</div>
<div ng-if="node.data.callDetails.context.length > 1">
<h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
<p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
</p>
<p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
</div>
<p class="advice" ng-if="node.data.resultsNumber === 0">
The query returned 0 results. Could it be unused or dead code?
</p>
<div ng-if="node.data.parsedBacktrace">
<h4>Backtrace</h4>
<div class="table">
<div ng-repeat="trace in node.data.parsedBacktrace track by $index">
<div>{{trace.fnName || '(anonymous)'}}</div>
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
</div>
<div ng-if="node.data.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
<div>can't find any backtrace :/</div>
</div>
</div>
</div>
<div ng-if="node.children.length > 0">
<h4>Sub processes</h4>
<div class="table">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div>Duration</div>
</div>
<div ng-repeat="node in node.children">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">
{{node.data.callDetails.arguments[0]}}
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
</div>
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms</div>
<div class="duration" ng-if="node.data.time == undefined"></div>
</div>
</div>
</div>
</div>
</div>
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
</div>
</div>
</div>
</div>
<div class="footer">
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
</div>
<script>
var _phantomas_results = %%RESULTS%%;
var GA_ID = '%%GA_ID%%';
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', GA_ID, 'auto');
ga('send', 'pageview');
}
</script>
</body>
</html>
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,96.00C 144.341,96.00, 47.559,161.021,0.00,256.00c 47.559,94.979, 144.341,160.00, 256.00,160.00c 111.657,0.00, 208.439-65.021, 256.00-160.00 C 464.442,161.021, 367.657,96.00, 256.00,96.00z M 382.225,180.852c 30.081,19.187, 55.571,44.887, 74.717,75.148 c-19.146,30.261-44.637,55.961-74.718,75.149C 344.427,355.257, 300.779,368.00, 256.00,368.00c-44.78,0.00-88.428-12.743-126.225-36.852 C 99.695,311.962, 74.205,286.262, 55.058,256.00c 19.146-30.262, 44.637-55.962, 74.717-75.148c 1.959-1.25, 3.938-2.461, 5.929-3.65 C 130.725,190.866, 128.00,205.613, 128.00,221.00c0.00,70.692, 57.308,128.00, 128.00,128.00s 128.00-57.308, 128.00-128.00c0.00-15.387-2.725-30.134-7.704-43.799 C 378.286,178.39, 380.265,179.602, 382.225,180.852z M 256.00,205.00c0.00,26.51-21.49,48.00-48.00,48.00s-48.00-21.49-48.00-48.00s 21.49-48.00, 48.00-48.00 S 256.00,178.49, 256.00,205.00z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 224.00,384.00A32.00,32.00 2520.00 1,0 288.00,384A32.00,32.00 2520.00 1,0 224.00,384zM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

+4 -2
View File
@@ -1,3 +1,5 @@
angular.module('Spaghetti', [
'Results'
angular.module('YellowLabTools', [
'Results',
'ngModal',
'ShowOffendersDirective'
]);
View File
-48
View File
@@ -1,48 +0,0 @@
var app = angular.module('Results', []);
app.controller('ResultsCtrl', function ($scope) {
// Grab results from nodeJS served page
$scope.phantomasResults = window._phantomas_results;
$scope.slowRequestsOn = false;
$scope.slowRequestsLimit = 5;
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.data.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.phantomasResults.javascript.children.forEach(function(currentNode) {
currentNode.data.showDetails = false;
});
// Parse the backtrace
if (!node.data.parsedBacktrace) {
node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
}
}
node.data.showDetails = !isOpen;
};
function parseBacktrace(str) {
var splited = str.split(' / ');
var out = [];
splited.forEach(function(trace) {
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
if (result && result[2].length > 0) {
var filePath = result[2];
var chunks = filePath.split('/');
var fileName = chunks[chunks.length - 1];
out.push({
fnName: result[1],
fileName: fileName,
filePath: filePath,
line: result[3]
});
}
});
return out;
}
});
+406
View File
@@ -0,0 +1,406 @@
var app = angular.module('Results', []);
app.controller('ResultsCtrl', function ($scope) {
// Grab results from nodeJS served page
$scope.phantomasResults = window._phantomas_results;
$scope.view = 'execution';
if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
// Get the execution tree from the offenders
$scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
// Sort globalVariables offenders alphabetically
if ($scope.phantomasResults.offenders.globalVariables) {
$scope.phantomasResults.offenders.globalVariables.sort();
}
initSummaryView();
initJSTimelineView();
}
$scope.setView = function(viewName) {
$scope.view = viewName;
};
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.data.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.javascript.children.forEach(function(currentNode) {
currentNode.data.showDetails = false;
});
// Parse the backtrace
if (!node.data.parsedBacktrace) {
node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
}
}
node.data.showDetails = !isOpen;
};
function initSummaryView() {
// Read the main elements of the tree and sum the total time
$scope.totalJSTime = 0;
$scope.inBodyDomManipulations = 0;
treeRunner($scope.javascript, function(node) {
if (node.data.time) {
$scope.totalJSTime += node.data.time;
}
if (node.data.timestamp < $scope.phantomasResults.metrics.domInteractive &&
node.data.type !== 'jQuery - onDOMReady') {
$scope.inBodyDomManipulations ++;
}
if (node.data.type !== 'main') {
// Don't check the children
return false;
}
});
// Grab the notes
$scope.notations = {
domComplexity: getDomComplexityScore(),
jsDomManipulations: getJsDomManipulationsScore(),
jsBadPractices: getJSBadPracticesScore(),
jQueryLoading: getJQueryLoadingScore(),
cssComplexity: getCSSComplexityScore(),
badCss: getBadCssScore(),
requests: requestsScore(),
network: networkScore()
};
}
function initJSTimelineView() {
if (!$scope.javascript.children) {
return;
}
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
treeRunner($scope.javascript, function(node) {
switch(node.data.type) {
case 'domInteractive':
$scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
$scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
$scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
$scope.phantomasResults.metrics.domComplete = node.data.timestamp;
break;
}
if (node.data.type !== 'main') {
// Don't check the children
return false;
}
});
// Now read the tree and display it on a timeline
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
// Pre-fill array of as many elements as there are milleseconds
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
// Create the milliseconds array from the execution tree
treeRunner($scope.javascript, function(node) {
if (node.data.time !== undefined) {
// Ignore artefacts (durations > 100ms)
var time = Math.min(node.data.time, 100) || 1;
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
millisecondsArray[i] |= 1;
}
}
if (node.data.type !== 'main') {
// Don't check the children
return false;
}
});
// Pre-fill array of 200 elements
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
// Create the timeline from the milliseconds array
millisecondsArray.forEach(function(value, timestamp) {
if (value === 1) {
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
}
});
// Get the maximum value of the array (needed for display)
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
}
function getDomComplexityScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.DOMelementsCount +
Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
$scope.phantomasResults.metrics.iframesCount * 50 +
$scope.phantomasResults.metrics.DOMidDuplicated * 25;
if (score > 1000) {
note = 'B';
}
if (score > 1500) {
note = 'C';
}
if (score > 2000) {
note = 'D';
}
if (score > 3000) {
note = 'E';
}
if (score > 4000) {
note = 'F';
}
return note;
}
function getJsDomManipulationsScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
$scope.phantomasResults.metrics.DOMqueries +
$scope.phantomasResults.metrics.DOMqueriesAvoidable * 2 +
$scope.phantomasResults.metrics.eventsBound;
if (score > 300) {
note = 'B';
}
if (score > 500) {
note = 'C';
}
if (score > 700) {
note = 'D';
}
if (score > 1000) {
note = 'E';
}
if (score > 1400) {
note = 'F';
}
return note;
}
function getJSBadPracticesScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
$scope.phantomasResults.metrics.evalCalls * 2 +
$scope.phantomasResults.metrics.jsErrors * 10 +
$scope.phantomasResults.metrics.consoleMessages / 2 +
$scope.phantomasResults.metrics.globalVariables / 20 +
Math.sqrt($scope.inBodyDomManipulations);
if (score > 10) {
note = 'B';
}
if (score > 15) {
note = 'C';
}
if (score > 20) {
note = 'D';
}
if (score > 30) {
note = 'E';
}
if (score > 45) {
note = 'F';
}
return note;
}
function getJQueryLoadingScore() {
var note = 'NA';
if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
note = 'F';
} else if ($scope.phantomasResults.metrics.jQueryVersion) {
if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
note = 'A';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
note = 'B';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
note = 'C';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
note = 'D';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
note = 'E';
}
}
return note;
}
function getCSSComplexityScore() {
if (!$scope.phantomasResults.metrics.cssRules) {
return 'NA';
}
var note = 'A';
var score = $scope.phantomasResults.metrics.cssRules +
$scope.phantomasResults.metrics.cssComplexSelectors * 5 +
$scope.phantomasResults.metrics.cssComplexSelectorsByAttribute * 10;
if (score > 800) {
note = 'B';
}
if (score > 1200) {
note = 'C';
}
if (score > 2500) {
note = 'D';
}
if (score > 4000) {
note = 'E';
}
if (score > 6000) {
note = 'F';
}
return note;
}
function getBadCssScore() {
if ($scope.phantomasResults.metrics.cssParsingErrors) {
return 'F';
} else if (!$scope.phantomasResults.metrics.cssRules) {
return 'NA';
}
var note = 'A';
var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
$scope.phantomasResults.metrics.cssDuplicatedProperties +
$scope.phantomasResults.metrics.cssEmptyRules +
$scope.phantomasResults.metrics.cssExpressions * 10 +
$scope.phantomasResults.metrics.cssImportants * 2 +
$scope.phantomasResults.metrics.cssOldIEFixes * 10 +
$scope.phantomasResults.metrics.cssOldPropertyPrefixes +
$scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
$scope.phantomasResults.metrics.cssRedundantBodySelectors * 0.5 +
$scope.phantomasResults.metrics.cssRedundantChildNodesSelectors * 0.5 +
$scope.phantomasResults.metrics.cssImports * 50;
if (score > 50) {
note = 'B';
}
if (score > 100) {
note = 'C';
}
if (score > 200) {
note = 'D';
}
if (score > 500) {
note = 'E';
}
if (score > 1000) {
note = 'F';
}
return note;
}
function requestsScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.requests;
if (score > 30) {
note = 'B';
}
if (score > 45) {
note = 'C';
}
if (score > 60) {
note = 'D';
}
if (score > 80) {
note = 'E';
}
if (score > 100) {
note = 'F';
}
return note;
}
function networkScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.notFound * 25 +
$scope.phantomasResults.metrics.closedConnections * 10 +
$scope.phantomasResults.metrics.multipleRequests * 10 +
$scope.phantomasResults.metrics.cachingDisabled * 2 +
$scope.phantomasResults.metrics.cachingNotSpecified +
$scope.phantomasResults.metrics.cachingTooShort / 2 +
$scope.phantomasResults.metrics.domains;
if (score > 20) {
note = 'B';
}
if (score > 40) {
note = 'C';
}
if (score > 60) {
note = 'D';
}
if (score > 80) {
note = 'E';
}
if (score > 100) {
note = 'F';
}
return note;
}
function parseBacktrace(str) {
if (!str) {
return null;
}
var out = [];
var splited = str.split(' / ');
splited.forEach(function(trace) {
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
if (result && result[2].length > 0) {
var filePath = result[2];
var chunks = filePath.split('/');
var fileName = chunks[chunks.length - 1];
out.push({
fnName: result[1],
fileName: fileName,
filePath: filePath,
line: result[3]
});
}
});
return out;
}
// 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.
function treeRunner(node, fn) {
if (fn(node) !== false && node.children) {
node.children.forEach(function(child) {
treeRunner(child, fn);
});
}
}
});
+17
View File
@@ -0,0 +1,17 @@
var app = angular.module("ShowOffendersDirective", []);
app.directive('showOffenders', function() {
return {
restrict: 'E',
transclude: true,
scope: {
modalTitle: "@",
metricName: "@",
phantomasResults: "="
},
controller: function($scope, $element, $attrs, $location) {
$scope.dialogShown = false;
},
template: '&nbsp;<span ng-click="dialogShown = true" class="icon-eye" title="See offenders"></span><modal-dialog show="dialogShown" dialog-title="{{modalTitle}}: {{phantomasResults.metrics[metricName]}}" width="70%"><ul><li ng-repeat="offender in phantomasResults.offenders[metricName] track by $index">{{offender}}</li></ul></modal-dialog>'
};
});
+25 -21
View File
@@ -1,29 +1,33 @@
.promess {
padding: 0em 2em 3em;
font-weight: normal;
font-size: 1.2em;
}
.url {
width: 50%;
width: 50%;
}
.launchBtn {
background: #e74c3c;
color: #fff;
background: #e74c3c;
color: #fff;
}
input[type=submit], input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
input[type=submit],
input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
+24 -2
View File
@@ -1,3 +1,25 @@
#status {
font-size: 1.5em;
}
margin-top: 2em;
font-size: 2.5em;
}
@-webkit-keyframes rotating {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.waiting .icon-lab {
-webkit-animation: rotating 3s linear 0s infinite;
-webkit-transform: translateZ(0);
animation: rotating 3s linear 0s infinite;
}
+56
View File
@@ -0,0 +1,56 @@
@eye-font-family: "fontsmith-icons";
@eye-value: "\e004";
@eye: '"fontsmith-icons"' '"\\e004"';
@lab-font-family: "fontsmith-icons";
@lab-value: "\e003";
@lab: '"fontsmith-icons"' '"\\e003"';
@bars-font-family: "fontsmith-icons";
@bars-value: "\e005";
@bars: '"fontsmith-icons"' '"\\e005"';
@warning-font-family: "fontsmith-icons";
@warning-value: "\e000";
@warning: '"fontsmith-icons"' '"\\e000"';
@arrow-left3-font-family: "fontsmith-icons";
@arrow-left3-value: "\e006";
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
@question-font-family: "fontsmith-icons";
@question-value: "\e001";
@question: '"fontsmith-icons"' '"\\e001"';
@list-font-family: "fontsmith-icons";
@list-value: "\e002";
@list: '"fontsmith-icons"' '"\\e002"';
.icon-font-family(@char) {
font-family: ~`@{char}[0]`;
}
.icon-font() {
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-content(@char) {
content: ~`@{char}[1]`;
}
.icon(@char) {
.icon-font-family(@char);
.icon-font();
&:before {
.icon-content(@char);
}
}
@font-face {
font-family: "fontsmith-icons";
src:url("/public/fonts/icons.woff") format("woff"),
;
font-weight: normal;
font-style: normal;
}
+35
View File
@@ -0,0 +1,35 @@
.promess {
padding: 0em 2em 3em;
font-weight: normal;
font-size: 1.2em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
color: #fff;
}
input[type=submit], input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
+19
View File
@@ -0,0 +1,19 @@
#status {
margin-top: 2em;
font-size: 2.5em;
}
@-webkit-keyframes rotating {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(360deg); }
}
@keyframes rotating {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.waiting .icon-lab {
-webkit-animation: rotating 3s linear 0s infinite;
-webkit-transform: translateZ(0);
animation: rotating 3s linear 0s infinite;
}
+66
View File
@@ -0,0 +1,66 @@
@import "icons.less";
html {
margin: 100px 50px;
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
}
body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
}
input[type=submit] {
cursor: pointer;
}
h1 span {
display: inline-block;
height: 1em;
width: 1em;
color: #ffa319;
}
.footer {
padding: 3em;
color: #4e1836;
a {
color: inherit;
}
.star {
font-weight: bold;
span {
font-size: 1.2em;
}
}
}
/* Icons */
.icon-lab {
.icon(@lab);
}
.icon-question {
.icon(@question);
}
.icon-warning {
.icon(@warning);
}
.icon-back {
.icon(@arrow-left3);
}
.icon-summary {
.icon(@list);
}
.icon-spaghetti {
.icon(@bars);
}
.icon-eye {
.icon(@eye);
}
+497
View File
@@ -0,0 +1,497 @@
/* Timeline colors, related to Window Performances */
@domCreationColor: #FF6600;
@domCreationBg: #FFE0CC;
@domContentLoadedColor: #A7E846;
@domContentLoadedBg: #E0FFD1;
@domContentLoadedEndColor: #7ECCCC;
@domContentLoadedEndBg: #D8F0F0;
@domCompleteColor: #C2A3FF;
@domCompleteBg: #EDE3FF;
@domInteractiveColor: #FFE433;
@domInteractiveBg: #FFFCCC;
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
}
.resultsMenu .back {
color: #5e2846;
border-color: #5e2846;
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
}
.resultsMenu .menuItem:hover {
color: #ffa319;
}
.resultsMenu span {
position: relative;
top: 0.5em;
}
.testedUrl {
color: inherit;
}
h4 {
margin-bottom: 0.5em;
}
.summary, .metrics, .execution {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.notations {
display: table;
width: 90%;
margin: 0 10%;
border-spacing: 1em;
}
.notations > div {
display: table-row;
}
.notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.notations .notation {
font-weight: bold;
text-align: center;
}
.notations .criteria {
font-weight: normal;
}
.notations .A, .notations .B, .notations .C, .notations .D, .notations .E, .notations .F, .notations .NA {
width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.notations .A {
/* green */
background: #00DB61;
}
.notations .B {
/* green */
background: #CAD63D;
}
.notations .C {
/* yellow */
background: #FFD119;
}
.notations .D {
/* orange */
background: #FFA319;
}
.notations .E {
/* red */
background: #FF6600;
}
.notations .F {
/* red */
background: #FF1919;
}
.notations .NA {
/* Non applicable */
background: #CCC;
}
.notations .icon-eye {
color: #9c4274;
cursor: pointer;
}
.notations .criteria .table {
width: 75%;
}
.notations .criteria .label {
width: 70%;
}
.notations .criteria .result {
width: 20%;
font-weight: bold;
white-space: nowrap;
text-align: center;
}
.notations .warning .label, .notations .warning .result {
color: #FF1919;
}
.notations .criteria .info {
width: 10%;
text-align: center;
}
.notations .criteria .icon-question {
color: #f1c40f;
cursor: pointer;
}
.timeline {
margin: 2em 0 5em;
}
.timeline .chart {
position: relative;
width: 100%;
border-bottom: 1px solid #000;
}
.timeline .startTime, .timeline .endTime {
position: absolute;
bottom: 0.5em;
font-size: 0.8em;
}
.timeline .startTime {
left: 0em;
}
.timeline .endTime {
right: 0em;
}
.timeline .chartPoints {
display: table;
height: 100px;
width: 99%;
margin: 0 auto;
}
.timeline .interval {
display: table-cell;
position: relative;
height: 100px;
width: 0.5%;
}
.timeline .interval .color {
position: absolute;
bottom: 0;
width: 100%;
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: @domCompleteBg;
}
.timeline .domComplete .color {
background: @domCompleteColor;
}
.timeline .domContentLoadedEnd.interval {
background: @domContentLoadedEndBg;
}
.timeline .domContentLoadedEnd .color {
background: @domContentLoadedEndColor;
}
.timeline .domContentLoaded.interval {
background: @domContentLoadedBg;
}
.timeline .domContentLoaded .color {
background: @domContentLoadedColor;
}
.timeline .domInteractive.interval {
background: @domInteractiveBg;
}
.timeline .domInteractive .color {
background: @domInteractiveColor;
}
.timeline .domCreation.interval {
background: @domCreationBg;
}
.timeline .domCreation .color {
background: @domCreationColor;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
display: none;
width: auto;
padding: 0.5em 1em;
top: -1.5em;
right: 1em;
}
.timeline .interval:hover .tooltip {
display: block;
}
.timeline .legend {
display: table;
width: 100%;
margin-top: 1em;
}
.timeline .legend > div {
display: table-row;
}
.timeline .legend > div > div {
position: relative;
display: table-cell;
margin-top: 1em;
}
.timeline .titles {
font-weight: bold;
}
.timeline .titles > div {
padding: 0 1em 0 2em;
}
.timeline .tips {
font-size: 0.7em;
}
.timeline .tips > div {
padding: 1em 1em 0 0;
}
.timeline .legend .color {
display: block;
position: absolute;
left: 0;
height: 1.5em;
width: 1.5em;
border-radius: 0.2em;
}
.metrics h4 {
padding-left: 2em;
}
.metrics .module {
padding-left: 4em;
padding-top: 0.5em;
}
.metrics .legend {
font-style: italic;
color: #aaa;
}
.metrics .offenders {
padding-left: 0em;
font-size: 0.8em;
}
.metrics .offenders div {
cursor: pointer;
}
.metrics .offenders ul {
margin-top: 0.5em;
}
.filters {
margin: 1em 0;
padding: 0.5em;
border: 1px dotted #aaa;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type, .table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime {
background: #EBD8E2;
}
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div > .index {
color: #bbb;
}
.table > div > .type {
white-space:nowrap;
}
.table > div > .value {
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
}
.table .details .icon-question {
color: #f1c40f;
cursor: pointer;
}
.table .details .icon-warning {
cursor: pointer;
}
.detailsOverlay {
position: absolute;
right: 3em;
top: -3em;
width: 45em;
min-height: 1em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .advice {
color: #e74c3c;
font-weight: bold;
}
.detailsOverlay .trace {
word-break: break-all;
}
.table > div > .duration, .table > div > .startTime {
text-align: center;
white-space:nowrap;
}
.table > div > .startTime.domComplete {
background: @domCompleteBg;
}
.table > div > .startTime.domContentLoadedEnd {
background: @domContentLoadedEndBg;
}
.table > div > .startTime.domContentLoaded {
background: @domContentLoadedBg;
}
.table > div > .startTime.domInteractive {
background: @domInteractiveBg;
}
.table > div > .startTime.domCreation {
background: @domCreationBg;
}
.table .icon-warning {
color: #e74c3c;
}
/**** NgModal popin (have a look inside bower_components) ****/
.ng-modal {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: left;
}
.ng-modal-overlay {
position: absolute;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
}
.ng-modal-dialog {
z-index: 10000;
position: absolute;
top: 50%;
left: 50%;
width: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
background-color: #fff;
padding: 10px;
border: 3px solid #f1c40f;
border-radius: 0.5em;
color: #000;
}
.ng-modal-dialog-content {
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
max-height: 20em;
font-weight: normal;
white-space: normal;
}
.ng-modal-close {
position: absolute;
top: 3px;
right: 5px;
cursor: pointer;
font-size: 120%;
padding: 5px;
display: inline-block;
}
.ng-modal-close-x {
font-weight: bold;
font-family: Arial, sans-serif;
}
.ng-modal-title {
font-weight: bold;
font-size: 1.5em;
display: block;
margin-bottom: 10px;
padding-bottom: 7px;
border-bottom: solid 1px #999;
}
+133 -18
View File
@@ -1,24 +1,139 @@
@font-face {
font-family: "fontsmith-icons";
src: url("/public/fonts/icons.woff") format("woff");
font-weight: normal;
font-style: normal;
}
html {
margin: 100px 50px;
margin: 100px 50px;
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
}
body, input[type=submit], input[type=text], input[type=number], button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
body,
input[type=submit],
input[type=text],
input[type=url],
input[type=number],
button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
}
input[type=text] {
}
input[type=submit] {
cursor: pointer;
}
cursor: pointer;
}
h1 span {
display: inline-block;
height: 1em;
width: 1em;
color: #ffa319;
}
.footer {
padding: 3em;
color: #4e1836;
}
.footer a {
color: inherit;
}
.footer .star {
font-weight: bold;
}
.footer .star span {
font-size: 1.2em;
}
/* Icons */
.icon-lab {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-lab:before {
content: "\e003";
}
.icon-question {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-question:before {
content: "\e001";
}
.icon-warning {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-warning:before {
content: "\e000";
}
.icon-back {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-back:before {
content: "\e006";
}
.icon-summary {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-summary:before {
content: "\e002";
}
.icon-spaghetti {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-spaghetti:before {
content: "\e005";
}
.icon-eye {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-eye:before {
content: "\e004";
}
+422 -90
View File
@@ -1,133 +1,465 @@
/* Timeline colors, related to Window Performances */
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
}
.resultsMenu .back {
color: #5e2846;
border-color: #5e2846;
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu .active,
.resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
}
.resultsMenu .menuItem:hover {
color: #ffa319;
}
.resultsMenu span {
position: relative;
top: 0.5em;
}
.testedUrl {
color: inherit;
color: inherit;
}
h4 {
margin-bottom: 0.5em;
margin-bottom: 0.5em;
}
.summary,
.metrics,
.execution {
margin-top: 3em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.notations {
display: table;
width: 90%;
margin: 0 10%;
border-spacing: 1em;
}
.notations > div {
display: table-row;
}
.notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.notations .notation {
font-weight: bold;
text-align: center;
}
.notations .criteria {
font-weight: normal;
}
.notations .A,
.notations .B,
.notations .C,
.notations .D,
.notations .E,
.notations .F,
.notations .NA {
width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.notations .A {
/* green */
background: #00DB61;
}
.notations .B {
/* green */
background: #CAD63D;
}
.notations .C {
/* yellow */
background: #FFD119;
}
.notations .D {
/* orange */
background: #FFA319;
}
.notations .E {
/* red */
background: #FF6600;
}
.notations .F {
/* red */
background: #FF1919;
}
.notations .NA {
/* Non applicable */
background: #CCC;
}
.notations .icon-eye {
color: #9c4274;
cursor: pointer;
}
.notations .criteria .table {
width: 75%;
}
.notations .criteria .label {
width: 70%;
}
.notations .criteria .result {
width: 20%;
font-weight: bold;
white-space: nowrap;
text-align: center;
}
.notations .warning .label,
.notations .warning .result {
color: #FF1919;
}
.notations .criteria .info {
width: 10%;
text-align: center;
}
.notations .criteria .icon-question {
color: #f1c40f;
cursor: pointer;
}
.timeline {
margin: 2em 0 5em;
}
.timeline .chart {
position: relative;
width: 100%;
border-bottom: 1px solid #000;
}
.timeline .startTime,
.timeline .endTime {
position: absolute;
bottom: 0.5em;
font-size: 0.8em;
}
.timeline .startTime {
left: 0em;
}
.timeline .endTime {
right: 0em;
}
.timeline .chartPoints {
display: table;
height: 100px;
width: 99%;
margin: 0 auto;
}
.timeline .interval {
display: table-cell;
position: relative;
height: 100px;
width: 0.5%;
}
.timeline .interval .color {
position: absolute;
bottom: 0;
width: 100%;
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
}
.timeline .domComplete .color {
background: #c2a3ff;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
}
.timeline .domInteractive.interval {
background: #fffccc;
}
.timeline .domInteractive .color {
background: #ffe433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
}
.timeline .domCreation .color {
background: #ff6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
display: none;
width: auto;
padding: 0.5em 1em;
top: -1.5em;
right: 1em;
}
.timeline .interval:hover .tooltip {
display: block;
}
.timeline .legend {
display: table;
width: 100%;
margin-top: 1em;
}
.timeline .legend > div {
display: table-row;
}
.timeline .legend > div > div {
position: relative;
display: table-cell;
margin-top: 1em;
}
.timeline .titles {
font-weight: bold;
}
.timeline .titles > div {
padding: 0 1em 0 2em;
}
.timeline .tips {
font-size: 0.7em;
}
.timeline .tips > div {
padding: 1em 1em 0 0;
}
.timeline .legend .color {
display: block;
position: absolute;
left: 0;
height: 1.5em;
width: 1.5em;
border-radius: 0.2em;
}
.metrics h4 {
padding-left: 2em;
}
.metrics .module {
padding-left: 4em;
padding-top: 0.5em;
}
.metrics .legend {
font-style: italic;
color: #aaa;
}
.metrics .offenders {
padding-left: 0em;
font-size: 0.8em;
}
.metrics .offenders div {
cursor: pointer;
}
.metrics .offenders ul {
margin-top: 0.5em;
}
.filters {
margin: 1em 0;
padding: 0.5em;
border: 1px dotted #aaa;
margin: 1em 0;
padding: 0.5em;
border: 1px dotted #aaa;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type, .table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
.table > div.jsError > .type,
.table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.windowPerformance > div,
.table > div.windowPerformance > div.startTime {
background: #EBD8E2;
}
.table > div.showingDetails > div {
background: #f1c40f;
background: #f1c40f;
}
.table > div > .index {
color: #bbb;
color: #bbb;
}
.table > div > .type {
white-space:nowrap;
white-space: nowrap;
}
.table > div > .value {
width: 70%;
word-break: break-all;
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
position: relative;
}
.table > div > .details button {
color: #f1c40f;
font-weight: bold;
border: 2px solid #f1c40f;
border-radius: 50%;
background: transparent;
height: 2em;
width: 2em;
cursor: pointer;
.table .details .icon-question {
color: #f1c40f;
cursor: pointer;
}
.table .details .icon-warning {
cursor: pointer;
}
.detailsOverlay {
position: absolute;
right: 3em;
top: -3em;
width: 45em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
position: absolute;
right: 3em;
top: -3em;
width: 45em;
min-height: 1em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .advice {
color: #e74c3c;
font-weight: bold;
}
.detailsOverlay .trace {
word-break: break-all;
word-break: break-all;
}
.table > div > .duration {
text-align: center;
white-space:nowrap;
.table > div > .duration,
.table > div > .startTime {
text-align: center;
white-space: nowrap;
}
.table > div > .startTime.domComplete {
background: #ede3ff;
}
.table > div > .startTime.domContentLoadedEnd {
background: #d8f0f0;
}
.table > div > .startTime.domContentLoaded {
background: #e0ffd1;
}
.table > div > .startTime.domInteractive {
background: #fffccc;
}
.table > div > .startTime.domCreation {
background: #ffe0cc;
}
.table .icon-warning {
color: #e74c3c;
}
/**** NgModal popin (have a look inside bower_components) ****/
.ng-modal {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: left;
}
.ng-modal-overlay {
position: absolute;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
}
.ng-modal-dialog {
z-index: 10000;
position: absolute;
top: 50%;
left: 50%;
width: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
background-color: #fff;
padding: 10px;
border: 3px solid #f1c40f;
border-radius: 0.5em;
color: #000;
}
.ng-modal-dialog-content {
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
max-height: 20em;
font-weight: normal;
white-space: normal;
}
.ng-modal-close {
position: absolute;
top: 3px;
right: 5px;
cursor: pointer;
font-size: 120%;
padding: 5px;
display: inline-block;
}
.ng-modal-close-x {
font-weight: bold;
font-family: Arial, sans-serif;
}
.ng-modal-title {
font-weight: bold;
font-size: 1.5em;
display: block;
margin-bottom: 10px;
padding-bottom: 7px;
border-bottom: solid 1px #999;
}
.warningIcon {
display: inline-block;
width: 1.5em;
height: 1.5em;
background: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><defs/><g fill="none" fill-rule="evenodd" id="Icons new Arranged Names Color" stroke="none" stroke-width="1"><g fill="#e74c3c" id="101 Warning"><path d="M14.4242327,6.14839275 C15.2942987,4.74072976 16.707028,4.74408442 17.5750205,6.14839275 L28.3601099,23.59738 C29.5216388,25.4765951 28.6755462,27 26.4714068,27 L5.5278464,27 C3.32321557,27 2.47386317,25.4826642 3.63914331,23.59738 Z M16,20 C16.5522847,20 17,19.5469637 17,19.0029699 L17,12.9970301 C17,12.4463856 16.5561352,12 16,12 C15.4477153,12 15,12.4530363 15,12.9970301 L15,19.0029699 C15,19.5536144 15.4438648,20 16,20 Z M16,24 C16.5522848,24 17,23.5522848 17,23 C17,22.4477152 16.5522848,22 16,22 C15.4477152,22 15,22.4477152 15,23 C15,23.5522848 15.4477152,24 16,24 Z M16,24" id="Triangle 29"/></g></g></svg>') no-repeat 0 3px;
}
-16
View File
@@ -1,16 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Javascript Spaghetti Profiler</title>
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/index.css">
</head>
<body>
<h1>Javascript Spaghetti Profiler</h1>
<form method="post" action="/launchTest">
<input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
</form>
</body>
</html>
-47
View File
@@ -1,47 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Javascript Spaghetti Profiler</title>
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<h1>Javascript Spaghetti Profiler</h1>
<div id="status"></div>
<div>%%TEST_URL%%</div>
<script>
var testId = '%%TEST_ID%%';
var statusElement = document.getElementById('status');
var socket = io();
function askStatus() {
socket.emit('waiting', testId);
}
socket.on('running', function() {
statusElement.innerHTML = 'Running';
setTimeout(askStatus, 200);
});
socket.on('position', function(position) {
statusElement.innerHTML = 'Waiting behind ' + position + ' other tests';
setTimeout(askStatus, 2000);
});
socket.on('complete', function() {
statusElement.innerHTML = 'Test complete';
window.location.replace('/results/' + testId);
});
socket.on('404', function() {
statusElement.innerHTML = 'Test not found';
});
askStatus();
</script>
</body>
</html>
-113
View File
@@ -1,113 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Javascript Spaghetti Profiler</title>
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/results.css">
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/public/scripts/app.js"></script>
<script src="/public/scripts/resultsController.js"></script>
<script src="/public/scripts/filters.js"></script>
</head>
<body ng-app="Spaghetti" ng-controller="ResultsCtrl">
<h1>Javascript Spaghetti Profiler</h1>
<div ng-if="undefined">Untangling and counting the spaghettis...</div>
<div class="ng-cloak">
<div>Tested url: <a class="testedUrl" href="phantomasResults.url" target="_blank">{{phantomasResults.url}}</a></div>
<div ng-if="phantomasResults.error || !phantomasResults.javascript.data">
<h2>Error: {{phantomasResults.error}}</h2>
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
<div ng-if="!phantomasResults.error && !phantomasResults.javascript.data">Javascript execution tree error</div>
</div>
<div ng-if="!phantomasResults.error" class="execution">
<h2>Javascript interactions with the DOM</h2>
<div class="filters">
<div>
<input type="checkbox" ng-model="textFilterOn" />
Filter by
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
</div>
<div>
<input type="checkbox" ng-model="slowRequestsOn" id="slowRequests" />
<label for="slowRequests">Requests slower than</label>
<input type="number" value="5" min="0" ng-model="slowRequestsLimit" class="slowRequestsLimit"> ms
</div>
</div>
<div class="table">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Duration</div>
</div>
<div ng-repeat="node in phantomasResults.javascript.children"
ng-if="(!slowRequestsOn || node.data.time > slowRequestsLimit) && (!textFilterOn || !textFilter.length || node.data.type.indexOf(textFilter) >= 0 || node.data.callDetails.arguments[0].indexOf(textFilter) >= 0)"
ng-class="{showingDetails: node.data.showDetails, jsError: node.data.type == 'error'}">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">{{node.data.callDetails.arguments[0]}}</div>
<div class="details">
<button ng-click="onNodeDetailsClick(node)">i</button>
<div class="detailsOverlay" ng-show="node.data.showDetails">
<div class="closeBtn" ng-click="onNodeDetailsClick(node)"></div>
<div ng-if="node.data.callDetails.context.domElement">
<h4>Called on DOM element</h4>
<div>{{node.data.callDetails.context.domElement}}</div>
</div>
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
<h4>Called on 1 jQuery element</h4>
<div>{{node.data.callDetails.context.firstElementPath}}</div>
</div>
<h4>Backtrace</h4>
<div class="table">
<div ng-repeat="trace in node.data.parsedBacktrace track by $index">
<div>{{trace.fnName || '(anonymous)'}}</div>
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
</div>
<div ng-if="node.data.parsedBacktrace.length == 0">
<div>can't find any backtrace :/</div>
</div>
</div>
<div ng-if="node.children.length > 0">
<h4>Sub processes</h4>
<div class="table">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div>Duration</div>
</div>
<div ng-repeat="node in node.children">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">{{node.data.callDetails.arguments[0]}}</div>
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms <div ng-if="node.data.time > slowRequestsLimit" class="warningIcon"></div></div>
<div class="duration" ng-if="node.data.time == undefined"></div>
</div>
</div>
</div>
</div>
</div>
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms <div ng-if="node.data.time > slowRequestsLimit" class="warningIcon"></div></div>
<div class="duration" ng-if="node.data.time == undefined"></div>
</div>
</div>
</div>
</div>
<script>var _phantomas_results = %%RESULTS%%;</script>
</body>
</html>
+3 -2
View File
@@ -1,6 +1,7 @@
{
"name": "js-spaghetti-profiler",
"name": "yellowlabtools",
"dependencies": {
"angular": "1.3.0-beta.16"
"angular": "~1.3.1",
"ngModal": "git://github.com/gmetais/ngModal.git#1.2.3"
}
}
+28 -11
View File
@@ -1,16 +1,33 @@
{
"name": "jsspaghettiprofiler",
"version": "0.0.0",
"name": "yellowlabtools",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git://github.com/gmetais/YellowLabTools.git"
},
"dependencies": {
"phantomas": "git+https://git@github.com/gmetais/phantomas.git#jquery-profiler",
"express": "^4.6.1",
"async": "^0.9.0",
"socket.io": "^1.0.6",
"body-parser": "^1.5.0"
"phantomas": "1.7.0",
"express": "~4.10.1",
"async": "~0.9.0",
"socket.io": "~1.2.0",
"body-parser": "~1.9.2",
"compression": "~1.2.0"
},
"devDependencies": {},
"engines": {
"node": ">=0.10.0"
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-jshint": "^0.10.0",
"matchdep": "^0.3.0",
"grunt-mocha-test": "^0.12.2",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-blanket": "^0.0.8",
"chai": "^1.9.2",
"mocha": "^2.0.1",
"phantomjs": "^1.9.10",
"grunt-fontsmith": "^0.9.1",
"grunt-contrib-less": "^0.12.0"
},
"scripts": {}
"scripts": {
"test": "grunt test"
}
}
+191
View File
@@ -0,0 +1,191 @@
/**
* Overwritting the original spying functions in scope.js
* This is done so we now have a before AND an after callback on the spy
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
});
phantomas.once('init', function() {
phantomas.evaluate(function(responseEndTime, deepAnalysis) {
(function(phantomas) {
// Overwritting phantomas spy function
(function() {
var enabled = true;
// turn off spying to not include internal phantomas actions
function spyEnabled(state, reason) {
enabled = (state === true);
phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
}
phantomas.log('Overwritting phantomas spy function');
function spy(obj, fn, callbackBefore, callbackAfter) {
var origFn = obj[fn];
if (typeof origFn !== 'function') {
return false;
}
phantomas.log('Attaching a spy to "' + fn + '" function...');
obj[fn] = function() {
var result;
// Before
if (enabled) {
callbackBefore.apply(this, arguments);
}
// Execute
try {
result = origFn.apply(this, arguments);
} catch(e) {
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
phantomas.log(arguments);
} finally {
// After
if (enabled && callbackAfter) {
callbackAfter.call(this, result);
}
}
return result;
};
// copy custom properties of original function to the mocked one
Object.keys(origFn).forEach(function(key) {
obj[fn][key] = origFn[key];
});
obj[fn].prototype = origFn.prototype;
return true;
}
phantomas.spyEnabled = spyEnabled;
phantomas.spy = spy;
})();
// Adding some code for the Javascript execution tree construction
(function() {
var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root;
if (deepAnalysis) {
phantomas.log('Entering deep Javascript analysis mode');
}
var depth = 0;
// Add a child but don't enter his context
function pushContext(data) {
if (depth === 0 || deepAnalysis) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
currentContext.addChild(data);
}
}
// Add a child to the current context and enter his context
function enterContext(data) {
if (depth === 0 || deepAnalysis) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
currentContext = currentContext.addChild(data);
}
depth ++;
}
// Save given data in the current context and jump change current context to its parent
function leaveContext(moreData) {
if (depth === 1 || deepAnalysis) {
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
// Merge previous data with moreData (ovewrites if exists)
if (moreData) {
for (var key in moreData) {
currentContext.data[key] = moreData[key];
}
}
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
}
}
depth --;
}
function getContextData() {
return currentContext.data;
}
// Returns a clean object, without the parent which causes recursive loops
function readFullTree() {
// Return null if the contextTree is not correctly closed
if (root !== currentContext) {
return null;
}
var current = currentContext;
function recusiveRead(node) {
if (node.children.length === 0) {
delete node.children;
} else {
for (var i=0, max=node.children.length ; i<max ; i++) {
recusiveRead(node.children[i]);
}
}
delete node.parent;
}
recusiveRead(root);
return root;
}
function ContextTreeNode(parent, data) {
this.data = data;
this.parent = parent;
this.children = [];
this.addChild = function(data) {
var child = new ContextTreeNode(this, data);
this.children.push(child);
return child;
};
}
phantomas.log('Adding some contextTree functions to phantomas');
phantomas.pushContext = pushContext;
phantomas.enterContext = enterContext;
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
})();
})(window.__phantomas);
}, responseEndTime, phantomas.getParam('js-deep-analysis'));
});
};
@@ -0,0 +1,135 @@
/**
* Analyzes DOM complexity
*/
/* global document: true, Node: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// total length of HTML comments (including <!-- --> brackets)
phantomas.setMetric('commentsSize'); // @desc the size of HTML comments on the page @offenders
// total length of text nodes with whitespaces only (i.e. pretty formatting of HTML)
phantomas.setMetric('whiteSpacesSize'); // @desc the size of text nodes with whitespaces only
// count all tags
phantomas.setMetric('DOMelementsCount'); // @desc total number of HTML element nodes
phantomas.setMetric('DOMelementMaxDepth'); // @desc maximum level on nesting of HTML element node
// nodes with inlines CSS (style attribute)
phantomas.setMetric('nodesWithInlineCSS'); // @desc number of nodes with inline CSS styling (with style attribute) @offenders
// images
phantomas.setMetric('imagesScaledDown'); // @desc number of <img> nodes that have images scaled down in HTML @offenders
phantomas.setMetric('imagesWithoutDimensions'); // @desc number of <img> nodes without both width and height attribute @offenders
// duplicated ID (issue #392)
phantomas.setMetric('DOMidDuplicated'); // @desc number of duplicated IDs found in DOM
var Collection = require('../../../node_modules/phantomas/lib/collection'),
DOMids = new Collection();
phantomas.on('domId', function(id) {
DOMids.push(id);
});
// HTML size
phantomas.on('report', function() {
phantomas.setMetricEvaluate('bodyHTMLSize', function() { // @desc the size of body tag content (document.body.innerHTML.length)
return document.body && document.body.innerHTML.length || 0;
});
phantomas.evaluate(function() {
(function(phantomas) {
var runner = new phantomas.nodeRunner(),
whitespacesRegExp = /^\s+$/,
DOMelementMaxDepth = 0,
DOMelementMaxDepthElts = [],
size = 0;
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
case Node.COMMENT_NODE:
size = node.textContent.length + 7; // '<!--' + '-->'.length
phantomas.incrMetric('commentsSize', size);
// log HTML comments bigger than 64 characters
if (size > 64) {
phantomas.addOffender('commentsSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
break;
case Node.ELEMENT_NODE:
phantomas.incrMetric('DOMelementsCount');
if (depth > DOMelementMaxDepth) {
DOMelementMaxDepth = depth;
DOMelementMaxDepthElts = [phantomas.getDOMPath(node)];
} else if (depth === DOMelementMaxDepth) {
DOMelementMaxDepthElts.push(phantomas.getDOMPath(node));
}
// report duplicated ID (issue #392)
if (node.id) {
phantomas.emit('domId', node.id);
}
// ignore inline <script> tags
if (node.nodeName === 'SCRIPT') {
return false;
}
// images
if (node.nodeName === 'IMG') {
if (!node.hasAttribute('width') || !node.hasAttribute('height')) {
phantomas.incrMetric('imagesWithoutDimensions');
phantomas.addOffender('imagesWithoutDimensions', '%s <%s>', phantomas.getDOMPath(node), node.src);
}
if (node.naturalHeight && node.naturalWidth && node.height && node.width) {
if (node.naturalHeight > node.height || node.naturalWidth > node.width) {
phantomas.incrMetric('imagesScaledDown');
phantomas.addOffender('imagesScaledDown', '%s (%dx%d -> %dx%d)', node.src, node.naturalWidth, node.naturalHeight, node.width, node.height);
}
}
}
// count nodes with inline CSS
if (node.hasAttribute('style')) {
phantomas.incrMetric('nodesWithInlineCSS');
phantomas.addOffender('nodesWithInlineCSS', phantomas.getDOMPath(node) + ' (' + node.getAttribute('style') + ')');
}
break;
case Node.TEXT_NODE:
if (whitespacesRegExp.test(node.textContent)) {
phantomas.incrMetric('whiteSpacesSize', node.textContent.length);
}
break;
}
});
phantomas.setMetric('DOMelementMaxDepth', DOMelementMaxDepth);
DOMelementMaxDepthElts.forEach(function(path) {
phantomas.addOffender('DOMelementMaxDepth', path);
});
phantomas.spyEnabled(false, 'counting iframes and images');
// count <iframe> tags
phantomas.setMetric('iframesCount', document.querySelectorAll('iframe').length); // @desc number of iframe nodes
phantomas.spyEnabled(true);
}(window.__phantomas));
});
DOMids.sort().forEach(function(id, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMidDuplicated');
phantomas.addOffender('DOMidDuplicated', '%s: %d occurrences', id, cnt);
}
});
});
};
+270
View File
@@ -0,0 +1,270 @@
/**
* Analyzes DOM queries done via native DOM methods
*/
/* global Element: true, Document: true, Node: true, window: true */
exports.version = '0.9.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
// fake native DOM functions
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName, context) {
phantomas.emit('domQuery', type, query, fnName, context); // @desc DOM query has been made
}
phantomas.spy(Document.prototype, 'getElementById', function(id) {
phantomas.incrMetric('DOMqueriesById');
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
querySpy('id', '#' + id, 'getElementById', '#document');
phantomas.enterContext({
type: 'getElementById',
callDetails: {
context: {
domElement: '#document'
},
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
var moreData = {
resultsNumber : result ? 1 : 0
};
phantomas.leaveContext(moreData);
});
// selectors by class name
function selectorClassNameSpyBefore(className) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByClassName');
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
querySpy('class', '.' + className, 'getElementsByClassName', context);
phantomas.enterContext({
type: 'getElementsByClassName',
callDetails: {
context: {
domElement: context
},
arguments: ['.' + className]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorClassNameAfter(result) {
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
// selectors by tag name
function selectorTagNameSpyBefore(tagName) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByTagName');
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context);
phantomas.enterContext({
type: 'getElementsByTagName',
callDetails: {
context: {
domElement: context
},
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorTagNameSpyAfter(result) {
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
// selector queries
function selectorQuerySpy(selector, context) {
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
querySpy('selector', selector, 'querySelectorAll', context);
}
function selectorQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelector',
callDetails: {
context: {
domElement: context
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorQuerySpyAfter(result) {
var moreData = {
resultsNumber : result ? 1 : 0
};
phantomas.leaveContext(moreData);
}
function selectorAllQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelectorAll',
callDetails: {
context: {
domElement: context
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorAllQuerySpryAfter(result) {
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
// count DOM inserts
function appendChild(child, element, context, appended) {
/*jshint validthis: true */
// ignore appending to the node that's not yet added to DOM tree
if (!element.parentNode) {
return;
}
// don't count elements added to fragments as a DOM inserts (issue #350)
// DocumentFragment > div[0]
if (context.indexOf('DocumentFragment') === 0) {
return;
}
phantomas.incrMetric('DOMinserts');
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
//phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
}
function appendChildSpyBefore(child) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'appendChild',
callDetails: {
context: {
domElement: context
},
arguments: [appended]
},
backtrace: phantomas.getBacktrace()
});
}
function insertBeforeSpyBefore(child) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
domElement: context
},
arguments: [appended]
},
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
phantomas.leaveContext();
});
})(window.__phantomas);
});
});
// count DOM queries by either ID, tag name, class name and selector query
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
var Collection = require('../../../node_modules/phantomas/lib/collection'),
DOMqueries = new Collection();
phantomas.on('domQuery', function(type, query, fnName, context) {
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
phantomas.incrMetric('DOMqueries');
if (context && context.indexOf('DocumentFragment') === -1) {
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
}
});
phantomas.on('report', function() {
DOMqueries.sort().forEach(function(query, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMqueriesDuplicated');
phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
}
});
});
};
@@ -0,0 +1,48 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
domElement: path
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
})(window.__phantomas);
});
});
};
+326
View File
@@ -0,0 +1,326 @@
/**
* Analyzes jQuery activity
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
/* jshint -W030 */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
phantomas.setMetric('jQuerySizzleCallsDuplicated'); // @desc number of calls on the same Sizzle request
phantomas.setMetric('jQueryDifferentVersions'); //@desc number of different jQuery versions loaded on the page (not counting iframes)
var jQueryFunctions = [
// DOM manipulations
'html',
'append',
'appendTo',
'prepend',
'prependTo',
'before',
'insertBefore',
'after',
'insertAfter',
'remove',
'detach',
'empty',
'clone',
'replaceWith',
'replaceAll',
'text',
'wrap',
'wrapAll',
'wrapInner',
'unwrap',
// Style manipulations
'css',
'offset',
'position',
'height',
'innerHeight',
'outerHeight',
'width',
'innerWidth',
'outerWidth',
'scrollLeft',
'scrollTop',
// generic events
'on',
'off',
'live',
'die',
'delegate',
'undelegate',
'one',
'unbind',
// more events
'blur',
'change',
'click',
'dblclick',
'error',
'focus',
'focusin',
'focusout',
'hover',
'keydown',
'keypress',
'keyup',
'load',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'resize',
'scroll',
'select',
'submit',
'toggle',
'unload',
// attributes
'attr',
'prop',
'removeAttr',
'removeProp',
'val',
'hasClass',
'addClass',
'removeClass',
'toggleClass'
];
// spy calls to jQuery functions
phantomas.once('init', function() {
phantomas.evaluate(function(jQueryFunctions) {
(function(phantomas) {
var jQuery;
// TODO: create a helper - phantomas.spyGlobalVar() ?
window.__defineSetter__('jQuery', function(val) {
var version;
var jQueryFn;
var oldJQuery = jQuery;
if (!val || !val.fn) {
phantomas.log('jQuery: unable to detect version!');
return;
}
version = val.fn.jquery;
jQuery = val;
jQueryFn = val.fn;
// Older jQuery (v?.?) compatibility
if (!jQueryFn) {
jQueryFn = jQuery;
}
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
phantomas.emit('jQueryLoaded', version);
phantomas.pushContext({
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
callDetails: {
arguments: ['version ' + version]
},
backtrace: phantomas.getBacktrace()
});
// jQuery.ready.promise
// works for jQuery 1.8.0+ (released Aug 09 2012)
phantomas.spy(val.ready, 'promise', function(func) {
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
phantomas.pushContext({
type: 'jQuery - onDOMReady',
callDetails: {
arguments: [func]
},
backtrace: phantomas.getBacktrace()
});
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
// Sizzle calls - jQuery.find
// works for jQuery 1.3+ (released Jan 13 2009)
phantomas.spy(val, 'find', function(selector, context) {
phantomas.incrMetric('jQuerySizzleCalls');
phantomas.emit('onSizzleCall', selector + ' (context: ' + (phantomas.getDOMPath(context) || 'unknown') + ')');
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(context)
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
// $().bind - jQuery.bind
// works for jQuery v?.?
phantomas.spy(jQueryFn, 'bind', function(eventTypes, func) {
phantomas.enterContext({
type: 'jQuery - bind',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0]),
selector: this.selector
},
arguments: [eventTypes, func]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
phantomas.leaveContext();
}) || phantomas.log('jQuery: can not measure jQueryBindCalls (jQuery used on the page is too old)!');
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
phantomas.spy(jQueryFn, functionName, function(args) {
// Clean args
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof Object) {
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
} else if (arg instanceof HTMLElement) {
arg = phantomas.getDOMPath(arg) || 'unknown';
} else if (typeof arg === 'function') {
arg = '(function)';
} else {
try {
arg = JSON.stringify(arg);
} catch(e) {
arg = '[Object]';
}
}
}
if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
arg = arg.substring(0, 200) + '...';
}
if (typeof arg === 'function') {
arg = '(function)';
}
if (arg === true) {
arg = 'true';
}
if (arg === false) {
arg = 'false';
}
if (arg === null) {
arg = 'null';
}
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
arg = 'undefined';
}
args[index] = arg;
});
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0])
},
arguments: args
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
phantomas.leaveContext();
}) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
});
});
window.__defineGetter__('jQuery', function() {
return jQuery;
});
})(window.__phantomas);
}, jQueryFunctions);
});
// count Sizzle calls to detect duplicated queries
var Collection = require('../../../node_modules/phantomas/lib/collection'),
sizzleCalls = new Collection(),
jQueryLoading = new Collection();
phantomas.on('onSizzleCall', function(request) {
sizzleCalls.push(request);
});
phantomas.on('jQueryLoaded', function(version) {
jQueryLoading.push(version);
});
phantomas.on('report', function() {
sizzleCalls.sort().forEach(function(id, cnt) {
if (cnt > 1) {
phantomas.incrMetric('jQuerySizzleCallsDuplicated');
phantomas.addOffender('jQuerySizzleCallsDuplicated', '%s: %d', id, cnt);
}
});
jQueryLoading.forEach(function(version) {
phantomas.incrMetric('jQueryDifferentVersions');
phantomas.addOffender('jQueryDifferentVersions', '%s', version);
});
});
};
@@ -0,0 +1,49 @@
/**
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
*/
exports.version = '0.3.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
function formatTrace(trace) {
var ret = [];
if(Array.isArray(trace)) {
trace.forEach(function(entry) {
ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line);
});
}
return ret;
}
phantomas.on('jserror', function(msg, trace) {
trace = formatTrace(trace);
phantomas.log(msg);
phantomas.log('Backtrace: ' + trace.join(' / '));
phantomas.incrMetric('jsErrors');
phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
// Yeah, this is weird, i'm sending the error back to the browser...
phantomas.evaluate(function(msg, caller, trace) {
(function(phantomas) {
phantomas.pushContext({
type: 'error',
callDetails: {
arguments: [msg]
},
caller: caller,
backtrace: trace
});
})(window.__phantomas);
}, msg, trace[0], trace.join(' / '));
});
};
@@ -0,0 +1,29 @@
/**
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
*/
exports.version = '0.0';
exports.module = function(phantomas) {
'use strict';
/*phantomas.on('recv', function(entry, res) {
if (!entry.isJS) {
return;
}
// Yeah, this is weird, i'm sending the information back to the browser...
phantomas.evaluate(function(url) {
(function(phantomas) {
phantomas.pushContext({
type: 'script loaded',
callDetails: {
arguments: [url]
}
});
})(window.__phantomas);
}, entry.url);
});*/
};
@@ -0,0 +1,31 @@
/**
* Saves the javascript interractions with the DOM
*
* Run phantomas with --js-execution-tree option to use this module
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
// save data
phantomas.on('report', function() {
phantomas.log('Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
if (fullTree === null) {
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
return;
}
phantomas.setMetric('javascriptExecutionTree', true, true);
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
})(window.__phantomas);});
});
};
@@ -0,0 +1,37 @@
/**
* Analyzes if HTTP responses keep the connections alive.
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('closedConnections'); // @desc requests not keeping the connection alive and slowing down the next request @offenders
var closedConnectionHosts = {};
phantomas.on('recv', function(entry, res) {
var connectionHeader = (entry.headers.Connection || '').toLowerCase();
// Taking the protocol in account, in case the same domain is called with two different protocols.
var host = entry.protocol + '://' + entry.domain;
if (connectionHeader.indexOf('close') >= 0) {
// Don't blame it immediatly, wait to see if the connection is needed a second time.
closedConnectionHosts[host] = entry.url;
}
});
phantomas.on('send', function(entry, res) {
var host = entry.protocol + '://' + entry.domain;
var previousClosedConnection = closedConnectionHosts[host];
if (previousClosedConnection) {
// There was a closed connection. We can blame it safely now!
phantomas.incrMetric('closedConnections');
phantomas.addOffender('closedConnections', previousClosedConnection);
closedConnectionHosts[host] = null;
}
});
};
@@ -0,0 +1,150 @@
/**
* Measure when the page reaches certain states
*
* @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading
* @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// times below are calculated relative to performance.timing.responseEnd (#117)
phantomas.setMetric('domInteractive'); // @desc time it took to parse the HTML and construct the DOM
phantomas.setMetric('domContentLoaded'); // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady)
phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable
phantomas.setMetric('domComplete'); // @desc time it took to load all page resources, the loading spinner has stopped spinning
// backend vs frontend time
phantomas.setMetric('timeBackend'); // @desc time to the first byte compared to the total loading time [%]
phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%]
// measure dom... metrics from the moment HTML response was fully received
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
phantomas.log('Performance timing: responseEnd = %d', responseEndTime);
});
phantomas.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(function(phantomas) {
phantomas.spyEnabled(false, 'installing window.performance metrics');
phantomas.currentStep = 'domCreation';
// extend window.performance
// "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192)
if (typeof window.performance === 'undefined') {
window.performance = {
timing: {
responseEnd: responseEndTime
}
};
phantomas.log('Performance timing: emulating window.performance');
}
else {
phantomas.log('Performance timing: using native window.performance');
}
// onDOMReady
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
// use NavigationTiming if possible
var time = window.performance.timing.domContentLoadedEventEnd ?
(window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd)
:
(Date.now() - responseEndTime);
phantomas.currentStep = 'domContentLoadedEnd';
phantomas.setMetric('domContentLoadedEnd', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoadedEnd'
});
}, 0);
var time = Date.now() - responseEndTime;
phantomas.currentStep = 'domContentLoaded';
phantomas.setMetric('domContentLoaded', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoaded'
});
});
// emulate Navigation Timing
document.addEventListener('readystatechange', function() {
var readyState = document.readyState,
responseEndTime = window.performance.timing.responseEnd,
time = Date.now() - responseEndTime,
metricName;
// @see http://www.w3.org/TR/html5/dom.html#documentreadystate
switch(readyState) {
// the browser has finished parsing all of the HTML and DOM construction is complete
case 'interactive':
metricName = 'domInteractive';
break;
// the processing is complete and all of the resources on the page have finished downloading
case 'complete':
metricName = 'domComplete';
phantomas.log('Performance timing: %j', window.performance.timing);
break;
default:
phantomas.log('Performance timing: unhandled "%s" state!', readyState);
return;
}
phantomas.currentStep = metricName;
phantomas.setMetric(metricName, time, true);
phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time);
phantomas.pushContext({
type: metricName
});
});
phantomas.spyEnabled(true);
})(window.__phantomas);
}, responseEndTime);
});
/**
* Emit metrics with backend vs frontend time
*
* Performance Golden Rule:
* "80-90% of the end-user response time is spent on the frontend. Start there."
*
* @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
*/
phantomas.on('report', function() {
// The “backend” time is the time it takes the server to get the first byte back to the client.
// The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete)
var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10),
frontendTime = parseInt(phantomas.getMetric('domComplete'), 10),
totalTime = backendTime + frontendTime,
backendTimePercentage;
if (totalTime === 0) {
return;
}
backendTimePercentage = Math.round(backendTime / totalTime * 100);
phantomas.setMetric('timeBackend', backendTimePercentage);
phantomas.setMetric('timeFrontend', 100 - backendTimePercentage);
phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage);
});
};
+38 -179
View File
@@ -1,200 +1,59 @@
var fs = require('fs');
var async = require('async');
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
var bodyParser = require('body-parser');
var phantomas = require('phantomas');
// Config file
var settings = require('./server_config/settings.json');
// Libraries
var fs = require('fs');
var async = require('async');
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
var bodyParser = require('body-parser');
var compress = require('compression');
// Internals
var indexController = require('./app/node_controllers/indexController');
var launchTestController = require('./app/node_controllers/launchTestController');
var resultsController = require('./app/node_controllers/resultsController');
var waitingQueueSocket = require('./app/node_controllers/waitingQueueSocket');
var testQueue = require('./app/lib/testQueue');
app.use(compress());
app.use(bodyParser.urlencoded({ extended: false }));
// Home page
app.get('/', function(req, res) {
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/views/index.html', {encoding: 'utf8'}, callback);
}
}, function(err, results) {
res.setHeader('Content-Type', 'text/html');
res.send(results.htmlTemplate);
});
});
// Waiting queue page
app.post('/launchTest', function(req, res) {
// Generate test id
var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
var url = req.body.url;
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
url = 'http://' + url;
// Redirect www.yellowlab.tools to yellowlab.tools (for SEO)
app.all('*', function(req, res, next) {
if (req.hostname && req.hostname.match(/^www\.yellowlab\.tools/) !== null) {
res.redirect('http://' + req.hostname.replace(/^www\.yellowlab\.tools/, 'yellowlab.tools') + req.url);
} else {
next();
}
async.waterfall([
function htmlTemplate(callback) {
fs.readFile('./app/views/launchTest.html', {encoding: 'utf8'}, callback);
},
function sendResponse(html, callback) {
html = html.replace('%%TEST_URL%%', url);
html = html.replace('%%TEST_ID%%', testId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
callback();
},
function createFolder(callback) {
// Create results folder
fs.mkdir(resultsPath, callback);
},
function executePhantomas(callback) {
var options = {
timeout: 60,
'js-execution-tree': true,
reporter: 'json:pretty'
};
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
var task = {
testId: testId,
url: url,
options: options
};
console.log(JSON.stringify(task, null, 4));
taskQueue.push(task, callback);
},
function writeResults(json, resultsObject, callback) {
console.log('Saving Phantomas results file to ' + phantomasResultsPath);
fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
}
], function(err) {
if (err) {
console.log('An error occured while launching the phantomas test : ', err);
fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
if (err) {
console.log('Could not even write an error message on file ' + phantomasResultsPath);
console.log(err);
}
});
}
});
});
// Results page
app.get('/results/:testId', function(req, res) {
var testId = req.params.testId;
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
console.log('Opening test ' + testId + ' results as HTML');
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/views/results.html', {encoding: 'utf8'}, callback);
},
phantomasResults: function(callback) {
fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
}
}, function(err, results) {
if (err) {
console.log(err);
return res.status(404).send('Sorry, test not found...');
}
var html = results.htmlTemplate;
html = html.replace('%%RESULTS%%', results.phantomasResults);
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
});
// Routes definition
app.get('/', function(req, res) { indexController(req, res, settings.googleAnalyticsId); });
app.post('/launchTest', function(req, res) { launchTestController(req, res, testQueue, settings.googleAnalyticsId); });
app.get('/results/:testId', function(req, res) { resultsController(req, res, settings.googleAnalyticsId); });
// Static files
app.use('/public', express.static(__dirname + '/app/public'));
app.use('/bower_components', express.static(__dirname + '/bower_components'));
app.use('/public', express.static(__dirname + '/app/public'));
app.use('/bower_components', express.static(__dirname + '/bower_components'));
// Socket.io
io.on('connection', function(socket){
socket.on('waiting', function(testId) {
console.log('User waiting for test id ' + testId);
socket.testId = testId;
// Check task position in queue
var positionInQueue = -1;
if (taskQueue.length() > 0) {
taskQueue.tasks.forEach(function(task, index) {
if (task.data.testId === testId) {
positionInQueue = index;
}
});
}
if (positionInQueue >= 0) {
socket.emit('position', positionInQueue);
} else if (currentTask && currentTask.testId === testId) {
socket.emit('running');
} else {
// Find in results files
var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
if (exists) {
// TODO : use eventEmitter to make sure the file is completly written on disk
setTimeout(function() {
socket.emit('complete');
}, 1000);
} else {
socket.emit('404');
}
});
}
});
});
// Creating a queue and defining the worker function
var currentTask = null;
var taskQueue = async.queue(function queueWorker(task, callback) {
currentTask = task;
console.log('Starting test ' + task.testId);
// It's time to launch the test!!!
phantomas(task.url, task.options, function(err, json, results) {
console.log('Test ' + task.testId + ' complete');
currentTask = null;
callback(err, json, results);
});
waitingQueueSocket(socket, testQueue);
});
// Create the results folder if it doesn't exist
var resultsPath = 'results';
if (!fs.existsSync(resultsPath)) {
fs.mkdirSync(resultsPath);
}
// Launch the server
server.listen(8383, function() {
server.listen(settings.serverPort, function() {
console.log('Listening on port %d', server.address().port);
});
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# APT-GET
sudo apt-get update
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
sudo apt-get install curl git python-software-properties -y --force-yes
# Installation of NodeJS
sudo add-apt-repository -y ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install -y nodejs
source ~/.profile
# Installation of some packages globally
npm install bower -g
npm install forever -g
# Installation of YellowLabTools
sudo mkdir /space
sudo chown $USER /space
cd /space
git clone https://github.com/gmetais/YellowLabTools.git --branch master
cd YellowLabTools
npm install --production
bower install --config.interactive=false --allow-root
# Start the server
rm server_config/settings.json
cp server_config/settings-prod.json server_config/settings.json
forever start -c "node --stack-size=65500" server.js
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
cd /space/YellowLabTools
# Stop the server
forever stopall
# Keep the settings.json file
git stash
git pull
git stash pop
# In case something was added in package.json or bower.json
rm -rf node_modules
npm install --production
rm -rf bower_components
bower install --config.interactive=false --allow-root
# Restart the server
forever start -c "node --stack-size=65500" server.js
+4
View File
@@ -0,0 +1,4 @@
{
"serverPort": 80,
"googleAnalyticsId": "UA-54493828-1"
}
+4
View File
@@ -0,0 +1,4 @@
{
"serverPort": 8383,
"googleAnalyticsId": ""
}
+7
View File
@@ -0,0 +1,7 @@
var path = require('path');
var srcDir = path.join(__dirname, '..', 'server');
require('blanket')({
// Only files that match the pattern will be instrumented
pattern: srcDir
});
+53
View File
@@ -0,0 +1,53 @@
var should = require('chai').should();
var phantomasWrapper = require('../../app/lib/phantomasWrapper.js');
describe('phantomasWrapper', function() {
it('should have a method execute', function() {
phantomasWrapper.should.have.property('execute').that.is.a('function');
});
it('should execute', function(done) {
var url = 'http://www.google.fr/';
this.timeout(20000);
phantomasWrapper.execute({
url: url,
testId: '123abc',
options:{
}
}, function(err, json, results) {
should.not.exist(err);
json.should.be.an('object');
json.should.have.a.property('generator');
json.generator.should.contain('phantomas');
json.should.have.a.property('url').that.equals(url);
json.should.have.a.property('metrics').that.is.an('object').not.empty;
json.should.have.a.property('offenders').that.is.an('object').not.empty;
json.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
done();
});
});
it('should fail with error 254', function(done) {
var url = 'http://www.not.existing';
this.timeout(15000);
phantomasWrapper.execute({
url: url,
testId: '123abc',
options:{
}
}, function(err, json, results) {
should.exist(err);
err.should.equal(254);
should.not.exist(json);
done();
});
});
});
+23
View File
@@ -0,0 +1,23 @@
var should = require('chai').should();
var testQueue = require('../../app/lib/testQueue.js');
describe('testQueue', function() {
var url = 'http://www.not.existing';
it('should accept a new test with method push', function() {
testQueue.should.have.property('push').that.is.a('function');
var task = {
testId: 'aaaaa',
url: url,
options: {}
};
testQueue.push(task, function(err, json, results) {
done();
});
});
});