Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@

DeviceTiming is a tool for measuring parse & execution times for JavaScript files. DeviceTiming has server and client components - the server waits for the clients to send timing data, stores it and produces static HTML reports. The client is added to your javascript files individually along with instrumentation to perform the tests. This assumes you have some kind of development or test server running your website's code, which you modify for this purpose and then restore. **It is a tool built for testing in a controlled environment, use in production considered harmful**.

## Installation
**This is a forked and modified repo of the original repo maintained at [https://github.com/etsy/DeviceTiming]**

**The original work is done by Daniel Espeset along with Performance and Frontend Infrastructure at Etsy. Some Modifications have been made by Joseph on top of the original tool. See below for the list of modifications**

## Modifications
1. Added Charting capabilities for reports generated. Now user can visualize the data in the form of column charts.
2. Using Express server. Using seperate server instances for instrumentation and reporting. See server.js and report.js for more info.
3. Client and browser detection from the user agent string. The mobile device names cannot be captured as of now. Better detection mechanism will be provided in future.
4. All reports by default are generated inside a "reports" folder under the output path specified by user in the report command.
5. Once reports and visualization charts are generated the reporting server automatically launches the default browser and serves the visualization page.

To start reporting server and visualize charts see **Running the reports and visualizing the data** section.

## Installation
Clone the repo and install the dependecies:

```.sh
Expand All @@ -13,7 +25,6 @@ npm install
```

## Setup the test

Note that DeviceTiming **will modify your javascript files - it is not reccomended that you use this tool without having a backup of your code**.

So first, backup your code:
Expand Down Expand Up @@ -47,6 +58,19 @@ To run the tests, visit your development server hosting the instrumented code fr

The instrumentation assumes that the DeviceTiming server can be found at the same hostname as the locations you hit in the browser, if you need to use a different hostname for those beacons, use the `--hostname` argument with `devicetiming server`.

## Running the reports and visualizing the data
Start the reporting server and provide the ouput path for reports

```.sh
report /path/to/results.json /path/to/output
```
This dumps the reports (report.html, report.json) and visualization pages inside a "reports" folder under the output path. The reporting server automatically launches the default browser and serves the visualization page at port 3000. The reporting server also responds to the following GET requests (eg. http://localhost:3000/data):

```.sh
/data returns the results data as JSON
/summary returns the results data reduced to mean values as JSON
```

## Methodology

The first draft of our tests was done by adding a timer to our primary javascript bundle that started on the first line, and ended on the last. This effectively timed the initial execution time of the js file. However, it left out parsing time. In order to capture both we crafted the `instrument.js` processor for our compiled JavaScripts that performs these steps on each file:
Expand All @@ -67,11 +91,13 @@ See slides and resources from [Unpacking the Black Box: Benchmarking JS Parsing

## Questions and comments welcome

Open an issue, submit a pull request or tweet at [@danielespeset][twitter] on twitter.
Open an issue, submit a pull request or tweet at [@danielespeset][twitter1] on twitter - the original source for the tool.
For any questions or issue with the modifications, please tweet at [@joseph_rialab][twitter2]

## Credits

Made by Performance and Frontend Infrastructure at Etsy.

[talk]: http://talks.desp.in/unpacking-the-black-box
[twitter]: http://twitter.com/danielespeset
[twitter1]: http://twitter.com/danielespeset
[twitter2]: https://twitter.com/joseph_rialab

56 changes: 37 additions & 19 deletions devicetiming
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/usr/bin/env node

/**
* @author Daniel Espeset <desp@etsy.com>
* @copyright (c) 2014 Etsy, Inc.
* Modified by Joseph Khan <jkhan@yodlee.com> - 11 Oct 2014
*
* This script exposes the command line interface for the different components
* that make up the devicetiming tool. It is responsible for handling command
* line arguments, validating them and passing them through to the different
Expand All @@ -12,15 +16,25 @@
*
* See usage for more.
*
* @author Daniel Espeset <desp@etsy.com>
* @copyright (c) 2014 Etsy, Inc.
*
* To log reports and start the visualization server
*
* report /path/to/results.json /path/to/output
*
* This dumps the reports and visualization pages inside a reports folder under the output path
*
* Modifications:
* 1. reports folder generated by node under output path
* 2. server command starts an express server. See server.js
* 3. report command starts another express server instance. See server.js
**/

var argv = require('minimist')(process.argv.slice(2)),
fs = require('fs'),
readline = require('readline'),
path = require('path');


function usage() {
process.stdout.write([
"DeviceTiming is a tool for profiling parse and execute times of JS files on pageload.",
Expand Down Expand Up @@ -103,7 +117,7 @@ var commands = {
server: function(args) {
this.instrument(args, function(){
console.log("\nDone, launching server");
require(path.resolve(__dirname, 'server')).start(args);
require(path.resolve(__dirname, 'server')).startServer(args);
});
},

Expand All @@ -117,19 +131,19 @@ var commands = {
if (f.substr(-3) === '.js') { targetFiles.push(path.resolve(d, f)); }
}
});
function overwriteThenStartServer(yes) {
function overwriteThenStartServer(yes) {
if (!yes) {
return errorExit('Aborted instrumentation, maybe prepare a backup and try again?', 1);
}
console.log("Adding instrumentation to " + targetPath);
require(path.resolve(__dirname, 'instrument')).rewriteTargetFiles(targetFiles, targetPath, args.n || args.hostname, args.p || args.port);
done && done();
}
if (args.f || args.force) {
return overwriteThenStartServer(true);
}
if (args.f || args.force) {
return overwriteThenStartServer(true);
}
// Ask "are you for seriously?" then add instrumentation
confirm(dangerMessage(targetFiles, targetPath), overwriteThenStartServer);
confirm(dangerMessage(targetFiles, targetPath), overwriteThenStartServer);
},

// Generate a static report from a results.json file
Expand All @@ -140,19 +154,23 @@ var commands = {
} catch (e) {
errorExit("Error trying to load JSON file.\n"+e, 1);
}
// Quick 'n dirty mkdir -p implementation for output path
var outputPath = path.resolve(args._[2]);

//reports are dumped inside a "reports" folder under the output path
var outputPath = path.resolve(args._[2] + "/reports") ;

console.log('-- LOG -- Output Path for reports: ' + outputPath);

if (!fs.existsSync(outputPath)) {
outputPath.split(path.sep).forEach(function(part, i){
var p = outputPath.split(path.sep);
p.splice(i);
if (!fs.existsSync(path.resolve(p, part))) {
fs.mkdirSync(path.resolve(p, part));
}
});
fs.mkdirSync(outputPath); //creates a folder 'reports' under the output path specified
}

//calls the outputReport() method inside report.js and generates the report files in the output path
require(path.resolve(__dirname, 'report')).outputReport(results, outputPath);
process.exit(0);

console.log('-- LOG -- Reports Generated. Now launching server');

//calls the startExpress() method inside server.js to launch a server and serve the report and visualization pages
require(path.resolve(__dirname, 'server')).startReportServer(results, outputPath, 3000);
},

// I need somebody
Expand All @@ -178,4 +196,4 @@ if (!commands[cmd]) {
}

// Execute command
commands[cmd](argv);
commands[cmd](argv);
153 changes: 153 additions & 0 deletions lib/client_detection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* This is the client|browser detection component. It exposes a function that takes an
* user agent string and returns the os | browser names and their versions by spoofing against the user agent.
*
* Shortcomings - 1. cannot detect mobile device name as of now
* 2. may not be as efficient. Better libraries may be out there.
*
* @author Joseph Khan <jkhan@yodlee.com>
* MIT License
**/

function clientDetect(userAgentString) {
//return browser, version, os, osverion
var verOffset,
version,
majorVersion,
browser;
unknown = '-',
iOSDevice = "";

// Opera
if ((verOffset = userAgentString.indexOf('Opera')) != -1) {
browser = 'Opera';
version = userAgentString.substring(verOffset + 6);
if ((verOffset = userAgentString.indexOf('Version')) != -1) {
version = userAgentString.substring(verOffset + 8);
}
}
// MSIE
else if ((verOffset = userAgentString.indexOf('MSIE')) != -1) {
browser = 'IE';
version = userAgentString.substring(verOffset + 5);
}
// Chrome
else if ((verOffset = userAgentString.indexOf('Chrome')) != -1) {
browser = 'Chrome';
version = userAgentString.substring(verOffset + 7);
}
// Safari
else if ((verOffset = userAgentString.indexOf('Safari')) != -1) {
browser = 'Safari';
version = userAgentString.substring(verOffset + 7);
if ((verOffset = userAgentString.indexOf('Version')) != -1) {
version = userAgentString.substring(verOffset + 8);
}
}
// Firefox
else if ((verOffset = userAgentString.indexOf('Firefox')) != -1) {
browser = 'Firefox';
version = userAgentString.substring(verOffset + 8);
}
// MSIE 11+
else if (userAgentString.indexOf('Trident/') != -1) {
browser = 'IE';
version = userAgentString.substring(userAgentString.indexOf('rv:') + 3);
}
// Other browsers
else if ((nameOffset = userAgentString.lastIndexOf(' ') + 1) < (verOffset = userAgentString.lastIndexOf('/'))) {
browser = userAgentString.substring(nameOffset, verOffset);
version = userAgentString.substring(verOffset + 1);
if (browser.toLowerCase() == browser.toUpperCase()) {
browser = navigator.appName;
}
}

// trim the version string
if ((ix = version.indexOf(';')) != -1) version = version.substring(0, ix);
if ((ix = version.indexOf(' ')) != -1) version = version.substring(0, ix);
if ((ix = version.indexOf(')')) != -1) version = version.substring(0, ix);

majorVersion = parseInt('' + version, 10);

// system
var os = unknown;
var clientStrings = [
{s:'Windows 3.11', r:/Win16/},
{s:'Windows 95', r:/(Windows 95|Win95|Windows_95)/},
{s:'Windows ME', r:/(Win 9x 4.90|Windows ME)/},
{s:'Windows 98', r:/(Windows 98|Win98)/},
{s:'Windows CE', r:/Windows CE/},
{s:'Windows 2000', r:/(Windows NT 5.0|Windows 2000)/},
{s:'Windows XP', r:/(Windows NT 5.1|Windows XP)/},
{s:'Windows Server 2003', r:/Windows NT 5.2/},
{s:'Windows Vista', r:/Windows NT 6.0/},
{s:'Windows 7', r:/(Windows 7|Windows NT 6.1)/},
{s:'Windows 8.1', r:/(Windows 8.1|Windows NT 6.3)/},
{s:'Windows 8', r:/(Windows 8|Windows NT 6.2)/},
{s:'Windows NT 4.0', r:/(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/},
{s:'Windows ME', r:/Windows ME/},
{s:'Android', r:/Android/},
{s:'Open BSD', r:/OpenBSD/},
{s:'Sun OS', r:/SunOS/},
{s:'Linux', r:/(Linux|X11)/},
{s:'iOS', r:/(iPhone|iPad|iPod)/},
{s:'Mac OS X', r:/Mac OS X/},
{s:'Mac OS', r:/(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/},
{s:'QNX', r:/QNX/},
{s:'UNIX', r:/UNIX/},
{s:'BeOS', r:/BeOS/},
{s:'OS/2', r:/OS\/2/},
{s:'Search Bot', r:/(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/}
];

for (var id in clientStrings) {
var cs = clientStrings[id];
if (cs.r.test(userAgentString)) {
os = cs.s;
break;
}
}

var osVersion = unknown;

if (/Windows/.test(os)) {
osVersion = /Windows (.*)/.exec(os)[1];
os = 'Windows';
}

switch (os) {
case 'Mac OS X':
osVersion = /Mac OS X (10[\.\_\d]+)/.exec(userAgentString)[1];
break;

case 'Android':
osVersion = /Android ([\.\_\d]+)/.exec(userAgentString)[1];
break;

case 'iOS':
osVersion = /OS (\d+)_(\d+)_?(\d+)?/.exec(userAgentString);
osVersion = osVersion[1] + '.' + osVersion[2] + '.' + (osVersion[3] | 0);

//if it is iOS, I want to show iPhone, iPod or iPad
if(/iPhone/.test(userAgentString)) {
iOSDevice = "iPhone";
} else if(/iPad/.test(userAgentString)) {
iOSDevice = "iPad";
} else if(/iPod/.test(userAgentString)) {
iOSDevice = "iPod";
}

break;
}

if(os === "iOS") {
return browser + "" + majorVersion + "/" + iOSDevice + "(iOS" + osVersion + ")";
}
return browser + "" + majorVersion + "/" + os + "" + osVersion;

}

module.exports = {
clientDetect : clientDetect
};
Loading