Skip to content

Commit 18eb932

Browse files
committed
ref(contextlines): store split file in cache
1 parent 17f31c6 commit 18eb932

File tree

3 files changed

+132
-13
lines changed

3 files changed

+132
-13
lines changed

packages/node/src/integrations/contextlines.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { addContextToFrame } from '@sentry/utils';
33
import { readFile } from 'fs';
44
import { LRUMap } from 'lru_map';
55

6-
const FILE_CONTENT_CACHE = new LRUMap<string, string | null>(100);
6+
const FILE_CONTENT_CACHE = new LRUMap<string, string[] | null>(100);
77
const DEFAULT_LINES_OF_CONTEXT = 7;
88

99
// TODO: Replace with promisify when minimum supported node >= v8
@@ -102,8 +102,8 @@ export class ContextLines implements Integration {
102102
// and attempt to add source context to frames.
103103
if (this._contextLines > 0 && event.exception?.values) {
104104
for (const exception of event.exception.values) {
105-
if (exception.stacktrace?.frames) {
106-
this.addSourceContextToFrames(exception.stacktrace.frames);
105+
if (exception.stacktrace && exception.stacktrace.frames) {
106+
await this.addSourceContextToFrames(exception.stacktrace.frames);
107107
}
108108
}
109109
}
@@ -116,12 +116,11 @@ export class ContextLines implements Integration {
116116
for (const frame of frames) {
117117
// Only add context if we have a filename and it hasn't already been added
118118
if (frame.filename && frame.context_line === undefined) {
119-
const sourceFile = FILE_CONTENT_CACHE.get(frame.filename);
119+
const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename);
120120

121-
if (sourceFile) {
121+
if (sourceFileLines) {
122122
try {
123-
const lines = sourceFile.split('\n');
124-
addContextToFrame(lines, frame, this._contextLines);
123+
addContextToFrame(sourceFileLines, frame, this._contextLines);
125124
} catch (e) {
126125
// anomaly, being defensive in case
127126
// unlikely to ever happen in practice but can definitely happen in theory
@@ -134,25 +133,34 @@ export class ContextLines implements Integration {
134133

135134
/**
136135
* Reads file contents and caches them in a global LRU cache.
136+
* If reading fails, mark the file as null in the cache so we don't try again.
137137
*
138138
* @param filename filepath to read content from.
139139
*/
140-
async function _readSourceFile(filename: string): Promise<string | null> {
140+
async function _readSourceFile(filename: string): Promise<string[] | null> {
141141
const cachedFile = FILE_CONTENT_CACHE.get(filename);
142-
// We have a cache hit
142+
143+
// We have already attempted to read this file and failed, do not try again
144+
if (cachedFile === null) {
145+
return null;
146+
}
147+
148+
// We have a cache hit, return it
143149
if (cachedFile !== undefined) {
144150
return cachedFile;
145151
}
146152

147-
let content: string | null = null;
148-
149153
// Guard from throwing if readFile fails, this enables us to use Promise.all and
150154
// not have it short circuiting if one of the promises rejects + since context lines are added
151155
// on a best effort basis, we want to throw here anyways.
156+
157+
// If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it
158+
let content: string[] | null = null;
152159
try {
153-
content = await readTextFileAsync(filename);
160+
const rawFileContents = await readTextFileAsync(filename);
161+
content = rawFileContents.split('\n');
154162
} catch (_) {
155-
//
163+
// if we fail, we will mark the file as null in the cache and short circuit next time we try to read it
156164
}
157165

158166
FILE_CONTENT_CACHE.set(filename, content);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { StackFrame } from '@sentry/types';
2+
import * as fs from 'fs';
3+
4+
import { parseStackFrames } from '../src/eventbuilder';
5+
import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines';
6+
import { defaultStackParser } from '../src/sdk';
7+
import { getError } from './helper/error';
8+
9+
import * as Benchmark from 'benchmark';
10+
11+
const lines = new ContextLines({});
12+
13+
const source = {
14+
exception: {
15+
values: [
16+
{
17+
stacktrace: {
18+
frames: [
19+
{
20+
colno: 1,
21+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/requestdata.test.ts',
22+
lineno: 1,
23+
function: 'fxn1',
24+
},
25+
{
26+
colno: 1,
27+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/requestdata.test.ts',
28+
lineno: 1,
29+
function: 'fxn1',
30+
},
31+
{
32+
colno: 1,
33+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/sdk.test.ts',
34+
lineno: 1,
35+
function: 'fxn1',
36+
},
37+
{
38+
colno: 1,
39+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/stacktrace.test.ts',
40+
lineno: 1,
41+
function: 'fxn1',
42+
},
43+
{
44+
colno: 1,
45+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/utils.test.ts',
46+
lineno: 1,
47+
function: 'fxn1',
48+
},
49+
{
50+
colno: 1,
51+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/transports/http.test.ts',
52+
lineno: 1,
53+
function: 'fxn1',
54+
},
55+
{
56+
colno: 1,
57+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/test/transports/https.test.ts',
58+
lineno: 1,
59+
function: 'fxn1',
60+
},
61+
{
62+
colno: 1,
63+
filename: '/Users/jonasbadalic/code/sentry-javascript/packages/node/tsconfig.json',
64+
lineno: 1,
65+
function: 'fxn1',
66+
},
67+
],
68+
},
69+
},
70+
],
71+
},
72+
};
73+
74+
const suite = new Benchmark.Suite({ setup: resetFileContentCache, teardown: resetFileContentCache });
75+
76+
suite
77+
.add('parallel io', async function () {
78+
await lines.addSourceContext(source);
79+
await lines.addSourceContext(source);
80+
})
81+
.on('cycle', function (event: any) {
82+
console.log(String(event.target));
83+
})
84+
.run({ async: true });

packages/node/test/context-lines.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,31 @@ describe('ContextLines', () => {
107107
expect(readFileSpy).toHaveBeenCalledTimes(0);
108108
});
109109
});
110+
test.only('does not attempt to readfile multiple times if it fails', async () => {
111+
expect.assertions(1);
112+
contextLines = new ContextLines({});
113+
114+
readFileSpy.mockImplementation(() => {
115+
throw new Error("ENOENT: no such file or directory, open '/does/not/exist.js'");
116+
});
117+
118+
await addContext([
119+
{
120+
colno: 1,
121+
filename: '/does/not/exist.js',
122+
lineno: 1,
123+
function: 'fxn1',
124+
},
125+
]);
126+
await addContext([
127+
{
128+
colno: 1,
129+
filename: '/does/not/exist.js',
130+
lineno: 1,
131+
function: 'fxn1',
132+
},
133+
]);
134+
135+
expect(readFileSpy).toHaveBeenCalledTimes(1);
136+
});
110137
});

0 commit comments

Comments
 (0)