diff --git a/pkg/apiserver/topsql/service.go b/pkg/apiserver/topsql/service.go index 6636af309c..c1adf52df4 100644 --- a/pkg/apiserver/topsql/service.go +++ b/pkg/apiserver/topsql/service.go @@ -80,11 +80,13 @@ type GetSummaryRequest struct { Start string `json:"start"` End string `json:"end"` Top string `json:"top"` + GroupBy string `json:"group_by"` Window string `json:"window"` } type SummaryResponse struct { - Data []SummaryItem `json:"data"` + Data []SummaryItem `json:"data"` + DataBy []SummaryByItem `json:"data_by"` } type SummaryItem struct { @@ -99,6 +101,13 @@ type SummaryItem struct { Plans []SummaryPlanItem `json:"plans"` } +type SummaryByItem struct { + Text string `json:"text"` + TimestampSec []uint64 `json:"timestamp_sec"` + CPUTimeMs []uint64 `json:"cpu_time_ms,omitempty"` + CPUTimeMsSum uint64 `json:"cpu_time_ms_sum"` +} + type SummaryPlanItem struct { PlanDigest string `json:"plan_digest"` PlanText string `json:"plan_text"` diff --git a/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts b/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts index 3c8d4c15e5..05ac271454 100644 --- a/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts +++ b/ui/packages/tidb-dashboard-client/src/client/api/api/default-api.ts @@ -3710,6 +3710,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati * * @summary Get summaries * @param {string} [end] + * @param {string} [groupBy] * @param {string} [instance] * @param {string} [instanceType] * @param {string} [start] @@ -3718,7 +3719,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati * @param {*} [options] Override http request option. * @throws {RequiredError} */ - topsqlSummaryGet: async (end?: string, instance?: string, instanceType?: string, start?: string, top?: string, window?: string, options: AxiosRequestConfig = {}): Promise => { + topsqlSummaryGet: async (end?: string, groupBy?: string, instance?: string, instanceType?: string, start?: string, top?: string, window?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/topsql/summary`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -3738,6 +3739,10 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati localVarQueryParameter['end'] = end; } + if (groupBy !== undefined) { + localVarQueryParameter['group_by'] = groupBy; + } + if (instance !== undefined) { localVarQueryParameter['instance'] = instance; } @@ -5206,6 +5211,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { * * @summary Get summaries * @param {string} [end] + * @param {string} [groupBy] * @param {string} [instance] * @param {string} [instanceType] * @param {string} [start] @@ -5214,8 +5220,8 @@ export const DefaultApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async topsqlSummaryGet(end?: string, instance?: string, instanceType?: string, start?: string, top?: string, window?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlSummaryGet(end, instance, instanceType, start, top, window, options); + async topsqlSummaryGet(end?: string, groupBy?: string, instance?: string, instanceType?: string, start?: string, top?: string, window?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.topsqlSummaryGet(end, groupBy, instance, instanceType, start, top, window, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6277,6 +6283,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa * * @summary Get summaries * @param {string} [end] + * @param {string} [groupBy] * @param {string} [instance] * @param {string} [instanceType] * @param {string} [start] @@ -6285,8 +6292,8 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - topsqlSummaryGet(end?: string, instance?: string, instanceType?: string, start?: string, top?: string, window?: string, options?: any): AxiosPromise { - return localVarFp.topsqlSummaryGet(end, instance, instanceType, start, top, window, options).then((request) => request(axios, basePath)); + topsqlSummaryGet(end?: string, groupBy?: string, instance?: string, instanceType?: string, start?: string, top?: string, window?: string, options?: any): AxiosPromise { + return localVarFp.topsqlSummaryGet(end, groupBy, instance, instanceType, start, top, window, options).then((request) => request(axios, basePath)); }, /** * @@ -7497,6 +7504,13 @@ export interface DefaultApiTopsqlSummaryGetRequest { */ readonly end?: string + /** + * + * @type {string} + * @memberof DefaultApiTopsqlSummaryGet + */ + readonly groupBy?: string + /** * * @type {string} @@ -8730,7 +8744,7 @@ export class DefaultApi extends BaseAPI { * @memberof DefaultApi */ public topsqlSummaryGet(requestParameters: DefaultApiTopsqlSummaryGetRequest = {}, options?: AxiosRequestConfig) { - return DefaultApiFp(this.configuration).topsqlSummaryGet(requestParameters.end, requestParameters.instance, requestParameters.instanceType, requestParameters.start, requestParameters.top, requestParameters.window, options).then((request) => request(this.axios, this.basePath)); + return DefaultApiFp(this.configuration).topsqlSummaryGet(requestParameters.end, requestParameters.groupBy, requestParameters.instance, requestParameters.instanceType, requestParameters.start, requestParameters.top, requestParameters.window, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts index 38e5b0bf01..06c479647f 100644 --- a/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/index.ts @@ -86,6 +86,7 @@ export * from './topology-ti-proxy-info'; export * from './topsql-editable-config'; export * from './topsql-instance-item'; export * from './topsql-instance-response'; +export * from './topsql-summary-by-item'; export * from './topsql-summary-item'; export * from './topsql-summary-plan-item'; export * from './topsql-summary-response'; diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-by-item.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-by-item.ts new file mode 100644 index 0000000000..ddfacd058f --- /dev/null +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-by-item.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Dashboard API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface TopsqlSummaryByItem + */ +export interface TopsqlSummaryByItem { + /** + * + * @type {Array} + * @memberof TopsqlSummaryByItem + */ + 'cpu_time_ms'?: Array; + /** + * + * @type {number} + * @memberof TopsqlSummaryByItem + */ + 'cpu_time_ms_sum'?: number; + /** + * + * @type {string} + * @memberof TopsqlSummaryByItem + */ + 'text'?: string; + /** + * + * @type {Array} + * @memberof TopsqlSummaryByItem + */ + 'timestamp_sec'?: Array; +} + diff --git a/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-response.ts b/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-response.ts index 89be073f04..64edf430f1 100644 --- a/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-response.ts +++ b/ui/packages/tidb-dashboard-client/src/client/api/models/topsql-summary-response.ts @@ -13,6 +13,7 @@ */ +import { TopsqlSummaryByItem } from './topsql-summary-by-item'; import { TopsqlSummaryItem } from './topsql-summary-item'; /** @@ -27,5 +28,11 @@ export interface TopsqlSummaryResponse { * @memberof TopsqlSummaryResponse */ 'data'?: Array; + /** + * + * @type {Array} + * @memberof TopsqlSummaryResponse + */ + 'data_by'?: Array; } diff --git a/ui/packages/tidb-dashboard-client/swagger/spec.json b/ui/packages/tidb-dashboard-client/swagger/spec.json index 9d923b210c..cbcd146dfc 100644 --- a/ui/packages/tidb-dashboard-client/swagger/spec.json +++ b/ui/packages/tidb-dashboard-client/swagger/spec.json @@ -3508,6 +3508,11 @@ "name": "end", "in": "query" }, + { + "type": "string", + "name": "group_by", + "in": "query" + }, { "type": "string", "name": "instance", @@ -6019,6 +6024,29 @@ } } }, + "topsql.SummaryByItem": { + "type": "object", + "properties": { + "cpu_time_ms": { + "type": "array", + "items": { + "type": "integer" + } + }, + "cpu_time_ms_sum": { + "type": "integer" + }, + "text": { + "type": "string" + }, + "timestamp_sec": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, "topsql.SummaryItem": { "type": "object", "properties": { @@ -6097,6 +6125,12 @@ "items": { "$ref": "#/definitions/topsql.SummaryItem" } + }, + "data_by": { + "type": "array", + "items": { + "$ref": "#/definitions/topsql.SummaryByItem" + } } } }, diff --git a/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/context.ts b/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/context.ts index 436ad3bc19..3717662cbc 100644 --- a/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/context.ts +++ b/ui/packages/tidb-dashboard-for-clinic-cloud/src/apps/TopSQL/context.ts @@ -22,6 +22,7 @@ class DataSource implements ITopSQLDataSource { topsqlSummaryGet( end?: string, + groupBy?: string, instance?: string, instanceType?: string, start?: string, @@ -32,6 +33,7 @@ class DataSource implements ITopSQLDataSource { return client.getInstance().topsqlSummaryGet( { end, + groupBy, instance, instanceType, start, diff --git a/ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/context.ts b/ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/context.ts index c94dc3d77d..66e63af359 100644 --- a/ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/context.ts +++ b/ui/packages/tidb-dashboard-for-op/src/apps/TopSQL/context.ts @@ -21,6 +21,7 @@ class DataSource implements ITopSQLDataSource { topsqlSummaryGet( end?: string, + groupBy?: string, instance?: string, instanceType?: string, start?: string, @@ -31,6 +32,7 @@ class DataSource implements ITopSQLDataSource { return client.getInstance().topsqlSummaryGet( { end, + groupBy, instance, instanceType, start, @@ -49,6 +51,7 @@ export const ctx: ITopSQLContext = { cfg: { checkNgm: true, showSetting: true, - showLimit: true + showLimit: true, + showGroupBy: true } } diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/context/index.ts b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/context/index.ts index 28c02cdc5f..dc2f51ce8f 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/context/index.ts +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/context/index.ts @@ -26,6 +26,7 @@ export interface ITopSQLDataSource { topsqlSummaryGet( end?: string, + groupBy?: string, instance?: string, instanceType?: string, start?: string, @@ -53,6 +54,7 @@ export interface ITopSQLConfig { showSearchInStatements?: boolean showLimit?: boolean + showGroupBy?: boolean } export interface ITopSQLContext { diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx index 23bfea8036..f19d65caae 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/List.tsx @@ -27,7 +27,12 @@ import { useMount, useSessionStorage } from 'react-use' import { useMemoizedFn } from 'ahooks' import { sortBy } from 'lodash' import formatSql from '@lib/utils/sqlFormatter' -import { TopsqlInstanceItem, TopsqlSummaryItem } from '@lib/client' +import { + TopsqlInstanceItem, + TopsqlSummaryByItem, + TopsqlSummaryItem, + TopsqlSummaryResponse +} from '@lib/client' import { Card, toTimeRangeValue as _toTimeRangeValue, @@ -57,6 +62,13 @@ const CHART_BAR_WIDTH = 8 const RECENT_RANGE_OFFSET = -60 const LIMITS = [5, 20, 100] +export enum AggLevel { + Query = 'query', + Table = 'table', + Schema = 'db' +} +const GROUP = [AggLevel.Query, AggLevel.Table, AggLevel.Schema] + const toTimeRangeValue: typeof _toTimeRangeValue = (v) => { return _toTimeRangeValue(v, v?.type === 'recent' ? RECENT_RANGE_OFFSET : 0) } @@ -73,6 +85,7 @@ export function TopSQLList() { ) const { timeRange, setTimeRange } = useURLTimeRange() const [limit, setLimit] = useState(5) + const [groupBy, setGroupBy] = useState(AggLevel.Query) const [timeWindowSize, setTimeWindowSize] = useState(0) const containerRef = useRef(null) const computeTimeWindowSize = useMemoizedFn( @@ -89,7 +102,7 @@ export function TopSQLList() { topSQLData, isLoading: isDataLoading, updateTopSQLData - } = useTopSQLData(instance, timeRange, limit, computeTimeWindowSize) + } = useTopSQLData(instance, timeRange, limit, groupBy, computeTimeWindowSize) const isLoading = isConfigLoading || isDataLoading const { instances, @@ -195,6 +208,10 @@ export function TopSQLList() { if (inst) { telemetry.finishSelectInstance(inst?.instance_type!) } + // only group by sql when instance is not tikv + if (inst?.instance_type !== 'tikv') { + setGroupBy(AggLevel.Query) + } }} instances={instances} disabled={isLoading || isInstancesLoading} @@ -231,6 +248,20 @@ export function TopSQLList() { ))} )} + {ctx?.cfg.showGroupBy && instance?.instance_type === 'tikv' && ( + + )} )} @@ -368,11 +401,12 @@ const useTopSQLData = ( instance: TopsqlInstanceItem | null, timeRange: TimeRange, limit: number, + groupBy: string, computeTimeWindowSize: (ts: TimeRangeValue) => number ) => { const ctx = useContext(TopSQLContext) - const [topSQLData, setTopSQLData] = useState([]) + const [topSQLData, setTopSQLData] = useState([]) const [isLoading, setIsLoading] = useState(false) const updateTopSQLData = useMemoizedFn( async ( @@ -384,7 +418,7 @@ const useTopSQLData = ( return } - let data: TopsqlSummaryItem[] + let dataResp: TopsqlSummaryResponse const ts = toTimeRangeValue(_timeRange) const timeWindowSize = computeTimeWindowSize(ts) @@ -393,41 +427,61 @@ const useTopSQLData = ( setIsLoading(true) const resp = await ctx!.ds.topsqlSummaryGet( String(end), + _instance.instance_type === 'tidb' ? AggLevel.Query : groupBy, _instance.instance, _instance.instance_type, String(start), String(limit), `${timeWindowSize}s` ) - data = resp.data.data ?? [] + dataResp = resp.data } finally { setIsLoading(false) } - // Sort data by digest - // If this digest occurs continuously on the timeline, we can easily see the sequential overhead - data.sort((a, b) => a.sql_digest?.localeCompare(b.sql_digest!) || 0) + if (groupBy === AggLevel.Query || instance?.instance_type === 'tidb') { + // Sort data by digest + let data: TopsqlSummaryItem[] = dataResp.data ?? [] + // If this digest occurs continuously on the timeline, we can easily see the sequential overhead + data.sort((a, b) => a.sql_digest?.localeCompare(b.sql_digest!) || 0) + + data.forEach((d) => { + d.sql_text = formatSql(d.sql_text) + d.plans?.forEach((item) => { + // Filter empty cpu time data + item.timestamp_sec = item.timestamp_sec?.filter( + (_, index) => !!item.cpu_time_ms?.[index] + ) + item.cpu_time_ms = item.cpu_time_ms?.filter((c) => !!c) + + item.timestamp_sec = item.timestamp_sec?.map((t) => t * 1000) + }) + }) + setTopSQLData(data) + } - data.forEach((d) => { - d.sql_text = formatSql(d.sql_text) - d.plans?.forEach((item) => { + if (groupBy === AggLevel.Table || groupBy === AggLevel.Schema) { + let data: TopsqlSummaryByItem[] = dataResp.data_by ?? [] + // Sort data by table + // If this table occurs continuously on the timeline, we can easily see the sequential overhead + // data.sort((a, b) => a.table_?.localeCompare(b.table!) || 0) + data.forEach((d) => { // Filter empty cpu time data - item.timestamp_sec = item.timestamp_sec?.filter( - (_, index) => !!item.cpu_time_ms?.[index] + d.timestamp_sec = d.timestamp_sec?.filter( + (_, index) => !!d.cpu_time_ms?.[index] ) - item.cpu_time_ms = item.cpu_time_ms?.filter((c) => !!c) + d.cpu_time_ms = d.cpu_time_ms?.filter((c) => !!c) - item.timestamp_sec = item.timestamp_sec?.map((t) => t * 1000) + d.timestamp_sec = d.timestamp_sec?.map((t) => t * 1000) }) - }) - - setTopSQLData(data) + setTopSQLData(data) + } } ) useEffect(() => { updateTopSQLData(instance, timeRange, limit) - }, [instance, timeRange, limit, updateTopSQLData]) + }, [instance, timeRange, limit, groupBy, updateTopSQLData]) return { topSQLData, isLoading, updateTopSQLData } } diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListChart.tsx b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListChart.tsx index 2e5bec58cd..d0423e6f75 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListChart.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListChart.tsx @@ -10,33 +10,40 @@ import { import { orderBy, toPairs } from 'lodash' import React, { useMemo, useState, forwardRef } from 'react' import { getValueFormat } from '@baurine/grafana-value-formats' -import { TopsqlSummaryItem } from '@lib/client' +import { TopsqlSummaryItem, TopsqlSummaryByItem } from '@lib/client' import { useTranslation } from 'react-i18next' -import { isOthersDigest } from '../../utils/specialRecord' import { useChange } from '@lib/utils/useChange' import { DEFAULT_CHART_SETTINGS, timeTickFormatter } from '@lib/utils/charts' +import { AggLevel } from './List' export interface ListChartProps { - data: TopsqlSummaryItem[] + data: any[] timeWindowSize: number + groupBy: string timeRangeTimestamp: [number, number] onBrushEnd: BrushEndListener } +const isQueryAggLevel = (groupBy: string) => { + // default is query + return !(groupBy === AggLevel.Table || groupBy === AggLevel.Schema) +} + export const ListChart = forwardRef( - ({ onBrushEnd, data, timeWindowSize, timeRangeTimestamp }, ref) => { + ({ onBrushEnd, data, groupBy, timeWindowSize, timeRangeTimestamp }, ref) => { const { t } = useTranslation() // And we need update all the data at the same time and let the chart refresh only once for a better experience. const [bundle, setBundle] = useState({ data, + groupBy, timeWindowSize, timeRangeTimestamp }) - const { chartData } = useChartData(bundle.data) - const { digestMap } = useDigestMap(bundle.data) + const { chartData } = useChartData(bundle.data, bundle.groupBy) + const { digestMap } = useDigestMap(bundle.data, bundle.groupBy) useChange(() => { - setBundle({ data, timeWindowSize, timeRangeTimestamp }) + setBundle({ data, groupBy, timeWindowSize, timeRangeTimestamp }) }, [data]) return ( @@ -67,29 +74,34 @@ export const ListChart = forwardRef( tickFormat={(v) => getValueFormat('ms')(v, 1)} ticks={5} /> - {Object.keys(chartData).map((digest) => { - const sql = digestMap?.[digest] || '' + {Object.keys(chartData).map((originText) => { + const sql = digestMap?.[originText] || '' let text = sql - - if (isOthersDigest(digest)) { + if (!originText) { text = t('topsql.table.others') - // is unknown sql text + // is unknown text } else if (!sql) { - text = `(SQL ${digest.slice(0, 8)})` + if (isQueryAggLevel(bundle.groupBy)) { + // cannot find the sql text, but we agg by sql + text = `(SQL ${originText.slice(0, 8)})` + } else { + text = originText + } } else { + // text too long, show a part of it text = sql.length > 50 ? `${sql.slice(0, 50)}...` : sql } return ( ) @@ -114,8 +126,15 @@ export const ListChart = forwardRef( } ) -function useDigestMap(seriesData: TopsqlSummaryItem[]) { +function useDigestMap(seriesDataO: any[] = [], groupBy: string) { const digestMap = useMemo(() => { + if (!seriesDataO) { + return {} + } + if (!isQueryAggLevel(groupBy)) { + return {} + } + let seriesData = seriesDataO as TopsqlSummaryItem[] if (!seriesData) { return {} } @@ -123,63 +142,83 @@ function useDigestMap(seriesData: TopsqlSummaryItem[]) { prev[sql_digest!] = sql_text return prev }, {} as { [digest: string]: string | undefined }) - }, [seriesData]) + }, [seriesDataO, groupBy]) return { digestMap } } -function useChartData(seriesData: TopsqlSummaryItem[]) { - const chartData = useMemo(() => { - if (!seriesData) { - return {} - } - // Group by SQL digest + timestamp and sum their values - const valuesByDigestAndTs: Record> = {} - const sumValueByDigest: Record = {} - seriesData.forEach((series) => { - const seriesDigest = series.sql_digest! - - if (!valuesByDigestAndTs[seriesDigest]) { - valuesByDigestAndTs[seriesDigest] = {} +function useChartData(seriesDataO: any[], groupBy: string) { + let chartData: Record> = {} + chartData = useMemo(() => { + if (isQueryAggLevel(groupBy)) { + if (!seriesDataO) { + return {} } - const map = valuesByDigestAndTs[seriesDigest] - let sum = 0 - series.plans?.forEach((values) => { - values.timestamp_sec?.forEach((t, i) => { - if (!map[t]) { - map[t] = values.cpu_time_ms![i] - } else { - map[t] += values.cpu_time_ms![i] - } - sum += values.cpu_time_ms![i] + let seriesData = seriesDataO as TopsqlSummaryItem[] + // Group by SQL digest + timestamp and sum their values + const valuesByDigestAndTs: Record> = {} + const sumValueByDigest: Record = {} + seriesData.forEach((series) => { + const seriesDigest = series.sql_digest! + + if (!valuesByDigestAndTs[seriesDigest]) { + valuesByDigestAndTs[seriesDigest] = {} + } + const map = valuesByDigestAndTs[seriesDigest] + let sum = 0 + series.plans?.forEach((values) => { + values.timestamp_sec?.forEach((t, i) => { + if (!map[t]) { + map[t] = values.cpu_time_ms![i] + } else { + map[t] += values.cpu_time_ms![i] + } + sum += values.cpu_time_ms![i] + }) }) + + if (!sumValueByDigest[seriesDigest]) { + sumValueByDigest[seriesDigest] = 0 + } + sumValueByDigest[seriesDigest] += sum }) - if (!sumValueByDigest[seriesDigest]) { - sumValueByDigest[seriesDigest] = 0 - } - sumValueByDigest[seriesDigest] += sum - }) + // Order by digest + const orderedDigests = orderBy(toPairs(sumValueByDigest), ['1'], ['desc']) + .filter((v) => v[1] > 0) + .map((v) => v[0]) - // Order by digest - const orderedDigests = orderBy(toPairs(sumValueByDigest), ['1'], ['desc']) - .filter((v) => v[1] > 0) - .map((v) => v[0]) + const datumByDigest: Record> = {} + for (const digest of orderedDigests) { + const datum: Array<[number, number]> = [] - const datumByDigest: Record> = {} - for (const digest of orderedDigests) { - const datum: Array<[number, number]> = [] + const valuesByTs = valuesByDigestAndTs[digest] + for (const ts in valuesByTs) { + const value = valuesByTs[ts] + datum.push([Number(ts), value]) + } - const valuesByTs = valuesByDigestAndTs[digest] - for (const ts in valuesByTs) { - const value = valuesByTs[ts] - datum.push([Number(ts), value]) + datumByDigest[digest] = datum } - datumByDigest[digest] = datum + return datumByDigest + } else { + if (!seriesDataO) { + return {} + } + let seriesData = seriesDataO as TopsqlSummaryByItem[] + const datumBy: Record> = {} + seriesData.forEach((series) => { + const key = series.text! + if (!datumBy[key]) { + datumBy[key] = [] + } + series.cpu_time_ms?.forEach((v, i) => { + datumBy[key].push([series.timestamp_sec![i], v]) + }) + }) + return datumBy } - - return datumByDigest - }, [seriesData]) + }, [seriesDataO, groupBy]) return { chartData diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListTable.tsx b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListTable.tsx index be76c8a6ab..4422ccd0e0 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListTable.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/pages/List/ListTable.tsx @@ -9,7 +9,7 @@ import { import { QuestionCircleOutlined } from '@ant-design/icons' import { CSVLink } from 'react-csv' -import { TopsqlSummaryItem } from '@lib/client' +import { TopsqlSummaryItem, TopsqlSummaryByItem } from '@lib/client' import { Card, CardTable, @@ -23,16 +23,22 @@ import { import { useRecordSelection } from '../../utils/useRecordSelection' import { ListDetail } from './ListDetail' -import { isOthersRecord, isUnknownSQLRecord } from '../../utils/specialRecord' +import { + isOthersRecord, + isSummaryByRecord, + isUnknownSQLRecord +} from '../../utils/specialRecord' import { InstanceType } from './ListDetail/ListDetailTable' import { useMemoizedFn } from 'ahooks' import { telemetry } from '../../utils/telemetry' import openLink from '@lib/utils/openLink' import { useNavigate } from 'react-router-dom' import { TopSQLContext } from '../../context' +import { AggLevel } from './List' interface ListTableProps { - data: TopsqlSummaryItem[] + data: any[] + groupBy: string topN: number instanceType: InstanceType timeRange: TimeRange @@ -42,12 +48,19 @@ interface ListTableProps { const emptyFn = () => {} -export type SQLRecord = TopsqlSummaryItem & { - cpuTime: number +export type SQLRecord = TopsqlSummaryItem & + TopsqlSummaryByItem & { + cpuTime: number + } + +function isConvertNumber(value: string): boolean { + let res = !isNaN(Number(value)) + return res } export function ListTable({ data, + groupBy, topN, instanceType, timeRange, @@ -59,22 +72,21 @@ export function ListTable({ const navigate = useNavigate() const ctx = useContext(TopSQLContext) - function goDetail(ev: React.MouseEvent, record: SQLRecord) { - const sv = sessionStorage.getItem('statement.query_options') - if (sv) { - const queryOptions = JSON.parse(sv) - queryOptions.searchText = record.sql_digest - sessionStorage.setItem( - 'statement.query_options', - JSON.stringify(queryOptions) - ) - } - - const tv = toTimeRangeValue(timeRange) - openLink(`/statement?from=${tv[0]}&to=${tv[1]}`, ev, navigate) - } - const tableColumns = useMemo(() => { + function goDetail(ev: React.MouseEvent, record: SQLRecord) { + const sv = sessionStorage.getItem('statement.query_options') + if (sv) { + const queryOptions = JSON.parse(sv) + queryOptions.searchText = record.sql_digest + sessionStorage.setItem( + 'statement.query_options', + JSON.stringify(queryOptions) + ) + } + + const tv = toTimeRangeValue(timeRange) + openLink(`/statement?from=${tv[0]}&to=${tv[1]}`, ev, navigate) + } let cols = [ { name: t('topsql.table.fields.cpu_time'), @@ -88,14 +100,32 @@ export function ListTable({ ) }, { - name: t('topsql.table.fields.sql'), - key: 'sql_text', + name: + groupBy === AggLevel.Table + ? t('topsql.table.fields.table') + : groupBy === AggLevel.Schema + ? t('topsql.table.fields.db') + : t('topsql.table.fields.sql'), + key: + groupBy === AggLevel.Table || groupBy === AggLevel.Schema + ? 'text' + : 'sql_text', minWidth: 250, maxWidth: 550, onRender: (rec: SQLRecord) => { - const text = isUnknownSQLRecord(rec) + let text = rec.text + ? rec.text + : isUnknownSQLRecord(rec) ? `(SQL ${rec.sql_digest?.slice(0, 8)})` : rec.sql_text! + + // parser the table name if the text is like "tableId-tableName" + text = + text.includes('-') && text.split('-').length > 1 + ? isConvertNumber(text.split('-')[0]) + ? text.split('-')[1] + ' ( tid =' + text.split('-')[0] + ' )' + : text + : text return isOthersRecord(rec) ? ( { - if (!isOthersRecord(rec)) { + if (!isOthersRecord(rec) && !isSummaryByRecord(rec)) { return ( goDetail(ev, rec)}> {t('topsql.table.actions.search_in_statements')} @@ -145,25 +175,33 @@ export function ListTable({ cols = cols.filter((c) => c.key !== 'actions') } return cols - }, [capacity, t, topN, ctx?.cfg.showSearchInStatements]) + }, [ + capacity, + t, + topN, + groupBy, + navigate, + timeRange, + ctx?.cfg.showSearchInStatements + ]) const csvHeaders = tableColumns .slice(0, 2) .map((c) => ({ label: c.name, key: c.key })) - const getKey = useMemoizedFn((r: SQLRecord) => r.sql_digest!) + const getKey = useMemoizedFn((r: SQLRecord) => r?.sql_digest ?? r?.text ?? '') const { selectedRecord, selection } = useRecordSelection({ storageKey: 'topsql.list_table_selected_key', selections: tableRecords, options: { - getKey + getKey, + canSelectItem: (r) => !isSummaryByRecord(r) } }) - const onRenderRow = useMemoizedFn((props: any) => (
onRowOver(props.item.sql_digest)} + onMouseEnter={() => onRowOver(props.item?.sql_digest ?? props.item?.text)} onMouseLeave={onRowLeave} onClick={() => telemetry.clickStatement(props.itemIndex, props.itemIndex === topN) @@ -205,19 +243,21 @@ export function ListTable({ onRenderRow={onRenderRow} /> - {selectedRecord && ( - - )} + {selectedRecord && + groupBy !== AggLevel.Table && + groupBy !== AggLevel.Schema && ( + + )} ) : null } -function useTableData(records: TopsqlSummaryItem[]) { +function useTableData(records: any[]) { const tableData: { data: SQLRecord[]; capacity: number } = useMemo(() => { if (!records) { return { data: [], capacity: 0 } @@ -232,6 +272,10 @@ function useTableData(records: TopsqlSummaryItem[]) { }) }) + if (r.cpu_time_ms_sum && (r.text?.length ?? 0) > 0) { + cpuTime = r.cpu_time_ms_sum + } + if (capacity < cpuTime) { capacity = cpuTime } @@ -247,6 +291,5 @@ function useTableData(records: TopsqlSummaryItem[]) { .sort((a, b) => (b.is_other ? -1 : 0)) return { data: d, capacity } }, [records]) - return tableData } diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/en.yaml b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/en.yaml index b83a5a54b7..3491d39a61 100755 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/en.yaml +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/en.yaml @@ -29,11 +29,13 @@ topsql: table: description: The following table shows which top {{topN}} queries are contributing the most to load in the current time range. Click one to see details. description_no_recent_data: There is no data currently. You need to wait for about 1 minute for new data being collected. - others: Other Statements - others_tooltip: All of other non Top {{topN}} statements + others: Others + others_tooltip: All of other non Top {{topN}} Items fields: cpu_time: Total CPU Time sql: SQL Statement + table: Table Name + db: Database Name actions: search_in_statements: Search in SQL Statements detail: diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/zh.yaml b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/zh.yaml index bb6a449fb2..187b3e5402 100755 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/zh.yaml +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/translations/zh.yaml @@ -29,11 +29,13 @@ topsql: table: description: 以下表格展示了当前时间范围内消耗负载最多的 {{topN}} 类 SQL 查询,点击后可进一步显示详情。 description_no_recent_data: 当前暂无数据,您需要等待约 1 分钟完成新数据采集。 - others: 其他语句 - others_tooltip: 所有其他非 Top {{topN}} 的语句 + others: 其他 + others_tooltip: 所有其他非 Top {{topN}} 的条目 fields: cpu_time: 累计 CPU 耗时 sql: SQL 语句 + table: 表名 + db: 数据库名 actions: search_in_statements: 在 SQL 语句分析中搜索 detail: diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/specialRecord.ts b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/specialRecord.ts index f62a11334f..364f796438 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/specialRecord.ts +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSQL/utils/specialRecord.ts @@ -21,10 +21,12 @@ export const createOverallRecord = (record: SQLRecord): PlanRecord => { } export const isOthersRecord = (r: SQLRecord) => { - return r.is_other + return r.is_other || r.text === 'other' } -export const isOthersDigest = (d: string) => !d +export const isSummaryByRecord = (r: SQLRecord) => { + return (r.text?.length ?? 0) > 0 +} const NO_PLAN_IDENTIFIER = '__NO_PLAN_IDENTIFIER__' diff --git a/ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/List.tsx b/ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/List.tsx index 2666755fee..0b9f86a3bd 100644 --- a/ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/List.tsx +++ b/ui/packages/tidb-dashboard-lib/src/apps/TopSlowQuery/pages/List.tsx @@ -6,7 +6,6 @@ import { Card, MultiSelect, TimeRangeValue, - fromTimeRangeValue, toTimeRangeValue } from '@lib/components' diff --git a/ui/packages/tidb-dashboard-lib/src/client/models.ts b/ui/packages/tidb-dashboard-lib/src/client/models.ts index 64c41d53dd..ba64a1cf7c 100644 --- a/ui/packages/tidb-dashboard-lib/src/client/models.ts +++ b/ui/packages/tidb-dashboard-lib/src/client/models.ts @@ -3965,6 +3965,41 @@ export interface TopsqlInstanceResponse { +/** + * + * @export + * @interface TopsqlSummaryByItem + */ +export interface TopsqlSummaryByItem { + /** + * + * @type {Array} + * @memberof TopsqlSummaryByItem + */ + 'cpu_time_ms'?: Array; + /** + * + * @type {number} + * @memberof TopsqlSummaryByItem + */ + 'cpu_time_ms_sum'?: number; + /** + * + * @type {string} + * @memberof TopsqlSummaryByItem + */ + 'text'?: string; + /** + * + * @type {Array} + * @memberof TopsqlSummaryByItem + */ + 'timestamp_sec'?: Array; +} + + + + /** * * @export @@ -4101,6 +4136,12 @@ export interface TopsqlSummaryResponse { * @memberof TopsqlSummaryResponse */ 'data'?: Array; + /** + * + * @type {Array} + * @memberof TopsqlSummaryResponse + */ + 'data_by'?: Array; }