Improve performance #182:

increase profile timer and chat search throttle delays
reduce GC pressure in derived stores
use requestIdleCallback for non-critical storage writes
batch repository update processing in feeds
This commit is contained in:
Jon Staab
2026-04-10 10:28:51 -07:00
parent a75e1f96eb
commit f42889c3c2
3 changed files with 132 additions and 83 deletions
+87 -58
View File
@@ -1,5 +1,6 @@
import {get, writable} from "svelte/store" import {writable} from "svelte/store"
import { import {
batch,
call, call,
uniq, uniq,
int, int,
@@ -25,7 +26,8 @@ import {
sortEventsDesc, sortEventsDesc,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, Filter, List} from "@welshman/util" import type {TrustedEvent, Filter, List} from "@welshman/util"
import {load, request} from "@welshman/net" import {load, request, mergeRepositoryUpdates} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {repository, loadRelay, tracker} from "@welshman/app" import {repository, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
@@ -56,57 +58,71 @@ export const makeFeed = ({
let backwardWindow = [at - interval, at] let backwardWindow = [at - interval, at]
let forwardWindow = [at, at + interval] let forwardWindow = [at, at + interval]
const insertEvent = (event: TrustedEvent) => { const insertIntoBuffer = (event: TrustedEvent) => {
let handled = false for (let i = 0; i < buffer.length; i++) {
if (buffer[i].created_at > event.created_at) {
buffer.splice(i, 0, event)
return
}
}
buffer.push(event)
}
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) { // Batch-insert events into the visible store with a single update
const $events = get(events) const insertEvents = (newEvents: TrustedEvent[]) => {
const visible: TrustedEvent[] = []
for (let i = 0; i < $events.length; i++) { for (const event of newEvents) {
if ($events[i].created_at > event.created_at) { if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
events.set(insertAt(i, event, $events)) visible.push(event)
handled = true } else {
break insertIntoBuffer(event)
}
}
if (visible.length > 0) {
events.update($events => {
for (const event of visible) {
let inserted = false
for (let i = 0; i < $events.length; i++) {
if ($events[i].created_at > event.created_at) {
$events = insertAt(i, event, $events)
inserted = true
break
}
}
if (!inserted) {
$events = [...$events, event]
}
} }
} return $events
})
if (!handled) {
events.set([...$events, event])
}
} else {
for (let i = 0; i < buffer.length; i++) {
if (buffer[i].created_at > event.created_at) {
buffer.splice(i, 0, event)
handled = true
break
}
}
if (!handled) {
buffer.push(event)
}
} }
} }
const unsubscribers = [ const unsubscribers = [
on(repository, "update", ({added, removed}) => { on(repository, "update", batch(16, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
if (removed.size > 0) { if (removed.size > 0) {
buffer = buffer.filter(e => !removed.has(e.id)) buffer = buffer.filter(e => !removed.has(e.id))
events.update($events => $events.filter(e => !removed.has(e.id))) events.update($events => $events.filter(e => !removed.has(e.id)))
} }
for (const event of added) { const matching = added.filter(
if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) { event => matchFilters(filters, event) && tracker.getRelays(event.id).has(url),
insertEvent(event) )
}
if (matching.length > 0) {
insertEvents(matching)
} }
}), })),
on(tracker, "add", (id: string, trackerUrl: string) => { on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) { if (trackerUrl === url) {
const event = repository.getEvent(id) const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
insertEvent(event) insertEvents([event])
} }
} }
}), }),
@@ -137,9 +153,7 @@ export const makeFeed = ({
backwardWindow = [since - interval, since] backwardWindow = [since - interval, since]
for (const event of buffer.splice(0, 30)) { insertEvents(buffer.splice(0, 30))
insertEvent(event)
}
if (until > now() - int(2, YEAR)) { if (until > now() - int(2, YEAR)) {
loadTimeframe(since, until) loadTimeframe(since, until)
@@ -160,9 +174,7 @@ export const makeFeed = ({
forwardWindow = [until, until + interval] forwardWindow = [until, until + interval]
for (const event of buffer.splice(0, 30)) { insertEvents(buffer.splice(0, 30))
insertEvent(event)
}
if (until < now()) { if (until < now()) {
loadTimeframe(since, until) loadTimeframe(since, until)
@@ -208,40 +220,57 @@ export const makeCalendarFeed = ({
const events = writable(sortBy(getStart, getEventsForUrl(url, filters))) const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
const insertEvent = (event: TrustedEvent) => { // Batch-insert calendar events into the store with a single update
const start = getStart(event) const insertEvents = (newEvents: TrustedEvent[]) => {
const address = getAddress(event) const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e)))
if (valid.length === 0) return
if (isNaN(start) || isNaN(getEnd(event))) return
events.update($events => { events.update($events => {
for (let i = 0; i < $events.length; i++) { for (const event of valid) {
if ($events[i].id === event.id) return $events const start = getStart(event)
if (getStart($events[i]) > start) return insertAt(i, event, $events) const address = getAddress(event)
}
return [...$events.filter(e => getAddress(e) !== address), event] let handled = false
for (let i = 0; i < $events.length; i++) {
if ($events[i].id === event.id) {
handled = true
break
}
if (getStart($events[i]) > start) {
$events = insertAt(i, event, $events)
handled = true
break
}
}
if (!handled) {
$events = [...$events.filter(e => getAddress(e) !== address), event]
}
}
return $events
}) })
} }
const unsubscribers = [ const unsubscribers = [
on(repository, "update", ({added, removed}) => { on(repository, "update", batch(16, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
if (removed.size > 0) { if (removed.size > 0) {
events.update($events => $events.filter(e => !removed.has(e.id))) events.update($events => $events.filter(e => !removed.has(e.id)))
} }
for (const event of added) { const matching = added.filter(event => matchFilters(filters, event))
if (matchFilters(filters, event)) {
insertEvent(event) if (matching.length > 0) {
} insertEvents(matching)
} }
}), })),
on(tracker, "add", (id: string, trackerUrl: string) => { on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) { if (trackerUrl === url) {
const event = repository.getEvent(id) const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
insertEvent(event) insertEvents([event])
} }
} }
}), }),
+29 -21
View File
@@ -554,7 +554,7 @@ export const chatsById = call(() => {
setTimeout(() => { setTimeout(() => {
addEvents(added) addEvents(added)
removeEvents(removed) removeEvents(removed)
}, 50) }, 200)
}), }),
] ]
@@ -568,7 +568,7 @@ export const deriveChat = call(() => {
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys)) return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
}) })
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => { export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => {
return createSearch( return createSearch(
sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())), sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
{ {
@@ -607,7 +607,7 @@ export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
}) })
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => { export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
const metaByIdByUrl = new Map<string, Map<string, Room>>() const result = new Map<string, Room[]>()
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) { for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values()) const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
@@ -619,6 +619,8 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
} }
} }
const metaById = new Map<string, Room>()
for (const event of metaEvents) { for (const event of metaEvents) {
const meta = tryCatch(() => readRoomMeta(event)) const meta = tryCatch(() => readRoomMeta(event))
@@ -626,22 +628,14 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
continue continue
} }
let metaById = metaByIdByUrl.get(url)
if (!metaById) {
metaById = new Map()
metaByIdByUrl.set(url, metaById)
}
const id = makeRoomId(url, meta.h) const id = makeRoomId(url, meta.h)
metaById.set(id, {...meta, url, id}) metaById.set(id, {...meta, url, id})
} }
}
const result = new Map<string, Room[]>() if (metaById.size > 0) {
result.set(url, Array.from(metaById.values()))
for (const [url, metaById] of metaByIdByUrl.entries()) { }
result.set(url, Array.from(metaById.values()))
} }
return result return result
@@ -949,18 +943,32 @@ export const deriveSpaceActionItems = (url: string) =>
for (const [h, roomEvents] of groupBy(getRoomId, $events)) { for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
if (!h) continue if (!h) continue
const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN})) const roomJoins: TrustedEvent[] = []
const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE})) const roomLeaves: TrustedEvent[] = []
const roomMembershipEvents = roomEvents.filter(event => const roomMembershipEvents: TrustedEvent[] = []
[ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER].includes(event.kind),
) for (const event of roomEvents) {
switch (event.kind) {
case ROOM_JOIN:
roomJoins.push(event)
break
case ROOM_LEAVE:
roomLeaves.push(event)
break
case ROOM_MEMBERS:
case ROOM_ADD_MEMBER:
case ROOM_REMOVE_MEMBER:
roomMembershipEvents.push(event)
break
}
}
const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents)) const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents))
pendingJoins.push( pendingJoins.push(
...removeUndefined( ...removeUndefined(
Array.from(groupBy(e => e.pubkey, roomJoins).values()) Array.from(groupBy(e => e.pubkey, roomJoins).values())
.map(sortEventsDesc) .map(events => first(sortEventsDesc(events))),
.map(first),
).filter(({pubkey, created_at}) => { ).filter(({pubkey, created_at}) => {
if (roomMembers.has(pubkey)) return false if (roomMembers.has(pubkey)) return false
if ( if (
+16 -4
View File
@@ -48,6 +48,18 @@ import {
import type {Unsubscriber} from "svelte/store" import type {Unsubscriber} from "svelte/store"
import {db} from "@app/core/storage" import {db} from "@app/core/storage"
// Shared interval for all non-critical store flushes, so they batch on the same cadence
const FLUSH_INTERVAL = 3000
// Wraps a write callback to run during idle time (non-critical persistence)
const idleWrite = <T>(f: (xs: T[]) => void): ((xs: T[]) => void) => {
if (typeof requestIdleCallback !== "undefined") {
return (xs: T[]) => requestIdleCallback(() => f(xs))
}
return f
}
const kinds = { const kinds = {
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS], meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
@@ -199,14 +211,14 @@ const loadCriticalRelays = async () => {
relaysByUrl.set(indexBy(r => r.url, await table.getAll())) relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
} }
const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut)) const syncRelays = () => onRelay(batch(FLUSH_INTERVAL, idleWrite(db.table<RelayProfile>("relays").bulkPut)))
const initRelayStats = async () => { const initRelayStats = async () => {
const table = db.table<RelayStats>("relayStats") const table = db.table<RelayStats>("relayStats")
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll())) relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
return onRelayStats(batch(1000, table.bulkPut)) return onRelayStats(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
} }
const initHandles = async () => { const initHandles = async () => {
@@ -214,7 +226,7 @@ const initHandles = async () => {
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll())) handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
return onHandle(batch(1000, table.bulkPut)) return onHandle(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
} }
const initZappers = async () => { const initZappers = async () => {
@@ -222,7 +234,7 @@ const initZappers = async () => {
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll())) zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
return onZapper(batch(3000, table.bulkPut)) return onZapper(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
} }
const initPlaintext = async () => { const initPlaintext = async () => {