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 `<head>` section
```html
<script type="text/javascript">window.DB_BASE="http://couchdb.server.url/databasename/_design";</script>
```
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 <code@r.nparashuram.com>"
],
"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 <configFile>', 'Specify a configuration file. If other options are specified, they have precedence over options in config file')
.option('-s, --selenium <serverURL>', 'Specify Selenium Server, like localhost:4444 or ondemand.saucelabs.com:80', 'localhost:4444')
.option('-u --username <username>', 'Sauce, BrowserStack or Selenium User Name')
.option('-a --accesskey <accesskey>', 'Sauce, BrowserStack or Selenium Access Key')
.option('--browsers <browsers>', 'List of browsers to run the tests on')
.option('--couch-server <couchServer>', 'Location of the couchDB server')
.option('--couch-database <couchDatabase>', 'Name of the couch database')
.option('--couch-user <couchUser>', 'Username of the couch user that can create design documents and save data')
.option('--couch-pwd <couchPwd>', 'Password of the couchDB user')
.option('--name <name>', 'A friendly name for the URL. This is shown as component name in the dashboard')
.option('--run <run>', 'A hash for the commit, or any identifier displayed in the x-axis in the dashboard')
.option('--time <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 <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 <newDbName>', '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>', 'Username of the couch user that can create design documents and save data')
.option('-p --password <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 <code@nparashuram.com>",
"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
================================================
<!doctype html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
var css = {
'width': '300px',
'height': '300px',
'margin': '10px',
'borderRadius': '400px',
'background': 'radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.15) 30%, rgba(255,255,255,.3) 32%, rgba(255,255,255,0) 33%) 0 0, radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.1) 11%, rgba(255,255,255,.3) 13%, rgba(255,255,255,0) 14%) 0 0, radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.2) 17%, rgba(255,255,255,.43) 19%, rgba(255,255,255,0) 20%) 0 110px, radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.2) 11%, rgba(255,255,255,.4) 13%, rgba(255,255,255,0) 14%) -130px -170px, radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.2) 11%, rgba(255,255,255,.4) 13%, rgba(255,255,255,0) 14%) 130px 370px, radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.1) 11%, rgba(255,255,255,.2) 13%, rgba(255,255,255,0) 14%) 0 0, linear-gradient(45deg, #343702 0%, #184500 20%, #187546 30%, #006782 40%, #0b1284 50%, #760ea1 60%, #83096e 70%, #840b2a 80%, #b13e12 90%, #e27412 100%)',
'backgroundSize': '470px 470px, 970px 970px, 410px 410px, 610px 610px, 530px 530px, 730px 730px, 100% 100%',
'backgroundColor': '#840b2a',
'box-shadow': '0 0 20px 20px rgb(100, 100, 100, 0.4)'
};
for (var i = 0; i < 10; i++) {
var x = document.createElement('div');
for (var key in css) {
x.style[key] = css[key];
}
document.body.appendChild(x);
}
// Testing if Chrome was loaded with the required flags
if (window.chrome) {
console.log('Extension:' + typeof window.chrome.gpuBenchmarking);
}
window.addEventListener('MozAfterPaint', function() {
console.log('Extension:true');
}, true);
</script>
</body>
</html>
================================================
FILE: test/res/test2.html
================================================
<!doctype html>
<html>
<head>
</head>
<body>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
<div style = "font-size: 40px">DIV</div>
</body>
</html>
================================================
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
================================================
<div class="page all-metrics">
<h1 class = "page-header">
All Metrics
<small class="page-details pull-right">
{{pagename}} on
<em title="{{browser}}">{{browser}}</em>
<a class="btn btn-xs btn-primary" href="#/page-select" title="Select a different page or browser">change</a>
</small>
</h1>
<div>
<div class="input-group input-group-lg col-md-4">
<input type="text" class="form-control" ng-model="query" placeholder="Filter Metrics"/>
<span class="input-group-addon icon-filter"></span>
</div>
<ul class="list-unstyled metric-names row">
<li ng-repeat="metric in metrics.metricNames | orderBy:'-importance' | metricFilter:query" class="col-xs-12 col-sm-6 col-md-4" ng-class="{'disabled': metric.browsers.indexOf(browser) === -1}">
<div class="list-group-item">
<h4 class="list-group-item-heading">{{metric.name | formatMetric}}</h4>
<p class="list-group-item-text">{{metric.summary}}</p>
<div class="pull-left browser-icons">
<span class="icon-{{browser}}" title="{{browser}}" ng-class="{'disabled': metric.browsers.indexOf(browser) === -1}" ng-repeat="browser in ['chrome', 'android', 'safari', 'ie', 'firefox']"></span>
</div>
<div class="pull-right">
<a class="btn btn-xs btn-info" ng-href="#/detail?pagename={{pagename}}&browser={{browser}}&metric={{metric.name}}">
View Graph »
</a>
</div>
<div class="clearfix"></div>
</div>
</li>
</ul>
</div>
</div>
================================================
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
================================================
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#/">
<span class="logo-icon icon-gauge"></span>
<span class="logo">perfJankie</span>
</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="/_utils">
<i class="icon-database"></i>
Database
</a>
</li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-github"></i>
Source Code <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a href="http://github.com/axemclion/browser-perf" target="_blank">browser-perf</a>
</li>
<li>
<a href="http://github.com/axemclion/perfjankie" target="_blank">perfJankie</a>
</li>
<li class="divider"></li>
<li>
<a href="http://nparashuram.com/contact.html" target="_blank">Contact</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
================================================
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
================================================
<div class="sidebar col-md-2 nav" ng-controller="SideBarCtrl as sidebar" ng-cloak>
<div ng-if="!pagename || !browser" class="page-select">
<h4>Select a test case to view metrics</h4>
<hr/>
</div>
<div ng-if="pagename && browser">
<div class="metadata">
<span class="browser icon-{{browser}}" title="Metrics collected on {{browser}}"></span>
<h4> <strong title="{{pagename}}">{{pagename}}</strong>
</h4>
<a href = "#/page" class="btn btn-xs btn-primary">select</a>
</div>
<ul class="nav">
<li>
<a href="#/summary?pagename={{pagename}}&browser={{browser}}">
<span class = "icon-gauge"></span>
Summary
</a>
</li>
<li ng-repeat = "(category, values) in sidebar.categories">
<a ng-click="subMenu=(subMenu===category?'':category)">
<span class = "icon-{{category}}"></span>
{{category}}
<span class="pull-right" ng-if="subMenu!==category">+</span>
<span class="pull-right" ng-if="subMenu===category">-</span>
</a>
<ul class="nav sub-menu" ng-show="subMenu===category">
<li ng-repeat="metric in values">
<a ng-href="#/detail?pagename={{pagename}}&browser={{browser}}&metric={{metric}}">
<span class="icon-{{category}}-sub"></span>
{{metric | formatMetric}}
</a>
</li>
</ul>
</li>
<li>
<a href="#/all-metrics?pagename={{pagename}}&browser={{browser}}">
<span class = "icon-sort-alphabet-outline"></span>
All metrics
</a>
</li>
</ul>
</div>
<div class="help pj-brand">
<!-- TODO Add help text -->
</div>
</div>
================================================
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
================================================
<div class="page metric-detail">
<h1 class = "page-header" ng-cloak>
{{metric.name | formatMetric}}
<a class="metric-help" title="{{metric.summary}}" ng-click="showHelp=!showHelp">
<span ng-hide="showHelp">?</span>
<span ng-show="showHelp">-</span>
</a>
<small class="page-details pull-right">
{{pagename}} (
<span title="{{browser}}" class="icon-{{browser}}"></span>
)
<a class="btn btn-xs btn-primary" href="#/page-select" title="Select a different page or browser">change</a>
</small>
</h1>
<div class="explanation" ng-show="showHelp">
<p class="metric-summary"> <em>Details:</em>
{{metric.metadata.summary}}
<br/> <em>Unit:</em>
{{metric.metadata.unit}}
|
<em>Source:</em>
{{metric.metadata.source}}
|
<em>Supported Browsers:</em>
<span ng-repeat="browser in metric.metadata.browsers">
{{browser}}
<span ng-if="!$last">,</span>
</span>
</p>
<p class="metric-details">{{metric.metadata.details}}</p>
</div>
<div class="graph-modifiers" ng-cloaak>
<div ng-show="1 < metric.stats.length " class="pull-left"> <strong>Stat Type :</strong>
<label class="radio-inline">
<input type="radio" name="stat" value="" ng-model="modifier.stat"/>
total
</label>
<label class="radio-inline" ng-repeat="statval in metric.stats" >
<input type="radio" name="stat" value="_{{statval}}" ng-model="modifier.stat"/>
{{statval}}
</label>
</div>
<div class="pull-right"> <strong>Show last :</strong>
<label class="radio-inline" ng-repeat="limit in [10,20,40,'all']" >
<input type="radio" name="limit" value="{{limit}}" ng-model="modifier.limit"/>
{{limit}}
</label>
</div>
</div>
<pj-metrics-details-graph class="clearfix graph" style="height:{{metric.height}}px" data="metric.data" unit="metric.metadata.unit">
<span class="graph-error">
<h1>
{{data}}
<span class="icon-attention"></span>
Could not plot graph
</h1>
This could be either due to insufficient data or error in data.
</span>
</pj-metrics-details-graph>
</div>
================================================
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: '<div ng-hide="error" id="' + id + '"></div><div ng-transclude ng-show="error"></div>'
};
});
================================================
FILE: www/app/page-select/page-select.html
================================================
<div class="page page-select">
<h1 class="page-header">Test Cases</h1>
<p class="lead">Select a test case to view metrics</p>
<ul class="suites list-unstyled">
<li ng-repeat="(suite, pages) in pageselect.pagelist">
<h3>
<span class="icon-folder-open"></span>
{{suite}}
</h3>
<div class="row">
<div class = "col-md-4" ng-repeat="(page, browsers) in pages">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<span class="icon-calendar"></span> <strong>{{page}}</strong>
<small class="pull-right"># of tests</small>
</h3>
</div>
<ul class="list-group">
<li class="list-group-item" ng-repeat="val in browsers">
<a ng-href="#/summary?pagename={{page}}&browser={{val.browser}}">
<span class="icon-{{val.browser}}"></span>
{{val.browser}}
<span class="badge pull-right" title="Number of commits/deploys">{{val.runCount}}</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</li>
</ul>
</div>
================================================
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: '<div ng-hide="error" id="' + id + '"></div>'
};
});
================================================
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: '<div ng-hide="error" id="' + id + '"></div><div ng-transclude ng-show="error"></div>'
};
});
================================================
FILE: www/app/summary/summary.html
================================================
<div class="page summary">
<h1 class = "page-header">
<small>Summary of</small>
{{pagename}}
<small>on {{browser}}</small>
<small class="page-details pull-right">
<a class="btn btn-xs btn-primary" href="#/page-select" title="Select a different page or browser">change</a>
</small>
</h1>
<div class="row">
<div class="col-lg-7 col-sm-12">
<div class="panel panel-default">
<div class="panel-heading clearfix">
<span class="icon-movie"></span>
Frame Rate Trend
<small class="pull-right">(higher is better)</small>
</div>
<div class="panel-body">
<pj-metrics-details-graph class="clearfix graph" style="height:500px" data="summary.frameRateData" unit="'frames per second'">
<span class="graph-error">
<span class="icon-attention"></span>
Could not plot graph due to insufficient data
</span>
</pj-metrics-details-graph>
</div>
</div>
</div>
<div class="col-lg-5 col-sm-12">
<div class="panel panel-default test-runs">
<div class="panel-heading">
<span class="icon-flag"></span>
Test runs
</div>
<div class="list-group-item list-group-item-warning">
<strong>Deploy/Run Tag</strong>
<span class="pull-right"># times run</span>
</div>
<div class="content">
<a ng-repeat="run in summary.runList" href="#/summary?pagename={{run.pagename}}&browser={{run.browser}}&time={{run.time}}" class="list-group-item" ng-class="{active:run.time===summary.time}" >
{{run.run}}
<span class="badge">{{run.runCount}}</span>
</a>
</div>
</div>
</div>
</div>
<!--div class = "tiles">
<div class="col-md-3 col-sm-6" ng-repeat="tile in summary.tiles">
<div class="panel" ng-class="'panel-' + $index">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class = "icon" ng-class="'icon-' + tile.metric"></span>
</div>
<div class="col-xs-9 text-right">
<div class="huge">{{tile.value | formatMetricValue: tile.unit}}</div>
<div class="unit">{{tile.unit}}</div>
</div>
</div>
</div>
<a ng-href="#/detail?pagename={{pagename}}&browser={{browser}}&metric={{tile.metric}}">
<div class="panel-footer">
<span class="pull-left">{{tile.metric | formatMetric}}</span>
<span class="pull-right">»</span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
</div-->
<div class="row">
<div class="col-lg-5 col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
<span class="icon-paintbucket"></span>
Paint Cycle
<small> <em>for selected run</em>
</small>
</div>
<pj-paint-cycle-graph data="summary.currentRunData">
<span class="graph-error">
<span class="icon-attention"></span>
Could not plot graph. This could be due to insufficient data
</span>
</pj-paint-cycle-graph>
</div>
</div>
<div class="col-lg-7 col-sm-12">
<div class="panel panel-default">
<div class="panel-heading">
<span class="icon-signal"></span>
Network
<small> <em>for selected run</em>
</small>
</div>
<div class="panel-body">
<pj-network-timing-graph class="network-timing" data="summary.currentRunData"></pj-network-timing-graph>
</div>
</div>
</div>
</div>
</div>
================================================
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
================================================
<div class="col-md-3 col-sm-6" ng-repeat="tile in tiles">
<div class="panel" ng-class="'panel-' + $index">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class = "icon" ng-class="'icon-' + tile.metric"></span>
</div>
<div class="col-xs-9 text-right">
<div class="huge">{{tile.value | formatMetricValue: tile.unit}}</div>
<div class="unit">{{tile.unit}}</div>
</div>
</div>
</div>
<a ng-href="#/detail?pagename={{pagename}}&browser={{browser}}&metric={{tile.metric}}">
<div class="panel-footer">
<span class="pull-left">{{tile.metric | formatMetric}}</span>
<span class="pull-right">»</span>
<div class="clearfix"></div>
</div>
</a>
</div>
</div>
================================================
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
================================================
<!DOCTYPE HTML>
<html ng-app="perfjankie">
<head>
<title>PerfJankie - Rendering Performance Analysis</title>
<!-- build:remove:dist -->
<link rel="stylesheet" type="text/css" href="bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="jqplot-bower/dist/jquery.jqplot.min.css" />
<!-- /build -->
<!-- build:remove:dev -->
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
<!-- /build -->
<link rel="stylesheet" type="text/css" href="main.css" />
</head>
<body ng-controller="MainPageCtrl" ng-class="{'no-pj-brand': noPjBrand}">
<!-- build:include:dist app/main-page/navbar.html -->
<div ng-include="'app/main-page/navbar.html'"></div>
<!-- /build -->
<!-- build:include:dist app/main-page/sidebar.html -->
<div ng-include="'app/main-page/sidebar.html'"></div>
<!-- /build -->
<div class="container-fluid content-container">
<div class="row">
<div class="col-md-10 col-md-offset-2">
<div class="content" ng-view ng-hide="pageError"></div>
<div class="content" style="display:none" ng-class="{show: pageError}">
<div class="jumbotron error">
<h2>
<span class="icon icon-attention"></span>
Error loading page
</h2>
<p class="lead">
An error occured when trying to load this page.
<br/>
Please
<a onclick="document.location.reload()">refresh</a>
this page,
or go back to the
<a ng-click="goHome()">home page</a>
.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="page-loading" ng-show="pageLoading">
<div class="spin-container">
<span class="icon-spin5 animate-spin"></span>
</div>
</div>
<!-- build:remove:dev -->
<script type="text/javascript" src = "//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src = "//ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.min.js"></script>
<script type="text/javascript" src = "//ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular-route.js"></script>
<script type="text/javascript" src = "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<!-- /build -->
<!-- build:remove:dist -->
<script type="text/javascript" src="jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="angular/angular.min.js"></script>
<script type="text/javascript" src="angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="server/endpoints.js"></script>
<script type="text/javascript" src="metrics.js"></script>
<!-- /build -->
<!-- build:template
<% _.each(scripts, function(script){ %>
<script type="text/javascript" src="<%= script%>"></script>
<% }); %>
/build -->
<!-- -->
</body>
</html>
================================================
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;
}
]);
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
SYMBOL INDEX (19 symbols across 11 files)
FILE: lib/couchSite.js
function contentType (line 19) | function contentType(filename) {
function readFileContents (line 28) | function readFileContents(files, siteDest) {
function removeSite (line 57) | function removeSite() {
FILE: lib/couchViews.js
function uploadViews (line 4) | function uploadViews(db, log) {
FILE: lib/index.js
function runTests (line 9) | function runTests(config) {
FILE: lib/options.js
function assert (line 16) | function assert(expr, msg) {
FILE: migrations/migrate-0.4.0.js
function Metrics (line 37) | function Metrics(metrics) {
FILE: migrations/utility.js
function processBatch (line 6) | function processBatch(skip) {
FILE: www/app/metric-details/metricDetailsGraph.js
function prepareData (line 4) | function prepareData(val) {
function drawGraph (line 21) | function drawGraph(el, data, unit) {
function link (line 102) | function link(scope, element, attrs) {
FILE: www/app/summary/networkTimingGraph.js
function prepareData (line 17) | function prepareData(val) {
function drawGraph (line 43) | function drawGraph(el, series) {
function link (line 75) | function link(scope, element, attrs) {
FILE: www/app/summary/paintCycleGraph.js
function prepareData (line 6) | function prepareData(data) {
function drawGraph (line 14) | function drawGraph(el, data) {
function link (line 37) | function link(scope, element, attrs) {
FILE: www/app/summary/tiles.js
function getMetricUnit (line 20) | function getMetricUnit(metric) {
FILE: www/server/endpoints.js
function rowsToObj (line 9) | function rowsToObj(row, keys, val) {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (128K chars).
[
{
"path": ".gitignore",
"chars": 150,
"preview": "node_modules/\r\nbower_components/\r\nbin/\r\nbin-site/\r\n*.log\r\n.DS_Store\n.idea/*\n.tmp/*\ndist/*\n_replicator/*\n_users/*\npouch__"
},
{
"path": ".jshintrc",
"chars": 480,
"preview": "{\r\n \"curly\": true,\r\n \"eqeqeq\": true,\r\n \"immed\": true,\r\n \"latedef\": true,\r\n \"newcap\": true,\r\n \"noarg\": true,\r\n \"su"
},
{
"path": ".npmignore",
"chars": 96,
"preview": "node_modules/\r\nbower_components/\r\nGruntfile.js\r\ntest/\r\nwww/\r\nbower.json\r\n.jshintrc\r\n.gitignore\r\n"
},
{
"path": ".travis.yml",
"chars": 412,
"preview": "sudo: false\nlanguage: node_js\nnode_js:\n - \"0.12\"\n - \"4.0\"\n - \"4.3\"\n - \"4\"\n - \"5.0\"\n - \"5\"\n - \"6\"\n - \"stable\"\nenv"
},
{
"path": "Gruntfile.js",
"chars": 5607,
"preview": "module.exports = function(grunt) {\r\n\r\n\tvar couchdb = require('./test/util').config({\r\n\t\tlog: 1\r\n\t}).couch;\r\n\tvar serveSt"
},
{
"path": "README.md",
"chars": 8937,
"preview": "# perfjankie\n\nPerfJankie is a tool to monitor smoothness and responsiveness of websites and Cordova/Hybrid apps over tim"
},
{
"path": "bower.json",
"chars": 535,
"preview": "{\n \"name\": \"perfjankie\",\n \"main\": \"index.js\",\n \"version\": \"0.0.0\",\n \"homepage\": \"https://github.com/axemclion/perfja"
},
{
"path": "lib/cli.js",
"chars": 3106,
"preview": "#!/usr/bin/env node\n\nvar program = require('commander'),\n\tfs = require('fs');\n\nprogram\n\t.version('0.0.1')\n\t.option('-c -"
},
{
"path": "lib/couch-views/metrics_data.js",
"chars": 366,
"preview": "{\r\n\t_id: \"_design/metrics_data\",\r\n\tlanguage: \"javascript\",\r\n\tviews: {\r\n\t\tstats: {\r\n\t\t\treduce: '_stats',\r\n\t\t\tmap: functio"
},
{
"path": "lib/couch-views/pagelist.js",
"chars": 205,
"preview": "{\r\n\t_id: \"_design/pagelist\",\r\n\tlanguage: \"javascript\",\r\n\tviews: {\r\n\t\tpages: {\r\n\t\t\treduce: \"_count\",\r\n\t\t\tmap: function(do"
},
{
"path": "lib/couch-views/runs.js",
"chars": 465,
"preview": "{\n\t_id: \"_design/runs\",\n\tlanguage: \"javascript\",\n\tviews: {\n\t\tlist: {\n\t\t\tmap: function(doc) {\n\t\t\t\temit([doc.browser, doc."
},
{
"path": "lib/couchData.js",
"chars": 1261,
"preview": "module.exports = function (config, data) {\r\n var server = require('./utils').getCouchDB(config.couch),\r\n Q = r"
},
{
"path": "lib/couchSite.js",
"chars": 2761,
"preview": "var url = require('url');\r\nmodule.exports = function (config) {\r\n\r\n var Q = require('q');\r\n\r\n if (!config.couch.up"
},
{
"path": "lib/couchViews.js",
"chars": 1409,
"preview": "/* jshint evil: true*/\r\nvar Q = require('q');\r\n\r\nfunction uploadViews(db, log) {\r\n var dfd = Q.defer();\r\n\r\n var fs"
},
{
"path": "lib/index.js",
"chars": 1019,
"preview": "var Q = require('q');\r\n\r\nvar init = require('./init'),\r\n\tsite = require('./couchSite'),\r\n\tviews = require('./couchViews'"
},
{
"path": "lib/init.js",
"chars": 800,
"preview": "module.exports = function (config) {\r\n var Q = require('q'),\r\n server = require('./utils').getCouchDB(config.c"
},
{
"path": "lib/mime.js",
"chars": 4322,
"preview": "// from http://github.com/felixge/node-paperboy\r\nmodule.exports = {\r\n \"aiff\": \"audio/x-aiff\",\r\n \"appcache\": \"text/cach"
},
{
"path": "lib/options.js",
"chars": 1397,
"preview": "module.exports = function(config) {\r\n\tvar noop = function() {};\r\n\tconfig.log = config.log || config.logger || noop;\r\n\r\n\t"
},
{
"path": "lib/perfTests.js",
"chars": 1002,
"preview": "module.exports = function(config) {\r\n\tvar Q = require('q'),\r\n\t\tdfd = Q.defer();\r\n\r\n\tif (config.couch.onlyUpdateSite) {\r\n"
},
{
"path": "lib/utils.js",
"chars": 491,
"preview": "var nano = require('nano');\n\n\nvar getCouchDB = function (options) {\n var serverUrl = options.server,\n server;\n"
},
{
"path": "migrations/cli.js",
"chars": 1240,
"preview": "#!/usr/bin/env node\n\nvar program = require('commander'),\n\tfs = require('fs');\n\nvar oldDB, newDB;\nprogram\n\t.version('0.0."
},
{
"path": "migrations/index.js",
"chars": 1540,
"preview": "var semver = require('semver');\r\nvar glob = require('glob');\r\nvar nano = require('nano');\r\nvar Q = require('q');\r\n\r\nQ.lo"
},
{
"path": "migrations/migrate-0.2.0.js",
"chars": 1175,
"preview": "// Migrating from 0.1.x to 0.2.0\r\n\r\nvar Q = require('q');\r\n\r\nvar utility = require('./utility');\r\n\r\nmodule.exports = fun"
},
{
"path": "migrations/migrate-0.3.0.js",
"chars": 1027,
"preview": "// Migrating from 0.2.x to 1.2.x\n\nvar Q = require('q');\n\nvar utility = require('./utility');\n\nmodule.exports = function("
},
{
"path": "migrations/migrate-0.4.0.js",
"chars": 1501,
"preview": "// Migrating to browser-perf@1.3.0. Names of some metrics have changed\n\nvar Q = require('q');\n\nvar utility = require('./"
},
{
"path": "migrations/utility.js",
"chars": 841,
"preview": "var Q = require('q');\r\nvar MAX_LIMIT = 53;\r\n\r\nmodule.exports = {\r\n\tforEachDoc: function(oldDb, newDb, callback) {\r\n\t\tfun"
},
{
"path": "package.json",
"chars": 1768,
"preview": "{\n \"name\": \"perfjankie\",\n \"version\": \"2.1.2\",\n \"dbVersion\": \"0.4.0\",\n \"description\": \"Browser Performance regression"
},
{
"path": "tasks/metricsgen.js",
"chars": 950,
"preview": "var Q = require('q');\nvar browserPerf = require('browser-perf');\n\nmodule.exports = function(grunt) {\n\tgrunt.registerMult"
},
{
"path": "tasks/task.js",
"chars": 2227,
"preview": "module.exports = function(grunt) {\n grunt.registerMultiTask('perfjankie', 'Run rendering performance test cases', funct"
},
{
"path": "test/index.spec.js",
"chars": 2051,
"preview": "var expect = require('chai').expect,\r\n sinon = require('sinon'),\r\n fs = require('fs'),\r\n nano = require('nano')"
},
{
"path": "test/res/local.config.json",
"chars": 280,
"preview": "{\r\n\t\"browsers\": [{\r\n\t\t\"browserName\": \"chrome\",\r\n\t\t\"version\": 32\r\n\t}],\r\n\t\"selenium\": {\r\n\t\t\"hostname\": \"localhost\",\r\n\t\t\"po"
},
{
"path": "test/res/sample-perf-results.json",
"chars": 3459,
"preview": "[{\n\t\"numAnimationFrames\": 397,\n\t\"numFramesSentToScreen\": 397,\n\t\"droppedFrameCount\": 60,\n\t\"meanFrameTime\": 19.36006281407"
},
{
"path": "test/res/test1.html",
"chars": 1825,
"preview": "<!doctype html>\r\n<html>\r\n<head>\r\n\t<title></title>\r\n</head>\r\n<body>\r\n\t<script type=\"text/javascript\">\r\n\t\tvar css = {\r\n\t\t\t"
},
{
"path": "test/res/test2.html",
"chars": 2216,
"preview": "<!doctype html>\r\n<html>\r\n<head>\r\n</head>\r\n<body>\r\n\t<div style = \"font-size: 40px\">DIV</div>\r\n\t<div style = \"font-size: 4"
},
{
"path": "test/seedData.js",
"chars": 1088,
"preview": "module.exports = function(callback, count) {\n\tcount = count || 1000;\n\tvar path = require('path');\n\tvar sampleData = requ"
},
{
"path": "test/util.js",
"chars": 1133,
"preview": "module.exports = {\r\n\tconfig: function(config) {\r\n\t\tconfig = config || {};\r\n\t\tvar options = {\r\n\t\t\t\"url\": \"http://localhos"
},
{
"path": "www/app/all-metrics/all-metrics.html",
"chars": 1504,
"preview": "<div class=\"page all-metrics\">\r\n\t<h1 class = \"page-header\">\r\n\t\tAll Metrics\r\n\t\t<small class=\"page-details pull-right\">\r\n\t"
},
{
"path": "www/app/all-metrics/all-metrics.less",
"chars": 512,
"preview": ".page.all-metrics {\n\t.metric-names {\n\t\tli {\n\t\t\tmargin: 30px 0 0 0;\n\t\t\t.list-group-item{\n\t\t\t\tbackground: #fefefe;\n\t\t\t\tflo"
},
{
"path": "www/app/all-metrics/allmetrics.js",
"chars": 1028,
"preview": "angular\r\n\t.module('allmetrics', ['ngRoute', 'Backend', 'metricdetail'])\r\n\t.config(['$routeProvider',\r\n\t\tfunction($routeP"
},
{
"path": "www/app/app.js",
"chars": 1697,
"preview": "angular\r\n\t.module('perfjankie', ['ngRoute', 'sidebar', 'pageSelect', 'summary', 'allmetrics'])\r\n\t.config(['$routeProvide"
},
{
"path": "www/app/backend.js",
"chars": 806,
"preview": "angular\r\n\t.module('Backend', ['Endpoints'])\r\n\t.factory('Data', ['Resource',\r\n\t\tfunction(resource) {\r\n\t\t\treturn {\r\n\t\t\t\tpa"
},
{
"path": "www/app/font.less",
"chars": 1220,
"preview": "@font-face {\r\n font-family: 'fontello';\r\n src: url('assets/fonts/fontello.eot?37370699');\r\n src: url('assets/fonts/fo"
},
{
"path": "www/app/main-page/error.less",
"chars": 48,
"preview": ".error{\n\ttext-align: center;\n\tmargin-top: 10%;\n}"
},
{
"path": "www/app/main-page/navbar.html",
"chars": 1395,
"preview": "<div class=\"navbar navbar-default navbar-fixed-top\" role=\"navigation\">\r\n\t<div class=\"container-fluid\">\r\n\t\t<div class=\"na"
},
{
"path": "www/app/main-page/navbar.less",
"chars": 390,
"preview": ".navbar-brand {\r\n\tpadding: 0 15px;\r\n\t.logo-icon {\r\n\t\tfont-size: 2.5em;\r\n\t\tcolor: #aaa;\r\n\t\tvertical-align: middle;\r\n\t}\r\n\t"
},
{
"path": "www/app/main-page/no-pj-brand.less",
"chars": 185,
"preview": "body.no-pj-brand {\n\t.navbar {\n\t\tdisplay: none !important;\n\t}\n\t.sidebar {\n\t\tmargin-top: 0px;\n\t}\n\t.content-container {\n\t\tp"
},
{
"path": "www/app/main-page/sidebar.html",
"chars": 1605,
"preview": "<div class=\"sidebar col-md-2 nav\" ng-controller=\"SideBarCtrl as sidebar\" ng-cloak>\r\n\t<div ng-if=\"!pagename || !browser\" "
},
{
"path": "www/app/main-page/sidebar.js",
"chars": 894,
"preview": "angular\n\t.module('sidebar', ['ngRoute'])\n\t.controller('SideBarCtrl', ['$routeParams', '$scope',\n\t\tfunction($routeParams,"
},
{
"path": "www/app/main-page/sidebar.less",
"chars": 1474,
"preview": "@media (min-width:992px) {\r\n\t.sidebar {\r\n\t\tposition: fixed;\r\n\t}\r\n}\r\n.sidebar {\r\n\tuser-select: none;\r\n\tbackground: #f8f8f"
},
{
"path": "www/app/main.less",
"chars": 911,
"preview": "html, body {\r\n\twidth: 100%;\r\n\theight: 100%;\r\n}\r\nbody {\r\n\tfont-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\r\n\t"
},
{
"path": "www/app/metric-details/metric-detail.html",
"chars": 2056,
"preview": "<div class=\"page metric-detail\">\n\t<h1 class = \"page-header\" ng-cloak>\n\t\t{{metric.name | formatMetric}}\n\t\t<a class=\"metri"
},
{
"path": "www/app/metric-details/metric-detail.less",
"chars": 566,
"preview": ".page.metric-detail {\r\n\t.metric-help {\r\n\t\tbackground: #ccc;\r\n\t\tborder-radius: 50%;\r\n\t\tfont-size: 50%;\r\n\t\tcolor: #fff;\r\n\t"
},
{
"path": "www/app/metric-details/metricDetail.js",
"chars": 1800,
"preview": "angular\n\t.module('metricdetail', ['ngRoute', 'metricsGraphDetails', 'Backend'])\n\t.config(['$routeProvider',\n\t\tfunction($"
},
{
"path": "www/app/metric-details/metricDetailsGraph.js",
"chars": 2736,
"preview": "angular\n\t.module('metricsGraphDetails', [])\n\t.directive('pjMetricsDetailsGraph', function() {\n\t\tfunction prepareData(val"
},
{
"path": "www/app/page-select/page-select.html",
"chars": 1104,
"preview": "<div class=\"page page-select\">\r\n\t<h1 class=\"page-header\">Test Cases</h1>\r\n\t<p class=\"lead\">Select a test case to view me"
},
{
"path": "www/app/page-select/page-select.less",
"chars": 272,
"preview": ".page.page-select{\r\n\t.suites{\r\n\t\t.panel{\r\n\t\t\t.list-group{\r\n\t\t\t\t.list-group-item{\r\n\t\t\t\t\tpadding: 0;\r\n\t\t\t\t\ta{\r\n\t\t\t\t\t\tdispl"
},
{
"path": "www/app/page-select/pageSelect.js",
"chars": 527,
"preview": "angular\r\n\t.module('pageSelect', ['ngRoute', 'Backend'])\r\n\t.config(['$routeProvider',\r\n\t\tfunction($routeProvider) {\r\n\t\t\t$"
},
{
"path": "www/app/summary/networkTimingGraph.js",
"chars": 2189,
"preview": "angular\n\t.module('networkTiming', [])\n\t.directive('pjNetworkTimingGraph', function() {\n\t\tvar ticks = ['onLoad', 'Process"
},
{
"path": "www/app/summary/paintCycleGraph.js",
"chars": 1284,
"preview": "angular\n\t.module('paintCycleGraph', [])\n\t.directive('pjPaintCycleGraph', function() {\n\t\tvar id = 'paintCycle' + Math.flo"
},
{
"path": "www/app/summary/summary.html",
"chars": 3394,
"preview": "<div class=\"page summary\">\r\n\t<h1 class = \"page-header\">\r\n\t\t<small>Summary of</small>\r\n\t\t{{pagename}}\r\n\t\t<small>on {{brow"
},
{
"path": "www/app/summary/summary.js",
"chars": 1383,
"preview": "angular\r\n\t.module('summary', ['ngRoute', 'paintCycleGraph', 'networkTiming', 'Backend'])\r\n\t.config(['$routeProvider',\r\n\t"
},
{
"path": "www/app/summary/summary.less",
"chars": 237,
"preview": ".summary{\n\t.test-runs{\n\t\t.list-group-item{\n\t\t\tborder-radius: 0 !important;\n\t\t\tborder-left: none;\n\t\t\tborder-right: none;\n"
},
{
"path": "www/app/summary/tiles.js",
"chars": 1528,
"preview": "angular\n\t.module('summaryTiles', ['Backend'])\n\t.directive('pjSummaryTiles', ['Data', '$q',\n\t\tfunction(data, $q) {\n\t\t\tvar"
},
{
"path": "www/app/summary/tiles.less",
"chars": 862,
"preview": ".tiles {\n\t.panel {\n\t\t&.panel-0 {\n\t\t\tbackground: #428bca;\n\t\t\tborder-color: #428bca;\n\t\t\ta {\n\t\t\t\tcolor: #2a6496;\n\t\t\t}\n\t\t}\n\t"
},
{
"path": "www/app/summary/tiles.tpl.html",
"chars": 745,
"preview": "<div class=\"col-md-3 col-sm-6\" ng-repeat=\"tile in tiles\">\n\t<div class=\"panel\" ng-class=\"'panel-' + $index\">\n\t\t<div class"
},
{
"path": "www/assets/css/animation.css",
"chars": 1857,
"preview": "/*\n Animation example, for spinners\n*/\n.animate-spin {\n -moz-animation: spin 2s infinite linear;\n -o-animation: spin"
},
{
"path": "www/assets/css/config.json",
"chars": 5670,
"preview": "{\n \"name\": \"\",\n \"css_prefix_text\": \"icon-\",\n \"css_use_suffix\": false,\n \"hinting\": true,\n \"units_per_em\": 1000,\n \"a"
},
{
"path": "www/assets/css/fontello-codes.css",
"chars": 1269,
"preview": "\n.icon-spin5:before { content: '\\25cc'; } /* '◌' */\n.icon-firefox:before { content: '\\e800'; } /* '' */\n.icon-chrome:be"
},
{
"path": "www/assets/fonts/fontello-codes.css",
"chars": 899,
"preview": "\n.icon-firefox:before { content: '\\e800'; } /* '' */\n.icon-chrome:before { content: '\\e801'; } /* '' */\n.icon-safari:b"
},
{
"path": "www/index.html",
"chars": 2958,
"preview": "<!DOCTYPE HTML>\r\n<html ng-app=\"perfjankie\">\r\n<head>\r\n\t<title>PerfJankie - Rendering Performance Analysis</title>\r\n\r\n\t<!-"
},
{
"path": "www/server/endpoints.js",
"chars": 3054,
"preview": "if (typeof window.DB_BASE !== 'string') {\r\n\twindow.DB_BASE = '..';\r\n}\r\n\r\nangular\r\n\t.module('Endpoints', [])\r\n\t.factory('"
}
]
About this extraction
This page contains the full source code of the axemclion/perfjankie GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (106.6 KB), approximately 34.9k tokens, and a symbol index with 19 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.