mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 提前记录 pending 用量、优化流遥测时序与前端活跃请求发现机制
- 将 record_pending 调用移至执行开始前(sync/stream 两路),确保请求在执行前即有 pending 记录 - stream_pump 在收到第一个数据块前优先 yield 遥测帧,保证 ttfb 早于 data 帧到达 - stream execution 增加 should_refresh_stream_usage_telemetry,在遥测帧携带新 ttfb/elapsed 时及时更新 record_stream_started - access_log 对高频轮询路径(usage/active、usage/records 等)降级为 TRACE 日志,减少日志噪音 - 前端新增 reconcileActiveRequestDiscovery 工具函数及 discoverActiveRequests 逻辑,活跃请求发现与全局自动刷新解耦,空闲时降频为 5 秒扫描 - RequestDetailDrawer 调整:进行中请求不再自动开启轮询,由用户手动触发;刷新按钮 title 动态适配状态
This commit is contained in:
@@ -77,7 +77,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="loading && !autoRefreshing"
|
||||
:title="autoRefreshing ? '停止自动刷新' : '刷新'"
|
||||
:title="refreshButtonTitle"
|
||||
@click="refreshDetail"
|
||||
>
|
||||
<RefreshCw
|
||||
@@ -813,6 +813,10 @@ let timelineMountTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const fullRequestId = computed(() => detail.value?.request_id || detail.value?.id || '-')
|
||||
const displayRequestId = computed(() => formatShortRequestId(fullRequestId.value))
|
||||
const refreshButtonTitle = computed(() => {
|
||||
if (autoRefreshing.value) return '停止自动刷新'
|
||||
return isRequestCompleted() ? '刷新' : '开启自动刷新'
|
||||
})
|
||||
const displayInputTokens = computed(() => {
|
||||
if (!detail.value) return 0
|
||||
return getEffectiveInputTokens({
|
||||
@@ -1686,13 +1690,9 @@ async function loadDetail(id: string, silent = false) {
|
||||
timelineRef.value?.refresh()
|
||||
}
|
||||
|
||||
// 抽屉打开时,对进行中请求自动保持刷新,保证详情实时更新
|
||||
if (props.isOpen) {
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
} else {
|
||||
startAutoRefresh()
|
||||
}
|
||||
// 已完成请求需要停止自动刷新;进行中的请求只在用户手动开启后才保持刷新
|
||||
if (props.isOpen && isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
} catch (err) {
|
||||
if (requestId !== loadDetailRequestId) return
|
||||
@@ -1781,10 +1781,6 @@ function handleVisibilityChange() {
|
||||
isPageVisible.value = !document.hidden
|
||||
if (!isPageVisible.value) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
if (props.isOpen && props.requestId && !isRequestCompleted()) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:class="autoRefresh ? 'text-primary' : ''"
|
||||
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新(每3秒刷新)'"
|
||||
:title="autoRefresh ? '点击关闭自动刷新' : '点击开启自动刷新'"
|
||||
@click="$emit('update:autoRefresh', !autoRefresh)"
|
||||
>
|
||||
<RefreshCcw
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { reconcileActiveRequestDiscovery } from '../activeRequestDiscovery'
|
||||
|
||||
describe('reconcileActiveRequestDiscovery', () => {
|
||||
it('returns unseen active request ids while retaining still-pending discoveries', () => {
|
||||
const result = reconcileActiveRequestDiscovery({
|
||||
activeRequestIds: ['req-new', 'req-retained', 'req-new'],
|
||||
knownRecordIds: ['req-known'],
|
||||
discoveredActiveRequestIds: ['req-retained', 'req-stale']
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
retainedDiscoveredActiveRequestIds: ['req-retained'],
|
||||
unseenActiveRequestIds: ['req-new']
|
||||
})
|
||||
})
|
||||
|
||||
it('drops discovered ids once they are known in the table', () => {
|
||||
const result = reconcileActiveRequestDiscovery({
|
||||
activeRequestIds: ['req-known', 'req-fresh'],
|
||||
knownRecordIds: ['req-known'],
|
||||
discoveredActiveRequestIds: ['req-known']
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
retainedDiscoveredActiveRequestIds: [],
|
||||
unseenActiveRequestIds: ['req-fresh']
|
||||
})
|
||||
})
|
||||
|
||||
it('returns no unseen ids when every active request is already known or retained', () => {
|
||||
const result = reconcileActiveRequestDiscovery({
|
||||
activeRequestIds: ['req-known', 'req-retained'],
|
||||
knownRecordIds: ['req-known'],
|
||||
discoveredActiveRequestIds: ['req-retained']
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
retainedDiscoveredActiveRequestIds: ['req-retained'],
|
||||
unseenActiveRequestIds: []
|
||||
})
|
||||
})
|
||||
})
|
||||
43
frontend/src/features/usage/utils/activeRequestDiscovery.ts
Normal file
43
frontend/src/features/usage/utils/activeRequestDiscovery.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface ActiveRequestDiscoverySnapshot {
|
||||
activeRequestIds: Iterable<string>
|
||||
knownRecordIds: Iterable<string>
|
||||
discoveredActiveRequestIds: Iterable<string>
|
||||
}
|
||||
|
||||
export interface ActiveRequestDiscoveryResult {
|
||||
retainedDiscoveredActiveRequestIds: string[]
|
||||
unseenActiveRequestIds: string[]
|
||||
}
|
||||
|
||||
export function reconcileActiveRequestDiscovery(
|
||||
snapshot: ActiveRequestDiscoverySnapshot
|
||||
): ActiveRequestDiscoveryResult {
|
||||
const knownRecordIds = new Set(snapshot.knownRecordIds)
|
||||
const activeRequestIds: string[] = []
|
||||
const activeRequestIdSet = new Set<string>()
|
||||
|
||||
for (const id of snapshot.activeRequestIds) {
|
||||
if (!id || activeRequestIdSet.has(id)) continue
|
||||
activeRequestIdSet.add(id)
|
||||
activeRequestIds.push(id)
|
||||
}
|
||||
|
||||
const retainedDiscoveredActiveRequestIds: string[] = []
|
||||
const retainedDiscoveredSet = new Set<string>()
|
||||
|
||||
for (const id of snapshot.discoveredActiveRequestIds) {
|
||||
if (!id || retainedDiscoveredSet.has(id)) continue
|
||||
if (knownRecordIds.has(id) || !activeRequestIdSet.has(id)) continue
|
||||
retainedDiscoveredSet.add(id)
|
||||
retainedDiscoveredActiveRequestIds.push(id)
|
||||
}
|
||||
|
||||
const unseenActiveRequestIds = activeRequestIds.filter(
|
||||
id => !knownRecordIds.has(id) && !retainedDiscoveredSet.has(id)
|
||||
)
|
||||
|
||||
return {
|
||||
retainedDiscoveredActiveRequestIds,
|
||||
unseenActiveRequestIds
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,7 @@ import {
|
||||
useUsageData,
|
||||
getDateRangeFromPeriod
|
||||
} from '@/features/usage/composables'
|
||||
import { reconcileActiveRequestDiscovery } from '@/features/usage/utils/activeRequestDiscovery'
|
||||
import type { DateRangeParams, FilterStatusValue } from '@/features/usage/types'
|
||||
import type { UserOption } from '@/features/usage/components/UsageRecordsTable.vue'
|
||||
import { log } from '@/utils/logger'
|
||||
@@ -334,9 +335,12 @@ const hasActiveRequests = computed(() => activeRequestIds.value.length > 0)
|
||||
|
||||
// 自动刷新定时器
|
||||
let autoRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let activeDiscoveryTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
let refreshInFlight: Promise<void> | null = null
|
||||
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次(用于活跃请求)
|
||||
const ACTIVE_DISCOVERY_HOT_INTERVAL = 1000 // 有活跃请求时 1 秒扫描一次
|
||||
const ACTIVE_DISCOVERY_IDLE_INTERVAL = 5000 // 空闲时降频,避免后台持续刷日志
|
||||
const GLOBAL_AUTO_REFRESH_INTERVAL = 3000 // 3秒刷新一次(全局自动刷新)
|
||||
const globalAutoRefresh = ref(false) // 全局自动刷新开关(默认关闭)
|
||||
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
|
||||
@@ -344,6 +348,17 @@ const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hid
|
||||
// 轮询活跃请求状态(轻量级,只更新状态变化的记录)
|
||||
|
||||
let pollInFlight = false
|
||||
let activeDiscoveryInFlight = false
|
||||
const discoveredActiveRequestIds = new Set<string>()
|
||||
|
||||
async function loadActiveRequestUpdates(ids?: string[]) {
|
||||
if (isAdminPage.value) {
|
||||
return usageApi.getActiveRequests(ids)
|
||||
}
|
||||
const idsParam = ids?.length ? ids.join(',') : undefined
|
||||
return meApi.getActiveRequests(idsParam)
|
||||
}
|
||||
|
||||
async function pollActiveRequests() {
|
||||
if (!isPageVisible.value) return
|
||||
if (!hasActiveRequests.value) return
|
||||
@@ -351,11 +366,7 @@ async function pollActiveRequests() {
|
||||
pollInFlight = true
|
||||
|
||||
try {
|
||||
// 根据页面类型选择不同的 API
|
||||
const idsParam = activeRequestIds.value.join(',')
|
||||
const { requests } = isAdminPage.value
|
||||
? await usageApi.getActiveRequests(activeRequestIds.value)
|
||||
: await meApi.getActiveRequests(idsParam)
|
||||
const { requests } = await loadActiveRequestUpdates(activeRequestIds.value)
|
||||
|
||||
let shouldRefresh = false
|
||||
|
||||
@@ -435,6 +446,37 @@ async function pollActiveRequests() {
|
||||
}
|
||||
}
|
||||
|
||||
async function discoverActiveRequests() {
|
||||
if (!isPageVisible.value) return
|
||||
if (activeDiscoveryInFlight) return
|
||||
if (refreshInFlight || isLoadingRecords.value) return
|
||||
activeDiscoveryInFlight = true
|
||||
|
||||
try {
|
||||
const { requests } = await loadActiveRequestUpdates()
|
||||
const {
|
||||
retainedDiscoveredActiveRequestIds,
|
||||
unseenActiveRequestIds
|
||||
} = reconcileActiveRequestDiscovery({
|
||||
activeRequestIds: requests.map(request => request.id),
|
||||
knownRecordIds: currentRecords.value.map(record => record.id),
|
||||
discoveredActiveRequestIds
|
||||
})
|
||||
|
||||
discoveredActiveRequestIds.clear()
|
||||
retainedDiscoveredActiveRequestIds.forEach(id => discoveredActiveRequestIds.add(id))
|
||||
|
||||
if (unseenActiveRequestIds.length > 0) {
|
||||
unseenActiveRequestIds.forEach(id => discoveredActiveRequestIds.add(id))
|
||||
await refreshData()
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('发现新活跃请求失败:', error)
|
||||
} finally {
|
||||
activeDiscoveryInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextAutoRefresh() {
|
||||
if (autoRefreshTimer) return
|
||||
if (!isPageVisible.value || !hasActiveRequests.value) return
|
||||
@@ -445,12 +487,34 @@ function scheduleNextAutoRefresh() {
|
||||
}, AUTO_REFRESH_INTERVAL)
|
||||
}
|
||||
|
||||
function scheduleNextActiveDiscovery() {
|
||||
if (activeDiscoveryTimer) return
|
||||
if (!isPageVisible.value) return
|
||||
const interval = hasActiveRequests.value || discoveredActiveRequestIds.size > 0
|
||||
? ACTIVE_DISCOVERY_HOT_INTERVAL
|
||||
: ACTIVE_DISCOVERY_IDLE_INTERVAL
|
||||
activeDiscoveryTimer = setTimeout(async () => {
|
||||
activeDiscoveryTimer = null
|
||||
await discoverActiveRequests()
|
||||
scheduleNextActiveDiscovery()
|
||||
}, interval)
|
||||
}
|
||||
|
||||
// 启动自动刷新
|
||||
function startAutoRefresh() {
|
||||
if (!isPageVisible.value) return
|
||||
scheduleNextAutoRefresh()
|
||||
}
|
||||
|
||||
function startActiveDiscovery() {
|
||||
if (!isPageVisible.value) return
|
||||
if (activeDiscoveryTimer || activeDiscoveryInFlight) return
|
||||
void (async () => {
|
||||
await discoverActiveRequests()
|
||||
scheduleNextActiveDiscovery()
|
||||
})()
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshTimer) {
|
||||
@@ -459,10 +523,17 @@ function stopAutoRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
function stopActiveDiscovery() {
|
||||
if (activeDiscoveryTimer) {
|
||||
clearTimeout(activeDiscoveryTimer)
|
||||
activeDiscoveryTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 监听活跃请求状态,自动启动/停止刷新
|
||||
// 1秒轮询始终用于活跃请求的实时更新,不受全局刷新影响
|
||||
// 活跃请求的 1 秒轮询受“自动刷新”开关控制
|
||||
watch(hasActiveRequests, (hasActive) => {
|
||||
if (hasActive && isPageVisible.value) {
|
||||
if (globalAutoRefresh.value && hasActive && isPageVisible.value) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
@@ -490,9 +561,15 @@ function handleAutoRefreshChange(value: boolean) {
|
||||
if (value) {
|
||||
if (isPageVisible.value) {
|
||||
refreshData() // 立即刷新一次
|
||||
startActiveDiscovery()
|
||||
if (hasActiveRequests.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
}
|
||||
startGlobalAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
stopActiveDiscovery()
|
||||
stopGlobalAutoRefresh()
|
||||
}
|
||||
}
|
||||
@@ -501,13 +578,15 @@ function handleVisibilityChange() {
|
||||
isPageVisible.value = !document.hidden
|
||||
if (!isPageVisible.value) {
|
||||
stopAutoRefresh()
|
||||
stopActiveDiscovery()
|
||||
stopGlobalAutoRefresh()
|
||||
return
|
||||
}
|
||||
if (hasActiveRequests.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
if (globalAutoRefresh.value) {
|
||||
startActiveDiscovery()
|
||||
if (hasActiveRequests.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
refreshData()
|
||||
startGlobalAutoRefresh()
|
||||
}
|
||||
@@ -517,6 +596,7 @@ function handleVisibilityChange() {
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
stopAutoRefresh()
|
||||
stopActiveDiscovery()
|
||||
stopGlobalAutoRefresh()
|
||||
})
|
||||
|
||||
@@ -574,6 +654,10 @@ onMounted(async () => {
|
||||
])
|
||||
}
|
||||
|
||||
if (globalAutoRefresh.value && isPageVisible.value) {
|
||||
startActiveDiscovery()
|
||||
}
|
||||
|
||||
if (globalAutoRefresh.value && isPageVisible.value) {
|
||||
startGlobalAutoRefresh()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user