From 5d754e2c546b849844e2057a3b05e2ddece08544 Mon Sep 17 00:00:00 2001 From: Stinson Zhao Date: Thu, 21 May 2026 18:33:00 +0800 Subject: [PATCH] feat: add RU V2 fields to slow query and statement for clinic cloud Issue: pingcap-inc/clinic-ui#651 Surface the RU V2 metrics that clinic backend (PR #1415) added to slow query and statement responses. Visibility is gated by a new ISlowQueryConfig.showRuV2 / IStatementConfig.showRuV2 flag enabled only in tidb-dashboard-for-clinic-cloud, so standalone TiDB dashboard and clinic-op are unaffected. Slow query - New "ru_v2" / "ru_v2_detail" optional columns in the column selector (selectability still gated by available_fields per tier). - Premium: the ru_v2 value is added to the existing Basic info tab. - Starter / Essential: a dedicated "RU V2" tab is inserted between Transaction and Warnings, with three sections: - RU V2: single numeric value - RU V2 Detail: full text, wrap-friendly via
    - RU V2 Metrics: name/value table (no description column) over
      the RequestUnitV2Metrics struct (22 sub-fields).
  Per-tier routing is driven by available_fields schema, not data.

Statement
- New avg_ru_v2 / sum_ru_v2 columns and avg_ru_v2 / sum_ru_v2 /
  max_ru_v2 / sum_ru rows in the Basic info detail tab.

Types
- src/client/clinic-extensions.d.ts adds the new fields via module
  augmentation on @lib/client. This keeps the generated models.ts
  untouched, so a future `make generate_swagger_spec` run will not
  overwrite our additions. Dashboard Go models are intentionally
  unchanged.
---
 .../src/apps/SlowQuery/context.ts             |   1 +
 .../src/apps/Statement/context.ts             |   1 +
 .../src/apps/SlowQuery/context/index.ts       |   4 +
 .../SlowQuery/pages/Detail/DetailTabBasic.tsx |   6 +-
 .../SlowQuery/pages/Detail/DetailTabRuV2.tsx  | 157 ++++++++++++++++++
 .../SlowQuery/pages/Detail/DetailTabs.tsx     |  25 ++-
 .../src/apps/SlowQuery/translations/en.yaml   |  29 ++++
 .../src/apps/SlowQuery/translations/zh.yaml   |  29 ++++
 .../src/apps/SlowQuery/utils/tableColumns.tsx |   2 +
 .../src/apps/Statement/context/index.ts       |   4 +
 .../pages/Detail/PlanDetailTabBasic.tsx       |  25 ++-
 .../Statement/pages/Detail/PlanDetailTabs.tsx |   2 +-
 .../src/apps/Statement/translations/en.yaml   |   6 +
 .../src/apps/Statement/translations/zh.yaml   |   6 +
 .../src/apps/Statement/utils/tableColumns.tsx |   7 +
 .../src/client/clinic-extensions.d.ts         |  58 +++++++
 16 files changed, 357 insertions(+), 5 deletions(-)
 create mode 100644 ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabRuV2.tsx
 create mode 100644 ui/packages/tidb-dashboard-lib/src/client/clinic-extensions.d.ts

diff --git a/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/context.ts b/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/context.ts
index 555b2ab537..6f0f5e45a0 100644
--- a/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/context.ts
+++ b/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/SlowQuery/context.ts
@@ -209,6 +209,7 @@ export const ctx: (cfg: Partial) => ISlowQueryContext = (
       showResourceGroupFilter: true,
       showDownloadSlowQueryDBFile: true,
       showInternalFilter: true,
+      showRuV2: true,
       ...cfg
     }
   }
diff --git a/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/context.ts b/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/context.ts
index 014d9b16ae..85bfee725e 100644
--- a/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/context.ts
+++ b/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/Statement/context.ts
@@ -184,6 +184,7 @@ export const ctx: (cfg: Partial) => IStatementContext = (
     ds: new DataSource(),
     cfg: {
       apiPathBase: client.getBasePath(),
+      showRuV2: true,
       ...cfg
     }
   }
diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/context/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/context/index.ts
index 35d5862413..b8c260340c 100644
--- a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/context/index.ts
+++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/context/index.ts
@@ -98,6 +98,10 @@ export interface ISlowQueryConfig extends IContextConfig {
 
   // show internal slow queries
   showInternalFilter?: boolean
+
+  // show RU V2 fields (clinic-only: ru_v2, ru_v2_detail columns,
+  // Basic-tab rows, and the dedicated RU V2 Metrics tab)
+  showRuV2?: boolean
 }
 
 export interface ISlowQueryContext {
diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabBasic.tsx b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabBasic.tsx
index c2848c676f..37d31c2392 100644
--- a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabBasic.tsx
+++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabBasic.tsx
@@ -3,7 +3,10 @@ import { SlowqueryModel } from '@lib/client'
 import { DateTime } from '@lib/components'
 import { getValueFormat } from '@baurine/grafana-value-formats'
 
-export const tabBasicItems = (data: SlowqueryModel) => [
+export const tabBasicItems = (
+  data: SlowqueryModel,
+  options?: { showRuV2?: boolean }
+) => [
   {
     key: 'timestamp',
     value: 
@@ -35,5 +38,6 @@ export const tabBasicItems = (data: SlowqueryModel) => [
   { key: 'user', value: data.user },
   { key: 'host', value: data.host },
   { key: 'ru', value: data.ru },
+  ...(options?.showRuV2 ? [{ key: 'ru_v2', value: data.ru_v2 }] : []),
   { key: 'resource_group', value: data.resource_group }
 ]
diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabRuV2.tsx b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabRuV2.tsx
new file mode 100644
index 0000000000..d1141f157d
--- /dev/null
+++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabRuV2.tsx
@@ -0,0 +1,157 @@
+import React, { ReactNode } from 'react'
+import { useTranslation } from 'react-i18next'
+import { getValueFormat } from '@baurine/grafana-value-formats'
+
+import { RequestUnitV2Metrics, SlowqueryModel } from '@lib/client'
+import { CardTable, Pre } from '@lib/components'
+import { valueColumns } from '@lib/utils/tableColumns'
+
+const num = (v: number | undefined, unit: 'short' | 'bytes' = 'short') =>
+  v == null ? '' : getValueFormat(unit)(v, 2)
+
+const mapToCompactJson = (map: Record | undefined): string => {
+  if (!map || Object.keys(map).length === 0) return ''
+  return JSON.stringify(map)
+}
+
+/**
+ * Metrics struct rows for the metrics table inside the RU V2 tab.
+ */
+function buildMetricsItems(data: SlowqueryModel) {
+  const m: RequestUnitV2Metrics = data.ru_v2_metrics ?? {}
+  return [
+    { key: 'ru_v2_metrics.total_ru', value: num(m.total_ru) },
+    { key: 'ru_v2_metrics.tidb_ru', value: num(m.tidb_ru) },
+    { key: 'ru_v2_metrics.tikv_ru', value: num(m.tikv_ru) },
+    { key: 'ru_v2_metrics.tiflash_ru', value: num(m.tiflash_ru) },
+    { key: 'ru_v2_metrics.txn_cnt', value: num(m.txn_cnt) },
+    { key: 'ru_v2_metrics.plan_cnt', value: num(m.plan_cnt) },
+    {
+      key: 'ru_v2_metrics.plan_derive_stats_paths',
+      value: num(m.plan_derive_stats_paths)
+    },
+    {
+      key: 'ru_v2_metrics.session_parser_total',
+      value: num(m.session_parser_total)
+    },
+    { key: 'ru_v2_metrics.executor_l1', value: num(m.executor_l1) },
+    { key: 'ru_v2_metrics.executor_l2', value: num(m.executor_l2) },
+    { key: 'ru_v2_metrics.executor_l3', value: num(m.executor_l3) },
+    {
+      key: 'ru_v2_metrics.executor_l5_insert_rows',
+      value: num(m.executor_l5_insert_rows)
+    },
+    {
+      key: 'ru_v2_metrics.result_chunk_cells',
+      value: num(m.result_chunk_cells)
+    },
+    {
+      key: 'ru_v2_metrics.resource_manager_read_cnt',
+      value: num(m.resource_manager_read_cnt)
+    },
+    {
+      key: 'ru_v2_metrics.resource_manager_write_cnt',
+      value: num(m.resource_manager_write_cnt)
+    },
+    {
+      key: 'ru_v2_metrics.tikv_coprocessor_executor_iterations',
+      value: num(m.tikv_coprocessor_executor_iterations)
+    },
+    {
+      key: 'ru_v2_metrics.tikv_coprocessor_response_bytes',
+      value: num(m.tikv_coprocessor_response_bytes, 'bytes')
+    },
+    {
+      key: 'ru_v2_metrics.tikv_coprocessor_executor_work_total',
+      value: mapToCompactJson(m.tikv_coprocessor_executor_work_total)
+    },
+    {
+      key: 'ru_v2_metrics.tikv_storage_processed_keys_get',
+      value: num(m.tikv_storage_processed_keys_get)
+    },
+    {
+      key: 'ru_v2_metrics.tikv_storage_processed_keys_batch_get',
+      value: num(m.tikv_storage_processed_keys_batch_get)
+    },
+    {
+      key: 'ru_v2_metrics.tikv_kv_engine_cache_miss',
+      value: num(m.tikv_kv_engine_cache_miss)
+    },
+    {
+      key: 'ru_v2_metrics.tikv_raftstore_store_write_trigger_wb_bytes',
+      value: num(m.tikv_raftstore_store_write_trigger_wb_bytes, 'bytes')
+    }
+  ]
+}
+
+function Section({ title, children }: { title: string; children: ReactNode }) {
+  return (
+    
+
+ {title} +
+ {children} +
+ ) +} + +export function RuV2TabContent({ + data, + schemaColumns +}: { + data: SlowqueryModel + schemaColumns: string[] +}) { + const { t } = useTranslation() + const schemaSet = new Set(schemaColumns) + const v2 = data.ru_v2 + const v2Detail = data.ru_v2_detail + + // Schema (available_fields) tells us which RU V2 fields the backend can + // populate for this cluster tier. Premium only declares `ru_v2`; Starter / + // Essential declare `ru_v2`, `ru_v2_detail`, `ru_v2_metrics`. Hide each + // section if its field is not in the schema, regardless of the data value. + const showV2 = schemaSet.has('ru_v2') + const showDetail = schemaSet.has('ru_v2_detail') + const showMetrics = schemaSet.has('ru_v2_metrics') + const metricsItems = showMetrics ? buildMetricsItems(data) : [] + + // Columns: Name + Value only (drop the Description column). + const nameValueColumns = valueColumns('slow_query.fields.').slice(0, 2) + + return ( +
+ {showV2 && ( +
+ {num(v2)} +
+ )} + + {showDetail && ( +
+
+            {v2Detail ?? ''}
+          
+
+ )} + + {showMetrics && ( +
+ +
+ )} +
+ ) +} diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabs.tsx b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabs.tsx index 1dc66fc4c3..5301742337 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabs.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/pages/Detail/DetailTabs.tsx @@ -10,11 +10,13 @@ import { tabBasicItems } from './DetailTabBasic' import { tabTimeItems } from './DetailTabTime' import { tabCoprItems } from './DetailTabCopr' import { tabTxnItems } from './DetailTabTxn' +import { RuV2TabContent } from './DetailTabRuV2' import { useSchemaColumns } from '../../utils/useSchemaColumns' import { SlowQueryContext } from '../../context' export default function DetailTabs({ data }: { data: SlowqueryModel }) { const ctx = useContext(SlowQueryContext) + const showRuV2 = !!ctx?.cfg.showRuV2 const { t } = useTranslation() const { schemaColumns } = useSchemaColumns( @@ -22,12 +24,22 @@ export default function DetailTabs({ data }: { data: SlowqueryModel }) { ) const tabs = useMemo(() => { + const schemaSet = new Set(schemaColumns) + // Premium tier only declares `ru_v2` in the schema; Starter / Essential + // also declare `ru_v2_detail` and `ru_v2_metrics`. Use the presence of the + // metrics struct as the tier signal: when absent it's Premium, so the + // ru_v2 row stays inside the Basic tab; when present, RU V2 / Detail / + // Metrics get their own dedicated tab. + const isEssentialLike = schemaSet.has('ru_v2_metrics') + const showRuV2InBasic = + showRuV2 && schemaSet.has('ru_v2') && !isEssentialLike + const showRuV2Tab = showRuV2 && isEssentialLike const tbs = [ { key: 'basic', title: t('slow_query.detail.tabs.basic'), content: () => { - const items = tabBasicItems(data) + const items = tabBasicItems(data, { showRuV2: showRuV2InBasic }) const columns = valueColumns('slow_query.fields.') return ( ( + + ) + }) + } if (data.warnings) { tbs.push({ key: 'warnings', @@ -112,6 +133,6 @@ export default function DetailTabs({ data }: { data: SlowqueryModel }) { }) } return tbs - }, [schemaColumns, data, t]) + }, [schemaColumns, data, t, showRuV2]) return } diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/en.yaml b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/en.yaml index d0f07031c3..5892a0aeb2 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/en.yaml +++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/en.yaml @@ -122,6 +122,34 @@ slow_query: ru: RU ru_tooltip: request units + ru_v2: RU V2 + ru_v2_tooltip: Total RU V2 consumed by this query + ru_v2_detail: RU V2 Detail + ru_v2_detail_tooltip: Per-component RU V2 breakdown (raw) + ru_v2_metrics_label: RU V2 Metrics + ru_v2_metrics: + total_ru: Total RU + tidb_ru: TiDB RU + tikv_ru: TiKV RU + tiflash_ru: TiFlash RU + txn_cnt: Transaction Count + plan_cnt: Plan Count + plan_derive_stats_paths: Plan Derive Stats Paths + session_parser_total: Session Parser Total + executor_l1: Executor L1 + executor_l2: Executor L2 + executor_l3: Executor L3 + executor_l5_insert_rows: Executor L5 Insert Rows + result_chunk_cells: Result Chunk Cells + resource_manager_read_cnt: Resource Manager Read Count + resource_manager_write_cnt: Resource Manager Write Count + tikv_coprocessor_executor_iterations: TiKV Coprocessor Executor Iterations + tikv_coprocessor_response_bytes: TiKV Coprocessor Response Bytes + tikv_coprocessor_executor_work_total: TiKV Coprocessor Executor Work Total + tikv_storage_processed_keys_get: TiKV Storage Processed Keys (Get) + tikv_storage_processed_keys_batch_get: TiKV Storage Processed Keys (Batch Get) + tikv_kv_engine_cache_miss: TiKV KV Engine Cache Miss + tikv_raftstore_store_write_trigger_wb_bytes: TiKV Raftstore Write Trigger WB Bytes resource_group: Resource Group resource_group_tooltip: The resource group that the query belongs to time_queued_by_rc: The total time queued by RC @@ -174,6 +202,7 @@ slow_query: time: Time copr: Coprocessor txn: Transaction + ru_v2: RU V2 warnings: Warnings toolbar: schemas: diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/zh.yaml b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/zh.yaml index 15eb3a912e..fc48c2541e 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/zh.yaml +++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/translations/zh.yaml @@ -124,6 +124,34 @@ slow_query: ru: RU ru_tooltip: 资源单位(RU) + ru_v2: RU V2 + ru_v2_tooltip: 本次查询消耗的 RU V2 总量 + ru_v2_detail: RU V2 明细 + ru_v2_detail_tooltip: 各组件 RU V2 拆分(原始数据) + ru_v2_metrics_label: RU V2 Metrics + ru_v2_metrics: + total_ru: Total RU + tidb_ru: TiDB RU + tikv_ru: TiKV RU + tiflash_ru: TiFlash RU + txn_cnt: 事务数 + plan_cnt: Plan 数 + plan_derive_stats_paths: Plan Derive Stats Paths + session_parser_total: Session Parser Total + executor_l1: Executor L1 + executor_l2: Executor L2 + executor_l3: Executor L3 + executor_l5_insert_rows: Executor L5 插入行数 + result_chunk_cells: Result Chunk Cells + resource_manager_read_cnt: Resource Manager 读次数 + resource_manager_write_cnt: Resource Manager 写次数 + tikv_coprocessor_executor_iterations: TiKV Coprocessor Executor 迭代数 + tikv_coprocessor_response_bytes: TiKV Coprocessor 响应字节 + tikv_coprocessor_executor_work_total: TiKV Coprocessor Executor 工作总量 + tikv_storage_processed_keys_get: TiKV Storage Processed Keys (Get) + tikv_storage_processed_keys_batch_get: TiKV Storage Processed Keys (Batch Get) + tikv_kv_engine_cache_miss: TiKV KV Engine Cache Miss + tikv_raftstore_store_write_trigger_wb_bytes: TiKV Raftstore Write Trigger WB Bytes resource_group: 资源组 resource_group_tooltip: SQL 语句所属的资源组 time_queued_by_rc: RC 等待累积耗时 @@ -178,6 +206,7 @@ slow_query: time: 执行时间 copr: Coprocessor 读取 txn: 事务 + ru_v2: RU V2 warnings: 警告 toolbar: schemas: diff --git a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/tableColumns.tsx b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/tableColumns.tsx index 419bc47c88..2d92549ac4 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/tableColumns.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/SlowQuery/utils/tableColumns.tsx @@ -125,6 +125,8 @@ export function slowQueryColumns( }), // resource control tcf.bar.single('ru', 'none', rows), + tcf.bar.single('ru_v2', 'none', rows), + tcf.textWithTooltip('ru_v2_detail', rows), tcf.textWithTooltip('resource_group', rows), tcf.bar.single('time_queued_by_rc', 's', rows), diff --git a/ui/packages/tidb-dashboard-lib/src/apps/Statement/context/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/Statement/context/index.ts index df762f4a82..df286120df 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/Statement/context/index.ts +++ b/ui/packages/tidb-dashboard-lib/src/apps/Statement/context/index.ts @@ -115,6 +115,10 @@ export interface IStatementConfig extends IContextConfig { customAbsoluteRangePicker: boolean timeRangeLimit?: number } + + // show RU V2 fields (clinic-only: avg_ru_v2 / sum_ru_v2 columns and the + // matching Basic-tab rows) + showRuV2?: boolean } export interface IStatementContext { diff --git a/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabBasic.tsx b/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabBasic.tsx index cf8c64fb91..5d594b7e32 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabBasic.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabBasic.tsx @@ -5,7 +5,10 @@ import { getValueFormat } from '@baurine/grafana-value-formats' import { StatementModel } from '@lib/client' import { DateTime, Pre, ValueWithTooltip, TextWrap } from '@lib/components' -export const tabBasicItems = (data: StatementModel) => [ +export const tabBasicItems = ( + data: StatementModel, + options?: { showRuV2?: boolean } +) => [ { key: 'table_names', value: ( @@ -82,6 +85,26 @@ export const tabBasicItems = (data: StatementModel) => [ key: 'max_ru', value: getValueFormat('short')(data.max_ru || 0, 1) }, + ...(options?.showRuV2 + ? [ + { + key: 'avg_ru_v2', + value: getValueFormat('short')(data.avg_ru_v2 || 0, 1) + }, + { + key: 'sum_ru_v2', + value: getValueFormat('short')(data.sum_ru_v2 || 0, 1) + }, + { + key: 'max_ru_v2', + value: getValueFormat('short')(data.max_ru_v2 || 0, 1) + }, + { + key: 'sum_ru', + value: getValueFormat('short')(data.sum_ru || 0, 1) + } + ] + : []), { key: 'resource_group', value: data.resource_group diff --git a/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabs.tsx b/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabs.tsx index 6bebb0a34e..b8af43b55a 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabs.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/Statement/pages/Detail/PlanDetailTabs.tsx @@ -34,7 +34,7 @@ export default function DetailTabs({ key: 'basic', title: t('statement.pages.detail.tabs.basic'), content: () => { - const items = tabBasicItems(data) + const items = tabBasicItems(data, { showRuV2: !!ctx?.cfg.showRuV2 }) const columns = valueColumns('statement.fields.') return ( + tikv_storage_processed_keys_get?: number + tikv_storage_processed_keys_batch_get?: number + tikv_kv_engine_cache_miss?: number + tikv_raftstore_store_write_trigger_wb_bytes?: number + } + + interface SlowqueryModel { + ru_v2?: number + ru_v2_detail?: string + ru_v2_metrics?: RequestUnitV2Metrics + } + + interface StatementModel { + avg_ru_v2?: number + sum_ru_v2?: number + max_ru_v2?: number + } +}