react-native/local-cli/server/middleware/heapCaptureMiddleware.js

172 lines
5.4 KiB
JavaScript

/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
/*eslint no-console-disallow: "off"*/
const spawn = require('child_process').spawn;
const fs = require('fs');
const http = require('http');
const path = require('path');
const urlLib = require('url');
const SourceMapConsumer = require('source-map').SourceMapConsumer;
// url: string
// onSuccess: function (SourceMapConsumer)
// onFailure: function (string)
function getSourceMapForUrl(url, onFailure, onSuccess) {
if (!url) {
onFailure('must provide a URL');
return;
}
if (url === 'assets://default_bundle') {
onFailure('Don\'t know how to symbolicate in-app bundle, please load from server');
return;
}
const parsedUrl = urlLib.parse(url);
const mapPath = parsedUrl.pathname.replace(/\.bundle$/, '.map');
const options = {
host: 'localhost',
port: parsedUrl.port,
path: mapPath + parsedUrl.search + '&babelSourcemap=true',
};
http.get(options, (res) => {
res.setEncoding('utf8');
let sawEnd = false;
let resBody = '';
res.on('data', (chunk) => {
resBody += chunk;
}).on('end', () => {
sawEnd = true;
onSuccess(new SourceMapConsumer(resBody));
}).on('close', (err) => {
if (!sawEnd) {
onFailure('Connection terminated prematurely because of: '
+ err.code + ' for url: ' + url);
}
});
}).on('error', (err) => {
onFailure('Could not get response from: ' + url + ', error: ' + err.message);
});
}
// capture: capture object
// onSuccess: function (Map of url -> SourceMapConsumer)
// onFailure: function (string)
function getSourceMapsForCapture(capture, onFailure, onSuccess) {
const urls = new Set();
const sourcemaps = new Map();
for (const id in capture.refs) {
const ref = capture.refs[id];
if ((ref.type === 'ScriptExecutable' ||
ref.type === 'EvalExecutable' ||
ref.type === 'ProgramExecutable' ||
ref.type === 'FunctionExecutable') && ref.value.url) {
urls.add(ref.value.url);
}
}
urls.forEach((url) => {
getSourceMapForUrl(url, onFailure, (sourcemap) => {
sourcemaps.set(url, sourcemap);
urls.delete(url);
if (urls.size === 0) {
onSuccess(sourcemaps);
}
});
});
if (urls.size === 0) {
console.warn('No source information found in capture');
onSuccess(sourcemaps);
}
}
// capture: capture object
// onSuccess: function (capture object)
// onFailure: function (string)
function symbolicateHeapCaptureFunctions(capture, onFailure, onSuccess) {
getSourceMapsForCapture(capture, onFailure, (sourcemaps) => {
for (const id in capture.refs) {
const ref = capture.refs[id];
if (ref.type === 'ScriptExecutable' ||
ref.type === 'EvalExecutable' ||
ref.type === 'ProgramExecutable' ||
ref.type === 'FunctionExecutable') {
const sourcemap = sourcemaps.get(ref.value.url);
if (sourcemap) {
const original = sourcemap.originalPositionFor({
line: ref.value.line,
column: ref.value.col,
});
if (original.name) {
ref.value.name = original.name;
} else if (!ref.value.name) {
ref.value.name = path.posix.basename(original.source || '') + ':' + original.line;
}
ref.value.url = original.source;
ref.value.line = original.line;
ref.value.col = original.column;
}
}
}
onSuccess(capture);
});
}
module.exports = function(req, res, next) {
if (req.url !== '/jscheapcaptureupload') {
next();
return;
}
console.log('symbolicating Heap Capture');
symbolicateHeapCaptureFunctions(JSON.parse(req.rawBody), (err) => {
console.error('Error when symbolicating: ' + err);
},
(capture) => {
const preload = path.join(__dirname, 'heapCapture/preLoadedCapture.js');
fs.writeFileSync(preload, 'var preLoadedCapture = ');
fs.appendFileSync(preload, JSON.stringify(capture));
fs.appendFileSync(preload, ';');
const captureDir = path.join(__dirname, 'heapCapture/captures');
if (!fs.existsSync(captureDir)) {
fs.mkdirSync(captureDir);
}
console.log('Packaging Trace');
var captureHtml = captureDir + '/capture_' + Date.now() + '.html';
var capture = fs.createWriteStream(captureHtml);
var inliner = spawn(
'inliner',
['--nocompress', 'heapCapture.html'],
{ cwd: path.join(__dirname, '/heapCapture/'),
stdio: [ process.stdin, 'pipe', process.stderr ],
});
inliner.stdout.pipe(capture);
inliner.on('error', (err) => {
console.error('Error processing heap capture: ' + err.message);
console.error('make sure you have installed inliner with \'npm install inliner -g\'');
});
inliner.on('exit', (code, signal) => {
if (code === 0) {
var response = captureHtml;
console.log('Heap capture written to: ' + response);
res.end(response);
} else {
var response = 'Error processing heap capture, inliner returned code: ' + code;
console.error(response);
res.statusCode = 500;
res.end(response);
}
});
}
);
};