From 99bdb3d61764f38f59b4fab26597ca4a19b5dad0 Mon Sep 17 00:00:00 2001 From: Eric Harmeling Date: Mon, 7 Nov 2022 12:24:38 -0500 Subject: [PATCH] ui: add insight count to sql activity execution fingerprint overviews This commit adds a column for the distinct insight types for each fingerprint listed on the Statement and Transaction Fingerprint Overview pages. Fixes #83780. Release note (ui change): Added Insights column to the SQL Activity Fingerprint Overview pages of the DB Console. --- .../cluster-ui/src/api/stmtInsightsApi.ts | 98 +++++++++++++++++++ .../cluster-ui/src/api/txnInsightsApi.ts | 87 ++++++++++++++++ .../cluster-ui/src/insights/types.ts | 5 + .../statementsPage/statementsPage.fixture.ts | 8 +- .../statementsPage.selectors.ts | 12 ++- .../src/statementsPage/statementsPage.tsx | 18 +++- .../statementsPageConnected.tsx | 6 ++ .../src/statementsTable/statementsTable.tsx | 35 ++++++- .../statementsTableContent.tsx | 21 +++- .../src/statsTableUtil/statsTableUtil.tsx | 33 +++++++ .../insights/statementInsightCounts/index.ts | 12 +++ .../statementInsightCounts.reducer.ts | 55 +++++++++++ .../statementInsightCounts.sagas.ts | 42 ++++++++ .../transactionInsightCounts/index.ts | 12 +++ .../transactionInsightCounts.reducer.ts | 55 +++++++++++ .../transactionInsightCounts.sagas.ts | 42 ++++++++ .../cluster-ui/src/store/reducers.ts | 12 +++ .../workspaces/cluster-ui/src/store/sagas.ts | 4 + .../transactionsPage/transactions.fixture.ts | 42 ++++++++ .../transactionsPage.selectors.ts | 12 ++- .../transactionsPage.stories.tsx | 11 +++ .../src/transactionsPage/transactionsPage.tsx | 23 ++++- .../transactionsPageConnected.tsx | 6 ++ .../cluster-ui/src/transactionsPage/utils.ts | 27 +++++ .../transactionsTable/transactionsTable.tsx | 8 ++ .../db-console/src/redux/apiReducers.ts | 26 +++++ .../src/views/insights/insightsSelectors.ts | 16 +++ .../src/views/statements/statementsPage.tsx | 4 + .../views/transactions/transactionsPage.tsx | 4 + 29 files changed, 726 insertions(+), 10 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/index.ts create mode 100644 pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.reducer.ts create mode 100644 pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.sagas.ts create mode 100644 pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/index.ts create mode 100644 pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.reducer.ts create mode 100644 pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.sagas.ts diff --git a/pkg/ui/workspaces/cluster-ui/src/api/stmtInsightsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/stmtInsightsApi.ts index 93c8f7479d73..d733d553b6e1 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/stmtInsightsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/stmtInsightsApi.ts @@ -14,12 +14,15 @@ import { LONG_TIMEOUT, sqlApiErrorMessage, SqlExecutionRequest, + SqlExecutionResponse, sqlResultsAreEmpty, SqlTxnResult, } from "./sqlApi"; import { + ExecutionInsightCountEvent, getInsightsFromProblemsAndCauses, InsightExecEnum, + InsightNameEnum, StmtInsightEvent, } from "src/insights"; import moment from "moment"; @@ -218,3 +221,98 @@ export function formatStmtInsights( } as StmtInsightEvent; }); } + +// Statement Insight Counts + +// Note that insight counts show the number of distinct insight types for a given execution, not the number of +// individual insight events. + +export type StatementInsightCounts = ExecutionInsightCountEvent[]; + +type StatementInsightCountResponseRow = { + stmt_fingerprint_id: string; // hex string + problem: string; + causes: string[]; +}; + +function getStatementInsightCountResponse( + response: SqlExecutionResponse, +): StatementInsightCounts { + if (!response.execution.txn_results[0].rows) { + return []; + } + + const stmtInsightMap = new Map>(); + response.execution.txn_results[0].rows.forEach(row => { + const stmtInsights = getInsightsFromProblemsAndCauses( + [row.problem], + row.causes, + InsightExecEnum.TRANSACTION, + ); + if (!stmtInsightMap.has(row.stmt_fingerprint_id)) { + const stmtInsightTypes = new Set(); + stmtInsights.forEach(insight => stmtInsightTypes.add(insight.name)); + stmtInsightMap.set(row.stmt_fingerprint_id, stmtInsightTypes); + } else { + stmtInsights.forEach(insight => { + const mapValues = stmtInsightMap.get(row.stmt_fingerprint_id); + !mapValues.has(insight.name) && mapValues.add(insight.name); + }); + } + }); + + const res: StatementInsightCounts = Array.from( + stmtInsightMap, + ([name, value]) => ({ fingerprintID: name, insightCount: value.size }), + ); + + return res; +} + +const stmtInsightCountsQuery = (filters?: StmtInsightsReq) => { + const stmtColumns = ` + encode(stmt_fingerprint_id, 'hex') AS stmt_fingerprint_id, + problem, + causes`; + + let whereClause = ` +WHERE app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%' +AND txn_id != '00000000-0000-0000-0000-000000000000'`; + + if (filters?.start) { + whereClause += ` AND start_time >= '${filters.start.toISOString()}'`; + } + + if (filters?.end) { + whereClause += ` AND end_time <= '${filters.end.toISOString()}'`; + } + + return ` + SELECT DISTINCT ON (stmt_fingerprint_id, problem, causes) + ${stmtColumns} + FROM + crdb_internal.cluster_execution_insights + ${whereClause} + ORDER BY stmt_fingerprint_id, problem, causes, end_time DESC +`; +}; + +export function getStatementInsightCount( + req: StmtInsightsReq, +): Promise { + const request: SqlExecutionRequest = { + statements: [ + { + sql: stmtInsightCountsQuery(req), + }, + ], + execute: true, + max_result_size: LARGE_RESULT_SIZE, + timeout: LONG_TIMEOUT, + }; + return executeInternalSql(request).then( + result => { + return getStatementInsightCountResponse(result); + }, + ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.ts index 7f51fdee1bdd..1337d23fd895 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/txnInsightsApi.ts @@ -20,9 +20,11 @@ import { } from "./sqlApi"; import { BlockedContentionDetails, + ExecutionInsightCountEvent, getInsightsFromProblemsAndCauses, InsightExecEnum, InsightNameEnum, + InsightType, TxnContentionInsightDetails, TxnInsightDetails, TxnInsightEvent, @@ -650,3 +652,88 @@ export async function getTxnInsightDetailsApi( errors, }; } + +// Transaction Insight Counts + +// Note that insight counts show the number of distinct insight types for a given execution, not the number of +// individual insight events. +export type TransactionInsightCounts = ExecutionInsightCountEvent[]; + +const txnInsightCountsQuery = (filters?: TxnQueryFilters) => { + const txnColumns = ` + encode(txn_fingerprint_id, 'hex') AS txn_fingerprint_id, + problems, + causes`; + + let whereClause = ` +WHERE app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%' +AND txn_id != '00000000-0000-0000-0000-000000000000'`; + + if (filters?.start) { + whereClause += ` AND start_time >= '${filters.start.toISOString()}'`; + } + + if (filters?.end) { + whereClause += ` AND end_time <= '${filters.end.toISOString()}'`; + } + + return ` + SELECT DISTINCT ON (txn_fingerprint_id, problems, causes) + ${txnColumns} + FROM + ${TXN_INSIGHTS_TABLE_NAME} + ${whereClause} + ORDER BY txn_fingerprint_id, problems, causes, end_time DESC +`; +}; + +type TransactionInsightCountResponseRow = { + txn_fingerprint_id: string; // hex string + problems: string[]; + causes: string[]; +}; + +function getTransactionInsightCountResponse( + response: SqlExecutionResponse, +): TransactionInsightCounts { + if (!response.execution.txn_results[0].rows) { + return []; + } + + const txnInsightMap = new Map>(); + response.execution.txn_results[0].rows.forEach(row => { + const txnInsights = getInsightsFromProblemsAndCauses( + row.problems, + row.causes, + InsightExecEnum.TRANSACTION, + ); + if (!txnInsightMap.has(row.txn_fingerprint_id)) { + const txnInsightTypes = new Set(); + txnInsights.forEach(insight => txnInsightTypes.add(insight.name)); + txnInsightMap.set(row.txn_fingerprint_id, txnInsightTypes); + } else { + txnInsights.forEach(insight => { + const mapValues = txnInsightMap.get(row.txn_fingerprint_id); + !mapValues.has(insight.name) && mapValues.add(insight.name); + }); + } + }); + + const res: TransactionInsightCounts = Array.from( + txnInsightMap, + ([name, value]) => ({ fingerprintID: name, insightCount: value.size }), + ); + + return res; +} + +export function getTransactionInsightCount( + req: TxnInsightsRequest, +): Promise { + const request = makeInsightsSqlRequest([txnInsightCountsQuery(req)]); + return executeInternalSql(request).then( + result => { + return getTransactionInsightCountResponse(result); + }, + ); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/insights/types.ts b/pkg/ui/workspaces/cluster-ui/src/insights/types.ts index e7a069dc2022..f392ca7d1ce3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/insights/types.ts +++ b/pkg/ui/workspaces/cluster-ui/src/insights/types.ts @@ -126,6 +126,11 @@ export type ContentionEvent = { execType: InsightExecEnum; }; +export type ExecutionInsightCountEvent = { + fingerprintID: string; + insightCount: number; +}; + export const highContentionInsight = ( execType: InsightExecEnum, latencyThresholdMs?: number, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts index aef877e3147e..fd8793b7e66f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -15,7 +15,7 @@ import { createMemoryHistory } from "history"; import Long from "long"; import { noop } from "lodash"; import * as protos from "@cockroachlabs/crdb-protobuf-client"; -import { RequestError } from "src/util"; +import {HexStringToInt64String, RequestError} from "src/util"; import { StatementDiagnosticsReport } from "../api"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import ILatencyInfo = cockroach.sql.ILatencyInfo; @@ -268,6 +268,7 @@ const diagnosticsReportsInProgress: StatementDiagnosticsReport[] = [ const aggregatedTs = Date.parse("Sep 15 2021 01:00:00 GMT") * 1e-3; const lastUpdated = moment("Sep 15 2021 01:30:00 GMT"); const aggregationInterval = 3600; // 1 hour +const stmt_fingerprint_id = HexStringToInt64String("\\x76245b7acd82d39d"); const statementsPagePropsFixture: StatementsPageProps = { history, @@ -946,6 +947,10 @@ const statementsPagePropsFixture: StatementsPageProps = { lastReset: "2020-04-13 07:22:23", columns: null, isTenant: false, + insightCounts: [{ + insightCount: 1, + fingerprintID: stmt_fingerprint_id, + }], hasViewActivityRedactedRole: false, hasAdminRole: true, dismissAlertMessage: noop, @@ -955,6 +960,7 @@ const statementsPagePropsFixture: StatementsPageProps = { refreshUserSQLRoles: noop, refreshNodes: noop, resetSQLStats: noop, + refreshInsightCount: noop, onTimeScaleChange: noop, onActivateStatementDiagnostics: noop, onDiagnosticsModalOpen: noop, diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts index eb124b787100..8bc4be627e13 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -31,7 +31,10 @@ import { selectDiagnosticsReportsPerStatement } from "../store/statementDiagnost import { AggregateStatistics } from "../statementsTable"; import { sqlStatsSelector } from "../store/sqlStats/sqlStats.selector"; import { SQLStatsState } from "../store/sqlStats"; -import { localStorageSelector } from "../store/utils/selectors"; +import { + adminUISelector, + localStorageSelector, +} from "../store/utils/selectors"; import { databasesListSelector } from "src/store/databasesList/databasesList.selectors"; type ICollectedStatementStatistics = @@ -243,3 +246,10 @@ export const selectSearch = createSelector( localStorageSelector, localStorage => localStorage["search/StatementsPage"], ); + +export const selectStatementInsightCounts = createSelector( + adminUISelector, + state => { + return state?.statementInsightCounts?.data; + }, +); diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index 487870836ac0..b0bd39d1f9bf 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -34,6 +34,7 @@ import { import { calculateTotalWorkload, syncHistory, unique } from "src/util"; import { + addInsightCounts, AggregateStatistics, makeStatementsColumns, populateRegionNodeForStatements, @@ -65,6 +66,7 @@ import { TimeScale, timeScale1hMinOptions, TimeScaleDropdown, + timeScaleRangeToObj, timeScaleToString, toRoundedDateRange, } from "../timeScaleDropdown"; @@ -76,8 +78,10 @@ import moment from "moment"; import { InsertStmtDiagnosticRequest, StatementDiagnosticsReport, + StmtInsightsReq, } from "../api"; import { filteredStatementsData } from "../sqlActivity/util"; +import { ExecutionInsightCountEvent } from "../insights"; const cx = classNames.bind(styles); const sortableTableCx = classNames.bind(sortableTableStyles); @@ -96,6 +100,7 @@ export interface StatementsPageDispatchProps { refreshNodes: () => void; refreshUserSQLRoles: () => void; resetSQLStats: (req: StatementsRequest) => void; + refreshInsightCount: (req: StmtInsightsReq) => void; dismissAlertMessage: () => void; onActivateStatementDiagnostics: ( insertStmtDiagnosticsRequest: InsertStmtDiagnosticRequest, @@ -132,6 +137,7 @@ export interface StatementsPageStateProps { sortSetting: SortSetting; filters: Filters; search: string; + insightCounts: ExecutionInsightCountEvent[]; isTenant?: UIConfigState["isTenant"]; hasViewActivityRedactedRole?: UIConfigState["hasViewActivityRedactedRole"]; hasAdminRole?: UIConfigState["hasAdminRole"]; @@ -303,9 +309,10 @@ export class StatementsPage extends React.Component< refreshStatements = (ts?: TimeScale): void => { const time = ts ?? this.props.timeScale; + const insightCountReq = timeScaleRangeToObj(time); + this.props.refreshInsightCount(insightCountReq); const req = stmtsRequestFromTimeScale(time); this.props.refreshStatements(req); - this.resetPolling(time); }; @@ -349,6 +356,11 @@ export class StatementsPage extends React.Component< ); } + if (!this.props.insightCounts) { + const insightCountReq = timeScaleRangeToObj(this.props.timeScale); + this.props.refreshInsightCount(insightCountReq); + } + this.refreshDatabases(); this.props.refreshUserSQLRoles(); @@ -499,14 +511,16 @@ export class StatementsPage extends React.Component< hasViewActivityRedactedRole, sortSetting, search, + insightCounts, } = this.props; - const data = filteredStatementsData( + const stmts = filteredStatementsData( filters, search, statements, nodeRegions, isTenant, ); + const data = addInsightCounts(stmts, insightCounts); const totalWorkload = calculateTotalWorkload(statements); const totalCount = data.length; const isEmptySearchResults = statements?.length > 0 && search?.length > 0; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index 3535f5aee738..b975053bd489 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -19,6 +19,7 @@ import { actions as localStorageActions } from "src/store/localStorage"; import { actions as sqlStatsActions } from "src/store/sqlStats"; import { actions as databasesListActions } from "src/store/databasesList"; import { actions as nodesActions } from "../store/nodes"; +import { actions as insightCountActions } from "../store/insights/statementInsightCounts"; import { StatementsPageDispatchProps, StatementsPageStateProps, @@ -36,6 +37,7 @@ import { selectFilters, selectSearch, selectStatementsLastUpdated, + selectStatementInsightCounts, } from "./statementsPage.selectors"; import { selectTimeScale } from "../store/utils/selectors"; import { @@ -59,6 +61,7 @@ import { mapStateToRecentStatementsPageProps, } from "./recentStatementsPage.selectors"; import { + StmtInsightsReq, InsertStmtDiagnosticRequest, StatementDiagnosticsReport, } from "../api"; @@ -100,6 +103,7 @@ export const ConnectedStatementsPage = withRouter( lastUpdated: selectStatementsLastUpdated(state), statementsError: selectStatementsLastError(state), totalFingerprints: selectTotalFingerprints(state), + insightCounts: selectStatementInsightCounts(state), }, activePageProps: mapStateToRecentStatementsPageProps(state), }), @@ -122,6 +126,8 @@ export const ConnectedStatementsPage = withRouter( dispatch(uiConfigActions.refreshUserSQLRoles()), resetSQLStats: (req: StatementsRequest) => dispatch(sqlStatsActions.reset(req)), + refreshInsightCount: (req: StmtInsightsReq) => + dispatch(insightCountActions.refresh(req)), dismissAlertMessage: () => dispatch( localStorageActions.update({ diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx index a2ac151b9223..5154b67dce3e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTable.tsx @@ -20,6 +20,7 @@ import { TimestampToNumber, TimestampToMoment, unset, + HexStringToInt64String, } from "src/util"; import { DATE_FORMAT } from "src/util/format"; import { @@ -41,7 +42,7 @@ import { } from "src/sortedtable"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import { StatementTableCell } from "./statementsTableContent"; +import { insightTableCell, StatementTableCell } from "./statementsTableContent"; import { statisticsTableTitles, StatisticType, @@ -51,6 +52,7 @@ type ICollectedStatementStatistics = cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics; import styles from "./statementsTable.module.scss"; import { StatementDiagnosticsReport } from "../api"; +import { ExecutionInsightCountEvent } from "../insights"; const cx = classNames.bind(styles); export interface AggregateStatistics { @@ -75,6 +77,7 @@ export interface AggregateStatistics { totalWorkload?: Long; regions?: string[]; regionNodes?: string[]; + insightCount?: number; } export class StatementsSortedTable extends SortedTable {} @@ -212,6 +215,12 @@ export function makeStatementsColumns( cell: latencyBar, sort: (stmt: AggregateStatistics) => stmt.stats.service_lat.mean, }, + { + name: "insightCount", + title: statisticsTableTitles.insightCount(statType), + cell: (stmt: AggregateStatistics) => insightTableCell(stmt.insightCount), + sort: (stmt: AggregateStatistics) => stmt.insightCount, + }, { name: "contention", title: statisticsTableTitles.contention(statType), @@ -378,3 +387,27 @@ export function populateRegionNodeForStatements( stmt.regionNodes = regionNodes; }); } + +export const addInsightCounts = function ( + stmts: AggregateStatistics[], + insightCounts: ExecutionInsightCountEvent[], +): AggregateStatistics[] { + if (!insightCounts) { + return stmts; + } + const res: AggregateStatistics[] = []; + stmts.forEach(stmt => { + const count = insightCounts?.find( + insightCount => + HexStringToInt64String(insightCount.fingerprintID) === + stmt.aggregatedFingerprintID, + )?.insightCount; + if (count) { + stmt.insightCount = count; + } else { + stmt.insightCount = 0; + } + res.push(stmt); + }); + return res; +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx index 0dbe63d918b5..f74c7825519f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsTable/statementsTableContent.tsx @@ -8,7 +8,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import React from "react"; +import React, { ReactElement } from "react"; import { Link } from "react-router-dom"; import classNames from "classnames/bind"; import { noop } from "lodash"; @@ -28,12 +28,19 @@ import { appNamesAttr, } from "src/util"; import styles from "./statementsTableContent.module.scss"; +import insightStyles from "src/insights/workloadInsights/util/workloadInsights.module.scss"; + +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { EllipsisVertical } from "@cockroachlabs/icons"; import { getBasePath, StatementDiagnosticsReport } from "../api"; import moment from "moment"; export type NodeNames = { [nodeId: string]: string }; const cx = classNames.bind(styles); +const insightCx = classNames.bind(insightStyles); + +type IStatementDiagnosticsReport = + cockroach.server.serverpb.IStatementDiagnosticsReport; export const StatementTableCell = { statements: @@ -258,3 +265,15 @@ export const NodeLink = (props: { ); + +export const insightTableCell = (count: number): ReactElement | string => { + let text: string; + if (!count || count === 0) { + return "0"; + } else if (count === 1) { + text = "1 insight"; + } else { + text = `${count} insights`; + } + return {text}; +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx b/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx index df437e447622..cdaaa64603d3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statsTableUtil/statsTableUtil.tsx @@ -15,6 +15,7 @@ import moment from "moment"; import { Tooltip } from "@cockroachlabs/ui-components"; import { contentionTime, + insights, planningExecutionTime, readFromDisk, readsAndWrites, @@ -64,6 +65,7 @@ export const statisticsColumnLabels = { lastExecTimestamp: "Last Execution Time (UTC)", statementFingerprintId: "Statement Fingerprint ID", transactionFingerprintId: "Transaction Fingerprint ID", + insightCount: "Insights", }; export const contentModifiers = { @@ -591,6 +593,37 @@ export const statisticsTableTitles: StatisticTableTitleType = { ); }, + insightCount: (statType: StatisticType) => { + let contentModifier = ""; + switch (statType) { + case "transaction": + contentModifier = contentModifiers.transaction; + break; + case "statement": + contentModifier = contentModifiers.statement; + break; + } + + return ( + +

+ {`The number of distinct `} + + insights + + {` detected for each ${contentModifier}.`} +

+ + } + > + {getLabel("insightCount")} +
+ ); + }, contention: (statType: StatisticType) => { let contentModifier = ""; let fingerprintModifier = ""; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/index.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/index.ts new file mode 100644 index 000000000000..d62479ce43e5 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/index.ts @@ -0,0 +1,12 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +export * from "./statementInsightCounts.reducer"; +export * from "./statementInsightCounts.sagas"; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.reducer.ts new file mode 100644 index 000000000000..e17f4a0ea1e6 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.reducer.ts @@ -0,0 +1,55 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { DOMAIN_NAME } from "../../utils"; +import moment, { Moment } from "moment"; +import { + StmtInsightsReq, + StatementInsightCounts, +} from "src/api/stmtInsightsApi"; + +export type StatementInsightCountsState = { + data: StatementInsightCounts; + lastUpdated: Moment; + lastError: Error; + valid: boolean; +}; + +const initialState: StatementInsightCountsState = { + data: null, + lastUpdated: null, + lastError: null, + valid: true, +}; + +const statementInsightCountsSlice = createSlice({ + name: `${DOMAIN_NAME}/statementInsightCountsSlice`, + initialState, + reducers: { + received: (state, action: PayloadAction) => { + state.data = action.payload; + state.valid = true; + state.lastError = null; + state.lastUpdated = moment.utc(); + }, + failed: (state, action: PayloadAction) => { + state.valid = false; + state.lastError = action.payload; + }, + invalidated: state => { + state.valid = false; + }, + refresh: (_, _action: PayloadAction) => {}, + request: (_, _action: PayloadAction) => {}, + }, +}); + +export const { reducer, actions } = statementInsightCountsSlice; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.sagas.ts new file mode 100644 index 000000000000..4f44c41be428 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/statementInsightCounts/statementInsightCounts.sagas.ts @@ -0,0 +1,42 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import { all, call, put, takeLatest } from "redux-saga/effects"; + +import { actions } from "./statementInsightCounts.reducer"; +import { + StmtInsightsReq, + getStatementInsightCount, +} from "src/api/stmtInsightsApi"; +import { PayloadAction } from "@reduxjs/toolkit"; + +export function* refreshStatementInsightCountsSaga( + action: PayloadAction, +) { + yield put(actions.request(action.payload)); +} + +export function* requestStatementInsightCountsSaga( + action: PayloadAction, +): any { + try { + const result = yield call(getStatementInsightCount, action.payload); + yield put(actions.received(result)); + } catch (e) { + yield put(actions.failed(e)); + } +} + +export function* statementInsightCountsSaga() { + yield all([ + takeLatest(actions.refresh, refreshStatementInsightCountsSaga), + takeLatest(actions.request, requestStatementInsightCountsSaga), + ]); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/index.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/index.ts new file mode 100644 index 000000000000..ecd29554f1ea --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/index.ts @@ -0,0 +1,12 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +export * from "./transactionInsightCounts.reducer"; +export * from "./transactionInsightCounts.sagas"; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.reducer.ts new file mode 100644 index 000000000000..17d2962e56c7 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.reducer.ts @@ -0,0 +1,55 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { DOMAIN_NAME } from "../../utils"; +import moment, { Moment } from "moment"; +import { + TxnInsightsRequest, + TransactionInsightCounts, +} from "src/api/txnInsightsApi"; + +export type TransactionInsightCountsState = { + data: TransactionInsightCounts; + lastUpdated: Moment; + lastError: Error; + valid: boolean; +}; + +const initialState: TransactionInsightCountsState = { + data: null, + lastUpdated: null, + lastError: null, + valid: true, +}; + +const transactionInsightCountsSlice = createSlice({ + name: `${DOMAIN_NAME}/transactionInsightCountsSlice`, + initialState, + reducers: { + received: (state, action: PayloadAction) => { + state.data = action.payload; + state.valid = true; + state.lastError = null; + state.lastUpdated = moment.utc(); + }, + failed: (state, action: PayloadAction) => { + state.valid = false; + state.lastError = action.payload; + }, + invalidated: state => { + state.valid = false; + }, + refresh: (_, _action: PayloadAction) => {}, + request: (_, _action: PayloadAction) => {}, + }, +}); + +export const { reducer, actions } = transactionInsightCountsSlice; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.sagas.ts new file mode 100644 index 000000000000..e3259aff5d0a --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/store/insights/transactionInsightCounts/transactionInsightCounts.sagas.ts @@ -0,0 +1,42 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import { all, call, put, takeLatest } from "redux-saga/effects"; + +import { actions } from "./transactionInsightCounts.reducer"; +import { + TxnInsightsRequest, + getTransactionInsightCount, +} from "src/api/txnInsightsApi"; +import { PayloadAction } from "@reduxjs/toolkit"; + +export function* refreshTransactionInsightCountsSaga( + action: PayloadAction, +) { + yield put(actions.request(action.payload)); +} + +export function* requestTransactionInsightCountsSaga( + action: PayloadAction, +): any { + try { + const result = yield call(getTransactionInsightCount, action.payload); + yield put(actions.received(result)); + } catch (e) { + yield put(actions.failed(e)); + } +} + +export function* transactionInsightCountsSaga() { + yield all([ + takeLatest(actions.refresh, refreshTransactionInsightCountsSaga), + takeLatest(actions.request, requestTransactionInsightCountsSaga), + ]); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts index 8e7437455bff..b82513b1eb72 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/reducers.ts @@ -63,6 +63,14 @@ import { reducer as statementFingerprintInsights, StatementFingerprintInsightsCachedState, } from "./insights/statementFingerprintInsights"; +import { + StatementInsightCountsState, + reducer as statementInsightCounts, +} from "./insights/statementInsightCounts"; +import { + TransactionInsightCountsState, + reducer as transactionInsightCounts, +} from "./insights/transactionInsightCounts"; export type AdminUiState = { statementDiagnostics: StatementDiagnosticsState; @@ -84,6 +92,8 @@ export type AdminUiState = { txnInsights: TxnInsightsState; schemaInsights: SchemaInsightsState; statementFingerprintInsights: StatementFingerprintInsightsCachedState; + statementInsightCounts: StatementInsightCountsState; + transactionInsightCounts: TransactionInsightCountsState; }; export type AppState = { @@ -110,6 +120,8 @@ export const reducers = combineReducers({ databasesList, schemaInsights, statementFingerprintInsights, + statementInsightCounts, + transactionInsightCounts, }); export const rootActions = { diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts index 4f8b97c127b4..a1ffa7fbe18d 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sagas.ts @@ -31,6 +31,8 @@ import { statementInsightsSaga } from "./insights/statementInsights"; import { schemaInsightsSaga } from "./schemaInsights"; import { uiConfigSaga } from "./uiConfig"; import { statementFingerprintInsightsSaga } from "./insights/statementFingerprintInsights"; +import { statementInsightCountsSaga } from "./insights/statementInsightCounts"; +import { transactionInsightCountsSaga } from "./insights/transactionInsightCounts"; export function* sagas(cacheInvalidationPeriod?: number): SagaIterator { yield all([ @@ -54,5 +56,7 @@ export function* sagas(cacheInvalidationPeriod?: number): SagaIterator { fork(schemaInsightsSaga), fork(uiConfigSaga, cacheInvalidationPeriod), fork(statementFingerprintInsightsSaga), + fork(statementInsightCountsSaga), + fork(transactionInsightCountsSaga), ]); } diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts index ec88fe77a466..7768a38c9937 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactions.fixture.ts @@ -16,6 +16,8 @@ import * as protos from "@cockroachlabs/crdb-protobuf-client"; import { SortSetting } from "../sortedtable"; import { Filters } from "../queryFilter"; import { TimeScale } from "../timeScaleDropdown"; +import { ExecutionInsightCountEvent } from "../insights"; +import { HexStringToInt64String } from "../util"; const history = createMemoryHistory({ initialEntries: ["/transactions"] }); @@ -69,6 +71,8 @@ export const filters: Filters = { export const lastUpdated = moment(); +const txn_fingerprint_id = HexStringToInt64String("\\x76245b7acd82d39d"); + export const data: cockroach.server.serverpb.IStatementsResponse = { statements: [ { @@ -785,6 +789,7 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { transactions: [ { stats_data: { + transaction_fingerprint_id: Long.fromInt(Number(txn_fingerprint_id)), statement_fingerprint_ids: [Long.fromInt(100)], app: "$ internal-select-running/get-claimed-jobs", aggregated_ts: timestamp, @@ -807,6 +812,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 1, + ), statement_fingerprint_ids: [Long.fromInt(101)], app: "$ internal-stmt-diag-poll", aggregated_ts: timestamp, @@ -829,6 +837,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 2, + ), statement_fingerprint_ids: [Long.fromInt(102)], app: "$ internal-get-tables", aggregated_ts: timestamp, @@ -845,6 +856,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 3, + ), statement_fingerprint_ids: [Long.fromInt(103)], app: "$ internal-read orphaned leases", aggregated_ts: timestamp, @@ -861,6 +875,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 4, + ), statement_fingerprint_ids: [Long.fromInt(104)], app: "$ internal-expire-sessions", aggregated_ts: timestamp, @@ -880,6 +897,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 5, + ), statement_fingerprint_ids: [Long.fromInt(105)], app: "$ internal-show-version", aggregated_ts: timestamp, @@ -896,6 +916,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 6, + ), statement_fingerprint_ids: [Long.fromInt(106), Long.fromInt(107)], app: "$ internal-delete-sessions", aggregated_ts: timestamp, @@ -918,6 +941,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 7, + ), statement_fingerprint_ids: [Long.fromInt(108)], app: "$ TEST", aggregated_ts: timestamp, @@ -937,6 +963,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 8, + ), statement_fingerprint_ids: [Long.fromInt(109)], app: "$ TEST", aggregated_ts: timestamp, @@ -959,6 +988,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 9, + ), statement_fingerprint_ids: [Long.fromInt(107)], app: "$ TEST", aggregated_ts: timestamp, @@ -981,6 +1013,9 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, { stats_data: { + transaction_fingerprint_id: Long.fromInt( + Number(txn_fingerprint_id) + 10, + ), statement_fingerprint_ids: [Long.fromInt(107)], app: "$ TEST EXACT", aggregated_ts: timestamp, @@ -1003,3 +1038,10 @@ export const data: cockroach.server.serverpb.IStatementsResponse = { }, ], }; + +export const insightCounts: ExecutionInsightCountEvent[] = [ + { + insightCount: 1, + fingerprintID: txn_fingerprint_id, + }, +]; diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts index d95f71ecfeee..4d93f39a5da4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.selectors.ts @@ -10,7 +10,10 @@ import { createSelector } from "reselect"; -import { localStorageSelector } from "../store/utils/selectors"; +import { + adminUISelector, + localStorageSelector, +} from "../store/utils/selectors"; import { sqlStatsSelector } from "../store/sqlStats/sqlStats.selector"; export const selectTransactionsData = createSelector( @@ -48,3 +51,10 @@ export const selectSearch = createSelector( localStorageSelector, localStorage => localStorage["search/TransactionsPage"], ); + +export const selectTransactionInsightCounts = createSelector( + adminUISelector, + state => { + return state?.transactionInsightCounts?.data; + }, +); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx index fece79e6c210..34f4468c098f 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.stories.tsx @@ -21,6 +21,7 @@ import { sortSetting, filters, lastUpdated, + insightCounts, } from "./transactions.fixture"; import { TransactionsPage } from "."; @@ -49,10 +50,12 @@ storiesOf("Transactions Page", module) refreshData={noop} refreshNodes={noop} refreshUserSQLRoles={noop} + refreshInsightCount={noop} resetSQLStats={noop} search={""} sortSetting={sortSetting} lastUpdated={lastUpdated} + insightCounts={insightCounts} /> )) .add("without data", () => { @@ -71,10 +74,12 @@ storiesOf("Transactions Page", module) refreshData={noop} refreshNodes={noop} refreshUserSQLRoles={noop} + refreshInsightCount={noop} resetSQLStats={noop} search={""} sortSetting={sortSetting} lastUpdated={lastUpdated} + insightCounts={insightCounts} /> ); }) @@ -101,10 +106,12 @@ storiesOf("Transactions Page", module) refreshData={noop} refreshNodes={noop} refreshUserSQLRoles={noop} + refreshInsightCount={noop} resetSQLStats={noop} search={""} sortSetting={sortSetting} lastUpdated={lastUpdated} + insightCounts={insightCounts} /> ); }) @@ -124,10 +131,12 @@ storiesOf("Transactions Page", module) refreshData={noop} refreshNodes={noop} refreshUserSQLRoles={noop} + refreshInsightCount={noop} resetSQLStats={noop} search={""} sortSetting={sortSetting} lastUpdated={lastUpdated} + insightCounts={insightCounts} /> ); }) @@ -154,10 +163,12 @@ storiesOf("Transactions Page", module) refreshData={noop} refreshNodes={noop} refreshUserSQLRoles={noop} + refreshInsightCount={noop} resetSQLStats={noop} search={""} sortSetting={sortSetting} lastUpdated={lastUpdated} + insightCounts={insightCounts} /> ); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx index d5102531cea4..c38aca755577 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -34,6 +34,7 @@ import { getTrxAppFilterOptions, searchTransactionsData, filterTransactions, + addInsightCounts, } from "./utils"; import Long from "long"; import { merge } from "lodash"; @@ -68,11 +69,14 @@ import { timeScale1hMinOptions, getValidOption, toRoundedDateRange, + timeScaleRangeToObj, } from "../timeScaleDropdown"; +import { ExecutionInsightCountEvent } from "src/insights/types"; import { InlineAlert } from "@cockroachlabs/ui-components"; import { TransactionViewType } from "./transactionsPageTypes"; import { isSelectedColumn } from "../columnsSelector/utils"; import moment from "moment"; +import { TxnInsightsRequest } from "../api"; type IStatementsResponse = protos.cockroach.server.serverpb.IStatementsResponse; @@ -98,12 +102,14 @@ export interface TransactionsPageStateProps { search: string; sortSetting: SortSetting; hasAdminRole?: UIConfigState["hasAdminRole"]; + insightCounts: ExecutionInsightCountEvent[]; } export interface TransactionsPageDispatchProps { refreshData: (req: StatementsRequest) => void; refreshNodes: () => void; refreshUserSQLRoles: () => void; + refreshInsightCount: (req: TxnInsightsRequest) => void; resetSQLStats: (req: StatementsRequest) => void; onTimeScaleChange?: (ts: TimeScale) => void; onColumnsChange?: (selectedColumns: string[]) => void; @@ -130,6 +136,7 @@ function stmtsRequestFromTimeScale( end: Long.fromNumber(end.unix()), }); } + export class TransactionsPage extends React.Component< TransactionsPageProps, TState @@ -195,7 +202,7 @@ export class TransactionsPage extends React.Component< }; clearRefreshDataTimeout(): void { - if (this.refreshDataTimeout != null) { + if (this.refreshDataTimeout !== null) { clearTimeout(this.refreshDataTimeout); } } @@ -216,6 +223,8 @@ export class TransactionsPage extends React.Component< refreshData = (ts?: TimeScale): void => { const time = ts ?? this.props.timeScale; const req = stmtsRequestFromTimeScale(time); + const insightCountReq = timeScaleRangeToObj(time); + this.props.refreshInsightCount(insightCountReq); this.props.refreshData(req); this.resetPolling(time); @@ -251,6 +260,11 @@ export class TransactionsPage extends React.Component< ); } + if (!this.props.insightCounts) { + const insightCountReq = timeScaleRangeToObj(this.props.timeScale); + this.props.refreshInsightCount(insightCountReq); + } + this.props.refreshNodes(); this.props.refreshUserSQLRoles(); } @@ -415,6 +429,7 @@ export class TransactionsPage extends React.Component< sortSetting, search, hasAdminRole, + insightCounts, } = this.props; const internal_app_name_prefix = data?.internal_app_name_prefix || ""; const statements = data?.statements || []; @@ -503,7 +518,7 @@ export class TransactionsPage extends React.Component< error={this.props?.error} render={() => { const { pagination } = this.state; - const transactionsToDisplay: TransactionInfo[] = + const transactionsToDisplay: TransactionInfo[] = addInsightCounts( aggregateAcrossNodeIDs(filteredTransactions, statements).map( t => ({ stats_data: t.stats_data, @@ -511,7 +526,9 @@ export class TransactionsPage extends React.Component< regions: generateRegion(t, statements, nodeRegions), regionNodes: generateRegionNode(t, statements, nodeRegions), }), - ); + ), + insightCounts, + ); const { current, pageSize } = pagination; const hasData = data.transactions?.length > 0; const isUsedFilter = search?.length > 0; diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx index 504934470822..4e77064eb723 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx @@ -15,6 +15,7 @@ import { Dispatch } from "redux"; import { AppState, uiConfigActions } from "src/store"; import { actions as nodesActions } from "src/store/nodes"; import { actions as sqlStatsActions } from "src/store/sqlStats"; +import { actions as insightCountActions } from "src/store/insights/transactionInsightCounts"; import { TransactionsPageStateProps, TransactionsPageDispatchProps, @@ -26,6 +27,7 @@ import { selectSortSetting, selectFilters, selectSearch, + selectTransactionInsightCounts, } from "./transactionsPage.selectors"; import { selectHasAdminRole, selectIsTenant } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; @@ -51,6 +53,7 @@ import { RecentTransactionsViewStateProps, RecentTransactionsViewDispatchProps, } from "./recentTransactionsView"; +import { TxnInsightsRequest } from "../api"; type StateProps = { fingerprintsPageProps: TransactionsPageStateProps & RouteComponentProps; @@ -84,6 +87,7 @@ export const TransactionsPageConnected = withRouter( search: selectSearch(state), sortSetting: selectSortSetting(state), hasAdminRole: selectHasAdminRole(state), + insightCounts: selectTransactionInsightCounts(state), }, activePageProps: mapStateToRecentTransactionsPageProps(state), }), @@ -94,6 +98,8 @@ export const TransactionsPageConnected = withRouter( refreshNodes: () => dispatch(nodesActions.refresh()), refreshUserSQLRoles: () => dispatch(uiConfigActions.refreshUserSQLRoles()), + refreshInsightCount: (req: TxnInsightsRequest) => + dispatch(insightCountActions.refresh(req)), resetSQLStats: (req: StatementsRequest) => dispatch(sqlStatsActions.reset(req)), onTimeScaleChange: (ts: TimeScale) => { diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/utils.ts b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/utils.ts index 382e7004389c..f28cfed647f7 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/utils.ts +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/utils.ts @@ -29,7 +29,10 @@ import { computeOrUseStmtSummary, transactionScopedStatementKey, unset, + HexStringToInt64String, } from "../util"; +import { ExecutionInsightCountEvent } from "../insights"; +import { TransactionInfo } from "../transactionsTable"; type Statement = protos.cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics; @@ -438,3 +441,27 @@ export const aggregateAcrossNodeIDs = function ( .values() .value(); }; + +export const addInsightCounts = function ( + txns: TransactionInfo[], + insightCounts: ExecutionInsightCountEvent[], +): TransactionInfo[] { + if (!insightCounts) { + return txns; + } + const res: TransactionInfo[] = []; + txns.forEach(txn => { + const count = insightCounts?.find( + insightCount => + HexStringToInt64String(insightCount.fingerprintID) === + txn.stats_data.transaction_fingerprint_id.toString(), + )?.insightCount; + if (count) { + txn.insightCount = count; + } else { + txn.insightCount = 0; + } + res.push(txn); + }); + return res; +}; diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsTable/transactionsTable.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsTable/transactionsTable.tsx index 46c1e699ea3c..7ea2db0660a4 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsTable/transactionsTable.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsTable/transactionsTable.tsx @@ -46,6 +46,7 @@ import { } from "../transactionsPage/utils"; import classNames from "classnames/bind"; import statsTablePageStyles from "src/statementsTable/statementsTableContent.module.scss"; +import { insightTableCell } from "../statementsTable"; export type Transaction = protos.cockroach.server.serverpb.StatementsResponse.IExtendedCollectedTransactionStatistics; @@ -64,6 +65,7 @@ interface TransactionsTable { export interface TransactionInfo extends Transaction { regions: string[]; regionNodes: string[]; + insightCount?: number; } const { latencyClasses } = tableClasses; @@ -203,6 +205,12 @@ export function makeTransactionsColumns( className: latencyClasses.column, sort: (item: TransactionInfo) => item.stats_data.stats.service_lat.mean, }, + { + name: "insightCount", + title: statisticsTableTitles.insightCount(statType), + cell: (item: TransactionInfo) => insightTableCell(item.insightCount), + sort: (item: TransactionInfo) => item.insightCount, + }, { name: "contention", title: statisticsTableTitles.contention(statType), diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index 091e63cde6d6..456d222f2c3b 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -429,6 +429,26 @@ const txnInsightsReducerObj = new CachedDataReducer( export const refreshTxnInsights = txnInsightsReducerObj.refresh; export const invalidateTxnInsights = txnInsightsReducerObj.invalidateData; +const statementInsightCountsReducerObj = new CachedDataReducer( + clusterUiApi.getStatementInsightCount, + "statementInsightCounts", + null, + moment.duration(5, "m"), +); + +export const refreshStatementInsightCounts = + statementInsightCountsReducerObj.refresh; + +const transactionInsightCountsReducerObj = new CachedDataReducer( + clusterUiApi.getTransactionInsightCount, + "transactionInsightCounts", + null, + moment.duration(5, "m"), +); + +export const refreshTransactionInsightCounts = + transactionInsightCountsReducerObj.refresh; + export const txnInsightsRequestKey = ( req: clusterUiApi.TxnInsightDetailsRequest, ): string => req.txnExecutionID; @@ -560,6 +580,8 @@ export interface APIReducersState { stmtInsights: CachedDataReducerState; txnInsightDetails: KeyedCachedDataReducerState; txnInsights: CachedDataReducerState; + statementInsightCounts: CachedDataReducerState; + transactionInsightCounts: CachedDataReducerState; schemaInsights: CachedDataReducerState; statementFingerprintInsights: KeyedCachedDataReducerState; schedules: KeyedCachedDataReducerState; @@ -620,6 +642,10 @@ export const apiReducersReducer = combineReducers({ [rawTraceReducerObj.actionNamespace]: rawTraceReducerObj.reducer, [statementFingerprintInsightsReducerObj.actionNamespace]: statementFingerprintInsightsReducerObj.reducer, + [statementInsightCountsReducerObj.actionNamespace]: + statementInsightCountsReducerObj.reducer, + [transactionInsightCountsReducerObj.actionNamespace]: + transactionInsightCountsReducerObj.reducer, }); export { CachedDataReducerState, KeyedCachedDataReducerState }; diff --git a/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts b/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts index fa07aa1cccb2..5b0940438823 100644 --- a/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts +++ b/pkg/ui/workspaces/db-console/src/views/insights/insightsSelectors.ts @@ -183,3 +183,19 @@ export const selectSchemaInsightsTypes = createSelector( ).sort(); }, ); + +export const selectStatementInsightCounts = createSelector( + (state: AdminUIState) => state.cachedData, + state => { + if (!state.statementInsightCounts.data) return null; + return state.statementInsightCounts.data; + }, +); + +export const selectTransactionInsightCounts = createSelector( + (state: AdminUIState) => state.cachedData, + state => { + if (!state.transactionInsightCounts.data) return null; + return state.transactionInsightCounts.data; + }, +); diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx index 5c9c8866a91b..b46f11ee7047 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx @@ -17,6 +17,7 @@ import { refreshNodes, refreshDatabases, refreshStatementDiagnosticsRequests, + refreshStatementInsightCounts, refreshStatements, refreshUserSQLRoles, } from "src/redux/apiReducers"; @@ -72,6 +73,7 @@ import { selectStatementsDataValid, } from "src/selectors/executionFingerprintsSelectors"; import { api as clusterUiApi } from "@cockroachlabs/cluster-ui"; +import { selectStatementInsightCounts } from "src/views/insights/insightsSelectors"; type ICollectedStatementStatistics = protos.cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics; @@ -295,6 +297,7 @@ const fingerprintsPageActions = { refreshNodes, refreshUserSQLRoles, resetSQLStats: resetSQLStatsAction, + refreshInsightCount: refreshStatementInsightCounts, dismissAlertMessage: () => { return (dispatch: AppDispatch) => { dispatch( @@ -388,6 +391,7 @@ export default withRouter( totalFingerprints: selectTotalFingerprints(state), hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state), hasAdminRole: selectHasAdminRole(state), + insightCounts: selectStatementInsightCounts(state), }, activePageProps: mapStateToRecentStatementViewProps(state), }), diff --git a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx index d28bbb933efe..72bd8fbdf510 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -15,6 +15,7 @@ import { refreshNodes, refreshStatements, refreshUserSQLRoles, + refreshTransactionInsightCounts, } from "src/redux/apiReducers"; import { resetSQLStatsAction } from "src/redux/sqlStats"; import { CachedDataReducerState } from "src/redux/cachedDataReducer"; @@ -48,6 +49,7 @@ import { selectStatementsLastUpdated, selectStatementsDataValid, } from "src/selectors/executionFingerprintsSelectors"; +import { selectTransactionInsightCounts } from "src/views/insights/insightsSelectors"; // selectStatements returns the array of AggregateStatistics to show on the // TransactionsPage, based on if the appAttr route parameter is set. @@ -106,6 +108,7 @@ const fingerprintsPageActions = { refreshNodes, refreshUserSQLRoles, resetSQLStats: resetSQLStatsAction, + refreshInsightCount: refreshTransactionInsightCounts, onTimeScaleChange: setGlobalTimeScaleAction, // We use `null` when the value was never set and it will show all columns. // If the user modifies the selection and no columns are selected, @@ -161,6 +164,7 @@ const TransactionsPageConnected = withRouter( sortSetting: sortSettingLocalSetting.selector(state), statementsError: state.cachedData.statements.lastError, hasAdminRole: selectHasAdminRole(state), + insightCounts: selectTransactionInsightCounts(state), }, activePageProps: mapStateToRecentTransactionsPageProps(state), }),