Skip to content

Commit fe2587c

Browse files
committed
Big performance boost for event data processing
This is most notable under bursts of traffic or when importing a HAR - in the pathological case, this provides performance boosts of about 60x. This works by avoiding complete regeneration of the eventStore.exchanges etc filtered lists (now manually adding/removing new items, instead of recalculating the list) and using an id to event index (also with efficient recalculation) to find existing exchanges quickly without needing to scan the list.
1 parent 785e856 commit fe2587c

File tree

8 files changed

+182
-78
lines changed

8 files changed

+182
-78
lines changed

src/components/intercept/connected-sources.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const ConnectedSource = styled.div`
1818
}
1919
`;
2020

21-
export const ConnectedSources = styled((props: { activeSources: TrafficSource[], className?: string }) =>
21+
export const ConnectedSources = styled((props: { activeSources: ReadonlyArray<TrafficSource>, className?: string }) =>
2222
<BigCard className={props.className}>
2323
<h1>Connected Sources</h1>
2424
{

src/components/view/view-event-list-buttons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const ClearAllButton = observer((props: {
2929
export const ExportAsHarButton = inject('accountStore')(observer((props: {
3030
className?: string,
3131
accountStore?: AccountStore,
32-
events: CollectedEvent[]
32+
events: ReadonlyArray<CollectedEvent>
3333
}) => {
3434
const { isPaidUser } = props.accountStore!;
3535

src/components/view/view-event-list-footer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ export const ViewEventListFooter = styled(observer((props: {
6262
onFiltersConsidered: (filters: FilterSet | undefined) => void,
6363
onScrollToEnd: () => void,
6464

65-
allEvents: CollectedEvent[],
66-
filteredEvents: CollectedEvent[],
65+
allEvents: ReadonlyArray<CollectedEvent>,
66+
filteredEvents: ReadonlyArray<CollectedEvent>,
6767

6868
// We track this separately because it's not 100% accurate to show it
6969
// live from the events above, because filteredEvents is debounced. If

src/components/view/view-event-list.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ const EmptyStateOverlay = styled(EmptyState)`
4848

4949
interface ViewEventListProps {
5050
className?: string;
51-
events: CollectedEvent[];
52-
filteredEvents: CollectedEvent[];
51+
events: ReadonlyArray<CollectedEvent>;
52+
filteredEvents: ReadonlyArray<CollectedEvent>;
5353
selectedEvent: CollectedEvent | undefined;
5454
isPaused: boolean;
5555

@@ -328,7 +328,7 @@ export const TableHeaderRow = styled.div<{ role: 'row' }>`
328328
interface EventRowProps extends ListChildComponentProps {
329329
data: {
330330
selectedEvent: CollectedEvent | undefined;
331-
events: CollectedEvent[];
331+
events: ReadonlyArray<CollectedEvent>;
332332
contextMenuBuilder: ViewEventContextMenuBuilder;
333333
}
334334
}

src/components/view/view-page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class ViewPage extends React.Component<ViewPageProps> {
187187

188188
@debounceComputed(10) // Debounce slightly - most important for body filtering performance
189189
get filteredEventState(): {
190-
filteredEvents: CollectedEvent[],
190+
filteredEvents: ReadonlyArray<CollectedEvent>,
191191
filteredEventCount: [filtered: number, fromTotal: number]
192192
} {
193193
const { events } = this.props.eventsStore;
@@ -300,7 +300,7 @@ class ViewPage extends React.Component<ViewPageProps> {
300300
oldFilteredEvents !== newFilteredEvents &&
301301
oldFilteredEvents !== this.props.eventsStore.events
302302
) {
303-
oldFilteredEvents.length = 0;
303+
(oldFilteredEvents as CollectedEvent[]).length = 0;
304304
}
305305
})
306306
);

src/model/events/events-store.ts

Lines changed: 45 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import { ApiStore } from '../api/api-store';
4040
import { RulesStore } from '../rules/rules-store';
4141
import { findItem, isRuleGroup } from '../rules/rules-structure';
4242

43+
import { ObservableEventsList } from './observable-events-list';
44+
4345
import { parseHar } from '../http/har';
4446

4547
import { FailedTlsConnection } from '../tls/failed-tls-connection';
@@ -183,40 +185,16 @@ export class EventsStore {
183185
}
184186
}
185187

186-
readonly events = observable.array<CollectedEvent>([], { deep: false });
187-
188-
@computed.struct
189-
get exchanges(): Array<HttpExchange> {
190-
return this.events.filter((e): e is HttpExchange => e.isHttp());
191-
}
192-
193-
@computed.struct
194-
get websockets(): Array<WebSocketStream> {
195-
return this.exchanges.filter((e): e is WebSocketStream => e.isWebSocket());
196-
}
197-
198-
@computed.struct
199-
get rtcConnections(): Array<RTCConnection> {
200-
return this.events.filter((e): e is RTCConnection => e.isRTCConnection());
201-
}
202-
203-
@computed.struct
204-
get rtcDataChannels(): Array<RTCDataChannel> {
205-
return this.events.filter((e): e is RTCDataChannel => e.isRTCDataChannel());
206-
}
188+
readonly eventsList = new ObservableEventsList();
207189

208-
@computed.struct
209-
get rtcMediaTracks(): Array<RTCMediaTrack> {
210-
return this.events.filter((e): e is RTCMediaTrack => e.isRTCMediaTrack());
211-
}
212-
213-
@computed.struct
214-
get activeSources() {
215-
return _(this.exchanges)
216-
.map(e => e.request.source)
217-
.uniqBy(s => s.summary)
218-
.value();
219-
}
190+
get events() { return this.eventsList.events; }
191+
get exchanges() { return this.eventsList.exchanges; }
192+
get websockets() { return this.eventsList.websockets; }
193+
get tlsFailures() { return this.eventsList.tlsFailures; }
194+
get rtcConnections() { return this.eventsList.rtcConnections; }
195+
get rtcDataChannels() { return this.eventsList.rtcDataChannels; }
196+
get rtcMediaTracks() { return this.eventsList.rtcMediaTracks; }
197+
get activeSources() { return this.eventsList.activeSources; }
220198

221199
@action.bound
222200
private flushQueuedUpdates() {
@@ -316,11 +294,11 @@ export class EventsStore {
316294
private addInitiatedRequest(request: InputInitiatedRequest) {
317295
// Due to race conditions, it's possible this request already exists. If so,
318296
// we just skip this - the existing data will be more up to date.
319-
const existingEventIndex = _.findIndex(this.events, { id: request.id });
320-
if (existingEventIndex === -1) {
321-
const exchange = new HttpExchange(this.apiStore, request);
322-
this.events.push(exchange);
323-
}
297+
const existingEvent = this.eventsList.getById(request.id);
298+
if (existingEvent) return;
299+
300+
const exchange = new HttpExchange(this.apiStore, request);
301+
this.eventsList.push(exchange);
324302
}
325303

326304
private getMatchedRule(request: InputCompletedRequest) {
@@ -343,23 +321,23 @@ export class EventsStore {
343321
// are received, and this one later when the full body is received.
344322
// We add the request from scratch if it's somehow missing, which can happen given
345323
// races or if the server doesn't support request-initiated events.
346-
const existingEventIndex = _.findIndex(this.events, { id: request.id });
324+
const existingEvent = this.eventsList.getById(request.id);
347325

348326
let event: HttpExchange;
349-
if (existingEventIndex >= 0) {
350-
event = this.events[existingEventIndex] as HttpExchange;
327+
if (existingEvent) {
328+
event = existingEvent as HttpExchange
351329
} else {
352330
event = new HttpExchange(this.apiStore, { ...request });
353331
// ^ This mutates request to use it, so we have to shallow-clone to use it below too:
354-
this.events.push(event);
332+
this.eventsList.push(event);
355333
}
356334

357335
event.updateFromCompletedRequest(request, this.getMatchedRule(request));
358336
}
359337

360338
@action
361339
private markRequestAborted(request: InputFailedRequest) {
362-
const exchange = _.find(this.exchanges, { id: request.id });
340+
const exchange = this.eventsList.getExchangeById(request.id);
363341

364342
if (!exchange) {
365343
// Handle this later, once the request has arrived
@@ -372,7 +350,7 @@ export class EventsStore {
372350

373351
@action
374352
private setResponse(response: InputResponse) {
375-
const exchange = _.find(this.exchanges, { id: response.id });
353+
const exchange = this.eventsList.getExchangeById(response.id);
376354

377355
if (!exchange) {
378356
// Handle this later, once the request has arrived
@@ -389,12 +367,12 @@ export class EventsStore {
389367
// ^ This mutates request to use it, so we have to shallow-clone to use it below too
390368

391369
stream.updateFromCompletedRequest(request, this.getMatchedRule(request));
392-
this.events.push(stream);
370+
this.eventsList.push(stream);
393371
}
394372

395373
@action
396374
private addAcceptedWebSocketResponse(response: InputResponse) {
397-
const stream = _.find(this.websockets, { id: response.id });
375+
const stream = this.eventsList.getWebSocketById(response.id);
398376

399377
if (!stream) {
400378
// Handle this later, once the request has arrived
@@ -408,7 +386,7 @@ export class EventsStore {
408386

409387
@action
410388
private addWebSocketMessage(message: InputWebSocketMessage) {
411-
const stream = _.find(this.websockets, { id: message.streamId });
389+
const stream = this.eventsList.getWebSocketById(message.streamId);
412390

413391
if (!stream) {
414392
// Handle this later, once the request has arrived
@@ -424,7 +402,7 @@ export class EventsStore {
424402

425403
@action
426404
private markWebSocketClosed(close: InputWebSocketClose) {
427-
const stream = _.find(this.websockets, { id: close.streamId });
405+
const stream = this.eventsList.getWebSocketById(close.streamId);
428406

429407
if (!stream) {
430408
// Handle this later, once the request has arrived
@@ -439,12 +417,12 @@ export class EventsStore {
439417

440418
@action
441419
private addTlsTunnel(openEvent: InputTlsPassthrough) {
442-
this.events.push(new TlsTunnel(openEvent));
420+
this.eventsList.push(new TlsTunnel(openEvent));
443421
}
444422

445423
@action
446424
private markTlsTunnelClosed(closeEvent: InputTlsPassthrough) {
447-
const tunnel = _.find(this.events, { id: closeEvent.id }) as TlsTunnel | undefined;
425+
const tunnel = this.eventsList.getTlsTunnelById(closeEvent.id);
448426

449427
if (!tunnel) {
450428
// Handle this later, once the tunnel open event has arrived
@@ -459,13 +437,12 @@ export class EventsStore {
459437

460438
@action
461439
private addFailedTlsRequest(request: InputTlsFailure) {
462-
if (_.some(this.events, (event) =>
463-
event.isTlsFailure() &&
464-
event.upstreamHostname === request.hostname &&
465-
event.remoteIpAddress === request.remoteIpAddress
440+
if (this.tlsFailures.some((failure) =>
441+
failure.upstreamHostname === request.hostname &&
442+
failure.remoteIpAddress === request.remoteIpAddress
466443
)) return; // Drop duplicate TLS failures
467444

468-
this.events.push(new FailedTlsConnection(request));
445+
this.eventsList.push(new FailedTlsConnection(request));
469446
}
470447

471448
@action
@@ -497,12 +474,12 @@ export class EventsStore {
497474
exchange.setResponse(error.response);
498475
}
499476

500-
this.events.push(exchange);
477+
this.eventsList.push(exchange);
501478
}
502479

503480
@action
504481
private addRuleEvent(event: InputRuleEvent) {
505-
const exchange = _.find(this.exchanges, { id: event.requestId });
482+
const exchange = this.eventsList.getExchangeById(event.requestId);
506483

507484
if (!exchange) {
508485
// Handle this later, once the request has arrived
@@ -527,12 +504,12 @@ export class EventsStore {
527504

528505
@action
529506
private addRTCPeerConnection(event: InputRTCPeerConnected) {
530-
this.events.push(new RTCConnection(event));
507+
this.eventsList.push(new RTCConnection(event));
531508
}
532509

533510
@action
534511
private attachExternalRTCPeer(event: InputRTCExternalPeerAttached) {
535-
const conn = this.rtcConnections.find(c => c.id === event.sessionId);
512+
const conn = this.eventsList.getRTCConnectionById(event.sessionId);
536513
const otherHalf = this.rtcConnections.find(c => c.isOtherHalfOf(event));
537514

538515
if (conn) {
@@ -545,7 +522,7 @@ export class EventsStore {
545522

546523
@action
547524
private markRTCPeerDisconnected(event: InputRTCPeerDisconnected) {
548-
const conn = this.rtcConnections.find(c => c.id === event.sessionId);
525+
const conn = this.eventsList.getRTCConnectionById(event.sessionId);
549526
if (conn) {
550527
conn.markClosed(event);
551528
} else {
@@ -555,10 +532,10 @@ export class EventsStore {
555532

556533
@action
557534
private addRTCDataChannel(event: InputRTCDataChannelOpened) {
558-
const conn = this.rtcConnections.find(c => c.id === event.sessionId);
535+
const conn = this.eventsList.getRTCConnectionById(event.sessionId);
559536
if (conn) {
560537
const dc = new RTCDataChannel(event, conn);
561-
this.events.push(dc);
538+
this.eventsList.push(dc);
562539
conn.addStream(dc);
563540
} else {
564541
this.orphanedEvents[event.sessionId] = { type: 'data-channel-opened', event };
@@ -587,10 +564,10 @@ export class EventsStore {
587564

588565
@action
589566
private addRTCMediaTrack(event: InputRTCMediaTrackOpened) {
590-
const conn = this.rtcConnections.find(c => c.id === event.sessionId);
567+
const conn = this.eventsList.getRTCConnectionById(event.sessionId);
591568
if (conn) {
592569
const track = new RTCMediaTrack(event, conn);
593-
this.events.push(track);
570+
this.eventsList.push(track);
594571
conn.addStream(track);
595572
} else {
596573
this.orphanedEvents[event.sessionId] = { type: 'media-track-opened', event };
@@ -619,7 +596,7 @@ export class EventsStore {
619596

620597
@action.bound
621598
deleteEvent(event: CollectedEvent) {
622-
this.events.remove(event);
599+
this.eventsList.remove(event);
623600

624601
if (event.isRTCDataChannel() || event.isRTCMediaTrack()) {
625602
event.rtcConnection.removeStream(event);
@@ -638,10 +615,10 @@ export class EventsStore {
638615
clearPinned ? () => false : (ex) => ex.pinned
639616
);
640617

641-
this.events.clear();
618+
this.eventsList.clear();
642619
unpinnedEvents.forEach((event) => { if ('cleanup' in event) event.cleanup() });
643620

644-
this.events.push(...pinnedEvents);
621+
this.eventsList.push(...pinnedEvents);
645622
this.orphanedEvents = {};
646623

647624
// If GC is exposed (desktop 0.1.22+), trigger it when data is cleared,
@@ -699,7 +676,7 @@ export class EventsStore {
699676
// ^ This mutates request to use it, so we have to shallow-clone to use it below too:
700677
exchange.updateFromCompletedRequest(request, false);
701678

702-
this.events.push(exchange);
679+
this.eventsList.push(exchange);
703680
return exchange;
704681
}
705682

0 commit comments

Comments
 (0)