Skip to content

Commit 1efffef

Browse files
committed
fix: re-process events from debouncing
1 parent b0e83b9 commit 1efffef

File tree

7 files changed

+122
-30
lines changed

7 files changed

+122
-30
lines changed

src/stories/mockComponentsv3/App.tsx

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const ticketOperationTracer = traceManager.createTracer({
6060
const simulateEventPeriodically = (ticketId: number | null) => {
6161
const interval = setInterval(() => {
6262
performance.mark(`ticket-${ticketId}-event`)
63-
}, 2_000)
63+
}, 4_000)
6464

6565
return () => void clearInterval(interval)
6666
}
@@ -73,22 +73,6 @@ export const App: React.FC = () => {
7373
[selectedTicketId],
7474
)
7575

76-
observePerformanceWithTraceManager(traceManager, [
77-
'element',
78-
'event',
79-
'first-input',
80-
'largest-contentful-paint',
81-
'layout-shift',
82-
// 'long-animation-frame',
83-
'longtask',
84-
'mark',
85-
'measure',
86-
'navigation',
87-
'paint',
88-
'resource',
89-
'visibility-state',
90-
])
91-
9276
const handleTicketClick = (id: number) => {
9377
// traceManager.startOperation({
9478
// operationName: `ticket-activation`,
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable import/no-extraneous-dependencies */
22
import React from 'react'
3-
import { Cell, Row } from '@zendeskgarden/react-tables'
3+
import { Table } from '@zendeskgarden/react-tables'
44

55
interface TicketProps {
66
id: number
@@ -9,12 +9,12 @@ interface TicketProps {
99
}
1010

1111
export const Ticket: React.FC<TicketProps> = ({ id, subject, onClick }) => (
12-
<Row
12+
<Table.Row
1313
onClick={() => void onClick(id)}
1414
style={{ cursor: 'pointer' }}
1515
isStriped={id % 2 === 0}
1616
>
17-
<Cell width={70}>{id}</Cell>
18-
<Cell>{subject}</Cell>
19-
</Row>
17+
<Table.Cell width={70}>{id}</Table.Cell>
18+
<Table.Cell>{subject}</Table.Cell>
19+
</Table.Row>
2020
)

src/stories/mockComponentsv3/TicketView.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Paragraph, Span, XXL } from '@zendeskgarden/react-typography'
1010
import { ReactComponent as UserIcon } from '@zendeskgarden/svg-icons/src/16/user-solo-stroke.svg'
1111
import { TimingComponent } from '../../v2/element'
1212
import { mockTickets } from './mockTickets'
13+
import { triggerLongTasks } from './simulateLongTasks'
1314
import { useBeacon } from './traceManager'
1415

1516
export const StyledSpan = styled(Span).attrs({ isBold: true, hue: 'blue' })`
@@ -40,6 +41,16 @@ export const TicketView: React.FC<TicketViewProps> = ({
4041
isIdle: cached,
4142
})
4243

44+
useEffect(
45+
() =>
46+
triggerLongTasks({
47+
minTime: 50,
48+
maxTime: 100,
49+
totalClusterDuration: 300,
50+
}),
51+
[ticketId, cached],
52+
)
53+
4354
const ticket = mockTickets.find((ticket) => ticket.id === ticketId)
4455

4556
useEffect(() => {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export function triggerLongTasks({
2+
minTime,
3+
maxTime,
4+
totalClusterDuration,
5+
}: {
6+
minTime: number
7+
maxTime: number
8+
totalClusterDuration: number
9+
}): () => void {
10+
const controller = new AbortController()
11+
const startTime = Date.now()
12+
13+
function randomDuration(min: number, max: number): number {
14+
return Math.floor(Math.random() * (max - min + 1)) + min
15+
}
16+
17+
function executeLongTask() {
18+
const taskDuration = randomDuration(minTime, maxTime)
19+
const endTime = Date.now()
20+
21+
if (controller.signal.aborted) {
22+
console.log('Cluster aborted.')
23+
return
24+
}
25+
26+
if (endTime - startTime < totalClusterDuration) {
27+
console.log(`Starting long task for ${taskDuration} ms`)
28+
const taskEnd = Date.now() + taskDuration
29+
30+
// Simulating a blocking long task
31+
while (Date.now() < taskEnd) {
32+
if (controller.signal.aborted) {
33+
console.log('Task aborted.')
34+
return
35+
}
36+
}
37+
38+
executeLongTask() // Trigger the next task
39+
} else {
40+
console.log('Completed all tasks within the cluster duration.')
41+
}
42+
}
43+
44+
executeLongTask()
45+
46+
// Return a callback that can abort the current cluster
47+
return () => void controller.abort()
48+
}

src/stories/mockComponentsv3/traceManager.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { generateUseBeacon } from '../../v3/hooks'
2+
import { observePerformanceWithTraceManager } from '../../v3/observePerformanceWithTraceManager'
23
import { TraceManager } from '../../v3/traceManager'
34

45
export interface TicketIdScope {
@@ -13,4 +14,20 @@ export const traceManager = new TraceManager<TicketIdScope>({
1314
generateId: () => Math.random().toString(36).slice(2),
1415
})
1516

17+
observePerformanceWithTraceManager(traceManager, [
18+
'element',
19+
'event',
20+
'first-input',
21+
'largest-contentful-paint',
22+
'layout-shift',
23+
'long-animation-frame',
24+
'longtask',
25+
'mark',
26+
'measure',
27+
'navigation',
28+
'paint',
29+
'resource',
30+
'visibility-state',
31+
])
32+
1633
export const useBeacon = generateUseBeacon(traceManager)

src/v3/ActiveTrace.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
116116
interactiveDeadline: number = Number.POSITIVE_INFINITY
117117
timeoutDeadline: number = Number.POSITIVE_INFINITY
118118

119+
// while debouncing, we need to buffer any spans that come in so they can be re-processed
120+
// once we transition to the 'waiting-for-interactive' state
121+
// otherwise we might miss out on spans that are relevant to calculating the interactive
122+
debouncingSpanBuffer: SpanAndAnnotation<ScopeT>[] = []
123+
119124
readonly states = {
120125
recording: {
121126
onEnterState: () => {
@@ -158,13 +163,6 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
158163
}
159164
}
160165

161-
console.log(
162-
`# processing span ${spanAndAnnotation.span.name} ${spanAndAnnotation.annotation.occurrence}`,
163-
'requiredToEnd',
164-
this.context.definition.requiredToEnd,
165-
'this span',
166-
spanAndAnnotation,
167-
)
168166
for (let i = 0; i < this.context.definition.requiredToEnd.length; i++) {
169167
if (!this.context.requiredToEndIndexChecklist.has(i)) {
170168
// we previously checked off this index
@@ -258,6 +256,9 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
258256
interruptionReason: 'timeout',
259257
}
260258
}
259+
260+
this.debouncingSpanBuffer.push(spanAndAnnotation)
261+
261262
if (spanEndTimeEpoch > this.debounceDeadline) {
262263
// done debouncing
263264
return { transitionToState: 'waiting-for-interactive' }
@@ -361,6 +362,27 @@ export class TraceStateMachine<ScopeT extends ScopeBase> {
361362
typeof interactiveConfig === 'object' ? interactiveConfig : {},
362363
)
363364

365+
// sort the buffer before processing
366+
// DECISION TODO: do we want to sort by end time or start time?
367+
this.debouncingSpanBuffer.sort(
368+
(a, b) =>
369+
a.span.startTime.now +
370+
a.span.duration -
371+
(b.span.startTime.now + b.span.duration),
372+
)
373+
374+
// process any spans that were buffered during the debouncing phase
375+
while (this.debouncingSpanBuffer.length > 0) {
376+
const span = this.debouncingSpanBuffer.shift()!
377+
const transition: OnEnterStatePayload<ScopeT> | undefined = this.emit(
378+
'onProcessSpan',
379+
span,
380+
)
381+
if (transition) {
382+
return transition
383+
}
384+
}
385+
364386
return undefined
365387
},
366388

@@ -609,6 +631,15 @@ export class ActiveTrace<ScopeT extends ScopeBase> {
609631
!existingAnnotation &&
610632
(!transition || transition.transitionToState !== 'interrupted')
611633

634+
console.log(
635+
`# processed span ${spanAndAnnotation.span.type} ${spanAndAnnotation.span.name} ${spanAndAnnotation.annotation.occurrence}`,
636+
spanAndAnnotation,
637+
'shouldRecord?',
638+
shouldRecord,
639+
'transition?',
640+
transition,
641+
)
642+
612643
// DECISION: if the final state is interrupted, we should not record the entry nor annotate it externally
613644
if (shouldRecord) {
614645
this.recordedItems.push(spanAndAnnotation)

src/v3/observePerformanceWithTraceManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getSpanFromPerformanceEntry } from './getSpanFromPerformanceEntry'
2+
import type { NativePerformanceEntryType } from './spanTypes'
23
import type { TraceManager } from './traceManager'
34
import type { ScopeBase } from './types'
45

@@ -23,7 +24,7 @@ import type { ScopeBase } from './types'
2324

2425
export const observePerformanceWithTraceManager = <ScopeT extends ScopeBase>(
2526
traceManager: TraceManager<ScopeT>,
26-
entryTypes: string[],
27+
entryTypes: NativePerformanceEntryType[],
2728
) => {
2829
const observer = new PerformanceObserver((entryList) => {
2930
entryList.getEntries().forEach((entry) => {

0 commit comments

Comments
 (0)