Skip to content

Commit a1d15cb

Browse files
authored
fix: Prevent ST from holding onto Event refs in memory when aborted (#1491)
1 parent 09ff0ad commit a1d15cb

3 files changed

Lines changed: 52 additions & 18 deletions

File tree

src/features/session_trace/aggregate/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class Aggregate extends AggregateBase {
8686
registerHandler('bstResource', (...args) => this.events.storeResources(...args), this.featureName, this.ee)
8787
registerHandler('bstHist', (...args) => this.events.storeHist(...args), this.featureName, this.ee)
8888
registerHandler('bstXhrAgg', (...args) => this.events.storeXhrAgg(...args), this.featureName, this.ee)
89-
registerHandler('bstApi', (...args) => this.events.storeSTN(...args), this.featureName, this.ee)
89+
registerHandler('bstApi', (...args) => this.events.storeNode(...args), this.featureName, this.ee)
9090
registerHandler('trace-jserror', (...args) => this.events.storeErrorAgg(...args), this.featureName, this.ee)
9191
registerHandler('pvtAdded', (...args) => this.events.processPVT(...args), this.featureName, this.ee)
9292

src/features/session_trace/aggregate/trace/storage.js

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,18 @@ export class TraceStorage {
4141
this.parent = parent
4242
}
4343

44-
/** Central function called by all the other store__ & addToTrace API to append a trace node. */
45-
storeSTN (stn) {
46-
if (this.parent.blocked) return
44+
#canStoreNewNode () {
45+
if (this.parent.blocked) return false
4746
if (this.nodeCount >= MAX_NODES_PER_HARVEST) { // limit the amount of pending data awaiting next harvest
48-
if (this.parent.mode !== MODE.ERROR) return
47+
if (this.parent.mode !== MODE.ERROR) return false
4948
const openedSpace = this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // but maybe we could make some space by discarding irrelevant nodes if we're in sessioned Error mode
50-
if (openedSpace === 0) return
49+
if (openedSpace === 0) return false
5150
}
51+
return true
52+
}
5253

54+
/** Central internal function called by all the other store__ & addToTrace API to append a trace node. They MUST all have checked #canStoreNewNode before calling this func!! */
55+
#storeSTN (stn) {
5356
if (this.trace[stn.n]) this.trace[stn.n].push(stn)
5457
else this.trace[stn.n] = [stn]
5558

@@ -135,6 +138,11 @@ export class TraceStorage {
135138
}
136139
}
137140

141+
storeNode (node) {
142+
if (!this.#canStoreNewNode()) return
143+
this.#storeSTN(node)
144+
}
145+
138146
processPVT (name, value, attrs) {
139147
this.storeTiming({ [name]: value })
140148
}
@@ -160,13 +168,16 @@ export class TraceStorage {
160168
Math.floor(this.parent.timeKeeper.correctAbsoluteTimestamp(val))
161169
)
162170
}
163-
this.storeSTN(new TraceNode(key, val, val, 'document', 'timing'))
171+
if (!this.#canStoreNewNode()) return // at any point when no new nodes can be stored, there's no point in processing the rest of the timing entries
172+
this.#storeSTN(new TraceNode(key, val, val, 'document', 'timing'))
164173
}
165174
}
166175

167176
// Tracks the events and their listener's duration on objects wrapped by wrap-events.
168177
storeEvent (currentEvent, target, start, end) {
169178
if (this.shouldIgnoreEvent(currentEvent, target)) return
179+
if (!this.#canStoreNewNode()) return // need to check if adding node will succeed BEFORE storing event ref below (*cli Jun'25 - addressing memory leak in aborted ST issue #NR-420780)
180+
170181
if (this.prevStoredEvents.has(currentEvent)) return // prevent multiple listeners of an event from creating duplicate trace nodes per occurrence. Cleared every harvest. near-zero chance for re-duplication after clearing per harvest since the timestamps of the event are considered for uniqueness.
171182
this.prevStoredEvents.add(currentEvent)
172183

@@ -178,7 +189,7 @@ export class TraceStorage {
178189
} catch (e) {
179190
evt.o = eventOrigin(null, target, this.parent.ee)
180191
}
181-
this.storeSTN(evt)
192+
this.#storeSTN(evt)
182193
}
183194

184195
shouldIgnoreEvent (event, target) {
@@ -216,36 +227,42 @@ export class TraceStorage {
216227

217228
// Tracks when the window history API specified by wrap-history is used.
218229
storeHist (path, old, time) {
219-
this.storeSTN(new TraceNode('history.pushState', time, time, path, old))
230+
if (!this.#canStoreNewNode()) return
231+
this.#storeSTN(new TraceNode('history.pushState', time, time, path, old))
220232
}
221233

222234
#laststart = 0
223235
// Processes all the PerformanceResourceTiming entries captured (by observer).
224236
storeResources (resources) {
225237
if (!resources || resources.length === 0) return
226238

227-
resources.forEach((currentResource) => {
228-
if ((currentResource.fetchStart | 0) <= this.#laststart) return // don't recollect already-seen resources
239+
for (let i = 0; i < resources.length; i++) {
240+
const currentResource = resources[i]
241+
if ((currentResource.fetchStart | 0) <= this.#laststart) continue // don't recollect already-seen resources
242+
if (!this.#canStoreNewNode()) break // stop processing if we can't store any more resource nodes anyways
229243

230244
const { initiatorType, fetchStart, responseEnd, entryType } = currentResource
231245
const { protocol, hostname, port, pathname } = parseUrl(currentResource.name)
232246
const res = new TraceNode(initiatorType, fetchStart | 0, responseEnd | 0, `${protocol}://${hostname}:${port}${pathname}`, entryType)
233-
this.storeSTN(res)
234-
})
247+
248+
this.#storeSTN(res)
249+
}
235250

236251
this.#laststart = resources[resources.length - 1].fetchStart | 0
237252
}
238253

239254
// JavascriptError (FEATURE) events pipes into ST here.
240255
storeErrorAgg (type, name, params, metrics) {
241256
if (type !== 'err') return // internal errors are purposefully ignored
242-
this.storeSTN(new TraceNode('error', metrics.time, metrics.time, params.message, params.stackHash))
257+
if (!this.#canStoreNewNode()) return
258+
this.#storeSTN(new TraceNode('error', metrics.time, metrics.time, params.message, params.stackHash))
243259
}
244260

245261
// Ajax (FEATURE) events--XML & fetches--pipes into ST here.
246262
storeXhrAgg (type, name, params, metrics) {
247263
if (type !== 'xhr') return
248-
this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, `${params.status} ${params.method}: ${params.host}${params.pathname}`, 'ajax'))
264+
if (!this.#canStoreNewNode()) return
265+
this.#storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, `${params.status} ${params.method}: ${params.host}${params.pathname}`, 'ajax'))
249266
}
250267

251268
/* Below are the interface expected & required of whatever storage is used across all features on an individual basis. This allows a common `.events` property on Trace shared with AggregateBase.
@@ -271,7 +288,12 @@ export class TraceStorage {
271288
}
272289

273290
reloadSave () {
274-
Object.values(this.#backupTrace).forEach(stnsArray => stnsArray.forEach(stn => this.storeSTN(stn)))
291+
for (const stnsArray of Object.values(this.#backupTrace)) {
292+
for (const stn of stnsArray) {
293+
if (!this.#canStoreNewNode()) return // stop attempting to re-store nodes
294+
this.#storeSTN(stn)
295+
}
296+
}
275297
}
276298

277299
clearSave () {

tests/components/session_trace/aggregate.test.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ test('when max nodes per harvest is reached, no node is further added in FULL mo
104104
sessionTraceAggregate.events.nodeCount = MAX_NODES_PER_HARVEST
105105
sessionTraceAggregate.mode = MODE.FULL
106106

107-
sessionTraceAggregate.events.storeSTN({ n: 'someNode', s: 123 })
107+
sessionTraceAggregate.events.storeNode({ n: 'someNode', s: 123 })
108108
expect(sessionTraceAggregate.events.nodeCount).toEqual(MAX_NODES_PER_HARVEST)
109109
expect(Object.keys(sessionTraceAggregate.events.trace).length).toEqual(0)
110110
})
@@ -114,7 +114,19 @@ test('when max nodes per harvest is reached, node is still added in ERROR mode',
114114
sessionTraceAggregate.mode = MODE.ERROR
115115
jest.spyOn(sessionTraceAggregate.events, 'trimSTNs').mockReturnValue(MAX_NODES_PER_HARVEST)
116116

117-
sessionTraceAggregate.events.storeSTN({ n: 'someNode', s: 123 })
117+
sessionTraceAggregate.events.storeNode({ n: 'someNode', s: 123 })
118118
expect(sessionTraceAggregate.events.nodeCount).toEqual(MAX_NODES_PER_HARVEST + 1)
119119
expect(Object.keys(sessionTraceAggregate.events.trace).length).toEqual(1)
120120
})
121+
122+
test('aborted ST feat does not continue to hog event ref in memory from storeEvent', () => {
123+
sessionTraceAggregate.blocked = true // simulate aborted condition
124+
sessionTraceAggregate.events.clear()
125+
expect(sessionTraceAggregate.events.prevStoredEvents.size).toEqual(0)
126+
127+
const someClick = new Event('click')
128+
sessionTraceAggregate.events.storeEvent(someClick, 'document', 0, 100)
129+
expect(sessionTraceAggregate.events.prevStoredEvents.size).toEqual(0)
130+
131+
sessionTraceAggregate.blocked = false // reset blocked state
132+
})

0 commit comments

Comments
 (0)