Skip to content

Commit 374835f

Browse files
craig[bot]xinhaoz
andcommitted
107292: cluster-ui: speed up insights page request r=xinhaoz a=xinhaoz Previously, we were querying from `crdb_internal.statement_statistics` and `crdb_internal.transaction_statistics` on the schema insights and txn insights pages. Querying from this table is slow because it triggers a cluster-wide fanout to collect unflushsed sql stats. We can use the persisted table instead, which will lag behind a little in live data, but will be much faster to query from. Epic: none Fixes: cockroachdb#107291 Release note (bug fix): Schema insights page should hit request timeouts less frequently, if at all. DB Console Loom, showing pages issuing requests work as expected: https://www.loom.com/share/ef1baad105d0444596a32c8430234b88 Co-authored-by: Xin Hao Zhang <[email protected]>
2 parents ecf8daa + 076683a commit 374835f

File tree

6 files changed

+308
-60
lines changed

6 files changed

+308
-60
lines changed

pkg/ui/workspaces/cluster-ui/src/api/contentionApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export async function getContentionDetailsApi(
6161
const result = await executeInternalSql<ContentionResponseColumns>(request);
6262

6363
if (sqlResultsAreEmpty(result)) {
64-
if (result.error) {
64+
if (result?.error) {
6565
// We don't return an error if it failed to retrieve the contention information.
6666
getLogger().error(
6767
"Insights encounter an error while retrieving contention information.",
@@ -76,7 +76,7 @@ export async function getContentionDetailsApi(
7676
}
7777

7878
const contentionDetails: ContentionDetails[] = [];
79-
result.execution.txn_results.forEach(x => {
79+
result.execution?.txn_results.forEach(x => {
8080
x.rows.forEach(row => {
8181
contentionDetails.push({
8282
blockingExecutionID: row.blocking_txn_id,

pkg/ui/workspaces/cluster-ui/src/api/schemaInsightsApi.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -175,26 +175,34 @@ const dropUnusedIndexQuery: SchemaInsightQuery<ClusterIndexUsageStatistic> = {
175175
const createIndexRecommendationsQuery: SchemaInsightQuery<CreateIndexRecommendationsResponse> =
176176
{
177177
name: "CreateIndex",
178-
query: `SELECT
179-
encode(fingerprint_id, 'hex') AS fingerprint_id,
180-
metadata ->> 'db' AS db,
181-
metadata ->> 'query' AS query,
182-
metadata ->> 'querySummary' as querySummary,
183-
metadata ->> 'implicitTxn' AS implicitTxn,
184-
index_recommendations
185-
FROM (
186-
SELECT
187-
fingerprint_id,
188-
statistics -> 'statistics' ->> 'lastExecAt' as lastExecAt,
189-
metadata,
190-
index_recommendations,
191-
row_number() over(
192-
PARTITION BY
193-
fingerprint_id
194-
ORDER BY statistics -> 'statistics' ->> 'lastExecAt' DESC
195-
) AS rank
196-
FROM crdb_internal.statement_statistics WHERE aggregated_ts >= now() - INTERVAL '1 week')
197-
WHERE rank=1 AND array_length(index_recommendations,1) > 0;`,
178+
query: `
179+
SELECT
180+
encode(fingerprint_id, 'hex') AS fingerprint_id,
181+
metadata ->> 'db' AS db,
182+
metadata ->> 'query' AS query,
183+
metadata ->> 'querySummary' as querySummary,
184+
metadata ->> 'implicitTxn' AS implicitTxn,
185+
index_recommendations
186+
FROM
187+
(
188+
SELECT
189+
fingerprint_id,
190+
statistics -> 'statistics' ->> 'lastExecAt' as lastExecAt,
191+
metadata,
192+
index_recommendations,
193+
row_number() over(
194+
PARTITION BY fingerprint_id
195+
ORDER BY
196+
statistics -> 'statistics' ->> 'lastExecAt' DESC
197+
) AS rank
198+
FROM
199+
crdb_internal.statement_statistics_persisted
200+
WHERE
201+
aggregated_ts >= now() - INTERVAL '1 week'
202+
)
203+
WHERE
204+
rank = 1 AND array_length(index_recommendations, 1) > 0;
205+
`,
198206
toSchemaInsight: createIndexRecommendationsToSchemaInsight,
199207
};
200208

pkg/ui/workspaces/cluster-ui/src/api/sqlApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ export function sqlResultsAreEmpty(
126126
result: SqlExecutionResponse<unknown>,
127127
): boolean {
128128
return (
129-
!result.execution?.txn_results?.length ||
130-
result.execution.txn_results.every(txn => txnResultIsEmpty(txn))
129+
!result?.execution?.txn_results?.length ||
130+
result?.execution.txn_results.every(txn => txnResultIsEmpty(txn))
131131
);
132132
}
133133

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright 2023 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
import {
12+
getTxnInsightsContentionDetailsApi,
13+
TxnStmtFingerprintsResponseColumns,
14+
FingerprintStmtsResponseColumns,
15+
} from "./txnInsightsApi";
16+
import * as sqlApi from "./sqlApi";
17+
import { SqlExecutionResponse } from "./sqlApi";
18+
import {
19+
InsightExecEnum,
20+
InsightNameEnum,
21+
TxnContentionInsightDetails,
22+
} from "../insights";
23+
import { ContentionResponseColumns } from "./contentionApi";
24+
import moment from "moment-timezone";
25+
26+
function mockSqlResponse<T>(rows: T[]): SqlExecutionResponse<T> {
27+
return {
28+
execution: {
29+
retries: 0,
30+
txn_results: [
31+
{
32+
tag: "",
33+
start: "",
34+
end: "",
35+
rows_affected: 0,
36+
statement: 1,
37+
rows: [...rows],
38+
},
39+
],
40+
},
41+
};
42+
}
43+
44+
type TxnContentionDetailsTests = {
45+
name: string;
46+
contentionResp: SqlExecutionResponse<ContentionResponseColumns>;
47+
txnFingerprintsResp: SqlExecutionResponse<TxnStmtFingerprintsResponseColumns>;
48+
stmtsFingerprintsResp: SqlExecutionResponse<FingerprintStmtsResponseColumns>;
49+
expected: TxnContentionInsightDetails;
50+
};
51+
52+
describe("test txn insights api functions", () => {
53+
const waitingTxnID = "1a2a4828-5fc6-42d1-ab93-fadd4a514b69";
54+
const contentionDetailsMock: ContentionResponseColumns = {
55+
contention_duration: "00:00:00.00866",
56+
waiting_stmt_id: "17761e953a52c0300000000000000001",
57+
waiting_stmt_fingerprint_id: "b75264458f6e2ef3",
58+
schema_name: "public",
59+
database_name: "system",
60+
table_name: "namespace",
61+
index_name: "primary",
62+
key: `/NamespaceTable/30/1/0/0/"movr"/4/1`,
63+
collection_ts: "2023-07-28 19:25:36.434081+00",
64+
blocking_txn_id: "a13773b3-9bca-4019-9cfb-a376d6a4f412",
65+
blocking_txn_fingerprint_id: "4329ab5f4493f82d",
66+
waiting_txn_id: waitingTxnID,
67+
waiting_txn_fingerprint_id: "1831d909096f992c",
68+
};
69+
70+
afterEach(() => {
71+
jest.resetAllMocks();
72+
jest.clearAllMocks();
73+
});
74+
75+
test.each([
76+
{
77+
name: "all api responses empty",
78+
contentionResp: mockSqlResponse([]),
79+
txnFingerprintsResp: mockSqlResponse([]),
80+
stmtsFingerprintsResp: mockSqlResponse([]),
81+
expected: null,
82+
},
83+
{
84+
name: "no fingerprints available",
85+
contentionResp: mockSqlResponse([contentionDetailsMock]),
86+
txnFingerprintsResp: mockSqlResponse([]),
87+
stmtsFingerprintsResp: mockSqlResponse([]),
88+
expected: {
89+
transactionExecutionID: contentionDetailsMock.waiting_txn_id,
90+
application: undefined,
91+
transactionFingerprintID:
92+
contentionDetailsMock.waiting_txn_fingerprint_id,
93+
blockingContentionDetails: [
94+
{
95+
blockingExecutionID: contentionDetailsMock.blocking_txn_id,
96+
blockingTxnFingerprintID:
97+
contentionDetailsMock.blocking_txn_fingerprint_id,
98+
blockingTxnQuery: null,
99+
collectionTimeStamp: moment("2023-07-28 19:25:36.434081+00").utc(),
100+
contendedKey: '/NamespaceTable/30/1/0/0/"movr"/4/1',
101+
contentionTimeMs: 9,
102+
databaseName: contentionDetailsMock.database_name,
103+
indexName: contentionDetailsMock.index_name,
104+
schemaName: contentionDetailsMock.schema_name,
105+
tableName: contentionDetailsMock.table_name,
106+
waitingStmtFingerprintID:
107+
contentionDetailsMock.waiting_stmt_fingerprint_id,
108+
waitingStmtID: contentionDetailsMock.waiting_stmt_id,
109+
waitingTxnFingerprintID:
110+
contentionDetailsMock.waiting_txn_fingerprint_id,
111+
waitingTxnID: contentionDetailsMock.waiting_txn_id,
112+
},
113+
],
114+
execType: InsightExecEnum.TRANSACTION,
115+
insightName: InsightNameEnum.highContention,
116+
},
117+
},
118+
{
119+
name: "no stmt fingerprints available",
120+
contentionResp: mockSqlResponse([contentionDetailsMock]),
121+
txnFingerprintsResp: mockSqlResponse<TxnStmtFingerprintsResponseColumns>([
122+
{
123+
transaction_fingerprint_id:
124+
contentionDetailsMock.blocking_txn_fingerprint_id,
125+
query_ids: ["a", "b", "c"],
126+
app_name: undefined,
127+
},
128+
]),
129+
stmtsFingerprintsResp: mockSqlResponse([]),
130+
expected: {
131+
transactionExecutionID: contentionDetailsMock.waiting_txn_id,
132+
application: undefined,
133+
transactionFingerprintID:
134+
contentionDetailsMock.waiting_txn_fingerprint_id,
135+
blockingContentionDetails: [
136+
{
137+
blockingExecutionID: contentionDetailsMock.blocking_txn_id,
138+
blockingTxnFingerprintID:
139+
contentionDetailsMock.blocking_txn_fingerprint_id,
140+
blockingTxnQuery: [
141+
"Query unavailable for statement fingerprint 000000000000000a",
142+
"Query unavailable for statement fingerprint 000000000000000b",
143+
"Query unavailable for statement fingerprint 000000000000000c",
144+
],
145+
collectionTimeStamp: moment("2023-07-28 19:25:36.434081+00").utc(),
146+
contendedKey: '/NamespaceTable/30/1/0/0/"movr"/4/1',
147+
contentionTimeMs: 9,
148+
databaseName: contentionDetailsMock.database_name,
149+
indexName: contentionDetailsMock.index_name,
150+
schemaName: contentionDetailsMock.schema_name,
151+
tableName: contentionDetailsMock.table_name,
152+
waitingStmtFingerprintID:
153+
contentionDetailsMock.waiting_stmt_fingerprint_id,
154+
waitingStmtID: contentionDetailsMock.waiting_stmt_id,
155+
waitingTxnFingerprintID:
156+
contentionDetailsMock.waiting_txn_fingerprint_id,
157+
waitingTxnID: contentionDetailsMock.waiting_txn_id,
158+
},
159+
],
160+
execType: InsightExecEnum.TRANSACTION,
161+
insightName: InsightNameEnum.highContention,
162+
},
163+
},
164+
{
165+
name: "all info available",
166+
contentionResp: mockSqlResponse([contentionDetailsMock]),
167+
txnFingerprintsResp: mockSqlResponse<TxnStmtFingerprintsResponseColumns>([
168+
{
169+
transaction_fingerprint_id:
170+
contentionDetailsMock.blocking_txn_fingerprint_id,
171+
query_ids: ["a", "b", "c"],
172+
app_name: undefined,
173+
},
174+
]),
175+
stmtsFingerprintsResp: mockSqlResponse<FingerprintStmtsResponseColumns>([
176+
{
177+
statement_fingerprint_id: "a",
178+
query: "select 1",
179+
},
180+
{
181+
statement_fingerprint_id: "b",
182+
query: "select 2",
183+
},
184+
{
185+
statement_fingerprint_id: "c",
186+
query: "select 3",
187+
},
188+
]),
189+
expected: {
190+
transactionExecutionID: contentionDetailsMock.waiting_txn_id,
191+
application: undefined,
192+
transactionFingerprintID:
193+
contentionDetailsMock.waiting_txn_fingerprint_id,
194+
blockingContentionDetails: [
195+
{
196+
blockingExecutionID: contentionDetailsMock.blocking_txn_id,
197+
blockingTxnFingerprintID:
198+
contentionDetailsMock.blocking_txn_fingerprint_id,
199+
blockingTxnQuery: ["select 1", "select 2", "select 3"],
200+
collectionTimeStamp: moment("2023-07-28 19:25:36.434081+00").utc(),
201+
contendedKey: '/NamespaceTable/30/1/0/0/"movr"/4/1',
202+
contentionTimeMs: 9,
203+
databaseName: contentionDetailsMock.database_name,
204+
indexName: contentionDetailsMock.index_name,
205+
schemaName: contentionDetailsMock.schema_name,
206+
tableName: contentionDetailsMock.table_name,
207+
waitingStmtFingerprintID:
208+
contentionDetailsMock.waiting_stmt_fingerprint_id,
209+
waitingStmtID: contentionDetailsMock.waiting_stmt_id,
210+
waitingTxnFingerprintID:
211+
contentionDetailsMock.waiting_txn_fingerprint_id,
212+
waitingTxnID: contentionDetailsMock.waiting_txn_id,
213+
},
214+
],
215+
execType: InsightExecEnum.TRANSACTION,
216+
insightName: InsightNameEnum.highContention,
217+
},
218+
},
219+
] as TxnContentionDetailsTests[])(
220+
"$name",
221+
async (tc: TxnContentionDetailsTests) => {
222+
await jest
223+
.spyOn(sqlApi, "executeInternalSql")
224+
.mockReturnValueOnce(Promise.resolve(tc.contentionResp))
225+
.mockReturnValueOnce(Promise.resolve(tc.txnFingerprintsResp))
226+
.mockReturnValueOnce(Promise.resolve(tc.stmtsFingerprintsResp));
227+
228+
const res = await getTxnInsightsContentionDetailsApi({
229+
txnExecutionID: waitingTxnID,
230+
});
231+
expect(res).toEqual(tc.expected);
232+
},
233+
);
234+
});

0 commit comments

Comments
 (0)