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

Commit 7735425

Browse files
Louis-YeBenjamin E. Coe
andauthored
fix: periodically reset v8 session to prevent memory leak (#957)
* fix: periodically reset v8 session to prevent memory leak This patch makes the cloud debugger reset the v8 debugger session to prevent the memory leak caused by v8 breakpoint hits whenenever the number of breakpoint hits reach the threshold. The threshold can be set through environment variable. Fixes #811 * fix: lint issue, remove reading from env about reset threshold * fix: run lint on all the files * fix: update comments for default value Co-authored-by: Benjamin E. Coe <[email protected]>
1 parent b312004 commit 7735425

File tree

4 files changed

+176
-21
lines changed

4 files changed

+176
-21
lines changed

src/agent/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,14 @@ export interface ResolvedDebugAgentConfig extends GoogleAuthOptions {
357357
* used to set a default api url
358358
*/
359359
apiUrl?: string;
360+
361+
/**
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.
366+
*/
367+
resetV8DebuggerThreshold: number;
360368
}
361369

362370
export interface StackdriverConfig extends GoogleAuthOptions {
@@ -406,4 +414,5 @@ export const defaultConfig: ResolvedDebugAgentConfig = {
406414

407415
forceNewAgent_: false,
408416
testMode_: false,
417+
resetV8DebuggerThreshold: 30,
409418
};

src/agent/v8/inspector-debugapi.ts

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ interface InspectorOptions {
4141
useWellFormattedUrl: boolean;
4242
}
4343

44+
/** Data related to the v8 inspector. */
45+
interface V8Data {
46+
session: inspector.Session;
47+
// Options for behavior when interfacing with the Inspector API.
48+
inspectorOptions: InspectorOptions;
49+
inspector: V8Inspector;
50+
// Store the v8 setBreakpoint parameters for each v8 breakpoint so that later
51+
// the recorded parameters can be used to reset the breakpoints.
52+
setBreakpointsParams: {
53+
[
54+
v8BreakpointId: string
55+
]: inspector.Debugger.SetBreakpointByUrlParameterType;
56+
};
57+
}
58+
4459
/**
4560
* In older versions of Node, the script source as seen by the Inspector
4661
* backend is wrapped in `require('module').wrapper`, and in new versions
@@ -68,7 +83,6 @@ export class InspectorDebugApi implements debugapi.DebugApi {
6883
fileStats: ScanStats;
6984
breakpoints: {[id: string]: BreakpointData} = {};
7085
sourcemapper: SourceMapper;
71-
session: inspector.Session;
7286
// TODO: listeners, scrpitmapper, location mapper and breakpointmapper can use
7387
// Map in the future after resolving Map.prototype.get(key) returns V or
7488
// undefined.
@@ -81,9 +95,9 @@ export class InspectorDebugApi implements debugapi.DebugApi {
8195
// stackdriver breakpoint id.
8296
breakpointMapper: {[id: string]: stackdriver.BreakpointId[]} = {};
8397
numBreakpoints = 0;
84-
// Options for behavior when interfacing with the Inspector API.
85-
private inspectorOptions: InspectorOptions;
86-
v8Inspector: V8Inspector;
98+
numBreakpointHitsBeforeReset = 0;
99+
v8: V8Data;
100+
87101
constructor(
88102
logger: consoleLogLevel.Logger,
89103
config: ResolvedDebugAgentConfig,
@@ -94,25 +108,36 @@ export class InspectorDebugApi implements debugapi.DebugApi {
94108
this.config = config;
95109
this.fileStats = jsFiles;
96110
this.sourcemapper = sourcemapper;
97-
this.session = new inspector.Session();
98-
this.session.connect();
99-
this.session.on('Debugger.scriptParsed', script => {
111+
this.scriptMapper = {};
112+
this.v8 = this.createV8Data();
113+
}
114+
115+
/** Creates a new V8 Debugging session and the related data. */
116+
private createV8Data(): V8Data {
117+
const session = new inspector.Session();
118+
session.connect();
119+
session.on('Debugger.scriptParsed', script => {
100120
this.scriptMapper[script.params.scriptId] = script.params;
101121
});
102-
this.session.post('Debugger.enable');
103-
this.session.post('Debugger.setBreakpointsActive', {active: true});
104-
this.session.on('Debugger.paused', message => {
122+
session.post('Debugger.enable');
123+
session.post('Debugger.setBreakpointsActive', {active: true});
124+
session.on('Debugger.paused', message => {
105125
try {
106126
this.handleDebugPausedEvent(message.params);
107127
} catch (error) {
108128
this.logger.error(error);
109129
}
110130
});
111-
this.inspectorOptions = {
112-
// Well-Formatted URL is required in Node 10.11.1+.
113-
useWellFormattedUrl: utils.satisfies(process.version, '>10.11.0'),
131+
132+
return {
133+
session,
134+
inspectorOptions: {
135+
// Well-Formatted URL is required in Node 10.11.1+.
136+
useWellFormattedUrl: utils.satisfies(process.version, '>10.11.0'),
137+
},
138+
inspector: new V8Inspector(session),
139+
setBreakpointsParams: {},
114140
};
115-
this.v8Inspector = new V8Inspector(this.session);
116141
}
117142

118143
set(
@@ -219,7 +244,8 @@ export class InspectorDebugApi implements debugapi.DebugApi {
219244
if (!this.breakpointMapper[breakpointData.id]) {
220245
// When breakpointmapper does not countain current v8/inspector breakpoint
221246
// id, we should remove this breakpoint from v8.
222-
result = this.v8Inspector.removeBreakpoint(breakpointData.id);
247+
result = this.v8.inspector.removeBreakpoint(breakpointData.id);
248+
delete this.v8.setBreakpointsParams[breakpointData.id];
223249
}
224250
delete this.breakpoints[breakpoint.id];
225251
delete this.listeners[breakpoint.id];
@@ -297,7 +323,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
297323
}
298324

299325
disconnect(): void {
300-
this.session.disconnect();
326+
this.v8.session.disconnect();
301327
}
302328

303329
numBreakpoints_(): number {
@@ -487,7 +513,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
487513
let v8BreakpointId; // v8/inspector breakpoint id
488514
if (!this.locationMapper[locationStr]) {
489515
// The first time when a breakpoint was set to this location.
490-
const rawUrl = this.inspectorOptions.useWellFormattedUrl
516+
const rawUrl = this.v8.inspectorOptions.useWellFormattedUrl
491517
? `file://${matchingScript}`
492518
: matchingScript;
493519
// on windows on Node 11+, the url must start with file:///
@@ -496,17 +522,20 @@ export class InspectorDebugApi implements debugapi.DebugApi {
496522
process.platform === 'win32' && utils.satisfies(process.version, '>=11')
497523
? rawUrl.replace(/^file:\/\//, 'file:///').replace(/\\/g, '/')
498524
: rawUrl;
499-
const res = this.v8Inspector.setBreakpointByUrl({
525+
const params = {
500526
lineNumber: line - 1,
501527
url,
502528
columnNumber: column - 1,
503529
condition: breakpoint.condition || undefined,
504-
});
530+
};
531+
const res = this.v8.inspector.setBreakpointByUrl(params);
505532
if (res.error || !res.response) {
506533
// Error case.
507534
return null;
508535
}
509536
v8BreakpointId = res.response.breakpointId;
537+
this.v8.setBreakpointsParams[v8BreakpointId] = params;
538+
510539
this.locationMapper[locationStr] = [];
511540
this.breakpointMapper[v8BreakpointId] = [];
512541
} else {
@@ -593,7 +622,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
593622
const evaluatedExpressions = breakpoint.expressions.map(exp => {
594623
// returnByValue is set to true here so that the JSON string of the
595624
// value will be returned to log.
596-
const result = state.evaluate(exp, frame, that.v8Inspector, true);
625+
const result = state.evaluate(exp, frame, that.v8.inspector, true);
597626
if (result.error) {
598627
return result.error;
599628
} else {
@@ -608,7 +637,7 @@ export class InspectorDebugApi implements debugapi.DebugApi {
608637
breakpoint,
609638
this.config,
610639
this.scriptMapper,
611-
this.v8Inspector
640+
this.v8.inspector
612641
);
613642
if (
614643
breakpoint.location &&
@@ -642,5 +671,37 @@ export class InspectorDebugApi implements debugapi.DebugApi {
642671
} catch (e) {
643672
this.logger.warn('Internal V8 error on breakpoint event: ' + e);
644673
}
674+
675+
this.tryResetV8Debugger();
676+
}
677+
678+
/**
679+
* Periodically resets breakpoints to prevent memory leaks in V8 (for holding
680+
* contexts of previous breakpoint hits).
681+
*/
682+
private tryResetV8Debugger() {
683+
this.numBreakpointHitsBeforeReset += 1;
684+
if (
685+
this.numBreakpointHitsBeforeReset < this.config.resetV8DebuggerThreshold
686+
) {
687+
return;
688+
}
689+
this.numBreakpointHitsBeforeReset = 0;
690+
691+
const storedParams = this.v8.setBreakpointsParams;
692+
693+
// Re-connect the session to clean the memory usage.
694+
this.disconnect();
695+
this.scriptMapper = {};
696+
this.v8 = this.createV8Data();
697+
this.v8.setBreakpointsParams = storedParams;
698+
699+
// Setting the v8 breakpoints again according to the stored parameters.
700+
for (const params of Object.values(storedParams)) {
701+
const res = this.v8.inspector.setBreakpointByUrl(params);
702+
if (res.error || !res.response) {
703+
this.logger.error('Error upon re-setting breakpoint: ' + res);
704+
}
705+
}
645706
}
646707
}

test/test-debuglet.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,20 @@ describe('Debuglet', () => {
286286
debuglet.start();
287287
});
288288

289+
it('should have default resetV8DebuggerThreshold value', done => {
290+
const debuglet = new Debuglet(new Debug({}, packageInfo), {});
291+
assert.strictEqual(debuglet.config.resetV8DebuggerThreshold, 30);
292+
done();
293+
});
294+
295+
it('should overwrite resetV8DebuggerThreshold when available', done => {
296+
const debuglet = new Debuglet(new Debug({}, packageInfo), {
297+
resetV8DebuggerThreshold: 123,
298+
});
299+
assert.strictEqual(debuglet.config.resetV8DebuggerThreshold, 123);
300+
done();
301+
});
302+
289303
it('should not fail if files cannot be read', done => {
290304
const MOCKED_DIRECTORY = process.cwd();
291305
const errors: Array<{filename: string; error: string}> = [];

test/test-v8debugapi.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import * as extend from 'extend';
3535
import * as debugapi from '../src/agent/v8/debugapi';
3636
import {defaultConfig} from '../src/agent/config';
3737
import {StatusMessage} from '../src/client/stackdriver/status-message';
38+
import {InspectorDebugApi} from '../src/agent/v8/inspector-debugapi';
3839
import * as scanner from '../src/agent/io/scanner';
3940
import * as SourceMapper from '../src/agent/io/sourcemapper';
4041
import * as path from 'path';
@@ -722,6 +723,76 @@ describe('v8debugapi', () => {
722723
});
723724
});
724725

726+
describe('InspectorDebugApi', () => {
727+
let oldLPS: number;
728+
let oldDS: number;
729+
730+
before(() => {
731+
oldLPS = config.log.maxLogsPerSecond;
732+
oldDS = config.log.logDelaySeconds;
733+
config.log.maxLogsPerSecond = config.resetV8DebuggerThreshold * 3;
734+
config.log.logDelaySeconds = 1;
735+
});
736+
737+
after(() => {
738+
config.log.maxLogsPerSecond = oldLPS;
739+
config.log.logDelaySeconds = oldDS;
740+
assert(stateIsClean(api));
741+
});
742+
743+
it('should perform v8 breakpoints reset when meeting threshold', done => {
744+
// The test is only eligible for the InspectorDebugApi test target.
745+
if (!(api instanceof InspectorDebugApi)) {
746+
done();
747+
return;
748+
}
749+
750+
const bp: stackdriver.Breakpoint = {
751+
id: breakpointInFoo.id,
752+
location: breakpointInFoo.location,
753+
action: 'LOG',
754+
logMessageFormat: 'cat',
755+
} as {} as stackdriver.Breakpoint;
756+
api.set(bp, err1 => {
757+
let logpointEvaluatedTimes = 0;
758+
759+
assert.ifError(err1);
760+
api.log(
761+
bp,
762+
() => {
763+
logpointEvaluatedTimes += 1;
764+
},
765+
() => false
766+
);
767+
768+
const inspectorDebugApi = api as InspectorDebugApi;
769+
const v8BeforeReset = inspectorDebugApi.v8;
770+
771+
// The loop should trigger the breakpoints reset.
772+
for (let i = 0; i < config.resetV8DebuggerThreshold; i++) {
773+
code.foo(1);
774+
}
775+
776+
// Expect the current v8 data is no longer the previous one.
777+
assert.notStrictEqual(inspectorDebugApi.v8, v8BeforeReset);
778+
779+
// Make sure the logpoint is still triggered correctly after the second reset.
780+
for (let i = 0; i < config.resetV8DebuggerThreshold + 1; i++) {
781+
code.foo(1);
782+
}
783+
assert.strictEqual(
784+
logpointEvaluatedTimes,
785+
config.resetV8DebuggerThreshold * 2 + 1
786+
);
787+
788+
api.clear(bp, err2 => {
789+
assert.ifError(err2);
790+
done();
791+
});
792+
});
793+
});
794+
});
795+
725796
describe('set and wait', () => {
726797
it('should be possible to wait on a breakpoint', done => {
727798
// clone a clean breakpointInFoo

0 commit comments

Comments
 (0)