commit fb4602d99f14a45386b4f1b4d930c7a2b2103659 Author: Gaël Métais Date: Fri Aug 1 16:34:12 2014 +0200 Second commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b31a59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +bower_components +results/* \ No newline at end of file diff --git a/app/public/scripts/app.js b/app/public/scripts/app.js new file mode 100644 index 0000000..3e968a1 --- /dev/null +++ b/app/public/scripts/app.js @@ -0,0 +1,3 @@ +angular.module('Spaghetti', [ + 'Results' +]); \ No newline at end of file diff --git a/app/public/scripts/filters.js b/app/public/scripts/filters.js new file mode 100644 index 0000000..e69de29 diff --git a/app/public/scripts/resultsController.js b/app/public/scripts/resultsController.js new file mode 100644 index 0000000..d9d40c8 --- /dev/null +++ b/app/public/scripts/resultsController.js @@ -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; + } + +}); \ No newline at end of file diff --git a/app/public/styles/index.css b/app/public/styles/index.css new file mode 100644 index 0000000..f3d26bc --- /dev/null +++ b/app/public/styles/index.css @@ -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; +} \ No newline at end of file diff --git a/app/public/styles/launchTest.css b/app/public/styles/launchTest.css new file mode 100644 index 0000000..b23b5ca --- /dev/null +++ b/app/public/styles/launchTest.css @@ -0,0 +1,3 @@ +#status { + font-size: 1.5em; +} \ No newline at end of file diff --git a/app/public/styles/main.css b/app/public/styles/main.css new file mode 100644 index 0000000..2f49947 --- /dev/null +++ b/app/public/styles/main.css @@ -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; +} \ No newline at end of file diff --git a/app/public/styles/results.css b/app/public/styles/results.css new file mode 100644 index 0000000..5fe4b55 --- /dev/null +++ b/app/public/styles/results.css @@ -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,') no-repeat 0 3px; +} \ No newline at end of file diff --git a/app/views/index.html b/app/views/index.html new file mode 100644 index 0000000..8841a4b --- /dev/null +++ b/app/views/index.html @@ -0,0 +1,16 @@ + + + + Javascript Spaghetti Profiler + + + + +

Javascript Spaghetti Profiler

+ +
+ + +
+ + \ No newline at end of file diff --git a/app/views/launchTest.html b/app/views/launchTest.html new file mode 100644 index 0000000..622738c --- /dev/null +++ b/app/views/launchTest.html @@ -0,0 +1,47 @@ + + + + Javascript Spaghetti Profiler + + + + + +

Javascript Spaghetti Profiler

+ +
+ +
%%TEST_URL%%
+ + + + \ No newline at end of file diff --git a/app/views/results.html b/app/views/results.html new file mode 100644 index 0000000..a663f2e --- /dev/null +++ b/app/views/results.html @@ -0,0 +1,113 @@ + + + + Javascript Spaghetti Profiler + + + + + + + + +

Javascript Spaghetti Profiler

+ +
Untangling and counting the spaghettis...
+ +
+
Tested url: {{phantomasResults.url}}
+ +
+

Error: {{phantomasResults.error}}

+
Phantomas timed out
+
Phantomas config error
+
Phantomas failed to load page
+
Phantomas internal error
+
Javascript execution tree error
+
+ +
+

Javascript interactions with the DOM

+
+
+ + Filter by + +
+
+ + + ms +
+
+
+
+
+
Type
+
Params
+
+
Duration
+
+
+
{{$index}}
+
{{node.data.type}}
+
{{node.data.callDetails.arguments[0]}}
+
+ +
+
+ +
+

Called on DOM element

+
{{node.data.callDetails.context.domElement}}
+
+ +
+

Called on 1 jQuery element

+
{{node.data.callDetails.context.firstElementPath}}
+
+ +

Backtrace

+
+
+
{{trace.fnName || '(anonymous)'}}
+ +
+
+
can't find any backtrace :/
+
+
+ +
+

Sub processes

+
+
+
+
Type
+
Params
+
Duration
+
+
+
{{$index}}
+
{{node.data.type}}
+
{{node.data.callDetails.arguments[0]}}
+
{{node.data.time}} ms
+
+
+
+
+
+
+
{{node.data.time}} ms
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..da2fd58 --- /dev/null +++ b/bower.json @@ -0,0 +1,6 @@ +{ + "name": "js-spaghetti-profiler", + "dependencies": { + "angular": "1.3.0-beta.16" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ec1c20 --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..9d5aa80 --- /dev/null +++ b/server.js @@ -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); +}); \ No newline at end of file