mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
perf(frontend): 优化图表更新链路与缓存监控倒计时开销
- 收敛 LineChart 的配置构建逻辑,复用 options 生成函数
- 将 LineChart 的 data/options 监听从深监听改为引用监听
- 统一使用 chart.update('none'),减少不必要的动画与重绘
- 为 ScatterChart 新增 prepareRenderData 流程,合并间隙压缩与点位转换
- 消除 createChart/updateChart 中重复的数据预处理逻辑
- 将散点图更新改为监听 data、compressGaps、gapThreshold、compressedGapSize
- 移除 compressGaps 切换时的 destroy + recreate 路径,改为原图更新
- 将散点图 options 更新改为无动画刷新,降低全量重算成本
- 为缓存监控页新增 nextExpireAt 状态,跟踪最近过期时间
- 在拉取 affinity 列表后立即按当前时间裁剪已过期数据
- 将每秒全表 filter 改为按最近过期时间触发清理
- 页面恢复可见时先补执行过期清理,再恢复倒计时
- 保留每秒 currentTime 更新,仅用于倒计时显示,降低常驻扫描开销
This commit is contained in:
@@ -46,6 +46,13 @@ interface Props {
|
|||||||
const chartRef = ref<HTMLCanvasElement>()
|
const chartRef = ref<HTMLCanvasElement>()
|
||||||
let chart: ChartJS<'line'> | null = null
|
let chart: ChartJS<'line'> | null = null
|
||||||
|
|
||||||
|
function buildChartOptions(): ChartOptions<'line'> {
|
||||||
|
return {
|
||||||
|
...defaultOptions,
|
||||||
|
...props.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defaultOptions: ChartOptions<'line'> = {
|
const defaultOptions: ChartOptions<'line'> = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -89,10 +96,7 @@ function createChart() {
|
|||||||
chart = new ChartJS(chartRef.value, {
|
chart = new ChartJS(chartRef.value, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: props.data,
|
data: props.data,
|
||||||
options: {
|
options: buildChartOptions()
|
||||||
...defaultOptions,
|
|
||||||
...props.options
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,15 +119,12 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听数据变化
|
// 监听引用变化,避免深监听触发整图重算
|
||||||
watch(() => props.data, updateChart, { deep: true })
|
watch(() => props.data, updateChart)
|
||||||
watch(() => props.options, () => {
|
watch(() => props.options, () => {
|
||||||
if (chart) {
|
if (chart) {
|
||||||
chart.options = {
|
chart.options = buildChartOptions()
|
||||||
...defaultOptions,
|
chart.update('none')
|
||||||
...props.options
|
|
||||||
}
|
|
||||||
chart.update()
|
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -119,6 +119,11 @@ let chart: ChartJS<'scatter'> | null = null
|
|||||||
const crosshairY = ref<number | null>(null)
|
const crosshairY = ref<number | null>(null)
|
||||||
const gapInfoList = ref<GapInfo[]>([])
|
const gapInfoList = ref<GapInfo[]>([])
|
||||||
|
|
||||||
|
interface PreparedRenderData {
|
||||||
|
chartData: ChartData<'scatter'>
|
||||||
|
gaps: GapInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
const crosshairStats = computed<CrosshairStats | null>(() => {
|
const crosshairStats = computed<CrosshairStats | null>(() => {
|
||||||
if (crosshairY.value === null || !props.data.datasets) return null
|
if (crosshairY.value === null || !props.data.datasets) return null
|
||||||
|
|
||||||
@@ -294,6 +299,22 @@ function transformData(data: ChartData<'scatter'>): ChartData<'scatter'> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareRenderData(): PreparedRenderData {
|
||||||
|
let dataToUse = props.data
|
||||||
|
let gaps: GapInfo[] = []
|
||||||
|
|
||||||
|
if (props.compressGaps) {
|
||||||
|
const compressedResult = compressTimeGaps(props.data)
|
||||||
|
dataToUse = compressedResult.data
|
||||||
|
gaps = compressedResult.gaps
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartData: transformData(dataToUse),
|
||||||
|
gaps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化时长
|
// 格式化时长
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
const hours = Math.floor(ms / (1000 * 60 * 60))
|
const hours = Math.floor(ms / (1000 * 60 * 60))
|
||||||
@@ -516,22 +537,12 @@ function handleMouseLeave() {
|
|||||||
function createChart() {
|
function createChart() {
|
||||||
if (!chartRef.value) return
|
if (!chartRef.value) return
|
||||||
|
|
||||||
let dataToUse = props.data
|
const { chartData, gaps } = prepareRenderData()
|
||||||
gapInfoList.value = []
|
gapInfoList.value = gaps
|
||||||
|
|
||||||
// 如果启用间隙压缩
|
|
||||||
if (props.compressGaps) {
|
|
||||||
const { data: compressedData, gaps } = compressTimeGaps(props.data)
|
|
||||||
dataToUse = compressedData
|
|
||||||
gapInfoList.value = gaps
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换数据
|
|
||||||
const transformedData = transformData(dataToUse)
|
|
||||||
|
|
||||||
chart = new ChartJS(chartRef.value, {
|
chart = new ChartJS(chartRef.value, {
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
data: transformedData,
|
data: chartData,
|
||||||
options: {
|
options: {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...props.options
|
...props.options
|
||||||
@@ -544,16 +555,9 @@ function createChart() {
|
|||||||
|
|
||||||
function updateChart() {
|
function updateChart() {
|
||||||
if (chart) {
|
if (chart) {
|
||||||
let dataToUse = props.data
|
const { chartData, gaps } = prepareRenderData()
|
||||||
gapInfoList.value = []
|
gapInfoList.value = gaps
|
||||||
|
chart.data = chartData
|
||||||
if (props.compressGaps) {
|
|
||||||
const { data: compressedData, gaps } = compressTimeGaps(props.data)
|
|
||||||
dataToUse = compressedData
|
|
||||||
gapInfoList.value = gaps
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.data = transformData(dataToUse)
|
|
||||||
chart.update('none')
|
chart.update('none')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,21 +577,22 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.data, updateChart, { deep: true })
|
watch(
|
||||||
watch(() => props.compressGaps, () => {
|
[
|
||||||
if (chart) {
|
() => props.data,
|
||||||
chart.destroy()
|
() => props.compressGaps,
|
||||||
chart = null
|
() => props.gapThreshold,
|
||||||
}
|
() => props.compressedGapSize
|
||||||
createChart()
|
],
|
||||||
})
|
updateChart
|
||||||
|
)
|
||||||
watch(() => props.options, () => {
|
watch(() => props.options, () => {
|
||||||
if (chart) {
|
if (chart) {
|
||||||
chart.options = {
|
chart.options = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...props.options
|
...props.options
|
||||||
}
|
}
|
||||||
chart.update()
|
chart.update('none')
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const currentPage = ref(1)
|
|||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
const currentTime = ref(Math.floor(Date.now() / 1000))
|
const currentTime = ref(Math.floor(Date.now() / 1000))
|
||||||
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
|
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
|
||||||
|
const nextExpireAt = ref<number | null>(null)
|
||||||
|
|
||||||
// ==================== 模型映射缓存 ====================
|
// ==================== 模型映射缓存 ====================
|
||||||
|
|
||||||
@@ -121,6 +122,8 @@ async function fetchAffinityList(keyword?: string) {
|
|||||||
const response = await cacheApi.listAffinities(keyword)
|
const response = await cacheApi.listAffinities(keyword)
|
||||||
affinityList.value = response.items
|
affinityList.value = response.items
|
||||||
matchedUserId.value = response.matched_user_id ?? null
|
matchedUserId.value = response.matched_user_id ?? null
|
||||||
|
currentTime.value = Math.floor(Date.now() / 1000)
|
||||||
|
pruneExpiredAffinities(currentTime.value, true)
|
||||||
|
|
||||||
if (keyword && response.total === 0) {
|
if (keyword && response.total === 0) {
|
||||||
showInfo('未找到匹配的缓存记录')
|
showInfo('未找到匹配的缓存记录')
|
||||||
@@ -238,6 +241,38 @@ function handlePageChange() {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recalculateNextExpireAt(now: number = currentTime.value) {
|
||||||
|
let nearestExpireAt: number | null = null
|
||||||
|
|
||||||
|
for (const item of affinityList.value) {
|
||||||
|
if (!item.expire_at || item.expire_at <= now) continue
|
||||||
|
if (nearestExpireAt === null || item.expire_at < nearestExpireAt) {
|
||||||
|
nearestExpireAt = item.expire_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextExpireAt.value = nearestExpireAt
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneExpiredAffinities(now: number, silent = false) {
|
||||||
|
const beforeCount = affinityList.value.length
|
||||||
|
const activeItems = affinityList.value.filter(
|
||||||
|
item => item.expire_at && item.expire_at > now
|
||||||
|
)
|
||||||
|
|
||||||
|
if (activeItems.length === beforeCount) {
|
||||||
|
recalculateNextExpireAt(now)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
affinityList.value = activeItems
|
||||||
|
recalculateNextExpireAt(now)
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
showInfo(`${beforeCount - activeItems.length} 个缓存已自动过期移除`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 定时器管理 ====================
|
// ==================== 定时器管理 ====================
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
@@ -247,14 +282,8 @@ function startCountdown() {
|
|||||||
countdownTimer = setInterval(() => {
|
countdownTimer = setInterval(() => {
|
||||||
currentTime.value = Math.floor(Date.now() / 1000)
|
currentTime.value = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
const beforeCount = affinityList.value.length
|
if (nextExpireAt.value !== null && currentTime.value >= nextExpireAt.value) {
|
||||||
affinityList.value = affinityList.value.filter(
|
pruneExpiredAffinities(currentTime.value)
|
||||||
item => item.expire_at && item.expire_at > currentTime.value
|
|
||||||
)
|
|
||||||
|
|
||||||
if (beforeCount > affinityList.value.length) {
|
|
||||||
const removedCount = beforeCount - affinityList.value.length
|
|
||||||
showInfo(`${removedCount} 个缓存已自动过期移除`)
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@@ -273,6 +302,9 @@ function handleVisibilityChange() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentTime.value = Math.floor(Date.now() / 1000)
|
currentTime.value = Math.floor(Date.now() / 1000)
|
||||||
|
if (nextExpireAt.value !== null && currentTime.value >= nextExpireAt.value) {
|
||||||
|
pruneExpiredAffinities(currentTime.value)
|
||||||
|
}
|
||||||
startCountdown()
|
startCountdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user