summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-04 23:34:12 -0600
committerAdam <[email protected]>2026-01-05 13:21:30 -0600
commit001b48635602470ab1bac21b6af9fe207ccd5a17 (patch)
treeb6b4540a4cd974a5ebaa37ec6913d440a0fa6223
parentd315026abc0a07e88b3bec6b74253e272161659b (diff)
downloadopencode-001b48635602470ab1bac21b6af9fe207ccd5a17.tar.gz
opencode-001b48635602470ab1bac21b6af9fe207ccd5a17.zip
fix(app): performance improvements through event batching
-rw-r--r--packages/app/src/context/global-sdk.tsx73
-rw-r--r--packages/app/src/context/global-sync.tsx5
-rw-r--r--packages/app/src/context/local.tsx5
-rw-r--r--packages/app/src/context/notification.tsx4
-rw-r--r--packages/app/src/context/sdk.tsx4
5 files changed, 81 insertions, 10 deletions
diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx
index 8809badde..dc8f937ff 100644
--- a/packages/app/src/context/global-sdk.tsx
+++ b/packages/app/src/context/global-sdk.tsx
@@ -1,7 +1,7 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
-import { onCleanup } from "solid-js"
+import { batch, onCleanup } from "solid-js"
import { usePlatform } from "./platform"
import { useServer } from "./server"
@@ -19,14 +19,79 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
[key: string]: Event
}>()
+ type Queued = { directory: string; payload: Event }
+
+ let queue: Array<Queued | undefined> = []
+ const coalesced = new Map<string, number>()
+ let timer: ReturnType<typeof setTimeout> | undefined
+ let last = 0
+
+ const key = (directory: string, payload: Event) => {
+ if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
+ if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
+ if (payload.type === "message.part.updated") {
+ const part = payload.properties.part
+ return `message.part.updated:${directory}:${part.messageID}:${part.id}`
+ }
+ }
+
+ const flush = () => {
+ if (timer) clearTimeout(timer)
+ timer = undefined
+
+ const events = queue
+ queue = []
+ coalesced.clear()
+ if (events.length === 0) return
+
+ last = Date.now()
+ batch(() => {
+ for (const event of events) {
+ if (!event) continue
+ emitter.emit(event.directory, event.payload)
+ }
+ })
+ }
+
+ const schedule = () => {
+ if (timer) return
+ const elapsed = Date.now() - last
+ timer = setTimeout(flush, Math.max(0, 16 - elapsed))
+ }
+
+ const stop = () => {
+ flush()
+ }
+
void (async () => {
const events = await eventSdk.global.event()
+ let yielded = Date.now()
for await (const event of events.stream) {
- emitter.emit(event.directory ?? "global", event.payload)
+ const directory = event.directory ?? "global"
+ const payload = event.payload
+ const k = key(directory, payload)
+ if (k) {
+ const i = coalesced.get(k)
+ if (i !== undefined) {
+ queue[i] = undefined
+ }
+ coalesced.set(k, queue.length)
+ }
+ queue.push({ directory, payload })
+ schedule()
+
+ if (Date.now() - yielded < 8) continue
+ yielded = Date.now()
+ await new Promise<void>((resolve) => setTimeout(resolve, 0))
}
- })().catch(() => undefined)
+ })()
+ .finally(stop)
+ .catch(() => undefined)
- onCleanup(() => abort.abort())
+ onCleanup(() => {
+ abort.abort()
+ stop()
+ })
const platform = usePlatform()
const sdk = createOpencodeClient({
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index 913e54d10..a0656c5fc 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -23,7 +23,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
-import { batch, createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
+import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
@@ -212,7 +212,7 @@ function createGlobalSync() {
.catch((e) => setGlobalStore("error", e))
}
- globalSDK.event.listen((e) => {
+ const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -404,6 +404,7 @@ function createGlobalSync() {
}
}
})
+ onCleanup(unsub)
async function bootstrap() {
const health = await globalSDK.client.global
diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx
index a6c0dccb6..3af840556 100644
--- a/packages/app/src/context/local.tsx
+++ b/packages/app/src/context/local.tsx
@@ -1,5 +1,5 @@
import { createStore, produce, reconcile } from "solid-js/store"
-import { batch, createMemo } from "solid-js"
+import { batch, createMemo, onCleanup } from "solid-js"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -465,7 +465,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const searchFilesAndDirectories = (query: string) =>
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
- sdk.event.listen((e) => {
+ const unsub = sdk.event.listen((e) => {
const event = e.details
switch (event.type) {
case "file.watcher.updated":
@@ -475,6 +475,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
break
}
})
+ onCleanup(unsub)
return {
node: async (path: string) => {
diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx
index 33d72d4f2..09f32a3c4 100644
--- a/packages/app/src/context/notification.tsx
+++ b/packages/app/src/context/notification.tsx
@@ -1,4 +1,5 @@
import { createStore } from "solid-js/store"
+import { onCleanup } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
@@ -54,7 +55,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
}),
)
- globalSDK.event.listen((e) => {
+ const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
const base = {
@@ -104,6 +105,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
}
}
})
+ onCleanup(unsub)
return {
ready,
diff --git a/packages/app/src/context/sdk.tsx b/packages/app/src/context/sdk.tsx
index 6fd86db49..aa4820c49 100644
--- a/packages/app/src/context/sdk.tsx
+++ b/packages/app/src/context/sdk.tsx
@@ -1,6 +1,7 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
+import { onCleanup } from "solid-js"
import { useGlobalSDK } from "./global-sdk"
import { usePlatform } from "./platform"
@@ -20,9 +21,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
[key in Event["type"]]: Extract<Event, { type: key }>
}>()
- globalSDK.event.on(props.directory, async (event) => {
+ const unsub = globalSDK.event.on(props.directory, (event) => {
emitter.emit(event.type, event)
})
+ onCleanup(unsub)
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
},