Skip to content

Commit a8e977f

Browse files
committed
Break up HAR import (or other large queues) to avoid blocking the UI
1 parent fe2587c commit a8e977f

File tree

2 files changed

+40
-30
lines changed

2 files changed

+40
-30
lines changed

src/model/events/events-store.ts

+33-29
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ import {
2828
InputRTCMediaStats,
2929
InputRTCMediaTrackClosed,
3030
InputRTCExternalPeerAttached,
31-
InputRuleEvent,
32-
TimingEvents
31+
InputRuleEvent
3332
} from '../../types';
3433

3534
import { lazyObservablePromise } from '../../util/observable';
@@ -114,9 +113,11 @@ type EventType =
114113
| MockttpEventType
115114
| MockRTCEventType;
116115

117-
export type QueuedEvent = ({
118-
[T in EventType]: { type: T, event: EventTypesMap[T] }
119-
}[EventType]);
116+
export type QueuedEvent =
117+
| ({ // Received Mockttp event data:
118+
[T in EventType]: { type: T, event: EventTypesMap[T] }
119+
}[EventType])
120+
| { type: 'queued-callback', cb: () => void } // Or a callback to run after data is processed
120121

121122
type OrphanableQueuedEvent<T extends
122123
| 'response'
@@ -137,6 +138,8 @@ type OrphanableQueuedEvent<T extends
137138
| 'tls-passthrough-closed'
138139
> = { type: T, event: EventTypesMap[T] };
139140

141+
const LARGE_QUEUE_BATCH_SIZE = 1033; // Off by 33 for a new ticking UI effect
142+
140143
export class EventsStore {
141144

142145
constructor(
@@ -204,8 +207,18 @@ export class EventsStore {
204207
// on request animation frame, so batches get larger and cheaper if
205208
// the frame rate starts to drop.
206209

207-
this.eventQueue.forEach(this.updateFromQueuedEvent);
208-
this.eventQueue = [];
210+
if (this.eventQueue.length > LARGE_QUEUE_BATCH_SIZE) {
211+
// If there's a lot of events in the queue (only ever likely to happen
212+
// in an import of a large file) we break it up to keep the UI responsive.
213+
this.eventQueue.slice(0, LARGE_QUEUE_BATCH_SIZE).forEach(this.updateFromQueuedEvent);
214+
this.eventQueue = this.eventQueue.slice(LARGE_QUEUE_BATCH_SIZE);
215+
setTimeout(() => {
216+
this.queueEventFlush();
217+
}, 10);
218+
} else {
219+
this.eventQueue.forEach(this.updateFromQueuedEvent);
220+
this.eventQueue = [];
221+
}
209222
}
210223

211224
private updateFromQueuedEvent = (queuedEvent: QueuedEvent) => {
@@ -263,6 +276,10 @@ export class EventsStore {
263276
return this.addRTCMediaTrackStats(queuedEvent.event);
264277
case 'media-track-closed':
265278
return this.markRTCMediaTrackClosed(queuedEvent.event);
279+
280+
case 'queued-callback':
281+
queuedEvent.cb();
282+
return;
266283
}
267284
} catch (e) {
268285
// It's possible we might fail to parse an input event. This shouldn't happen, but if it
@@ -643,31 +660,18 @@ export class EventsStore {
643660

644661
// We now take each of these input items, and put them on the queue to be added
645662
// to the UI like any other seen request data. Arguably we could call addRequest &
646-
// setResponse etc directly, but this is nicer if the UI thread is already under strain.
647-
648-
// First, we run through the request & TLS error events together, in order, since these
649-
// define the initial event ordering
650-
const [initialEvents, updateEvents] = _.partition(events, ({ type }) =>
651-
type === 'request' ||
652-
type === 'websocket-request' ||
653-
type === 'tls-client-error'
654-
);
655-
this.eventQueue.push(..._.sortBy(initialEvents, (e) =>
656-
(e.event as { timingEvents: TimingEvents }).timingEvents.startTime
657-
));
663+
// setResponse etc directly, but this is nicer in case the UI thread is already under strain.
664+
this.eventQueue.push(...events);
658665

659-
// Then we add everything else (responses & aborts). They just update, so order doesn't matter:
660-
this.eventQueue.push(...updateEvents);
666+
// After all events are handled, set the required pins:
667+
this.eventQueue.push({
668+
type: 'queued-callback',
669+
cb: action(() => pinnedIds.forEach((id) => {
670+
this.events.find(e => e.id === id)!.pinned = true;
671+
}))
672+
});
661673

662674
this.queueEventFlush();
663-
664-
if (pinnedIds.length) {
665-
// This rAF will be scheduled after the queued flush, so the event should
666-
// always be fully imported by this stage:
667-
requestAnimationFrame(action(() => pinnedIds.forEach((id) => {
668-
this.events.find(e => e.id === id)!.pinned = true;
669-
})));
670-
}
671675
}
672676

673677
@action

src/model/http/har.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,13 @@ export async function parseHar(harContents: unknown): Promise<ParsedHar> {
527527
const events: QueuedEvent[] = [];
528528
const pinnedIds: string[] = []
529529

530-
har.log.entries.forEach((entry, i) => {
530+
har.log.entries
531+
.sort((a, b) => {
532+
const aStartTime = dateFns.parse(a.startedDateTime).getTime();
533+
const bStartTime = dateFns.parse(b.startedDateTime).getTime();
534+
return aStartTime - bStartTime;
535+
})
536+
.forEach((entry, i) => {
531537
const id = baseId + i;
532538
const isWebSocket = entry._resourceType === 'websocket';
533539

0 commit comments

Comments
 (0)