WIP - SkipList for performance
This commit is contained in:
@@ -18,6 +18,7 @@ import { LISTS_UPDATE_EVENT } from "../room-list/RoomListStore";
|
||||
import { AllRoomsFilter } from "./filters/AllRoomsFilter";
|
||||
import { FavouriteFilter } from "./filters/FavouriteFilter";
|
||||
import { RecencySorter } from "./sorters/RecencySorter";
|
||||
import { RoomSkipList } from "./RoomSkipList";
|
||||
|
||||
export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
/**
|
||||
@@ -25,6 +26,8 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
*/
|
||||
private rooms: Room[] = [];
|
||||
|
||||
private roomSkipList?: RoomSkipList;
|
||||
|
||||
private readonly msc3946ProcessDynamicPredecessor: boolean;
|
||||
|
||||
/**
|
||||
@@ -114,6 +117,12 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
rooms = rooms.filter((r) => VisibilityProvider.instance.isRoomVisible(r));
|
||||
return rooms;
|
||||
}
|
||||
|
||||
public createSkipList(): void {
|
||||
const rooms = this.fetchRoomsFromSdk();
|
||||
this.roomSkipList = new RoomSkipList();
|
||||
this.roomSkipList.create(rooms!);
|
||||
}
|
||||
}
|
||||
|
||||
export default class RoomListStoreV3 {
|
||||
|
||||
149
src/stores/room-list-v3/RoomSkipList.ts
Normal file
149
src/stores/room-list-v3/RoomSkipList.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { getLastTs, sortRooms } from "../room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
|
||||
// See https://en.wikipedia.org/wiki/Skip_list
|
||||
|
||||
export class RecencySorter {
|
||||
public sort(rooms: Room[]): Room[] {
|
||||
return sortRooms(rooms);
|
||||
}
|
||||
|
||||
public comparator(roomA: Room, roomB: Room): number {
|
||||
let myUserId = "";
|
||||
if (MatrixClientPeg.get()) {
|
||||
myUserId = MatrixClientPeg.get()!.getSafeUserId();
|
||||
}
|
||||
const roomALastTs = getLastTs(roomA, myUserId);
|
||||
const roomBLastTs = getLastTs(roomB, myUserId);
|
||||
|
||||
return roomBLastTs - roomALastTs;
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomSkipList {
|
||||
private readonly sentinels: Sentinel[] = [];
|
||||
private readonly roomNodeMap: Map<string, RoomNode> = new Map();
|
||||
private sorter: RecencySorter = new RecencySorter();
|
||||
|
||||
public create(rooms: Room[]): void {
|
||||
if (rooms.length === 0) {
|
||||
// No rooms, just create an empty level
|
||||
this.sentinels[0] = new Sentinel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. First sort the rooms and create a base sorted linked list
|
||||
const sortedRoomNodes = this.sorter.sort(rooms).map((room) => new RoomNode(room));
|
||||
let sentinel = new Sentinel(0);
|
||||
for (const node of sortedRoomNodes) {
|
||||
sentinel.setNext(node);
|
||||
this.roomNodeMap.set(node.room.roomId, node);
|
||||
}
|
||||
|
||||
// 2. Create the rest of the sub linked lists
|
||||
do {
|
||||
this.sentinels[sentinel.level] = sentinel;
|
||||
sentinel = sentinel.generateNextLevel();
|
||||
// todo: set max level
|
||||
} while (sentinel.size > 1);
|
||||
}
|
||||
|
||||
public removeRoom(room: Room): void {
|
||||
const existingNode = this.roomNodeMap.get(room.roomId);
|
||||
if (existingNode) {
|
||||
for (const sentinel of this.sentinels) {
|
||||
sentinel.removeNode(existingNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addRoom(room: Room): void {
|
||||
// First, let's delete this room from the skip list
|
||||
this.removeRoom(room);
|
||||
const newNode = new RoomNode(room);
|
||||
|
||||
// Start on the highest level, account for empty levels
|
||||
let sentinel = this.sentinels[0];
|
||||
for (let i = this.sentinels.length - 1; i >= 0; --i) {
|
||||
if (this.sentinels[i].size) {
|
||||
sentinel = this.sentinels[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const current = sentinel.head;
|
||||
for (let i = sentinel.level; i >= 0; --i) {
|
||||
let nextNode = current?.next[i];
|
||||
while (this.sorter.comparator(room, nextNode.room) > 0) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Sentinel {
|
||||
private current?: RoomNode;
|
||||
public head?: RoomNode;
|
||||
public size: number = 0;
|
||||
|
||||
public constructor(public readonly level: number) {}
|
||||
|
||||
public setNext(node: RoomNode): void {
|
||||
if (!this.head) this.head = node;
|
||||
if (!this.current) {
|
||||
this.current = node;
|
||||
} else {
|
||||
node.previous[this.level] = this.current;
|
||||
this.current.next[this.level] = node;
|
||||
this.current = node;
|
||||
}
|
||||
this.size++;
|
||||
}
|
||||
|
||||
public generateNextLevel(): Sentinel {
|
||||
const nextLevelSentinel = new Sentinel(this.level + 1);
|
||||
let current = this.head;
|
||||
while (current) {
|
||||
if (this.shouldPromote()) {
|
||||
nextLevelSentinel.setNext(current);
|
||||
}
|
||||
current = current.next[this.level];
|
||||
}
|
||||
return nextLevelSentinel;
|
||||
}
|
||||
|
||||
public removeNode(node: RoomNode): void {
|
||||
// Let's first see if this node is even in this level
|
||||
const nodeInThisLevel = this.head === node || node.previous[this.level];
|
||||
if (!nodeInThisLevel) {
|
||||
// This node is not in this sentinel level, so nothing to do.
|
||||
return;
|
||||
}
|
||||
const prev = node.previous[this.level];
|
||||
if (prev) {
|
||||
prev.next[this.level] = node.next[this.level];
|
||||
} else {
|
||||
// This node was the head since it has no back links!
|
||||
// so update the head.
|
||||
this.head = node.next[this.level];
|
||||
}
|
||||
this.size--;
|
||||
}
|
||||
|
||||
private shouldPromote(): boolean {
|
||||
return Math.random() < 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomNode {
|
||||
public constructor(public readonly room: Room) {}
|
||||
|
||||
public next: RoomNode[] = [];
|
||||
public previous: RoomNode[] = [];
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const sortRooms = (rooms: Room[]): Room[] => {
|
||||
});
|
||||
};
|
||||
|
||||
const getLastTs = (r: Room, userId: string): number => {
|
||||
export const getLastTs = (r: Room, userId: string): number => {
|
||||
const mainTimelineLastTs = ((): number => {
|
||||
// Apparently we can have rooms without timelines, at least under testing
|
||||
// environments. Just return MAX_INT when this happens.
|
||||
|
||||
Reference in New Issue
Block a user