Просмотр исходного кода

fix(app): performance improvements through event batching

Adam 1 месяц назад
Родитель
Сommit
001b486356

+ 69 - 4
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({

+ 3 - 2
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

+ 3 - 2
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) => {

+ 3 - 1
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,

+ 3 - 1
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 }
   },