Skip to content

Commit

Permalink
ui: add time picker to insights pages
Browse files Browse the repository at this point in the history
This commit adds a time picker to the workload insights overview
pages. The time picker store is shared across SQL activity and
insights components, enabling us to better correlate the insight
events with fingerprints on the SQL activity pages by time
interval.

The start and end values of the time picker (stored in the
timeScale/SQLActivity local setting) form the request to the
insights "backend", where we use these start and end values to
filter queries to the internal insights and contention tables.
The "backend" queries select events across the interval, then
partition and filter on rank, ordered by descending end time.

Part of #83780.

Release note (ui change): Added a time picker to the Workload
Insights Overview pages in the DB Console.
  • Loading branch information
ericharmeling committed Jan 10, 2023
1 parent 290edbd commit 12ea614
Show file tree
Hide file tree
Showing 38 changed files with 500 additions and 202 deletions.
123 changes: 87 additions & 36 deletions pkg/ui/workspaces/cluster-ui/src/api/insightsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,33 @@ import moment from "moment";
import { INTERNAL_APP_NAME_PREFIX } from "src/recentExecutions/recentStatementUtils";
import { FixFingerprintHexValue } from "../util";

function getTxnContentionWhereClause(
clause: string,
filters?: QueryFilterFields,
): string {
let whereClause = clause;
if (filters?.start) {
whereClause =
whereClause + ` AND collection_ts >= '${filters.start.toISOString()}'`;
}
if (filters?.end) {
whereClause =
whereClause +
` AND (collection_ts + contention_duration) <= '${filters.end.toISOString()}'`;
}
return whereClause;
}

// Transaction contention insight events.

// txnContentionQuery selects all transaction contention events that are
// above the insights latency threshold.
const txnContentionQuery = `
SELECT * FROM
function txnContentionQuery(filters?: QueryFilterFields) {
const whereClause = getTxnContentionWhereClause(
` WHERE encode(waiting_txn_fingerprint_id, 'hex') != '0000000000000000'`,
filters,
);
return `SELECT * FROM
(
SELECT
waiting_txn_id,
Expand All @@ -59,12 +80,13 @@ SELECT * FROM
max(collection_ts) AS collection_ts,
sum(contention_duration) AS total_contention_duration
FROM crdb_internal.transaction_contention_events
WHERE encode(waiting_txn_fingerprint_id, 'hex') != '0000000000000000'
${whereClause}
GROUP BY waiting_txn_id, waiting_txn_fingerprint_id
)
WHERE total_contention_duration > threshold
)
WHERE rank = 1`;
}

type TransactionContentionResponseColumns = {
waiting_txn_id: string;
Expand Down Expand Up @@ -195,17 +217,17 @@ const makeInsightsSqlRequest = (queries: string[]): SqlExecutionRequest => ({
* txn contention insights and the query strings of txns involved in the contention.
* @returns a list of txn contention insights
*/
export async function getTxnInsightEvents(): Promise<
TxnContentionInsightEvent[]
> {
export async function getTxnInsightEvents(
req?: ExecutionInsightsRequest,
): Promise<TxnContentionInsightEvent[]> {
// Note that any errors encountered fetching these results are caught
// earlier in the call stack.

// Step 1: Get transaction contention events that are over the insights
// latency threshold.
const contentionResults =
await executeInternalSql<TransactionContentionResponseColumns>(
makeInsightsSqlRequest([txnContentionQuery]),
makeInsightsSqlRequest([txnContentionQuery(req)]),
);
if (sqlResultsAreEmpty(contentionResults)) {
return [];
Expand Down Expand Up @@ -297,7 +319,7 @@ function buildTxnContentionInsightEvents(
// 2. Reuse the queries/types defined above to get the waiting and blocking queries.
// After we get the results from these tables, we combine them on the frontend.

export type TxnContentionInsightDetailsRequest = { id: string };
export type TxnContentionInsightDetailsRequest = QueryFilterFields;

// Query 1 types, functions.
export type TransactionContentionEventDetails = Omit<
Expand All @@ -306,8 +328,12 @@ export type TransactionContentionEventDetails = Omit<
>;

// txnContentionDetailsQuery selects information about a specific transaction contention event.
const txnContentionDetailsQuery = (id: string) => `
SELECT
function txnContentionDetailsQuery(filters: QueryFilterFields) {
const whereClause = getTxnContentionWhereClause(
` WHERE waiting_txn_id = '${filters.id}'`,
filters,
);
return `SELECT
collection_ts,
blocking_txn_id,
encode( blocking_txn_fingerprint_id, 'hex' ) AS blocking_txn_fingerprint_id,
Expand All @@ -326,10 +352,11 @@ FROM
FROM [SHOW CLUSTER SETTING sql.insights.latency_threshold]
),
crdb_internal.transaction_contention_events AS tce
LEFT OUTER JOIN crdb_internal.ranges AS ranges
ON tce.contending_key BETWEEN ranges.start_key AND ranges.end_key
WHERE waiting_txn_id = '${id}'
LEFT OUTER JOIN crdb_internal.ranges AS ranges
ON tce.contending_key BETWEEN ranges.start_key AND ranges.end_key
${whereClause}
`;
}

type TxnContentionDetailsResponseColumns = {
waiting_txn_id: string;
Expand Down Expand Up @@ -428,7 +455,7 @@ export async function getTransactionInsightEventDetailsState(
// Get contention results for requested transaction.
const contentionResults =
await executeInternalSql<TxnContentionDetailsResponseColumns>(
makeInsightsSqlRequest([txnContentionDetailsQuery(req.id)]),
makeInsightsSqlRequest([txnContentionDetailsQuery(req)]),
);
if (sqlResultsAreEmpty(contentionResults)) {
return;
Expand Down Expand Up @@ -651,23 +678,40 @@ function organizeExecutionInsightsResponseIntoTxns(
}

type InsightQuery<ResponseColumnType, State> = {
name: InsightNameEnum;
query: string;
toState: (response: SqlExecutionResponse<ResponseColumnType>) => State;
};

const workloadInsightsQuery: InsightQuery<
ExecutionInsightsResponseRow,
TxnInsightEvent[]
> = {
name: InsightNameEnum.highContention,
// We only surface the most recently observed problem for a given statement.
// Note that we don't filter by problem != 'None', so that we can get all
// stmts in the problematic transaction.
query: `
export type QueryFilterFields = {
id?: string;
start?: moment.Moment;
end?: moment.Moment;
};

function workloadInsightsQuery(
filters?: QueryFilterFields,
): InsightQuery<ExecutionInsightsResponseRow, TxnInsightEvent[]> {
let whereClause = ` WHERE app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%'`;
if (filters?.start) {
whereClause =
whereClause + ` AND start_time >= '${filters.start.toISOString()}'`;
}
if (filters?.end) {
whereClause =
whereClause + ` AND end_time <= '${filters.end.toISOString()}'`;
}
return {
// We only surface the most recently observed problem for a given statement.
// Note that we don't filter by problem != 'None', so that we can get all
// stmts in the problematic transaction.
query: `
WITH insightsTable as (
SELECT * FROM crdb_internal.cluster_execution_insights
)
SELECT
*
FROM
crdb_internal.cluster_execution_insights
${whereClause}
)
SELECT
session_id,
insights.txn_id as txn_id,
Expand Down Expand Up @@ -696,23 +740,30 @@ SELECT
FROM
(
SELECT
txn_id,
row_number() OVER ( PARTITION BY txn_fingerprint_id ORDER BY end_time DESC ) as rank
txn_id,
row_number() OVER ( PARTITION BY txn_fingerprint_id ORDER BY end_time DESC ) as rank
FROM insightsTable
) as latestTxns
JOIN insightsTable AS insights
ON latestTxns.txn_id = insights.txn_id
WHERE latestTxns.rank = 1 AND app_name NOT LIKE '${INTERNAL_APP_NAME_PREFIX}%'
JOIN insightsTable AS insights
ON latestTxns.txn_id = insights.txn_id
WHERE latestTxns.rank = 1
`,
toState: organizeExecutionInsightsResponseIntoTxns,
};
toState: organizeExecutionInsightsResponseIntoTxns,
};
}

export type ExecutionInsights = TxnInsightEvent[];
export function getClusterInsightsApi(): Promise<ExecutionInsights> {

export type ExecutionInsightsRequest = Pick<QueryFilterFields, "start" | "end">;

export function getClusterInsightsApi(
req?: ExecutionInsightsRequest,
): Promise<ExecutionInsights> {
const insightsQuery = workloadInsightsQuery(req);
const request: SqlExecutionRequest = {
statements: [
{
sql: workloadInsightsQuery.query,
sql: insightsQuery.query,
},
],
execute: true,
Expand All @@ -721,7 +772,7 @@ export function getClusterInsightsApi(): Promise<ExecutionInsights> {
};
return executeInternalSql<ExecutionInsightsResponseRow>(request).then(
result => {
return workloadInsightsQuery.toState(result);
return insightsQuery.toState(result);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import React from "react";
import { SummaryCard, SummaryCardItem } from "src/summaryCard";

import { ContendedExecution, ExecutionType } from "src/recentExecutions";
import { capitalize, Duration } from "../util";
import { capitalize, Duration, NO_SAMPLES_FOUND } from "../util";

import { Heading } from "@cockroachlabs/ui-components";
import { ExecutionContentionTable } from "../recentExecutions/recentTransactionsTable/execContentionTable";
Expand Down Expand Up @@ -89,7 +89,7 @@ export const WaitTimeInsightsPanel: React.FC<WaitTimeInsightsPanelProps> = ({
value={
waitTime
? Duration(waitTime.asMilliseconds() * 1e6)
: "no samples"
: NO_SAMPLES_FOUND
}
/>
{schemaName && (
Expand Down
28 changes: 25 additions & 3 deletions pkg/ui/workspaces/cluster-ui/src/insights/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
// licenses/APL.txt.

import { limitStringArray, unset } from "src/util";
import { FlattenedStmtInsights } from "src/api/insightsApi";
import {
ExecutionInsightsRequest,
FlattenedStmtInsights,
} from "src/api/insightsApi";
import {
ExecutionDetails,
FlattenedStmtInsightEvent,
Expand All @@ -28,6 +31,7 @@ import {
TxnInsightEvent,
WorkloadInsightEventFilters,
} from "./types";
import { TimeScale, toDateRange } from "../timeScaleDropdown";

export const filterTransactionInsights = (
transactions: MergedTxnInsightEvent[] | null,
Expand Down Expand Up @@ -275,7 +279,7 @@ export function getInsightsFromProblemsAndCauses(

/**
* flattenTxnInsightsToStmts flattens the txn insights array
* into its stmt insights, including the txn level ifnormation.
* into its stmt insights, including the txn level information.
* Only stmts with non-empty insights array will be included.
* @param txnInsights array of transaction insights
* @returns An array of FlattenedStmtInsightEvent where each elem
Expand All @@ -287,11 +291,18 @@ export function flattenTxnInsightsToStmts(
): FlattenedStmtInsightEvent[] {
if (!txnInsights?.length) return [];
const stmtInsights: FlattenedStmtInsightEvent[] = [];
const seenExecutions = new Set<string>();
txnInsights.forEach(txnInsight => {
const { statementInsights, ...txnInfo } = txnInsight;
statementInsights?.forEach(stmt => {
if (!stmt.insights?.length) return;
if (
!stmt.insights?.length ||
seenExecutions.has(stmt.statementExecutionID)
) {
return;
}
stmtInsights.push({ ...txnInfo, ...stmt, query: stmt.query });
seenExecutions.add(stmt.statementExecutionID);
});
});
return stmtInsights;
Expand Down Expand Up @@ -510,3 +521,14 @@ export function dedupInsights(insights: Insight[]): Insight[] {
return deduped;
}, []);
}

export function executionInsightsRequestFromTimeScale(
ts: TimeScale,
): ExecutionInsightsRequest {
if (ts === null) return {};
const [startTime, endTime] = toDateRange(ts);
return {
start: startTime,
end: endTime,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import { SqlBox, SqlBoxSize } from "src/sql";
import { getMatchParamByName, idAttr } from "src/util";
import { FlattenedStmtInsightEvent } from "../types";
import { InsightsError } from "../insightsErrorComponent";
import classNames from "classnames/bind";

import { commonStyles } from "src/common";
import { getExplainPlanFromGist } from "src/api/decodePlanGistApi";
import { StatementInsightDetailsOverviewTab } from "./statementInsightDetailsOverviewTab";
import { ExecutionInsightsRequest } from "../../api";
import { executionInsightsRequestFromTimeScale } from "../utils";
import { TimeScale } from "../../timeScaleDropdown";

// Styles
import classNames from "classnames/bind";
import { commonStyles } from "src/common";
import insightsDetailsStyles from "src/insights/workloadInsightDetails/insightsDetails.module.scss";
import LoadingError from "../../sqlActivity/errorComponent";

Expand All @@ -42,11 +43,12 @@ export interface StatementInsightDetailsStateProps {
insightEventDetails: FlattenedStmtInsightEvent;
insightError: Error | null;
isTenant?: boolean;
timeScale?: TimeScale;
}

export interface StatementInsightDetailsDispatchProps {
refreshStatementInsights: (req: ExecutionInsightsRequest) => void;
setTimeScale: (ts: TimeScale) => void;
refreshStatementInsights: () => void;
}

export type StatementInsightDetailsProps = StatementInsightDetailsStateProps &
Expand All @@ -67,6 +69,7 @@ export const StatementInsightDetails: React.FC<
insightError,
match,
isTenant,
timeScale,
setTimeScale,
refreshStatementInsights,
}) => {
Expand Down Expand Up @@ -101,10 +104,11 @@ export const StatementInsightDetails: React.FC<
const executionID = getMatchParamByName(match, idAttr);

useEffect(() => {
if (insightEventDetails == null) {
refreshStatementInsights();
if (!insightEventDetails || insightEventDetails === null) {
const req = executionInsightsRequestFromTimeScale(timeScale);
refreshStatementInsights(req);
}
}, [insightEventDetails, refreshStatementInsights]);
}, [insightEventDetails, timeScale, refreshStatementInsights]);

return (
<div>
Expand All @@ -124,8 +128,8 @@ export const StatementInsightDetails: React.FC<
</h3>
<div>
<Loading
loading={insightEventDetails == null}
page={"Transaction Insight details"}
loading={insightEventDetails === null}
page={"Statement Insight details"}
error={insightError}
renderError={() => InsightsError()}
>
Expand Down
Loading

0 comments on commit 12ea614

Please sign in to comment.