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;