Repository: axemclion/perfjankie
Branch: master
Commit: 23c65ac3c8b4
Files: 71
Total size: 106.6 KB
Directory structure:
gitextract_r04kxl2y/
├── .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
├── index.html
└── server/
└── endpoints.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules/
bower_components/
bin/
bin-site/
*.log
.DS_Store
.idea/*
.tmp/*
dist/*
_replicator/*
_users/*
pouch__all_dbs__/*
version/*
log.txt
================================================
FILE: .jshintrc
================================================
{
"curly": true,
"eqeqeq": true,
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"sub": true,
"undef": true,
"boss": true,
"eqnull": true,
"node": true,
"shadow": true,
"expr": true,
"globals": {
"angular": false,
"$": false,
"window": false,
"ENDPOINTS": false,
"describe": false,
"it": false,
"beforeEach": false,
"before": false,
"xit": false,
"xdescribe": false
}
}
================================================
FILE: .npmignore
================================================
node_modules/
bower_components/
Gruntfile.js
test/
www/
bower.json
.jshintrc
.gitignore
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
node_js:
- "0.12"
- "4.0"
- "4.3"
- "4"
- "5.0"
- "5"
- "6"
- "stable"
env:
- NPM_VERSION=2
- NPM_VERSION=3
services: couchdb
before_install:
- npm install -g npm@$NPM_VERSION
- npm install -g grunt-cli
before_script:
- npm install
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3 # give xvfb some time to start
script: npm test
================================================
FILE: Gruntfile.js
================================================
module.exports = function(grunt) {
var couchdb = require('./test/util').config({
log: 1
}).couch;
var serveStatic = require('serve-static');
var path = require('path');
var jqplot = [
'jquery.jqplot.min.js',
'plugins/jqplot.categoryAxisRenderer.min.js',
'plugins/jqplot.highlighter.min.js',
'plugins/jqplot.canvasTextRenderer.min.js',
'plugins/jqplot.canvasAxisTickRenderer.min.js',
'plugins/jqplot.canvasAxisLabelRenderer.min.js',
'plugins/jqplot.barRenderer.min.js',
'plugins/jqplot.trendline.min.js',
'plugins/jqplot.pieRenderer.min.js'
];
grunt.initConfig({
jshint: {
all: [
'Gruntfile.js',
'lib/*.js',
'test/**/*.js',
'www/**/*.js'
],
options: {
jshintrc: '.jshintrc'
},
},
metricsgen: {
files: {
dest: 'bin-site/metrics.js'
}
},
uglify: {
options: {
mangle: false,
sourceMap: true,
sourceMapName: 'bin-site/main.js.map',
},
js: {
files: {
'bin-site/main.js': ['www/**/*.js', 'bin-site/**/*.js']
}
}
},
concat: {
jqplot: {
src: jqplot.map(function(file) {
return 'bower_components/jqplot-bower/dist/' + file;
}),
dest: 'bin-site/jqplot.js'
},
less: {
src: ['bower_components/jqplot-bower/dist/jquery.jqplot.min.css', 'www/app/**/*.less', 'www/assets/css/*.css'],
dest: 'bin-site/main.less'
}
},
less: {
dev: {
files: {
'bin-site/main.css': 'bin-site/main.less'
}
},
dist: {
options: {
compress: true
},
files: {
'bin-site/main.css': 'bin-site/main.less'
}
}
},
autoprefixer: {
less: {
src: 'bin-site/main.css',
dest: 'bin-site/main.css'
}
},
copy: {
partials: {
expand: true,
cwd: 'www/app',
src: ['**/*.html'],
dest: 'bin-site/app'
},
fonts: {
expand: true,
cwd: 'www/assets',
src: ['fonts/*.*'],
dest: 'bin-site/assets'
},
endpoints: {
src: ['www/server/endpoints.js'],
dest: 'bin-site/server/endpoints.js'
}
},
processhtml: {
dev: {
options: {
strip: true,
data: {
scripts: jqplot.map(function(file) {
return 'jqplot-bower/dist/' + file;
}).concat(grunt.file.expand({
cwd: 'www'
}, 'app/**/*.js')),
}
},
files: {
'bin-site/index.html': 'www/index.html'
}
},
dist: {
options: {
data: {
scripts: ['main.js']
}
},
files: {
'bin-site/index.html': 'www/index.html'
}
}
},
htmlmin: {
dist: {
options: {
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true,
collapseBooleanAttributes: true
},
files: {
'bin-site/index.html': 'bin-site/index.html'
}
},
},
connect: {
proxies: [{
changeOrigin: false,
host: 'localhost',
port: '5984',
context: grunt.file.expand('lib/couch-views/**/*.js').map(function(file) {
return '/' + path.basename(file, '.js');
}),
rewrite: (function(files) {
var res = {};
files.forEach(function(file) {
var view = path.basename(file, '.js');
res[view + '/_view'] = ['/', couchdb.database, '/_design/', view, '/_view'].join('');
});
return res;
}(grunt.file.expand('lib/couch-views/**/*.js')))
}],
dev: {
options: {
hostname: '*',
port: 9000,
base: ['test/res', 'bower_components', 'bin-site', 'www'],
livereload: true,
middleware: function(connect, options) {
var middlewares = [];
if (!Array.isArray(options.base)) {
options.base = [options.base];
}
middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest);
options.base.forEach(function(base) {
middlewares.push(serveStatic(base));
});
return middlewares;
},
useAvailablePort: true,
}
}
},
watch: {
options: {
livereload: true,
},
views: {
files: ['lib/couch-views/*.js'],
tasks: ['deployViews']
},
less: {
files: ['www/**/*.less'],
tasks: ['concat:less', 'less:dev', 'autoprefixer']
},
html: {
files: ['www/index.html'],
tasks: ['processhtml:dev']
},
others: {
files: ['www/app/**/*.html', 'www/app/**/*.js'],
tasks: []
}
},
mochaTest: {
options: {
reporter: 'dot',
timeout: 1000 * 60 * 10
},
unit: {
src: ['test/**/*.spec.js'],
}
},
clean: {
all: ['bin-site', 'test.log'],
dist: ['bin-site/jqplot.js', 'bin-site/main.less', 'bin-site/metrics.js']
}
});
require('load-grunt-tasks')(grunt);
require('./tasks/metricsgen')(grunt);
grunt.registerTask('seedData', function() {
var done = this.async();
require('./test/seedData')(done, 100);
});
grunt.registerTask('deployViews', function() {
var done = this.async();
require('./lib/couchViews')(require('./test/util.js').config(), function(err, res) {
console.log(err, res);
done(!err);
});
});
grunt.registerTask('dev', ['metricsgen', 'concat:less', 'less:dev', 'autoprefixer', 'processhtml:dev', 'configureProxies:server', 'connect:dev', 'watch']);
grunt.registerTask('dist', ['jshint', 'concat', 'metricsgen', 'uglify', 'less:dist', 'autoprefixer', 'copy', 'processhtml:dist', 'htmlmin', 'clean:dist']);
grunt.registerTask('test', ['clean', 'dist', 'mochaTest']);
grunt.registerTask('default', ['dev']);
};
================================================
FILE: README.md
================================================
# perfjankie
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.
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.
After running the tests, navigate to the following url to see the results dashboard.
> http://couchdb.serverl.url/databasename/_design/site/index.html
Here is a [dashboard](http://nparashuram.com/perfslides/perfjankie) created from a [sample project](http://github.com/axemclion/perfslides).

## Why ?
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.
The results dashboard
## Setup
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.
## Usage
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`.
### Node Module
The API call looks like the following
```javascript
var perfjankie = require('perfjankie');
perfjankie({
"url": "http://localhost:9000/testpage.html", // URL of the page that you would like to test.
/* The next set of values identify the test */
name: "Component or Webpage Name", // A friendly name for the URL. This is shown as component name in the dashboard
suite: "optional suite name", // Displayed as the title in the dashboard. Only 1 suite name for all components
time: new Date().getTime(), // Used to sort the data when displaying graph. Can be the time when a commit was made
run: "commit#Hash", // A hash for the commit, displayed in the x-axis in the dashboard
repeat: 3, // Run the tests 3 times. Default is 1 time
/* Identifies where the data and the dashboard are saved */
couch: {
server: 'http://localhost:5984',
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.
database: 'performance',
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.
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.
},
callback: function(err, res) {
// The callback function, err is falsy if all of the following happen
// 1. Browsers perf tests ran
// 2. Data has been saved in couchDB
// err is not falsy even if update site fails.
},
/* OPTIONS PASSED TO BROWSER-PERF */
// Properties identifying the test environment */
browsers: [{ // This can also be a ["chrome", "firefox"] or "chrome,firefox"
browserName: "chrome",
version: 32,
platform: "Windows 8.1"
}], // See browser perf browser configuration for all options.
selenium: {
hostname: "ondemand.saucelabs.com", // or localhost or hub.browserstack.com
port: 80,
},
BROWSERSTACK_USERNAME: process.env.BROWSERSTACK_USERNAME, // If using browserStack
BROWSERSTACK_KEY: process.env.BROWSERSTACK_KEY, // If using browserStack, this is automatically added to browsers object
SAUCE_USERNAME: process.env.SAUCE_USERNAME, // If using Saucelabs
SAUCE_ACCESSKEY: process.env.SAUCE_ACCESSKEY, // If using Saucelabs
/* A way to log the information - can be bunyan, or grunt logs. */
log: { // Expects the following methods,
fatal: grunt.fail.fatal.bind(grunt.fail),
error: grunt.fail.warn.bind(grunt.fail),
warn: grunt.log.error.bind(grunt.log),
info: grunt.log.ok.bind(grunt.log),
debug: grunt.verbose.writeln.bind(grunt.verbose),
trace: grunt.log.debug.bind(grunt.log)
}
});
```
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.
### Grunt Task
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.
### Command line
Run `perfjankie --help` to see a list of all the options.
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
```bash
$ perfjankie --config-file=local.config.json --only-update-site 'example.com'
```
Or without a config file
```bash
$ 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'
```
The config file can contain server configuration and can look like [this](https://github.com/axemclion/perfjankie/blob/master/test/res/local.config.json).
## Hosting dashboard on a different server
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,
1. Use the npm module and host the contents of the `site` folder.
2. Open index.html and insert the following snippet in the `
` section
```html
```
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.
## Login before running tests
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.
## Migrating data from older versions
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
```bash
$ perfjankie --config-file=local.config.json --migrate=newDatabaseName
```
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`
## What does it measure?
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
* 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.
* 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.
For more information, see the documentation for [browser-perf](http://github.com/axemclion/browser-perf)
# Development
## Dev setup
Any changes should be verified with unit tests, see `test`-folder.
To run the tests you local couchdb installed with a database, see `test/res/local.config.json` for details:
1. start couchdb
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`
3. run tests via `npm test`
================================================
FILE: bower.json
================================================
{
"name": "perfjankie",
"main": "index.js",
"version": "0.0.0",
"homepage": "https://github.com/axemclion/perfjankie",
"authors": [
"Parashuram "
],
"description": "Website for Perfjankie",
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"angular": "~1.2.18",
"bootstrap": "~3.1.1",
"jqplot-bower": "~1.0.8",
"jquery": "~2.1.1",
"angular-route": "~1.2.25"
}
}
================================================
FILE: lib/cli.js
================================================
#!/usr/bin/env node
var program = require('commander'),
fs = require('fs');
program
.version('0.0.1')
.option('-c --config-file ', 'Specify a configuration file. If other options are specified, they have precedence over options in config file')
.option('-s, --selenium ', 'Specify Selenium Server, like localhost:4444 or ondemand.saucelabs.com:80', 'localhost:4444')
.option('-u --username ', 'Sauce, BrowserStack or Selenium User Name')
.option('-a --accesskey ', 'Sauce, BrowserStack or Selenium Access Key')
.option('--browsers ', 'List of browsers to run the tests on')
.option('--couch-server ', 'Location of the couchDB server')
.option('--couch-database ', 'Name of the couch database')
.option('--couch-user ', 'Username of the couch user that can create design documents and save data')
.option('--couch-pwd ', 'Password of the couchDB user')
.option('--name ', 'A friendly name for the URL. This is shown as component name in the dashboard')
.option('--run ', 'A hash for the commit, or any identifier displayed in the x-axis in the dashboard')
.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())
.option('--suite ', 'Displayed as the title in the dashboard.')
.option('--update-site', 'Update the site in addition to running the tests', true)
.option('--only-update-site', 'Only update the site, do not run tests or save data for the site', false)
.option('--migrate ', 'Migrate Database to the latest version')
.parse(process.argv);
var config = {};
if (program.configFile) {
try {
var config = JSON.parse(fs.readFileSync(program.configFile));
} catch (e) {
throw e;
}
}
var extend = function(obj1, obj2) {
for (var key in obj2) {
if (typeof obj1[key] !== 'undefined' && typeof obj2[key] === 'object') {
obj1[key] = extend(obj1[key], obj2[key]);
} else if (typeof obj2[key] !== 'undefined') {
obj1[key] = obj2[key];
}
}
return obj1;
};
config = extend(config, {
url: program.args[0],
name: program.name,
suite: program.suite,
time: program.time,
run: program.run,
selenium: program.serverURL,
browsers: program.browsers,
username: program.username,
accesskey: program.accesskey,
log: {
'fatal': console.error.bind(console),
'error': console.error.bind(console),
'warn': console.warn.bind(console),
'info': console.info.bind(console),
'debug': console.log.bind(console),
'trace': console.log.bind(console)
},
couch: {
server: program.couchServer,
database: program.couchDatabase,
updateSite: program.onlyUpdateSite ? true : program.updateSite,
onlyUpdateSite: program.onlyUpdateSite,
username: program.couchUser,
pwd: program.couchPwd
},
callback: function(err, res) {
console.log(err, res);
}
});
if (program.migrate) {
console.log('Runnung database migrations');
require('../migrations/index.js')(config, program.migrate).done();
} else {
require('./')(config);
}
================================================
FILE: lib/couch-views/metrics_data.js
================================================
{
_id: "_design/metrics_data",
language: "javascript",
views: {
stats: {
reduce: '_stats',
map: function(doc) {
if (doc.type === 'perfData') {
for (var key in doc.data) {
if (typeof doc.data[key] === 'number') {
emit([doc.browser, doc.name, key, doc.time, doc.run], doc.data[key]);
}
}
}
}
}
}
}
================================================
FILE: lib/couch-views/pagelist.js
================================================
{
_id: "_design/pagelist",
language: "javascript",
views: {
pages: {
reduce: "_count",
map: function(doc) {
emit([doc.suite, doc.name, doc.meta._browserName], null);
}
}
}
}
================================================
FILE: lib/couch-views/runs.js
================================================
{
_id: "_design/runs",
language: "javascript",
views: {
list: {
map: function(doc) {
emit([doc.browser, doc.name, doc.time, doc.run], null);
},
reduce: "_count"
},
data: {
map: function(doc) {
if (doc.type === 'perfData') {
for (var key in doc.data) {
if (typeof doc.data[key] === 'number') {
emit([doc.browser, doc.name, doc.time, doc.run, key], doc.data[key]);
}
}
}
},
reduce: "_stats"
}
}
}
================================================
FILE: lib/couchData.js
================================================
module.exports = function (config, data) {
var server = require('./utils').getCouchDB(config.couch),
Q = require('q'),
dfd = Q.defer();
var db = null,
log = config.log;
db = server.use(config.couch.database);
log.debug('Saving data');
if (typeof data === 'undefined') {
return;
}
db.bulk({
docs: data.map(function (val) {
var res = {
url: config.url,
data: val,
meta: {},
name: config.name,
suite: config.suite,
browser: val._browser || val._browserName,
run: config.run || config.time,
time: config.time,
type: 'perfData'
};
for (var key in res.data) {
if (key.indexOf('_') === 0) {
res.meta[key] = res.data[key];
delete res.data[key];
}
}
return res;
})
}, {
new_edits: true
}, function (err, res) {
log.debug('Got result back after saving data');
err ? dfd.reject(err) : dfd.resolve(res);
});
return dfd.promise;
};
================================================
FILE: lib/couchSite.js
================================================
var url = require('url');
module.exports = function (config) {
var Q = require('q');
if (!config.couch.updateSite) {
return Q(1); // jshint ignore:line
}
var path = require('path'),
log = config.log;
var siteDest = path.join(__dirname, '../bin-site'),
server = require('./utils').getCouchDB(config.couch),
db = server.use(config.couch.database);
var mime = require('./mime');
function contentType(filename) {
var contentType = path.extname(filename);
if (contentType.length > 0) {
return mime[contentType.substring(1)];
} else {
return 'text';
}
}
function readFileContents(files, siteDest) {
var fs = require('fs'),
path = require('path');
var dfd = Q.defer();
var fileContents = [];
(function readFile(i) {
if (i < files.length) {
fs.readFile(path.join(siteDest, files[i]), function (err, data) {
if (!err) {
fileContents.push({
name: files[i],
data: data,
contentType: contentType(files[i]),
content_type: contentType(files[i])
});
}
readFile(i + 1);
});
} else {
log.debug('Completed reading all files');
dfd.resolve(fileContents);
}
}(0));
return dfd.promise;
}
function removeSite() {
var dfd = Q.defer();
db.get('_design/site', function (err, res) {
if (!err) {
db.destroy('_design/site', res._rev, function (err, res) {
if (err) {
dfd.reject(err);
} else {
dfd.resolve();
}
});
} else {
dfd.resolve();
}
});
return dfd.promise;
}
return removeSite().then(function () {
return Q.nfcall(require('glob'), '**/*.*', {
cwd: siteDest
});
}).then(function (files) {
return readFileContents(files, siteDest);
}).then(function (fileContents) {
return Q.ninvoke(db.multipart, 'insert', {}, fileContents, '_design/site');
}).then(function () {
var link = url.parse([config.couch.server, config.couch.database, '_design/site/index.html'].join('/'));
link.auth = null;
log.info('Site Updated. View graphs at ' + url.format(link));
});
};
================================================
FILE: lib/couchViews.js
================================================
/* jshint evil: true*/
var Q = require('q');
function uploadViews(db, log) {
var dfd = Q.defer();
var fs = require('fs'),
glob = require('glob').sync;
var views = glob(__dirname + '/couch-views/*.js');
log.debug('Starting to upload views');
(function uploadView(i) {
if (i < views.length) {
log.debug('Checking View ' + views[i]);
var view = JSON.parse(JSON.stringify(eval('_x_ = ' + fs.readFileSync(views[i], 'utf-8')), function (key, val) {
if (typeof val === 'function') {
return val.toString();
}
return val;
}));
db.get(view._id, function (err, res) {
if (!err) {
view._rev = res._rev;
}
db.insert(view, function (err, res) {
uploadView(i + 1);
});
});
} else {
log.debug('All views updated');
dfd.resolve();
}
}(0));
return dfd.promise;
}
module.exports = function (config) {
if (!config.couch.updateSite) {
return Q(); // jshint ignore:line
}
var log = config.log,
server = require('./utils').getCouchDB(config.couch),
db = server.use(config.couch.database);
return uploadViews(db, log);
};
================================================
FILE: lib/index.js
================================================
var Q = require('q');
var init = require('./init'),
site = require('./couchSite'),
views = require('./couchViews'),
data = require('./couchData'),
perf = require('./perfTests');
function runTests(config) {
var dfd = Q.defer();
(function next(i) {
if (i < config.repeat) {
perf(config).then(function(results) {
return data(config, results);
}).then(function() {
next(i + 1);
}, function(err) {
dfd.reject(err);
}).done();
} else {
dfd.resolve();
}
}(0));
return dfd.promise;
}
module.exports = function(config) {
var options = require('./options')(config),
log = options.log,
cb = options.callback;
log.info('Starting PerfJankie');
init(config).then(function() {
return runTests(config);
}).then(function() {
return Q.allSettled([site(config), views(config)]);
}).then(function(res) {
log.debug('Successfully done all tasks');
cb(null, res);
}, function(err) {
log.debug(err);
cb(err, null);
}).done();
};
================================================
FILE: lib/init.js
================================================
module.exports = function (config) {
var Q = require('q'),
server = require('./utils').getCouchDB(config.couch),
log = config.log;
log.debug('Trying to see if the database exists');
return Q.ninvoke(server.db, 'get', config.couch.database).catch(function (err) {
log.debug('Could not find database: %s\nCreating a new database %s', err.reason, config.couch.database);
return Q.ninvoke(server.db, 'create', config.couch.database);
}).then(function () {
var db = server.use(config.couch.database);
return Q.ninvoke(db, 'get', 'version').catch(function (err) {
return Q.ninvoke(db, 'insert', {
version: require('../package.json').dbVersion
}, 'version');
});
});
};
================================================
FILE: lib/mime.js
================================================
// from http://github.com/felixge/node-paperboy
module.exports = {
"aiff": "audio/x-aiff",
"appcache": "text/cache-manifest",
"arj": "application/x-arj-compressed",
"asf": "video/x-ms-asf",
"asx": "video/x-ms-asx",
"au": "audio/ulaw",
"avi": "video/x-msvideo",
"bcpio": "application/x-bcpio",
"ccad": "application/clariscad",
"cod": "application/vnd.rim.cod",
"com": "application/x-msdos-program",
"cpio": "application/x-cpio",
"cpt": "application/mac-compactpro",
"csh": "application/x-csh",
"css": "text/css",
"deb": "application/x-debian-package",
"dl": "video/dl",
"doc": "application/msword",
"drw": "application/drafting",
"dvi": "application/x-dvi",
"dwg": "application/acad",
"dxf": "application/dxf",
"dxr": "application/x-director",
"etx": "text/x-setext",
"ez": "application/andrew-inset",
"fli": "video/x-fli",
"flv": "video/x-flv",
"gif": "image/gif",
"gl": "video/gl",
"gtar": "application/x-gtar",
"gz": "application/x-gzip",
"hdf": "application/x-hdf",
"hqx": "application/mac-binhex40",
"html": "text/html",
"ice": "x-conference/x-cooltalk",
"ico": "image/x-icon",
"ief": "image/ief",
"igs": "model/iges",
"ips": "application/x-ipscript",
"ipx": "application/x-ipix",
"jad": "text/vnd.sun.j2me.app-descriptor",
"jar": "application/java-archive",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "text/javascript",
"json": "application/json",
"latex": "application/x-latex",
"lsp": "application/x-lisp",
"lzh": "application/octet-stream",
"m": "text/plain",
"m3u": "audio/x-mpegurl",
"man": "application/x-troff-man",
"me": "application/x-troff-me",
"midi": "audio/midi",
"mif": "application/x-mif",
"mime": "www/mime",
"movie": "video/x-sgi-movie",
"mustache": "text/plain",
"mp4": "video/mp4",
"mpg": "video/mpeg",
"mpga": "audio/mpeg",
"ms": "application/x-troff-ms",
"nc": "application/x-netcdf",
"oda": "application/oda",
"ogm": "application/ogg",
"pbm": "image/x-portable-bitmap",
"pdf": "application/pdf",
"pgm": "image/x-portable-graymap",
"pgn": "application/x-chess-pgn",
"pgp": "application/pgp",
"pm": "application/x-perl",
"png": "image/png",
"pnm": "image/x-portable-anymap",
"ppm": "image/x-portable-pixmap",
"ppz": "application/vnd.ms-powerpoint",
"pre": "application/x-freelance",
"prt": "application/pro_eng",
"ps": "application/postscript",
"qt": "video/quicktime",
"ra": "audio/x-realaudio",
"rar": "application/x-rar-compressed",
"ras": "image/x-cmu-raster",
"rgb": "image/x-rgb",
"rm": "audio/x-pn-realaudio",
"rpm": "audio/x-pn-realaudio-plugin",
"rtf": "text/rtf",
"rtx": "text/richtext",
"scm": "application/x-lotusscreencam",
"set": "application/set",
"sgml": "text/sgml",
"sh": "application/x-sh",
"shar": "application/x-shar",
"silo": "model/mesh",
"sit": "application/x-stuffit",
"skt": "application/x-koan",
"smil": "application/smil",
"snd": "audio/basic",
"sol": "application/solids",
"spl": "application/x-futuresplash",
"src": "application/x-wais-source",
"stl": "application/SLA",
"stp": "application/STEP",
"sv4cpio": "application/x-sv4cpio",
"sv4crc": "application/x-sv4crc",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"tar": "application/x-tar",
"tcl": "application/x-tcl",
"tex": "application/x-tex",
"texinfo": "application/x-texinfo",
"tgz": "application/x-tar-gz",
"tiff": "image/tiff",
"tr": "application/x-troff",
"tsi": "audio/TSP-audio",
"tsp": "application/dsptype",
"tsv": "text/tab-separated-values",
"unv": "application/i-deas",
"ustar": "application/x-ustar",
"vcd": "application/x-cdlink",
"vda": "application/vda",
"vivo": "video/vnd.vivo",
"vrm": "x-world/x-vrml",
"wav": "audio/x-wav",
"wax": "audio/x-ms-wax",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"wmx": "video/x-ms-wmx",
"wrl": "model/vrml",
"wvx": "video/x-ms-wvx",
"xbm": "image/x-xbitmap",
"xlw": "application/vnd.ms-excel",
"xml": "text/xml",
"xpm": "image/x-xpixmap",
"xwd": "image/x-xwindowdump",
"xyz": "chemical/x-pdb",
"zip": "application/zip"
};
================================================
FILE: lib/options.js
================================================
module.exports = function(config) {
var noop = function() {};
config.log = config.log || config.logger || noop;
if (typeof config.log === 'function') {
config.log = {
'fatal': config.log,
'error': config.log,
'warn': config.log,
'info': config.log,
'debug': config.log,
'trace': config.log
};
}
function assert(expr, msg) {
if (!expr) {
throw new Error(msg);
}
}
config.selenium = config.selenium || {
hostname: 'localhost',
port: 4444
};
config.repeat = config.repeat || 1;
config.name = config.name || config.url;
config.suite = config.suite || 'Default Test Suite';
if (typeof config.callback !== 'function') {
config.callback = noop;
}
if (typeof config.couch === 'undefined') {
config.couch = {};
}
if (typeof config.couch.username !== 'undefined') {
var url = require('url');
var href = url.parse(config.couch.server);
href.auth = config.couch.username + ':' + config.couch.pwd;
config.couch.server = url.format(href);
}
assert(typeof config.couch.server !== 'undefined', 'Location to save results is not defined. Please define a couchDB server');
config.couch.database = config.couch.db || config.couch.database;
assert(typeof config.couch.server !== 'undefined', 'Location to save results is not defined. Please define a Database to save the results');
return config;
};
================================================
FILE: lib/perfTests.js
================================================
module.exports = function(config) {
var Q = require('q'),
dfd = Q.defer();
if (config.couch.onlyUpdateSite) {
dfd.resolve();
} else {
var browserPerf = config.browserPerf || require('browser-perf'),
log = config.log;
log.debug('Starting Browser Perf');
browserPerf(config.url, function(err, results) {
if (err) {
dfd.reject(err);
} else {
log.debug('Got Browser Perf results back, now saving the results');
dfd.resolve(results);
}
}, {
browsers: config.browsers,
selenium: config.selenium,
debugBrowser: config.debug,
preScript: config.preScript,
preScriptFile: config.preScriptFile,
actions: config.actions,
metrics: config.metrics,
SAUCE_ACCESSKEY: config.SAUCE_ACCESSKEY || undefined,
SAUCE_USERNAME: config.SAUCE_USERNAME || undefined,
BROWSERSTACK_USERNAME: config.BROWSERSTACK_USERNAME || undefined,
BROWSERSTACK_KEY: config.BROWSERSTACK_KEY || undefined
});
}
return dfd.promise;
};
================================================
FILE: lib/utils.js
================================================
var nano = require('nano');
var getCouchDB = function (options) {
var serverUrl = options.server,
server;
if (options.requestOptions) {
server = nano({
"url": serverUrl,
"parseUrl": false,
"requestDefaults": options.requestOptions
});
} else {
server = nano({
"url": serverUrl,
"parseUrl": false
});
}
return server;
};
module.exports = {
getCouchDB: getCouchDB
};
================================================
FILE: migrations/cli.js
================================================
#!/usr/bin/env node
var program = require('commander'),
fs = require('fs');
var oldDB, newDB;
program
.version('0.0.1')
.option('-c --couchServer', 'Location of the couchDB server')
.option('-u --username ', 'Username of the couch user that can create design documents and save data')
.option('-p --password ', 'Password of the couchDB user')
.parse(process.argv);
program.on('--help', function() {
console.log('Usage:');
console.log('');
console.log(' $ perfjankie-dbmigrate old-database new-database [options]');
console.log('');
});
program.parse(process.argv);
if (program.args.length !== 2) {
program.help();
}
var config = {
log: {
'fatal': console.error.bind(console),
'error': console.error.bind(console),
'warn': console.warn.bind(console),
'info': console.info.bind(console),
'debug': console.log.bind(console),
'trace': console.log.bind(console),
},
couch: {
server: program.couchServer || 'http://localhost:5984',
username: program.username,
pwd: program.password
},
callback: function(err, res) {
console.log(err, res);
}
};
console.log(program.username, program.password, oldDB, newDB)
require('./index.js')(config, program.args[0], program.args[1]).done();
================================================
FILE: migrations/index.js
================================================
var semver = require('semver');
var glob = require('glob');
var nano = require('nano');
var Q = require('q');
Q.longStackSupport = true;
var dbInit = require('../lib/init.js');
module.exports = function migrate(opts, oldDBName, newDBName) {
var config = require('../lib/options')(opts);
var server = nano(config.couch.server);
var oldDB = server.use(oldDBName);
config.couch.updateSite = true;
config.couch.database = newDBName;
return dbInit(config).then(function() {
config.log.info('Migrating from ', oldDBName, 'to', newDBName);
return Q.ninvoke(oldDB, 'get', 'version').then(function(version) {
return version[0].version;
}, function() {
return '0.1.0';
});
}).then(function(oldDBVersion) {
return glob.sync('migrate-*.js', {
cwd: __dirname
}).filter(function(file) {
return semver.lt(oldDBVersion, file.slice(8, -3));
}).sort(function(a, b) {
return semver.gt(a.slice(8, -3), b.slice(8, -3));
});
}).then(function(files) {
if (files.length === 0) {
config.log.info('Database is already up to date');
}
return files.map(function(file) {
var script = require('./' + file);
config.log.info('\nRunning migration - %s', file);
return script(oldDB, server.use(newDBName), config);
}).reduce(Q.when, Q());
}).then(function() {
config.log.info('Updating views');
return require('../lib/couchViews.js')(config);
}).then(function() {
config.log.info('Updating site');
return require('../lib/couchSite.js')(config);
});
};
================================================
FILE: migrations/migrate-0.2.0.js
================================================
// Migrating from 0.1.x to 0.2.0
var Q = require('q');
var utility = require('./utility');
module.exports = function(oldDb, newDb, config) {
var log = config.log;
return utility.forEachDoc(oldDb, newDb, function(doc) {
if (doc.type !== 'perfData') {
return null;
}
delete doc._id;
delete doc._rev;
doc.url = null;
doc.browser = doc.meta._browserName || null;
for (var key in doc.data) {
doc.data[key] = doc.data[key].value;
}
doc.data[events[0]] = 0;
for (var i = 1; i < events.length; i++) {
if (typeof doc.data[events[i]] === 'undefined') {
doc.data[events[i]] = 0;
}
doc.data[events[i]] = doc.data[events[i]] + doc.data[events[i - 1]];
}
return doc;
});
};
var events = [
'navigationStart',
'unloadEventStart',
'unloadEventEnd',
'redirectStart',
'redirectEnd',
'fetchStart',
'domainLookupStart',
'domainLookupEnd',
'connectStart',
'connectEnd',
'secureConnectionStart',
'requestStart',
'responseStart',
'domLoading',
'domInteractive',
'domContentLoadedEventStart',
'domContentLoadedEventEnd',
'domComplete',
'loadEventStart',
'loadEventEnd'
];
================================================
FILE: migrations/migrate-0.3.0.js
================================================
// Migrating from 0.2.x to 1.2.x
var Q = require('q');
var utility = require('./utility');
module.exports = function(oldDb, newDb, config) {
var log = config.log;
return utility.forEachDoc(oldDb, newDb, function(doc) {
if (doc.type !== 'perfData') {
return null;
}
delete doc._id;
delete doc._rev;
for (var key in doc.data) {
if (typeof doc.data.mean_frame_time === 'number') { // From TracingMetrics
doc.data.frames_per_sec = 1000 / doc.data.mean_frame_time;
}
if (typeof doc.data.meanFrameTime === 'number') { // from RAF
doc.data.framesPerSec_raf = 1000 / doc.data.meanFrameTime;
}
}
return doc;
});
};
var getFramesPerSec = function(val, metrics) {
var mft;
// Iterate over each candidate to calculate FPS
for (var i = 0; i < metrics.length; i++) {
if (val[metrics[i]]) {
mft = val[metrics[i]].sum / val[metrics[i]].count;
}
if (mft >= 10 && mft <= 60) {
break;
} else {
mft = null;
}
}
if (mft) {
return {
sum: 1000 / mft,
count: 1
};
}
};
================================================
FILE: migrations/migrate-0.4.0.js
================================================
// Migrating to browser-perf@1.3.0. Names of some metrics have changed
var Q = require('q');
var utility = require('./utility');
module.exports = function(oldDb, newDb, config) {
var log = config.log;
return utility.forEachDoc(oldDb, newDb, function(doc) {
if (doc.type !== 'perfData') {
return null;
}
delete doc._id;
delete doc._rev;
doc.data.meanFrameTime_raf = doc.data.meanFrameTime;
var metrics = new Metrics(doc.data);
metrics.addMetric('loadTime', 'loadEventEnd', 'fetchStart');
metrics.addMetric('domReadyTime', 'domComplete', 'domInteractive');
metrics.addMetric('readyStart', 'fetchStart', 'navigationStart');
metrics.addMetric('redirectTime', 'redirectEnd', 'redirectStart');
metrics.addMetric('appcacheTime', 'domainLookupStart', 'fetchStart');
metrics.addMetric('unloadEventTime', 'unloadEventEnd', 'unloadEventStart');
metrics.addMetric('domainLookupTime', 'domainLookupEnd', 'domainLookupStart');
metrics.addMetric('connectTime', 'connectEnd', 'connectStart');
metrics.addMetric('requestTime', 'responseEnd', 'requestStart');
metrics.addMetric('initDomTreeTime', 'domInteractive', 'responseEnd');
metrics.addMetric('loadEventTime', 'loadEventEnd', 'loadEventStart');
return doc;
});
};
function Metrics(metrics) {
this.timing = metrics;
}
Metrics.prototype.addMetric = function(prop, a, b) {
if (typeof this.timing[a] === 'number' && typeof this.timing[b] === 'number') {
this.timing[prop] = this.timing[a] - this.timing[b];
}
}
================================================
FILE: migrations/utility.js
================================================
var Q = require('q');
var MAX_LIMIT = 53;
module.exports = {
forEachDoc: function(oldDb, newDb, callback) {
function processBatch(skip) {
skip = skip || 0;
var count = 0;
return Q.ninvoke(oldDb, 'get', '_all_docs', {
limit: MAX_LIMIT,
skip: skip,
include_docs: true
}).then(function(docs) {
count = docs[0].rows.length;
return result = docs[0].rows.map(function(data) {
return callback(data.doc);
});
}).then(function(results) {
return Q.ninvoke(newDb, 'bulk', {
docs: results.filter(function(val) {
return val !== null;
})
}, {
new_edits: true
});
}).then(function() {
if (count >= MAX_LIMIT) {
return processBatch(skip + MAX_LIMIT);
} else {
return Q();
}
});
}
return processBatch();
}
}
================================================
FILE: package.json
================================================
{
"name": "perfjankie",
"version": "2.1.2",
"dbVersion": "0.4.0",
"description": "Browser Performance regression suite",
"main": "lib/index.js",
"scripts": {
"test": "bower install && grunt test",
"prepublish": "grunt clean dist"
},
"bin": {
"perfjankie": "lib/cli.js",
"perfjankie-dbmigrate": "migrations/cli.js"
},
"author": "Parashuram ",
"license": "BSD-2-Clause",
"dependencies": {
"browser-perf": "~1.4.0",
"commander": "~2.8.1",
"glob": "~5.0.14",
"nano": "~6.1.5",
"q": "~1.4.1",
"sauce-tunnel": "^2.2.3",
"semver": "^5.0.1",
"serve-static": "^1.10.0"
},
"devDependencies": {
"bunyan": "~1.5.1",
"bower": "^1.7.9",
"chai": "~3.2.0",
"chai-as-promised": "^5.1.0",
"chromedriver": "^2.21.2",
"dtrace-provider": "^0.6.0",
"grunt": "~0.4.5",
"grunt-autoprefixer": "^3.0.3",
"grunt-connect-proxy": "^0.2.0",
"grunt-contrib-clean": "~0.6.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-connect": "~0.11.2",
"grunt-contrib-copy": "^0.8.0",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-jshint": "~0.11.2",
"grunt-contrib-less": "^1.0.1",
"grunt-contrib-uglify": "^0.9.1",
"grunt-contrib-watch": "~0.6.1",
"grunt-mocha-test": "~0.12.7",
"grunt-processhtml": "^0.3.8",
"load-grunt-tasks": "~3.2.0",
"mocha": "~2.2.5",
"selenium-server": "^2.53.0",
"sinon": "~1.15.4"
},
"keywords": [
"browser-perf",
"telemetry",
"gruntplugin"
],
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git://github.com/axemclion/perfjankie.git"
},
"bugs": {
"url": "https://github.com/axemclion/perfjankie/issues"
}
}
================================================
FILE: tasks/metricsgen.js
================================================
var Q = require('q');
var browserPerf = require('browser-perf');
module.exports = function(grunt) {
grunt.registerMultiTask('metricsgen', 'Generates the names of metrics', function() {
var apiDocs = new browserPerf.docs();
var regex = /(_avg|_max|_count)$/;
var doc = {};
for (var key in apiDocs.metrics) {
var modifier = null;
if (apiDocs.metrics[key].source === 'TimelineMetrics' && key.match(regex)) {
var idx = key.lastIndexOf('_');
modifier = key.substr(idx + 1);
key = key.substr(0, idx);
}
if (typeof doc[key] === 'undefined') {
doc[key] = apiDocs.metrics[key] || {};
doc[key].stats = [];
}
if (modifier) {
doc[key].stats.push(modifier);
}
}
var metrics = [];
for (var key in doc) {
doc[key].name = key;
metrics.push(doc[key]);
}
this.files.forEach(function(file) {
grunt.file.write(file.dest, ['var METRICS_LIST =', JSON.stringify(metrics)].join(''));
});
});
};
================================================
FILE: tasks/task.js
================================================
module.exports = function(grunt) {
grunt.registerMultiTask('perfjankie', 'Run rendering performance test cases', function() {
var done = this.async(),
path = require('path'),
options = this.options({
log: { // Expects the following methods,
fatal: grunt.fail.fatal.bind(grunt.fail),
error: grunt.fail.warn.bind(grunt.fail),
warn: grunt.log.error.bind(grunt.log),
info: grunt.log.ok.bind(grunt.log),
debug: grunt.verbose.writeln.bind(grunt.verbose),
trace: grunt.log.debug.bind(grunt.log)
},
time: new Date().getTime()
}),
files = options.urls;
options.time = parseFloat(options.time, 10);
if (options.sauceTunnel) {
var SauceTunnel = require('sauce-tunnel');
grunt.log.writeln('Starting Saucelabs Tunnel');
var tunnel = new SauceTunnel(options.SAUCE_USERNAME, options.SAUCE_ACCESSKEY, options.sauceTunnel, true);
tunnel.start(function(status) {
grunt.log.ok('Saucelabs Tunnel started - ' + status);
if (status === false) {
done(false);
} else {
runPerfTest(files, options, function(res) {
grunt.verbose.writeln('All perf tests completed');
tunnel.stop(function() {
done(res);
});
});
}
});
} else {
runPerfTest(files, options, done);
}
});
var perfjankie = require('..');
var runPerfTest = function(files, options, cb) {
var success = true;
(function runTest(i) {
if (i < files.length) {
grunt.log.writeln('Testing File ', files[i]);
var config = {
url: files[i],
name: files[i].replace(/(\S)*\/|\.html$/gi, ''),
callback: function(err, res) {
if (err) {
success = false;
console.log(res);
grunt.log.warn(err);
} else {
grunt.log.ok('Saved performance metrics');
}
runTest(i + 1);
}
};
for (var key in options) {
config[key] = options[key];
}
perfjankie(config);
} else {
cb(success);
}
}(0));
}
};
================================================
FILE: test/index.spec.js
================================================
var expect = require('chai').expect,
sinon = require('sinon'),
fs = require('fs'),
nano = require('nano');
require('q').longStackSupport = true;
describe('App', function () {
var browserPerfStub = sinon.stub();
var sampleData;
var util = require('./util'),
app = require('../'),
config = util.config({});
before(function (done) {
nano(config.couch.server).db.destroy(config.couch.database, function(err, res) {
done();
});
});
beforeEach(function () {
config.log.info('===========');
sampleData = JSON.parse(fs.readFileSync(__dirname + '/res/sample-perf-results.json', 'utf8'));
browserPerfStub.callsArgWith(1, null, sampleData);
});
it('should only update data', function (done) {
app(util.config({
couch: {
updateSite: false
},
callback: function (err, res) {
console.log(err);
expect(err).to.not.be.ok;
expect(res).to.be.ok;
done();
},
browserPerf: browserPerfStub
}));
});
it('should only update site', function (done) {
var couchDataStub = sinon.stub();
couchDataStub.callsArgWith(2, null, []);
app(util.config({
couch: {
onlyUpdateSite: true
},
callback: function (err, res) {
expect(couchDataStub.called).to.not.be.true;
expect(err).to.not.be.ok;
expect(res).to.be.ok;
done();
}
}));
});
it('should run performance tests and save results in a database', function (done) {
app(util.config({
callback: function (err, res) {
expect(err).to.not.be.ok;
expect(res).to.be.ok;
done();
},
browserPerf: browserPerfStub
}));
});
});
================================================
FILE: test/res/local.config.json
================================================
{
"browsers": [{
"browserName": "chrome",
"version": 32
}],
"selenium": {
"hostname": "localhost",
"port": 4444
},
"couch": {
"server": "http://localhost:5984",
"username": "admin_user",
"pwd": "admin_pass",
"database": "perfjankie-test"
}
}
================================================
FILE: test/res/sample-perf-results.json
================================================
[{
"numAnimationFrames": 397,
"numFramesSentToScreen": 397,
"droppedFrameCount": 60,
"meanFrameTime": 19.36006281407138,
"fetchStart": 1411941392008,
"redirectStart": 0,
"domComplete": 1411941394116,
"redirectEnd": 0,
"loadEventStart": 1411941394116,
"navigationStart": 1411941391221,
"requestStart": 1411941392105,
"responseEnd": 1411941392800,
"secureConnectionStart": 0,
"domLoading": 1411941392268,
"domInteractive": 1411941393139,
"domainLookupEnd": 1411941392026,
"domContentLoadedEventStart": 1411941393139,
"loadEventEnd": 1411941394141,
"connectEnd": 1411941392105,
"responseStart": 1411941392256,
"unloadEventStart": 0,
"domContentLoadedEventEnd": 1411941393423,
"connectStart": 1411941392026,
"unloadEventEnd": 0,
"domainLookupStart": 1411941392009,
"Program": 3199.1089999601245,
"Program_avg": 0.691250864295619,
"Program_max": 406.51899999938905,
"Program_count": 4628,
"UpdateLayerTree": 26.194000008516014,
"UpdateLayerTree_avg": 0.06120093459933648,
"UpdateLayerTree_max": 0.5839999997988343,
"UpdateLayerTree_count": 428,
"EvaluateScript": 430.41699999943376,
"EvaluateScript_avg": 5.380212499992922,
"EvaluateScript_max": 198.59800000023097,
"EvaluateScript_count": 80,
"ParseHTML": 420.78200000245124,
"ParseHTML_avg": 2.963253521144023,
"ParseHTML_max": 318.4650000007823,
"ParseHTML_count": 142,
"RecalculateStyles": 105.08500000182539,
"RecalculateStyles_avg": 0.5937005649820644,
"RecalculateStyles_max": 27.915000000037253,
"RecalculateStyles_count": 177,
"Layout": 133.47499999776483,
"Layout_avg": 1.4830555555307203,
"Layout_max": 41.8179999999702,
"Layout_count": 90,
"EventDispatch": 589.1679999958724,
"EventDispatch_avg": 1.812824615371915,
"EventDispatch_max": 283.63100000005215,
"EventDispatch_count": 325,
"FunctionCall": 1250.5349999787286,
"FunctionCall_avg": 0.9762177985782424,
"FunctionCall_max": 283.51899999938905,
"FunctionCall_count": 1281,
"TimerFire": 405.9339999919757,
"TimerFire_avg": 0.9900829268096969,
"TimerFire_max": 58.355999999679625,
"TimerFire_count": 410,
"GCEvent": 70.74999999906868,
"GCEvent_avg": 7.861111111007631,
"GCEvent_max": 32.51300000026822,
"GCEvent_count": 9,
"FireAnimationFrame": 67.84199998434633,
"FireAnimationFrame_avg": 0.16751111107246008,
"FireAnimationFrame_max": 0.7209999999031425,
"FireAnimationFrame_count": 405,
"PaintSetup": 7.717000005766749,
"PaintSetup_avg": 0.11692424251161741,
"PaintSetup_max": 0.7190000005066395,
"PaintSetup_count": 66,
"Paint": 217.3209999995306,
"Paint_avg": 1.3013233532906026,
"Paint_max": 38.62199999950826,
"Paint_count": 167,
"DecodeImage": 12.86499999742955,
"DecodeImage_avg": 0.31378048774218414,
"DecodeImage_max": 1.894000000320375,
"DecodeImage_count": 41,
"CompositeLayers": 103.9480000063777,
"CompositeLayers_avg": 0.2541515892576472,
"CompositeLayers_max": 10.35800000000745,
"CompositeLayers_count": 409,
"XHRReadyStateChange": 91.92799999844283,
"XHRReadyStateChange_avg": 2.553555555512301,
"XHRReadyStateChange_max": 75.00499999988824,
"XHRReadyStateChange_count": 36,
"mean_frame_time": 17.56031067961447,
"jank": 75.3956754589968,
"mostly_smooth": 18.0550000006333,
"Layers": 7,
"PaintedArea_total": 17244083,
"PaintedArea_avg": 103257.98203592814,
"NodePerLayout_avg": 575.2888888888889,
"ExpensivePaints": 2,
"GCInsideAnimation": 0,
"ExpensiveEventHandlers": 3,
"_browserName": "chrome",
"_url": "http://amazon.com"
}]
================================================
FILE: test/res/test1.html
================================================
================================================
FILE: test/res/test2.html
================================================
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
DIV
================================================
FILE: test/seedData.js
================================================
module.exports = function(callback, count) {
count = count || 1000;
var path = require('path');
var sampleData = require('fs').readFileSync(path.join(__dirname, '/res/sample-perf-results.json'), 'utf8');
var browsers = ['firefox', 'chrome'],
components = ['component1', 'component2'],
commits = ['commit#1', 'commit#2', 'commit#3', 'commit#4', 'commit#5', 'commit#3'];
var couchData = require('./../lib/couchData'),
config = require('./util').config();
var rand = function(arr) {
return arr[Math.floor(Math.random() * arr.length)];
};
(function genData(i) {
config.name = rand(components);
config.time = 7 + Math.floor(Math.random() * 100 % 6);
config.run = commits[config.time - 7];
config.suite = 'Test Suite 1';
var data = JSON.parse(sampleData);
for (var key in data[0]) {
data[0][key] = data[0][key] * Math.random() * 3;
}
data[0]._browserName = rand(browsers);
couchData(config, data).then(function() {
if (i < count) {
genData(i + 1);
} else {
callback(true);
}
}, function() {
callback(false);
}).done();
}(0));
};
================================================
FILE: test/util.js
================================================
module.exports = {
config: function(config) {
config = config || {};
var options = {
"url": "http://localhost:9000/test1.html",
//"url": "https://axemclion.cloudant.com/",
"name": "Page - Test1",
"suite": 'Suite1',
"time": new Date().getTime(),
"run": 'commit#' + new Date().getMilliseconds(),
"browsers": ['chrome', 'firefox'],
"selenium": {
hostname: "localhost",
port: 4444
},
"log": config.log || require('bunyan').createLogger({
name: 'test',
src: true,
level: 'debug',
//stream: process.stdout,
streams: [{
path: 'test.log'
}]
}),
"couch": {
server: 'http://localhost:5984',
database: 'perfjankie-test',
updateSite: true,
onlyUpdateSite: false
}
};
var extend = function(options, config) {
for (var key in config) {
if (typeof options[key] === 'object' && typeof config[key] === 'object') {
options[key] = extend(options[key], config[key]);
} else {
options[key] = config[key];
}
}
return options;
};
return extend(options, config || {});
}
};
================================================
FILE: www/app/all-metrics/all-metrics.html
================================================
{{metric.name | formatMetric}}
{{metric.summary}}
================================================
FILE: www/app/all-metrics/all-metrics.less
================================================
.page.all-metrics {
.metric-names {
li {
margin: 30px 0 0 0;
.list-group-item{
background: #fefefe;
float: left;
padding: 10px;
border: SOLID 1px #ccc;
display: block;
width: 100%;
border-radius: 5px;
p{
height: 70px;
}
.browser-icons{
font-size: 1.1em;
color: #333;
span.disabled{
color: #ccc;
}
}
}
&.disabled{
opacity: 0.5;
.list-group-item{
background: #ccc;
cursor: not-allowed;
}
}
}
}
}
================================================
FILE: www/app/all-metrics/allmetrics.js
================================================
angular
.module('allmetrics', ['ngRoute', 'Backend', 'metricdetail'])
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.when('/all-metrics', {
templateUrl: 'app/all-metrics/all-metrics.html',
controller: 'AllMetricsCtrl',
controllerAs: 'metrics',
resolve: {
MetricNames: ['Data',
function(data, $routeParams) {
return data.getAllMetrics();
}
]
}
});
}
])
.controller('AllMetricsCtrl', ['$routeParams', 'MetricNames',
function($routeParams, MetricNames) {
this.metricNames = MetricNames;
}
])
.filter('metricFilter', ['$filter',
function($filter) {
return function(input, query) {
if (!query) {
return input;
}
var result = [];
var regex = new RegExp(query, 'i');
var filter = $filter('formatMetric');
for (var i = 0; i < input.length; i++) {
if (regex.test(filter(input[i].name))) {
result.push(input[i]);
}
}
return result;
};
}
]);
================================================
FILE: www/app/app.js
================================================
angular
.module('perfjankie', ['ngRoute', 'sidebar', 'pageSelect', 'summary', 'allmetrics'])
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.otherwise({
redirectTo: '/page-select'
});
}
])
.controller('MainPageCtrl', ['$scope', '$location', '$routeParams',
function($scope, $location, $routeParams) {
$scope.$on('$routeChangeSuccess', function(scope, next, current) {
$scope.pagename = $routeParams.pagename;
$scope.browser = $routeParams.browser;
$scope.pageLoading = false;
});
$scope.$on('$routeChangeStart', function() {
$scope.pageLoading = true;
});
$scope.$on('$routeChangeError', function(a, b, c, err) {
$scope.pageError = true;
$scope.pageLoading = false;
});
$scope.goHome = function() {
$location.url('/page-select');
window.document.location.reload();
};
if (window.location !== window.top.location) {
$scope.noPjBrand = true;
}
}
])
.filter('formatMetric', function() {
return function(input) {
input = input.replace(/_/g, " ").replace(/([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g, "$1$4 $2$3$5");
return input.toLowerCase().replace(/([^a-z]|^)([a-z])(?=[a-z]{2})/g, function(_, g1, g2) {
return g1 + g2.toUpperCase();
});
};
})
.filter('formatMetricValue', ['$filter',
function($filter) {
return function(value, unit) {
var fraction = 0;
if (unit === 'ms' || unit === 'fps') {
fraction = 2;
}
if (value > 1000) {
return $filter('number')(value / 1000, value > 100000 ? 0 : 2) + 'K';
} else {
return $filter('number')(value, fraction);
}
};
}
]);
================================================
FILE: www/app/backend.js
================================================
angular
.module('Backend', ['Endpoints'])
.factory('Data', ['Resource',
function(resource) {
return {
pagelist: function() {
return resource('/pagelist');
},
runList: function(opts) {
return resource('/runList', {
browser: opts.browser,
pagename: opts.pagename
});
},
runData: function(opts) {
return resource('/runData', {
browser: opts.browser,
pagename: opts.pagename,
time: opts.time
});
},
getAllMetrics: function() {
return resource('/all-metrics');
},
metricsData: function(opts) {
return resource('/metrics-data', {
browser: opts.browser,
pagename: opts.pagename,
metric: opts.metric,
limit: opts.limit
});
}
};
}
]);
================================================
FILE: www/app/font.less
================================================
@font-face {
font-family: 'fontello';
src: url('assets/fonts/fontello.eot?37370699');
src: url('assets/fonts/fontello.eot?37370699#iefix') format('embedded-opentype'),
url('assets/fonts/fontello.woff?37370699') format('woff'),
url('assets/fonts/fontello.ttf?37370699') format('truetype'),
url('assets/fonts/fontello.svg?37370699#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
================================================
FILE: www/app/main-page/error.less
================================================
.error{
text-align: center;
margin-top: 10%;
}
================================================
FILE: www/app/main-page/navbar.html
================================================
================================================
FILE: www/app/main-page/navbar.less
================================================
.navbar-brand {
padding: 0 15px;
.logo-icon {
font-size: 2.5em;
color: #aaa;
vertical-align: middle;
}
.logo {
vertical-align: middle;
font-weight: bold;
font-size: 1.3em;
&:after {
content: 'perfJankie';
position: relative;
top: -33px;
left: 69px;
display: block;
height: 10px;
overflow: hidden;
background: #f8f8f8;
}
}
}
================================================
FILE: www/app/main-page/no-pj-brand.less
================================================
body.no-pj-brand {
.navbar {
display: none !important;
}
.sidebar {
margin-top: 0px;
}
.content-container {
padding-top: 0px;
}
.pj-brand {
display: none !important;
}
}
================================================
FILE: www/app/main-page/sidebar.html
================================================
================================================
FILE: www/app/main-page/sidebar.js
================================================
angular
.module('sidebar', ['ngRoute'])
.controller('SideBarCtrl', ['$routeParams', '$scope',
function($routeParams, $scope) {
var self = this;
$scope.$on('$routeChangeSuccess', function(scope, next, current) {
var browser = $routeParams.browser;
self.categories = {
'Frame Rates': ['framesPerSec_raf', 'meanFrameTime_raf', 'droppedFrameCount'],
};
if (['chrome', 'safari', 'android'].indexOf(browser) !== -1) {
self.categories['Paint'] = ['Paint', 'Layout', 'RecalculateStyles', 'CompositeLayers'];
self.categories['Javascript'] = ['TimerInstall', 'TimerFire', 'EventDispatch', 'FunctionCall'];
self.categories['Frame Rates'].unshift('frames_per_sec', 'mean_frame_time');
}
if (browser !== 'safari') {
self.categories['Network'] = ['domReadyTime', 'loadTime', 'domainLookupTime', 'requestTime', 'loadEventTime'];
}
});
}
]);
================================================
FILE: www/app/main-page/sidebar.less
================================================
@media (min-width:992px) {
.sidebar {
position: fixed;
}
}
.sidebar {
user-select: none;
background: #f8f8f8;
border: SOLID 1px #e7e7e7;
top: 0;
bottom: 0;
left: 0;
margin-top: 50px;
padding: 0;
overflow-x: hidden;
.page-select {
padding: 10px;
h4 {
text-align: center;
font-size: 2.5em;
color: #ccc;
font-weight: bold;
}
}
.metadata {
padding: 10px 15px;
strong {
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
vertical-align: middle;
}
.browser {
color: #aaa;
vertical-align: middle;
font-size: 3em;
float: right;
margin: -0.6em -0.6em 0 0;
}
}
ul.nav {
border-top: 1px solid #e7e7e7;
li {
border-bottom: 1px solid #e7e7e7;
&.active {
background-color: #eee;
}
.sub-menu{
a{
margin-left: 20px;
}
}
.icon-Paint:before {&:extend(.icon-brush:before);}
.icon-Paint-sub:before{ &:extend(.icon-paintbucket:before);}
.icon-Content:before {&:extend(.icon-code:before);}
.icon-Content-sub:before {&:extend(.icon-file-code:before);}
.icon-Javascript:before {&:extend(.icon-cog-alt:before);}
.icon-Javascript-sub:before {&:extend(.icon-cog:before);}
.icon-Frame:before {&:extend(.icon-movie:before);}
.icon-Network:before, .icon-Network-sub:before {&:extend(.icon-signal:before);}
}
}
.help {
padding: 10px;
}
}
================================================
FILE: www/app/main.less
================================================
html, body {
width: 100%;
height: 100%;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: #fff;
.page-loading {
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -50px;
.spin-container {
font-size: 6em;
background: #fff;
box-shadow: 0 0 10px 0 #333;
border-radius: 20px;
}
}
a {
cursor: pointer;
}
.content-container {
padding-top: 50px;
min-height: 100%;
.row {
min-height: 100%;
}
.content {
min-height: 100%;
.page-details {
margin-top: 14px;
}
}
}
.graph {
display: block;
&>div.jqplot-target{
height: 100%;
}
.graph-error {
em {
font-size: 2em;
display: block;
margin-bottom: 0.5em;
font-style: normal;
}
padding-top: 2em;
display: block;
text-align: center;
font-size: 1.5em;
}
}
}
================================================
FILE: www/app/metric-details/metric-detail.html
================================================
Details:
{{metric.metadata.summary}}
Unit:
{{metric.metadata.unit}}
|
Source:
{{metric.metadata.source}}
|
Supported Browsers:
{{browser}}
,
{{metric.metadata.details}}
{{data}}
Could not plot graph
This could be either due to insufficient data or error in data.
================================================
FILE: www/app/metric-details/metric-detail.less
================================================
.page.metric-detail {
.metric-help {
background: #ccc;
border-radius: 50%;
font-size: 50%;
color: #fff;
padding: 2px 6px;
font-family: courier;
vertical-align: middle;
&:hover{
text-decoration: none;
}
}
.graph-modifiers {
margin: -20px 0 20px 0;
padding: 10px;
height: 3em;
input[type=radio] {
margin-left: -15px;
}
}
.explanation {
margin: -10px 0 20px 0;
border-bottom: SOLID #eee 1px;
em{
font-style: normal;
color: #777;
}
}
.jqplot-highlighter-tooltip{
height: 20px;
}
}
================================================
FILE: www/app/metric-details/metricDetail.js
================================================
angular
.module('metricdetail', ['ngRoute', 'metricsGraphDetails', 'Backend'])
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.when('/detail', {
templateUrl: 'app/metric-details/metric-detail.html',
controller: 'MetricDetailCtrl',
controllerAs: 'metric',
resolve: {
MetricsList: ['Data',
function(data) {
return data.getAllMetrics();
}
],
Data: ['Data', '$route',
function(Data, $route) {
$route.current.params.limit = $route.current.params.limit || 40;
$route.current.params.stat = $route.current.params.stat || '';
var params = $route.current.params;
return Data.metricsData({
browser: params.browser,
pagename: params.pagename,
metric: params.metric + params.stat,
limit: params.limit === 'all' ? undefined : params.limit
});
}
]
}
});
}
])
.controller('MetricDetailCtrl', ['$routeParams', '$scope', '$location', 'Data', 'MetricsList',
function($routeParams, $scope, $location, data, metricsList) {
this.name = $routeParams.metric;
this.data = data;
for (var i = 0; i < metricsList.length; i++){
if (metricsList[i].name === this.name){
this.metadata = metricsList[i];
break;
}
}
$scope.modifier = {
limit: $routeParams.limit,
stat: $routeParams.stat
};
var pos = $('.graph').position();
// Sets height of graph with a minimum of 500px
this.height = Math.max(window.innerHeight - pos.top - 100, 500);
$scope.$watchCollection('modifier', function(val, old, scope) {
if ($routeParams.stat !== val.stat) {
$location.search('stat', val.stat);
} else if ($routeParams.limit !== val.limit) {
$location.search('limit', val.limit);
}
});
}
]);
================================================
FILE: www/app/metric-details/metricDetailsGraph.js
================================================
angular
.module('metricsGraphDetails', [])
.directive('pjMetricsDetailsGraph', function() {
function prepareData(val) {
var result = {
series: [],
max: [],
min: [],
xaxis: {}
};
for (var i = val.length - 1; i >= 0; i--) {
var p = val[i];
result.series.push([p.key, p.value.sum / p.value.count, p.value.min, p.value.max]);
result.min.push([p.key, p.value.min]);
result.max.push([p.key, p.value.max]);
result.xaxis[p.key] = p.label;
}
return result;
}
function drawGraph(el, data, unit) {
$.jqplot(el, [data.series, data.min, data.max], {
fillBetween: {
series1: 1,
series2: 2,
color: "rgba(67, 142, 185, 0.2)",
baseSeries: 0,
fill: true
},
series: [{
show: true,
shadow: false,
breakOnNull: true,
rendererOptions: {
smooth: false,
},
trendline: {
show: true,
shadow: false,
color: '#666',
lineWidth: 2,
linePattern: 'dashed',
label: 'trend'
}
}],
seriesDefaults: {
show: false,
rendererOptions: {
smooth: true
}
},
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
label: 'Runs',
labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
rendererOptions: {
sortMergedLabels: false
},
tickOptions: {
mark: 'cross',
showMark: true,
showGridline: true,
markSize: 5,
angle: -90,
show: true,
showLabel: true,
formatter: function(formatString, value) {
return data.xaxis[value];
}
},
showTicks: true, // wether or not to show the tick labels,
showTickMarks: true,
},
yaxis: {
tickOptions: {},
rendererOptions: {
forceTickAt0: false
},
label: unit || 'Y AXIS',
labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
tickRenderer: $.jqplot.CanvasAxisTickRenderer
}
},
grid: {
shadow: false,
borderWidth: 0
},
highlighter: {
show: true,
showLabel: true,
tooltipAxes: 'y',
sizeAdjust: 7.5,
tooltipLocation: 'ne'
}
});
}
var id = 'metricDetails' + Math.floor(Math.random() * 10000);
function link(scope, element, attrs) {
scope.$watch('data', function(val) {
if (val) {
try {
drawGraph(id, prepareData(val), scope.unit);
} catch (e) {
scope.error = e;
}
}
});
}
return {
link: link,
restrict: 'E',
transclude: true,
scope: {
data: "=",
unit: "="
},
template: '
'
};
});
================================================
FILE: www/app/page-select/page-select.html
================================================
Select a test case to view metrics
================================================
FILE: www/app/page-select/page-select.less
================================================
.page.page-select{
.suites{
.panel{
.list-group{
.list-group-item{
padding: 0;
a{
display: block;
padding: 10px 15px;
&:hover{
text-decoration: none;
background: #ccc;
}
}
}
}
}
}
}
================================================
FILE: www/app/page-select/pageSelect.js
================================================
angular
.module('pageSelect', ['ngRoute', 'Backend'])
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.when('/page-select', {
templateUrl: 'app/page-select/page-select.html',
controller: 'PageSelectCtrl',
controllerAs: 'pageselect',
resolve: {
PageList: ['Data',
function(data) {
return data.pagelist();
}
]
}
});
}
])
.controller('PageSelectCtrl', ['PageList',
function(PageList) {
this.pagelist = PageList;
}
]);
================================================
FILE: www/app/summary/networkTimingGraph.js
================================================
angular
.module('networkTiming', [])
.directive('pjNetworkTimingGraph', function() {
var ticks = ['onLoad', 'Processing', 'Response', 'Request', 'TCP', 'DNS', 'AppCache', 'Unload', 'Start/Redirect'];
var events = [
['loadEventStart', 'loadEventEnd'],
['domLoading', 'domComplete'],
['responseStart', 'responseEnd'],
['requestStart', 'responseStart'],
['connectStart', 'connectEnd'],
['domainLookupStart', 'domainLookupEnd'],
['fetchStart', 'domainLookupStart'],
['unloadStart', 'unloadEnd'],
['redirectStart', 'redirectStop'],
];
function prepareData(val) {
var series = [
[],
[]
];
var initial = val['navigationStart'].sum / val['navigationStart'].count;
var prev = initial;
for (var i = 0; i < events.length; i++) {
var start = val[events[i][0]];
var end = val[events[i][1]];
if (start && start.sum > 0) {
start = start.sum / start.count;
} else {
start = (i > 0 ? series[0][i - 1] + initial : initial);
}
if (end && end.sum > 0) {
end = end.sum / end.count;
} else {
end = start;
}
series[0].push(start - initial);
series[1].push(end - initial);
}
return series;
}
function drawGraph(el, series) {
$.jqplot(el, series, {
stackSeries: true,
seriesDefaults: {
renderer: $.jqplot.BarRenderer,
rendererOptions: {
barDirection: 'horizontal',
barPadding: 0,
barMargin: 0,
shadowDepth: 0,
stacked: true
}
},
series: [{
color: 'rgba(0,0,0,0)'
}],
axes: {
yaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks,
tickRenderer: $.jqplot.CanvasAxisTickRenderer
},
},
grid: {
shadow: false,
borderWidth: 0
},
});
}
var id = 'networkTimings' + Math.floor(Math.random() * 10000);
function link(scope, element, attrs) {
scope.$watch('data', function(val) {
if (val && !angular.equals(val, {})) {
drawGraph(id, prepareData(val));
}
});
}
return {
restrict: 'E',
transclude: true,
scope: {
data: "="
},
link: link,
template: '
'
};
});
================================================
FILE: www/app/summary/paintCycleGraph.js
================================================
angular
.module('paintCycleGraph', [])
.directive('pjPaintCycleGraph', function() {
var id = 'paintCycle' + Math.floor(Math.random() * 10000);
function prepareData(data) {
var paints = [];
angular.forEach(['Layout', 'CompositeLayers', 'Paint', 'RecalculateStyles'], function(key) {
paints.push([key, data[key].sum]);
});
return paints;
}
function drawGraph(el, data) {
$.jqplot(el, [data], {
seriesColors: ['#7AA9E5', '#EFC453', '#9A7EE6', '#71B363'],
seriesDefaults: {
renderer: $.jqplot.PieRenderer,
rendererOptions: {
showDataLabels: true,
dataLabels: ['value'],
dataLabelFormatString: '%d ms',
highlightMouseOver: true
}
},
grid: {
shadow: false,
borderWidth: 0
},
legend: {
show: true,
location: 'e'
}
});
}
function link(scope, element, attrs) {
scope.$watch('data', function(val) {
if (val && !angular.equals(val, {})) {
try {
drawGraph(id, prepareData(val));
} catch (e) {
scope.error = e;
}
}
});
}
return {
link: link,
restrict: 'E',
transclude: true,
scope: {
data: "="
},
template: '
'
};
});
================================================
FILE: www/app/summary/summary.html
================================================
Frame Rate Trend
(higher is better)
Could not plot graph due to insufficient data
Test runs
Deploy/Run Tag
# times run
Paint Cycle
for selected run
Could not plot graph. This could be due to insufficient data
================================================
FILE: www/app/summary/summary.js
================================================
angular
.module('summary', ['ngRoute', 'paintCycleGraph', 'networkTiming', 'Backend'])
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.when('/summary', {
templateUrl: 'app/summary/summary.html',
controller: 'SummaryCtrl',
controllerAs: 'summary',
resolve: {
runList: ['Data', '$route',
function(data, $route) {
var res = {};
var params = $route.current.params;
return data.runList({
browser: params.browser,
pagename: params.pagename
});
}
],
}
});
}
])
.controller('SummaryCtrl', ['$routeParams', '$location', 'runList', 'Data',
function($routeParams, $location, runList, Data) {
this.time = $routeParams.time || runList[0].time;
this.runList = runList;
var self = this;
this.tiles = [];
this.currentRunData = {};
var metric = 'framesPerSec_raf';
Data.runData({
browser: $routeParams.browser,
pagename: $routeParams.pagename,
time: this.time
}).then(function(data) {
self.currentRunData = data;
Data.metricsData({
browser: $routeParams.browser,
pagename: $routeParams.pagename,
metric: data['frames_per_sec'] ? 'frames_per_sec' : 'framesPerSec_raf',
limit: 20
}).then(function(data) {
self.frameRateData = data;
});
});
}
]);
================================================
FILE: www/app/summary/summary.less
================================================
.summary{
.test-runs{
.list-group-item{
border-radius: 0 !important;
border-left: none;
border-right: none;
&.heading{
border-bottom: SOLID 1px gray;
}
}
.content{
height: 490px;
overflow: scroll;
}
}
}
================================================
FILE: www/app/summary/tiles.js
================================================
angular
.module('summaryTiles', ['Backend'])
.directive('pjSummaryTiles', ['Data', '$q',
function(data, $q) {
var _metricsList;
var metricsList = function() {
if (_metricsList) {
return $q.when(_metricsList);
} else {
return data.getAllMetrics().then(function(result) {
return _metricsList = result;
});
}
};
var prepareData = function(val) {
var tiles = [];
return metricsList().then(function(metricsList) {
function getMetricUnit(metric) {
for (var i = 0; i < metricsList.length; i++) {
if (metric === metricsList[i].name) {
return metricsList[i].unit;
}
return '';
}
}
angular.forEach(['frames_per_sec', 'framesPerSec_raf', 'firstPaint', 'ExpensivePaints', 'NodePerLayout_avg', 'ExpensiveEventHandlers', ], function(metric) {
if (typeof val[metric] === 'object') {
tiles.push({
metric: metric,
unit: getMetricUnit(metric),
value: val[metric].sum / val[metric].count,
link: metric
});
}
});
return tiles;
});
};
return {
restrict: 'E',
transclude: true,
scope: {
data: "=",
pagename: "=",
browser: "="
},
link: function(scope, element, attrs) {
scope.$watch('data', function(val) {
if (!val) {
return;
}
prepareData(val).then(function(res) {
scope.tiles = res.slice(0, 4);
});
});
},
templateUrl: 'app/summary/tiles.tpl.html'
};
}
]);
================================================
FILE: www/app/summary/tiles.less
================================================
.tiles {
.panel {
&.panel-0 {
background: #428bca;
border-color: #428bca;
a {
color: #2a6496;
}
}
&.panel-1 {
background: #5cb85c;
border-color: #5cb85c;
a {
color: #5cb85c;
}
}
&.panel-2 {
background: #f0ad4e;
border-color: #f0ad4e;
a {
color: #f0ad4e;
}
}
&.panel-3 {
background: #d9534f;
border-color: #d9534f;
a {
color: #d9534f;
}
}
.panel-heading {
color: white;
.huge {
font-size: 3em;
}
.unit {
margin-top: -3px;
}
.icon {
margin-left: -10px;
font-size: 4em;
&.icon-frames_per_sec:before {
content: '\e815';
}
&.icon-ExpensivePaints:before{
content: '\e804';
}
&.icon-ExpensiveEventHandlers:before{
content: '\e80b';
}
&.icon-NodePerLayout_avg:before{
content: '\e808';
}
}
}
}
}
================================================
FILE: www/app/summary/tiles.tpl.html
================================================
{{tile.value | formatMetricValue: tile.unit}}
{{tile.unit}}
================================================
FILE: www/assets/css/animation.css
================================================
/*
Animation example, for spinners
*/
.animate-spin {
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
display: inline-block;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
================================================
FILE: www/assets/css/config.json
================================================
{
"name": "",
"css_prefix_text": "icon-",
"css_use_suffix": false,
"hinting": true,
"units_per_em": 1000,
"ascent": 850,
"glyphs": [
{
"uid": "5156114528976f4ffab0f04526d12e89",
"css": "safari",
"code": 59394,
"src": "custom_icons",
"selected": true,
"svg": {
"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",
"width": 908.3969465648854
},
"search": [
"glyph"
]
},
{
"uid": "d35c5d63e9d9056d3ba81c78c5a7fe58",
"css": "paintbucket",
"code": 59406,
"src": "custom_icons",
"selected": true,
"svg": {
"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",
"width": 857.421875
},
"search": [
"glyph"
]
},
{
"uid": "5d2d07f112b8de19f2c0dbfec3e42c05",
"css": "spin5",
"code": 9676,
"src": "fontelico"
},
{
"uid": "62c089cb34e74b3a1200bc7f5314eb4e",
"css": "firefox",
"code": 59392,
"src": "fontelico"
},
{
"uid": "9c2b737b16ae2c8d66b7bfd29ba5ecd8",
"css": "chrome",
"code": 59393,
"src": "fontelico"
},
{
"uid": "2a46f1d1c9bd036e17a74e46613c1636",
"css": "ie",
"code": 59405,
"src": "fontelico"
},
{
"uid": "7034e4d22866af82bef811f52fb1ba46",
"css": "code",
"code": 59408,
"src": "fontawesome"
},
{
"uid": "c76b7947c957c9b78b11741173c8349b",
"css": "attention",
"code": 59409,
"src": "fontawesome"
},
{
"uid": "26613a2e6bc41593c54bead46f8c8ee3",
"css": "file-code",
"code": 59398,
"src": "fontawesome"
},
{
"uid": "e99461abfef3923546da8d745372c995",
"css": "cog",
"code": 59407,
"src": "fontawesome"
},
{
"uid": "98687378abd1faf8f6af97c254eb6cd6",
"css": "cog-alt",
"code": 59403,
"src": "fontawesome"
},
{
"uid": "531bc468eecbb8867d822f1c11f1e039",
"css": "calendar",
"code": 59412,
"src": "fontawesome"
},
{
"uid": "4109c474ff99cad28fd5a2c38af2ec6f",
"css": "filter",
"code": 59404,
"src": "fontawesome"
},
{
"uid": "347c38a8b96a509270fdcabc951e7571",
"css": "database",
"code": 59401,
"src": "fontawesome"
},
{
"uid": "5e0a374728ffa8d0ae1f331a8f648231",
"css": "github",
"code": 59399,
"src": "fontawesome"
},
{
"uid": "1d2a6c3d9236b88b0f185c7c4530fa52",
"css": "flag",
"code": 59410,
"src": "entypo"
},
{
"uid": "84a7262985600b683bbab0da9298776d",
"css": "signal",
"code": 59397,
"src": "entypo"
},
{
"uid": "8a1d446e5555e76f82ddb1c8b526f579",
"css": "flow-tree",
"code": 59400,
"src": "entypo"
},
{
"uid": "3a6f0140c3a390bdb203f56d1bfdefcb",
"css": "gauge",
"code": 59402,
"src": "entypo"
},
{
"uid": "ef8560a06ed46a192092bf1f08c142a6",
"css": "sort-alphabet-outline",
"code": 59395,
"src": "typicons"
},
{
"uid": "b3a9e2dab4d19ea3b2f628242c926bfe",
"css": "brush",
"code": 59396,
"src": "iconic"
},
{
"uid": "d2c499942f8a7c037d5a94f123eeb478",
"css": "layers",
"code": 59411,
"src": "iconic"
},
{
"uid": "eea613bc40c77b7eab137b29dba0c62f",
"css": "movie",
"code": 59413,
"src": "mfglabs"
},
{
"uid": "aaf371ab44841e9aaffebd179d324ce4",
"css": "android",
"code": 59414,
"src": "zocial"
}
]
}
================================================
FILE: www/assets/css/fontello-codes.css
================================================
.icon-spin5:before { content: '\25cc'; } /* '◌' */
.icon-firefox:before { content: '\e800'; } /* '' */
.icon-chrome:before { content: '\e801'; } /* '' */
.icon-safari:before { content: '\e802'; } /* '' */
.icon-sort-alphabet-outline:before { content: '\e803'; } /* '' */
.icon-brush:before { content: '\e804'; } /* '' */
.icon-signal:before { content: '\e805'; } /* '' */
.icon-file-code:before { content: '\e806'; } /* '' */
.icon-github:before { content: '\e807'; } /* '' */
.icon-flow-tree:before { content: '\e808'; } /* '' */
.icon-database:before { content: '\e809'; } /* '' */
.icon-gauge:before { content: '\e80a'; } /* '' */
.icon-cog-alt:before { content: '\e80b'; } /* '' */
.icon-filter:before { content: '\e80c'; } /* '' */
.icon-ie:before { content: '\e80d'; } /* '' */
.icon-paintbucket:before { content: '\e80e'; } /* '' */
.icon-cog:before { content: '\e80f'; } /* '' */
.icon-code:before { content: '\e810'; } /* '' */
.icon-attention:before { content: '\e811'; } /* '' */
.icon-flag:before { content: '\e812'; } /* '' */
.icon-layers:before { content: '\e813'; } /* '' */
.icon-calendar:before { content: '\e814'; } /* '' */
.icon-movie:before { content: '\e815'; } /* '' */
.icon-android:before { content: '\e816'; } /* '' */
================================================
FILE: www/assets/fonts/fontello-codes.css
================================================
.icon-firefox:before { content: '\e800'; } /* '' */
.icon-chrome:before { content: '\e801'; } /* '' */
.icon-safari:before { content: '\e802'; } /* '' */
.icon-sort-alphabet-outline:before { content: '\e803'; } /* '' */
.icon-brush:before { content: '\e804'; } /* '' */
.icon-signal:before { content: '\e805'; } /* '' */
.icon-file-code:before { content: '\e806'; } /* '' */
.icon-github:before { content: '\e807'; } /* '' */
.icon-spin5:before { content: '\e808'; } /* '' */
.icon-database:before { content: '\e809'; } /* '' */
.icon-gauge:before { content: '\e80a'; } /* '' */
.icon-cog-alt:before { content: '\e80b'; } /* '' */
.icon-filter:before { content: '\e80c'; } /* '' */
.icon-ie:before { content: '\e80d'; } /* '' */
.icon-paintbucket:before { content: '\e80e'; } /* '' */
.icon-cog:before { content: '\e80f'; } /* '' */
.icon-code:before { content: '\e810'; } /* '' */
================================================
FILE: www/index.html
================================================
PerfJankie - Rendering Performance Analysis
Error loading page
An error occured when trying to load this page.
Please
refresh
this page,
or go back to the
home page
.
================================================
FILE: www/server/endpoints.js
================================================
if (typeof window.DB_BASE !== 'string') {
window.DB_BASE = '..';
}
angular
.module('Endpoints', [])
.factory('Resource', ['$http', '$q',
function($http, $q) {
function rowsToObj(row, keys, val) {
var res = {};
res[val] = row.value;
angular.forEach(keys, function(key, i) {
res[key] = row.key[i];
});
return res;
}
var server = {
'/pagelist': function() {
return $http.get(window.DB_BASE + '/pagelist/_view/pages?group=true').then(function(resp) {
var result = {};
angular.forEach(resp.data.rows, function(row) {
var res = rowsToObj(row, ['suite', 'pagename', 'browser'], 'runCount');
result[res.suite] = result[res.suite] || {};
result[res.suite][res.pagename] = result[res.suite][res.pagename] || [];
result[res.suite][res.pagename].push({
browser: res.browser,
runCount: res.runCount
});
});
return result;
});
},
'/all-metrics': function() {
return $q.when(window.METRICS_LIST);
},
'/runList': function(opts) {
return $http.get(window.DB_BASE + '/runs/_view/list', {
params: {
endkey: JSON.stringify([opts.browser, opts.pagename, null]),
startkey: JSON.stringify([opts.browser, opts.pagename, {}]),
group: true,
descending: true
}
}).then(function(resp) {
var res = [];
angular.forEach(resp.data.rows, function(row) {
res.push(rowsToObj(row, ['browser', 'pagename', 'time', 'run'], 'runCount'));
});
return res;
});
},
'/runData': function(opts) {
return $http.get(window.DB_BASE + '/runs/_view/data', {
params: {
startkey: JSON.stringify([opts.browser, opts.pagename, opts.time, null]),
endkey: JSON.stringify([opts.browser, opts.pagename, opts.time, {}]),
group: true
}
}).then(function(resp) {
var res = {};
angular.forEach(resp.data.rows, function(row) {
var obj = rowsToObj(row, ['browser', 'pagename', 'time', 'run', 'metric'], 'value');
res[obj.metric] = obj.value;
});
return res;
});
},
'/metrics-data': function(opts) {
var config = {
params: {
endkey: JSON.stringify([opts.browser, opts.pagename, opts.metric, null]),
startkey: JSON.stringify([opts.browser, opts.pagename, opts.metric, {}]),
group: true,
descending: true
}
};
var limit = parseInt(opts.limit, 10);
if (!isNaN(limit)) {
config.params.limit = limit;
}
return $http.get(window.DB_BASE + '/metrics_data/_view/stats', config).then(function(resp) {
var res = [];
angular.forEach(resp.data.rows, function(obj, index) {
obj.label = obj.key[4];
obj.key = obj.key[3];
res.push(obj);
});
return res;
});
}
};
var fetch = function(url, params) {
return server[url](params);
};
return fetch;
}
]);