Skip to content

Commit

Permalink
ui: databases page requires click to load table info
Browse files Browse the repository at this point in the history
Currently, the databases page automatically loads stats and info
on all tables in all database simultaneously. This can cause heavy
load and contention on clusters with hundreds of tables and/or
databases.

This change introduces a quick fix to prevent the immediate problem
with the expectation that we will revisit the databases page for
a more thorough redesign in the near future.

This change adds a button next to each database in the
databases page titled "Load stats for all tables" which triggers
the queries to load table-specific stats for just that one database.
The tables will be loaded in sequence by converting the loading
loop to use async/await to ensure one query is executed at a time.

One problem this introduces is that the database stats summary
box on the right computes its data incrementally and will show
changing data as more tables load for a specific database. We have
chosen to not show the updated data until all table info has been
loaded for the given database.

Release note (admin ui change): Loading table-level stats requires
a button click per-database in order to prevent contention for
clusters with many databases and/or tables. In addition, the loading
of table data is staggered by table instead of triggered simultaneously
for all tables.
  • Loading branch information
dhartunian committed Nov 3, 2020
1 parent 63f61aa commit f11a64e
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 125 deletions.
43 changes: 26 additions & 17 deletions pkg/ui/src/views/databases/containers/databaseSummary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { TableInfo } from "src/views/databases/data/tableInfo";
// on a DatabaseSummary component.
export interface DatabaseSummaryExplicitData {
name: string;
updateOnLoad?: boolean;
}

// DatabaseSummaryConnectedData describes properties which are applied to a
Expand All @@ -44,45 +45,53 @@ interface DatabaseSummaryActions {
refreshTableStats: typeof refreshTableStats;
}

type DatabaseSummaryProps = DatabaseSummaryExplicitData & DatabaseSummaryConnectedData & DatabaseSummaryActions;
export type DatabaseSummaryProps = DatabaseSummaryExplicitData & DatabaseSummaryConnectedData & DatabaseSummaryActions;

interface DatabaseSummaryState {
finishedLoadingTableData: boolean;
}

// DatabaseSummaryBase implements common lifecycle methods for DatabaseSummary
// components, which differ primarily by their render() method.
// TODO(mrtracy): We need to find a better abstraction for the common
// "refresh-on-mount-or-receiveProps" we have in many of our connected
// components; that would allow us to avoid this inheritance.
export class DatabaseSummaryBase extends React.Component<DatabaseSummaryProps, {}> {
export class DatabaseSummaryBase extends React.Component<DatabaseSummaryProps, DatabaseSummaryState> {
// loadTableDetails loads data for each table which have no info in the store.
// TODO(mrtracy): Should this be refreshing data always? Not sure if there
// is a performance concern with invalidation periods.
loadTableDetails(props = this.props) {
async loadTableDetails(props = this.props) {
if (props.tableInfos && props.tableInfos.length > 0) {
_.each(props.tableInfos, (tblInfo) => {
if (_.isUndefined(tblInfo.numColumns)) {
props.refreshTableDetails(new protos.cockroach.server.serverpb.TableDetailsRequest({
for (const tblInfo of props.tableInfos) {
// TODO(davidh): this is a stopgap inserted to deal with DBs containing hundreds of tables
await Promise.all([
_.isUndefined(tblInfo.numColumns) ? props.refreshTableDetails(new protos.cockroach.server.serverpb.TableDetailsRequest({
database: props.name,
table: tblInfo.name,
}));
}
if (_.isUndefined(tblInfo.physicalSize)) {
props.refreshTableStats(new protos.cockroach.server.serverpb.TableStatsRequest({
})) : null,
_.isUndefined(tblInfo.physicalSize) ? props.refreshTableStats(new protos.cockroach.server.serverpb.TableStatsRequest({
database: props.name,
table: tblInfo.name,
}));
}
});
})) : null,
]);
}
}
this.setState({finishedLoadingTableData: true});
}

// Refresh when the component is mounted.
componentDidMount() {
async componentDidMount() {
this.props.refreshDatabaseDetails(new protos.cockroach.server.serverpb.DatabaseDetailsRequest({ database: this.props.name }));
this.loadTableDetails();
if (this.props.updateOnLoad) {
await this.loadTableDetails();
}
}

// Refresh when the component receives properties.
componentDidUpdate() {
this.loadTableDetails(this.props);
async componentDidUpdate() {
if (this.props.updateOnLoad) {
await this.loadTableDetails(this.props);
}
}

render(): React.ReactElement<any> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

@require "~styl/base/palette.styl"

.empty-state
display flex
justify-content center
Expand All @@ -22,3 +24,25 @@
position relative
top 3px
right 2px

.sort-table__cell a
color: $text-color;
&:hover
color: $link-color;

.table-name a
display flex
align-items center
width 100%
height 100%
svg
margin-right 11px
&:hover
path
fill $colors--primary-blue-3

.database-summary-title
display flex

.database-summary-load-button
margin-left 11px
201 changes: 95 additions & 106 deletions pkg/ui/src/views/databases/containers/databaseTables/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,143 +8,133 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import tableIcon from "!!raw-loader!assets/tableIcon.svg";
import _ from "lodash";
import { SummaryCard } from "oss/src/views/shared/components/summaryCard";
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { refreshDatabaseDetails, refreshTableDetails, refreshTableStats } from "src/redux/apiReducers";
import { LocalSetting } from "src/redux/localsettings";
import { AdminUIState } from "src/redux/state";
import { Bytes } from "src/util/format";
import { trustIcon } from "src/util/trust";
import { databaseDetails, DatabaseSummaryBase, DatabaseSummaryExplicitData, grants, tableInfos as selectTableInfos } from "src/views/databases/containers/databaseSummary";
import {
databaseDetails,
DatabaseSummaryBase,
DatabaseSummaryExplicitData,
DatabaseSummaryProps,
grants,
tableInfos as selectTableInfos,
} from "src/views/databases/containers/databaseSummary";
import { TableInfo } from "src/views/databases/data/tableInfo";
import { SortSetting } from "src/views/shared/components/sortabletable";
import { SortedTable } from "src/views/shared/components/sortedtable";
import { SummaryBar, SummaryHeadlineStat } from "src/views/shared/components/summaryBar";
import "./databaseTables.styl";
import { SummaryCard } from "src/views/shared/components/summaryCard";
import { SummaryHeadlineStat } from "src/views/shared/components/summaryBar";
import { Button } from "src/components";

const databaseTablesSortSetting = new LocalSetting<AdminUIState, SortSetting>(
"databases/sort_setting/tables", (s) => s.localSettings,
);

class DatabaseTableListSortedTable extends SortedTable<TableInfo> {}

class DatabaseTableListEmpty extends React.Component {
render() {
return (
<table className="sort-table">
<thead>
<tr className="sort-table__row sort-table__row--header">
<th className="sort-table__cell sort-table__cell--sortable">
Table Name
</th>
<th className="sort-table__cell sort-table__cell--sortable">
Size
</th>
<th className="sort-table__cell sort-table__cell--sortable">
Ranges
</th>
<th className="sort-table__cell sort-table__cell--sortable">
# of Columns
</th>
<th className="sort-table__cell sort-table__cell--sortable">
# of Indices
</th>
</tr>
</thead>
<tbody>
<tr className="sort-table__row sort-table__row--body">
<td className="sort-table__cell" colSpan={5}>
<div className="empty-state">
<div className="empty-state__line">
<span className="table-icon" dangerouslySetInnerHTML={trustIcon(tableIcon)} />
This database has no tables.
</div>
</div>
</td>
</tr>
</tbody>
</table>
);
}
}

// DatabaseSummaryTables displays a summary section describing the tables
// contained in a single database.
class DatabaseSummaryTables extends DatabaseSummaryBase {
export class DatabaseSummaryTables extends DatabaseSummaryBase {
constructor(props: DatabaseSummaryProps) {
super(props);

this.state = {
finishedLoadingTableData: props.tableInfos && props.tableInfos.every(ti => ti.detailsAndStatsLoaded()),
};
}

totalSize() {
const tableInfos = this.props.tableInfos;
return _.sumBy(tableInfos, (ti) => ti.physicalSize);
if (this.state.finishedLoadingTableData) {
return _.sumBy(tableInfos, (ti) => ti.physicalSize);
} else {
return null;
}
}

totalRangeCount() {
const tableInfos = this.props.tableInfos;
return _.sumBy(tableInfos, (ti) => ti.rangeCount);
if (this.state.finishedLoadingTableData) {
return _.sumBy(tableInfos, (ti) => ti.rangeCount);
} else {
return null;
}
}

noDatabaseResults = () => (
<>
<h3 className="table__no-results--title">This database has no tables.</h3>
</>
)

render() {
const { tableInfos, sortSetting } = this.props;
const { tableInfos, dbResponse, sortSetting } = this.props;
const dbID = this.props.name;

const loading = dbResponse ? !!dbResponse.inFlight : true;
const numTables = tableInfos && tableInfos.length || 0;

return (
<div className="database-summary">
<SummaryCard>
<div className="database-summary-title">
<h2 className="base-heading">{dbID}</h2>
</div>
<div className="l-columns">
<div className="l-columns__left">
<div className="database-summary-table sql-table">
<div className="database-summary-title">
<h2 className="base-heading">{dbID}</h2>
{this.state.finishedLoadingTableData || numTables === 0 ? null :
<Button type="secondary" className="database-summary-load-button" onClick={async () => {
await this.loadTableDetails(this.props);
}}>Load stats for all tables</Button>
}
</div>
<div className="l-columns">
<div className="l-columns__left">
<DatabaseTableListSortedTable
data={tableInfos}
sortSetting={sortSetting}
onChangeSortSetting={(setting) => this.props.setSort(setting)}
firstCellBordered
columns={[
{
title: "Table Name",
cell: (tableInfo) => {
return (
<div className="sort-table__unbounded-column table-name">
<Link to={`/database/${dbID}/table/${tableInfo.name}`}>{tableInfo.name}</Link>
</div>
);
},
sort: (tableInfo) => tableInfo.name,
className: "expand-link", // don't pad the td element to allow the link to expand
},
{
(numTables === 0) ? <DatabaseTableListEmpty /> :
<DatabaseTableListSortedTable
data={tableInfos}
sortSetting={sortSetting}
onChangeSortSetting={(setting) => this.props.setSort(setting)}
columns={[
{
title: "Table Name",
cell: (tableInfo) => {
return (
<div className="sort-table__unbounded-column">
<Link to={`/database/${dbID}/table/${tableInfo.name}`}>{tableInfo.name}</Link>
</div>
);
},
sort: (tableInfo) => tableInfo.name,
className: "expand-link", // don't pad the td element to allow the link to expand
},
{
title: "Size",
cell: (tableInfo) => Bytes(tableInfo.physicalSize),
sort: (tableInfo) => tableInfo.physicalSize,
},
{
title: "Ranges",
cell: (tableInfo) => tableInfo.rangeCount,
sort: (tableInfo) => tableInfo.rangeCount,
},
{
title: "# of Columns",
cell: (tableInfo) => tableInfo.numColumns,
sort: (tableInfo) => tableInfo.numColumns,
},
{
title: "# of Indices",
cell: (tableInfo) => tableInfo.numIndices,
sort: (tableInfo) => tableInfo.numIndices,
},
]} />
}
</div>
</div>
<div className="l-columns__right">
<SummaryBar>
title: "Size",
cell: (tableInfo) => _.isUndefined(tableInfo.physicalSize) ? "" : Bytes(tableInfo.physicalSize),
sort: (tableInfo) => tableInfo.physicalSize,
},
{
title: "Ranges",
cell: (tableInfo) => tableInfo.rangeCount,
sort: (tableInfo) => tableInfo.rangeCount,
},
{
title: "# of Columns",
cell: (tableInfo) => tableInfo.numColumns,
sort: (tableInfo) => tableInfo.numColumns,
},
{
title: "# of Indices",
cell: (tableInfo) => tableInfo.numIndices,
sort: (tableInfo) => tableInfo.numIndices,
},
]}
loading={loading}
renderNoResult={loading ? undefined : this.noDatabaseResults()}
/>
</div>
<div className="l-columns__right">
<SummaryCard>
<SummaryHeadlineStat
title="Database Size"
tooltip="Approximate total disk size of this database across all table replicas."
Expand All @@ -158,10 +148,9 @@ class DatabaseSummaryTables extends DatabaseSummaryBase {
title="Total Range Count"
tooltip="The total ranges across all tables in this database."
value={this.totalRangeCount()} />
</SummaryBar>
</div>
</SummaryCard>
</div>
</SummaryCard>
</div>
</div>
);
}
Expand All @@ -170,7 +159,7 @@ class DatabaseSummaryTables extends DatabaseSummaryBase {
const mapStateToProps = (state: AdminUIState, ownProps: DatabaseSummaryExplicitData) => ({ // RootState contains declaration for whole state
tableInfos: selectTableInfos(state, ownProps.name),
sortSetting: databaseTablesSortSetting.selector(state),
dbResponse: databaseDetails(state)[ownProps.name] && databaseDetails(state)[ownProps.name].data,
dbResponse: databaseDetails(state)[ownProps.name],
grants: grants(state, ownProps.name),
});

Expand Down
4 changes: 2 additions & 2 deletions pkg/ui/src/views/databases/containers/databases/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ class DatabaseTablesList extends React.Component<DatabaseListProps> {
<DatabaseListNav selected="tables" onChange={this.handleOnNavigationListChange}/>
<div className="section databases">
{
user.map(n => <DatabaseSummaryTables name={n} key={n} />)
user.map(n => <DatabaseSummaryTables name={n} key={n} updateOnLoad={false} />)
}
<hr />
{
system.map(n => <DatabaseSummaryTables name={n} key={n} />)
system.map(n => <DatabaseSummaryTables name={n} key={n} updateOnLoad={false} />)
}
<NonTableSummary />
</div>
Expand Down
4 changes: 4 additions & 0 deletions pkg/ui/src/views/databases/data/tableInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ export class TableInfo {
this.physicalSize = FixLong(stats.approximate_disk_bytes).toNumber();
}
}

public detailsAndStatsLoaded(): boolean {
return this.id !== undefined && this.physicalSize !== undefined;
}
}

0 comments on commit f11a64e

Please sign in to comment.