Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Parsestack refactor + tests to fix memory explosion #320

Merged
merged 4 commits into from
May 17, 2017
Merged
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
coverage/
test/instrumentation/node-*
test/manual/largeModule.js
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@ node_js:
- "5"
- "6"
- "7"
before_install:
- "npm install --upgrade npm -g"
script: npm run test-full
98 changes: 45 additions & 53 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ var transports = require('./transports');
var path = require('path');
var lsmod = require('lsmod');
var stacktrace = require('stack-trace');
var Promise = require('promise/domains');

var ravenVersion = require('../package.json').version;

Expand Down Expand Up @@ -142,84 +141,77 @@ function getModule(filename, base) {
return file;
}

function parseLines(lines, frame) {
frame.pre_context = lines.slice(Math.max(0, frame.lineno - (LINES_OF_CONTEXT + 1)), frame.lineno - 1);
frame.context_line = lines[frame.lineno - 1];
frame.post_context = lines.slice(frame.lineno, frame.lineno + LINES_OF_CONTEXT);
}
function readSourceFiles(filenames, cb) {
// we're relying on filenames being de-duped already
if (filenames.length === 0) return setTimeout(cb, 0, {});

function readFilePromise(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, function (err, file) {
return err ? reject(err) : resolve(file);
var sourceFiles = {};
var numFilesToRead = filenames.length;
return filenames.forEach(function (filename) {
fs.readFile(filename, function (readErr, file) {
if (!readErr) sourceFiles[filename] = file.toString().split('\n');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should you wrap this line in a try catch, in case toString throws due to a weird file? to ensure that the callback always fires.

Copy link
Contributor Author

@LewisJEllis LewisJEllis May 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!readErr) guarantees that file is a Buffer according to fs.readFile docs, and Buffers have toString (as linked) even if they're empty, so I'm not concerned

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok 👍

if (--numFilesToRead === 0) cb(sourceFiles);
});
});
}

function parseStack(err, cb) {
var frames = [],
cache = {};

if (!err) {
return cb(frames);
}
if (!err) return cb([]);

var stack = stacktrace.parse(err);

// check to make sure that the stack is what we need it to be.
if (!stack || !Array.isArray(stack) || !stack.length || !stack[0].getFileName) {
// lol, stack is fucked
return cb(frames);
// the stack is not the useful thing we were expecting :/
return cb([]);
}

var callbacks = stack.length;

// Sentry requires the stack trace to be from oldest to newest
// Sentry expects the stack trace to be oldest -> newest, v8 provides newest -> oldest
stack.reverse();

return stack.forEach(function (line, index) {
var frames = [];
var filesToRead = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could new Object(null) here if you wanted idk

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

stack.forEach(function (line) {
var frame = {
filename: line.getFileName() || '',
lineno: line.getLineNumber(),
colno: line.getColumnNumber(),
'function': getFunction(line),
},
isInternal = line.isNative() ||
frame.filename[0] !== '/' &&
frame.filename[0] !== '.' &&
frame.filename.indexOf(':\\') !== 1;
filename: line.getFileName() || '',
lineno: line.getLineNumber(),
colno: line.getColumnNumber(),
'function': getFunction(line),
};

var isInternal = line.isNative() ||
frame.filename[0] !== '/' &&
frame.filename[0] !== '.' &&
frame.filename.indexOf(':\\') !== 1;

// in_app is all that's not an internal Node function or a module within node_modules
// note that isNative appears to return true even for node core libraries
// see https://github.com/getsentry/raven-node/issues/176
frame.in_app = !isInternal && frame.filename.indexOf('node_modules/') === -1;

// Extract a module name based on the filename
if (frame.filename) frame.module = getModule(frame.filename);

// internal Node files are not full path names. Ignore them.
if (isInternal) {
frames[index] = frame;
if (--callbacks === 0) cb(frames);
return;
if (frame.filename) {
frame.module = getModule(frame.filename);
if (!isInternal) filesToRead[frame.filename] = true;
}

if (!cache[frame.filename]) {
cache[frame.filename] = readFilePromise(frame.filename)
.then(function (file) {
return file.toString().split('\n');
}, function () {
return null;
});
}
frames.push(frame);
});

cache[frame.filename].then(function (file) {
if (file) {
parseLines(file, frame);
return readSourceFiles(Object.keys(filesToRead), function (sourceFiles) {
frames.forEach(function (frame) {
if (frame.filename && sourceFiles[frame.filename]) {
var lines = sourceFiles[frame.filename];
try {
frame.pre_context = lines.slice(Math.max(0, frame.lineno - (LINES_OF_CONTEXT + 1)), frame.lineno - 1);
frame.context_line = lines[frame.lineno - 1];
frame.post_context = lines.slice(frame.lineno, frame.lineno + LINES_OF_CONTEXT);
} catch (e) {
// anomaly, being defensive in case
Copy link
Contributor

@MaxBittker MaxBittker May 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want to set the properties to be empty strings /arrays here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, i think we'd rather not send them to the server at all; not sure what behavior diff would be but not sending them at all is what we do for native frames so i'll stick to that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

// unlikely to ever happen in practice but can definitely happen in theory
}
}
frames[index] = frame;
if (--callbacks === 0) cb(frames);
});

cb(frames);
});
}

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"cookie": "0.3.1",
"json-stringify-safe": "5.0.1",
"lsmod": "1.0.0",
"promise": "^7.1.1",
"stack-trace": "0.0.9",
"timed-out": "4.0.1",
"uuid": "3.0.0"
Expand Down
18 changes: 18 additions & 0 deletions test/manual/express-patient.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ app.get('/capture', function (req, res, next) {
next();
});

app.get('/capture_large_source', function (req, res, next) {
nockRequest();

// largeModule.run recurses 1000 times, largeModule is a 5MB file
// if we read the largeModule source once for each frame, we'll use a ton of memory
var largeModule = require('./largeModule');

try {
largeModule.run();
} catch (e) {
Raven.captureException(e);
}

memwatch.gc();
res.textToSend = 'capturing an exception!';
next();
});

app.use(function (req, res, next) {
if (req.query.doError) {
nockRequest();
Expand Down
Loading