Sortable/Groupable Memory Dashboard (#9014)

This commit is contained in:
Max Fitton
2020-06-19 14:26:35 -07:00
committed by GitHub
parent ad09aa985c
commit ca66f88b96
8 changed files with 367 additions and 204 deletions
+10 -6
View File
@@ -362,13 +362,17 @@ export type MemoryTableEntry = {
call_site: string;
};
export type MemoryTableGroups = {
[groupKey: string]: MemoryTableGroup;
};
export type MemoryTableGroup = {
entries: MemoryTableEntry[];
summary: MemoryTableSummary;
}
export type MemoryTableResponse = {
group: {
[groupKey: string]: {
entries: MemoryTableEntry[];
summary: MemoryTableSummary;
};
};
group: MemoryTableGroups;
summary: MemoryTableSummary;
};
@@ -0,0 +1,78 @@
import {
createStyles,
makeStyles,
TableHead,
TableRow,
TableSortLabel,
Theme,
} from "@material-ui/core";
import React from "react";
import { StyledTableCell } from "./TableCell";
import { Order } from "./tableUtils";
const useSortableTableHeadStyles = makeStyles((theme: Theme) =>
createStyles({
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}),
);
export type HeaderInfo<T> = {
id: keyof T;
label: string;
numeric: boolean;
};
type SortableTableHeadProps<T> = {
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof T) => void;
order: Order;
orderBy: string | null;
headerInfo: HeaderInfo<T>[];
};
const SortableTableHead = <T,>(props: SortableTableHeadProps<T>) => {
const { order, orderBy, onRequestSort, headerInfo } = props;
const classes = useSortableTableHeadStyles();
const createSortHandler = (property: keyof T) => (
event: React.MouseEvent<unknown>,
) => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
{headerInfo.map((headerInfo) => (
<StyledTableCell
key={headerInfo.label}
align={headerInfo.numeric ? "right" : "left"}
sortDirection={orderBy === headerInfo.id ? order : false}
>
<TableSortLabel
active={orderBy === headerInfo.id}
direction={orderBy === headerInfo.id ? order : "asc"}
onClick={createSortHandler(headerInfo.id)}
>
{headerInfo.label}
{orderBy === headerInfo.id ? (
<span className={classes.visuallyHidden}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</StyledTableCell>
))}
</TableRow>
</TableHead>
);
};
export default SortableTableHead;
@@ -0,0 +1,13 @@
import { TableCell } from "@material-ui/core";
import { styled } from "@material-ui/core/styles";
export const StyledTableCell = styled(TableCell)(({ theme }) => ({
padding: theme.spacing(1),
textAlign: "center",
}));
export const ExpandableStyledTableCell = styled(TableCell)(({ theme }) => ({
padding: theme.spacing(1),
textAlign: "center",
cursor: "pointer",
}));
@@ -0,0 +1,38 @@
export const descendingComparator = <T>(a: T, b: T, orderBy: keyof T) => {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
};
export type Order = "asc" | "desc";
export const getComparator = <Key extends keyof any>(
order: Order,
orderBy: Key,
): ((
a: { [key in Key]: number | string },
b: { [key in Key]: number | string },
) => number) => {
return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
};
export const stableSort = <T>(
array: T[],
comparator: (a: T, b: T) => number,
) => {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) {
return order;
}
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
};
@@ -1,25 +1,81 @@
import {
Button,
Checkbox,
createStyles,
FormControlLabel,
makeStyles,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Theme,
WithStyles,
withStyles,
} from "@material-ui/core";
import PauseIcon from "@material-ui/icons/Pause";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import React from "react";
import { connect } from "react-redux";
import { stopMemoryTableCollection } from "../../../api";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
MemoryTableEntry,
MemoryTableGroups,
stopMemoryTableCollection,
} from "../../../api";
import SortableTableHead, {
HeaderInfo,
} from "../../../common/SortableTableHead";
import { getComparator, Order, stableSort } from "../../../common/tableUtils";
import { StoreState } from "../../../store";
import { dashboardActions } from "../state";
import MemoryRowGroup from "./MemoryRowGroup";
import { MemoryTableRow } from "./MemoryTableRow";
const styles = (theme: Theme) =>
const makeGroupedEntries = (memoryTableGroups: MemoryTableGroups, order: Order, orderBy: keyof MemoryTableEntry | null) => {
const comparator = orderBy && getComparator(order, orderBy);
return Object.entries(memoryTableGroups).map(([groupKey, group]) => {
const sortedEntries = comparator
? stableSort(group.entries, comparator)
: group.entries;
return <MemoryRowGroup
groupKey={groupKey}
summary={group.summary}
entries={sortedEntries}
initialExpanded={true}
/>
});
};
const makeUngroupedEntries = (
memoryTableGroups: MemoryTableGroups,
order: Order,
orderBy: keyof MemoryTableEntry | null,
) => {
const allEntries = Object.values(memoryTableGroups).reduce(
(allEntries: Array<MemoryTableEntry>, memoryTableGroup) => {
const groupEntries = memoryTableGroup.entries;
return allEntries.concat(groupEntries);
},
[],
);
const sortedEntries =
orderBy === null
? allEntries
: stableSort(allEntries, getComparator(order, orderBy));
return sortedEntries.map((memoryTableEntry, index) => (
<MemoryTableRow
memoryTableEntry={memoryTableEntry}
key={`mem-row-${index}`}
/>
));
};
const memoryHeaderInfo: HeaderInfo<MemoryTableEntry>[] = [
{ id: "node_ip_address", label: "IP Address", numeric: true },
{ id: "pid", label: "pid", numeric: true },
{ id: "type", label: "Type", numeric: false },
{ id: "object_id", label: "Object ID", numeric: false },
{ id: "object_size", label: "Object Size (B)", numeric: true },
{ id: "reference_type", label: "Reference Type", numeric: false },
{ id: "call_site", label: "Call Site", numeric: false },
];
const useMemoryInfoStyles = makeStyles((theme: Theme) =>
createStyles({
table: {
marginTop: theme.spacing(1),
@@ -28,96 +84,83 @@ const styles = (theme: Theme) =>
padding: theme.spacing(1),
textAlign: "center",
},
});
}),
);
const mapStateToProps = (state: StoreState) => ({
const memoryInfoSelector = (state: StoreState) => ({
tab: state.dashboard.tab,
memoryTable: state.dashboard.memoryTable,
shouldObtainMemoryTable: state.dashboard.shouldObtainMemoryTable,
});
const mapDispatchToProps = dashboardActions;
type State = {
// If memory table is captured, it should stop renewing memory table.
pauseMemoryTable: boolean;
};
class MemoryInfo extends React.Component<
WithStyles<typeof styles> &
ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps,
State
> {
handlePauseMemoryTable = async () => {
const { shouldObtainMemoryTable } = this.props;
this.props.setShouldObtainMemoryTable(!shouldObtainMemoryTable);
const MemoryInfo: React.FC<{}> = () => {
const { memoryTable, shouldObtainMemoryTable } = useSelector(
memoryInfoSelector,
);
const { setShouldObtainMemoryTable } = useDispatch();
const toggleMemoryCollection = async () => {
setShouldObtainMemoryTable(!shouldObtainMemoryTable);
if (shouldObtainMemoryTable) {
await stopMemoryTableCollection();
}
};
renderIcon = () => {
if (this.props.shouldObtainMemoryTable) {
return <PauseIcon />;
} else {
return <PlayArrowIcon />;
}
};
const pauseButtonIcon = shouldObtainMemoryTable ? (
<PauseIcon />
) : (
<PlayArrowIcon />
);
const classes = useMemoryInfoStyles();
const [isGrouped, setIsGrouped] = useState(true);
const [order, setOrder] = React.useState<Order>("asc");
const toggleOrder = () => setOrder(order === "asc" ? "desc" : "asc");
const [orderBy, setOrderBy] = React.useState<keyof MemoryTableEntry | null>(
null,
);
return (
<React.Fragment>
{memoryTable !== null ? (
<React.Fragment>
<Button color="primary" onClick={toggleMemoryCollection}>
{pauseButtonIcon}
{shouldObtainMemoryTable ? "Pause Collection" : "Resume Collection"}
</Button>
<FormControlLabel
control={
<Checkbox
checked={isGrouped}
onChange={() => setIsGrouped(!isGrouped)}
color="primary"
/>
}
label="Group by host"
/>
<Table className={classes.table}>
<SortableTableHead
orderBy={orderBy || ""}
order={order}
onRequestSort={(event, property) => {
if (property === orderBy) {
toggleOrder();
} else {
setOrderBy(property);
setOrder("asc");
}
}}
headerInfo={memoryHeaderInfo}
/>
<TableBody>
{isGrouped
? makeGroupedEntries(memoryTable.group, order, orderBy)
: makeUngroupedEntries(memoryTable.group, order, orderBy)}
</TableBody>
</Table>
</React.Fragment>
) : (
<div>No Memory Table Information Provided</div>
)}
</React.Fragment>
);
};
render() {
const { classes, memoryTable } = this.props;
const memoryTableHeaders = [
"", // Padding
"IP Address",
"Pid",
"Type",
"Object ID",
"Object Size",
"Reference Type",
"Call Site",
];
return (
<React.Fragment>
{memoryTable !== null ? (
<React.Fragment>
<Button color="primary" onClick={this.handlePauseMemoryTable}>
{this.renderIcon()}
{this.props.shouldObtainMemoryTable
? "Pause Collection"
: "Resume Collection"}
</Button>
<Table className={classes.table}>
<TableHead>
<TableRow>
{memoryTableHeaders.map((header, index) => (
<TableCell key={index} className={classes.cell}>
{header}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{Object.keys(memoryTable.group).map((group_key, index) => (
<MemoryRowGroup
key={index}
groupKey={group_key}
memoryTableGroups={memoryTable.group}
initialExpanded={true}
/>
))}
</TableBody>
</Table>
</React.Fragment>
) : (
<div>No Memory Table Information Provided</div>
)}
</React.Fragment>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withStyles(styles)(MemoryInfo));
export default MemoryInfo;
@@ -1,28 +1,25 @@
import {
createStyles,
TableCell,
TableRow,
Theme,
withStyles,
WithStyles,
makeStyles
} from "@material-ui/core";
import AddIcon from "@material-ui/icons/Add";
import RemoveIcon from "@material-ui/icons/Remove";
import classNames from "classnames";
import React from "react";
import React, { useState } from "react";
import {
MemoryTableEntry,
MemoryTableResponse,
MemoryTableSummary,
} from "../../../api";
import {
ExpandableStyledTableCell,
StyledTableCell,
} from "../../../common/TableCell";
import MemorySummary from "./MemorySummary";
import { MemoryTableRow } from "./MemoryTableRow";
const styles = (theme: Theme) =>
const useMemoryRowGroupStyles = makeStyles((theme: Theme) =>
createStyles({
cell: {
padding: theme.spacing(1),
textAlign: "center",
},
expandCollapseCell: {
cursor: "pointer",
},
@@ -35,106 +32,62 @@ const styles = (theme: Theme) =>
fontFamily: "SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace",
whiteSpace: "pre",
},
});
}));
type Props = {
type MemoryRowGroupProps = {
groupKey: string;
memoryTableGroups: MemoryTableResponse["group"];
summary: MemoryTableSummary;
entries: MemoryTableEntry[];
initialExpanded: boolean;
};
type State = {
expanded: boolean;
const MemoryRowGroup: React.FC<MemoryRowGroupProps> = ({ groupKey, entries, summary, initialExpanded}) => {
const classes = useMemoryRowGroupStyles();
const [expanded, setExpanded] = useState(initialExpanded);
const toggleExpanded = () => setExpanded(!expanded);
const features = [
"node_ip_address",
"pid",
"type",
"object_id",
"object_size",
"reference_type",
"call_site",
];
return (
<React.Fragment>
<TableRow hover>
<ExpandableStyledTableCell onClick={toggleExpanded}>
{!expanded ? (
<AddIcon className={classes.expandCollapseIcon} />
) : (
<RemoveIcon className={classes.expandCollapseIcon} />
)}
</ExpandableStyledTableCell>
{features.map((feature, index) => (
<StyledTableCell key={index}>
{// TODO(sang): For now, it is always grouped by node_ip_address.
feature === "node_ip_address" ? groupKey : ""}
</StyledTableCell>
))}
</TableRow>
{expanded && (
<React.Fragment>
<MemorySummary initialExpanded={false} memoryTableSummary={summary} />
{entries.map((memoryTableEntry, index) => {
return (
<MemoryTableRow
memoryTableEntry={memoryTableEntry}
key={`${index}`}
/>
);
})}
</React.Fragment>
)}
</React.Fragment>
);
};
class MemoryRowGroup extends React.Component<
Props & WithStyles<typeof styles>,
State
> {
state: State = {
expanded: this.props.initialExpanded,
};
toggleExpand = () => {
this.setState((state) => ({
expanded: !state.expanded,
}));
};
render() {
const { classes, groupKey, memoryTableGroups } = this.props;
const { expanded } = this.state;
const features = [
"node_ip_address",
"pid",
"type",
"object_id",
"object_size",
"reference_type",
"call_site",
];
const memoryTableGroup = memoryTableGroups[groupKey];
const entries: Array<MemoryTableEntry> = memoryTableGroup["entries"];
const summary: MemoryTableSummary = memoryTableGroup["summary"];
return (
<React.Fragment>
<TableRow hover>
<TableCell
className={classNames(classes.cell, classes.expandCollapseCell)}
onClick={this.toggleExpand}
>
{!expanded ? (
<AddIcon className={classes.expandCollapseIcon} />
) : (
<RemoveIcon className={classes.expandCollapseIcon} />
)}
</TableCell>
{features.map((feature, index) => (
<TableCell className={classes.cell} key={index}>
{// TODO(sang): For now, it is always grouped by node_ip_address.
feature === "node_ip_address" ? groupKey : ""}
</TableCell>
))}
</TableRow>
{expanded && (
<React.Fragment>
<MemorySummary
initialExpanded={false}
memoryTableSummary={summary}
/>
{entries.map((memoryTableEntry, index) => {
const object_size =
memoryTableEntry.object_size === -1
? "?"
: `${memoryTableEntry.object_size} B`;
const memoryTableEntryValues = [
"", // Padding
memoryTableEntry.node_ip_address,
memoryTableEntry.pid,
memoryTableEntry.type,
memoryTableEntry.object_id,
object_size,
memoryTableEntry.reference_type,
memoryTableEntry.call_site,
];
return (
<TableRow hover key={index}>
{memoryTableEntryValues.map((value, index) => (
<TableCell key={index} className={classes.cell}>
{value}
</TableCell>
))}
</TableRow>
);
})}
</React.Fragment>
)}
</React.Fragment>
);
}
}
export default withStyles(styles)(MemoryRowGroup);
export default MemoryRowGroup;
@@ -0,0 +1,33 @@
import { TableRow } from "@material-ui/core";
import React from "react";
import { MemoryTableEntry } from "../../../api";
import { StyledTableCell } from "../../../common/TableCell";
type Props = {
memoryTableEntry: MemoryTableEntry;
key: string;
};
export const MemoryTableRow = (props: Props) => {
const { memoryTableEntry, key } = props;
const object_size =
memoryTableEntry.object_size === -1
? "?"
: `${memoryTableEntry.object_size} B`;
const memoryTableEntryValues = [
memoryTableEntry.node_ip_address,
memoryTableEntry.pid,
memoryTableEntry.type,
memoryTableEntry.object_id,
object_size,
memoryTableEntry.reference_type,
memoryTableEntry.call_site,
];
return (
<TableRow hover key={key}>
{memoryTableEntryValues.map((value, index) => (
<StyledTableCell key={`${key}-${index}`}>{value}</StyledTableCell>
))}
</TableRow>
);
};
@@ -15,6 +15,7 @@ import { connect } from "react-redux";
import { RayletInfoResponse } from "../../../api";
import { sum } from "../../../common/util";
import { StoreState } from "../../../store";
import {} from "../../../common/tableUtils";
import Errors from "./dialogs/errors/Errors";
import Logs from "./dialogs/logs/Logs";
import NodeRowGroup from "./NodeRowGroup";