├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── Gruntfile.js
├── README.md
├── bower.json
├── lib
├── cli.js
├── couch-views
│ ├── metrics_data.js
│ ├── pagelist.js
│ └── runs.js
├── couchData.js
├── couchSite.js
├── couchViews.js
├── index.js
├── init.js
├── mime.js
├── options.js
├── perfTests.js
└── utils.js
├── migrations
├── cli.js
├── index.js
├── migrate-0.2.0.js
├── migrate-0.3.0.js
├── migrate-0.4.0.js
└── utility.js
├── package.json
├── tasks
├── metricsgen.js
└── task.js
├── test
├── index.spec.js
├── res
│ ├── local.config.json
│ ├── sample-perf-results.json
│ ├── test1.html
│ └── test2.html
├── seedData.js
└── util.js
└── www
├── app
├── all-metrics
│ ├── all-metrics.html
│ ├── all-metrics.less
│ └── allmetrics.js
├── app.js
├── backend.js
├── font.less
├── main-page
│ ├── error.less
│ ├── navbar.html
│ ├── navbar.less
│ ├── no-pj-brand.less
│ ├── sidebar.html
│ ├── sidebar.js
│ └── sidebar.less
├── main.less
├── metric-details
│ ├── metric-detail.html
│ ├── metric-detail.less
│ ├── metricDetail.js
│ └── metricDetailsGraph.js
├── page-select
│ ├── page-select.html
│ ├── page-select.less
│ └── pageSelect.js
└── summary
│ ├── networkTimingGraph.js
│ ├── paintCycleGraph.js
│ ├── summary.html
│ ├── summary.js
│ ├── summary.less
│ ├── tiles.js
│ ├── tiles.less
│ └── tiles.tpl.html
├── assets
├── css
│ ├── animation.css
│ ├── config.json
│ └── fontello-codes.css
└── fonts
│ ├── fontello-codes.css
│ ├── fontello.eot
│ ├── fontello.svg
│ ├── fontello.ttf
│ └── fontello.woff
├── index.html
└── server
└── endpoints.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bower_components/
3 | bin/
4 | bin-site/
5 | *.log
6 | .DS_Store
7 | .idea/*
8 | .tmp/*
9 | dist/*
10 | _replicator/*
11 | _users/*
12 | pouch__all_dbs__/*
13 | version/*
14 | log.txt
15 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": true,
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "undef": true,
10 | "boss": true,
11 | "eqnull": true,
12 | "node": true,
13 | "shadow": true,
14 | "expr": true,
15 | "globals": {
16 | "angular": false,
17 | "$": false,
18 | "window": false,
19 | "ENDPOINTS": false,
20 | "describe": false,
21 | "it": false,
22 | "beforeEach": false,
23 | "before": false,
24 | "xit": false,
25 | "xdescribe": false
26 | }
27 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bower_components/
3 | Gruntfile.js
4 | test/
5 | www/
6 | bower.json
7 | .jshintrc
8 | .gitignore
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "0.12"
5 | - "4.0"
6 | - "4.3"
7 | - "4"
8 | - "5.0"
9 | - "5"
10 | - "6"
11 | - "stable"
12 | env:
13 | - NPM_VERSION=2
14 | - NPM_VERSION=3
15 |
16 | services: couchdb
17 |
18 | before_install:
19 | - npm install -g npm@$NPM_VERSION
20 | - npm install -g grunt-cli
21 | before_script:
22 | - npm install
23 | - "export DISPLAY=:99.0"
24 | - "sh -e /etc/init.d/xvfb start"
25 | - sleep 3 # give xvfb some time to start
26 | script: npm test
27 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | var couchdb = require('./test/util').config({
4 | log: 1
5 | }).couch;
6 | var serveStatic = require('serve-static');
7 | var path = require('path');
8 | var jqplot = [
9 | 'jquery.jqplot.min.js',
10 | 'plugins/jqplot.categoryAxisRenderer.min.js',
11 | 'plugins/jqplot.highlighter.min.js',
12 | 'plugins/jqplot.canvasTextRenderer.min.js',
13 | 'plugins/jqplot.canvasAxisTickRenderer.min.js',
14 | 'plugins/jqplot.canvasAxisLabelRenderer.min.js',
15 | 'plugins/jqplot.barRenderer.min.js',
16 | 'plugins/jqplot.trendline.min.js',
17 | 'plugins/jqplot.pieRenderer.min.js'
18 | ];
19 |
20 | grunt.initConfig({
21 | jshint: {
22 | all: [
23 | 'Gruntfile.js',
24 | 'lib/*.js',
25 | 'test/**/*.js',
26 | 'www/**/*.js'
27 | ],
28 | options: {
29 | jshintrc: '.jshintrc'
30 | },
31 | },
32 |
33 | metricsgen: {
34 | files: {
35 | dest: 'bin-site/metrics.js'
36 | }
37 | },
38 |
39 | uglify: {
40 | options: {
41 | mangle: false,
42 | sourceMap: true,
43 | sourceMapName: 'bin-site/main.js.map',
44 | },
45 | js: {
46 | files: {
47 | 'bin-site/main.js': ['www/**/*.js', 'bin-site/**/*.js']
48 | }
49 | }
50 | },
51 |
52 | concat: {
53 | jqplot: {
54 | src: jqplot.map(function(file) {
55 | return 'bower_components/jqplot-bower/dist/' + file;
56 | }),
57 | dest: 'bin-site/jqplot.js'
58 | },
59 | less: {
60 | src: ['bower_components/jqplot-bower/dist/jquery.jqplot.min.css', 'www/app/**/*.less', 'www/assets/css/*.css'],
61 | dest: 'bin-site/main.less'
62 | }
63 | },
64 |
65 | less: {
66 | dev: {
67 | files: {
68 | 'bin-site/main.css': 'bin-site/main.less'
69 | }
70 | },
71 | dist: {
72 | options: {
73 | compress: true
74 | },
75 | files: {
76 | 'bin-site/main.css': 'bin-site/main.less'
77 | }
78 | }
79 | },
80 |
81 | autoprefixer: {
82 | less: {
83 | src: 'bin-site/main.css',
84 | dest: 'bin-site/main.css'
85 | }
86 | },
87 |
88 | copy: {
89 | partials: {
90 | expand: true,
91 | cwd: 'www/app',
92 | src: ['**/*.html'],
93 | dest: 'bin-site/app'
94 | },
95 | fonts: {
96 | expand: true,
97 | cwd: 'www/assets',
98 | src: ['fonts/*.*'],
99 | dest: 'bin-site/assets'
100 | },
101 | endpoints: {
102 | src: ['www/server/endpoints.js'],
103 | dest: 'bin-site/server/endpoints.js'
104 | }
105 | },
106 |
107 | processhtml: {
108 | dev: {
109 | options: {
110 | strip: true,
111 | data: {
112 | scripts: jqplot.map(function(file) {
113 | return 'jqplot-bower/dist/' + file;
114 | }).concat(grunt.file.expand({
115 | cwd: 'www'
116 | }, 'app/**/*.js')),
117 | }
118 | },
119 | files: {
120 | 'bin-site/index.html': 'www/index.html'
121 | }
122 | },
123 | dist: {
124 | options: {
125 | data: {
126 | scripts: ['main.js']
127 | }
128 | },
129 | files: {
130 | 'bin-site/index.html': 'www/index.html'
131 | }
132 | }
133 | },
134 | htmlmin: {
135 | dist: {
136 | options: {
137 | removeComments: true,
138 | collapseWhitespace: true,
139 | conservativeCollapse: true,
140 | collapseBooleanAttributes: true
141 | },
142 | files: {
143 | 'bin-site/index.html': 'bin-site/index.html'
144 | }
145 | },
146 | },
147 | connect: {
148 | proxies: [{
149 | changeOrigin: false,
150 | host: 'localhost',
151 | port: '5984',
152 | context: grunt.file.expand('lib/couch-views/**/*.js').map(function(file) {
153 | return '/' + path.basename(file, '.js');
154 | }),
155 | rewrite: (function(files) {
156 | var res = {};
157 | files.forEach(function(file) {
158 | var view = path.basename(file, '.js');
159 | res[view + '/_view'] = ['/', couchdb.database, '/_design/', view, '/_view'].join('');
160 | });
161 | return res;
162 | }(grunt.file.expand('lib/couch-views/**/*.js')))
163 | }],
164 | dev: {
165 | options: {
166 | hostname: '*',
167 | port: 9000,
168 | base: ['test/res', 'bower_components', 'bin-site', 'www'],
169 | livereload: true,
170 | middleware: function(connect, options) {
171 | var middlewares = [];
172 | if (!Array.isArray(options.base)) {
173 | options.base = [options.base];
174 | }
175 | middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest);
176 | options.base.forEach(function(base) {
177 | middlewares.push(serveStatic(base));
178 | });
179 | return middlewares;
180 | },
181 | useAvailablePort: true,
182 | }
183 | }
184 | },
185 | watch: {
186 | options: {
187 | livereload: true,
188 | },
189 | views: {
190 | files: ['lib/couch-views/*.js'],
191 | tasks: ['deployViews']
192 | },
193 | less: {
194 | files: ['www/**/*.less'],
195 | tasks: ['concat:less', 'less:dev', 'autoprefixer']
196 | },
197 | html: {
198 | files: ['www/index.html'],
199 | tasks: ['processhtml:dev']
200 | },
201 | others: {
202 | files: ['www/app/**/*.html', 'www/app/**/*.js'],
203 | tasks: []
204 | }
205 | },
206 |
207 | mochaTest: {
208 | options: {
209 | reporter: 'dot',
210 | timeout: 1000 * 60 * 10
211 | },
212 | unit: {
213 | src: ['test/**/*.spec.js'],
214 | }
215 | },
216 | clean: {
217 | all: ['bin-site', 'test.log'],
218 | dist: ['bin-site/jqplot.js', 'bin-site/main.less', 'bin-site/metrics.js']
219 | }
220 | });
221 |
222 | require('load-grunt-tasks')(grunt);
223 | require('./tasks/metricsgen')(grunt);
224 |
225 | grunt.registerTask('seedData', function() {
226 | var done = this.async();
227 | require('./test/seedData')(done, 100);
228 | });
229 |
230 | grunt.registerTask('deployViews', function() {
231 | var done = this.async();
232 | require('./lib/couchViews')(require('./test/util.js').config(), function(err, res) {
233 | console.log(err, res);
234 | done(!err);
235 | });
236 | });
237 |
238 | grunt.registerTask('dev', ['metricsgen', 'concat:less', 'less:dev', 'autoprefixer', 'processhtml:dev', 'configureProxies:server', 'connect:dev', 'watch']);
239 | grunt.registerTask('dist', ['jshint', 'concat', 'metricsgen', 'uglify', 'less:dist', 'autoprefixer', 'copy', 'processhtml:dist', 'htmlmin', 'clean:dist']);
240 | grunt.registerTask('test', ['clean', 'dist', 'mochaTest']);
241 |
242 | grunt.registerTask('default', ['dev']);
243 | };
244 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # perfjankie
2 |
3 | PerfJankie is a tool to monitor smoothness and responsiveness of websites and Cordova/Hybrid apps over time. It runs performance tests using [browser-perf](http://github.com/axemclion/browser-perf) and saves the results in a CouchDB server.
4 | It also has a dashboard that displays graphs of the performance metrics collected over time that you help identify performance trends, or locate a single commit that can slow down a site.
5 |
6 | After running the tests, navigate to the following url to see the results dashboard.
7 |
8 | > http://couchdb.serverl.url/databasename/_design/site/index.html
9 |
10 | Here is a [dashboard](http://nparashuram.com/perfslides/perfjankie) created from a [sample project](http://github.com/axemclion/perfslides).
11 |
12 | 
13 |
14 | ## Why ?
15 | Checking for performance regressions is hard. Though most modern browsers have excellent performance measurement tools, it is hard for a developer to check these tools for every commit. Just as unit tests check for regressions in functionality, perfjankie will help with checking regressions in browser rendering performance when integrated into systems like Travis or Jenkins.
16 |
17 | The results dashboard
18 | ## Setup
19 | Perfjankie requires Selenium as the driver to run tests and CouchDB to store the results. Since this is based on browser-perf, look at [setting up browser-perf](https://github.com/axemclion/browser-perf/wiki/Setup-Instructions) for more information.
20 |
21 | ## Usage
22 |
23 | Perfjankie can be used as a node module, from the command line, or as a Grunt task and can be installed from npm using `npm install perfjankie`.
24 |
25 | ### Node Module
26 |
27 | The API call looks like the following
28 |
29 | ```javascript
30 |
31 | var perfjankie = require('perfjankie');
32 | perfjankie({
33 | "url": "http://localhost:9000/testpage.html", // URL of the page that you would like to test.
34 |
35 | /* The next set of values identify the test */
36 | name: "Component or Webpage Name", // A friendly name for the URL. This is shown as component name in the dashboard
37 | suite: "optional suite name", // Displayed as the title in the dashboard. Only 1 suite name for all components
38 | time: new Date().getTime(), // Used to sort the data when displaying graph. Can be the time when a commit was made
39 | run: "commit#Hash", // A hash for the commit, displayed in the x-axis in the dashboard
40 | repeat: 3, // Run the tests 3 times. Default is 1 time
41 |
42 | /* Identifies where the data and the dashboard are saved */
43 | couch: {
44 | server: 'http://localhost:5984',
45 | requestOptions : { "proxy" : "http://someproxy" }, // optional, e.g. useful for http basic auth, see Please check [request] for more information on the defaults. They support features like cookie jar, proxies, ssl, etc.
46 | database: 'performance',
47 | updateSite: !process.env.CI, // If true, updates the couchApp that shows the dashboard. Set to false in when running Continuous integration, run this the first time using command line.
48 | onlyUpdateSite: false // No data to upload, just update the site. Recommended to do from dev box as couchDB instance may require special access to create views.
49 | },
50 |
51 | callback: function(err, res) {
52 | // The callback function, err is falsy if all of the following happen
53 | // 1. Browsers perf tests ran
54 | // 2. Data has been saved in couchDB
55 | // err is not falsy even if update site fails.
56 | },
57 |
58 | /* OPTIONS PASSED TO BROWSER-PERF */
59 | // Properties identifying the test environment */
60 | browsers: [{ // This can also be a ["chrome", "firefox"] or "chrome,firefox"
61 | browserName: "chrome",
62 | version: 32,
63 | platform: "Windows 8.1"
64 | }], // See browser perf browser configuration for all options.
65 |
66 | selenium: {
67 | hostname: "ondemand.saucelabs.com", // or localhost or hub.browserstack.com
68 | port: 80,
69 | },
70 |
71 | BROWSERSTACK_USERNAME: process.env.BROWSERSTACK_USERNAME, // If using browserStack
72 | BROWSERSTACK_KEY: process.env.BROWSERSTACK_KEY, // If using browserStack, this is automatically added to browsers object
73 |
74 | SAUCE_USERNAME: process.env.SAUCE_USERNAME, // If using Saucelabs
75 | SAUCE_ACCESSKEY: process.env.SAUCE_ACCESSKEY, // If using Saucelabs
76 |
77 | /* A way to log the information - can be bunyan, or grunt logs. */
78 | log: { // Expects the following methods,
79 | fatal: grunt.fail.fatal.bind(grunt.fail),
80 | error: grunt.fail.warn.bind(grunt.fail),
81 | warn: grunt.log.error.bind(grunt.log),
82 | info: grunt.log.ok.bind(grunt.log),
83 | debug: grunt.verbose.writeln.bind(grunt.verbose),
84 | trace: grunt.log.debug.bind(grunt.log)
85 | }
86 |
87 | });
88 |
89 | ```
90 |
91 | Other options that can be passed include `preScript`, `actions`, `metrics`, `preScriptFile`, etc. Note that most of these options are similar to the options passed to browser-perf. Refer to the [browser-perf options](https://github.com/axemclion/browser-perf/wiki/Node-Module---API) for a mode detailed explanation.
92 |
93 | ### Grunt Task
94 | To run perfjankie as a Grunt task, simple load task using `grunt.loadNpmTasks('perfjankie');`, define a `perfjankie` task and pass in all the options from above as options to the Grunt task. [Here](https://github.com/axemclion/perfslides/blob/38b4f6e246c5ab971ce2957ec78bb701dbbc3038/Gruntfile.js#L57) is an example.
95 |
96 | ### Command line
97 | Run `perfjankie --help` to see a list of all the options.
98 | Quick Note - to only update site the first time, run the following from the command line. You need to quote the URL to work with parameters, e.g. https://www.google.de/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=angular
99 |
100 | ```bash
101 | $ perfjankie --config-file=local.config.json --only-update-site 'example.com'
102 | ```
103 | Or without a config file
104 |
105 | ```bash
106 | $ perfjankie --couch-server=http://localhost:5984 --couch-database=perfjankie-test --couch-user=admin_user --couch-pwd=admin_pass --name=Google 'https://www.google.de/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=angular'
107 | ```
108 |
109 | The config file can contain server configuration and can look like [this](https://github.com/axemclion/perfjankie/blob/master/test/res/local.config.json).
110 |
111 | ## Hosting dashboard on a different server
112 | You can also host the HTML/CSS/JS for displaying the results dashboard on not on CouchDB, but a different static server, possibly behind a CDN. In such cases,
113 | 1. Use the npm module and host the contents of the `site` folder.
114 | 2. Open index.html and insert the following snippet in the `
` section
115 |
116 | ```html
117 |
118 | ```
119 |
120 | This will ensure that all requests for data are made to the other CouchDB server. Also ensure that the CouchDB server has CORS turned on.
121 |
122 | ## Login before running tests
123 |
124 | You can login a user, or perform other kinds of page setup using the [preScript](https://github.com/axemclion/browser-perf/wiki/Node-Module---API#prescript) or the [preScriptFile](https://github.com/axemclion/browser-perf/wiki/Node-Module---API#prescriptfile) options. Here is an [example](https://github.com/axemclion/browser-perf/wiki/FAQ#how-can-i-test-a-page-that-requires-login) of a login action that can be passed in the preScript option.
125 |
126 | ## Migrating data from older versions
127 | If you have older data and want to move to the latest release of perfjankie, you may also have to migrate your data. You can migrate from older version of a database to a newer version using
128 |
129 | ```bash
130 | $ perfjankie --config-file=local.config.json --migrate=newDatabaseName
131 | ```
132 |
133 | This simply transforms all the old data into a format that will work with the newer version of perfjankie. Your version of the database is stored under a document called `version`, and the version supported by your installed version of perfjankie is the key `dbVersion` in the `package.json`
134 |
135 | ## What does it measure?
136 |
137 | Perfjankie measures page rendering times. It collects metrics like frame times, page load time, first paint time, scroll time, etc. It can be used on
138 | * long, scrollable web pages (like a search result page, an article page, etc). The impact of changes to CSS, sticky headers and scrolling event handlers can be seen in the results.
139 | * components (like bootstrap, jQuery UI components, ReactJS components, AngularJS components, etc). Component developers just have to place the component multiple times on a page and will know if they caused perf regressions as they continue developing the component.
140 | For more information, see the documentation for [browser-perf](http://github.com/axemclion/browser-perf)
141 |
142 | # Development
143 |
144 | ## Dev setup
145 |
146 | Any changes should be verified with unit tests, see `test`-folder.
147 | To run the tests you local couchdb installed with a database, see `test/res/local.config.json` for details:
148 |
149 | 1. start couchdb
150 | 2. start a local selenium grd: `java -jar node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar -Dwebdriver.chrome.driver=$(pwd)/chromedriver/lib/chromedriver/chromedriver`
151 | 3. run tests via `npm test`
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "perfjankie",
3 | "main": "index.js",
4 | "version": "0.0.0",
5 | "homepage": "https://github.com/axemclion/perfjankie",
6 | "authors": [
7 | "Parashuram "
8 | ],
9 | "description": "Website for Perfjankie",
10 | "license": "MIT",
11 | "private": true,
12 | "ignore": [
13 | "**/.*",
14 | "node_modules",
15 | "bower_components",
16 | "test",
17 | "tests"
18 | ],
19 | "dependencies": {
20 | "angular": "~1.2.18",
21 | "bootstrap": "~3.1.1",
22 | "jqplot-bower": "~1.0.8",
23 | "jquery": "~2.1.1",
24 | "angular-route": "~1.2.25"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var program = require('commander'),
4 | fs = require('fs');
5 |
6 | program
7 | .version('0.0.1')
8 | .option('-c --config-file ', 'Specify a configuration file. If other options are specified, they have precedence over options in config file')
9 | .option('-s, --selenium ', 'Specify Selenium Server, like localhost:4444 or ondemand.saucelabs.com:80', 'localhost:4444')
10 | .option('-u --username ', 'Sauce, BrowserStack or Selenium User Name')
11 | .option('-a --accesskey ', 'Sauce, BrowserStack or Selenium Access Key')
12 | .option('--browsers ', 'List of browsers to run the tests on')
13 | .option('--couch-server ', 'Location of the couchDB server')
14 | .option('--couch-database ', 'Name of the couch database')
15 | .option('--couch-user ', 'Username of the couch user that can create design documents and save data')
16 | .option('--couch-pwd ', 'Password of the couchDB user')
17 | .option('--name ', 'A friendly name for the URL. This is shown as component name in the dashboard')
18 | .option('--run ', 'A hash for the commit, or any identifier displayed in the x-axis in the dashboard')
19 | .option('--time ', 'Used to sort the data when displaying graph. Can be the time or a sequence number when a commit was made', new Date().getTime())
20 | .option('--suite ', 'Displayed as the title in the dashboard.')
21 | .option('--update-site', 'Update the site in addition to running the tests', true)
22 | .option('--only-update-site', 'Only update the site, do not run tests or save data for the site', false)
23 | .option('--migrate ', 'Migrate Database to the latest version')
24 | .parse(process.argv);
25 |
26 | var config = {};
27 | if (program.configFile) {
28 | try {
29 | var config = JSON.parse(fs.readFileSync(program.configFile));
30 | } catch (e) {
31 | throw e;
32 | }
33 | }
34 |
35 | var extend = function(obj1, obj2) {
36 | for (var key in obj2) {
37 | if (typeof obj1[key] !== 'undefined' && typeof obj2[key] === 'object') {
38 | obj1[key] = extend(obj1[key], obj2[key]);
39 | } else if (typeof obj2[key] !== 'undefined') {
40 | obj1[key] = obj2[key];
41 | }
42 | }
43 | return obj1;
44 | };
45 |
46 | config = extend(config, {
47 | url: program.args[0],
48 | name: program.name,
49 | suite: program.suite,
50 | time: program.time,
51 | run: program.run,
52 |
53 | selenium: program.serverURL,
54 | browsers: program.browsers,
55 | username: program.username,
56 | accesskey: program.accesskey,
57 |
58 | log: {
59 | 'fatal': console.error.bind(console),
60 | 'error': console.error.bind(console),
61 | 'warn': console.warn.bind(console),
62 | 'info': console.info.bind(console),
63 | 'debug': console.log.bind(console),
64 | 'trace': console.log.bind(console)
65 | },
66 |
67 | couch: {
68 | server: program.couchServer,
69 | database: program.couchDatabase,
70 | updateSite: program.onlyUpdateSite ? true : program.updateSite,
71 | onlyUpdateSite: program.onlyUpdateSite,
72 | username: program.couchUser,
73 | pwd: program.couchPwd
74 | },
75 |
76 | callback: function(err, res) {
77 | console.log(err, res);
78 | }
79 | });
80 |
81 | if (program.migrate) {
82 | console.log('Runnung database migrations');
83 | require('../migrations/index.js')(config, program.migrate).done();
84 | } else {
85 | require('./')(config);
86 | }
--------------------------------------------------------------------------------
/lib/couch-views/metrics_data.js:
--------------------------------------------------------------------------------
1 | {
2 | _id: "_design/metrics_data",
3 | language: "javascript",
4 | views: {
5 | stats: {
6 | reduce: '_stats',
7 | map: function(doc) {
8 | if (doc.type === 'perfData') {
9 | for (var key in doc.data) {
10 | if (typeof doc.data[key] === 'number') {
11 | emit([doc.browser, doc.name, key, doc.time, doc.run], doc.data[key]);
12 | }
13 | }
14 | }
15 | }
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/lib/couch-views/pagelist.js:
--------------------------------------------------------------------------------
1 | {
2 | _id: "_design/pagelist",
3 | language: "javascript",
4 | views: {
5 | pages: {
6 | reduce: "_count",
7 | map: function(doc) {
8 | emit([doc.suite, doc.name, doc.meta._browserName], null);
9 | }
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/lib/couch-views/runs.js:
--------------------------------------------------------------------------------
1 | {
2 | _id: "_design/runs",
3 | language: "javascript",
4 | views: {
5 | list: {
6 | map: function(doc) {
7 | emit([doc.browser, doc.name, doc.time, doc.run], null);
8 | },
9 | reduce: "_count"
10 | },
11 | data: {
12 | map: function(doc) {
13 | if (doc.type === 'perfData') {
14 | for (var key in doc.data) {
15 | if (typeof doc.data[key] === 'number') {
16 | emit([doc.browser, doc.name, doc.time, doc.run, key], doc.data[key]);
17 | }
18 | }
19 | }
20 | },
21 | reduce: "_stats"
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/lib/couchData.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config, data) {
2 | var server = require('./utils').getCouchDB(config.couch),
3 | Q = require('q'),
4 | dfd = Q.defer();
5 |
6 | var db = null,
7 | log = config.log;
8 |
9 | db = server.use(config.couch.database);
10 | log.debug('Saving data');
11 |
12 | if (typeof data === 'undefined') {
13 | return;
14 | }
15 |
16 | db.bulk({
17 | docs: data.map(function (val) {
18 | var res = {
19 | url: config.url,
20 | data: val,
21 | meta: {},
22 | name: config.name,
23 | suite: config.suite,
24 | browser: val._browser || val._browserName,
25 | run: config.run || config.time,
26 | time: config.time,
27 | type: 'perfData'
28 | };
29 | for (var key in res.data) {
30 | if (key.indexOf('_') === 0) {
31 | res.meta[key] = res.data[key];
32 | delete res.data[key];
33 | }
34 | }
35 | return res;
36 | })
37 | }, {
38 | new_edits: true
39 | }, function (err, res) {
40 | log.debug('Got result back after saving data');
41 | err ? dfd.reject(err) : dfd.resolve(res);
42 | });
43 |
44 | return dfd.promise;
45 | };
46 |
--------------------------------------------------------------------------------
/lib/couchSite.js:
--------------------------------------------------------------------------------
1 | var url = require('url');
2 | module.exports = function (config) {
3 |
4 | var Q = require('q');
5 |
6 | if (!config.couch.updateSite) {
7 | return Q(1); // jshint ignore:line
8 | }
9 |
10 | var path = require('path'),
11 | log = config.log;
12 |
13 | var siteDest = path.join(__dirname, '../bin-site'),
14 | server = require('./utils').getCouchDB(config.couch),
15 | db = server.use(config.couch.database);
16 |
17 | var mime = require('./mime');
18 |
19 | function contentType(filename) {
20 | var contentType = path.extname(filename);
21 | if (contentType.length > 0) {
22 | return mime[contentType.substring(1)];
23 | } else {
24 | return 'text';
25 | }
26 | }
27 |
28 | function readFileContents(files, siteDest) {
29 | var fs = require('fs'),
30 | path = require('path');
31 |
32 | var dfd = Q.defer();
33 | var fileContents = [];
34 |
35 | (function readFile(i) {
36 | if (i < files.length) {
37 | fs.readFile(path.join(siteDest, files[i]), function (err, data) {
38 | if (!err) {
39 | fileContents.push({
40 | name: files[i],
41 | data: data,
42 | contentType: contentType(files[i]),
43 | content_type: contentType(files[i])
44 | });
45 | }
46 | readFile(i + 1);
47 | });
48 | } else {
49 | log.debug('Completed reading all files');
50 | dfd.resolve(fileContents);
51 | }
52 | }(0));
53 |
54 | return dfd.promise;
55 | }
56 |
57 | function removeSite() {
58 | var dfd = Q.defer();
59 | db.get('_design/site', function (err, res) {
60 | if (!err) {
61 | db.destroy('_design/site', res._rev, function (err, res) {
62 | if (err) {
63 | dfd.reject(err);
64 | } else {
65 | dfd.resolve();
66 | }
67 | });
68 | } else {
69 | dfd.resolve();
70 | }
71 | });
72 | return dfd.promise;
73 | }
74 |
75 | return removeSite().then(function () {
76 | return Q.nfcall(require('glob'), '**/*.*', {
77 | cwd: siteDest
78 | });
79 | }).then(function (files) {
80 | return readFileContents(files, siteDest);
81 | }).then(function (fileContents) {
82 | return Q.ninvoke(db.multipart, 'insert', {}, fileContents, '_design/site');
83 | }).then(function () {
84 | var link = url.parse([config.couch.server, config.couch.database, '_design/site/index.html'].join('/'));
85 | link.auth = null;
86 | log.info('Site Updated. View graphs at ' + url.format(link));
87 | });
88 | };
89 |
--------------------------------------------------------------------------------
/lib/couchViews.js:
--------------------------------------------------------------------------------
1 | /* jshint evil: true*/
2 | var Q = require('q');
3 |
4 | function uploadViews(db, log) {
5 | var dfd = Q.defer();
6 |
7 | var fs = require('fs'),
8 | glob = require('glob').sync;
9 |
10 | var views = glob(__dirname + '/couch-views/*.js');
11 |
12 | log.debug('Starting to upload views');
13 | (function uploadView(i) {
14 | if (i < views.length) {
15 | log.debug('Checking View ' + views[i]);
16 | var view = JSON.parse(JSON.stringify(eval('_x_ = ' + fs.readFileSync(views[i], 'utf-8')), function (key, val) {
17 | if (typeof val === 'function') {
18 | return val.toString();
19 | }
20 | return val;
21 | }));
22 | db.get(view._id, function (err, res) {
23 | if (!err) {
24 | view._rev = res._rev;
25 | }
26 | db.insert(view, function (err, res) {
27 | uploadView(i + 1);
28 | });
29 | });
30 | } else {
31 | log.debug('All views updated');
32 | dfd.resolve();
33 | }
34 | }(0));
35 |
36 | return dfd.promise;
37 | }
38 |
39 | module.exports = function (config) {
40 | if (!config.couch.updateSite) {
41 | return Q(); // jshint ignore:line
42 | }
43 |
44 | var log = config.log,
45 | server = require('./utils').getCouchDB(config.couch),
46 | db = server.use(config.couch.database);
47 |
48 | return uploadViews(db, log);
49 | };
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 |
3 | var init = require('./init'),
4 | site = require('./couchSite'),
5 | views = require('./couchViews'),
6 | data = require('./couchData'),
7 | perf = require('./perfTests');
8 |
9 | function runTests(config) {
10 | var dfd = Q.defer();
11 |
12 | (function next(i) {
13 | if (i < config.repeat) {
14 | perf(config).then(function(results) {
15 | return data(config, results);
16 | }).then(function() {
17 | next(i + 1);
18 | }, function(err) {
19 | dfd.reject(err);
20 | }).done();
21 | } else {
22 | dfd.resolve();
23 | }
24 | }(0));
25 | return dfd.promise;
26 | }
27 |
28 | module.exports = function(config) {
29 | var options = require('./options')(config),
30 | log = options.log,
31 | cb = options.callback;
32 |
33 | log.info('Starting PerfJankie');
34 |
35 | init(config).then(function() {
36 | return runTests(config);
37 | }).then(function() {
38 | return Q.allSettled([site(config), views(config)]);
39 | }).then(function(res) {
40 | log.debug('Successfully done all tasks');
41 | cb(null, res);
42 | }, function(err) {
43 | log.debug(err);
44 | cb(err, null);
45 | }).done();
46 | };
--------------------------------------------------------------------------------
/lib/init.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | var Q = require('q'),
3 | server = require('./utils').getCouchDB(config.couch),
4 | log = config.log;
5 |
6 | log.debug('Trying to see if the database exists');
7 |
8 | return Q.ninvoke(server.db, 'get', config.couch.database).catch(function (err) {
9 | log.debug('Could not find database: %s\nCreating a new database %s', err.reason, config.couch.database);
10 | return Q.ninvoke(server.db, 'create', config.couch.database);
11 | }).then(function () {
12 | var db = server.use(config.couch.database);
13 | return Q.ninvoke(db, 'get', 'version').catch(function (err) {
14 | return Q.ninvoke(db, 'insert', {
15 | version: require('../package.json').dbVersion
16 | }, 'version');
17 | });
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/lib/mime.js:
--------------------------------------------------------------------------------
1 | // from http://github.com/felixge/node-paperboy
2 | module.exports = {
3 | "aiff": "audio/x-aiff",
4 | "appcache": "text/cache-manifest",
5 | "arj": "application/x-arj-compressed",
6 | "asf": "video/x-ms-asf",
7 | "asx": "video/x-ms-asx",
8 | "au": "audio/ulaw",
9 | "avi": "video/x-msvideo",
10 | "bcpio": "application/x-bcpio",
11 | "ccad": "application/clariscad",
12 | "cod": "application/vnd.rim.cod",
13 | "com": "application/x-msdos-program",
14 | "cpio": "application/x-cpio",
15 | "cpt": "application/mac-compactpro",
16 | "csh": "application/x-csh",
17 | "css": "text/css",
18 | "deb": "application/x-debian-package",
19 | "dl": "video/dl",
20 | "doc": "application/msword",
21 | "drw": "application/drafting",
22 | "dvi": "application/x-dvi",
23 | "dwg": "application/acad",
24 | "dxf": "application/dxf",
25 | "dxr": "application/x-director",
26 | "etx": "text/x-setext",
27 | "ez": "application/andrew-inset",
28 | "fli": "video/x-fli",
29 | "flv": "video/x-flv",
30 | "gif": "image/gif",
31 | "gl": "video/gl",
32 | "gtar": "application/x-gtar",
33 | "gz": "application/x-gzip",
34 | "hdf": "application/x-hdf",
35 | "hqx": "application/mac-binhex40",
36 | "html": "text/html",
37 | "ice": "x-conference/x-cooltalk",
38 | "ico": "image/x-icon",
39 | "ief": "image/ief",
40 | "igs": "model/iges",
41 | "ips": "application/x-ipscript",
42 | "ipx": "application/x-ipix",
43 | "jad": "text/vnd.sun.j2me.app-descriptor",
44 | "jar": "application/java-archive",
45 | "jpeg": "image/jpeg",
46 | "jpg": "image/jpeg",
47 | "js": "text/javascript",
48 | "json": "application/json",
49 | "latex": "application/x-latex",
50 | "lsp": "application/x-lisp",
51 | "lzh": "application/octet-stream",
52 | "m": "text/plain",
53 | "m3u": "audio/x-mpegurl",
54 | "man": "application/x-troff-man",
55 | "me": "application/x-troff-me",
56 | "midi": "audio/midi",
57 | "mif": "application/x-mif",
58 | "mime": "www/mime",
59 | "movie": "video/x-sgi-movie",
60 | "mustache": "text/plain",
61 | "mp4": "video/mp4",
62 | "mpg": "video/mpeg",
63 | "mpga": "audio/mpeg",
64 | "ms": "application/x-troff-ms",
65 | "nc": "application/x-netcdf",
66 | "oda": "application/oda",
67 | "ogm": "application/ogg",
68 | "pbm": "image/x-portable-bitmap",
69 | "pdf": "application/pdf",
70 | "pgm": "image/x-portable-graymap",
71 | "pgn": "application/x-chess-pgn",
72 | "pgp": "application/pgp",
73 | "pm": "application/x-perl",
74 | "png": "image/png",
75 | "pnm": "image/x-portable-anymap",
76 | "ppm": "image/x-portable-pixmap",
77 | "ppz": "application/vnd.ms-powerpoint",
78 | "pre": "application/x-freelance",
79 | "prt": "application/pro_eng",
80 | "ps": "application/postscript",
81 | "qt": "video/quicktime",
82 | "ra": "audio/x-realaudio",
83 | "rar": "application/x-rar-compressed",
84 | "ras": "image/x-cmu-raster",
85 | "rgb": "image/x-rgb",
86 | "rm": "audio/x-pn-realaudio",
87 | "rpm": "audio/x-pn-realaudio-plugin",
88 | "rtf": "text/rtf",
89 | "rtx": "text/richtext",
90 | "scm": "application/x-lotusscreencam",
91 | "set": "application/set",
92 | "sgml": "text/sgml",
93 | "sh": "application/x-sh",
94 | "shar": "application/x-shar",
95 | "silo": "model/mesh",
96 | "sit": "application/x-stuffit",
97 | "skt": "application/x-koan",
98 | "smil": "application/smil",
99 | "snd": "audio/basic",
100 | "sol": "application/solids",
101 | "spl": "application/x-futuresplash",
102 | "src": "application/x-wais-source",
103 | "stl": "application/SLA",
104 | "stp": "application/STEP",
105 | "sv4cpio": "application/x-sv4cpio",
106 | "sv4crc": "application/x-sv4crc",
107 | "svg": "image/svg+xml",
108 | "swf": "application/x-shockwave-flash",
109 | "tar": "application/x-tar",
110 | "tcl": "application/x-tcl",
111 | "tex": "application/x-tex",
112 | "texinfo": "application/x-texinfo",
113 | "tgz": "application/x-tar-gz",
114 | "tiff": "image/tiff",
115 | "tr": "application/x-troff",
116 | "tsi": "audio/TSP-audio",
117 | "tsp": "application/dsptype",
118 | "tsv": "text/tab-separated-values",
119 | "unv": "application/i-deas",
120 | "ustar": "application/x-ustar",
121 | "vcd": "application/x-cdlink",
122 | "vda": "application/vda",
123 | "vivo": "video/vnd.vivo",
124 | "vrm": "x-world/x-vrml",
125 | "wav": "audio/x-wav",
126 | "wax": "audio/x-ms-wax",
127 | "wma": "audio/x-ms-wma",
128 | "wmv": "video/x-ms-wmv",
129 | "wmx": "video/x-ms-wmx",
130 | "wrl": "model/vrml",
131 | "wvx": "video/x-ms-wvx",
132 | "xbm": "image/x-xbitmap",
133 | "xlw": "application/vnd.ms-excel",
134 | "xml": "text/xml",
135 | "xpm": "image/x-xpixmap",
136 | "xwd": "image/x-xwindowdump",
137 | "xyz": "chemical/x-pdb",
138 | "zip": "application/zip"
139 | };
--------------------------------------------------------------------------------
/lib/options.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | var noop = function() {};
3 | config.log = config.log || config.logger || noop;
4 |
5 | if (typeof config.log === 'function') {
6 | config.log = {
7 | 'fatal': config.log,
8 | 'error': config.log,
9 | 'warn': config.log,
10 | 'info': config.log,
11 | 'debug': config.log,
12 | 'trace': config.log
13 | };
14 | }
15 |
16 | function assert(expr, msg) {
17 | if (!expr) {
18 | throw new Error(msg);
19 | }
20 | }
21 | config.selenium = config.selenium || {
22 | hostname: 'localhost',
23 | port: 4444
24 | };
25 | config.repeat = config.repeat || 1;
26 | config.name = config.name || config.url;
27 | config.suite = config.suite || 'Default Test Suite';
28 |
29 | if (typeof config.callback !== 'function') {
30 | config.callback = noop;
31 | }
32 |
33 | if (typeof config.couch === 'undefined') {
34 | config.couch = {};
35 | }
36 |
37 | if (typeof config.couch.username !== 'undefined') {
38 | var url = require('url');
39 | var href = url.parse(config.couch.server);
40 | href.auth = config.couch.username + ':' + config.couch.pwd;
41 | config.couch.server = url.format(href);
42 | }
43 | assert(typeof config.couch.server !== 'undefined', 'Location to save results is not defined. Please define a couchDB server');
44 |
45 | config.couch.database = config.couch.db || config.couch.database;
46 | assert(typeof config.couch.server !== 'undefined', 'Location to save results is not defined. Please define a Database to save the results');
47 | return config;
48 | };
--------------------------------------------------------------------------------
/lib/perfTests.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | var Q = require('q'),
3 | dfd = Q.defer();
4 |
5 | if (config.couch.onlyUpdateSite) {
6 | dfd.resolve();
7 | } else {
8 | var browserPerf = config.browserPerf || require('browser-perf'),
9 | log = config.log;
10 |
11 | log.debug('Starting Browser Perf');
12 | browserPerf(config.url, function(err, results) {
13 | if (err) {
14 | dfd.reject(err);
15 | } else {
16 | log.debug('Got Browser Perf results back, now saving the results');
17 | dfd.resolve(results);
18 | }
19 | }, {
20 | browsers: config.browsers,
21 | selenium: config.selenium,
22 | debugBrowser: config.debug,
23 | preScript: config.preScript,
24 | preScriptFile: config.preScriptFile,
25 | actions: config.actions,
26 | metrics: config.metrics,
27 | SAUCE_ACCESSKEY: config.SAUCE_ACCESSKEY || undefined,
28 | SAUCE_USERNAME: config.SAUCE_USERNAME || undefined,
29 | BROWSERSTACK_USERNAME: config.BROWSERSTACK_USERNAME || undefined,
30 | BROWSERSTACK_KEY: config.BROWSERSTACK_KEY || undefined
31 | });
32 | }
33 | return dfd.promise;
34 | };
35 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | var nano = require('nano');
2 |
3 |
4 | var getCouchDB = function (options) {
5 | var serverUrl = options.server,
6 | server;
7 | if (options.requestOptions) {
8 | server = nano({
9 | "url": serverUrl,
10 | "parseUrl": false,
11 | "requestDefaults": options.requestOptions
12 | });
13 | } else {
14 | server = nano({
15 | "url": serverUrl,
16 | "parseUrl": false
17 | });
18 | }
19 | return server;
20 | };
21 |
22 | module.exports = {
23 | getCouchDB: getCouchDB
24 | };
--------------------------------------------------------------------------------
/migrations/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var program = require('commander'),
4 | fs = require('fs');
5 |
6 | var oldDB, newDB;
7 | program
8 | .version('0.0.1')
9 | .option('-c --couchServer', 'Location of the couchDB server')
10 | .option('-u --username ', 'Username of the couch user that can create design documents and save data')
11 | .option('-p --password ', 'Password of the couchDB user')
12 | .parse(process.argv);
13 |
14 | program.on('--help', function() {
15 | console.log('Usage:');
16 | console.log('');
17 | console.log(' $ perfjankie-dbmigrate old-database new-database [options]');
18 | console.log('');
19 | });
20 |
21 | program.parse(process.argv);
22 | if (program.args.length !== 2) {
23 | program.help();
24 | }
25 |
26 | var config = {
27 | log: {
28 | 'fatal': console.error.bind(console),
29 | 'error': console.error.bind(console),
30 | 'warn': console.warn.bind(console),
31 | 'info': console.info.bind(console),
32 | 'debug': console.log.bind(console),
33 | 'trace': console.log.bind(console),
34 | },
35 |
36 | couch: {
37 | server: program.couchServer || 'http://localhost:5984',
38 | username: program.username,
39 | pwd: program.password
40 | },
41 |
42 | callback: function(err, res) {
43 | console.log(err, res);
44 | }
45 | };
46 |
47 | console.log(program.username, program.password, oldDB, newDB)
48 |
49 | require('./index.js')(config, program.args[0], program.args[1]).done();
--------------------------------------------------------------------------------
/migrations/index.js:
--------------------------------------------------------------------------------
1 | var semver = require('semver');
2 | var glob = require('glob');
3 | var nano = require('nano');
4 | var Q = require('q');
5 |
6 | Q.longStackSupport = true;
7 |
8 | var dbInit = require('../lib/init.js');
9 |
10 | module.exports = function migrate(opts, oldDBName, newDBName) {
11 | var config = require('../lib/options')(opts);
12 | var server = nano(config.couch.server);
13 | var oldDB = server.use(oldDBName);
14 |
15 | config.couch.updateSite = true;
16 | config.couch.database = newDBName;
17 |
18 | return dbInit(config).then(function() {
19 | config.log.info('Migrating from ', oldDBName, 'to', newDBName);
20 | return Q.ninvoke(oldDB, 'get', 'version').then(function(version) {
21 | return version[0].version;
22 | }, function() {
23 | return '0.1.0';
24 | });
25 | }).then(function(oldDBVersion) {
26 | return glob.sync('migrate-*.js', {
27 | cwd: __dirname
28 | }).filter(function(file) {
29 | return semver.lt(oldDBVersion, file.slice(8, -3));
30 | }).sort(function(a, b) {
31 | return semver.gt(a.slice(8, -3), b.slice(8, -3));
32 | });
33 | }).then(function(files) {
34 | if (files.length === 0) {
35 | config.log.info('Database is already up to date');
36 | }
37 | return files.map(function(file) {
38 | var script = require('./' + file);
39 | config.log.info('\nRunning migration - %s', file);
40 | return script(oldDB, server.use(newDBName), config);
41 | }).reduce(Q.when, Q());
42 | }).then(function() {
43 | config.log.info('Updating views');
44 | return require('../lib/couchViews.js')(config);
45 | }).then(function() {
46 | config.log.info('Updating site');
47 | return require('../lib/couchSite.js')(config);
48 | });
49 | };
--------------------------------------------------------------------------------
/migrations/migrate-0.2.0.js:
--------------------------------------------------------------------------------
1 | // Migrating from 0.1.x to 0.2.0
2 |
3 | var Q = require('q');
4 |
5 | var utility = require('./utility');
6 |
7 | module.exports = function(oldDb, newDb, config) {
8 | var log = config.log;
9 |
10 | return utility.forEachDoc(oldDb, newDb, function(doc) {
11 | if (doc.type !== 'perfData') {
12 | return null;
13 | }
14 | delete doc._id;
15 | delete doc._rev;
16 | doc.url = null;
17 | doc.browser = doc.meta._browserName || null;
18 |
19 | for (var key in doc.data) {
20 | doc.data[key] = doc.data[key].value;
21 | }
22 |
23 | doc.data[events[0]] = 0;
24 | for (var i = 1; i < events.length; i++) {
25 | if (typeof doc.data[events[i]] === 'undefined') {
26 | doc.data[events[i]] = 0;
27 | }
28 | doc.data[events[i]] = doc.data[events[i]] + doc.data[events[i - 1]];
29 | }
30 |
31 | return doc;
32 | });
33 | };
34 |
35 | var events = [
36 | 'navigationStart',
37 | 'unloadEventStart',
38 | 'unloadEventEnd',
39 | 'redirectStart',
40 | 'redirectEnd',
41 | 'fetchStart',
42 | 'domainLookupStart',
43 | 'domainLookupEnd',
44 | 'connectStart',
45 | 'connectEnd',
46 | 'secureConnectionStart',
47 | 'requestStart',
48 | 'responseStart',
49 | 'domLoading',
50 | 'domInteractive',
51 | 'domContentLoadedEventStart',
52 | 'domContentLoadedEventEnd',
53 | 'domComplete',
54 | 'loadEventStart',
55 | 'loadEventEnd'
56 | ];
--------------------------------------------------------------------------------
/migrations/migrate-0.3.0.js:
--------------------------------------------------------------------------------
1 | // Migrating from 0.2.x to 1.2.x
2 |
3 | var Q = require('q');
4 |
5 | var utility = require('./utility');
6 |
7 | module.exports = function(oldDb, newDb, config) {
8 | var log = config.log;
9 |
10 | return utility.forEachDoc(oldDb, newDb, function(doc) {
11 | if (doc.type !== 'perfData') {
12 | return null;
13 | }
14 | delete doc._id;
15 | delete doc._rev;
16 |
17 | for (var key in doc.data) {
18 | if (typeof doc.data.mean_frame_time === 'number') { // From TracingMetrics
19 | doc.data.frames_per_sec = 1000 / doc.data.mean_frame_time;
20 | }
21 | if (typeof doc.data.meanFrameTime === 'number') { // from RAF
22 | doc.data.framesPerSec_raf = 1000 / doc.data.meanFrameTime;
23 |
24 | }
25 | }
26 |
27 | return doc;
28 | });
29 | };
30 |
31 | var getFramesPerSec = function(val, metrics) {
32 | var mft;
33 | // Iterate over each candidate to calculate FPS
34 | for (var i = 0; i < metrics.length; i++) {
35 | if (val[metrics[i]]) {
36 | mft = val[metrics[i]].sum / val[metrics[i]].count;
37 | }
38 | if (mft >= 10 && mft <= 60) {
39 | break;
40 | } else {
41 | mft = null;
42 | }
43 | }
44 | if (mft) {
45 | return {
46 | sum: 1000 / mft,
47 | count: 1
48 | };
49 | }
50 | };
--------------------------------------------------------------------------------
/migrations/migrate-0.4.0.js:
--------------------------------------------------------------------------------
1 | // Migrating to browser-perf@1.3.0. Names of some metrics have changed
2 |
3 | var Q = require('q');
4 |
5 | var utility = require('./utility');
6 |
7 | module.exports = function(oldDb, newDb, config) {
8 | var log = config.log;
9 |
10 | return utility.forEachDoc(oldDb, newDb, function(doc) {
11 | if (doc.type !== 'perfData') {
12 | return null;
13 | }
14 | delete doc._id;
15 | delete doc._rev;
16 |
17 | doc.data.meanFrameTime_raf = doc.data.meanFrameTime;
18 |
19 | var metrics = new Metrics(doc.data);
20 |
21 | metrics.addMetric('loadTime', 'loadEventEnd', 'fetchStart');
22 | metrics.addMetric('domReadyTime', 'domComplete', 'domInteractive');
23 | metrics.addMetric('readyStart', 'fetchStart', 'navigationStart');
24 | metrics.addMetric('redirectTime', 'redirectEnd', 'redirectStart');
25 | metrics.addMetric('appcacheTime', 'domainLookupStart', 'fetchStart');
26 | metrics.addMetric('unloadEventTime', 'unloadEventEnd', 'unloadEventStart');
27 | metrics.addMetric('domainLookupTime', 'domainLookupEnd', 'domainLookupStart');
28 | metrics.addMetric('connectTime', 'connectEnd', 'connectStart');
29 | metrics.addMetric('requestTime', 'responseEnd', 'requestStart');
30 | metrics.addMetric('initDomTreeTime', 'domInteractive', 'responseEnd');
31 | metrics.addMetric('loadEventTime', 'loadEventEnd', 'loadEventStart');
32 |
33 | return doc;
34 | });
35 | };
36 |
37 | function Metrics(metrics) {
38 | this.timing = metrics;
39 | }
40 |
41 | Metrics.prototype.addMetric = function(prop, a, b) {
42 | if (typeof this.timing[a] === 'number' && typeof this.timing[b] === 'number') {
43 | this.timing[prop] = this.timing[a] - this.timing[b];
44 | }
45 | }
--------------------------------------------------------------------------------
/migrations/utility.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var MAX_LIMIT = 53;
3 |
4 | module.exports = {
5 | forEachDoc: function(oldDb, newDb, callback) {
6 | function processBatch(skip) {
7 | skip = skip || 0;
8 | var count = 0;
9 | return Q.ninvoke(oldDb, 'get', '_all_docs', {
10 | limit: MAX_LIMIT,
11 | skip: skip,
12 | include_docs: true
13 | }).then(function(docs) {
14 | count = docs[0].rows.length;
15 | return result = docs[0].rows.map(function(data) {
16 | return callback(data.doc);
17 | });
18 | }).then(function(results) {
19 | return Q.ninvoke(newDb, 'bulk', {
20 | docs: results.filter(function(val) {
21 | return val !== null;
22 | })
23 | }, {
24 | new_edits: true
25 | });
26 | }).then(function() {
27 | if (count >= MAX_LIMIT) {
28 | return processBatch(skip + MAX_LIMIT);
29 | } else {
30 | return Q();
31 | }
32 | });
33 | }
34 |
35 | return processBatch();
36 | }
37 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "perfjankie",
3 | "version": "2.1.2",
4 | "dbVersion": "0.4.0",
5 | "description": "Browser Performance regression suite",
6 | "main": "lib/index.js",
7 | "scripts": {
8 | "test": "bower install && grunt test",
9 | "prepublish": "grunt clean dist"
10 | },
11 | "bin": {
12 | "perfjankie": "lib/cli.js",
13 | "perfjankie-dbmigrate": "migrations/cli.js"
14 | },
15 | "author": "Parashuram ",
16 | "license": "BSD-2-Clause",
17 | "dependencies": {
18 | "browser-perf": "~1.4.0",
19 | "commander": "~2.8.1",
20 | "glob": "~5.0.14",
21 | "nano": "~6.1.5",
22 | "q": "~1.4.1",
23 | "sauce-tunnel": "^2.2.3",
24 | "semver": "^5.0.1",
25 | "serve-static": "^1.10.0"
26 | },
27 | "devDependencies": {
28 | "bunyan": "~1.5.1",
29 | "bower": "^1.7.9",
30 | "chai": "~3.2.0",
31 | "chai-as-promised": "^5.1.0",
32 | "chromedriver": "^2.21.2",
33 | "dtrace-provider": "^0.6.0",
34 | "grunt": "~0.4.5",
35 | "grunt-autoprefixer": "^3.0.3",
36 | "grunt-connect-proxy": "^0.2.0",
37 | "grunt-contrib-clean": "~0.6.0",
38 | "grunt-contrib-concat": "^0.5.1",
39 | "grunt-contrib-connect": "~0.11.2",
40 | "grunt-contrib-copy": "^0.8.0",
41 | "grunt-contrib-htmlmin": "^0.4.0",
42 | "grunt-contrib-jshint": "~0.11.2",
43 | "grunt-contrib-less": "^1.0.1",
44 | "grunt-contrib-uglify": "^0.9.1",
45 | "grunt-contrib-watch": "~0.6.1",
46 | "grunt-mocha-test": "~0.12.7",
47 | "grunt-processhtml": "^0.3.8",
48 | "load-grunt-tasks": "~3.2.0",
49 | "mocha": "~2.2.5",
50 | "selenium-server": "^2.53.0",
51 | "sinon": "~1.15.4"
52 | },
53 | "keywords": [
54 | "browser-perf",
55 | "telemetry",
56 | "gruntplugin"
57 | ],
58 | "directories": {
59 | "test": "test"
60 | },
61 | "repository": {
62 | "type": "git",
63 | "url": "git://github.com/axemclion/perfjankie.git"
64 | },
65 | "bugs": {
66 | "url": "https://github.com/axemclion/perfjankie/issues"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tasks/metricsgen.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var browserPerf = require('browser-perf');
3 |
4 | module.exports = function(grunt) {
5 | grunt.registerMultiTask('metricsgen', 'Generates the names of metrics', function() {
6 | var apiDocs = new browserPerf.docs();
7 | var regex = /(_avg|_max|_count)$/;
8 | var doc = {};
9 |
10 | for (var key in apiDocs.metrics) {
11 | var modifier = null;
12 | if (apiDocs.metrics[key].source === 'TimelineMetrics' && key.match(regex)) {
13 | var idx = key.lastIndexOf('_');
14 | modifier = key.substr(idx + 1);
15 | key = key.substr(0, idx);
16 | }
17 | if (typeof doc[key] === 'undefined') {
18 | doc[key] = apiDocs.metrics[key] || {};
19 | doc[key].stats = [];
20 | }
21 | if (modifier) {
22 | doc[key].stats.push(modifier);
23 | }
24 | }
25 |
26 | var metrics = [];
27 | for (var key in doc) {
28 | doc[key].name = key;
29 | metrics.push(doc[key]);
30 | }
31 |
32 | this.files.forEach(function(file) {
33 | grunt.file.write(file.dest, ['var METRICS_LIST =', JSON.stringify(metrics)].join(''));
34 | });
35 | });
36 | };
--------------------------------------------------------------------------------
/tasks/task.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | grunt.registerMultiTask('perfjankie', 'Run rendering performance test cases', function() {
3 | var done = this.async(),
4 | path = require('path'),
5 | options = this.options({
6 | log: { // Expects the following methods,
7 | fatal: grunt.fail.fatal.bind(grunt.fail),
8 | error: grunt.fail.warn.bind(grunt.fail),
9 | warn: grunt.log.error.bind(grunt.log),
10 | info: grunt.log.ok.bind(grunt.log),
11 | debug: grunt.verbose.writeln.bind(grunt.verbose),
12 | trace: grunt.log.debug.bind(grunt.log)
13 | },
14 | time: new Date().getTime()
15 | }),
16 | files = options.urls;
17 |
18 | options.time = parseFloat(options.time, 10);
19 | if (options.sauceTunnel) {
20 | var SauceTunnel = require('sauce-tunnel');
21 | grunt.log.writeln('Starting Saucelabs Tunnel');
22 | var tunnel = new SauceTunnel(options.SAUCE_USERNAME, options.SAUCE_ACCESSKEY, options.sauceTunnel, true);
23 | tunnel.start(function(status) {
24 | grunt.log.ok('Saucelabs Tunnel started - ' + status);
25 | if (status === false) {
26 | done(false);
27 | } else {
28 | runPerfTest(files, options, function(res) {
29 | grunt.verbose.writeln('All perf tests completed');
30 | tunnel.stop(function() {
31 | done(res);
32 | });
33 | });
34 | }
35 | });
36 | } else {
37 | runPerfTest(files, options, done);
38 | }
39 | });
40 |
41 | var perfjankie = require('..');
42 |
43 | var runPerfTest = function(files, options, cb) {
44 | var success = true;
45 | (function runTest(i) {
46 | if (i < files.length) {
47 | grunt.log.writeln('Testing File ', files[i]);
48 | var config = {
49 | url: files[i],
50 | name: files[i].replace(/(\S)*\/|\.html$/gi, ''),
51 | callback: function(err, res) {
52 | if (err) {
53 | success = false;
54 | console.log(res);
55 | grunt.log.warn(err);
56 | } else {
57 | grunt.log.ok('Saved performance metrics');
58 | }
59 | runTest(i + 1);
60 | }
61 | };
62 | for (var key in options) {
63 | config[key] = options[key];
64 | }
65 | perfjankie(config);
66 | } else {
67 | cb(success);
68 | }
69 | }(0));
70 | }
71 | };
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | var expect = require('chai').expect,
2 | sinon = require('sinon'),
3 | fs = require('fs'),
4 | nano = require('nano');
5 |
6 | require('q').longStackSupport = true;
7 |
8 | describe('App', function () {
9 | var browserPerfStub = sinon.stub();
10 | var sampleData;
11 | var util = require('./util'),
12 | app = require('../'),
13 | config = util.config({});
14 |
15 | before(function (done) {
16 | nano(config.couch.server).db.destroy(config.couch.database, function(err, res) {
17 | done();
18 | });
19 | });
20 |
21 | beforeEach(function () {
22 | config.log.info('===========');
23 | sampleData = JSON.parse(fs.readFileSync(__dirname + '/res/sample-perf-results.json', 'utf8'));
24 | browserPerfStub.callsArgWith(1, null, sampleData);
25 | });
26 |
27 | it('should only update data', function (done) {
28 | app(util.config({
29 | couch: {
30 | updateSite: false
31 | },
32 | callback: function (err, res) {
33 | console.log(err);
34 | expect(err).to.not.be.ok;
35 | expect(res).to.be.ok;
36 | done();
37 | },
38 | browserPerf: browserPerfStub
39 | }));
40 | });
41 |
42 | it('should only update site', function (done) {
43 | var couchDataStub = sinon.stub();
44 | couchDataStub.callsArgWith(2, null, []);
45 | app(util.config({
46 | couch: {
47 | onlyUpdateSite: true
48 | },
49 | callback: function (err, res) {
50 | expect(couchDataStub.called).to.not.be.true;
51 | expect(err).to.not.be.ok;
52 | expect(res).to.be.ok;
53 | done();
54 | }
55 | }));
56 | });
57 |
58 | it('should run performance tests and save results in a database', function (done) {
59 | app(util.config({
60 | callback: function (err, res) {
61 | expect(err).to.not.be.ok;
62 | expect(res).to.be.ok;
63 | done();
64 | },
65 | browserPerf: browserPerfStub
66 | }));
67 | });
68 | });
--------------------------------------------------------------------------------
/test/res/local.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "browsers": [{
3 | "browserName": "chrome",
4 | "version": 32
5 | }],
6 | "selenium": {
7 | "hostname": "localhost",
8 | "port": 4444
9 | },
10 |
11 | "couch": {
12 | "server": "http://localhost:5984",
13 | "username": "admin_user",
14 | "pwd": "admin_pass",
15 | "database": "perfjankie-test"
16 | }
17 | }
--------------------------------------------------------------------------------
/test/res/sample-perf-results.json:
--------------------------------------------------------------------------------
1 | [{
2 | "numAnimationFrames": 397,
3 | "numFramesSentToScreen": 397,
4 | "droppedFrameCount": 60,
5 | "meanFrameTime": 19.36006281407138,
6 | "fetchStart": 1411941392008,
7 | "redirectStart": 0,
8 | "domComplete": 1411941394116,
9 | "redirectEnd": 0,
10 | "loadEventStart": 1411941394116,
11 | "navigationStart": 1411941391221,
12 | "requestStart": 1411941392105,
13 | "responseEnd": 1411941392800,
14 | "secureConnectionStart": 0,
15 | "domLoading": 1411941392268,
16 | "domInteractive": 1411941393139,
17 | "domainLookupEnd": 1411941392026,
18 | "domContentLoadedEventStart": 1411941393139,
19 | "loadEventEnd": 1411941394141,
20 | "connectEnd": 1411941392105,
21 | "responseStart": 1411941392256,
22 | "unloadEventStart": 0,
23 | "domContentLoadedEventEnd": 1411941393423,
24 | "connectStart": 1411941392026,
25 | "unloadEventEnd": 0,
26 | "domainLookupStart": 1411941392009,
27 | "Program": 3199.1089999601245,
28 | "Program_avg": 0.691250864295619,
29 | "Program_max": 406.51899999938905,
30 | "Program_count": 4628,
31 | "UpdateLayerTree": 26.194000008516014,
32 | "UpdateLayerTree_avg": 0.06120093459933648,
33 | "UpdateLayerTree_max": 0.5839999997988343,
34 | "UpdateLayerTree_count": 428,
35 | "EvaluateScript": 430.41699999943376,
36 | "EvaluateScript_avg": 5.380212499992922,
37 | "EvaluateScript_max": 198.59800000023097,
38 | "EvaluateScript_count": 80,
39 | "ParseHTML": 420.78200000245124,
40 | "ParseHTML_avg": 2.963253521144023,
41 | "ParseHTML_max": 318.4650000007823,
42 | "ParseHTML_count": 142,
43 | "RecalculateStyles": 105.08500000182539,
44 | "RecalculateStyles_avg": 0.5937005649820644,
45 | "RecalculateStyles_max": 27.915000000037253,
46 | "RecalculateStyles_count": 177,
47 | "Layout": 133.47499999776483,
48 | "Layout_avg": 1.4830555555307203,
49 | "Layout_max": 41.8179999999702,
50 | "Layout_count": 90,
51 | "EventDispatch": 589.1679999958724,
52 | "EventDispatch_avg": 1.812824615371915,
53 | "EventDispatch_max": 283.63100000005215,
54 | "EventDispatch_count": 325,
55 | "FunctionCall": 1250.5349999787286,
56 | "FunctionCall_avg": 0.9762177985782424,
57 | "FunctionCall_max": 283.51899999938905,
58 | "FunctionCall_count": 1281,
59 | "TimerFire": 405.9339999919757,
60 | "TimerFire_avg": 0.9900829268096969,
61 | "TimerFire_max": 58.355999999679625,
62 | "TimerFire_count": 410,
63 | "GCEvent": 70.74999999906868,
64 | "GCEvent_avg": 7.861111111007631,
65 | "GCEvent_max": 32.51300000026822,
66 | "GCEvent_count": 9,
67 | "FireAnimationFrame": 67.84199998434633,
68 | "FireAnimationFrame_avg": 0.16751111107246008,
69 | "FireAnimationFrame_max": 0.7209999999031425,
70 | "FireAnimationFrame_count": 405,
71 | "PaintSetup": 7.717000005766749,
72 | "PaintSetup_avg": 0.11692424251161741,
73 | "PaintSetup_max": 0.7190000005066395,
74 | "PaintSetup_count": 66,
75 | "Paint": 217.3209999995306,
76 | "Paint_avg": 1.3013233532906026,
77 | "Paint_max": 38.62199999950826,
78 | "Paint_count": 167,
79 | "DecodeImage": 12.86499999742955,
80 | "DecodeImage_avg": 0.31378048774218414,
81 | "DecodeImage_max": 1.894000000320375,
82 | "DecodeImage_count": 41,
83 | "CompositeLayers": 103.9480000063777,
84 | "CompositeLayers_avg": 0.2541515892576472,
85 | "CompositeLayers_max": 10.35800000000745,
86 | "CompositeLayers_count": 409,
87 | "XHRReadyStateChange": 91.92799999844283,
88 | "XHRReadyStateChange_avg": 2.553555555512301,
89 | "XHRReadyStateChange_max": 75.00499999988824,
90 | "XHRReadyStateChange_count": 36,
91 | "mean_frame_time": 17.56031067961447,
92 | "jank": 75.3956754589968,
93 | "mostly_smooth": 18.0550000006333,
94 | "Layers": 7,
95 | "PaintedArea_total": 17244083,
96 | "PaintedArea_avg": 103257.98203592814,
97 | "NodePerLayout_avg": 575.2888888888889,
98 | "ExpensivePaints": 2,
99 | "GCInsideAnimation": 0,
100 | "ExpensiveEventHandlers": 3,
101 | "_browserName": "chrome",
102 | "_url": "http://amazon.com"
103 | }]
--------------------------------------------------------------------------------
/test/res/test1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
36 |
37 |
--------------------------------------------------------------------------------
/test/res/test2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DIV
7 | DIV
8 | DIV
9 | DIV
10 | DIV
11 | DIV
12 | DIV
13 | DIV
14 | DIV
15 | DIV
16 | DIV
17 | DIV
18 | DIV
19 | DIV
20 | DIV
21 | DIV
22 | DIV
23 | DIV
24 | DIV
25 | DIV
26 | DIV
27 | DIV
28 | DIV
29 | DIV
30 | DIV
31 | DIV
32 | DIV
33 | DIV
34 | DIV
35 | DIV
36 | DIV
37 | DIV
38 | DIV
39 | DIV
40 | DIV
41 | DIV
42 | DIV
43 | DIV
44 | DIV
45 | DIV
46 | DIV
47 | DIV
48 | DIV
49 | DIV
50 | DIV
51 | DIV
52 | DIV
53 | DIV
54 | DIV
55 | DIV
56 |
57 |
--------------------------------------------------------------------------------
/test/seedData.js:
--------------------------------------------------------------------------------
1 | module.exports = function(callback, count) {
2 | count = count || 1000;
3 | var path = require('path');
4 | var sampleData = require('fs').readFileSync(path.join(__dirname, '/res/sample-perf-results.json'), 'utf8');
5 |
6 | var browsers = ['firefox', 'chrome'],
7 | components = ['component1', 'component2'],
8 | commits = ['commit#1', 'commit#2', 'commit#3', 'commit#4', 'commit#5', 'commit#3'];
9 |
10 | var couchData = require('./../lib/couchData'),
11 | config = require('./util').config();
12 |
13 | var rand = function(arr) {
14 | return arr[Math.floor(Math.random() * arr.length)];
15 | };
16 |
17 | (function genData(i) {
18 | config.name = rand(components);
19 | config.time = 7 + Math.floor(Math.random() * 100 % 6);
20 | config.run = commits[config.time - 7];
21 | config.suite = 'Test Suite 1';
22 | var data = JSON.parse(sampleData);
23 | for (var key in data[0]) {
24 | data[0][key] = data[0][key] * Math.random() * 3;
25 | }
26 | data[0]._browserName = rand(browsers);
27 | couchData(config, data).then(function() {
28 | if (i < count) {
29 | genData(i + 1);
30 | } else {
31 | callback(true);
32 | }
33 | }, function() {
34 | callback(false);
35 | }).done();
36 | }(0));
37 | };
--------------------------------------------------------------------------------
/test/util.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | config: function(config) {
3 | config = config || {};
4 | var options = {
5 | "url": "http://localhost:9000/test1.html",
6 | //"url": "https://axemclion.cloudant.com/",
7 | "name": "Page - Test1",
8 | "suite": 'Suite1',
9 | "time": new Date().getTime(),
10 | "run": 'commit#' + new Date().getMilliseconds(),
11 | "browsers": ['chrome', 'firefox'],
12 | "selenium": {
13 | hostname: "localhost",
14 | port: 4444
15 | },
16 | "log": config.log || require('bunyan').createLogger({
17 | name: 'test',
18 | src: true,
19 | level: 'debug',
20 | //stream: process.stdout,
21 | streams: [{
22 | path: 'test.log'
23 | }]
24 | }),
25 | "couch": {
26 | server: 'http://localhost:5984',
27 | database: 'perfjankie-test',
28 | updateSite: true,
29 | onlyUpdateSite: false
30 | }
31 | };
32 |
33 | var extend = function(options, config) {
34 | for (var key in config) {
35 | if (typeof options[key] === 'object' && typeof config[key] === 'object') {
36 | options[key] = extend(options[key], config[key]);
37 | } else {
38 | options[key] = config[key];
39 | }
40 | }
41 | return options;
42 | };
43 |
44 | return extend(options, config || {});
45 | }
46 | };
--------------------------------------------------------------------------------
/www/app/all-metrics/all-metrics.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
{{metric.name | formatMetric}}
20 |
{{metric.summary}}
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/www/app/all-metrics/all-metrics.less:
--------------------------------------------------------------------------------
1 | .page.all-metrics {
2 | .metric-names {
3 | li {
4 | margin: 30px 0 0 0;
5 | .list-group-item{
6 | background: #fefefe;
7 | float: left;
8 | padding: 10px;
9 | border: SOLID 1px #ccc;
10 | display: block;
11 | width: 100%;
12 | border-radius: 5px;
13 | p{
14 | height: 70px;
15 | }
16 | .browser-icons{
17 | font-size: 1.1em;
18 | color: #333;
19 | span.disabled{
20 | color: #ccc;
21 | }
22 | }
23 |
24 | }
25 | &.disabled{
26 | opacity: 0.5;
27 | .list-group-item{
28 | background: #ccc;
29 | cursor: not-allowed;
30 | }
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/www/app/all-metrics/allmetrics.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('allmetrics', ['ngRoute', 'Backend', 'metricdetail'])
3 | .config(['$routeProvider',
4 | function($routeProvider) {
5 | $routeProvider.when('/all-metrics', {
6 | templateUrl: 'app/all-metrics/all-metrics.html',
7 | controller: 'AllMetricsCtrl',
8 | controllerAs: 'metrics',
9 | resolve: {
10 | MetricNames: ['Data',
11 | function(data, $routeParams) {
12 | return data.getAllMetrics();
13 | }
14 | ]
15 | }
16 | });
17 | }
18 | ])
19 | .controller('AllMetricsCtrl', ['$routeParams', 'MetricNames',
20 | function($routeParams, MetricNames) {
21 | this.metricNames = MetricNames;
22 | }
23 | ])
24 | .filter('metricFilter', ['$filter',
25 | function($filter) {
26 | return function(input, query) {
27 | if (!query) {
28 | return input;
29 | }
30 | var result = [];
31 | var regex = new RegExp(query, 'i');
32 | var filter = $filter('formatMetric');
33 | for (var i = 0; i < input.length; i++) {
34 | if (regex.test(filter(input[i].name))) {
35 | result.push(input[i]);
36 | }
37 | }
38 | return result;
39 | };
40 | }
41 | ]);
--------------------------------------------------------------------------------
/www/app/app.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('perfjankie', ['ngRoute', 'sidebar', 'pageSelect', 'summary', 'allmetrics'])
3 | .config(['$routeProvider',
4 | function($routeProvider) {
5 | $routeProvider.otherwise({
6 | redirectTo: '/page-select'
7 | });
8 | }
9 | ])
10 | .controller('MainPageCtrl', ['$scope', '$location', '$routeParams',
11 | function($scope, $location, $routeParams) {
12 | $scope.$on('$routeChangeSuccess', function(scope, next, current) {
13 | $scope.pagename = $routeParams.pagename;
14 | $scope.browser = $routeParams.browser;
15 | $scope.pageLoading = false;
16 | });
17 |
18 | $scope.$on('$routeChangeStart', function() {
19 | $scope.pageLoading = true;
20 | });
21 |
22 | $scope.$on('$routeChangeError', function(a, b, c, err) {
23 | $scope.pageError = true;
24 | $scope.pageLoading = false;
25 | });
26 |
27 | $scope.goHome = function() {
28 | $location.url('/page-select');
29 | window.document.location.reload();
30 | };
31 |
32 | if (window.location !== window.top.location) {
33 | $scope.noPjBrand = true;
34 | }
35 | }
36 | ])
37 | .filter('formatMetric', function() {
38 | return function(input) {
39 | input = input.replace(/_/g, " ").replace(/([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g, "$1$4 $2$3$5");
40 | return input.toLowerCase().replace(/([^a-z]|^)([a-z])(?=[a-z]{2})/g, function(_, g1, g2) {
41 | return g1 + g2.toUpperCase();
42 | });
43 | };
44 | })
45 | .filter('formatMetricValue', ['$filter',
46 | function($filter) {
47 | return function(value, unit) {
48 | var fraction = 0;
49 | if (unit === 'ms' || unit === 'fps') {
50 | fraction = 2;
51 | }
52 | if (value > 1000) {
53 | return $filter('number')(value / 1000, value > 100000 ? 0 : 2) + 'K';
54 | } else {
55 | return $filter('number')(value, fraction);
56 | }
57 | };
58 | }
59 | ]);
--------------------------------------------------------------------------------
/www/app/backend.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('Backend', ['Endpoints'])
3 | .factory('Data', ['Resource',
4 | function(resource) {
5 | return {
6 | pagelist: function() {
7 | return resource('/pagelist');
8 | },
9 | runList: function(opts) {
10 | return resource('/runList', {
11 | browser: opts.browser,
12 | pagename: opts.pagename
13 | });
14 | },
15 | runData: function(opts) {
16 | return resource('/runData', {
17 | browser: opts.browser,
18 | pagename: opts.pagename,
19 | time: opts.time
20 | });
21 | },
22 | getAllMetrics: function() {
23 | return resource('/all-metrics');
24 | },
25 | metricsData: function(opts) {
26 | return resource('/metrics-data', {
27 | browser: opts.browser,
28 | pagename: opts.pagename,
29 | metric: opts.metric,
30 | limit: opts.limit
31 | });
32 | }
33 | };
34 | }
35 | ]);
--------------------------------------------------------------------------------
/www/app/font.less:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'fontello';
3 | src: url('assets/fonts/fontello.eot?37370699');
4 | src: url('assets/fonts/fontello.eot?37370699#iefix') format('embedded-opentype'),
5 | url('assets/fonts/fontello.woff?37370699') format('woff'),
6 | url('assets/fonts/fontello.ttf?37370699') format('truetype'),
7 | url('assets/fonts/fontello.svg?37370699#fontello') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 |
13 | [class^="icon-"]:before, [class*=" icon-"]:before {
14 | font-family: "fontello";
15 | font-style: normal;
16 | font-weight: normal;
17 | speak: none;
18 |
19 | display: inline-block;
20 | text-decoration: inherit;
21 | width: 1em;
22 | margin-right: .2em;
23 | text-align: center;
24 |
25 | /* For safety - reset parent styles, that can break glyph codes*/
26 | font-variant: normal;
27 | text-transform: none;
28 |
29 | /* fix buttons height, for twitter bootstrap */
30 | line-height: 1em;
31 |
32 | /* Animation center compensation - margins should be symmetric */
33 | /* remove if not needed */
34 | margin-left: .2em;
35 |
36 | /* you can be more comfortable with increased icons size */
37 | /* font-size: 120%; */
38 |
39 | /* Uncomment for 3D effect */
40 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
41 | }
--------------------------------------------------------------------------------
/www/app/main-page/error.less:
--------------------------------------------------------------------------------
1 | .error{
2 | text-align: center;
3 | margin-top: 10%;
4 | }
--------------------------------------------------------------------------------
/www/app/main-page/navbar.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/www/app/main-page/navbar.less:
--------------------------------------------------------------------------------
1 | .navbar-brand {
2 | padding: 0 15px;
3 | .logo-icon {
4 | font-size: 2.5em;
5 | color: #aaa;
6 | vertical-align: middle;
7 | }
8 | .logo {
9 | vertical-align: middle;
10 | font-weight: bold;
11 | font-size: 1.3em;
12 | &:after {
13 | content: 'perfJankie';
14 | position: relative;
15 | top: -33px;
16 | left: 69px;
17 | display: block;
18 | height: 10px;
19 | overflow: hidden;
20 | background: #f8f8f8;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/www/app/main-page/no-pj-brand.less:
--------------------------------------------------------------------------------
1 | body.no-pj-brand {
2 | .navbar {
3 | display: none !important;
4 | }
5 | .sidebar {
6 | margin-top: 0px;
7 | }
8 | .content-container {
9 | padding-top: 0px;
10 | }
11 | .pj-brand {
12 | display: none !important;
13 | }
14 | }
--------------------------------------------------------------------------------
/www/app/main-page/sidebar.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/www/app/main-page/sidebar.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('sidebar', ['ngRoute'])
3 | .controller('SideBarCtrl', ['$routeParams', '$scope',
4 | function($routeParams, $scope) {
5 | var self = this;
6 | $scope.$on('$routeChangeSuccess', function(scope, next, current) {
7 | var browser = $routeParams.browser;
8 | self.categories = {
9 | 'Frame Rates': ['framesPerSec_raf', 'meanFrameTime_raf', 'droppedFrameCount'],
10 | };
11 | if (['chrome', 'safari', 'android'].indexOf(browser) !== -1) {
12 | self.categories['Paint'] = ['Paint', 'Layout', 'RecalculateStyles', 'CompositeLayers'];
13 | self.categories['Javascript'] = ['TimerInstall', 'TimerFire', 'EventDispatch', 'FunctionCall'];
14 | self.categories['Frame Rates'].unshift('frames_per_sec', 'mean_frame_time');
15 | }
16 | if (browser !== 'safari') {
17 | self.categories['Network'] = ['domReadyTime', 'loadTime', 'domainLookupTime', 'requestTime', 'loadEventTime'];
18 | }
19 | });
20 | }
21 | ]);
--------------------------------------------------------------------------------
/www/app/main-page/sidebar.less:
--------------------------------------------------------------------------------
1 | @media (min-width:992px) {
2 | .sidebar {
3 | position: fixed;
4 | }
5 | }
6 | .sidebar {
7 | user-select: none;
8 | background: #f8f8f8;
9 | border: SOLID 1px #e7e7e7;
10 | top: 0;
11 | bottom: 0;
12 | left: 0;
13 | margin-top: 50px;
14 | padding: 0;
15 | overflow-x: hidden;
16 | .page-select {
17 | padding: 10px;
18 | h4 {
19 | text-align: center;
20 | font-size: 2.5em;
21 | color: #ccc;
22 | font-weight: bold;
23 | }
24 | }
25 | .metadata {
26 | padding: 10px 15px;
27 | strong {
28 | white-space: nowrap;
29 | width: 100%;
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | text-transform: uppercase;
33 | vertical-align: middle;
34 | }
35 | .browser {
36 | color: #aaa;
37 | vertical-align: middle;
38 | font-size: 3em;
39 | float: right;
40 | margin: -0.6em -0.6em 0 0;
41 | }
42 | }
43 | ul.nav {
44 | border-top: 1px solid #e7e7e7;
45 | li {
46 | border-bottom: 1px solid #e7e7e7;
47 | &.active {
48 | background-color: #eee;
49 | }
50 | .sub-menu{
51 | a{
52 | margin-left: 20px;
53 | }
54 | }
55 | .icon-Paint:before {&:extend(.icon-brush:before);}
56 | .icon-Paint-sub:before{ &:extend(.icon-paintbucket:before);}
57 | .icon-Content:before {&:extend(.icon-code:before);}
58 | .icon-Content-sub:before {&:extend(.icon-file-code:before);}
59 | .icon-Javascript:before {&:extend(.icon-cog-alt:before);}
60 | .icon-Javascript-sub:before {&:extend(.icon-cog:before);}
61 | .icon-Frame:before {&:extend(.icon-movie:before);}
62 | .icon-Network:before, .icon-Network-sub:before {&:extend(.icon-signal:before);}
63 | }
64 | }
65 | .help {
66 | padding: 10px;
67 | }
68 | }
--------------------------------------------------------------------------------
/www/app/main.less:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | }
5 | body {
6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
7 | background: #fff;
8 | .page-loading {
9 | position: absolute;
10 | top: 50%;
11 | left: 50%;
12 | margin-top: -50px;
13 | margin-left: -50px;
14 | .spin-container {
15 | font-size: 6em;
16 | background: #fff;
17 | box-shadow: 0 0 10px 0 #333;
18 | border-radius: 20px;
19 | }
20 | }
21 | a {
22 | cursor: pointer;
23 | }
24 | .content-container {
25 | padding-top: 50px;
26 | min-height: 100%;
27 | .row {
28 | min-height: 100%;
29 | }
30 | .content {
31 | min-height: 100%;
32 | .page-details {
33 | margin-top: 14px;
34 | }
35 | }
36 | }
37 | .graph {
38 | display: block;
39 | &>div.jqplot-target{
40 | height: 100%;
41 | }
42 | .graph-error {
43 | em {
44 | font-size: 2em;
45 | display: block;
46 | margin-bottom: 0.5em;
47 | font-style: normal;
48 | }
49 | padding-top: 2em;
50 | display: block;
51 | text-align: center;
52 | font-size: 1.5em;
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/www/app/metric-details/metric-detail.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
Details:
17 | {{metric.metadata.summary}}
18 | Unit:
19 | {{metric.metadata.unit}}
20 | |
21 | Source:
22 | {{metric.metadata.source}}
23 | |
24 | Supported Browsers:
25 |
26 | {{browser}}
27 | ,
28 |
29 |
30 |
{{metric.metadata.details}}
31 |
32 |
50 |
51 |
52 |
53 | {{data}}
54 |
55 | Could not plot graph
56 |
57 | This could be either due to insufficient data or error in data.
58 |
59 |
60 |
--------------------------------------------------------------------------------
/www/app/metric-details/metric-detail.less:
--------------------------------------------------------------------------------
1 | .page.metric-detail {
2 | .metric-help {
3 | background: #ccc;
4 | border-radius: 50%;
5 | font-size: 50%;
6 | color: #fff;
7 | padding: 2px 6px;
8 | font-family: courier;
9 | vertical-align: middle;
10 | &:hover{
11 | text-decoration: none;
12 | }
13 | }
14 | .graph-modifiers {
15 | margin: -20px 0 20px 0;
16 | padding: 10px;
17 | height: 3em;
18 | input[type=radio] {
19 | margin-left: -15px;
20 | }
21 | }
22 | .explanation {
23 | margin: -10px 0 20px 0;
24 | border-bottom: SOLID #eee 1px;
25 | em{
26 | font-style: normal;
27 | color: #777;
28 | }
29 | }
30 | .jqplot-highlighter-tooltip{
31 | height: 20px;
32 | }
33 | }
--------------------------------------------------------------------------------
/www/app/metric-details/metricDetail.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('metricdetail', ['ngRoute', 'metricsGraphDetails', 'Backend'])
3 | .config(['$routeProvider',
4 | function($routeProvider) {
5 | $routeProvider.when('/detail', {
6 | templateUrl: 'app/metric-details/metric-detail.html',
7 | controller: 'MetricDetailCtrl',
8 | controllerAs: 'metric',
9 | resolve: {
10 | MetricsList: ['Data',
11 | function(data) {
12 | return data.getAllMetrics();
13 | }
14 | ],
15 | Data: ['Data', '$route',
16 | function(Data, $route) {
17 | $route.current.params.limit = $route.current.params.limit || 40;
18 | $route.current.params.stat = $route.current.params.stat || '';
19 |
20 | var params = $route.current.params;
21 | return Data.metricsData({
22 | browser: params.browser,
23 | pagename: params.pagename,
24 | metric: params.metric + params.stat,
25 | limit: params.limit === 'all' ? undefined : params.limit
26 | });
27 | }
28 | ]
29 | }
30 | });
31 | }
32 | ])
33 | .controller('MetricDetailCtrl', ['$routeParams', '$scope', '$location', 'Data', 'MetricsList',
34 | function($routeParams, $scope, $location, data, metricsList) {
35 | this.name = $routeParams.metric;
36 | this.data = data;
37 | for (var i = 0; i < metricsList.length; i++){
38 | if (metricsList[i].name === this.name){
39 | this.metadata = metricsList[i];
40 | break;
41 | }
42 | }
43 | $scope.modifier = {
44 | limit: $routeParams.limit,
45 | stat: $routeParams.stat
46 | };
47 |
48 | var pos = $('.graph').position();
49 |
50 | // Sets height of graph with a minimum of 500px
51 | this.height = Math.max(window.innerHeight - pos.top - 100, 500);
52 |
53 | $scope.$watchCollection('modifier', function(val, old, scope) {
54 | if ($routeParams.stat !== val.stat) {
55 | $location.search('stat', val.stat);
56 | } else if ($routeParams.limit !== val.limit) {
57 | $location.search('limit', val.limit);
58 | }
59 | });
60 | }
61 | ]);
--------------------------------------------------------------------------------
/www/app/metric-details/metricDetailsGraph.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('metricsGraphDetails', [])
3 | .directive('pjMetricsDetailsGraph', function() {
4 | function prepareData(val) {
5 | var result = {
6 | series: [],
7 | max: [],
8 | min: [],
9 | xaxis: {}
10 | };
11 | for (var i = val.length - 1; i >= 0; i--) {
12 | var p = val[i];
13 | result.series.push([p.key, p.value.sum / p.value.count, p.value.min, p.value.max]);
14 | result.min.push([p.key, p.value.min]);
15 | result.max.push([p.key, p.value.max]);
16 | result.xaxis[p.key] = p.label;
17 | }
18 | return result;
19 | }
20 |
21 | function drawGraph(el, data, unit) {
22 | $.jqplot(el, [data.series, data.min, data.max], {
23 | fillBetween: {
24 | series1: 1,
25 | series2: 2,
26 | color: "rgba(67, 142, 185, 0.2)",
27 | baseSeries: 0,
28 | fill: true
29 | },
30 | series: [{
31 | show: true,
32 | shadow: false,
33 | breakOnNull: true,
34 | rendererOptions: {
35 | smooth: false,
36 | },
37 | trendline: {
38 | show: true,
39 | shadow: false,
40 | color: '#666',
41 | lineWidth: 2,
42 | linePattern: 'dashed',
43 | label: 'trend'
44 | }
45 | }],
46 | seriesDefaults: {
47 | show: false,
48 | rendererOptions: {
49 | smooth: true
50 | }
51 | },
52 | axes: {
53 | xaxis: {
54 | renderer: $.jqplot.CategoryAxisRenderer,
55 | label: 'Runs',
56 | labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
57 | tickRenderer: $.jqplot.CanvasAxisTickRenderer,
58 | rendererOptions: {
59 | sortMergedLabels: false
60 | },
61 | tickOptions: {
62 | mark: 'cross',
63 | showMark: true,
64 | showGridline: true,
65 | markSize: 5,
66 | angle: -90,
67 | show: true,
68 | showLabel: true,
69 | formatter: function(formatString, value) {
70 | return data.xaxis[value];
71 | }
72 | },
73 | showTicks: true, // wether or not to show the tick labels,
74 | showTickMarks: true,
75 | },
76 | yaxis: {
77 | tickOptions: {},
78 | rendererOptions: {
79 | forceTickAt0: false
80 | },
81 | label: unit || 'Y AXIS',
82 | labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
83 | tickRenderer: $.jqplot.CanvasAxisTickRenderer
84 | }
85 | },
86 | grid: {
87 | shadow: false,
88 | borderWidth: 0
89 | },
90 | highlighter: {
91 | show: true,
92 | showLabel: true,
93 | tooltipAxes: 'y',
94 | sizeAdjust: 7.5,
95 | tooltipLocation: 'ne'
96 | }
97 | });
98 | }
99 |
100 | var id = 'metricDetails' + Math.floor(Math.random() * 10000);
101 |
102 | function link(scope, element, attrs) {
103 | scope.$watch('data', function(val) {
104 | if (val) {
105 | try {
106 | drawGraph(id, prepareData(val), scope.unit);
107 | } catch (e) {
108 | scope.error = e;
109 | }
110 | }
111 | });
112 | }
113 |
114 | return {
115 | link: link,
116 | restrict: 'E',
117 | transclude: true,
118 | scope: {
119 | data: "=",
120 | unit: "="
121 | },
122 | template: '
'
123 | };
124 | });
--------------------------------------------------------------------------------
/www/app/page-select/page-select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Select a test case to view metrics
4 |
5 |
6 |
7 |
8 | {{suite}}
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{page}}
16 | # of tests
17 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/www/app/page-select/page-select.less:
--------------------------------------------------------------------------------
1 | .page.page-select{
2 | .suites{
3 | .panel{
4 | .list-group{
5 | .list-group-item{
6 | padding: 0;
7 | a{
8 | display: block;
9 | padding: 10px 15px;
10 | &:hover{
11 | text-decoration: none;
12 | background: #ccc;
13 | }
14 | }
15 | }
16 | }
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/www/app/page-select/pageSelect.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('pageSelect', ['ngRoute', 'Backend'])
3 | .config(['$routeProvider',
4 | function($routeProvider) {
5 | $routeProvider.when('/page-select', {
6 | templateUrl: 'app/page-select/page-select.html',
7 | controller: 'PageSelectCtrl',
8 | controllerAs: 'pageselect',
9 | resolve: {
10 | PageList: ['Data',
11 | function(data) {
12 | return data.pagelist();
13 | }
14 | ]
15 | }
16 | });
17 | }
18 | ])
19 | .controller('PageSelectCtrl', ['PageList',
20 | function(PageList) {
21 | this.pagelist = PageList;
22 | }
23 | ]);
--------------------------------------------------------------------------------
/www/app/summary/networkTimingGraph.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('networkTiming', [])
3 | .directive('pjNetworkTimingGraph', function() {
4 | var ticks = ['onLoad', 'Processing', 'Response', 'Request', 'TCP', 'DNS', 'AppCache', 'Unload', 'Start/Redirect'];
5 | var events = [
6 | ['loadEventStart', 'loadEventEnd'],
7 | ['domLoading', 'domComplete'],
8 | ['responseStart', 'responseEnd'],
9 | ['requestStart', 'responseStart'],
10 | ['connectStart', 'connectEnd'],
11 | ['domainLookupStart', 'domainLookupEnd'],
12 | ['fetchStart', 'domainLookupStart'],
13 | ['unloadStart', 'unloadEnd'],
14 | ['redirectStart', 'redirectStop'],
15 | ];
16 |
17 | function prepareData(val) {
18 | var series = [
19 | [],
20 | []
21 | ];
22 | var initial = val['navigationStart'].sum / val['navigationStart'].count;
23 | var prev = initial;
24 | for (var i = 0; i < events.length; i++) {
25 | var start = val[events[i][0]];
26 | var end = val[events[i][1]];
27 | if (start && start.sum > 0) {
28 | start = start.sum / start.count;
29 | } else {
30 | start = (i > 0 ? series[0][i - 1] + initial : initial);
31 | }
32 | if (end && end.sum > 0) {
33 | end = end.sum / end.count;
34 | } else {
35 | end = start;
36 | }
37 | series[0].push(start - initial);
38 | series[1].push(end - initial);
39 | }
40 | return series;
41 | }
42 |
43 | function drawGraph(el, series) {
44 | $.jqplot(el, series, {
45 | stackSeries: true,
46 | seriesDefaults: {
47 | renderer: $.jqplot.BarRenderer,
48 | rendererOptions: {
49 | barDirection: 'horizontal',
50 | barPadding: 0,
51 | barMargin: 0,
52 | shadowDepth: 0,
53 | stacked: true
54 | }
55 | },
56 | series: [{
57 | color: 'rgba(0,0,0,0)'
58 | }],
59 | axes: {
60 | yaxis: {
61 | renderer: $.jqplot.CategoryAxisRenderer,
62 | ticks: ticks,
63 | tickRenderer: $.jqplot.CanvasAxisTickRenderer
64 | },
65 | },
66 | grid: {
67 | shadow: false,
68 | borderWidth: 0
69 | },
70 | });
71 | }
72 |
73 | var id = 'networkTimings' + Math.floor(Math.random() * 10000);
74 |
75 | function link(scope, element, attrs) {
76 | scope.$watch('data', function(val) {
77 | if (val && !angular.equals(val, {})) {
78 | drawGraph(id, prepareData(val));
79 | }
80 | });
81 | }
82 |
83 | return {
84 | restrict: 'E',
85 | transclude: true,
86 | scope: {
87 | data: "="
88 | },
89 | link: link,
90 | template: '
'
91 | };
92 | });
--------------------------------------------------------------------------------
/www/app/summary/paintCycleGraph.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('paintCycleGraph', [])
3 | .directive('pjPaintCycleGraph', function() {
4 | var id = 'paintCycle' + Math.floor(Math.random() * 10000);
5 |
6 | function prepareData(data) {
7 | var paints = [];
8 | angular.forEach(['Layout', 'CompositeLayers', 'Paint', 'RecalculateStyles'], function(key) {
9 | paints.push([key, data[key].sum]);
10 | });
11 | return paints;
12 | }
13 |
14 | function drawGraph(el, data) {
15 | $.jqplot(el, [data], {
16 | seriesColors: ['#7AA9E5', '#EFC453', '#9A7EE6', '#71B363'],
17 | seriesDefaults: {
18 | renderer: $.jqplot.PieRenderer,
19 | rendererOptions: {
20 | showDataLabels: true,
21 | dataLabels: ['value'],
22 | dataLabelFormatString: '%d ms',
23 | highlightMouseOver: true
24 | }
25 | },
26 | grid: {
27 | shadow: false,
28 | borderWidth: 0
29 | },
30 | legend: {
31 | show: true,
32 | location: 'e'
33 | }
34 | });
35 | }
36 |
37 | function link(scope, element, attrs) {
38 | scope.$watch('data', function(val) {
39 | if (val && !angular.equals(val, {})) {
40 | try {
41 | drawGraph(id, prepareData(val));
42 | } catch (e) {
43 | scope.error = e;
44 | }
45 | }
46 | });
47 | }
48 |
49 | return {
50 | link: link,
51 | restrict: 'E',
52 | transclude: true,
53 | scope: {
54 | data: "="
55 | },
56 | template: '
'
57 | };
58 | });
--------------------------------------------------------------------------------
/www/app/summary/summary.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 | Frame Rate Trend
16 | (higher is better)
17 |
18 |
19 |
20 |
21 |
22 | Could not plot graph due to insufficient data
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Test runs
33 |
34 |
35 | Deploy/Run Tag
36 | # times run
37 |
38 |
44 |
45 |
46 |
47 |
71 |
72 |
73 |
74 |
75 |
76 | Paint Cycle
77 | for selected run
78 |
79 |
80 |
81 |
82 |
83 | Could not plot graph. This could be due to insufficient data
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Network
93 | for selected run
94 |
95 |
96 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/www/app/summary/summary.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('summary', ['ngRoute', 'paintCycleGraph', 'networkTiming', 'Backend'])
3 | .config(['$routeProvider',
4 | function($routeProvider) {
5 | $routeProvider.when('/summary', {
6 | templateUrl: 'app/summary/summary.html',
7 | controller: 'SummaryCtrl',
8 | controllerAs: 'summary',
9 | resolve: {
10 | runList: ['Data', '$route',
11 | function(data, $route) {
12 | var res = {};
13 | var params = $route.current.params;
14 | return data.runList({
15 | browser: params.browser,
16 | pagename: params.pagename
17 | });
18 | }
19 | ],
20 | }
21 | });
22 | }
23 | ])
24 | .controller('SummaryCtrl', ['$routeParams', '$location', 'runList', 'Data',
25 | function($routeParams, $location, runList, Data) {
26 | this.time = $routeParams.time || runList[0].time;
27 | this.runList = runList;
28 | var self = this;
29 |
30 | this.tiles = [];
31 | this.currentRunData = {};
32 | var metric = 'framesPerSec_raf';
33 |
34 | Data.runData({
35 | browser: $routeParams.browser,
36 | pagename: $routeParams.pagename,
37 | time: this.time
38 | }).then(function(data) {
39 | self.currentRunData = data;
40 | Data.metricsData({
41 | browser: $routeParams.browser,
42 | pagename: $routeParams.pagename,
43 | metric: data['frames_per_sec'] ? 'frames_per_sec' : 'framesPerSec_raf',
44 | limit: 20
45 | }).then(function(data) {
46 | self.frameRateData = data;
47 | });
48 | });
49 | }
50 | ]);
--------------------------------------------------------------------------------
/www/app/summary/summary.less:
--------------------------------------------------------------------------------
1 | .summary{
2 | .test-runs{
3 | .list-group-item{
4 | border-radius: 0 !important;
5 | border-left: none;
6 | border-right: none;
7 | &.heading{
8 | border-bottom: SOLID 1px gray;
9 | }
10 | }
11 | .content{
12 | height: 490px;
13 | overflow: scroll;
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/www/app/summary/tiles.js:
--------------------------------------------------------------------------------
1 | angular
2 | .module('summaryTiles', ['Backend'])
3 | .directive('pjSummaryTiles', ['Data', '$q',
4 | function(data, $q) {
5 | var _metricsList;
6 |
7 | var metricsList = function() {
8 | if (_metricsList) {
9 | return $q.when(_metricsList);
10 | } else {
11 | return data.getAllMetrics().then(function(result) {
12 | return _metricsList = result;
13 | });
14 | }
15 | };
16 |
17 | var prepareData = function(val) {
18 | var tiles = [];
19 | return metricsList().then(function(metricsList) {
20 | function getMetricUnit(metric) {
21 | for (var i = 0; i < metricsList.length; i++) {
22 | if (metric === metricsList[i].name) {
23 | return metricsList[i].unit;
24 | }
25 | return '';
26 | }
27 | }
28 |
29 | angular.forEach(['frames_per_sec', 'framesPerSec_raf', 'firstPaint', 'ExpensivePaints', 'NodePerLayout_avg', 'ExpensiveEventHandlers', ], function(metric) {
30 | if (typeof val[metric] === 'object') {
31 | tiles.push({
32 | metric: metric,
33 | unit: getMetricUnit(metric),
34 | value: val[metric].sum / val[metric].count,
35 | link: metric
36 | });
37 | }
38 | });
39 | return tiles;
40 | });
41 | };
42 |
43 | return {
44 | restrict: 'E',
45 | transclude: true,
46 | scope: {
47 | data: "=",
48 | pagename: "=",
49 | browser: "="
50 | },
51 | link: function(scope, element, attrs) {
52 | scope.$watch('data', function(val) {
53 | if (!val) {
54 | return;
55 | }
56 | prepareData(val).then(function(res) {
57 | scope.tiles = res.slice(0, 4);
58 | });
59 | });
60 | },
61 | templateUrl: 'app/summary/tiles.tpl.html'
62 | };
63 | }
64 | ]);
--------------------------------------------------------------------------------
/www/app/summary/tiles.less:
--------------------------------------------------------------------------------
1 | .tiles {
2 | .panel {
3 | &.panel-0 {
4 | background: #428bca;
5 | border-color: #428bca;
6 | a {
7 | color: #2a6496;
8 | }
9 | }
10 | &.panel-1 {
11 | background: #5cb85c;
12 | border-color: #5cb85c;
13 | a {
14 | color: #5cb85c;
15 | }
16 | }
17 | &.panel-2 {
18 | background: #f0ad4e;
19 | border-color: #f0ad4e;
20 | a {
21 | color: #f0ad4e;
22 | }
23 | }
24 | &.panel-3 {
25 | background: #d9534f;
26 | border-color: #d9534f;
27 | a {
28 | color: #d9534f;
29 | }
30 | }
31 | .panel-heading {
32 | color: white;
33 | .huge {
34 | font-size: 3em;
35 | }
36 | .unit {
37 | margin-top: -3px;
38 | }
39 | .icon {
40 | margin-left: -10px;
41 | font-size: 4em;
42 | &.icon-frames_per_sec:before {
43 | content: '\e815';
44 | }
45 | &.icon-ExpensivePaints:before{
46 | content: '\e804';
47 | }
48 | &.icon-ExpensiveEventHandlers:before{
49 | content: '\e80b';
50 | }
51 | &.icon-NodePerLayout_avg:before{
52 | content: '\e808';
53 | }
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/www/app/summary/tiles.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{tile.value | formatMetricValue: tile.unit}}
10 |
{{tile.unit}}
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/www/assets/css/animation.css:
--------------------------------------------------------------------------------
1 | /*
2 | Animation example, for spinners
3 | */
4 | .animate-spin {
5 | -moz-animation: spin 2s infinite linear;
6 | -o-animation: spin 2s infinite linear;
7 | -webkit-animation: spin 2s infinite linear;
8 | animation: spin 2s infinite linear;
9 | display: inline-block;
10 | }
11 | @-moz-keyframes spin {
12 | 0% {
13 | -moz-transform: rotate(0deg);
14 | -o-transform: rotate(0deg);
15 | -webkit-transform: rotate(0deg);
16 | transform: rotate(0deg);
17 | }
18 |
19 | 100% {
20 | -moz-transform: rotate(359deg);
21 | -o-transform: rotate(359deg);
22 | -webkit-transform: rotate(359deg);
23 | transform: rotate(359deg);
24 | }
25 | }
26 | @-webkit-keyframes spin {
27 | 0% {
28 | -moz-transform: rotate(0deg);
29 | -o-transform: rotate(0deg);
30 | -webkit-transform: rotate(0deg);
31 | transform: rotate(0deg);
32 | }
33 |
34 | 100% {
35 | -moz-transform: rotate(359deg);
36 | -o-transform: rotate(359deg);
37 | -webkit-transform: rotate(359deg);
38 | transform: rotate(359deg);
39 | }
40 | }
41 | @-o-keyframes spin {
42 | 0% {
43 | -moz-transform: rotate(0deg);
44 | -o-transform: rotate(0deg);
45 | -webkit-transform: rotate(0deg);
46 | transform: rotate(0deg);
47 | }
48 |
49 | 100% {
50 | -moz-transform: rotate(359deg);
51 | -o-transform: rotate(359deg);
52 | -webkit-transform: rotate(359deg);
53 | transform: rotate(359deg);
54 | }
55 | }
56 | @-ms-keyframes spin {
57 | 0% {
58 | -moz-transform: rotate(0deg);
59 | -o-transform: rotate(0deg);
60 | -webkit-transform: rotate(0deg);
61 | transform: rotate(0deg);
62 | }
63 |
64 | 100% {
65 | -moz-transform: rotate(359deg);
66 | -o-transform: rotate(359deg);
67 | -webkit-transform: rotate(359deg);
68 | transform: rotate(359deg);
69 | }
70 | }
71 | @keyframes spin {
72 | 0% {
73 | -moz-transform: rotate(0deg);
74 | -o-transform: rotate(0deg);
75 | -webkit-transform: rotate(0deg);
76 | transform: rotate(0deg);
77 | }
78 |
79 | 100% {
80 | -moz-transform: rotate(359deg);
81 | -o-transform: rotate(359deg);
82 | -webkit-transform: rotate(359deg);
83 | transform: rotate(359deg);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/www/assets/css/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "css_prefix_text": "icon-",
4 | "css_use_suffix": false,
5 | "hinting": true,
6 | "units_per_em": 1000,
7 | "ascent": 850,
8 | "glyphs": [
9 | {
10 | "uid": "5156114528976f4ffab0f04526d12e89",
11 | "css": "safari",
12 | "code": 59394,
13 | "src": "custom_icons",
14 | "selected": true,
15 | "svg": {
16 | "path": "M347.2 626.7C335.7 610.9 327.5 592.3 323.9 571.7 311.1 499 359.6 429.8 432.3 416.9 452.8 413.3 473 414.6 491.7 419.9L405.1 512.8ZM507.8 687.9C531.9 678.9 552.6 664 568.6 645.2L651.2 682.9 585.6 620.7C597.5 599.4 604.2 575.1 604.4 549.8L755.5 496.1 594.3 494.6C588.3 479.2 579.9 465.1 569.6 452.8L675.3 222.9 503.3 407.5C488.3 402.4 472.3 399.6 455.8 399.6L456.3 399.5 402.4 247.8 401 410C378.2 418.9 358.5 433.1 343.1 450.9L262.7 414.3 326.3 474.6C313.7 496.4 306.7 521.6 306.6 547.9L306 545.8 154.3 599.7 316.2 601.2C321.9 616.1 329.8 629.8 339.5 641.8L229.1 858.9 401.9 687.4C417.9 693.6 435.1 697.1 452.9 697.4L506.4 848ZM531.8 992.4C285.8 1035.8 51.2 871.5 7.9 625.5-32 399.6 103.3 183.3 316.1 115.9 303.1 107.1 294 94.5 291.4 79.4 285.3 45.2 314.9 11.3 357.5 3.8 400.1-3.7 439.5 18 445.5 52.2 448.1 67.3 443.9 82.3 434.7 95 657.8 85.5 858.9 242.5 898.7 468.4 942.1 714.4 777.8 949 531.8 992.4ZM478.7 680.1C456.2 684 434.1 682.1 413.9 675.5L511.8 578.4 562.4 468.4C574.6 484.6 583.3 503.9 587 525.3 599.9 598 551.3 667.3 478.7 680.1ZM462.8 589.9C439.9 593.9 418.2 578.6 414.1 555.8 410.1 533 425.4 511.2 448.2 507.2 471 503.1 492.8 518.4 496.8 541.2 500.9 564 485.6 585.8 462.8 589.9ZM393.5 90C404.1 82.7 410.2 71.7 408.2 60.7 405.3 44.1 385.3 33.8 363.5 37.6 341.7 41.5 326.4 58.1 329.3 74.7 331.2 85.7 340.7 93.9 353.2 97.1 353.8 88.4 360.9 80.7 370.8 79 380.6 77.3 389.9 82 393.5 90ZM393.5 90",
17 | "width": 908.3969465648854
18 | },
19 | "search": [
20 | "glyph"
21 | ]
22 | },
23 | {
24 | "uid": "d35c5d63e9d9056d3ba81c78c5a7fe58",
25 | "css": "paintbucket",
26 | "code": 59406,
27 | "src": "custom_icons",
28 | "selected": true,
29 | "svg": {
30 | "path": "M759.8 478.5C769.5 488.3 769.5 503.9 759.8 513.7L363.3 910.2V910.2C353.5 919.9 337.9 919.9 328.1 910.2L7.8 589.8C-2 580.1-2 562.5 7.8 552.7V552.7L279.3 281.3 168 168C158.2 158.2 152.3 146.5 152.3 132.8 152.3 105.5 175.8 82 203.1 82 216.8 82 228.5 87.9 238.3 97.7L349.6 210.9 402.3 158.2C412.1 148.4 429.7 148.4 439.5 158.2V158.2 158.2L759.8 478.5V478.5ZM562.5 570.3L636.7 496.1 421.9 281.3 130.9 570.3H562.5ZM839.8 738.3C851.6 753.9 857.4 771.5 857.4 791 857.4 841.8 816.4 884.8 765.6 884.8S671.9 841.8 671.9 791C671.9 769.5 679.7 752 691.4 736.3L750 634.8C750 632.8 752 634.8 752 632.8V630.9 630.9C755.9 627 759.8 625 765.6 625S775.4 628.9 779.3 632.8V632.8 632.8 634.8Z",
31 | "width": 857.421875
32 | },
33 | "search": [
34 | "glyph"
35 | ]
36 | },
37 | {
38 | "uid": "5d2d07f112b8de19f2c0dbfec3e42c05",
39 | "css": "spin5",
40 | "code": 9676,
41 | "src": "fontelico"
42 | },
43 | {
44 | "uid": "62c089cb34e74b3a1200bc7f5314eb4e",
45 | "css": "firefox",
46 | "code": 59392,
47 | "src": "fontelico"
48 | },
49 | {
50 | "uid": "9c2b737b16ae2c8d66b7bfd29ba5ecd8",
51 | "css": "chrome",
52 | "code": 59393,
53 | "src": "fontelico"
54 | },
55 | {
56 | "uid": "2a46f1d1c9bd036e17a74e46613c1636",
57 | "css": "ie",
58 | "code": 59405,
59 | "src": "fontelico"
60 | },
61 | {
62 | "uid": "7034e4d22866af82bef811f52fb1ba46",
63 | "css": "code",
64 | "code": 59408,
65 | "src": "fontawesome"
66 | },
67 | {
68 | "uid": "c76b7947c957c9b78b11741173c8349b",
69 | "css": "attention",
70 | "code": 59409,
71 | "src": "fontawesome"
72 | },
73 | {
74 | "uid": "26613a2e6bc41593c54bead46f8c8ee3",
75 | "css": "file-code",
76 | "code": 59398,
77 | "src": "fontawesome"
78 | },
79 | {
80 | "uid": "e99461abfef3923546da8d745372c995",
81 | "css": "cog",
82 | "code": 59407,
83 | "src": "fontawesome"
84 | },
85 | {
86 | "uid": "98687378abd1faf8f6af97c254eb6cd6",
87 | "css": "cog-alt",
88 | "code": 59403,
89 | "src": "fontawesome"
90 | },
91 | {
92 | "uid": "531bc468eecbb8867d822f1c11f1e039",
93 | "css": "calendar",
94 | "code": 59412,
95 | "src": "fontawesome"
96 | },
97 | {
98 | "uid": "4109c474ff99cad28fd5a2c38af2ec6f",
99 | "css": "filter",
100 | "code": 59404,
101 | "src": "fontawesome"
102 | },
103 | {
104 | "uid": "347c38a8b96a509270fdcabc951e7571",
105 | "css": "database",
106 | "code": 59401,
107 | "src": "fontawesome"
108 | },
109 | {
110 | "uid": "5e0a374728ffa8d0ae1f331a8f648231",
111 | "css": "github",
112 | "code": 59399,
113 | "src": "fontawesome"
114 | },
115 | {
116 | "uid": "1d2a6c3d9236b88b0f185c7c4530fa52",
117 | "css": "flag",
118 | "code": 59410,
119 | "src": "entypo"
120 | },
121 | {
122 | "uid": "84a7262985600b683bbab0da9298776d",
123 | "css": "signal",
124 | "code": 59397,
125 | "src": "entypo"
126 | },
127 | {
128 | "uid": "8a1d446e5555e76f82ddb1c8b526f579",
129 | "css": "flow-tree",
130 | "code": 59400,
131 | "src": "entypo"
132 | },
133 | {
134 | "uid": "3a6f0140c3a390bdb203f56d1bfdefcb",
135 | "css": "gauge",
136 | "code": 59402,
137 | "src": "entypo"
138 | },
139 | {
140 | "uid": "ef8560a06ed46a192092bf1f08c142a6",
141 | "css": "sort-alphabet-outline",
142 | "code": 59395,
143 | "src": "typicons"
144 | },
145 | {
146 | "uid": "b3a9e2dab4d19ea3b2f628242c926bfe",
147 | "css": "brush",
148 | "code": 59396,
149 | "src": "iconic"
150 | },
151 | {
152 | "uid": "d2c499942f8a7c037d5a94f123eeb478",
153 | "css": "layers",
154 | "code": 59411,
155 | "src": "iconic"
156 | },
157 | {
158 | "uid": "eea613bc40c77b7eab137b29dba0c62f",
159 | "css": "movie",
160 | "code": 59413,
161 | "src": "mfglabs"
162 | },
163 | {
164 | "uid": "aaf371ab44841e9aaffebd179d324ce4",
165 | "css": "android",
166 | "code": 59414,
167 | "src": "zocial"
168 | }
169 | ]
170 | }
--------------------------------------------------------------------------------
/www/assets/css/fontello-codes.css:
--------------------------------------------------------------------------------
1 |
2 | .icon-spin5:before { content: '\25cc'; } /* '◌' */
3 | .icon-firefox:before { content: '\e800'; } /* '' */
4 | .icon-chrome:before { content: '\e801'; } /* '' */
5 | .icon-safari:before { content: '\e802'; } /* '' */
6 | .icon-sort-alphabet-outline:before { content: '\e803'; } /* '' */
7 | .icon-brush:before { content: '\e804'; } /* '' */
8 | .icon-signal:before { content: '\e805'; } /* '' */
9 | .icon-file-code:before { content: '\e806'; } /* '' */
10 | .icon-github:before { content: '\e807'; } /* '' */
11 | .icon-flow-tree:before { content: '\e808'; } /* '' */
12 | .icon-database:before { content: '\e809'; } /* '' */
13 | .icon-gauge:before { content: '\e80a'; } /* '' */
14 | .icon-cog-alt:before { content: '\e80b'; } /* '' */
15 | .icon-filter:before { content: '\e80c'; } /* '' */
16 | .icon-ie:before { content: '\e80d'; } /* '' */
17 | .icon-paintbucket:before { content: '\e80e'; } /* '' */
18 | .icon-cog:before { content: '\e80f'; } /* '' */
19 | .icon-code:before { content: '\e810'; } /* '' */
20 | .icon-attention:before { content: '\e811'; } /* '' */
21 | .icon-flag:before { content: '\e812'; } /* '' */
22 | .icon-layers:before { content: '\e813'; } /* '' */
23 | .icon-calendar:before { content: '\e814'; } /* '' */
24 | .icon-movie:before { content: '\e815'; } /* '' */
25 | .icon-android:before { content: '\e816'; } /* '' */
--------------------------------------------------------------------------------
/www/assets/fonts/fontello-codes.css:
--------------------------------------------------------------------------------
1 |
2 | .icon-firefox:before { content: '\e800'; } /* '' */
3 | .icon-chrome:before { content: '\e801'; } /* '' */
4 | .icon-safari:before { content: '\e802'; } /* '' */
5 | .icon-sort-alphabet-outline:before { content: '\e803'; } /* '' */
6 | .icon-brush:before { content: '\e804'; } /* '' */
7 | .icon-signal:before { content: '\e805'; } /* '' */
8 | .icon-file-code:before { content: '\e806'; } /* '' */
9 | .icon-github:before { content: '\e807'; } /* '' */
10 | .icon-spin5:before { content: '\e808'; } /* '' */
11 | .icon-database:before { content: '\e809'; } /* '' */
12 | .icon-gauge:before { content: '\e80a'; } /* '' */
13 | .icon-cog-alt:before { content: '\e80b'; } /* '' */
14 | .icon-filter:before { content: '\e80c'; } /* '' */
15 | .icon-ie:before { content: '\e80d'; } /* '' */
16 | .icon-paintbucket:before { content: '\e80e'; } /* '' */
17 | .icon-cog:before { content: '\e80f'; } /* '' */
18 | .icon-code:before { content: '\e810'; } /* '' */
--------------------------------------------------------------------------------
/www/assets/fonts/fontello.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axemclion/perfjankie/23c65ac3c8b4e3f99681446f7e0a46a53d30b0d7/www/assets/fonts/fontello.eot
--------------------------------------------------------------------------------
/www/assets/fonts/fontello.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Copyright (C) 2015 by original authors @ fontello.com
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/www/assets/fonts/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axemclion/perfjankie/23c65ac3c8b4e3f99681446f7e0a46a53d30b0d7/www/assets/fonts/fontello.ttf
--------------------------------------------------------------------------------
/www/assets/fonts/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axemclion/perfjankie/23c65ac3c8b4e3f99681446f7e0a46a53d30b0d7/www/assets/fonts/fontello.woff
--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PerfJankie - Rendering Performance Analysis
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Error loading page
36 |
37 |
38 | An error occured when trying to load this page.
39 |
40 | Please
41 | refresh
42 | this page,
43 | or go back to the
44 | home page
45 | .
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/www/server/endpoints.js:
--------------------------------------------------------------------------------
1 | if (typeof window.DB_BASE !== 'string') {
2 | window.DB_BASE = '..';
3 | }
4 |
5 | angular
6 | .module('Endpoints', [])
7 | .factory('Resource', ['$http', '$q',
8 | function($http, $q) {
9 | function rowsToObj(row, keys, val) {
10 | var res = {};
11 | res[val] = row.value;
12 | angular.forEach(keys, function(key, i) {
13 | res[key] = row.key[i];
14 | });
15 | return res;
16 | }
17 |
18 | var server = {
19 | '/pagelist': function() {
20 | return $http.get(window.DB_BASE + '/pagelist/_view/pages?group=true').then(function(resp) {
21 | var result = {};
22 | angular.forEach(resp.data.rows, function(row) {
23 | var res = rowsToObj(row, ['suite', 'pagename', 'browser'], 'runCount');
24 | result[res.suite] = result[res.suite] || {};
25 | result[res.suite][res.pagename] = result[res.suite][res.pagename] || [];
26 | result[res.suite][res.pagename].push({
27 | browser: res.browser,
28 | runCount: res.runCount
29 | });
30 | });
31 | return result;
32 | });
33 | },
34 | '/all-metrics': function() {
35 | return $q.when(window.METRICS_LIST);
36 | },
37 | '/runList': function(opts) {
38 | return $http.get(window.DB_BASE + '/runs/_view/list', {
39 | params: {
40 | endkey: JSON.stringify([opts.browser, opts.pagename, null]),
41 | startkey: JSON.stringify([opts.browser, opts.pagename, {}]),
42 | group: true,
43 | descending: true
44 | }
45 | }).then(function(resp) {
46 | var res = [];
47 | angular.forEach(resp.data.rows, function(row) {
48 | res.push(rowsToObj(row, ['browser', 'pagename', 'time', 'run'], 'runCount'));
49 | });
50 | return res;
51 | });
52 | },
53 | '/runData': function(opts) {
54 | return $http.get(window.DB_BASE + '/runs/_view/data', {
55 | params: {
56 | startkey: JSON.stringify([opts.browser, opts.pagename, opts.time, null]),
57 | endkey: JSON.stringify([opts.browser, opts.pagename, opts.time, {}]),
58 | group: true
59 | }
60 | }).then(function(resp) {
61 | var res = {};
62 | angular.forEach(resp.data.rows, function(row) {
63 | var obj = rowsToObj(row, ['browser', 'pagename', 'time', 'run', 'metric'], 'value');
64 | res[obj.metric] = obj.value;
65 | });
66 | return res;
67 | });
68 | },
69 | '/metrics-data': function(opts) {
70 | var config = {
71 | params: {
72 | endkey: JSON.stringify([opts.browser, opts.pagename, opts.metric, null]),
73 | startkey: JSON.stringify([opts.browser, opts.pagename, opts.metric, {}]),
74 | group: true,
75 | descending: true
76 | }
77 | };
78 | var limit = parseInt(opts.limit, 10);
79 | if (!isNaN(limit)) {
80 | config.params.limit = limit;
81 | }
82 | return $http.get(window.DB_BASE + '/metrics_data/_view/stats', config).then(function(resp) {
83 | var res = [];
84 | angular.forEach(resp.data.rows, function(obj, index) {
85 | obj.label = obj.key[4];
86 | obj.key = obj.key[3];
87 | res.push(obj);
88 | });
89 | return res;
90 | });
91 | }
92 | };
93 |
94 | var fetch = function(url, params) {
95 | return server[url](params);
96 | };
97 |
98 | return fetch;
99 | }
100 | ]);
--------------------------------------------------------------------------------