Просмотр исходного кода

feat(default): add model performance badges

Add a batched performance summary API for model square cards and show compact latency, throughput, and status metrics without increasing card size. Also fix OTP verification form submission.
CaIon 2 дней назад
Родитель
Сommit
e8cfb546fa

+ 23 - 0
controller/perf_metrics.go

@@ -9,6 +9,29 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
+func GetPerfMetricsSummary(c *gin.Context) {
+	hours := 24
+	if rawHours := c.Query("hours"); rawHours != "" {
+		if parsed, err := strconv.Atoi(rawHours); err == nil {
+			hours = parsed
+		}
+	}
+
+	result, err := perfmetrics.QuerySummaryAll(hours)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"data":    result,
+	})
+}
+
 func GetPerfMetrics(c *gin.Context) {
 	modelName := c.Query("model")
 	if modelName == "" {

+ 20 - 0
model/perf_metric.go

@@ -59,6 +59,26 @@ func GetPerfMetrics(modelName string, group string, startTs int64, endTs int64)
 	return metrics, err
 }
 
+type PerfMetricSummary struct {
+	ModelName      string `json:"model_name"`
+	RequestCount   int64  `json:"request_count"`
+	SuccessCount   int64  `json:"success_count"`
+	TotalLatencyMs int64  `json:"total_latency_ms"`
+	OutputTokens   int64  `json:"output_tokens"`
+	GenerationMs   int64  `json:"generation_ms"`
+}
+
+func GetPerfMetricsSummaryAll(startTs int64, endTs int64) ([]PerfMetricSummary, error) {
+	var summaries []PerfMetricSummary
+	err := DB.Model(&PerfMetric{}).
+		Select("model_name, SUM(request_count) as request_count, SUM(success_count) as success_count, SUM(total_latency_ms) as total_latency_ms, SUM(output_tokens) as output_tokens, SUM(generation_ms) as generation_ms").
+		Where("bucket_ts >= ? AND bucket_ts <= ?", startTs, endTs).
+		Group("model_name").
+		Having("SUM(request_count) > 0").
+		Find(&summaries).Error
+	return summaries, err
+}
+
 func DeletePerfMetricsBefore(cutoffTs int64) error {
 	if cutoffTs <= 0 {
 		return nil

+ 72 - 0
pkg/perf_metrics/metrics.go

@@ -3,6 +3,7 @@ package perfmetrics
 import (
 	"context"
 	"fmt"
+	"math"
 	"sort"
 	"sync"
 	"time"
@@ -121,6 +122,77 @@ func Query(params QueryParams) (QueryResult, error) {
 	return buildQueryResult(params.Model, merged), nil
 }
 
+func QuerySummaryAll(hours int) (SummaryAllResult, error) {
+	if hours <= 0 {
+		hours = 24
+	}
+	if hours > 24*30 {
+		hours = 24 * 30
+	}
+	endTs := time.Now().Unix()
+	startTs := endTs - int64(hours)*3600
+
+	rows, err := model.GetPerfMetricsSummaryAll(startTs, endTs)
+	if err != nil {
+		return SummaryAllResult{}, err
+	}
+
+	totals := map[string]counters{}
+	for _, row := range rows {
+		totals[row.ModelName] = counters{
+			requestCount:   row.RequestCount,
+			successCount:   row.SuccessCount,
+			totalLatencyMs: row.TotalLatencyMs,
+			outputTokens:   row.OutputTokens,
+			generationMs:   row.GenerationMs,
+		}
+	}
+
+	hotBuckets.Range(func(key, value any) bool {
+		k := key.(bucketKey)
+		if k.bucketTs < startTs || k.bucketTs > endTs {
+			return true
+		}
+		snap := value.(*atomicBucket).snapshot()
+		if snap.requestCount == 0 {
+			return true
+		}
+		cur := totals[k.model]
+		cur.requestCount += snap.requestCount
+		cur.successCount += snap.successCount
+		cur.totalLatencyMs += snap.totalLatencyMs
+		cur.outputTokens += snap.outputTokens
+		cur.generationMs += snap.generationMs
+		totals[k.model] = cur
+		return true
+	})
+
+	models := make([]ModelSummary, 0, len(totals))
+	for name, total := range totals {
+		if total.requestCount == 0 {
+			continue
+		}
+		avgLatency := total.totalLatencyMs / total.requestCount
+		successRate := float64(total.successCount) / float64(total.requestCount) * 100
+		avgTps := 0.0
+		if total.generationMs > 0 {
+			avgTps = float64(total.outputTokens) / (float64(total.generationMs) / 1000.0)
+		}
+		models = append(models, ModelSummary{
+			ModelName:    name,
+			AvgLatencyMs: avgLatency,
+			SuccessRate:  math.Round(successRate*100) / 100,
+			AvgTps:       math.Round(avgTps*100) / 100,
+			RequestCount: total.requestCount,
+		})
+	}
+	sort.Slice(models, func(i, j int) bool {
+		return models[i].ModelName < models[j].ModelName
+	})
+
+	return SummaryAllResult{Models: models}, nil
+}
+
 func bucketStart(ts int64) int64 {
 	bucketSeconds := perf_metrics_setting.GetBucketSeconds()
 	if bucketSeconds <= 0 {

+ 12 - 0
pkg/perf_metrics/types.go

@@ -47,6 +47,18 @@ type QueryResult struct {
 	Groups       []GroupResult `json:"groups"`
 }
 
+type ModelSummary struct {
+	ModelName    string  `json:"model_name"`
+	AvgLatencyMs int64   `json:"avg_latency_ms"`
+	SuccessRate  float64 `json:"success_rate"`
+	AvgTps       float64 `json:"avg_tps"`
+	RequestCount int64   `json:"request_count"`
+}
+
+type SummaryAllResult struct {
+	Models []ModelSummary `json:"models"`
+}
+
 type bucketKey struct {
 	model    string
 	group    string

+ 6 - 1
router/api-router.go

@@ -31,7 +31,12 @@ func SetApiRouter(router *gin.Engine) {
 		//apiRouter.GET("/midjourney", controller.GetMidjourney)
 		apiRouter.GET("/home_page_content", controller.GetHomePageContent)
 		apiRouter.GET("/pricing", middleware.TryUserAuth(), controller.GetPricing)
-		apiRouter.GET("/perf-metrics", middleware.TryUserAuth(), controller.GetPerfMetrics)
+		perfMetricsRoute := apiRouter.Group("/perf-metrics")
+		perfMetricsRoute.Use(middleware.TryUserAuth())
+		{
+			perfMetricsRoute.GET("/summary", controller.GetPerfMetricsSummary)
+			perfMetricsRoute.GET("", controller.GetPerfMetrics)
+		}
 		apiRouter.GET("/rankings", controller.GetRankings)
 		apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)

+ 1 - 1
web/default/src/features/auth/otp/components/otp-form.tsx

@@ -183,7 +183,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
           )}
         />
 
-        <Button className='mt-2 w-full' disabled={!isFormValid || isLoading}>
+        <Button type='submit' className='mt-2 w-full' disabled={!isFormValid || isLoading}>
           {isLoading ? <Loader2 className='h-4 w-4 animate-spin' /> : null}
           {t('Verify and Sign In')}
         </Button>

+ 23 - 0
web/default/src/features/pricing/api.ts

@@ -38,6 +38,29 @@ export type PerformanceMetricsData = {
   }
 }
 
+export type PerfModelSummary = {
+  model_name: string
+  avg_latency_ms: number
+  success_rate: number
+  avg_tps: number
+  request_count: number
+}
+
+export type PerfSummaryAllData = {
+  success: boolean
+  message?: string
+  data: {
+    models: PerfModelSummary[]
+  }
+}
+
+export async function getPerfMetricsSummary(
+  hours = 24
+): Promise<PerfSummaryAllData> {
+  const res = await api.get(`/api/perf-metrics/summary?hours=${hours}`)
+  return res.data
+}
+
 export async function getPerfMetrics(
   modelName: string,
   hours = 24

+ 21 - 0
web/default/src/features/pricing/components/model-card-grid.tsx

@@ -1,10 +1,13 @@
 import { useEffect, useMemo, useState } from 'react'
+import { useQuery } from '@tanstack/react-query'
 import { ChevronLeft, ChevronRight } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { Button } from '@/components/ui/button'
+import { getPerfMetricsSummary } from '../api'
 import { DEFAULT_PRICING_PAGE_SIZE, DEFAULT_TOKEN_UNIT } from '../constants'
 import type { PricingModel, TokenUnit } from '../types'
 import { ModelCard } from './model-card'
+import type { ModelPerfBadgeData } from './model-perf-badge'
 
 export interface ModelCardGridProps {
   models: PricingModel[]
@@ -22,6 +25,13 @@ export function ModelCardGrid(props: ModelCardGridProps) {
   const tokenUnit = props.tokenUnit ?? DEFAULT_TOKEN_UNIT
   const totalPages = Math.max(1, Math.ceil(props.models.length / pageSize))
 
+  const perfQuery = useQuery({
+    queryKey: ['perf-metrics-summary'],
+    queryFn: () => getPerfMetricsSummary(24),
+    staleTime: 60 * 1000,
+    retry: false,
+  })
+
   useEffect(() => {
     setPage(1)
   }, [props.models])
@@ -31,6 +41,16 @@ export function ModelCardGrid(props: ModelCardGridProps) {
     return props.models.slice(start, start + pageSize)
   }, [page, pageSize, props.models])
 
+  const perfMap = useMemo(() => {
+    const map = new Map<string, ModelPerfBadgeData>()
+    for (const model of perfQuery.data?.data?.models ?? []) {
+      if (model.request_count > 0) {
+        map.set(model.model_name, model)
+      }
+    }
+    return map
+  }, [perfQuery.data])
+
   if (props.models.length === 0) {
     return null
   }
@@ -46,6 +66,7 @@ export function ModelCardGrid(props: ModelCardGridProps) {
             priceRate={props.priceRate}
             usdExchangeRate={props.usdExchangeRate}
             showRechargePrice={props.showRechargePrice}
+            perf={perfMap.get(model.model_name || '')}
             onClick={() => props.onModelClick(model.model_name || '')}
           />
         ))}

+ 37 - 32
web/default/src/features/pricing/components/model-card.tsx

@@ -14,6 +14,8 @@ import { parseTags } from '../lib/filters'
 import { isTokenBasedModel } from '../lib/model-helpers'
 import { formatPrice, formatRequestPrice } from '../lib/price'
 import type { PricingModel, TokenUnit } from '../types'
+import { ModelPerfBadge } from './model-perf-badge'
+import type { ModelPerfBadgeData } from './model-perf-badge'
 
 export interface ModelCardProps {
   model: PricingModel
@@ -22,6 +24,7 @@ export interface ModelCardProps {
   usdExchangeRate?: number
   tokenUnit?: TokenUnit
   showRechargePrice?: boolean
+  perf?: ModelPerfBadgeData
 }
 
 export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
@@ -69,7 +72,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
   return (
     <div
       className={cn(
-        'group flex flex-col rounded-xl border p-3 transition-colors sm:p-5',
+        'group relative flex flex-col rounded-xl border p-3 transition-colors sm:p-5',
         'hover:bg-muted/20'
       )}
     >
@@ -206,41 +209,43 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
         {props.model.description || t('No description available.')}
       </p>
 
-      {/* Footer row 1: group + billing type */}
-      <div className='mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 sm:mt-4'>
-        {primaryGroup && (
+      {/* Footer: left metadata and right performance summary share row alignment */}
+      <div className='mt-2 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 gap-y-1 sm:mt-4'>
+        <div className='flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1'>
+          {primaryGroup && (
+            <span className='text-muted-foreground text-xs font-medium'>
+              {primaryGroup} {t('Groups')}
+            </span>
+          )}
           <span className='text-muted-foreground text-xs font-medium'>
-            {primaryGroup} {t('Groups')}
+            {isTokenBased ? t('Token-based') : t('Per Request')}
           </span>
-        )}
-        <span className='text-muted-foreground text-xs font-medium'>
-          {isTokenBased ? t('Token-based') : t('Per Request')}
-        </span>
-        {isDynamicPricing && (
-          <StatusBadge
-            label={t('Dynamic Pricing')}
-            variant='warning'
-            copyable={false}
-            size='sm'
-          />
-        )}
-      </div>
+          {isDynamicPricing && (
+            <StatusBadge
+              label={t('Dynamic Pricing')}
+              variant='warning'
+              copyable={false}
+              size='sm'
+            />
+          )}
+        </div>
+        <ModelPerfBadge perf={props.perf} className='row-span-2 self-start' />
 
-      {/* Footer row 2: endpoint + tag chips */}
-      <div className='mt-1.5 flex flex-wrap items-center gap-x-2.5 gap-y-0.5 sm:mt-2 sm:gap-x-3 sm:gap-y-1'>
-        {bottomTags.map((item) => (
-          <span key={item} className='text-muted-foreground/70 text-xs'>
-            {item}
+        <div className='flex min-w-0 flex-wrap items-center gap-x-2.5 gap-y-0.5 sm:gap-x-3 sm:gap-y-1'>
+          {bottomTags.map((item) => (
+            <span key={item} className='text-muted-foreground/70 text-xs'>
+              {item}
+            </span>
+          ))}
+          <span className='text-muted-foreground/50 text-xs'>
+            {tokenUnitLabel}
           </span>
-        ))}
-        <span className='text-muted-foreground/50 text-xs'>
-          {tokenUnitLabel}
-        </span>
-        {hiddenCount > 0 && (
-          <span className='text-muted-foreground/40 text-xs'>
-            +{hiddenCount}
-          </span>
-        )}
+          {hiddenCount > 0 && (
+            <span className='text-muted-foreground/40 text-xs'>
+              +{hiddenCount}
+            </span>
+          )}
+        </div>
       </div>
     </div>
   )

+ 77 - 0
web/default/src/features/pricing/components/model-perf-badge.tsx

@@ -0,0 +1,77 @@
+import { memo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { formatLatency, formatThroughput } from '../lib/mock-stats'
+
+export type ModelPerfBadgeData = {
+  avg_latency_ms: number
+  success_rate: number
+  avg_tps: number
+}
+
+export interface ModelPerfBadgeProps
+  extends React.HTMLAttributes<HTMLDivElement> {
+  perf: ModelPerfBadgeData | undefined
+}
+
+function formatCompactThroughput(tps: number): string {
+  return formatThroughput(tps).replace(' t/s', 'tps')
+}
+
+export const ModelPerfBadge = memo(function ModelPerfBadge(
+  props: ModelPerfBadgeProps
+) {
+  const { t } = useTranslation()
+
+  if (!props.perf) {
+    return null
+  }
+
+  const { avg_latency_ms, avg_tps, success_rate } = props.perf
+
+  let statusColor = 'bg-emerald-500'
+  if (success_rate < 99) {
+    statusColor = 'bg-red-500'
+  } else if (success_rate < 99.9) {
+    statusColor = 'bg-amber-500'
+  }
+
+  return (
+    <div
+      className={cn(
+        'hidden w-[132px] grid-cols-[38px_48px_30px] gap-x-2 text-right tabular-nums min-[460px]:grid',
+        props.className
+      )}
+    >
+      <div title={t('Average latency')} className='min-w-0'>
+        <div className='text-muted-foreground/55 text-[10px] leading-4'>
+          {t('Latency short')}
+        </div>
+        <div className='text-muted-foreground/80 whitespace-nowrap font-mono text-xs leading-4'>
+          {avg_latency_ms > 0 ? formatLatency(avg_latency_ms) : '—'}
+        </div>
+      </div>
+      <div title={t('Throughput')} className='min-w-0'>
+        <div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
+          {t('Throughput short')}
+        </div>
+        <div className='text-muted-foreground/80 whitespace-nowrap font-mono text-xs leading-4'>
+          {formatCompactThroughput(avg_tps)}
+        </div>
+      </div>
+      <div
+        title={`${t('Success rate')}: ${success_rate.toFixed(1)}%`}
+        className='min-w-0'
+      >
+        <div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
+          {t('Status short')}
+        </div>
+        <div className='flex h-4 items-center justify-end gap-0.5'>
+          <span className='bg-muted-foreground/10 h-2 w-1 rounded-full' />
+          <span className='bg-muted-foreground/15 h-2.5 w-1 rounded-full' />
+          <span className={cn('h-3 w-1 rounded-full', statusColor)} />
+        </div>
+      </div>
+    </div>
+  )
+})

+ 4 - 0
web/default/src/i18n/locales/en.json

@@ -446,6 +446,8 @@
     "Available Models": "Available Models",
     "Available Rewards": "Available Rewards",
     "Average latency": "Average latency",
+    "Latency": "Latency",
+    "Latency short": "Lat.",
     "Average latency, TTFT, and success rate by group": "Average latency, TTFT, and success rate by group",
     "Average RPM": "Average RPM",
     "Average time-to-first-token (TTFT) by group": "Average time-to-first-token (TTFT) by group",
@@ -3603,6 +3605,7 @@
     "Statistical tokens": "Statistical tokens",
     "Statistics reset": "Statistics reset",
     "Status": "Status",
+    "Status short": "Status",
     "Status & Sync": "Status & Sync",
     "Status Code": "Status Code",
     "Status Code Mapping": "Status Code Mapping",
@@ -3834,6 +3837,7 @@
     "This year": "This year",
     "Three steps to get started": "Three steps to get started",
     "Throughput": "Throughput",
+    "Throughput short": "TPS",
     "Throughput by group": "Throughput by group",
     "Throughput trend": "Throughput trend",
     "Tier": "Tier",

+ 4 - 0
web/default/src/i18n/locales/fr.json

@@ -446,6 +446,8 @@
     "Available Models": "Modèles disponibles",
     "Available Rewards": "Récompenses disponibles",
     "Average latency": "Latence moyenne",
+    "Latency": "Latence",
+    "Latency short": "Lat.",
     "Average latency, TTFT, and success rate by group": "Latence moyenne, TTFT et taux de réussite par groupe",
     "Average RPM": "RPM moyen",
     "Average time-to-first-token (TTFT) by group": "Temps moyen jusqu’au premier token (TTFT) par groupe",
@@ -3603,6 +3605,7 @@
     "Statistical tokens": "Jetons statistiques",
     "Statistics reset": "Statistiques réinitialisées",
     "Status": "Statut",
+    "Status short": "État",
     "Status & Sync": "Statut et synchronisation",
     "Status Code": "Code de statut",
     "Status Code Mapping": "Mappage des codes d'état",
@@ -3834,6 +3837,7 @@
     "This year": "Cette année",
     "Three steps to get started": "Trois étapes pour commencer",
     "Throughput": "Débit",
+    "Throughput short": "TPS",
     "Throughput by group": "Débit par groupe",
     "Throughput trend": "Tendance du débit",
     "Tier": "Palier",

+ 4 - 0
web/default/src/i18n/locales/ja.json

@@ -446,6 +446,8 @@
     "Available Models": "利用可能なモデル",
     "Available Rewards": "利用可能な報酬",
     "Average latency": "平均レイテンシ",
+    "Latency": "レイテンシ",
+    "Latency short": "遅延",
     "Average latency, TTFT, and success rate by group": "グループ別の平均レイテンシ、TTFT、成功率",
     "Average RPM": "平均RPM",
     "Average time-to-first-token (TTFT) by group": "グループ別の平均 Time to First Token(TTFT)",
@@ -3603,6 +3605,7 @@
     "Statistical tokens": "統計トークン",
     "Statistics reset": "統計をリセットしました",
     "Status": "ステータス",
+    "Status short": "状態",
     "Status & Sync": "ステータスと同期",
     "Status Code": "ステータスコード",
     "Status Code Mapping": "ステータスコードマッピング",
@@ -3834,6 +3837,7 @@
     "This year": "今年",
     "Three steps to get started": "3ステップで始める",
     "Throughput": "スループット",
+    "Throughput short": "TPS",
     "Throughput by group": "グループ別スループット",
     "Throughput trend": "スループット推移",
     "Tier": "ティア",

+ 4 - 0
web/default/src/i18n/locales/ru.json

@@ -446,6 +446,8 @@
     "Available Models": "Доступные модели",
     "Available Rewards": "Доступные награды",
     "Average latency": "Средняя задержка",
+    "Latency": "Задержка",
+    "Latency short": "Зад.",
     "Average latency, TTFT, and success rate by group": "Средняя задержка, TTFT и доля успешных запросов по группам",
     "Average RPM": "Среднее число оборотов в минуту",
     "Average time-to-first-token (TTFT) by group": "Среднее время до первого токена (TTFT) по группам",
@@ -3603,6 +3605,7 @@
     "Statistical tokens": "Статистические токены",
     "Statistics reset": "Статистика сброшена",
     "Status": "Статус",
+    "Status short": "Стат.",
     "Status & Sync": "Статус и синхронизация",
     "Status Code": "Код статуса",
     "Status Code Mapping": "Сопоставление кодов состояния",
@@ -3834,6 +3837,7 @@
     "This year": "Этот год",
     "Three steps to get started": "Три шага для начала работы",
     "Throughput": "Пропускная способность",
+    "Throughput short": "TPS",
     "Throughput by group": "Пропускная способность по группам",
     "Throughput trend": "Тренд пропускной способности",
     "Tier": "Уровень",

+ 4 - 0
web/default/src/i18n/locales/vi.json

@@ -446,6 +446,8 @@
     "Available Models": "Mô hình khả dụng",
     "Available Rewards": "Phần thưởng hiện có",
     "Average latency": "Độ trễ trung bình",
+    "Latency": "Độ trễ",
+    "Latency short": "Trễ",
     "Average latency, TTFT, and success rate by group": "Độ trễ trung bình, TTFT và tỷ lệ thành công theo nhóm",
     "Average RPM": "RPM trung bình",
     "Average time-to-first-token (TTFT) by group": "Thời gian trung bình tới token đầu tiên (TTFT) theo nhóm",
@@ -3603,6 +3605,7 @@
     "Statistical tokens": "Mã thông báo thống kê",
     "Statistics reset": "Đã đặt lại thống kê",
     "Status": "Trạng thái",
+    "Status short": "TT",
     "Status & Sync": "Trạng thái & Đồng bộ",
     "Status Code": "Mã trạng thái",
     "Status Code Mapping": "Ánh xạ mã trạng thái",
@@ -3834,6 +3837,7 @@
     "This year": "Năm nay",
     "Three steps to get started": "Ba bước để bắt đầu",
     "Throughput": "Thông lượng",
+    "Throughput short": "TPS",
     "Throughput by group": "Thông lượng theo nhóm",
     "Throughput trend": "Xu hướng thông lượng",
     "Tier": "Bậc",

+ 4 - 0
web/default/src/i18n/locales/zh.json

@@ -446,6 +446,8 @@
     "Available Models": "可用模型",
     "Available Rewards": "可用奖励",
     "Average latency": "平均延迟",
+    "Latency": "延迟",
+    "Latency short": "延迟",
     "Average latency, TTFT, and success rate by group": "各分组的平均延迟、首 Token 延迟和成功率",
     "Average RPM": "平均 RPM",
     "Average time-to-first-token (TTFT) by group": "各分组的平均首 Token 延迟(TTFT)",
@@ -3603,6 +3605,7 @@
     "Statistical tokens": "统计 Token 数",
     "Statistics reset": "统计已重置",
     "Status": "状态",
+    "Status short": "状态",
     "Status & Sync": "状态与同步",
     "Status Code": "状态码",
     "Status Code Mapping": "状态码映射",
@@ -3834,6 +3837,7 @@
     "This year": "本年",
     "Three steps to get started": "三步快速上手",
     "Throughput": "吞吐量",
+    "Throughput short": "吞吐",
     "Throughput by group": "各分组吞吐量",
     "Throughput trend": "吞吐量趋势",
     "Tier": "档位",