Skip to content

Commit

Permalink
ui/cluster-ui: create statements insights api
Browse files Browse the repository at this point in the history
This commit adds a function to the `cluster-ui` pkg that queries
`crdb_internal.cluster_execution_insights` to surface slow running
queries. The information retrieved  is intended to be used in the
insights statement overview and details pages.

Release justification: non-production code change
Release note: None
  • Loading branch information
xinhaoz committed Aug 22, 2022
1 parent a36edf7 commit 85150eb
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 77 deletions.
213 changes: 152 additions & 61 deletions pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
InsightEventDetails,
InsightExecEnum,
InsightNameEnum,
StatementInsightEvent,
} from "src/insights";
import moment from "moment";

Expand All @@ -30,10 +31,7 @@ export type InsightEventsResponse = InsightEventState[];
type InsightQuery<ResponseColumnType, State> = {
name: InsightNameEnum;
query: string;
toState: (
response: SqlExecutionResponse<ResponseColumnType>,
results: Record<string, State>,
) => State[];
toState: (response: SqlExecutionResponse<ResponseColumnType>) => State;
};

// The only insight we currently report is "High Wait Time", which is the insight
Expand All @@ -53,29 +51,28 @@ type TransactionContentionResponseColumns = {

function transactionContentionResultsToEventState(
response: SqlExecutionResponse<TransactionContentionResponseColumns>,
results: Record<string, InsightEventState>,
): InsightEventState[] {
response.execution.txn_results[0].rows.forEach(row => {
const key = row.blocking_txn_id;
if (!results[key]) {
results[key] = {
executionID: row.blocking_txn_id,
queries: row.blocking_queries,
startTime: moment(row.collection_ts),
elapsedTime: moment.duration(row.contention_duration).asMilliseconds(),
application: row.app_name,
insightName: highWaitTimeQuery.name,
execType: InsightExecEnum.TRANSACTION,
};
}
});
if (!response.execution.txn_results[0].rows) {
// No data.
return [];
}

return Object.values(results);
return response.execution.txn_results[0].rows.map(row => ({
transactionID: row.blocking_txn_id,
queries: row.blocking_queries,
startTime: moment(row.collection_ts),
elapsedTimeMillis: moment
.duration(row.contention_duration)
.asMilliseconds(),
application: row.app_name,
insightName: highWaitTimeQuery.name,
execType: InsightExecEnum.TRANSACTION,
}));
}

const highWaitTimeQuery: InsightQuery<
TransactionContentionResponseColumns,
InsightEventState
InsightEventsResponse
> = {
name: InsightNameEnum.highWaitTime,
query: `SELECT
Expand Down Expand Up @@ -116,13 +113,7 @@ export function getInsightEventState(): Promise<InsightEventsResponse> {
};
return executeSql<TransactionContentionResponseColumns>(request).then(
result => {
if (!result.execution.txn_results[0].rows) {
// No data.
return [];
}

const results: Record<string, InsightEventState> = {};
return highWaitTimeQuery.toState(result, results);
return highWaitTimeQuery.toState(result);
},
);
}
Expand Down Expand Up @@ -154,40 +145,36 @@ type TransactionContentionDetailsResponseColumns = {

function transactionContentionDetailsResultsToEventState(
response: SqlExecutionResponse<TransactionContentionDetailsResponseColumns>,
results: Record<string, InsightEventDetailsState>,
): InsightEventDetailsState[] {
response.execution.txn_results[0].rows.forEach(row => {
const key = row.blocking_txn_id;
if (!results[key]) {
results[key] = {
executionID: row.blocking_txn_id,
queries: row.blocking_queries,
startTime: moment(row.collection_ts),
elapsedTime: moment.duration(row.contention_duration).asMilliseconds(),
application: row.app_name,
fingerprintID: row.blocking_txn_fingerprint_id,
waitingExecutionID: row.waiting_txn_id,
waitingFingerprintID: row.waiting_txn_fingerprint_id,
waitingQueries: row.waiting_queries,
schemaName: row.schema_name,
databaseName: row.database_name,
tableName: row.table_name,
indexName: row.index_name,
contendedKey: row.key,
insightName: highWaitTimeQuery.name,
execType: InsightExecEnum.TRANSACTION,
};
}
});

return Object.values(results);
if (!response.execution.txn_results[0].rows) {
// No data.
return [];
}
return response.execution.txn_results[0].rows.map(row => ({
executionID: row.blocking_txn_id,
queries: row.blocking_queries,
startTime: moment(row.collection_ts),
elapsedTime: moment.duration(row.contention_duration).asMilliseconds(),
application: row.app_name,
fingerprintID: row.blocking_txn_fingerprint_id,
waitingExecutionID: row.waiting_txn_id,
waitingFingerprintID: row.waiting_txn_fingerprint_id,
waitingQueries: row.waiting_queries,
schemaName: row.schema_name,
databaseName: row.database_name,
tableName: row.table_name,
indexName: row.index_name,
contendedKey: row.key,
insightName: highWaitTimeQuery.name,
execType: InsightExecEnum.TRANSACTION,
}));
}

const highWaitTimeDetailsQuery = (
id: string,
): InsightQuery<
TransactionContentionDetailsResponseColumns,
InsightEventDetailsState
InsightEventDetailsResponse
> => {
return {
name: InsightNameEnum.highWaitTime,
Expand Down Expand Up @@ -251,12 +238,116 @@ export function getInsightEventDetailsState(
};
return executeSql<TransactionContentionDetailsResponseColumns>(request).then(
result => {
if (!result.execution.txn_results[0].rows) {
// No data.
return [];
}
const results: Record<string, InsightEventDetailsState> = {};
return detailsQuery.toState(result, results);
return detailsQuery.toState(result);
},
);
}

type ExecutionInsightsResponseRow = {
session_id: string;
txn_id: string;
txn_fingerprint_id: string; // bytes
stmt_id: string;
stmt_fingerprint_id: string; // bytes
query: string;
start_time: string; // Timestamp
end_time: string; // Timestamp
full_scan: boolean;
user_name: string;
app_name: string;
database_name: string;
rows_read: number;
rows_written: number;
priority: string;
retries: number;
exec_node_ids: number[];
contention: string; // interval
last_retry_reason?: string;
};

export type StatementInsights = Omit<StatementInsightEvent, "insights">[];

function getStatementInsightsFromClusterExecutionInsightsResponse(
response: SqlExecutionResponse<ExecutionInsightsResponseRow>,
): StatementInsights {
if (!response.execution.txn_results[0].rows) {
// No data.
return [];
}

return response.execution.txn_results[0].rows.map(row => {
const start = moment.utc(row.start_time);
const end = moment.utc(row.end_time);
return {
transactionID: row.txn_id,
queries: [row.query],
startTime: start,
endTime: end,
databaseName: row.database_name,
elapsedTimeMillis: start.diff(end, "milliseconds"),
application: row.app_name,
execType: InsightExecEnum.STATEMENT,
statementID: row.stmt_id,
statementFingerprintID: row.stmt_fingerprint_id,
sessionID: row.session_id,
isFullScan: row.full_scan,
rowsRead: row.rows_read,
rowsWritten: row.rows_written,
priority: row.priority,
retries: row.retries,
lastRetryReason: row.last_retry_reason,
timeSpentWaiting: row.contention ? moment.duration(row.contention) : null,
};
});
}

const statemenInsightsQuery: InsightQuery<
ExecutionInsightsResponseRow,
StatementInsights
> = {
name: InsightNameEnum.highWaitTime,
// We only surface the most recently observed problem for a given statement.
query: `SELECT * from (
SELECT
session_id,
txn_id,
txn_fingerprint_id,
stmt_id,
stmt_fingerprint_id,
query,
start_time,
end_time,
full_scan,
app_name,
database_name,
rows_read,
rows_written,
priority,
retries,
contention,
last_retry_reason,
row_number() OVER (
PARTITION BY txn_fingerprint_id
ORDER BY end_time DESC
) AS rank
FROM crdb_internal.cluster_execution_insights
WHERE array_length(problems, 1) > 0
) WHERE rank = 1
`,
toState: getStatementInsightsFromClusterExecutionInsightsResponse,
};

export function getStatementInsightsApi(): Promise<StatementInsights> {
const request: SqlExecutionRequest = {
statements: [
{
sql: `${statemenInsightsQuery.query}`,
},
],
execute: true,
max_result_size: 50000, // 50 kib
};
return executeSql<ExecutionInsightsResponseRow>(request).then(result => {
return statemenInsightsQuery.toState(result);
});
}
20 changes: 18 additions & 2 deletions pkg/ui/workspaces/cluster-ui/src/insights/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export enum InsightExecEnum {
}

export type InsightEvent = {
executionID: string;
transactionID: string;
queries: string[];
insights: Insight[];
startTime: Moment;
elapsedTime: number;
elapsedTimeMillis: number;
application: string;
execType: InsightExecEnum;
};
Expand All @@ -50,6 +50,22 @@ export type InsightEventDetails = {
execType: InsightExecEnum;
};

export type StatementInsightEvent = InsightEvent & {
// Some of these can probably be moved to InsightEvent type once txn query is updated.
statementID: string;
statementFingerprintID: string;
sessionID: string;
timeSpentWaiting?: moment.Duration;
isFullScan: boolean;
endTime: Moment;
databaseName: string;
rowsRead: number;
rowsWritten: number;
lastRetryReason?: string;
priority: string;
retries: number;
};

export type Insight = {
name: InsightNameEnum;
label: string;
Expand Down
6 changes: 3 additions & 3 deletions pkg/ui/workspaces/cluster-ui/src/insights/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ export function getInsightsFromState(
return;
} else {
insightEvents.push({
executionID: e.executionID,
transactionID: e.transactionID,
queries: e.queries,
insights: insightsForEvent,
startTime: e.startTime,
elapsedTime: e.elapsedTime,
elapsedTimeMillis: e.elapsedTimeMillis,
application: e.application,
execType: InsightExecEnum.TRANSACTION,
});
Expand Down Expand Up @@ -114,7 +114,7 @@ export const filterTransactionInsights = (
filteredTransactions = filteredTransactions.filter(
txn =>
!search ||
txn.executionID.toLowerCase()?.includes(search) ||
txn.transactionID.toLowerCase()?.includes(search) ||
txn.queries?.find(query => query.toLowerCase().includes(search)),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,36 @@ import { InsightExecEnum } from "../../types";
export const transactionInsightsPropsFixture: TransactionInsightsViewProps = {
transactions: [
{
executionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a5",
transactionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a5",
queries: [
"SELECT IFNULL(a, b) FROM (SELECT (SELECT code FROM promo_codes WHERE code > $1 ORDER BY code LIMIT _) AS a, (SELECT code FROM promo_codes ORDER BY code LIMIT _) AS b)",
],
insightName: "HIGH_WAIT_TIME",
startTime: moment.utc("2022.08.10"),
elapsedTime: moment.duration("00:00:00.25").asMilliseconds(),
elapsedTimeMillis: moment.duration("00:00:00.25").asMilliseconds(),
application: "demo",
execType: InsightExecEnum.TRANSACTION,
},
{
executionID: "e72f37ea-b3a0-451f-80b8-dfb27d0bc2a5",
transactionID: "e72f37ea-b3a0-451f-80b8-dfb27d0bc2a5",
queries: [
"INSERT INTO vehicles VALUES ($1, $2, __more6__)",
"INSERT INTO vehicles VALUES ($1, $2, __more6__)",
],
insightName: "HIGH_WAIT_TIME",
startTime: moment.utc("2022.08.10"),
elapsedTime: moment.duration("00:00:00.25").asMilliseconds(),
elapsedTimeMillis: moment.duration("00:00:00.25").asMilliseconds(),
application: "demo",
execType: InsightExecEnum.TRANSACTION,
},
{
executionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a0",
transactionID: "f72f37ea-b3a0-451f-80b8-dfb27d0bc2a0",
queries: [
"UPSERT INTO vehicle_location_histories VALUES ($1, $2, now(), $3, $4)",
],
insightName: "HIGH_WAIT_TIME",
startTime: moment.utc("2022.08.10"),
elapsedTime: moment.duration("00:00:00.25").asMilliseconds(),
elapsedTimeMillis: moment.duration("00:00:00.25").asMilliseconds(),
application: "demo",
execType: InsightExecEnum.TRANSACTION,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ export function makeTransactionInsightsColumns(): ColumnDescriptor<InsightEvent>
name: "executionID",
title: insightsTableTitles.executionID(execType),
cell: (item: InsightEvent) => (
<Link to={`/insights/${item.executionID}`}>
{String(item.executionID)}
<Link to={`/insights/${item.transactionID}`}>
{String(item.transactionID)}
</Link>
),
sort: (item: InsightEvent) => item.executionID,
sort: (item: InsightEvent) => item.transactionID,
},
{
name: "query",
Expand Down Expand Up @@ -67,8 +67,8 @@ export function makeTransactionInsightsColumns(): ColumnDescriptor<InsightEvent>
{
name: "elapsedTime",
title: insightsTableTitles.elapsedTime(execType),
cell: (item: InsightEvent) => Duration(item.elapsedTime * 1e6),
sort: (item: InsightEvent) => item.elapsedTime,
cell: (item: InsightEvent) => Duration(item.elapsedTimeMillis * 1e6),
sort: (item: InsightEvent) => item.elapsedTimeMillis,
},
{
name: "applicationName",
Expand Down
Loading

0 comments on commit 85150eb

Please sign in to comment.