perf(shared): optimize hot-path utilities across plugin
- task-list: replace O(n³) blocker resolution with Map lookup (C4) - logger: buffer log entries and flush periodically to reduce sync I/O (C5) - plugin-interface: create chatParamsHandler once at init (H3) - pattern-matcher: cache compiled RegExp for wildcard matchers (H6) - file-reference-resolver: use replaceAll instead of split/join (M9) - connected-providers-cache: add in-memory cache for read operations (L4)
This commit is contained in:
@@ -32,10 +32,7 @@ export function createPluginInterface(args: {
|
|||||||
return {
|
return {
|
||||||
tool: tools,
|
tool: tools,
|
||||||
|
|
||||||
"chat.params": async (input: unknown, output: unknown) => {
|
"chat.params": createChatParamsHandler({ anthropicEffort: hooks.anthropicEffort }),
|
||||||
const handler = createChatParamsHandler({ anthropicEffort: hooks.anthropicEffort })
|
|
||||||
await handler(input, output)
|
|
||||||
},
|
|
||||||
|
|
||||||
"chat.headers": createChatHeadersHandler({ ctx }),
|
"chat.headers": createChatHeadersHandler({ ctx }),
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export function createConnectedProvidersCacheStore(
|
|||||||
return join(getCacheDir(), filename)
|
return join(getCacheDir(), filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let memConnected: string[] | null | undefined
|
||||||
|
let memProviderModels: ProviderModelsCache | null | undefined
|
||||||
|
|
||||||
function ensureCacheDir(): void {
|
function ensureCacheDir(): void {
|
||||||
const cacheDir = getCacheDir()
|
const cacheDir = getCacheDir()
|
||||||
if (!existsSync(cacheDir)) {
|
if (!existsSync(cacheDir)) {
|
||||||
@@ -40,10 +43,12 @@ export function createConnectedProvidersCacheStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readConnectedProvidersCache(): string[] | null {
|
function readConnectedProvidersCache(): string[] | null {
|
||||||
|
if (memConnected !== undefined) return memConnected
|
||||||
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE)
|
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE)
|
||||||
|
|
||||||
if (!existsSync(cacheFile)) {
|
if (!existsSync(cacheFile)) {
|
||||||
log("[connected-providers-cache] Cache file not found", { cacheFile })
|
log("[connected-providers-cache] Cache file not found", { cacheFile })
|
||||||
|
memConnected = null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,9 +56,11 @@ export function createConnectedProvidersCacheStore(
|
|||||||
const content = readFileSync(cacheFile, "utf-8")
|
const content = readFileSync(cacheFile, "utf-8")
|
||||||
const data = JSON.parse(content) as ConnectedProvidersCache
|
const data = JSON.parse(content) as ConnectedProvidersCache
|
||||||
log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt })
|
log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt })
|
||||||
|
memConnected = data.connected
|
||||||
return data.connected
|
return data.connected
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("[connected-providers-cache] Error reading cache", { error: String(err) })
|
log("[connected-providers-cache] Error reading cache", { error: String(err) })
|
||||||
|
memConnected = null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +81,7 @@ export function createConnectedProvidersCacheStore(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
writeFileSync(cacheFile, JSON.stringify(data, null, 2))
|
writeFileSync(cacheFile, JSON.stringify(data, null, 2))
|
||||||
|
memConnected = connected
|
||||||
log("[connected-providers-cache] Cache written", { count: connected.length })
|
log("[connected-providers-cache] Cache written", { count: connected.length })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("[connected-providers-cache] Error writing cache", { error: String(err) })
|
log("[connected-providers-cache] Error writing cache", { error: String(err) })
|
||||||
@@ -81,10 +89,12 @@ export function createConnectedProvidersCacheStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readProviderModelsCache(): ProviderModelsCache | null {
|
function readProviderModelsCache(): ProviderModelsCache | null {
|
||||||
|
if (memProviderModels !== undefined) return memProviderModels
|
||||||
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE)
|
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE)
|
||||||
|
|
||||||
if (!existsSync(cacheFile)) {
|
if (!existsSync(cacheFile)) {
|
||||||
log("[connected-providers-cache] Provider-models cache file not found", { cacheFile })
|
log("[connected-providers-cache] Provider-models cache file not found", { cacheFile })
|
||||||
|
memProviderModels = null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +105,11 @@ export function createConnectedProvidersCacheStore(
|
|||||||
providerCount: Object.keys(data.models).length,
|
providerCount: Object.keys(data.models).length,
|
||||||
updatedAt: data.updatedAt,
|
updatedAt: data.updatedAt,
|
||||||
})
|
})
|
||||||
|
memProviderModels = data
|
||||||
return data
|
return data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) })
|
log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) })
|
||||||
|
memProviderModels = null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +130,7 @@ export function createConnectedProvidersCacheStore(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2))
|
writeFileSync(cacheFile, JSON.stringify(cacheData, null, 2))
|
||||||
|
memProviderModels = cacheData
|
||||||
log("[connected-providers-cache] Provider-models cache written", {
|
log("[connected-providers-cache] Provider-models cache written", {
|
||||||
providerCount: Object.keys(data.models).length,
|
providerCount: Object.keys(data.models).length,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export async function resolveFileReferencesInText(
|
|||||||
|
|
||||||
let resolved = text
|
let resolved = text
|
||||||
for (const [pattern, replacement] of replacements.entries()) {
|
for (const [pattern, replacement] of replacements.entries()) {
|
||||||
resolved = resolved.split(pattern).join(replacement)
|
resolved = resolved.replaceAll(pattern, replacement)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
||||||
|
|||||||
+29
-3
@@ -1,16 +1,42 @@
|
|||||||
// Shared logging utility for the plugin
|
|
||||||
|
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
|
|
||||||
const logFile = path.join(os.tmpdir(), "oh-my-opencode.log")
|
const logFile = path.join(os.tmpdir(), "oh-my-opencode.log")
|
||||||
|
|
||||||
|
let buffer: string[] = []
|
||||||
|
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const FLUSH_INTERVAL_MS = 500
|
||||||
|
const BUFFER_SIZE_LIMIT = 50
|
||||||
|
|
||||||
|
function flush(): void {
|
||||||
|
if (buffer.length === 0) return
|
||||||
|
const data = buffer.join("")
|
||||||
|
buffer = []
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(logFile, data)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFlush(): void {
|
||||||
|
if (flushTimer) return
|
||||||
|
flushTimer = setTimeout(() => {
|
||||||
|
flushTimer = null
|
||||||
|
flush()
|
||||||
|
}, FLUSH_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
export function log(message: string, data?: unknown): void {
|
export function log(message: string, data?: unknown): void {
|
||||||
try {
|
try {
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}\n`
|
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}\n`
|
||||||
fs.appendFileSync(logFile, logEntry)
|
buffer.push(logEntry)
|
||||||
|
if (buffer.length >= BUFFER_SIZE_LIMIT) {
|
||||||
|
flush()
|
||||||
|
} else {
|
||||||
|
scheduleFlush()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ function escapeRegexExceptAsterisk(str: string): string {
|
|||||||
return str.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
return str.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const regexCache = new Map<string, RegExp>()
|
||||||
|
|
||||||
export function matchesToolMatcher(toolName: string, matcher: string): boolean {
|
export function matchesToolMatcher(toolName: string, matcher: string): boolean {
|
||||||
if (!matcher) {
|
if (!matcher) {
|
||||||
return true
|
return true
|
||||||
@@ -17,8 +19,12 @@ export function matchesToolMatcher(toolName: string, matcher: string): boolean {
|
|||||||
return patterns.some((p) => {
|
return patterns.some((p) => {
|
||||||
if (p.includes("*")) {
|
if (p.includes("*")) {
|
||||||
// First escape regex special chars (except *), then convert * to .*
|
// First escape regex special chars (except *), then convert * to .*
|
||||||
const escaped = escapeRegexExceptAsterisk(p)
|
let regex = regexCache.get(p)
|
||||||
const regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i")
|
if (!regex) {
|
||||||
|
const escaped = escapeRegexExceptAsterisk(p)
|
||||||
|
regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i")
|
||||||
|
regexCache.set(p, regex)
|
||||||
|
}
|
||||||
return regex.test(toolName)
|
return regex.test(toolName)
|
||||||
}
|
}
|
||||||
return p.toLowerCase() === toolName.toLowerCase()
|
return p.toLowerCase() === toolName.toLowerCase()
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const taskMap = new Map(allTasks.map((t) => [t.id, t]))
|
||||||
|
|
||||||
// Filter out completed and deleted tasks
|
// Filter out completed and deleted tasks
|
||||||
const activeTasks = allTasks.filter(
|
const activeTasks = allTasks.filter(
|
||||||
(task) => task.status !== "completed" && task.status !== "deleted"
|
(task) => task.status !== "completed" && task.status !== "deleted"
|
||||||
@@ -54,7 +56,7 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
|
|||||||
const summaries: TaskSummary[] = activeTasks.map((task) => {
|
const summaries: TaskSummary[] = activeTasks.map((task) => {
|
||||||
// Filter blockedBy to only include unresolved (non-completed) blockers
|
// Filter blockedBy to only include unresolved (non-completed) blockers
|
||||||
const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
|
const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
|
||||||
const blockerTask = allTasks.find((t) => t.id === blockerId)
|
const blockerTask = taskMap.get(blockerId)
|
||||||
// Include if blocker doesn't exist (missing) or if it's not completed
|
// Include if blocker doesn't exist (missing) or if it's not completed
|
||||||
return !blockerTask || blockerTask.status !== "completed"
|
return !blockerTask || blockerTask.status !== "completed"
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user