Skip to content

Commit 5eb9164

Browse files
fix: Release backlog memory when features are blocked by RUM (#1102)
Co-authored-by: Patrick Housley <[email protected]>
1 parent 5022134 commit 5eb9164

File tree

8 files changed

+69
-24
lines changed

8 files changed

+69
-24
lines changed

src/common/drain/drain.js

+20-15
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ export function registerDrain (agentIdentifier, group) {
3232
* @param {*} group - The named "bucket" to be removed from the registry
3333
*/
3434
export function deregisterDrain (agentIdentifier, group) {
35-
curateRegistry(agentIdentifier)
35+
if (!agentIdentifier || !registry[agentIdentifier]) return
3636
if (registry[agentIdentifier].get(group)) registry[agentIdentifier].delete(group)
37+
drainGroup(agentIdentifier, group, false)
3738
if (registry[agentIdentifier].size) checkCanDrainAll(agentIdentifier)
3839
}
3940

@@ -86,29 +87,33 @@ function checkCanDrainAll (agentIdentifier) {
8687
* the subscribed handlers for the group.
8788
* @param {*} group - The name of a particular feature's event "bucket".
8889
*/
89-
function drainGroup (agentIdentifier, group) {
90+
function drainGroup (agentIdentifier, group, activateGroup = true) {
9091
const baseEE = agentIdentifier ? ee.get(agentIdentifier) : ee
9192
const handlers = defaultRegister.handlers // other storage in registerHandler
9293
if (!baseEE.backlog || !handlers) return
9394

94-
var bufferedEventsInGroup = baseEE.backlog[group]
95-
var groupHandlers = handlers[group] // each group in the registerHandler storage
96-
if (groupHandlers) {
97-
// We don't cache the length of the buffer while looping because events might still be added while processing.
98-
for (var i = 0; bufferedEventsInGroup && i < bufferedEventsInGroup.length; ++i) { // eslint-disable-line no-unmodified-loop-condition
99-
emitEvent(bufferedEventsInGroup[i], groupHandlers)
100-
}
95+
// Only activated features being drained should run queued listeners on buffered events. Deactivated features only need to release memory.
96+
if (activateGroup) {
97+
const bufferedEventsInGroup = baseEE.backlog[group]
98+
const groupHandlers = handlers[group] // each group in the registerHandler storage
99+
if (groupHandlers) {
100+
// We don't cache the length of the buffer while looping because events might still be added while processing.
101+
for (let i = 0; bufferedEventsInGroup && i < bufferedEventsInGroup.length; ++i) { // eslint-disable-line no-unmodified-loop-condition
102+
emitEvent(bufferedEventsInGroup[i], groupHandlers)
103+
}
101104

102-
mapOwn(groupHandlers, function (eventType, handlerRegistrationList) {
103-
mapOwn(handlerRegistrationList, function (i, registration) {
104-
// registration is an array of: [targetEE, eventHandler]
105-
registration[0].on(eventType, registration[1])
105+
mapOwn(groupHandlers, function (eventType, handlerRegistrationList) {
106+
mapOwn(handlerRegistrationList, function (i, registration) {
107+
// registration is an array of: [targetEE, eventHandler]
108+
registration[0].on(eventType, registration[1])
109+
})
106110
})
107-
})
111+
}
108112
}
113+
109114
if (!baseEE.isolatedBacklog) delete handlers[group]
110115
baseEE.backlog[group] = null
111-
baseEE.emit('drain-' + group, [])
116+
baseEE.emit('drain-' + group, []) // TODO: Code exists purely for a unit test and needs to be refined
112117
}
113118

114119
/**

tests/specs/cleanup-on-halt.e2e.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { testRumRequest } from '../../tools/testing-server/utils/expect-tests'
2+
3+
describe('Memory leaks', () => {
4+
it('does not occur on ee backlog when RUM flags are 0', async () => {
5+
await browser.testHandle.scheduleReply('bamServer', {
6+
test: testRumRequest,
7+
body: JSON.stringify({ st: 0, sr: 0, err: 0, ins: 0, spa: 0, loaded: 1 })
8+
})
9+
// This test relies on features to call deregisterDrain when not enabled by flags which in turn should clear their backlogs.
10+
11+
await browser.url(await browser.testHandle.assetURL('obfuscate-pii.html')).then(() => browser.waitForAgentLoad())
12+
const backlog = await browser.execute(function () {
13+
const backlog = {}
14+
for (const key in newrelic.ee.backlog) {
15+
const array = newrelic.ee.backlog[key]
16+
backlog[key] = array === null ? 0 : array.length
17+
}
18+
return backlog
19+
})
20+
21+
expect(backlog).toEqual(expect.objectContaining({
22+
ajax: 0, // ajax does not rely on any flags anyways so it's always drained
23+
jserrors: 0,
24+
metrics: 0,
25+
page_action: 0,
26+
page_view_event: 0, // no handler
27+
page_view_timing: 0, // does not rely on any flags
28+
session_trace: 0,
29+
spa: 0
30+
}))
31+
})
32+
})

tests/specs/harvesting/disable-harvesting.e2e.js

-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ describe('disable harvesting', () => {
88
st: 1,
99
err: 0,
1010
ins: 1,
11-
cap: 1,
1211
spa: 1,
1312
loaded: 1
1413
})
@@ -32,7 +31,6 @@ describe('disable harvesting', () => {
3231
st: 1,
3332
err: 1,
3433
ins: 1,
35-
cap: 1,
3634
spa: 0,
3735
loaded: 1
3836
})
@@ -59,7 +57,6 @@ describe('disable harvesting', () => {
5957
st: 1,
6058
err: 1,
6159
ins: 0,
62-
cap: 1,
6360
spa: 1,
6461
loaded: 1
6562
})
@@ -81,7 +78,6 @@ describe('disable harvesting', () => {
8178
st: 0,
8279
err: 1,
8380
ins: 1,
84-
cap: 1,
8581
spa: 1,
8682
loaded: 1
8783
})

tests/specs/session-replay/initialization.e2e.js

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ async function disqualifySR () {
1212
sts: 1,
1313
err: 1,
1414
ins: 1,
15-
cap: 1,
1615
spa: 1,
1716
loaded: 1,
1817
sr: 0,

tests/specs/session-replay/session-state.e2e.js

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ describe.withBrowsersMatching(notIE)('session manager state behavior', () => {
8585
await browser.enableSessionReplay()
8686
await browser.url(await browser.testHandle.assetURL('instrumented.html', srConfig()))
8787
.then(() => browser.waitForSessionReplayRecording())
88+
89+
await browser.closeWindow()
90+
await browser.switchToWindow((await browser.getWindowHandles())[0])
8891
})
8992
})
9093

tests/specs/session-trace/trace-nodes.e2e.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('Trace nodes', () => {
3333
await browser.testHandle.clearScheduledReplies('bamServer')
3434
await browser.testHandle.scheduleReply('bamServer', {
3535
test: testRumRequest,
36-
body: JSON.stringify({ st: 0, sts: 0, sr: 0, err: 1, ins: 1, cap: 1, spa: 0, loaded: 1 })
36+
body: JSON.stringify({ st: 0, sts: 0, sr: 0, err: 1, ins: 1, spa: 0, loaded: 1 })
3737
})
3838

3939
const url = await browser.testHandle.assetURL('pagehide.html', stConfig())

tests/specs/soft_navigations/integration.e2e.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('Soft navigations', () => {
3232
it('does not harvest when spa is blocked by rum response', async () => {
3333
await browser.testHandle.scheduleReply('bamServer', {
3434
test: testRumRequest,
35-
body: `${JSON.stringify({ st: 1, err: 1, ins: 1, cap: 1, spa: 0, loaded: 1 })}`
35+
body: `${JSON.stringify({ st: 1, err: 1, ins: 1, spa: 0, loaded: 1 })}`
3636
})
3737

3838
let url = await browser.testHandle.assetURL('instrumented.html', config)

tests/unit/common/drain/drain.test.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
let registerDrain, drain, ee
1+
let registerDrain, drain, deregisterDrain, ee, registerHandler
22
beforeEach(async () => {
33
jest.resetModules()
4-
;({ registerDrain, drain } = await import('../../../../src/common/drain/drain'))
4+
;({ registerDrain, drain, deregisterDrain } = await import('../../../../src/common/drain/drain'))
55
;({ ee } = await import('../../../../src/common/event-emitter/contextual-ee'))
6+
;({ registerHandler } = await import('../../../../src/common/event-emitter/register-handler'))
67
})
78

89
test('can register a feat and drain it', () => {
@@ -25,6 +26,15 @@ test('other unregistered drains do not affect feat reg & drain', () => {
2526
expect(emitSpy).toHaveBeenCalledWith('drain-page_view_event', expect.anything())
2627
})
2728

29+
test('unregistering groups clears their handlers from the buffering system', () => {
30+
registerDrain('abcd', 'pumbaa')
31+
registerHandler('tusks', () => {}, 'pumbaa', ee)
32+
expect(registerHandler.handlers.pumbaa).toEqual({ tusks: expect.anything() })
33+
34+
deregisterDrain('abcd', 'pumbaa')
35+
expect(registerHandler.handlers.pumbaa).toBeUndefined()
36+
})
37+
2838
describe('drain', () => {
2939
test('does not execute until all registered groups calls it and in order', () => {
3040
registerDrain('abcd', 'page_view_timing')

0 commit comments

Comments
 (0)