mirror of
https://git.sync.wtf/troed/oc-ls-stats.git
synced 2026-08-31 09:43:38 +03:00
chore: v1.1.0 — disable debug log, simplify PP/TG detection
This commit is contained in:
+4
-1
@@ -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"
|
||||
},
|
||||
|
||||
+208
@@ -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<number, { prevNd: number; idTask: number; classification: "TG" | "PP" | "neither" }>
|
||||
prevPromptTokensBySlot: Record<number, { tokens: number; idTask: number }>
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<number, { baseline: number; hasIncreased: boolean; idTask: number }>
|
||||
prevNdBySlot: Record<number, { prevNd: number; idTask: number; classification: "TG" | "PP" | "neither" }>
|
||||
prevPromptTokensBySlot: Record<number, { tokens: number; idTask: number }>
|
||||
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<number>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user