Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit ca0dfb6

Browse files
authored
Further improve performance with lots of hidden events (#10353)
* Avoid re-calculating shouldShowEvent in getReadReceiptsByShownEvent * Test that uses a MainGrouper * Cache more calls to shouldShowEvent
1 parent bc60e59 commit ca0dfb6

File tree

3 files changed

+266
-68
lines changed

3 files changed

+266
-68
lines changed

src/components/structures/MessagePanel.tsx

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
564564
/**
565565
* Find the next event in the list, and the next visible event in the list.
566566
*
567-
* @param event - the list of events to look in and whether they are shown
567+
* @param events - the list of events to look in and whether they are shown
568568
* @param i - where in the list we are now
569569
*
570570
* @returns { nextEvent, nextTile }
@@ -578,14 +578,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
578578
private getNextEventInfo(
579579
events: EventAndShouldShow[],
580580
i: number,
581-
): { nextEvent: MatrixEvent | null; nextTile: MatrixEvent | null } {
581+
): { nextEventAndShouldShow: EventAndShouldShow | null; nextTile: MatrixEvent | null } {
582582
// WARNING: this method is on a hot path.
583583

584-
const nextEvent = i < events.length - 1 ? events[i + 1].event : null;
584+
const nextEventAndShouldShow = i < events.length - 1 ? events[i + 1] : null;
585585

586586
const nextTile = findFirstShownAfter(i, events);
587587

588-
return { nextEvent, nextTile };
588+
return { nextEventAndShouldShow, nextTile };
589589
}
590590

591591
private get pendingEditItem(): string | undefined {
@@ -615,16 +615,16 @@ export default class MessagePanel extends React.Component<IProps, IState> {
615615

616616
let lastShownNonLocalEchoIndex = -1;
617617
for (let i = events.length - 1; i >= 0; i--) {
618-
const { event: mxEv, shouldShow } = events[i];
618+
const { event, shouldShow } = events[i];
619619
if (!shouldShow) {
620620
continue;
621621
}
622622

623623
if (lastShownEvent === undefined) {
624-
lastShownEvent = mxEv;
624+
lastShownEvent = event;
625625
}
626626

627-
if (mxEv.status) {
627+
if (event.status) {
628628
// this is a local echo
629629
continue;
630630
}
@@ -641,20 +641,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
641641
// to assume that sent receipts are to be shown more often.
642642
this.readReceiptsByEvent = {};
643643
if (this.props.showReadReceipts) {
644-
this.readReceiptsByEvent = this.getReadReceiptsByShownEvent();
644+
this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(events);
645645
}
646646

647647
let grouper: BaseGrouper | null = null;
648648

649649
for (let i = 0; i < events.length; i++) {
650-
const { event: mxEv, shouldShow } = events[i];
651-
const eventId = mxEv.getId();
652-
const last = mxEv === lastShownEvent;
653-
const { nextEvent, nextTile } = this.getNextEventInfo(events, i);
650+
const eventAndShouldShow = events[i];
651+
const { event, shouldShow } = eventAndShouldShow;
652+
const eventId = event.getId();
653+
const last = event === lastShownEvent;
654+
const { nextEventAndShouldShow, nextTile } = this.getNextEventInfo(events, i);
654655

655656
if (grouper) {
656-
if (grouper.shouldGroup(mxEv)) {
657-
grouper.add(mxEv);
657+
if (grouper.shouldGroup(eventAndShouldShow)) {
658+
grouper.add(eventAndShouldShow);
658659
continue;
659660
} else {
660661
// not part of group, so get the group tiles, close the
@@ -666,8 +667,15 @@ export default class MessagePanel extends React.Component<IProps, IState> {
666667
}
667668

668669
for (const Grouper of groupers) {
669-
if (Grouper.canStartGroup(this, mxEv) && !this.props.disableGrouping) {
670-
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile);
670+
if (Grouper.canStartGroup(this, eventAndShouldShow) && !this.props.disableGrouping) {
671+
grouper = new Grouper(
672+
this,
673+
eventAndShouldShow,
674+
prevEvent,
675+
lastShownEvent,
676+
nextEventAndShouldShow,
677+
nextTile,
678+
);
671679
break; // break on first grouper
672680
}
673681
}
@@ -677,8 +685,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
677685
// make sure we unpack the array returned by getTilesForEvent,
678686
// otherwise React will auto-generate keys, and we will end up
679687
// replacing all the DOM elements every time we paginate.
680-
ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, false, nextEvent, nextTile));
681-
prevEvent = mxEv;
688+
ret.push(...this.getTilesForEvent(prevEvent, event, last, false, nextEventAndShouldShow, nextTile));
689+
prevEvent = event;
682690
}
683691

684692
const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
@@ -698,8 +706,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
698706
mxEv: MatrixEvent,
699707
last = false,
700708
isGrouped = false,
701-
nextEvent?: MatrixEvent,
702-
nextEventWithTile?: MatrixEvent,
709+
nextEvent: EventAndShouldShow | null = null,
710+
nextEventWithTile: MatrixEvent | null = null,
703711
): ReactNode[] {
704712
const ret: ReactNode[] = [];
705713

@@ -745,20 +753,20 @@ export default class MessagePanel extends React.Component<IProps, IState> {
745753
const readReceipts = this.readReceiptsByEvent[eventId];
746754

747755
let isLastSuccessful = false;
748-
const isSentState = (s: EventStatus): boolean => !s || s === EventStatus.SENT;
756+
const isSentState = (s: EventStatus | null): boolean => !s || s === EventStatus.SENT;
749757
const isSent = isSentState(mxEv.getAssociatedStatus());
750-
const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent);
758+
const hasNextEvent = nextEvent?.shouldShow;
751759
if (!hasNextEvent && isSent) {
752760
isLastSuccessful = true;
753-
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
761+
} else if (hasNextEvent && isSent && !isSentState(nextEvent.event.getAssociatedStatus())) {
754762
isLastSuccessful = true;
755763
}
756764

757765
// This is a bit nuanced, but if our next event is hidden but a future event is not
758766
// hidden then we're not the last successful.
759767
if (
760768
nextEventWithTile &&
761-
nextEventWithTile !== nextEvent &&
769+
nextEventWithTile !== nextEvent?.event &&
762770
isSentState(nextEventWithTile.getAssociatedStatus())
763771
) {
764772
isLastSuccessful = false;
@@ -861,7 +869,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
861869
// Get an object that maps from event ID to a list of read receipts that
862870
// should be shown next to that event. If a hidden event has read receipts,
863871
// they are folded into the receipts of the last shown event.
864-
private getReadReceiptsByShownEvent(): Record<string, IReadReceiptProps[]> {
872+
private getReadReceiptsByShownEvent(events: EventAndShouldShow[]): Record<string, IReadReceiptProps[]> {
865873
const receiptsByEvent: Record<string, IReadReceiptProps[]> = {};
866874
const receiptsByUserId: Record<
867875
string,
@@ -872,8 +880,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
872880
> = {};
873881

874882
let lastShownEventId;
875-
for (const event of this.props.events) {
876-
if (this.shouldShowEvent(event)) {
883+
for (const { event, shouldShow } of events) {
884+
if (shouldShow) {
877885
lastShownEventId = event.getId();
878886
}
879887
if (!lastShownEventId) {
@@ -1065,7 +1073,7 @@ interface EventAndShouldShow {
10651073
}
10661074

10671075
abstract class BaseGrouper {
1068-
public static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true;
1076+
public static canStartGroup = (_panel: MessagePanel, _ev: EventAndShouldShow): boolean => true;
10691077

10701078
public events: MatrixEvent[] = [];
10711079
// events that we include in the group but then eject out and place above the group.
@@ -1074,17 +1082,20 @@ abstract class BaseGrouper {
10741082

10751083
public constructor(
10761084
public readonly panel: MessagePanel,
1077-
public readonly event: MatrixEvent,
1085+
public readonly firstEventAndShouldShow: EventAndShouldShow,
10781086
public readonly prevEvent: MatrixEvent | null,
1079-
public readonly lastShownEvent: MatrixEvent,
1080-
public readonly nextEvent?: MatrixEvent,
1081-
public readonly nextEventTile?: MatrixEvent,
1087+
public readonly lastShownEvent: MatrixEvent | undefined,
1088+
public readonly nextEvent: EventAndShouldShow | null,
1089+
public readonly nextEventTile?: MatrixEvent | null,
10821090
) {
1083-
this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent);
1091+
this.readMarker = panel.readMarkerForEvent(
1092+
firstEventAndShouldShow.event.getId(),
1093+
firstEventAndShouldShow.event === lastShownEvent,
1094+
);
10841095
}
10851096

1086-
public abstract shouldGroup(ev: MatrixEvent): boolean;
1087-
public abstract add(ev: MatrixEvent): void;
1097+
public abstract shouldGroup(ev: EventAndShouldShow): boolean;
1098+
public abstract add(ev: EventAndShouldShow): void;
10881099
public abstract getTiles(): ReactNode[];
10891100
public abstract getNewPrevEvent(): MatrixEvent;
10901101
}
@@ -1105,28 +1116,27 @@ abstract class BaseGrouper {
11051116
// Grouping only events sent by the same user that sent the `m.room.create` and only until
11061117
// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event
11071118
class CreationGrouper extends BaseGrouper {
1108-
public static canStartGroup = function (panel: MessagePanel, ev: MatrixEvent): boolean {
1109-
return ev.getType() === EventType.RoomCreate;
1119+
public static canStartGroup = function (_panel: MessagePanel, { event }: EventAndShouldShow): boolean {
1120+
return event.getType() === EventType.RoomCreate;
11101121
};
11111122

1112-
public shouldGroup(ev: MatrixEvent): boolean {
1123+
public shouldGroup({ event, shouldShow }: EventAndShouldShow): boolean {
11131124
const panel = this.panel;
1114-
const createEvent = this.event;
1115-
if (!panel.shouldShowEvent(ev)) {
1125+
const createEvent = this.firstEventAndShouldShow.event;
1126+
if (!shouldShow) {
11161127
return true;
11171128
}
1118-
if (panel.wantsDateSeparator(this.event, ev.getDate())) {
1129+
if (panel.wantsDateSeparator(this.firstEventAndShouldShow.event, event.getDate())) {
11191130
return false;
11201131
}
1132+
const eventType = event.getType();
11211133
if (
1122-
ev.getType() === EventType.RoomMember &&
1123-
(ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")
1134+
eventType === EventType.RoomMember &&
1135+
(event.getStateKey() !== createEvent.getSender() || event.getContent()["membership"] !== "join")
11241136
) {
11251137
return false;
11261138
}
11271139

1128-
const eventType = ev.getType();
1129-
11301140
// beacons are not part of room creation configuration
11311141
// should be shown in timeline
11321142
if (M_BEACON_INFO.matches(eventType)) {
@@ -1138,17 +1148,17 @@ class CreationGrouper extends BaseGrouper {
11381148
return false;
11391149
}
11401150

1141-
if (ev.isState() && ev.getSender() === createEvent.getSender()) {
1151+
if (event.isState() && event.getSender() === createEvent.getSender()) {
11421152
return true;
11431153
}
11441154

11451155
return false;
11461156
}
11471157

1148-
public add(ev: MatrixEvent): void {
1158+
public add({ event: ev, shouldShow }: EventAndShouldShow): void {
11491159
const panel = this.panel;
11501160
this.readMarker = this.readMarker || panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent);
1151-
if (!panel.shouldShowEvent(ev)) {
1161+
if (!shouldShow) {
11521162
return;
11531163
}
11541164
if (ev.getType() === EventType.RoomEncryption) {
@@ -1167,26 +1177,28 @@ class CreationGrouper extends BaseGrouper {
11671177
const panel = this.panel;
11681178
const ret: ReactNode[] = [];
11691179
const isGrouped = true;
1170-
const createEvent = this.event;
1180+
const createEvent = this.firstEventAndShouldShow;
11711181
const lastShownEvent = this.lastShownEvent;
11721182

1173-
if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
1174-
const ts = createEvent.getTs();
1183+
if (panel.wantsDateSeparator(this.prevEvent, createEvent.event.getDate())) {
1184+
const ts = createEvent.event.getTs();
11751185
ret.push(
11761186
<li key={ts + "~"}>
1177-
<DateSeparator roomId={createEvent.getRoomId()} ts={ts} />
1187+
<DateSeparator roomId={createEvent.event.getRoomId()} ts={ts} />
11781188
</li>,
11791189
);
11801190
}
11811191

11821192
// If this m.room.create event should be shown (room upgrade) then show it before the summary
1183-
if (panel.shouldShowEvent(createEvent)) {
1193+
if (createEvent.shouldShow) {
11841194
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
1185-
ret.push(...panel.getTilesForEvent(createEvent, createEvent));
1195+
ret.push(...panel.getTilesForEvent(createEvent.event, createEvent.event));
11861196
}
11871197

11881198
for (const ejected of this.ejectedEvents) {
1189-
ret.push(...panel.getTilesForEvent(createEvent, ejected, createEvent === lastShownEvent, isGrouped));
1199+
ret.push(
1200+
...panel.getTilesForEvent(createEvent.event, ejected, createEvent.event === lastShownEvent, isGrouped),
1201+
);
11901202
}
11911203

11921204
const eventTiles = this.events
@@ -1233,14 +1245,17 @@ class CreationGrouper extends BaseGrouper {
12331245
}
12341246

12351247
public getNewPrevEvent(): MatrixEvent {
1236-
return this.event;
1248+
return this.firstEventAndShouldShow.event;
12371249
}
12381250
}
12391251

12401252
// Wrap consecutive grouped events in a ListSummary
12411253
class MainGrouper extends BaseGrouper {
1242-
public static canStartGroup = function (panel: MessagePanel, ev: MatrixEvent): boolean {
1243-
if (!panel.shouldShowEvent(ev)) return false;
1254+
public static canStartGroup = function (
1255+
panel: MessagePanel,
1256+
{ event: ev, shouldShow }: EventAndShouldShow,
1257+
): boolean {
1258+
if (!shouldShow) return false;
12441259

12451260
if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) {
12461261
return true;
@@ -1259,18 +1274,18 @@ class MainGrouper extends BaseGrouper {
12591274

12601275
public constructor(
12611276
public readonly panel: MessagePanel,
1262-
public readonly event: MatrixEvent,
1277+
public readonly firstEventAndShouldShow: EventAndShouldShow,
12631278
public readonly prevEvent: MatrixEvent | null,
1264-
public readonly lastShownEvent: MatrixEvent,
1265-
nextEvent: MatrixEvent,
1266-
nextEventTile: MatrixEvent,
1279+
public readonly lastShownEvent: MatrixEvent | undefined,
1280+
nextEvent: EventAndShouldShow | null,
1281+
nextEventTile: MatrixEvent | null,
12671282
) {
1268-
super(panel, event, prevEvent, lastShownEvent, nextEvent, nextEventTile);
1269-
this.events = [event];
1283+
super(panel, firstEventAndShouldShow, prevEvent, lastShownEvent, nextEvent, nextEventTile);
1284+
this.events = [firstEventAndShouldShow.event];
12701285
}
12711286

1272-
public shouldGroup(ev: MatrixEvent): boolean {
1273-
if (!this.panel.shouldShowEvent(ev)) {
1287+
public shouldGroup({ event: ev, shouldShow }: EventAndShouldShow): boolean {
1288+
if (!shouldShow) {
12741289
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
12751290
return true;
12761291
}
@@ -1289,13 +1304,13 @@ class MainGrouper extends BaseGrouper {
12891304
return false;
12901305
}
12911306

1292-
public add(ev: MatrixEvent): void {
1307+
public add({ event: ev, shouldShow }: EventAndShouldShow): void {
12931308
if (ev.getType() === EventType.RoomMember) {
12941309
// We can ignore any events that don't actually have a message to display
12951310
if (!hasText(ev, this.panel.showHiddenEvents)) return;
12961311
}
12971312
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent);
1298-
if (!this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev)) {
1313+
if (!this.panel.showHiddenEvents && !shouldShow) {
12991314
// absorb hidden events to not split the summary
13001315
return;
13011316
}
@@ -1399,6 +1414,10 @@ const groupers = [CreationGrouper, MainGrouper];
13991414
* event that is >start items through the list, and is shown.
14001415
*/
14011416
function findFirstShownAfter(start: number, events: EventAndShouldShow[]): MatrixEvent | null {
1417+
// Note: this could be done with something like:
1418+
// events.slice(i + 1).find((e) => e.shouldShow)?.event ?? null;
1419+
// but it is ~10% slower, and this is on the critical path.
1420+
14021421
for (let n = start + 1; n < events.length; n++) {
14031422
const { event, shouldShow } = events[n];
14041423
if (shouldShow) {

0 commit comments

Comments
 (0)