perf(admin-dashboard): accelerate trend load with pre-aggregation and async user trend
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user