Second commit

This commit is contained in:
Gaël Métais
2014-08-01 16:34:12 +02:00
commit fb4602d99f
14 changed files with 641 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
bower_components
results/*
+3
View File
@@ -0,0 +1,3 @@
angular.module('Spaghetti', [
'Results'
]);
View File
+48
View File
@@ -0,0 +1,48 @@
var app = angular.module('Results', []);
app.controller('ResultsCtrl', function ($scope) {
// Grab results from nodeJS served page
$scope.phantomasResults = window._phantomas_results;
$scope.slowRequestsOn = false;
$scope.slowRequestsLimit = 5;
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.data.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.phantomasResults.javascript.children.forEach(function(currentNode) {
currentNode.data.showDetails = false;
});
// Parse the backtrace
if (!node.data.parsedBacktrace) {
node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
}
}
node.data.showDetails = !isOpen;
};
function parseBacktrace(str) {
var splited = str.split(' / ');
var out = [];
splited.forEach(function(trace) {
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
if (result && result[2].length > 0) {
var filePath = result[2];
var chunks = filePath.split('/');
var fileName = chunks[chunks.length - 1];
out.push({
fnName: result[1],
fileName: fileName,
filePath: filePath,
line: result[3]
});
}
});
return out;
}
});
+29
View File
@@ -0,0 +1,29 @@
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
color: #fff;
}
input[type=submit], input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
+3
View File
@@ -0,0 +1,3 @@
#status {
font-size: 1.5em;
}
+24
View File
@@ -0,0 +1,24 @@
html {
margin: 100px 50px;
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
}
body, input[type=submit], input[type=text], input[type=number], button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
}
input[type=text] {
}
input[type=submit] {
cursor: pointer;
}
+133
View File
@@ -0,0 +1,133 @@
.testedUrl {
color: inherit;
}
h4 {
margin-bottom: 0.5em;
}
.execution {
margin-top: 3em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.filters {
margin: 1em 0;
padding: 0.5em;
border: 1px dotted #aaa;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type, .table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div > .index {
color: #bbb;
}
.table > div > .type {
white-space:nowrap;
}
.table > div > .value {
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
}
.table > div > .details button {
color: #f1c40f;
font-weight: bold;
border: 2px solid #f1c40f;
border-radius: 50%;
background: transparent;
height: 2em;
width: 2em;
cursor: pointer;
}
.detailsOverlay {
position: absolute;
right: 3em;
top: -3em;
width: 45em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .trace {
word-break: break-all;
}
.table > div > .duration {
text-align: center;
white-space:nowrap;
}
.warningIcon {
display: inline-block;
width: 1.5em;
height: 1.5em;
background: url('data:image/svg+xml;utf8,<svg version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><defs/><g fill="none" fill-rule="evenodd" id="Icons new Arranged Names Color" stroke="none" stroke-width="1"><g fill="#e74c3c" id="101 Warning"><path d="M14.4242327,6.14839275 C15.2942987,4.74072976 16.707028,4.74408442 17.5750205,6.14839275 L28.3601099,23.59738 C29.5216388,25.4765951 28.6755462,27 26.4714068,27 L5.5278464,27 C3.32321557,27 2.47386317,25.4826642 3.63914331,23.59738 Z M16,20 C16.5522847,20 17,19.5469637 17,19.0029699 L17,12.9970301 C17,12.4463856 16.5561352,12 16,12 C15.4477153,12 15,12.4530363 15,12.9970301 L15,19.0029699 C15,19.5536144 15.4438648,20 16,20 Z M16,24 C16.5522848,24 17,23.5522848 17,23 C17,22.4477152 16.5522848,22 16,22 C15.4477152,22 15,22.4477152 15,23 C15,23.5522848 15.4477152,24 16,24 Z M16,24" id="Triangle 29"/></g></g></svg>') no-repeat 0 3px;
}
+16
View File
@@ -0,0 +1,16 @@
<html>
<head>
<meta charset="utf-8">
<title>Javascript Spaghetti Profiler</title>
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/index.css">
</head>
<body>
<h1>Javascript Spaghetti Profiler</h1>
<form method="post" action="/launchTest">
<input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
</form>
</body>
</html>
+47
View File
@@ -0,0 +1,47 @@
<html>
<head>
<meta charset="utf-8">
<title>Javascript Spaghetti Profiler</title>
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<h1>Javascript Spaghetti Profiler</h1>
<div id="status"></div>
<div>%%TEST_URL%%</div>
<script>
var testId = '%%TEST_ID%%';
var statusElement = document.getElementById('status');
var socket = io();
function askStatus() {
socket.emit('waiting', testId);
}
socket.on('running', function() {
statusElement.innerHTML = 'Running';
setTimeout(askStatus, 200);
});
socket.on('position', function(position) {
statusElement.innerHTML = 'Waiting behind ' + position + ' other tests';
setTimeout(askStatus, 2000);
});
socket.on('complete', function() {
statusElement.innerHTML = 'Test complete';
window.location.replace('/results/' + testId);
});
socket.on('404', function() {
statusElement.innerHTML = 'Test not found';
});
askStatus();
</script>
</body>
</html>
+113
View File
@@ -0,0 +1,113 @@
<html>
<head>
<meta charset="utf-8">
<title>Javascript Spaghetti Profiler</title>
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/results.css">
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/public/scripts/app.js"></script>
<script src="/public/scripts/resultsController.js"></script>
<script src="/public/scripts/filters.js"></script>
</head>
<body ng-app="Spaghetti" ng-controller="ResultsCtrl">
<h1>Javascript Spaghetti Profiler</h1>
<div ng-if="undefined">Untangling and counting the spaghettis...</div>
<div class="ng-cloak">
<div>Tested url: <a class="testedUrl" href="phantomasResults.url" target="_blank">{{phantomasResults.url}}</a></div>
<div ng-if="phantomasResults.error || !phantomasResults.javascript.data">
<h2>Error: {{phantomasResults.error}}</h2>
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
<div ng-if="!phantomasResults.error && !phantomasResults.javascript.data">Javascript execution tree error</div>
</div>
<div ng-if="!phantomasResults.error" class="execution">
<h2>Javascript interactions with the DOM</h2>
<div class="filters">
<div>
<input type="checkbox" ng-model="textFilterOn" />
Filter by
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
</div>
<div>
<input type="checkbox" ng-model="slowRequestsOn" id="slowRequests" />
<label for="slowRequests">Requests slower than</label>
<input type="number" value="5" min="0" ng-model="slowRequestsLimit" class="slowRequestsLimit"> ms
</div>
</div>
<div class="table">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Duration</div>
</div>
<div ng-repeat="node in phantomasResults.javascript.children"
ng-if="(!slowRequestsOn || node.data.time > slowRequestsLimit) && (!textFilterOn || !textFilter.length || node.data.type.indexOf(textFilter) >= 0 || node.data.callDetails.arguments[0].indexOf(textFilter) >= 0)"
ng-class="{showingDetails: node.data.showDetails, jsError: node.data.type == 'error'}">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">{{node.data.callDetails.arguments[0]}}</div>
<div class="details">
<button ng-click="onNodeDetailsClick(node)">i</button>
<div class="detailsOverlay" ng-show="node.data.showDetails">
<div class="closeBtn" ng-click="onNodeDetailsClick(node)"></div>
<div ng-if="node.data.callDetails.context.domElement">
<h4>Called on DOM element</h4>
<div>{{node.data.callDetails.context.domElement}}</div>
</div>
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
<h4>Called on 1 jQuery element</h4>
<div>{{node.data.callDetails.context.firstElementPath}}</div>
</div>
<h4>Backtrace</h4>
<div class="table">
<div ng-repeat="trace in node.data.parsedBacktrace track by $index">
<div>{{trace.fnName || '(anonymous)'}}</div>
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
</div>
<div ng-if="node.data.parsedBacktrace.length == 0">
<div>can't find any backtrace :/</div>
</div>
</div>
<div ng-if="node.children.length > 0">
<h4>Sub processes</h4>
<div class="table">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div>Duration</div>
</div>
<div ng-repeat="node in node.children">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">{{node.data.callDetails.arguments[0]}}</div>
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms <div ng-if="node.data.time > slowRequestsLimit" class="warningIcon"></div></div>
<div class="duration" ng-if="node.data.time == undefined"></div>
</div>
</div>
</div>
</div>
</div>
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms <div ng-if="node.data.time > slowRequestsLimit" class="warningIcon"></div></div>
<div class="duration" ng-if="node.data.time == undefined"></div>
</div>
</div>
</div>
</div>
<script>var _phantomas_results = %%RESULTS%%;</script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
{
"name": "js-spaghetti-profiler",
"dependencies": {
"angular": "1.3.0-beta.16"
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "jsspaghettiprofiler",
"version": "0.0.0",
"dependencies": {
"phantomas": "git+https://git@github.com/gmetais/phantomas.git#jquery-profiler",
"express": "^4.6.1",
"async": "^0.9.0",
"socket.io": "^1.0.6",
"body-parser": "^1.5.0"
},
"devDependencies": {},
"engines": {
"node": ">=0.10.0"
},
"scripts": {}
}
+200
View File
@@ -0,0 +1,200 @@
var fs = require('fs');
var async = require('async');
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
var bodyParser = require('body-parser');
var phantomas = require('phantomas');
app.use(bodyParser.urlencoded({ extended: false }));
// Home page
app.get('/', function(req, res) {
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/views/index.html', {encoding: 'utf8'}, callback);
}
}, function(err, results) {
res.setHeader('Content-Type', 'text/html');
res.send(results.htmlTemplate);
});
});
// Waiting queue page
app.post('/launchTest', function(req, res) {
// Generate test id
var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
var url = req.body.url;
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
url = 'http://' + url;
}
async.waterfall([
function htmlTemplate(callback) {
fs.readFile('./app/views/launchTest.html', {encoding: 'utf8'}, callback);
},
function sendResponse(html, callback) {
html = html.replace('%%TEST_URL%%', url);
html = html.replace('%%TEST_ID%%', testId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
callback();
},
function createFolder(callback) {
// Create results folder
fs.mkdir(resultsPath, callback);
},
function executePhantomas(callback) {
var options = {
timeout: 60,
'js-execution-tree': true,
reporter: 'json:pretty'
};
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
var task = {
testId: testId,
url: url,
options: options
};
console.log(JSON.stringify(task, null, 4));
taskQueue.push(task, callback);
},
function writeResults(json, resultsObject, callback) {
console.log('Saving Phantomas results file to ' + phantomasResultsPath);
fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
}
], function(err) {
if (err) {
console.log('An error occured while launching the phantomas test : ', err);
fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
if (err) {
console.log('Could not even write an error message on file ' + phantomasResultsPath);
console.log(err);
}
});
}
});
});
// Results page
app.get('/results/:testId', function(req, res) {
var testId = req.params.testId;
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
console.log('Opening test ' + testId + ' results as HTML');
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/views/results.html', {encoding: 'utf8'}, callback);
},
phantomasResults: function(callback) {
fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
}
}, function(err, results) {
if (err) {
console.log(err);
return res.status(404).send('Sorry, test not found...');
}
var html = results.htmlTemplate;
html = html.replace('%%RESULTS%%', results.phantomasResults);
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
});
// Static files
app.use('/public', express.static(__dirname + '/app/public'));
app.use('/bower_components', express.static(__dirname + '/bower_components'));
// Socket.io
io.on('connection', function(socket){
socket.on('waiting', function(testId) {
console.log('User waiting for test id ' + testId);
socket.testId = testId;
// Check task position in queue
var positionInQueue = -1;
if (taskQueue.length() > 0) {
taskQueue.tasks.forEach(function(task, index) {
if (task.data.testId === testId) {
positionInQueue = index;
}
});
}
if (positionInQueue >= 0) {
socket.emit('position', positionInQueue);
} else if (currentTask && currentTask.testId === testId) {
socket.emit('running');
} else {
// Find in results files
var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
if (exists) {
// TODO : use eventEmitter to make sure the file is completly written on disk
setTimeout(function() {
socket.emit('complete');
}, 1000);
} else {
socket.emit('404');
}
});
}
});
});
// Creating a queue and defining the worker function
var currentTask = null;
var taskQueue = async.queue(function queueWorker(task, callback) {
currentTask = task;
console.log('Starting test ' + task.testId);
// It's time to launch the test!!!
phantomas(task.url, task.options, function(err, json, results) {
console.log('Test ' + task.testId + ' complete');
currentTask = null;
callback(err, json, results);
});
});
// Launch the server
server.listen(8383, function() {
console.log('Listening on port %d', server.address().port);
});