Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

Commit b647106

Browse files
authored
fix: Add debugging information for sourcemapper (#977)
* fix: Add debugging information for sourcemapper * Fix lint issue * Separate the debugging info related test cases in sourcemapper * Use match instead of indexOf, use normalize for windows in tests * Use indexof for windows in test
1 parent afc5e3a commit b647106

16 files changed

+243
-223
lines changed

src/agent/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,10 @@ export interface ResolvedDebugAgentConfig extends GoogleAuthOptions {
359359
apiUrl?: string;
360360

361361
/**
362-
* Number of times the V8 pauses (could be breakpoint hits) before the
363-
* debugging session is reset. This is to release the memory usage held by V8
364-
* engine for each breakpoint hit to prevent the memory leak. The default
365-
* value is specified in defaultConfig.
362+
* Number of times of the V8 breakpoint hits events before resetting the
363+
* breakpoints. This is to release the memory usage held by V8 engine for each
364+
* breakpoint hit to prevent the memory leak. The default value is specified
365+
* in defaultConfig.
366366
*/
367367
resetV8DebuggerThreshold: number;
368368
}

src/agent/debuglet.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,13 @@ export class Debuglet extends EventEmitter {
396396

397397
let mapper;
398398
try {
399-
mapper = await SourceMapper.create(findResults.mapFiles);
399+
mapper = await SourceMapper.create(findResults.mapFiles, that.logger);
400400
} catch (err3) {
401401
that.logger.error('Error processing the sourcemaps.', err3);
402402
that.emit('initError', err3);
403403
return;
404404
}
405+
405406
that.v8debug = debugapi.create(
406407
that.logger,
407408
that.config,

src/agent/io/sourcemapper.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as path from 'path';
1818
import {promisify} from 'util';
1919
import * as sourceMap from 'source-map';
2020

21+
import {Logger} from '../config';
2122
import {findScriptsFuzzy} from '../util/utils';
2223

2324
const CONCURRENCY = 10;
@@ -167,17 +168,10 @@ async function processSourcemap(
167168
}
168169

169170
export class SourceMapper {
171+
/** Maps each original source path to the corresponding source map info. */
170172
infoMap: Map<string, MapInfoInput>;
171173

172-
/**
173-
* @param {Array.<string>} sourcemapPaths An array of paths to .map sourcemap
174-
* files that should be processed. The paths should be relative to the
175-
* current process's current working directory
176-
* @param {Logger} logger A logger that reports errors that occurred while
177-
* processing the given sourcemap files
178-
* @constructor
179-
*/
180-
constructor() {
174+
constructor(readonly logger: Logger) {
181175
this.infoMap = new Map();
182176
}
183177

@@ -211,6 +205,8 @@ export class SourceMapper {
211205
Array.from(this.infoMap.keys())
212206
);
213207

208+
this.logger.debug(`sourcemapper fuzzy matches: ${matches}`);
209+
214210
if (matches.length === 1) {
215211
return this.infoMap.get(matches[0]) as MapInfoInput;
216212
}
@@ -246,6 +242,8 @@ export class SourceMapper {
246242
colNumber: number,
247243
entry: MapInfoInput
248244
): MapInfoOutput | null {
245+
this.logger.debug(`sourcemapper inputPath: ${inputPath}`);
246+
249247
inputPath = path.normalize(inputPath);
250248

251249
const relPath = path
@@ -273,6 +271,8 @@ export class SourceMapper {
273271
column: colNumber, // to be zero-based
274272
};
275273

274+
this.logger.debug(`sourcemapper sourcePos: ${JSON.stringify(sourcePos)}`);
275+
276276
const allPos = entry.mapConsumer.allGeneratedPositionsFor(sourcePos);
277277
/*
278278
* Based on testing, it appears that the following code is needed to
@@ -290,6 +290,8 @@ export class SourceMapper {
290290
})
291291
: entry.mapConsumer.generatedPositionFor(sourcePos);
292292

293+
this.logger.debug(`sourcemapper mappedPos: ${JSON.stringify(mappedPos)}`);
294+
293295
return {
294296
file: entry.outputFile,
295297
line: (mappedPos.line ?? 0) - 1, // convert the one-based line numbers returned
@@ -305,11 +307,32 @@ export class SourceMapper {
305307
// output
306308
};
307309
}
310+
311+
/** Prints the debugging information of the source mapper to the logger. */
312+
debug() {
313+
this.logger.debug('Printing source mapper debugging information ...');
314+
for (const [key, value] of this.infoMap) {
315+
this.logger.debug(` source ${key}:`);
316+
this.logger.debug(` outputFile: ${value.outputFile}`);
317+
this.logger.debug(` mapFile: ${value.mapFile}`);
318+
this.logger.debug(` sources: ${value.sources}`);
319+
}
320+
}
308321
}
309322

310-
export async function create(sourcemapPaths: string[]): Promise<SourceMapper> {
323+
/**
324+
* @param {Array.<string>} sourcemapPaths An array of paths to .map sourcemap
325+
* files that should be processed. The paths should be relative to the
326+
* current process's current working directory
327+
* @param {Logger} logger A logger that reports errors that occurred while
328+
* processing the given sourcemap files
329+
*/
330+
export async function create(
331+
sourcemapPaths: string[],
332+
logger: Logger
333+
): Promise<SourceMapper> {
311334
const limit = pLimit(CONCURRENCY);
312-
const mapper = new SourceMapper();
335+
const mapper = new SourceMapper(logger);
313336
const promises = sourcemapPaths.map(path =>
314337
limit(() => processSourcemap(mapper.infoMap, path))
315338
);
@@ -320,5 +343,6 @@ export async function create(sourcemapPaths: string[]): Promise<SourceMapper> {
320343
'An error occurred while processing the sourcemap files' + err
321344
);
322345
}
346+
mapper.debug();
323347
return mapper;
324348
}

src/agent/v8/inspector-debugapi.ts

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@ import * as utils from '../util/utils';
3434
import * as debugapi from './debugapi';
3535
import {V8Inspector} from './v8inspector';
3636

37+
/**
38+
* An interface that describes options that set behavior when interacting with
39+
* the V8 Inspector API.
40+
*/
41+
interface InspectorOptions {
42+
/**
43+
* Whether to add a 'file://' prefix to a URL when setting breakpoints.
44+
*/
45+
useWellFormattedUrl: boolean;
46+
}
47+
48+
/** Data related to the v8 inspector. */
49+
interface V8Data {
50+
session: inspector.Session;
51+
// Options for behavior when interfacing with the Inspector API.
52+
inspectorOptions: InspectorOptions;
53+
inspector: V8Inspector;
54+
// Store the v8 setBreakpoint parameters for each v8 breakpoint so that later
55+
// the recorded parameters can be used to reset the breakpoints.
56+
setBreakpointsParams: {
57+
[
58+
v8BreakpointId: string
59+
]: inspector.Debugger.SetBreakpointByUrlParameterType;
60+
};
61+
}
62+
3763
/**
3864
* In older versions of Node, the script source as seen by the Inspector
3965
* backend is wrapped in `require('module').wrapper`, and in new versions
@@ -73,9 +99,8 @@ export class InspectorDebugApi implements debugapi.DebugApi {
7399
// stackdriver breakpoint id.
74100
breakpointMapper: {[id: string]: stackdriver.BreakpointId[]} = {};
75101
numBreakpoints = 0;
76-
77-
// The wrapper class that interacts with the V8 debugger.
78-
inspector: V8Inspector;
102+
numBreakpointHitsBeforeReset = 0;
103+
v8: V8Data;
79104

80105
constructor(
81106
logger: consoleLogLevel.Logger,
@@ -87,28 +112,36 @@ export class InspectorDebugApi implements debugapi.DebugApi {
87112
this.config = config;
88113
this.fileStats = jsFiles;
89114
this.sourcemapper = sourcemapper;
90-
this.inspector = new V8Inspector(
91-
/* logger=*/ logger,
92-
/*useWellFormattedUrl=*/ utils.satisfies(process.version, '>10.11.0'),
93-
/*resetV8DebuggerThreshold=*/ this.config.resetV8DebuggerThreshold,
94-
/*onScriptParsed=*/
95-
scriptParams => {
96-
this.scriptMapper[scriptParams.scriptId] = scriptParams;
97-
},
98-
/*onPaused=*/
99-
messageParams => {
100-
try {
101-
this.handleDebugPausedEvent(messageParams);
102-
} catch (error) {
103-
this.logger.error(error);
104-
}
105-
}
106-
);
115+
this.scriptMapper = {};
116+
this.v8 = this.createV8Data();
107117
}
108118

109-
/** Disconnects and marks the current V8 data as not connected. */
110-
disconnect(): void {
111-
this.inspector.detach();
119+
/** Creates a new V8 Debugging session and the related data. */
120+
private createV8Data(): V8Data {
121+
const session = new inspector.Session();
122+
session.connect();
123+
session.on('Debugger.scriptParsed', script => {
124+
this.scriptMapper[script.params.scriptId] = script.params;
125+
});
126+
session.post('Debugger.enable');
127+
session.post('Debugger.setBreakpointsActive', {active: true});
128+
session.on('Debugger.paused', message => {
129+
try {
130+
this.handleDebugPausedEvent(message.params);
131+
} catch (error) {
132+
this.logger.error(error);
133+
}
134+
});
135+
136+
return {
137+
session,
138+
inspectorOptions: {
139+
// Well-Formatted URL is required in Node 10.11.1+.
140+
useWellFormattedUrl: utils.satisfies(process.version, '>10.11.0'),
141+
},
142+
inspector: new V8Inspector(session),
143+
setBreakpointsParams: {},
144+
};
112145
}
113146

114147
set(
@@ -235,7 +268,8 @@ export class InspectorDebugApi implements debugapi.DebugApi {
235268
if (!this.breakpointMapper[breakpointData.id]) {
236269
// When breakpointmapper does not countain current v8/inspector breakpoint
237270
// id, we should remove this breakpoint from v8.
238-
result = this.inspector.removeBreakpoint(breakpointData.id);
271+
result = this.v8.inspector.removeBreakpoint(breakpointData.id);
272+
delete this.v8.setBreakpointsParams[breakpointData.id];
239273
}
240274
delete this.breakpoints[breakpoint.id];
241275
delete this.listeners[breakpoint.id];
@@ -312,6 +346,10 @@ export class InspectorDebugApi implements debugapi.DebugApi {
312346
this.listeners[breakpoint.id] = {enabled: true, listener};
313347
}
314348

349+
disconnect(): void {
350+
this.v8.session.disconnect();
351+
}
352+
315353
numBreakpoints_(): number {
316354
// Tracks the number of stackdriver breakpoints.
317355
return Object.keys(this.breakpoints).length;
@@ -499,7 +537,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
499537
let v8BreakpointId; // v8/inspector breakpoint id
500538
if (!this.locationMapper[locationStr]) {
501539
// The first time when a breakpoint was set to this location.
502-
const rawUrl = this.inspector.shouldUseWellFormattedUrl()
540+
const rawUrl = this.v8.inspectorOptions.useWellFormattedUrl
503541
? `file://${matchingScript}`
504542
: matchingScript;
505543
// on windows on Node 11+, the url must start with file:///
@@ -508,17 +546,19 @@ export class InspectorDebugApi implements debugapi.DebugApi {
508546
process.platform === 'win32' && utils.satisfies(process.version, '>=11')
509547
? rawUrl.replace(/^file:\/\//, 'file:///').replace(/\\/g, '/')
510548
: rawUrl;
511-
const res = this.inspector.setBreakpointByUrl({
549+
const params = {
512550
lineNumber: line - 1,
513551
url,
514552
columnNumber: column - 1,
515553
condition: breakpoint.condition || undefined,
516-
});
554+
};
555+
const res = this.v8.inspector.setBreakpointByUrl(params);
517556
if (res.error || !res.response) {
518557
// Error case.
519558
return null;
520559
}
521560
v8BreakpointId = res.response.breakpointId;
561+
this.v8.setBreakpointsParams[v8BreakpointId] = params;
522562

523563
this.locationMapper[locationStr] = [];
524564
this.breakpointMapper[v8BreakpointId] = [];
@@ -606,7 +646,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
606646
const evaluatedExpressions = breakpoint.expressions.map(exp => {
607647
// returnByValue is set to true here so that the JSON string of the
608648
// value will be returned to log.
609-
const result = state.evaluate(exp, frame, that.inspector, true);
649+
const result = state.evaluate(exp, frame, that.v8.inspector, true);
610650
if (result.error) {
611651
return result.error;
612652
} else {
@@ -621,7 +661,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
621661
breakpoint,
622662
this.config,
623663
this.scriptMapper,
624-
this.inspector
664+
this.v8.inspector
625665
);
626666
if (
627667
breakpoint.location &&
@@ -655,5 +695,37 @@ export class InspectorDebugApi implements debugapi.DebugApi {
655695
} catch (e) {
656696
this.logger.warn('Internal V8 error on breakpoint event: ' + e);
657697
}
698+
699+
this.tryResetV8Debugger();
700+
}
701+
702+
/**
703+
* Periodically resets breakpoints to prevent memory leaks in V8 (for holding
704+
* contexts of previous breakpoint hits).
705+
*/
706+
private tryResetV8Debugger() {
707+
this.numBreakpointHitsBeforeReset += 1;
708+
if (
709+
this.numBreakpointHitsBeforeReset < this.config.resetV8DebuggerThreshold
710+
) {
711+
return;
712+
}
713+
this.numBreakpointHitsBeforeReset = 0;
714+
715+
const storedParams = this.v8.setBreakpointsParams;
716+
717+
// Re-connect the session to clean the memory usage.
718+
this.disconnect();
719+
this.scriptMapper = {};
720+
this.v8 = this.createV8Data();
721+
this.v8.setBreakpointsParams = storedParams;
722+
723+
// Setting the v8 breakpoints again according to the stored parameters.
724+
for (const params of Object.values(storedParams)) {
725+
const res = this.v8.inspector.setBreakpointByUrl(params);
726+
if (res.error || !res.response) {
727+
this.logger.error('Error upon re-setting breakpoint: ' + res);
728+
}
729+
}
658730
}
659731
}

0 commit comments

Comments
 (0)