github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/statements/statementDetails.tsx (about) 1 // Copyright 2018 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 { Col, Row, Tabs } from "antd"; 12 import _ from "lodash"; 13 import React, { ReactNode } from "react"; 14 import { Helmet } from "react-helmet"; 15 import { connect } from "react-redux"; 16 import { Link, RouteComponentProps, match as Match, withRouter } from "react-router-dom"; 17 import { createSelector } from "reselect"; 18 19 import { refreshStatementDiagnosticsRequests, refreshStatements } from "src/redux/apiReducers"; 20 import { nodeDisplayNameByIDSelector, NodesSummary } from "src/redux/nodes"; 21 import { AdminUIState } from "src/redux/state"; 22 import { 23 combineStatementStats, 24 ExecutionStatistics, 25 flattenStatementStats, 26 NumericStat, 27 StatementStatistics, 28 stdDev, 29 } from "src/util/appStats"; 30 import { appAttr, implicitTxnAttr, statementAttr } from "src/util/constants"; 31 import { FixLong } from "src/util/fixLong"; 32 import { Duration } from "src/util/format"; 33 import { intersperse } from "src/util/intersperse"; 34 import { Pick } from "src/util/pick"; 35 import Loading from "src/views/shared/components/loading"; 36 import { SortSetting } from "src/views/shared/components/sortabletable"; 37 import SqlBox from "src/views/shared/components/sql/box"; 38 import { formatNumberForDisplay } from "src/views/shared/components/summaryBar"; 39 import { ToolTipWrapper } from "src/views/shared/components/toolTip"; 40 import { PlanView } from "src/views/statements/planView"; 41 import { SummaryCard } from "../shared/components/summaryCard"; 42 import { approximify, latencyBreakdown, longToInt, rowsBreakdown } from "./barCharts"; 43 import { AggregateStatistics, makeNodesColumns, StatementsSortedTable } from "./statementsTable"; 44 import { getMatchParamByName } from "src/util/query"; 45 import DiagnosticsView from "./diagnostics"; 46 import classNames from "classnames"; 47 import { 48 selectDiagnosticsReportsCountByStatementFingerprint, 49 } from "src/redux/statements/statementsSelectors"; 50 import { Button, BackIcon } from "src/components/button"; 51 import { trackSubnavSelection } from "src/util/analytics"; 52 53 const { TabPane } = Tabs; 54 55 interface Fraction { 56 numerator: number; 57 denominator: number; 58 } 59 60 interface SingleStatementStatistics { 61 statement: string; 62 app: string[]; 63 distSQL: Fraction; 64 opt: Fraction; 65 implicit_txn: Fraction; 66 failed: Fraction; 67 node_id: number[]; 68 stats: StatementStatistics; 69 byNode: AggregateStatistics[]; 70 } 71 72 function AppLink(props: { app: string }) { 73 if (!props.app) { 74 return <span className="app-name app-name__unset">(unset)</span>; 75 } 76 77 return ( 78 <Link className="app-name" to={ `/statements/${encodeURIComponent(props.app)}` }> 79 { props.app } 80 </Link> 81 ); 82 } 83 84 interface StatementDetailsOwnProps { 85 statement: SingleStatementStatistics; 86 statementsError: Error | null; 87 nodeNames: { [nodeId: string]: string }; 88 refreshStatements: typeof refreshStatements; 89 refreshStatementDiagnosticsRequests: typeof refreshStatementDiagnosticsRequests; 90 nodesSummary: NodesSummary; 91 diagnosticsCount: number; 92 } 93 94 type StatementDetailsProps = StatementDetailsOwnProps & RouteComponentProps; 95 96 interface StatementDetailsState { 97 sortSetting: SortSetting; 98 } 99 100 interface NumericStatRow { 101 name: string; 102 value: NumericStat; 103 bar?: () => ReactNode; 104 summary?: boolean; 105 } 106 107 interface NumericStatTableProps { 108 title?: string; 109 description?: string; 110 measure: string; 111 rows: NumericStatRow[]; 112 count: number; 113 format?: (v: number) => string; 114 } 115 116 class NumericStatTable extends React.Component<NumericStatTableProps> { 117 static defaultProps = { 118 format: (v: number) => `${v}`, 119 }; 120 121 render() { 122 const { rows } = this.props; 123 return ( 124 <table className="sort-table statements-table"> 125 <thead> 126 <tr className="sort-table__row sort-table__row--header"> 127 <th className="sort-table__cell sort-table__cell--header">Phase</th> 128 <th className="sort-table__cell">Mean {this.props.measure}</th> 129 <th className="sort-table__cell">Standard Deviation</th> 130 </tr> 131 </thead> 132 <tbody> 133 { 134 rows.map((row: NumericStatRow) => { 135 const className = classNames("sort-table__row sort-table__row--body", {"sort-table__row--summary": row.summary}); 136 return ( 137 <tr className={className}> 138 <th className="sort-table__cell sort-table__cell--header" style={{ textAlign: "left" }}>{ row.name }</th> 139 <td className="sort-table__cell">{ row.bar ? row.bar() : null }</td> 140 <td className="sort-table__cell sort-table__cell--active">{ this.props.format(stdDev(row.value, this.props.count)) }</td> 141 </tr> 142 ); 143 }) 144 } 145 </tbody> 146 </table> 147 ); 148 } 149 } 150 151 export class StatementDetails extends React.Component<StatementDetailsProps, StatementDetailsState> { 152 153 constructor(props: StatementDetailsProps) { 154 super(props); 155 this.state = { 156 sortSetting: { 157 sortKey: 5, // Latency 158 ascending: false, 159 }, 160 }; 161 } 162 163 changeSortSetting = (ss: SortSetting) => { 164 this.setState({ 165 sortSetting: ss, 166 }); 167 } 168 169 componentDidMount() { 170 this.props.refreshStatements(); 171 this.props.refreshStatementDiagnosticsRequests(); 172 } 173 174 componentDidUpdate() { 175 this.props.refreshStatements(); 176 this.props.refreshStatementDiagnosticsRequests(); 177 } 178 179 prevPage = () => this.props.history.goBack(); 180 181 render() { 182 const app = getMatchParamByName(this.props.match, appAttr); 183 return ( 184 <div> 185 <Helmet title={`Details | ${(app ? `${app} App |` : "")} Statements`} /> 186 <div className="section page--header"> 187 <Button 188 onClick={this.prevPage} 189 type="flat" 190 size="small" 191 className="crl-button--link-to" 192 icon={BackIcon} 193 iconPosition="left" 194 > 195 Statements 196 </Button> 197 <h1 className="base-heading page--header__title">Statement Details</h1> 198 </div> 199 <section className="section section--container"> 200 <Loading 201 loading={_.isNil(this.props.statement)} 202 error={this.props.statementsError} 203 render={this.renderContent} 204 /> 205 </section> 206 </div> 207 ); 208 } 209 210 renderContent = () => { 211 const { diagnosticsCount } = this.props; 212 213 if (!this.props.statement) { 214 return null; 215 } 216 const { stats, statement, app, opt, failed, implicit_txn } = this.props.statement; 217 218 if (!stats) { 219 const sourceApp = getMatchParamByName(this.props.match, appAttr); 220 const listUrl = "/statements" + (sourceApp ? "/" + sourceApp : ""); 221 222 return ( 223 <React.Fragment> 224 <section className="section"> 225 <SqlBox value={ statement } /> 226 </section> 227 <section className="section"> 228 <h3>Unable to find statement</h3> 229 There are no execution statistics for this statement.{" "} 230 <Link className="back-link" to={ listUrl }> 231 Back to Statements 232 </Link> 233 </section> 234 </React.Fragment> 235 ); 236 } 237 238 const count = FixLong(stats.count).toInt(); 239 240 const { rowsBarChart } = rowsBreakdown(this.props.statement); 241 const { parseBarChart, planBarChart, runBarChart, overheadBarChart, overallBarChart } = latencyBreakdown(this.props.statement); 242 243 const totalCountBarChart = longToInt(this.props.statement.stats.count); 244 const firstAttemptsBarChart = longToInt(this.props.statement.stats.first_attempt_count); 245 const retriesBarChart = totalCountBarChart - firstAttemptsBarChart; 246 const maxRetriesBarChart = longToInt(this.props.statement.stats.max_retries); 247 248 const statsByNode = this.props.statement.byNode; 249 const logicalPlan = stats.sensitive_info && stats.sensitive_info.most_recent_plan_description; 250 const duration = (v: number) => Duration(v * 1e9); 251 return ( 252 <Tabs defaultActiveKey="1" className="cockroach--tabs" onChange={trackSubnavSelection}> 253 <TabPane tab="Overview" key="overview"> 254 <Row gutter={16}> 255 <Col className="gutter-row" span={16}> 256 <SqlBox value={ statement } /> 257 </Col> 258 <Col className="gutter-row" span={8}> 259 <SummaryCard> 260 <Row> 261 <Col span={12}> 262 <div className="summary--card__counting"> 263 <h3 className="summary--card__counting--value">{formatNumberForDisplay(count * stats.service_lat.mean, duration)}</h3> 264 <p className="summary--card__counting--label">Total Time</p> 265 </div> 266 </Col> 267 <Col span={12}> 268 <div className="summary--card__counting"> 269 <h3 className="summary--card__counting--value">{formatNumberForDisplay(stats.service_lat.mean, duration)}</h3> 270 <p className="summary--card__counting--label">Mean Service Latency</p> 271 </div> 272 </Col> 273 </Row> 274 <p className="summary--card__divider"></p> 275 <div className="summary--card__item" style={{ justifyContent: "flex-start" }}> 276 <h4 className="summary--card__item--label">App:</h4> 277 <p className="summary--card__item--value">{ intersperse<ReactNode>(app.map(a => <AppLink app={ a } key={ a } />), ", ") }</p> 278 </div> 279 <div className="summary--card__item"> 280 <h4 className="summary--card__item--label">Transaction Type</h4> 281 <p className="summary--card__item--value">{ renderTransactionType(implicit_txn) }</p> 282 </div> 283 <div className="summary--card__item"> 284 <h4 className="summary--card__item--label">Distributed execution?</h4> 285 <p className="summary--card__item--value">{ renderBools(opt) }</p> 286 </div> 287 <div className="summary--card__item"> 288 <h4 className="summary--card__item--label">Used cost-based optimizer?</h4> 289 <p className="summary--card__item--value">{ renderBools(opt) }</p> 290 </div> 291 <div className="summary--card__item"> 292 <h4 className="summary--card__item--label">Failed?</h4> 293 <p className="summary--card__item--value">{ renderBools(failed) }</p> 294 </div> 295 </SummaryCard> 296 <SummaryCard> 297 <h2 className="base-heading summary--card__title">Execution Count</h2> 298 <div className="summary--card__item"> 299 <h4 className="summary--card__item--label">First Attempts</h4> 300 <p className="summary--card__item--value">{ firstAttemptsBarChart }</p> 301 </div> 302 <div className="summary--card__item"> 303 <h4 className="summary--card__item--label">Retries</h4> 304 <p className={classNames("summary--card__item--value", { "summary--card__item--value-red": retriesBarChart > 0})}>{ retriesBarChart }</p> 305 </div> 306 <div className="summary--card__item"> 307 <h4 className="summary--card__item--label">Max Retries</h4> 308 <p className={classNames("summary--card__item--value", { "summary--card__item--value-red": maxRetriesBarChart > 0})}>{ maxRetriesBarChart }</p> 309 </div> 310 <div className="summary--card__item"> 311 <h4 className="summary--card__item--label">Total</h4> 312 <p className="summary--card__item--value">{ totalCountBarChart }</p> 313 </div> 314 <p className="summary--card__divider"></p> 315 <h2 className="base-heading summary--card__title">Rows Affected</h2> 316 <div className="summary--card__item"> 317 <h4 className="summary--card__item--label">Mean Rows</h4> 318 <p className="summary--card__item--value">{ rowsBarChart(true) }</p> 319 </div> 320 <div className="summary--card__item"> 321 <h4 className="summary--card__item--label">Standard Deviation</h4> 322 <p className="summary--card__item--value">{ rowsBarChart() }</p> 323 </div> 324 </SummaryCard> 325 </Col> 326 </Row> 327 </TabPane> 328 <TabPane tab={`Diagnostics ${diagnosticsCount > 0 ? `(${diagnosticsCount})` : ""}`} key="diagnostics"> 329 <DiagnosticsView statementFingerprint={statement} /> 330 </TabPane> 331 <TabPane tab="Logical Plan" key="logical-plan"> 332 <SummaryCard> 333 <PlanView 334 title="Logical Plan" 335 plan={logicalPlan} 336 /> 337 </SummaryCard> 338 </TabPane> 339 <TabPane tab="Execution Stats" key="execution-stats"> 340 <SummaryCard> 341 <h2 className="base-heading summary--card__title"> 342 Execution Latency By Phase 343 <div className="numeric-stats-table__tooltip"> 344 <ToolTipWrapper text="The execution latency of this statement, broken down by phase."> 345 <div className="numeric-stats-table__tooltip-hover-area"> 346 <div className="numeric-stats-table__info-icon">i</div> 347 </div> 348 </ToolTipWrapper> 349 </div> 350 </h2> 351 <NumericStatTable 352 title="Phase" 353 measure="Latency" 354 count={ count } 355 format={ (v: number) => Duration(v * 1e9) } 356 rows={[ 357 { name: "Parse", value: stats.parse_lat, bar: parseBarChart }, 358 { name: "Plan", value: stats.plan_lat, bar: planBarChart }, 359 { name: "Run", value: stats.run_lat, bar: runBarChart }, 360 { name: "Overhead", value: stats.overhead_lat, bar: overheadBarChart }, 361 { name: "Overall", summary: true, value: stats.service_lat, bar: overallBarChart }, 362 ]} 363 /> 364 </SummaryCard> 365 <SummaryCard> 366 <h2 className="base-heading summary--card__title"> 367 Stats By Node 368 <div className="numeric-stats-table__tooltip"> 369 <ToolTipWrapper text="text"> 370 <div className="numeric-stats-table__tooltip-hover-area"> 371 <div className="numeric-stats-table__info-icon">i</div> 372 </div> 373 </ToolTipWrapper> 374 </div> 375 </h2> 376 <StatementsSortedTable 377 className="statements-table" 378 data={statsByNode} 379 columns={makeNodesColumns(statsByNode, this.props.nodeNames)} 380 sortSetting={this.state.sortSetting} 381 onChangeSortSetting={this.changeSortSetting} 382 firstCellBordered 383 /> 384 </SummaryCard> 385 </TabPane> 386 </Tabs> 387 ); 388 } 389 } 390 391 function renderTransactionType(implicitTxn: Fraction) { 392 if (Number.isNaN(implicitTxn.numerator)) { 393 return "(unknown)"; 394 } 395 if (implicitTxn.numerator === 0) { 396 return "Explicit"; 397 } 398 if (implicitTxn.numerator === implicitTxn.denominator) { 399 return "Implicit"; 400 } 401 const fraction = approximify(implicitTxn.numerator) + " of " + approximify(implicitTxn.denominator); 402 return `${fraction} were Implicit Txns`; 403 } 404 405 function renderBools(fraction: Fraction) { 406 if (Number.isNaN(fraction.numerator)) { 407 return "(unknown)"; 408 } 409 if (fraction.numerator === 0) { 410 return "No"; 411 } 412 if (fraction.numerator === fraction.denominator) { 413 return "Yes"; 414 } 415 return approximify(fraction.numerator) + " of " + approximify(fraction.denominator); 416 } 417 418 type StatementsState = Pick<AdminUIState, "cachedData", "statements">; 419 420 interface StatementDetailsData { 421 nodeId: number; 422 implicitTxn: boolean; 423 stats: StatementStatistics[]; 424 } 425 426 function keyByNodeAndImplicitTxn(stmt: ExecutionStatistics): string { 427 return stmt.node_id.toString() + stmt.implicit_txn; 428 } 429 430 function coalesceNodeStats(stats: ExecutionStatistics[]): AggregateStatistics[] { 431 const byNodeAndImplicitTxn: { [nodeId: string]: StatementDetailsData } = {}; 432 433 stats.forEach(stmt => { 434 const key = keyByNodeAndImplicitTxn(stmt); 435 if (!(key in byNodeAndImplicitTxn)) { 436 byNodeAndImplicitTxn[key] = { 437 nodeId: stmt.node_id, 438 implicitTxn: stmt.implicit_txn, 439 stats: [], 440 }; 441 } 442 byNodeAndImplicitTxn[key].stats.push(stmt.stats); 443 }); 444 445 return Object.keys(byNodeAndImplicitTxn).map(key => { 446 const stmt = byNodeAndImplicitTxn[key]; 447 return { 448 label: stmt.nodeId.toString(), 449 implicitTxn: stmt.implicitTxn, 450 stats: combineStatementStats(stmt.stats), 451 }; 452 }); 453 } 454 455 function fractionMatching(stats: ExecutionStatistics[], predicate: (stmt: ExecutionStatistics) => boolean): Fraction { 456 let numerator = 0; 457 let denominator = 0; 458 459 stats.forEach(stmt => { 460 const count = FixLong(stmt.stats.first_attempt_count).toInt(); 461 denominator += count; 462 if (predicate(stmt)) { 463 numerator += count; 464 } 465 }); 466 467 return { numerator, denominator }; 468 } 469 470 function filterByRouterParamsPredicate(match: Match<any>, internalAppNamePrefix: string): (stat: ExecutionStatistics) => boolean { 471 const statement = getMatchParamByName(match, statementAttr); 472 const implicitTxn = (getMatchParamByName(match, implicitTxnAttr) === "true"); 473 let app = getMatchParamByName(match, appAttr); 474 475 const filterByStatementAndImplicitTxn = (stmt: ExecutionStatistics) => 476 stmt.statement === statement && stmt.implicit_txn === implicitTxn; 477 478 if (!app) { 479 return filterByStatementAndImplicitTxn; 480 } 481 482 if (app === "(unset)") { 483 app = ""; 484 } 485 486 if (app === "(internal)") { 487 return (stmt: ExecutionStatistics) => 488 filterByStatementAndImplicitTxn(stmt) && stmt.app.startsWith(internalAppNamePrefix); 489 } 490 491 return (stmt: ExecutionStatistics) => 492 filterByStatementAndImplicitTxn(stmt) && stmt.app === app; 493 } 494 495 export const selectStatement = createSelector( 496 (state: StatementsState) => state.cachedData.statements, 497 (_state: StatementsState, props: RouteComponentProps) => props, 498 (statementsState, props) => { 499 const statements = statementsState.data?.statements; 500 if (!statements) { 501 return null; 502 } 503 504 const internalAppNamePrefix = statementsState.data?.internal_app_name_prefix; 505 const flattened = flattenStatementStats(statements); 506 const results = _.filter(flattened, filterByRouterParamsPredicate(props.match, internalAppNamePrefix)); 507 const statement = getMatchParamByName(props.match, statementAttr); 508 return { 509 statement, 510 stats: combineStatementStats(results.map(s => s.stats)), 511 byNode: coalesceNodeStats(results), 512 app: _.uniq(results.map(s => s.app)), 513 distSQL: fractionMatching(results, s => s.distSQL), 514 opt: fractionMatching(results, s => s.opt), 515 implicit_txn: fractionMatching(results, s => s.implicit_txn), 516 failed: fractionMatching(results, s => s.failed), 517 node_id: _.uniq(results.map(s => s.node_id)), 518 }; 519 }, 520 ); 521 522 const mapStateToProps = (state: AdminUIState, props: StatementDetailsProps) => { 523 const statement = selectStatement(state, props); 524 return { 525 statement, 526 statementsError: state.cachedData.statements.lastError, 527 nodeNames: nodeDisplayNameByIDSelector(state), 528 diagnosticsCount: selectDiagnosticsReportsCountByStatementFingerprint(state, statement?.statement), 529 }; 530 }; 531 532 const mapDispatchToProps = { 533 refreshStatements, 534 refreshStatementDiagnosticsRequests, 535 }; 536 537 // tslint:disable-next-line:variable-name 538 const StatementDetailsConnected = withRouter(connect( 539 mapStateToProps, 540 mapDispatchToProps, 541 )(StatementDetails)); 542 543 export default StatementDetailsConnected;