From d656e6e880fa60753a6dc558f0fd01a04830fe95 Mon Sep 17 00:00:00 2001 From: Louis Ye Date: Wed, 16 Jun 2021 11:21:12 -0400 Subject: [PATCH 1/5] fix: Add debugging information for sourcemapper --- src/agent/debuglet.ts | 3 +- src/agent/io/sourcemapper.ts | 43 +++++++++++++++------ test/test-circular.ts | 2 +- test/test-duplicate-expressions.ts | 2 +- test/test-duplicate-nested-expressions.ts | 2 +- test/test-evaluated-expressions.ts | 2 +- test/test-expression-side-effect.ts | 2 +- test/test-fat-arrow.ts | 2 +- test/test-max-data-size.ts | 2 +- test/test-sourcemapper.ts | 46 +++++++++++++++++++---- test/test-this-context.ts | 2 +- test/test-try-catch.ts | 2 +- test/test-v8debugapi.ts | 6 +-- 13 files changed, 84 insertions(+), 32 deletions(-) diff --git a/src/agent/debuglet.ts b/src/agent/debuglet.ts index 691b58fe..da3ca1ec 100644 --- a/src/agent/debuglet.ts +++ b/src/agent/debuglet.ts @@ -396,12 +396,13 @@ export class Debuglet extends EventEmitter { let mapper; try { - mapper = await SourceMapper.create(findResults.mapFiles); + mapper = await SourceMapper.create(findResults.mapFiles, that.logger); } catch (err3) { that.logger.error('Error processing the sourcemaps.', err3); that.emit('initError', err3); return; } + that.v8debug = debugapi.create( that.logger, that.config, diff --git a/src/agent/io/sourcemapper.ts b/src/agent/io/sourcemapper.ts index df58aec4..92319656 100644 --- a/src/agent/io/sourcemapper.ts +++ b/src/agent/io/sourcemapper.ts @@ -18,6 +18,7 @@ import * as path from 'path'; import {promisify} from 'util'; import * as sourceMap from 'source-map'; +import {Logger} from '../config'; import {findScriptsFuzzy} from '../util/utils'; const CONCURRENCY = 10; @@ -167,17 +168,10 @@ async function processSourcemap( } export class SourceMapper { + /** Maps each original source path to the corresponding source map info. */ infoMap: Map; - /** - * @param {Array.} sourcemapPaths An array of paths to .map sourcemap - * files that should be processed. The paths should be relative to the - * current process's current working directory - * @param {Logger} logger A logger that reports errors that occurred while - * processing the given sourcemap files - * @constructor - */ - constructor() { + constructor(readonly logger: Logger) { this.infoMap = new Map(); } @@ -211,6 +205,8 @@ export class SourceMapper { Array.from(this.infoMap.keys()) ); + this.logger.debug(`sourcemapper fuzzy matches: ${matches}`); + if (matches.length === 1) { return this.infoMap.get(matches[0]) as MapInfoInput; } @@ -246,6 +242,8 @@ export class SourceMapper { colNumber: number, entry: MapInfoInput ): MapInfoOutput | null { + this.logger.debug(`sourcemapper inputPath: ${inputPath}`); + inputPath = path.normalize(inputPath); const relPath = path @@ -273,6 +271,8 @@ export class SourceMapper { column: colNumber, // to be zero-based }; + this.logger.debug(`sourcemapper sourcePos: ${JSON.stringify(sourcePos)}`); + const allPos = entry.mapConsumer.allGeneratedPositionsFor(sourcePos); /* * Based on testing, it appears that the following code is needed to @@ -290,6 +290,8 @@ export class SourceMapper { }) : entry.mapConsumer.generatedPositionFor(sourcePos); + this.logger.debug(`sourcemapper mappedPos: ${JSON.stringify(mappedPos)}`); + return { file: entry.outputFile, line: (mappedPos.line ?? 0) - 1, // convert the one-based line numbers returned @@ -305,11 +307,29 @@ export class SourceMapper { // output }; } + + /** Prints the debugging information of the source mapper to the logger. */ + debug() { + this.logger.debug('Printing source mapper debugging information ...'); + for (const [key, value] of this.infoMap) { + this.logger.debug(` source ${key}:`); + this.logger.debug(` outputFile: ${value.outputFile}`); + this.logger.debug(` mapFile: ${value.mapFile}`); + this.logger.debug(` sources: ${value.sources}`); + } + } } -export async function create(sourcemapPaths: string[]): Promise { +/** + * @param {Array.} sourcemapPaths An array of paths to .map sourcemap + * files that should be processed. The paths should be relative to the + * current process's current working directory + * @param {Logger} logger A logger that reports errors that occurred while + * processing the given sourcemap files + */ +export async function create(sourcemapPaths: string[], logger: Logger): Promise { const limit = pLimit(CONCURRENCY); - const mapper = new SourceMapper(); + const mapper = new SourceMapper(logger); const promises = sourcemapPaths.map(path => limit(() => processSourcemap(mapper.infoMap, path)) ); @@ -320,5 +340,6 @@ export async function create(sourcemapPaths: string[]): Promise { 'An error occurred while processing the sourcemap files' + err ); } + mapper.debug(); return mapper; } diff --git a/test/test-circular.ts b/test/test-circular.ts index a6bcb93f..2717cacc 100644 --- a/test/test-circular.ts +++ b/test/test-circular.ts @@ -71,7 +71,7 @@ maybeDescribe(__filename, () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); api = debugapi.create( logger, config, diff --git a/test/test-duplicate-expressions.ts b/test/test-duplicate-expressions.ts index 7423473d..a88aa2b2 100644 --- a/test/test-duplicate-expressions.ts +++ b/test/test-duplicate-expressions.ts @@ -63,7 +63,7 @@ describe(__filename, () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO: Handle the case when mapper is undefined // TODO: Handle the case when v8debugapi.create returns null api = debugapi.create( diff --git a/test/test-duplicate-nested-expressions.ts b/test/test-duplicate-nested-expressions.ts index 978475df..99f20906 100644 --- a/test/test-duplicate-nested-expressions.ts +++ b/test/test-duplicate-nested-expressions.ts @@ -58,7 +58,7 @@ describe(__filename, () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO: Handle the case when mapper is undefined // TODO: Handle the case when v8debugapi.create returns null api = debugapi.create( diff --git a/test/test-evaluated-expressions.ts b/test/test-evaluated-expressions.ts index 40b8e96e..b6fe07ee 100644 --- a/test/test-evaluated-expressions.ts +++ b/test/test-evaluated-expressions.ts @@ -41,7 +41,7 @@ describe('debugger provides useful information', () => { scanner.scan(config.workingDirectory, /\.js$/).then(async fileStats => { const jsStats = fileStats.selectStats(/\.js$/); const mapFiles = fileStats.selectFiles(/\.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); assert(mapper); api = debugapi.create(logger, config, jsStats, mapper!); done(); diff --git a/test/test-expression-side-effect.ts b/test/test-expression-side-effect.ts index efb0ffb2..7c6abe3b 100644 --- a/test/test-expression-side-effect.ts +++ b/test/test-expression-side-effect.ts @@ -43,7 +43,7 @@ describe('evaluating expressions', () => { scanner.scan(config.workingDirectory, /\.js$/).then(async fileStats => { const jsStats = fileStats.selectStats(/\.js$/); const mapFiles = fileStats.selectFiles(/\.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); assert(mapper); api = debugapi.create(logger, config, jsStats, mapper!); done(); diff --git a/test/test-fat-arrow.ts b/test/test-fat-arrow.ts index 3e97e074..c8786406 100644 --- a/test/test-fat-arrow.ts +++ b/test/test-fat-arrow.ts @@ -60,7 +60,7 @@ describe(__filename, () => { const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); // TODO: Determine if the err parameter should be used. - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO: Handle the case when mapper is undefined // TODO: Handle the case when v8debugapi.create returns null api = debugapi.create( diff --git a/test/test-max-data-size.ts b/test/test-max-data-size.ts index 2d105ba1..f622930d 100644 --- a/test/test-max-data-size.ts +++ b/test/test-max-data-size.ts @@ -47,7 +47,7 @@ describe('maxDataSize', () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO: Handle the case when mapper is undefined // TODO: Handle the case when v8debugapi.create returns null api = debugapi.create( diff --git a/test/test-sourcemapper.ts b/test/test-sourcemapper.ts index 0fdf738d..cf800a69 100644 --- a/test/test-sourcemapper.ts +++ b/test/test-sourcemapper.ts @@ -20,6 +20,8 @@ import * as path from 'path'; import * as sm from '../src/agent/io/sourcemapper'; +import {MockLogger} from './mock-logger'; + const BASE_PATH = path.join(__dirname, 'fixtures', 'sourcemapper'); const QUICK_MILLISECONDS = 300; @@ -52,16 +54,28 @@ function testTool( const outputFilePath = path.join(BASE_PATH, relativeOutputFilePath); describe('sourcemapper for tool ' + tool, () => { + const logger = new MockLogger(); let sourcemapper: sm.SourceMapper; - it('for tool ' + tool, async () => { - const start = Date.now(); - sourcemapper = await sm.create([mapFilePath]); - assert( - Date.now() - start < QUICK_MILLISECONDS, - 'should create the SourceMapper quickly' - ); - }); + it('for tool ' + tool + ' sourcemapper should be created correctly', + async () => { + const start = Date.now(); + sourcemapper = await sm.create([mapFilePath], logger); + assert( + Date.now() - start < QUICK_MILLISECONDS, + 'should create the SourceMapper quickly'); + + // Verify if the debugging information is correctly printed. + assert.notStrictEqual( + logger.debugs[0].args[0].indexOf('debugging information ...'), -1); + assert.notStrictEqual(logger.debugs[1].args[0].indexOf('source '), -1); + assert.notStrictEqual( + logger.debugs[2].args[0].indexOf('outputFile'), -1); + assert.notStrictEqual(logger.debugs[3].args[0].indexOf('mapFile'), -1); + assert.notStrictEqual(logger.debugs[4].args[0].indexOf('sources'), -1); + assert.strictEqual( + logger.debugs.length, sourcemapper.infoMap.size * 4 + 1); + }); it( 'for tool ' + @@ -125,6 +139,22 @@ function testTool( 0, mapInfoInput! ); + + // Verify if the debugging information is correctly printed. + const debugsLength = logger.debugs.length; + assert.notStrictEqual( + logger.debugs[debugsLength - 3].args[0].indexOf( + 'sourcemapper inputPath:'), + -1); + assert.notStrictEqual( + logger.debugs[debugsLength - 2].args[0].indexOf( + 'sourcemapper sourcePos: {'), + -1); + assert.notStrictEqual( + logger.debugs[debugsLength - 1].args[0].indexOf( + 'sourcemapper mappedPos: {'), + -1); + assert.notStrictEqual( info, null, diff --git a/test/test-this-context.ts b/test/test-this-context.ts index ea49e2b0..a9991352 100644 --- a/test/test-this-context.ts +++ b/test/test-this-context.ts @@ -58,7 +58,7 @@ describe(__filename, () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO: Handle the case when mapper is undefined // TODO: Handle the case when v8debugapi.create returns null api = debugapi.create( diff --git a/test/test-try-catch.ts b/test/test-try-catch.ts index 053ffbd8..d17495d5 100644 --- a/test/test-try-catch.ts +++ b/test/test-try-catch.ts @@ -57,7 +57,7 @@ describe(__filename, () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO: Handle the case when mapper is undefined // TODO: Handle the case when v8debugapi.create returns null api = debugapi.create( diff --git a/test/test-v8debugapi.ts b/test/test-v8debugapi.ts index 54a8a35a..ce119775 100644 --- a/test/test-v8debugapi.ts +++ b/test/test-v8debugapi.ts @@ -178,7 +178,7 @@ describe('debugapi selection', () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.js.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO(dominickramer): Handle the case when mapper is undefined. // TODO(dominickramer): Handle the case when v8debugapi.create // returns null @@ -222,7 +222,7 @@ describeFn('debugapi selection on Node >=10', () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$/); const mapFiles = fileStats.selectFiles(/.js.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); assert(mapper); api = debugapi.create(logger, config, jsStats, mapper!); // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -250,7 +250,7 @@ describe('v8debugapi', () => { assert.strictEqual(fileStats.errors().size, 0); const jsStats = fileStats.selectStats(/.js$|.jsz$/); const mapFiles = fileStats.selectFiles(/.js.map$/, process.cwd()); - const mapper = await SourceMapper.create(mapFiles); + const mapper = await SourceMapper.create(mapFiles, logger); // TODO(dominickramer): Handle the case when mapper is undefined. // TODO(dominickramer): Handle the case when v8debugapi.create From 410e8eb9084a72203e63fb5df8d43adc8d183c54 Mon Sep 17 00:00:00 2001 From: Louis Ye Date: Wed, 16 Jun 2021 11:42:28 -0400 Subject: [PATCH 2/5] Fix lint issue --- src/agent/io/sourcemapper.ts | 5 ++- test/test-sourcemapper.ts | 69 ++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/agent/io/sourcemapper.ts b/src/agent/io/sourcemapper.ts index 92319656..3be5316a 100644 --- a/src/agent/io/sourcemapper.ts +++ b/src/agent/io/sourcemapper.ts @@ -327,7 +327,10 @@ export class SourceMapper { * @param {Logger} logger A logger that reports errors that occurred while * processing the given sourcemap files */ -export async function create(sourcemapPaths: string[], logger: Logger): Promise { +export async function create( + sourcemapPaths: string[], + logger: Logger +): Promise { const limit = pLimit(CONCURRENCY); const mapper = new SourceMapper(logger); const promises = sourcemapPaths.map(path => diff --git a/test/test-sourcemapper.ts b/test/test-sourcemapper.ts index cf800a69..56e517cd 100644 --- a/test/test-sourcemapper.ts +++ b/test/test-sourcemapper.ts @@ -57,25 +57,34 @@ function testTool( const logger = new MockLogger(); let sourcemapper: sm.SourceMapper; - it('for tool ' + tool + ' sourcemapper should be created correctly', - async () => { - const start = Date.now(); - sourcemapper = await sm.create([mapFilePath], logger); - assert( - Date.now() - start < QUICK_MILLISECONDS, - 'should create the SourceMapper quickly'); + it( + 'for tool ' + tool + ' sourcemapper should be created correctly', + async () => { + const start = Date.now(); + sourcemapper = await sm.create([mapFilePath], logger); + assert( + Date.now() - start < QUICK_MILLISECONDS, + 'should create the SourceMapper quickly' + ); - // Verify if the debugging information is correctly printed. - assert.notStrictEqual( - logger.debugs[0].args[0].indexOf('debugging information ...'), -1); - assert.notStrictEqual(logger.debugs[1].args[0].indexOf('source '), -1); - assert.notStrictEqual( - logger.debugs[2].args[0].indexOf('outputFile'), -1); - assert.notStrictEqual(logger.debugs[3].args[0].indexOf('mapFile'), -1); - assert.notStrictEqual(logger.debugs[4].args[0].indexOf('sources'), -1); - assert.strictEqual( - logger.debugs.length, sourcemapper.infoMap.size * 4 + 1); - }); + // Verify if the debugging information is correctly printed. + assert.notStrictEqual( + logger.debugs[0].args[0].indexOf('debugging information ...'), + -1 + ); + assert.notStrictEqual(logger.debugs[1].args[0].indexOf('source '), -1); + assert.notStrictEqual( + logger.debugs[2].args[0].indexOf('outputFile'), + -1 + ); + assert.notStrictEqual(logger.debugs[3].args[0].indexOf('mapFile'), -1); + assert.notStrictEqual(logger.debugs[4].args[0].indexOf('sources'), -1); + assert.strictEqual( + logger.debugs.length, + sourcemapper.infoMap.size * 4 + 1 + ); + } + ); it( 'for tool ' + @@ -143,17 +152,23 @@ function testTool( // Verify if the debugging information is correctly printed. const debugsLength = logger.debugs.length; assert.notStrictEqual( - logger.debugs[debugsLength - 3].args[0].indexOf( - 'sourcemapper inputPath:'), - -1); + logger.debugs[debugsLength - 3].args[0].indexOf( + 'sourcemapper inputPath:' + ), + -1 + ); assert.notStrictEqual( - logger.debugs[debugsLength - 2].args[0].indexOf( - 'sourcemapper sourcePos: {'), - -1); + logger.debugs[debugsLength - 2].args[0].indexOf( + 'sourcemapper sourcePos: {' + ), + -1 + ); assert.notStrictEqual( - logger.debugs[debugsLength - 1].args[0].indexOf( - 'sourcemapper mappedPos: {'), - -1); + logger.debugs[debugsLength - 1].args[0].indexOf( + 'sourcemapper mappedPos: {' + ), + -1 + ); assert.notStrictEqual( info, From d478a120714ff41190e6ad2ad0386e3cc7aa939e Mon Sep 17 00:00:00 2001 From: Louis Ye Date: Fri, 18 Jun 2021 10:46:53 -0400 Subject: [PATCH 3/5] Separate the debugging info related test cases in sourcemapper --- src/agent/config.ts | 8 +- src/agent/v8/inspector-debugapi.ts | 130 +++++++++++++++++++++------- src/agent/v8/v8inspector.ts | 134 +++-------------------------- test/test-sourcemapper.ts | 133 +++++++++++++++++----------- test/test-v8debugapi.ts | 46 +--------- 5 files changed, 203 insertions(+), 248 deletions(-) diff --git a/src/agent/config.ts b/src/agent/config.ts index 6b325a14..45ef4f36 100644 --- a/src/agent/config.ts +++ b/src/agent/config.ts @@ -359,10 +359,10 @@ export interface ResolvedDebugAgentConfig extends GoogleAuthOptions { apiUrl?: string; /** - * Number of times the V8 pauses (could be breakpoint hits) before the - * debugging session is reset. This is to release the memory usage held by V8 - * engine for each breakpoint hit to prevent the memory leak. The default - * value is specified in defaultConfig. + * Number of times of the V8 breakpoint hits events before resetting the + * breakpoints. This is to release the memory usage held by V8 engine for each + * breakpoint hit to prevent the memory leak. The default value is specified + * in defaultConfig. */ resetV8DebuggerThreshold: number; } diff --git a/src/agent/v8/inspector-debugapi.ts b/src/agent/v8/inspector-debugapi.ts index c47522fb..8e1b4807 100644 --- a/src/agent/v8/inspector-debugapi.ts +++ b/src/agent/v8/inspector-debugapi.ts @@ -34,6 +34,32 @@ import * as utils from '../util/utils'; import * as debugapi from './debugapi'; import {V8Inspector} from './v8inspector'; +/** + * An interface that describes options that set behavior when interacting with + * the V8 Inspector API. + */ +interface InspectorOptions { + /** + * Whether to add a 'file://' prefix to a URL when setting breakpoints. + */ + useWellFormattedUrl: boolean; +} + +/** Data related to the v8 inspector. */ +interface V8Data { + session: inspector.Session; + // Options for behavior when interfacing with the Inspector API. + inspectorOptions: InspectorOptions; + inspector: V8Inspector; + // Store the v8 setBreakpoint parameters for each v8 breakpoint so that later + // the recorded parameters can be used to reset the breakpoints. + setBreakpointsParams: { + [ + v8BreakpointId: string + ]: inspector.Debugger.SetBreakpointByUrlParameterType; + }; +} + /** * In older versions of Node, the script source as seen by the Inspector * backend is wrapped in `require('module').wrapper`, and in new versions @@ -73,9 +99,8 @@ export class InspectorDebugApi implements debugapi.DebugApi { // stackdriver breakpoint id. breakpointMapper: {[id: string]: stackdriver.BreakpointId[]} = {}; numBreakpoints = 0; - - // The wrapper class that interacts with the V8 debugger. - inspector: V8Inspector; + numBreakpointHitsBeforeReset = 0; + v8: V8Data; constructor( logger: consoleLogLevel.Logger, @@ -87,28 +112,36 @@ export class InspectorDebugApi implements debugapi.DebugApi { this.config = config; this.fileStats = jsFiles; this.sourcemapper = sourcemapper; - this.inspector = new V8Inspector( - /* logger=*/ logger, - /*useWellFormattedUrl=*/ utils.satisfies(process.version, '>10.11.0'), - /*resetV8DebuggerThreshold=*/ this.config.resetV8DebuggerThreshold, - /*onScriptParsed=*/ - scriptParams => { - this.scriptMapper[scriptParams.scriptId] = scriptParams; - }, - /*onPaused=*/ - messageParams => { - try { - this.handleDebugPausedEvent(messageParams); - } catch (error) { - this.logger.error(error); - } - } - ); + this.scriptMapper = {}; + this.v8 = this.createV8Data(); } - /** Disconnects and marks the current V8 data as not connected. */ - disconnect(): void { - this.inspector.detach(); + /** Creates a new V8 Debugging session and the related data. */ + private createV8Data(): V8Data { + const session = new inspector.Session(); + session.connect(); + session.on('Debugger.scriptParsed', script => { + this.scriptMapper[script.params.scriptId] = script.params; + }); + session.post('Debugger.enable'); + session.post('Debugger.setBreakpointsActive', {active: true}); + session.on('Debugger.paused', message => { + try { + this.handleDebugPausedEvent(message.params); + } catch (error) { + this.logger.error(error); + } + }); + + return { + session, + inspectorOptions: { + // Well-Formatted URL is required in Node 10.11.1+. + useWellFormattedUrl: utils.satisfies(process.version, '>10.11.0'), + }, + inspector: new V8Inspector(session), + setBreakpointsParams: {}, + }; } set( @@ -235,7 +268,8 @@ export class InspectorDebugApi implements debugapi.DebugApi { if (!this.breakpointMapper[breakpointData.id]) { // When breakpointmapper does not countain current v8/inspector breakpoint // id, we should remove this breakpoint from v8. - result = this.inspector.removeBreakpoint(breakpointData.id); + result = this.v8.inspector.removeBreakpoint(breakpointData.id); + delete this.v8.setBreakpointsParams[breakpointData.id]; } delete this.breakpoints[breakpoint.id]; delete this.listeners[breakpoint.id]; @@ -312,6 +346,10 @@ export class InspectorDebugApi implements debugapi.DebugApi { this.listeners[breakpoint.id] = {enabled: true, listener}; } + disconnect(): void { + this.v8.session.disconnect(); + } + numBreakpoints_(): number { // Tracks the number of stackdriver breakpoints. return Object.keys(this.breakpoints).length; @@ -499,7 +537,7 @@ export class InspectorDebugApi implements debugapi.DebugApi { let v8BreakpointId; // v8/inspector breakpoint id if (!this.locationMapper[locationStr]) { // The first time when a breakpoint was set to this location. - const rawUrl = this.inspector.shouldUseWellFormattedUrl() + const rawUrl = this.v8.inspectorOptions.useWellFormattedUrl ? `file://${matchingScript}` : matchingScript; // on windows on Node 11+, the url must start with file:/// @@ -508,17 +546,19 @@ export class InspectorDebugApi implements debugapi.DebugApi { process.platform === 'win32' && utils.satisfies(process.version, '>=11') ? rawUrl.replace(/^file:\/\//, 'file:///').replace(/\\/g, '/') : rawUrl; - const res = this.inspector.setBreakpointByUrl({ + const params = { lineNumber: line - 1, url, columnNumber: column - 1, condition: breakpoint.condition || undefined, - }); + }; + const res = this.v8.inspector.setBreakpointByUrl(params); if (res.error || !res.response) { // Error case. return null; } v8BreakpointId = res.response.breakpointId; + this.v8.setBreakpointsParams[v8BreakpointId] = params; this.locationMapper[locationStr] = []; this.breakpointMapper[v8BreakpointId] = []; @@ -606,7 +646,7 @@ export class InspectorDebugApi implements debugapi.DebugApi { const evaluatedExpressions = breakpoint.expressions.map(exp => { // returnByValue is set to true here so that the JSON string of the // value will be returned to log. - const result = state.evaluate(exp, frame, that.inspector, true); + const result = state.evaluate(exp, frame, that.v8.inspector, true); if (result.error) { return result.error; } else { @@ -621,7 +661,7 @@ export class InspectorDebugApi implements debugapi.DebugApi { breakpoint, this.config, this.scriptMapper, - this.inspector + this.v8.inspector ); if ( breakpoint.location && @@ -655,5 +695,37 @@ export class InspectorDebugApi implements debugapi.DebugApi { } catch (e) { this.logger.warn('Internal V8 error on breakpoint event: ' + e); } + + this.tryResetV8Debugger(); + } + + /** + * Periodically resets breakpoints to prevent memory leaks in V8 (for holding + * contexts of previous breakpoint hits). + */ + private tryResetV8Debugger() { + this.numBreakpointHitsBeforeReset += 1; + if ( + this.numBreakpointHitsBeforeReset < this.config.resetV8DebuggerThreshold + ) { + return; + } + this.numBreakpointHitsBeforeReset = 0; + + const storedParams = this.v8.setBreakpointsParams; + + // Re-connect the session to clean the memory usage. + this.disconnect(); + this.scriptMapper = {}; + this.v8 = this.createV8Data(); + this.v8.setBreakpointsParams = storedParams; + + // Setting the v8 breakpoints again according to the stored parameters. + for (const params of Object.values(storedParams)) { + const res = this.v8.inspector.setBreakpointByUrl(params); + if (res.error || !res.response) { + this.logger.error('Error upon re-setting breakpoint: ' + res); + } + } } } diff --git a/src/agent/v8/v8inspector.ts b/src/agent/v8/v8inspector.ts index 0e6e5afb..bc17ec69 100644 --- a/src/agent/v8/v8inspector.ts +++ b/src/agent/v8/v8inspector.ts @@ -15,61 +15,26 @@ // eslint-disable-next-line node/no-unsupported-features/node-builtins import * as inspector from 'inspector'; -import consoleLogLevel = require('console-log-level'); - export class V8Inspector { - // The V8 debugger session. - session: inspector.Session | null = null; - - // Store of the v8 setBreakpoint parameters for each v8 breakpoint so that - // later the recorded parameters can be used to reset the breakpoints. - storeSetBreakpointParams: { - [ - v8BreakpointId: string - ]: inspector.Debugger.SetBreakpointByUrlParameterType; - } = {}; - - // Number of paused events before the next reset. - numPausedBeforeReset = 0; - - constructor( - readonly logger: consoleLogLevel.Logger, - readonly useWellFormattedUrl: boolean, - readonly resetV8DebuggerThreshold: number, - readonly onScriptParsed: ( - script: inspector.Debugger.ScriptParsedEventDataType - ) => void, - readonly onPaused: (params: inspector.Debugger.PausedEventDataType) => void - ) {} - - /** - * Whether to add a 'file://' prefix to a URL when setting breakpoints. - */ - shouldUseWellFormattedUrl() { - return this.useWellFormattedUrl; + private session: inspector.Session; + constructor(session: inspector.Session) { + this.session = session; } - setBreakpointByUrl( - params: inspector.Debugger.SetBreakpointByUrlParameterType + options: inspector.Debugger.SetBreakpointByUrlParameterType ) { - this.attach(); - const result: { error?: Error; response?: inspector.Debugger.SetBreakpointByUrlReturnType; } = {}; - this.session!.post( + this.session.post( 'Debugger.setBreakpointByUrl', - params, + options, ( error: Error | null, response: inspector.Debugger.SetBreakpointByUrlReturnType ) => { - if (error) { - result.error = error; - } else { - this.storeSetBreakpointParams[response.breakpointId] = params; - } + if (error) result.error = error; result.response = response; } ); @@ -77,23 +42,12 @@ export class V8Inspector { } removeBreakpoint(breakpointId: string) { - this.attach(); - const result: {error?: Error} = {}; - this.session!.post( + this.session.post( 'Debugger.removeBreakpoint', {breakpointId}, (error: Error | null) => { - if (error) { - result.error = error; - } else { - delete this.storeSetBreakpointParams[breakpointId]; - } - - // If there is no active V8 breakpoints, then detach the session. - if (Object.keys(this.storeSetBreakpointParams).length === 0) { - this.detach(); - } + if (error) result.error = error; } ); return result; @@ -102,13 +56,11 @@ export class V8Inspector { evaluateOnCallFrame( options: inspector.Debugger.EvaluateOnCallFrameParameterType ) { - this.attach(); - const result: { error?: Error; response?: inspector.Debugger.EvaluateOnCallFrameReturnType; } = {}; - this.session!.post( + this.session.post( 'Debugger.evaluateOnCallFrame', options, ( @@ -127,9 +79,7 @@ export class V8Inspector { error?: Error; response?: inspector.Runtime.GetPropertiesReturnType; } = {}; - this.attach(); - - this.session!.post( + this.session.post( 'Runtime.getProperties', options, ( @@ -142,66 +92,4 @@ export class V8Inspector { ); return result; } - - /** Attaches to the V8 debugger. */ - private attach() { - if (this.session) { - return; - } - - const session = new inspector.Session(); - session.connect(); - session.on('Debugger.scriptParsed', script => { - this.onScriptParsed(script.params); - }); - session.post('Debugger.enable'); - session.post('Debugger.setBreakpointsActive', {active: true}); - session.on('Debugger.paused', message => { - this.onPaused(message.params); - this.resetV8DebuggerIfThresholdMet(); - }); - - this.session = session; - } - - /** - * Detaches from the V8 debugger. This will purge all the existing V8 - * breakpoints from the V8 debugger. - */ - detach() { - if (!this.session) { - return; - } - - this.session!.disconnect(); - this.session = null; - this.storeSetBreakpointParams = {}; - this.numPausedBeforeReset = 0; - } - - /** - * Resets the debugging session when the number of paused events meets the - * threshold. This is primarily for cleaning the memory usage hold by V8 - * debugger when hitting the V8 breakpoints too many times. - */ - private resetV8DebuggerIfThresholdMet() { - this.numPausedBeforeReset += 1; - if (this.numPausedBeforeReset < this.resetV8DebuggerThreshold) { - return; - } - this.numPausedBeforeReset = 0; - - const previouslyStoredParams = this.storeSetBreakpointParams; - - this.detach(); - this.attach(); - - // Setting the v8 breakpoints again according to the stored parameters. - for (const params of Object.values(previouslyStoredParams)) { - const res = this.setBreakpointByUrl(params); - if (res.error || !res.response) { - this.logger.error('Error upon re-setting breakpoint: ' + res); - } - } - } } diff --git a/test/test-sourcemapper.ts b/test/test-sourcemapper.ts index 56e517cd..ce9920b5 100644 --- a/test/test-sourcemapper.ts +++ b/test/test-sourcemapper.ts @@ -42,6 +42,81 @@ const QUICK_MILLISECONDS = 300; * * Note: The line numbers are zero-based */ + +describe('sourcemapper debug info', () => { + const logger = new MockLogger(); + const mapFilePath = path.join( + BASE_PATH, + path.join('typescript', 'out.js.map') + ); + + it('should be printed upon creation', async () => { + const sourcemapper = await sm.create([mapFilePath], logger); + + // Verify if the debugging information is correctly printed. + assert.notStrictEqual( + logger.debugs[0].args[0].match('debugging information ...'), + null + ); + assert.notStrictEqual( + logger.debugs[1].args[0].match( + 'test/fixtures/sourcemapper/typescript/in.ts' + ), + null + ); + assert.notStrictEqual( + logger.debugs[2].args[0].match( + 'test/fixtures/sourcemapper/typescript/out.js' + ), + null + ); + assert.notStrictEqual( + logger.debugs[3].args[0].match( + 'test/fixtures/sourcemapper/typescript/out.js.map' + ), + null + ); + assert.notStrictEqual( + logger.debugs[4].args[0].match('sources: in.ts'), + null + ); + assert.strictEqual(logger.debugs.length, sourcemapper.infoMap.size * 4 + 1); + }); + + it('should be printed when upon get infomap output', async () => { + const sourcemapper = await sm.create([mapFilePath], logger); + const inputFilePath = path.join( + BASE_PATH, + path.join('typescript', 'in.ts') + ); + + const mapInfoInput = sourcemapper.getMapInfoInput(inputFilePath); + assert.notEqual(mapInfoInput, null); + sourcemapper.getMapInfoOutput(inputFilePath, 1, 0, mapInfoInput!); + + // Verify if the debugging information is correctly printed. + const debugsLength = logger.debugs.length; + assert.notStrictEqual( + logger.debugs[debugsLength - 3].args[0].indexOf( + 'sourcemapper inputPath:' + ), + -1 + ); + assert.notStrictEqual( + logger.debugs[debugsLength - 2].args[0].indexOf( + 'sourcemapper sourcePos: {' + ), + -1 + ); + assert.notStrictEqual( + logger.debugs[debugsLength - 1].args[0].indexOf( + 'sourcemapper mappedPos: {' + ), + -1 + ); + }); +}); + function testTool( tool: string, relativeMapFilePath: string, @@ -57,34 +132,14 @@ function testTool( const logger = new MockLogger(); let sourcemapper: sm.SourceMapper; - it( - 'for tool ' + tool + ' sourcemapper should be created correctly', - async () => { - const start = Date.now(); - sourcemapper = await sm.create([mapFilePath], logger); - assert( - Date.now() - start < QUICK_MILLISECONDS, - 'should create the SourceMapper quickly' - ); - - // Verify if the debugging information is correctly printed. - assert.notStrictEqual( - logger.debugs[0].args[0].indexOf('debugging information ...'), - -1 - ); - assert.notStrictEqual(logger.debugs[1].args[0].indexOf('source '), -1); - assert.notStrictEqual( - logger.debugs[2].args[0].indexOf('outputFile'), - -1 - ); - assert.notStrictEqual(logger.debugs[3].args[0].indexOf('mapFile'), -1); - assert.notStrictEqual(logger.debugs[4].args[0].indexOf('sources'), -1); - assert.strictEqual( - logger.debugs.length, - sourcemapper.infoMap.size * 4 + 1 - ); - } - ); + it('for tool ' + tool, async () => { + const start = Date.now(); + sourcemapper = await sm.create([mapFilePath], logger); + assert( + Date.now() - start < QUICK_MILLISECONDS, + 'should create the SourceMapper quickly' + ); + }); it( 'for tool ' + @@ -148,28 +203,6 @@ function testTool( 0, mapInfoInput! ); - - // Verify if the debugging information is correctly printed. - const debugsLength = logger.debugs.length; - assert.notStrictEqual( - logger.debugs[debugsLength - 3].args[0].indexOf( - 'sourcemapper inputPath:' - ), - -1 - ); - assert.notStrictEqual( - logger.debugs[debugsLength - 2].args[0].indexOf( - 'sourcemapper sourcePos: {' - ), - -1 - ); - assert.notStrictEqual( - logger.debugs[debugsLength - 1].args[0].indexOf( - 'sourcemapper mappedPos: {' - ), - -1 - ); - assert.notStrictEqual( info, null, diff --git a/test/test-v8debugapi.ts b/test/test-v8debugapi.ts index ce119775..47c26e09 100644 --- a/test/test-v8debugapi.ts +++ b/test/test-v8debugapi.ts @@ -766,41 +766,7 @@ describe('v8debugapi', () => { assert(stateIsClean(api)); }); - it('should only attach to v8 when having active breakpoints', done => { - // The test is only eligible for the InspectorDebugApi test target. - if (!(api instanceof InspectorDebugApi)) { - done(); - return; - } - - const inspectorDebugApi = api as InspectorDebugApi; - assert.strictEqual(inspectorDebugApi.inspector.session, null); - - const bp: stackdriver.Breakpoint = { - id: breakpointInFoo.id, - location: breakpointInFoo.location, - action: 'LOG', - logMessageFormat: 'cat', - } as {} as stackdriver.Breakpoint; - api.set(bp, err1 => { - assert.ifError(err1); - api.log( - bp, - () => {}, - () => false - ); - - assert.notStrictEqual(inspectorDebugApi.inspector.session, null); - - api.clear(bp, err2 => { - assert.ifError(err2); - assert.strictEqual(inspectorDebugApi.inspector.session, null); - done(); - }); - }); - }); - - it('should reset v8 debugging session when meeting threshold', done => { + it('should perform v8 breakpoints reset when meeting threshold', done => { // The test is only eligible for the InspectorDebugApi test target. if (!(api instanceof InspectorDebugApi)) { done(); @@ -826,7 +792,7 @@ describe('v8debugapi', () => { ); const inspectorDebugApi = api as InspectorDebugApi; - const v8SessionBeforeReset = inspectorDebugApi.inspector.session; + const v8BeforeReset = inspectorDebugApi.v8; // The loop should trigger the breakpoints reset. for (let i = 0; i < config.resetV8DebuggerThreshold; i++) { @@ -834,13 +800,9 @@ describe('v8debugapi', () => { } // Expect the current v8 data is no longer the previous one. - assert.notStrictEqual( - inspectorDebugApi.inspector.session, - v8SessionBeforeReset - ); + assert.notStrictEqual(inspectorDebugApi.v8, v8BeforeReset); - // Make sure the logpoint is still triggered correctly after the second - // reset. + // Make sure the logpoint is still triggered correctly after the second reset. for (let i = 0; i < config.resetV8DebuggerThreshold + 1; i++) { code.foo(1); } From 454bd3b9fe55f646b10561dbb20e3a96522be3f6 Mon Sep 17 00:00:00 2001 From: Louis Ye Date: Fri, 18 Jun 2021 10:58:24 -0400 Subject: [PATCH 4/5] Use match instead of indexOf, use normalize for windows in tests --- test/test-sourcemapper.ts | 55 +++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/test/test-sourcemapper.ts b/test/test-sourcemapper.ts index ce9920b5..3606ffe1 100644 --- a/test/test-sourcemapper.ts +++ b/test/test-sourcemapper.ts @@ -58,28 +58,21 @@ describe('sourcemapper debug info', () => { logger.debugs[0].args[0].match('debugging information ...'), null ); - assert.notStrictEqual( - logger.debugs[1].args[0].match( - 'test/fixtures/sourcemapper/typescript/in.ts' - ), - null - ); - assert.notStrictEqual( - logger.debugs[2].args[0].match( - 'test/fixtures/sourcemapper/typescript/out.js' - ), - null - ); - assert.notStrictEqual( - logger.debugs[3].args[0].match( - 'test/fixtures/sourcemapper/typescript/out.js.map' - ), - null - ); - assert.notStrictEqual( - logger.debugs[4].args[0].match('sources: in.ts'), - null - ); + const expectedDebugMessages = [ + 'debugging information ...', + path.normalize('test/fixtures/sourcemapper/typescript/in.ts'), + path.normalize('test/fixtures/sourcemapper/typescript/out.js'), + path.normalize('test/fixtures/sourcemapper/typescript/out.js.map'), + 'sources: in.ts', + ]; + + for (let i = 0; i < expectedDebugMessages.length; i++) { + assert.notStrictEqual( + logger.debugs[i].args[0].match(expectedDebugMessages[i]), + null, + `'${logger.debugs[i].args[0]}' does not match '${expectedDebugMessages[i]}'` + ); + } assert.strictEqual(logger.debugs.length, sourcemapper.infoMap.size * 4 + 1); }); @@ -97,22 +90,22 @@ describe('sourcemapper debug info', () => { // Verify if the debugging information is correctly printed. const debugsLength = logger.debugs.length; assert.notStrictEqual( - logger.debugs[debugsLength - 3].args[0].indexOf( - 'sourcemapper inputPath:' + logger.debugs[debugsLength - 3].args[0].match( + `sourcemapper inputPath: ${inputFilePath}` ), - -1 + null ); assert.notStrictEqual( - logger.debugs[debugsLength - 2].args[0].indexOf( - 'sourcemapper sourcePos: {' + logger.debugs[debugsLength - 2].args[0].match( + 'sourcePos: {"source":"in.ts","line":2,"column":0}' ), - -1 + null ); assert.notStrictEqual( - logger.debugs[debugsLength - 1].args[0].indexOf( - 'sourcemapper mappedPos: {' + logger.debugs[debugsLength - 1].args[0].match( + 'mappedPos: {"line":6,"column":0,"lastColumn":null}' ), - -1 + null ); }); }); From a873a027d263287bee21be7df7a4e9e2e3870138 Mon Sep 17 00:00:00 2001 From: Louis Ye Date: Fri, 18 Jun 2021 11:29:53 -0400 Subject: [PATCH 5/5] Use indexof for windows in test --- test/test-sourcemapper.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/test-sourcemapper.ts b/test/test-sourcemapper.ts index 3606ffe1..834f4b73 100644 --- a/test/test-sourcemapper.ts +++ b/test/test-sourcemapper.ts @@ -54,10 +54,6 @@ describe('sourcemapper debug info', () => { const sourcemapper = await sm.create([mapFilePath], logger); // Verify if the debugging information is correctly printed. - assert.notStrictEqual( - logger.debugs[0].args[0].match('debugging information ...'), - null - ); const expectedDebugMessages = [ 'debugging information ...', path.normalize('test/fixtures/sourcemapper/typescript/in.ts'), @@ -67,9 +63,12 @@ describe('sourcemapper debug info', () => { ]; for (let i = 0; i < expectedDebugMessages.length; i++) { + // We use 'indexOf' here instead of 'match' to avoid parsing regular + // expression, which will confuse the result when having '\' in the path + // name on platform like Windows. assert.notStrictEqual( - logger.debugs[i].args[0].match(expectedDebugMessages[i]), - null, + logger.debugs[i].args[0].indexOf(expectedDebugMessages[i]), + -1, `'${logger.debugs[i].args[0]}' does not match '${expectedDebugMessages[i]}'` ); } @@ -88,24 +87,27 @@ describe('sourcemapper debug info', () => { sourcemapper.getMapInfoOutput(inputFilePath, 1, 0, mapInfoInput!); // Verify if the debugging information is correctly printed. + // We use 'indexOf' here instead of 'match' to avoid parsing regular + // expression, which will confuse the result when having '\' in the path + // name on platform like Windows. const debugsLength = logger.debugs.length; assert.notStrictEqual( - logger.debugs[debugsLength - 3].args[0].match( + logger.debugs[debugsLength - 3].args[0].indexOf( `sourcemapper inputPath: ${inputFilePath}` ), - null + -1 ); assert.notStrictEqual( - logger.debugs[debugsLength - 2].args[0].match( + logger.debugs[debugsLength - 2].args[0].indexOf( 'sourcePos: {"source":"in.ts","line":2,"column":0}' ), - null + -1 ); assert.notStrictEqual( - logger.debugs[debugsLength - 1].args[0].match( + logger.debugs[debugsLength - 1].args[0].indexOf( 'mappedPos: {"line":6,"column":0,"lastColumn":null}' ), - null + -1 ); }); });