diff --git a/local-cli/server/middleware/jscProfilerMiddleware.js b/local-cli/server/middleware/jscProfilerMiddleware.js index d656a4906..43a173591 100644 --- a/local-cli/server/middleware/jscProfilerMiddleware.js +++ b/local-cli/server/middleware/jscProfilerMiddleware.js @@ -8,7 +8,104 @@ */ 'use strict'; +const SourceMapConsumer = require('../../../node_modules/source-map').SourceMapConsumer; const fs = require('fs'); +const http = require('http'); +const urlLib = require('url'); + +class TreeTransformator { + constructor() { + this.urlResults = {}; + } + + transform(tree, callback) { + this.afterUrlsCacheBuild(tree, () => { + callback(this.transformNode(tree)); + }); + } + + // private + transformNode(tree) { + if (tree.url in this.urlResults) { + const original = this.urlResults[tree.url].originalPositionFor({ + line: tree.lineNumber, + column: tree.columnNumber, + }); + tree.scriptId = tree.id; + tree.url = 'file://' + original.source; + tree.lineNumber = original.line; + tree.columnNumber = original.column; + } + tree.children = tree.children.map((t) => this.transformNode(t)); + return tree; + } + + // private + afterUrlsCacheBuild(tree, callback) { + let urls = new Set(); + this.gatherUrls(tree, urls); + + let size = urls.size; + if (size === 0) { + callback(); + } else { + urls.forEach((url) => { + this.callUrlCached(url, () => { + --size; + if (size === 0) { + callback(); + } + }); + }); + } + } + + // private + gatherUrls(tree, urls) { + urls.add(tree.url); + tree.children.map((t) => this.gatherUrls(t, urls)); + } + + // private + callUrlCached(url, callback) { + if (url === '' || url === null || url in this.urlResults) { + callback(); + return; + } + + const parsedUrl = urlLib.parse(url); + const options = { + host: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname.replace(/\.bundle$/, '.map') + parsedUrl.search, + }; + + http.get(options, (res) => { + res.setEncoding('utf8'); + let sawEnd = false; + let resBody = ''; + res.on('data', (chunk) => { + resBody += chunk; + }).on('end', () => { + sawEnd = true; + const map = JSON.parse(resBody.replace(/^\)\]\}'/, '')); + this.urlResults[url] = new SourceMapConsumer(map); + callback(); + }).on('close', (err) => { + if (!sawEnd) { + console.error('Connection terminated prematurely because of: ' + + err.code + ' for url: ' + url); + this.urlResults[url] = null; + callback(); + } + }); + }).on('error', (err) => { + console.error('Could not get response from: ' + url); + this.urlResults[url] = null; + callback(); + }); + } +} module.exports = function(req, res, next) { if (req.url !== '/jsc-profile') { @@ -16,21 +113,28 @@ module.exports = function(req, res, next) { return; } - console.log('Dumping JSC profile information...'); - const dumpName = '/tmp/jsc-profile_' + Date.now() + '.cpuprofile'; - fs.writeFile(dumpName, req.rawBody, (err) => { - var response = ''; - if (err) { - response = - 'An error occured when trying to save the profile at ' + dumpName; - console.error(response, err); - } else { - response = - 'Your profile was generated at\n\n' + dumpName + '\n\n' + - 'Open `Chrome Dev Tools > Profiles > Load` ' - + 'and select the profile to visualize it.'; - console.log(response); - } - res.end(response); + console.log('Received request from JSC profiler, post processing it...'); + let profile = JSON.parse(req.rawBody); + (new TreeTransformator()).transform(profile.head, (newHead) => { + profile.head = newHead; + + console.log('Dumping JSC profile information...'); + const dumpName = '/tmp/jsc-profile_' + Date.now() + '.cpuprofile'; + + fs.writeFile(dumpName, JSON.stringify(profile), (err) => { + let response = ''; + if (err) { + response = + 'An error occured when trying to save the profile at ' + dumpName; + console.error(response, err); + } else { + response = + 'Your profile was generated at\n\n' + dumpName + '\n\n' + + 'Open `Chrome/Atom Dev Tools > Profiles > Load` ' + + 'and select the profile to visualize it.'; + console.log(response); + } + res.end(response); + }); }); };