Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7be8f4dc
Commit
7be8f4dc
authored
Mar 03, 2026
by
xvhuan
Browse files
perf(admin-dashboard): accelerate trend load with pre-aggregation and async user trend
parent
b7df7ce5
Changes
2
Hide whitespace changes
Inline
Side-by-side
backend/internal/repository/usage_log_repo.go
View file @
7be8f4dc
...
...
@@ -1655,6 +1655,13 @@ func (r *usageLogRepository) GetBatchAPIKeyUsageStats(ctx context.Context, apiKe
// GetUsageTrendWithFilters returns usage trend data with optional filters
func
(
r
*
usageLogRepository
)
GetUsageTrendWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
model
string
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
)
(
results
[]
TrendDataPoint
,
err
error
)
{
if
shouldUsePreaggregatedTrend
(
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
requestType
,
stream
,
billingType
)
{
aggregated
,
aggregatedErr
:=
r
.
getUsageTrendFromAggregates
(
ctx
,
startTime
,
endTime
,
granularity
)
if
aggregatedErr
==
nil
&&
len
(
aggregated
)
>
0
{
return
aggregated
,
nil
}
}
dateFormat
:=
safeDateFormat
(
granularity
)
query
:=
fmt
.
Sprintf
(
`
...
...
@@ -1719,6 +1726,78 @@ func (r *usageLogRepository) GetUsageTrendWithFilters(ctx context.Context, start
return
results
,
nil
}
func
shouldUsePreaggregatedTrend
(
granularity
string
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
model
string
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
)
bool
{
if
granularity
!=
"day"
&&
granularity
!=
"hour"
{
return
false
}
return
userID
==
0
&&
apiKeyID
==
0
&&
accountID
==
0
&&
groupID
==
0
&&
model
==
""
&&
requestType
==
nil
&&
stream
==
nil
&&
billingType
==
nil
}
func
(
r
*
usageLogRepository
)
getUsageTrendFromAggregates
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
)
(
results
[]
TrendDataPoint
,
err
error
)
{
dateFormat
:=
safeDateFormat
(
granularity
)
query
:=
""
args
:=
[]
any
{
startTime
,
endTime
}
switch
granularity
{
case
"hour"
:
query
=
fmt
.
Sprintf
(
`
SELECT
TO_CHAR(bucket_start, '%s') as date,
total_requests as requests,
input_tokens,
output_tokens,
(cache_creation_tokens + cache_read_tokens) as cache_tokens,
(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) as total_tokens,
total_cost as cost,
actual_cost
FROM usage_dashboard_hourly
WHERE bucket_start >= $1 AND bucket_start < $2
ORDER BY bucket_start ASC
`
,
dateFormat
)
case
"day"
:
query
=
fmt
.
Sprintf
(
`
SELECT
TO_CHAR(bucket_date::timestamp, '%s') as date,
total_requests as requests,
input_tokens,
output_tokens,
(cache_creation_tokens + cache_read_tokens) as cache_tokens,
(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) as total_tokens,
total_cost as cost,
actual_cost
FROM usage_dashboard_daily
WHERE bucket_date >= $1::date AND bucket_date < $2::date
ORDER BY bucket_date ASC
`
,
dateFormat
)
default
:
return
nil
,
nil
}
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
query
,
args
...
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
if
closeErr
:=
rows
.
Close
();
closeErr
!=
nil
&&
err
==
nil
{
err
=
closeErr
results
=
nil
}
}()
results
,
err
=
scanTrendRows
(
rows
)
if
err
!=
nil
{
return
nil
,
err
}
return
results
,
nil
}
// GetModelStatsWithFilters returns model statistics with optional filters
func
(
r
*
usageLogRepository
)
GetModelStatsWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
)
(
results
[]
ModelStat
,
err
error
)
{
actualCostExpr
:=
"COALESCE(SUM(actual_cost), 0) as actual_cost"
...
...
frontend/src/views/admin/DashboardView.vue
View file @
7be8f4dc
...
...
@@ -246,7 +246,10 @@
{{
t
(
'
admin.dashboard.recentUsage
'
)
}}
(Top 12)
</h3>
<div
class=
"h-64"
>
<Line
v-if=
"userTrendChartData"
:data=
"userTrendChartData"
:options=
"lineOptions"
/>
<div
v-if=
"userTrendLoading"
class=
"flex h-full items-center justify-center"
>
<LoadingSpinner
size=
"md"
/>
</div>
<Line
v-else-if=
"userTrendChartData"
:data=
"userTrendChartData"
:options=
"lineOptions"
/>
<div
v-else
class=
"flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400"
...
...
@@ -306,11 +309,13 @@ const appStore = useAppStore()
const
stats
=
ref
<
DashboardStats
|
null
>
(
null
)
const
loading
=
ref
(
false
)
const
chartsLoading
=
ref
(
false
)
const
userTrendLoading
=
ref
(
false
)
// Chart data
const
trendData
=
ref
<
TrendDataPoint
[]
>
([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
let
chartLoadSeq
=
0
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
...
...
@@ -531,7 +536,9 @@ const loadDashboardStats = async () => {
}
const
loadChartData
=
async
()
=>
{
const
currentSeq
=
++
chartLoadSeq
chartsLoading
.
value
=
true
userTrendLoading
.
value
=
true
try
{
const
params
=
{
start_date
:
startDate
.
value
,
...
...
@@ -539,20 +546,39 @@ const loadChartData = async () => {
granularity
:
granularity
.
value
}
const
[
trendResponse
,
modelResponse
,
userResponse
]
=
await
Promise
.
all
([
const
[
trendResponse
,
modelResponse
]
=
await
Promise
.
all
([
adminAPI
.
dashboard
.
getUsageTrend
(
params
),
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
}),
adminAPI
.
dashboard
.
getUserUsageTrend
({
...
params
,
limit
:
12
})
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
])
if
(
currentSeq
!==
chartLoadSeq
)
return
trendData
.
value
=
trendResponse
.
trend
||
[]
modelStats
.
value
=
modelResponse
.
models
||
[]
userTrend
.
value
=
userResponse
.
trend
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading chart data:
'
,
error
)
}
finally
{
if
(
currentSeq
!==
chartLoadSeq
)
return
chartsLoading
.
value
=
false
}
try
{
const
params
=
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
granularity
:
granularity
.
value
,
limit
:
12
}
const
userResponse
=
await
adminAPI
.
dashboard
.
getUserUsageTrend
(
params
)
if
(
currentSeq
!==
chartLoadSeq
)
return
userTrend
.
value
=
userResponse
.
trend
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading user trend:
'
,
error
)
}
finally
{
if
(
currentSeq
!==
chartLoadSeq
)
return
userTrendLoading
.
value
=
false
}
}
onMounted
(()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment