forked from coracle/flotilla
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:
+87
-58
@@ -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
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user