Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve boot time by implementing sliding sync #2290

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d137242
Improve boot time by implementing sliding sync
FadhlanR Mar 14, 2025
f9145a9
Fix host tests
FadhlanR Mar 17, 2025
f764fe5
revert mock MatrixEvent
FadhlanR Mar 17, 2025
d881f31
wait until all events consumed
FadhlanR Mar 17, 2025
843d928
Fix host tests
FadhlanR Mar 17, 2025
17e3a7c
set m.direct
FadhlanR Mar 18, 2025
06438a4
Another sdk patch
FadhlanR Mar 18, 2025
1d34114
lint fix
FadhlanR Mar 18, 2025
c1401d4
Load more rooms if needed
FadhlanR Mar 18, 2025
9cb49b5
Merge branch 'main' into cs-7985-implement-sliding-sync
FadhlanR Mar 18, 2025
a6e4ab3
Add test to load more rooms
FadhlanR Mar 18, 2025
52ce61f
lint fix
FadhlanR Mar 18, 2025
1576b13
Fix host tests
FadhlanR Mar 19, 2025
f127c9d
Merge branch 'main' into cs-7985-implement-sliding-sync
FadhlanR Mar 19, 2025
8c5705e
Execute loadAlltimelineEvents once at a time
FadhlanR Mar 19, 2025
bbbfed9
Update _client and _sliding-sync
FadhlanR Mar 20, 2025
9020bf4
Revert unnecessary updates
FadhlanR Mar 20, 2025
22723e3
Merge branch 'main' into cs-7985-implement-sliding-sync
FadhlanR Mar 20, 2025
5aa449b
Remove unnecessary disabled
FadhlanR Mar 20, 2025
b42d1d4
Wait for take percySnapshot
FadhlanR Mar 20, 2025
8bce43f
Update based on feedbacks
FadhlanR Mar 21, 2025
682d7ff
Revert unnecassry changes
FadhlanR Mar 21, 2025
b881a4b
Merge branch 'main' into cs-7985-implement-sliding-sync
FadhlanR Mar 21, 2025
6b6f0ea
Use roomResource.processing
FadhlanR Mar 21, 2025
d01bf2b
Simplified wait for event to be loaded
FadhlanR Mar 23, 2025
a14c957
Merge branch 'main' into cs-7985-implement-sliding-sync
FadhlanR Mar 23, 2025
a4fe8c9
revert instance updates
FadhlanR Mar 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Improve boot time by implementing sliding sync
  • Loading branch information
FadhlanR committed Mar 17, 2025
commit d137242b3fd36a895973b416b9c9bf6acf024096
3 changes: 2 additions & 1 deletion packages/host/app/components/ai-assistant/panel.gts
Original file line number Diff line number Diff line change
@@ -657,7 +657,8 @@ export default class AiAssistantPanel extends Component<Signature> {
return Boolean(
this.matrixService.currentRoomId &&
this.maybeMonacoSDK &&
this.doCreateRoom.isIdle,
this.doCreateRoom.isIdle &&
!this.matrixService.isLoadingTimeline,
);
}
}
216 changes: 189 additions & 27 deletions packages/host/app/services/matrix-service.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { debounce } from '@ember/runloop';
import Service, { service } from '@ember/service';
import { cached, tracked } from '@glimmer/tracking';

import { task } from 'ember-concurrency';
import { task, timeout, restartableTask } from 'ember-concurrency';
import window from 'ember-window-mock';
import { cloneDeep } from 'lodash';
import {
@@ -14,6 +14,12 @@ import {
type EmittedEvents,
type ISendEventResponse,
} from 'matrix-js-sdk';
import {
SlidingSync,
MSC3575List,
SlidingSyncEvent,
} from 'matrix-js-sdk/lib/sliding-sync';
import { SlidingSyncState } from 'matrix-js-sdk/lib/sliding-sync';
import stringify from 'safe-stable-stringify';
import { md5 } from 'super-fast-md5';
import { TrackedMap } from 'tracked-built-ins';
@@ -115,6 +121,10 @@ import type * as MatrixSDK from 'matrix-js-sdk';
const { matrixURL } = ENV;
const MAX_CARD_SIZE_KB = 60;
const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name'];
const SLIDING_SYNC_AI_ROOM_LIST_NAME = 'ai-room';
const SLIDING_SYNC_AUTH_ROOM_LIST_NAME = 'auth-room';
const SLIDING_SYNC_LIST_RANGE_SIZE = 2;
const SLIDING_SYNC_LIST_TIMELINE_LIMIT = 1;

const realmEventsLogger = logger('realm:events');

@@ -139,6 +149,12 @@ export default class MatrixService extends Service {
@tracked private _isNewUser = false;
@tracked private postLoginCompleted = false;
@tracked private _currentRoomId: string | undefined;
@tracked private timelineLoadingState: Map<string, boolean> =
new TrackedMap();
@tracked private currentAiRoomListRange = SLIDING_SYNC_LIST_RANGE_SIZE;
@tracked private totalAiRooms?: number;
@tracked private totalAuthRooms?: number;
@tracked private currentAuthRoomListRange = SLIDING_SYNC_LIST_RANGE_SIZE;

profile = getMatrixProfile(this, () => this.userId);

@@ -164,6 +180,7 @@ export default class MatrixService extends Service {
new TrackedMap();
private cardHashes: Map<string, string> = new Map(); // hashes <> event id
private skillCardHashes: Map<string, string> = new Map(); // hashes <> event id
private slidingSync: SlidingSync | undefined;

constructor(owner: Owner) {
super(owner);
@@ -181,6 +198,7 @@ export default class MatrixService extends Service {
set currentRoomId(value: string | undefined) {
this._currentRoomId = value;
if (value) {
this.loadAllTimelineEvents(value);
window.localStorage.setItem(CurrentRoomIdPersistenceKey, value);
} else {
window.localStorage.removeItem(CurrentRoomIdPersistenceKey);
@@ -475,8 +493,9 @@ export default class MatrixService extends Service {
this.bindEventListeners();

try {
await this._client.startClient();
let accountDataContent = await this._client.getAccountDataFromServer<{
this.initSlidingSync();
await this.client.startClient({ slidingSync: this.slidingSync });
let accountDataContent = await this.client.getAccountDataFromServer<{
realms: string[];
}>(APP_BOXEL_REALMS_EVENT_TYPE);
await this.realmServer.setAvailableRealmURLs(
@@ -498,6 +517,126 @@ export default class MatrixService extends Service {
}
}

private initSlidingSync() {
let lists: Map<string, MSC3575List> = new Map();
lists.set(SLIDING_SYNC_AI_ROOM_LIST_NAME, {
ranges: [[0, SLIDING_SYNC_LIST_RANGE_SIZE]],
filters: {
is_dm: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, this is adequate, since we only have AI rooms and Auth rooms, but generally we have been considering a room an AI room if the aibot it one if it's members. Is it possible to use that criteria here?

Copy link
Contributor Author

@FadhlanR FadhlanR Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the filter options are very limited, as you can see here. We could use the room_types filter if we defined the room type during room creation, but we don't.

},
timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT,
required_state: [['*', '*']],
});
lists.set(SLIDING_SYNC_AUTH_ROOM_LIST_NAME, {
ranges: [[0, SLIDING_SYNC_LIST_RANGE_SIZE]],
filters: {
is_dm: true,
},
timeline_limit: 1,
required_state: [['*', '*']],
});
this.slidingSync = new SlidingSync(
this.client.baseUrl,
lists,
{
timeline_limit: SLIDING_SYNC_LIST_TIMELINE_LIMIT,
},
this.client as any,
3000,
);

this.slidingSync.on(SlidingSyncEvent.Lifecycle, (state, response) => {
if (state !== SlidingSyncState.Complete || !response) {
return;
}

// Handle AI rooms
if (
response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME]?.ops?.[0].op === 'SYNC'
) {
if (
response.lists?.[SLIDING_SYNC_AI_ROOM_LIST_NAME]?.count !== undefined
) {
this.totalAiRooms =
response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].count;
const currentRange =
response.lists[SLIDING_SYNC_AI_ROOM_LIST_NAME].ops?.[0]?.range;

if (currentRange) {
this.currentAiRoomListRange = currentRange[1];
this.maybeLoadMoreAiRooms.perform();
}
}
}

// Handle Auth rooms
if (
response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME]?.ops?.[0].op === 'SYNC'
) {
if (
response.lists?.[SLIDING_SYNC_AUTH_ROOM_LIST_NAME]?.count !==
undefined
) {
this.totalAuthRooms =
response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME].count;
const currentAuthRange =
response.lists[SLIDING_SYNC_AUTH_ROOM_LIST_NAME].ops?.[0]?.range;

if (currentAuthRange) {
this.currentAuthRoomListRange = currentAuthRange[1];
this.maybeLoadMoreAuthRooms.perform();
}
}
}
});

return this.slidingSync;
}

private maybeLoadMoreAiRooms = restartableTask(async () => {
if (!this.totalAiRooms || !this.slidingSync) {
return;
}

while (this.currentAiRoomListRange < this.totalAiRooms) {
const nextRange = Math.min(
this.currentAiRoomListRange + 2,
this.totalAiRooms,
);

try {
await this.slidingSync.setListRanges('ai-room', [[0, nextRange]]);
this.currentAiRoomListRange = nextRange;
await timeout(500);
} catch (error) {
console.error('Error expanding room range:', error);
break;
}
}
});

private maybeLoadMoreAuthRooms = restartableTask(async () => {
if (!this.totalAuthRooms || !this.slidingSync) {
return;
}

while (this.currentAuthRoomListRange < this.totalAuthRooms) {
const nextRange = Math.min(
this.currentAuthRoomListRange + 2,
this.totalAuthRooms,
);

try {
await this.slidingSync.setListRanges('auth-room', [[0, nextRange]]);
this.currentAuthRoomListRange = nextRange;
await timeout(500);
} catch (error) {
console.error('Error expanding auth room range:', error);
break;
}
}
});

private async loginToRealms() {
// This is where we would actually load user-specific choices out of the
// user's profile based on this.client.getUserId();
@@ -534,7 +673,7 @@ export default class MatrixService extends Service {
| CommandResultWithOutputContent
| CommandDefinitionsContent,
) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
return roomData.mutex.dispatch(async () => {
if ('data' in content) {
const encodedContent = {
@@ -1008,7 +1147,7 @@ export default class MatrixService extends Service {
}

async setPowerLevel(roomId: string, userId: string, powerLevel: number) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
await roomData.mutex.dispatch(async () => {
return this.client.setPowerLevel(roomId, userId, powerLevel);
});
@@ -1045,7 +1184,7 @@ export default class MatrixService extends Service {
content: Record<string, any>,
stateKey: string = '',
) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
await roomData.mutex.dispatch(async () => {
return this.client.sendStateEvent(roomId, eventType, content, stateKey);
});
@@ -1059,7 +1198,7 @@ export default class MatrixService extends Service {
content: Record<string, any>,
) => Promise<Record<string, any>>,
) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
await roomData.mutex.dispatch(async () => {
let currentContent = await this.getStateEventSafe(
roomId,
@@ -1077,21 +1216,21 @@ export default class MatrixService extends Service {
}

async leave(roomId: string) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
await roomData.mutex.dispatch(async () => {
return this.client.leave(roomId);
});
}

async forget(roomId: string) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
await roomData.mutex.dispatch(async () => {
return this.client.forget(roomId);
});
}

async setRoomName(roomId: string, name: string) {
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
await roomData.mutex.dispatch(async () => {
return this.client.setRoomName(roomId, name);
});
@@ -1131,11 +1270,43 @@ export default class MatrixService extends Service {
return await this.client.isUsernameAvailable(username);
}

async getRoomState(roomId: string) {
return this.client
.getRoom(roomId)
?.getLiveTimeline()
.getState('f' as MatrixSDK.Direction);
private async loadAllTimelineEvents(roomId: string) {
let roomData = this.ensureRoomData(roomId);
let room = this.client.getRoom(roomId);

if (!room) {
throw new Error(`Cannot find room with id ${roomId}`);
}

this.timelineLoadingState.set(roomId, true);
try {
while (room.oldState.paginationToken != null) {
await this.client.scrollback(room);
await timeout(1000);
let rs = room.getLiveTimeline().getState('f' as MatrixSDK.Direction);
if (rs) {
roomData.notifyRoomStateUpdated(rs);
}
}

const timeline = room.getLiveTimeline();
const events = timeline.getEvents();
for (let event of events) {
await this.processDecryptedEvent(this.buildEventForProcessing(event));
}
} catch (error) {
console.error('Error loading timeline events:', error);
throw error;
} finally {
this.timelineLoadingState.set(roomId, false);
}
}

get isLoadingTimeline() {
if (!this.currentRoomId) {
return false;
}
return this.timelineLoadingState.get(this.currentRoomId) ?? false;
}

async sendActiveLLMEvent(roomId: string, model: string) {
@@ -1152,18 +1323,14 @@ export default class MatrixService extends Service {
`bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
let roomData = await this.ensureRoomData(roomId);
let roomData = this.ensureRoomData(roomId);
roomData.addEvent(event, oldEventId);
}

private async ensureRoomData(roomId: string) {
private ensureRoomData(roomId: string) {
let roomData = this.getRoomData(roomId);
if (!roomData) {
roomData = new Room(roomId);
let rs = await this.getRoomState(roomId);
if (rs) {
roomData.notifyRoomStateUpdated(rs);
}
this.setRoomData(roomId, roomData);
}
return roomData;
@@ -1282,7 +1449,7 @@ export default class MatrixService extends Service {
}
roomStates = Array.from(roomStateMap.values());
for (let rs of roomStates) {
let roomData = await this.ensureRoomData(rs.roomId);
let roomData = this.ensureRoomData(rs.roomId);
roomData.notifyRoomStateUpdated(rs);
}
roomStateUpdatesDrained!();
@@ -1482,11 +1649,6 @@ export default class MatrixService extends Service {
) {
this.commandService.executeCommandEventsIfNeeded(event);
}

if (room.oldState.paginationToken != null) {
// we need to scroll back to capture any room events fired before this one
await this.client?.scrollback(room);
}
}

async activateCodingSkill() {
4 changes: 3 additions & 1 deletion packages/host/ember-cli-build.js
Original file line number Diff line number Diff line change
@@ -86,7 +86,9 @@ module.exports = function (defaults) {
process: false,
},
alias: {
'matrix-js-sdk': 'matrix-js-sdk/src/browser-index.ts', // Consume matrix-js-sdk via Typescript ESM so that code splitting works to exlcude massive matrix-sdk-crypto-wasm from the main bundle
'matrix-js-sdk$': 'matrix-js-sdk/src/browser-index.ts', // Consume matrix-js-sdk via Typescript ESM so that code splitting works to exlcude massive matrix-sdk-crypto-wasm from the main bundle
'matrix-js-sdk/src/sliding-sync':
'matrix-js-sdk/src/sliding-sync.ts',
},
},
node: {
3 changes: 3 additions & 0 deletions packages/matrix/docker/synapse/dev/homeserver.yaml
Original file line number Diff line number Diff line change
@@ -99,3 +99,6 @@ email:

templates:
custom_template_directory: "/custom/templates/"

experimental_features:
msc3575_enabled: true
2 changes: 1 addition & 1 deletion packages/matrix/docker/synapse/index.ts
Original file line number Diff line number Diff line change
@@ -126,7 +126,7 @@ export async function synapseStart(
);
await dockerCreateNetwork({ networkName: 'boxel' });
const synapseId = await dockerRun({
image: 'matrixdotorg/synapse:v1.99.0rc1',
image: 'matrixdotorg/synapse:v1.126.0',
containerName: 'boxel-synapse',
dockerParams: [
'--rm',
Binary file modified patches/matrix-js-sdk@31.0.0.patch
Binary file not shown.
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.