From 8e3aeea19a02114caecbd66f4cdfd175efd9b758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Troed=20S=C3=A5ngberg?= Date: Wed, 8 Jul 2026 13:11:59 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20v1.1.0=20=E2=80=94=20disable=20debug?= =?UTF-8?q?=20log,=20simplify=20PP/TG=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- src/stats.ts | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++ tui.tsx | 136 +++++---------------------------- 3 files changed, 229 insertions(+), 120 deletions(-) create mode 100644 src/stats.ts diff --git a/package.json b/package.json index 20facab..4e73c60 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@troed/oc-ls-stats", - "version": "1.0.18", + "version": "1.1.0", "type": "module", "exports": { "./tui": { "import": "./tui.tsx" } }, + "scripts": { + "prepack": "npx -y tsx test-stats.ts" + }, "engines": { "opencode": ">=1.3.14" }, diff --git a/src/stats.ts b/src/stats.ts new file mode 100644 index 0000000..f1c9889 --- /dev/null +++ b/src/stats.ts @@ -0,0 +1,208 @@ +export interface SlotState { + id: number + is_processing: boolean + n_prompt_tokens: number + next_token: { n_decoded: number }[] + id_task: number +} + +export interface TrackerState { + prevNdBySlot: Record + prevPromptTokensBySlot: Record + isPrefilling: boolean + isGenerating: boolean + prefillSlotId: number | null + prefillCapturedTokens: number | null + prefillStartAt: number + lastPrefillRate: number + generateSlotId: number | null + generatePrevNd: number + generateStartAt: number + lastGeneratedTps: number +} + +export interface ClassificationResult { + slotId: number + nd: number + prevNd: number + nt: number + prevNt: number + classification: "TG" | "PP" | "neither" +} + +export interface PollResult { + classifications: ClassificationResult[] + isPrefilling: boolean + isGenerating: boolean + lastPrefillRate: number + lastGeneratedTps: number + ppEnded: boolean + tgEnded: boolean +} + +export function classifySlot( + slot: SlotState, + tracker: TrackerState, +): ClassificationResult { + const slotId = slot.id + const nd = slot.next_token?.[0]?.n_decoded ?? 0 + const nt = slot.n_prompt_tokens ?? 0 + const idTask = slot.id_task ?? 0 + + const ndEntry = tracker.prevNdBySlot[slotId] + const ntEntry = tracker.prevPromptTokensBySlot[slotId] + + // Check if we have valid baseline for this slot/task + const hasBaseline = ndEntry && ndEntry.idTask === idTask && ntEntry && ntEntry.idTask === idTask + const baselineNd = hasBaseline ? ndEntry.prevNd : nd + const baselineNt = hasBaseline ? ntEntry.tokens : nt + + // Check if values have reset (dropped below baseline) - indicates slot reuse or new request + const hasReset = hasBaseline && (nd < baselineNd || nt < baselineNt) + + let classification: "TG" | "PP" | "neither" = "neither" + let prevNd = nd + let prevNt = nt + + if (!hasBaseline || hasReset) { + // First time seeing this slot/task, or values have reset + // Use heuristic: if n_decoded > 0, we likely caught it mid-generation + if (nd > 0) { + classification = "TG" + } else { + classification = "PP" + } + } else { + // Compare against baseline values + if (nd > baselineNd) { + classification = "TG" + } else if (nt > baselineNt) { + classification = "PP" + } else { + // Neither has increased from baseline - persist previous classification + // This handles frequent polling where we don't see increases every poll + const prevClassification = ndEntry?.classification + if (prevClassification) { + classification = prevClassification + } + } + prevNd = baselineNd + prevNt = baselineNt + } + + return { slotId, nd, prevNd, nt, prevNt, classification } +} + +export function processPoll( + slots: SlotState[], + tracker: TrackerState, + now: number = Date.now(), +): PollResult { + const processingSlots = slots.filter(s => s.is_processing) + const processingSlotIds = new Set(processingSlots.map(s => s.id)) + + // Clean up tracking state for slots that went idle + for (const key of Object.keys(tracker.prevNdBySlot)) { + const slotId = Number(key) + if (!processingSlotIds.has(slotId)) { + delete tracker.prevNdBySlot[slotId] + } + } + for (const key of Object.keys(tracker.prevPromptTokensBySlot)) { + const slotId = Number(key) + if (!processingSlotIds.has(slotId)) { + delete tracker.prevPromptTokensBySlot[slotId] + } + } + + // Classify each processing slot + const classifications: ClassificationResult[] = [] + let prefillSlot: ClassificationResult | null = null + let generateSlot: ClassificationResult | null = null + + for (const slot of processingSlots) { + const result = classifySlot(slot, tracker) + classifications.push(result) + + if (result.classification === "TG") { + generateSlot = result + } else if (result.classification === "PP") { + prefillSlot = result + } + + // Update tracker state - store baseline values and classification + tracker.prevNdBySlot[slot.id] = { prevNd: result.nd, idTask: slot.id_task, classification: result.classification } + tracker.prevPromptTokensBySlot[slot.id] = { tokens: result.nt, idTask: slot.id_task } + } + + let anyPrefilling = false + let anyGenerating = false + let ppEnded = false + let tgEnded = false + + // Handle PP phase + if (prefillSlot) { + anyPrefilling = true + const nt = prefillSlot.nt + if (tracker.prefillSlotId === prefillSlot.slotId) { + const dt = (now - tracker.prefillStartAt) / 1000 + const delta = nt - (tracker.prefillCapturedTokens ?? 0) + if (dt > 0 && delta > 0) { + tracker.lastPrefillRate = delta / dt + } + } else { + tracker.prefillSlotId = prefillSlot.slotId + tracker.prefillCapturedTokens = nt + tracker.prefillStartAt = now + tracker.lastPrefillRate = 0 + } + } else if (processingSlotIds.size > 0 && !generateSlot) { + anyPrefilling = tracker.isPrefilling + } else if (tracker.isPrefilling) { + tracker.prefillSlotId = null + tracker.prefillCapturedTokens = null + tracker.prefillStartAt = 0 + ppEnded = true + } + + // Handle TG phase + if (generateSlot) { + anyGenerating = true + const nd = generateSlot.nd + if (tracker.generateSlotId === generateSlot.slotId) { + const dt = (now - tracker.generateStartAt) / 1000 + const delta = nd - tracker.generatePrevNd + if (dt > 0 && delta > 0) { + tracker.lastGeneratedTps = delta / dt + } + tracker.generatePrevNd = nd + tracker.generateStartAt = now + } else { + tracker.generateSlotId = generateSlot.slotId + tracker.generatePrevNd = nd + tracker.generateStartAt = now + tracker.lastGeneratedTps = 0 + } + } else if (processingSlotIds.size > 0 && !prefillSlot) { + anyGenerating = tracker.isGenerating + } else if (tracker.isGenerating) { + tracker.generateSlotId = null + tracker.generatePrevNd = 0 + tracker.generateStartAt = 0 + tracker.lastGeneratedTps = 0 + tgEnded = true + } + + tracker.isPrefilling = anyPrefilling + tracker.isGenerating = anyGenerating + + return { + classifications, + isPrefilling: tracker.isPrefilling, + isGenerating: tracker.isGenerating, + lastPrefillRate: tracker.lastPrefillRate, + lastGeneratedTps: tracker.lastGeneratedTps, + ppEnded, + tgEnded, + } +} diff --git a/tui.tsx b/tui.tsx index 5b19b6a..294bc78 100644 --- a/tui.tsx +++ b/tui.tsx @@ -2,8 +2,9 @@ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, createSignal } from "solid-js" import { appendFileSync } from "fs" +import { processPoll } from "./src/stats.ts" -const DEBUG_ENABLED = true +const DEBUG_ENABLED = false const DEBUG_LOG = "/tmp/oc-ls-stats-debug.log" function debug(...args: unknown[]) { if (!DEBUG_ENABLED) return @@ -41,7 +42,8 @@ type MessageTiming = { generateSlotId: number | null generatePrevNd: number generateStartAt: number - prevNdBySlot: Record + prevNdBySlot: Record + prevPromptTokensBySlot: Record failure: string | null } @@ -172,6 +174,7 @@ const tui: TuiPlugin = async (api) => { generatePrevNd: 0, generateStartAt: 0, prevNdBySlot: {}, + prevPromptTokensBySlot: {}, failure: null, } const [version, setVersion] = createSignal(0) @@ -237,18 +240,11 @@ const tui: TuiPlugin = async (api) => { } } - let anyPrefilling = false - let anyGenerating = false - let genEnded = false - - const prevProcessingSlotIds = new Set(Object.keys(tracker.prevNdBySlot).map(Number)) - const currProcessingSlotIds = new Set() - - debug(`pollMetrics: urls=${JSON.stringify(llamaServerUrls)} model=${model}`) + debug(`pollMetrics: urls=${JSON.stringify(llamaServerUrls)} model=${model}`) for (const baseUrl of llamaServerUrls) { debug(`pollMetrics: polling ${baseUrl}`) const slots = await fetchSlots(baseUrl, model) - debug(`pollMetrics: ${baseUrl} response=${JSON.stringify(slots)?.slice(0, 500)}`) + debug(`pollMetrics: ${baseUrl} response=${JSON.stringify(slots)}`) const slotList = Array.isArray(slots) ? slots : slots ? Object.values(slots) : [] debug(`pollMetrics: ${baseUrl} slotList.length=${slotList.length}`) if (slotList.length === 0) { @@ -259,121 +255,23 @@ const tui: TuiPlugin = async (api) => { tracker.failure = null bump() - + const result = processPoll(slotList, tracker) - for (const slot of slotList) { - if (slot?.is_processing) { - currProcessingSlotIds.add(slot.id) - } + if (result.isPrefilling) { + debug(`PP rate=${Math.round(result.lastPrefillRate)} tps`) } - - for (const slotId of prevProcessingSlotIds) { - if (!currProcessingSlotIds.has(slotId)) { - delete tracker.prevNdBySlot[slotId] - } + if (result.isGenerating) { + debug(`TG rate=${Math.round(result.lastGeneratedTps)} tps`) } - - let prefillSlot: any = null - let generateSlot: any = null - for (const slot of slotList) { - if (!slot?.is_processing) continue - const slotId = slot.id - const nd = slot.next_token?.[0]?.n_decoded ?? 0 - const idTask = slot.id_task ?? 0 - const entry = tracker.prevNdBySlot[slotId] - const baseline = entry?.idTask === idTask ? entry.baseline : nd - const hasIncreased = entry?.idTask === idTask ? entry.hasIncreased : false - - if (entry && entry.idTask !== idTask) { - debug(`slot ${slotId} id_task changed from ${entry.idTask} to ${idTask}, resetting state`) - } - - if (!hasIncreased && nd <= baseline) { - prefillSlot = slot - break - } else if (nd > baseline) { - tracker.prevNdBySlot[slotId] = { baseline, hasIncreased: true, idTask } - generateSlot = slot - } + if (result.ppEnded) { + debug(`PP ended rate=${Math.round(result.lastPrefillRate)} tps`) } - - for (const slot of slotList) { - if (slot?.is_processing) { - const slotId = slot.id - const nd = slot.next_token?.[0]?.n_decoded ?? 0 - const idTask = slot.id_task ?? 0 - const entry = tracker.prevNdBySlot[slotId] - if (entry === undefined || entry.idTask !== idTask) { - tracker.prevNdBySlot[slotId] = { baseline: nd, hasIncreased: false, idTask } - } - } + if (result.tgEnded) { + debug(`TG ended rate=${Math.round(result.lastGeneratedTps)} tps`) } - - if (prefillSlot) { - anyPrefilling = true - const slotId = prefillSlot.id - const now = Date.now() - const nt = prefillSlot.n_prompt_tokens ?? 0 - - if (tracker.prefillSlotId === slotId) { - const dt = (now - tracker.prefillStartAt) / 1000 - const delta = nt - tracker.prefillCapturedTokens! - if (dt > 0 && delta > 0) { - tracker.lastPrefillRate = delta / dt - } - } else { - tracker.prefillSlotId = slotId - tracker.prefillCapturedTokens = nt - tracker.prefillStartAt = now - tracker.lastPrefillRate = 0 - - } - } else { - if (tracker.isPrefilling) { - tracker.prefillSlotId = null - tracker.prefillCapturedTokens = null - tracker.prefillStartAt = 0 - } - } - - if (generateSlot) { - anyGenerating = true - const slotId = generateSlot.id - const now = Date.now() - const nd = generateSlot.next_token?.[0]?.n_decoded ?? 0 - - if (tracker.generateSlotId === slotId) { - const dt = (now - tracker.generateStartAt) / 1000 - const delta = nd - tracker.generatePrevNd - if (dt > 0 && delta > 0) { - tracker.lastGeneratedTps = delta / dt - - } - tracker.generatePrevNd = nd - tracker.generateStartAt = now - } else { - tracker.generateSlotId = slotId - tracker.generatePrevNd = nd - tracker.generateStartAt = now - tracker.lastGeneratedTps = 0 - - } - } else { - if (tracker.isGenerating) { - tracker.generateSlotId = null - tracker.generatePrevNd = 0 - tracker.generateStartAt = 0 - tracker.lastGeneratedTps = 0 - genEnded = true - - } - } - - tracker.isPrefilling = anyPrefilling - tracker.isGenerating = anyGenerating } - if (tracker.lastPrefillRate > 0 || tracker.lastGeneratedTps > 0 || genEnded) { + if (tracker.lastPrefillRate > 0 || tracker.lastGeneratedTps > 0) { bump() } }