Skip to content

Commit

Permalink
Merge #92285
Browse files Browse the repository at this point in the history
92285: ui: add time picker to insights pages r=ericharmeling a=ericharmeling

This commit adds a time picker to the workload insights overview pages.

Loom (CC Console): https://www.loom.com/share/379aa57e40934bdb92e9c212f4c31735

Loom (DB Console): https://www.loom.com/share/1a54e02644e84941af0a54e4cb88cff2

Fixes #94761.
Fixes #94380.

Part of #83780.

Release note (ui change): Added a time picker to the Workload Insights Overview pages in the DB Console.

Co-authored-by: Eric Harmeling <[email protected]>
  • Loading branch information
craig[bot] and ericharmeling committed Jan 11, 2023
2 parents 58b84bc + 12ea614 commit 8b1be12
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 8b1be12

Please sign in to comment.