Pull aggrow from facebookincubator/tracery-prerelease
Reviewed By: bnham Differential Revision: D4250937 fbshipit-source-id: b5f2cfdeb06c04399670e463b8b2498e2fe0074b
This commit is contained in:
parent
3094c36c81
commit
48d3cd7d26
File diff suppressed because one or more lines are too long
|
@ -3,14 +3,10 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>JSC Heap Capture</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react-dom.js"></script>
|
||||
<script src="out/aggrow.js"></script>
|
||||
<script src="out/table.js"></script>
|
||||
</head>
|
||||
<body style="margin:0px; height: 100%">
|
||||
Loading... This could take a while depending on how big the profile is. Check devtools console for errors.
|
||||
</body>
|
||||
<script src="preLoadedCapture.js"></script>
|
||||
<script src="out/heapCapture.js"></script>
|
||||
<script src="bundle.js"></script>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "jsc-heap-capture",
|
||||
"version": "1.0.0",
|
||||
"description": "processes captured heaps from javascript core",
|
||||
"main": "bundle.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "cwdick",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.17.0",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-plugin-transform-class-properties": "^6.16.0",
|
||||
"babel-preset-es2015": "^6.16.0",
|
||||
"babel-preset-react": "^6.16.0",
|
||||
"react": "^0.14.1",
|
||||
"react-dom": "^0.14.1",
|
||||
"webpack": "^1.13.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
// @flow
|
||||
|
||||
import invariant from 'invariant';
|
||||
|
||||
import AggrowData, {
|
||||
AggrowDoubleColumn,
|
||||
AggrowIntColumn,
|
||||
AggrowStackColumn,
|
||||
AggrowStringColumn } from './AggrowData';
|
||||
import AggrowExpander from './AggrowExpander';
|
||||
import type { FlattenedStack } from './StackRegistry';
|
||||
import StackRegistry from './StackRegistry';
|
||||
|
||||
export type FocusConfig = {
|
||||
pattern: RegExp,
|
||||
firstMatch: boolean,
|
||||
leftSide: boolean,
|
||||
}
|
||||
type FocusPredicate = (frameId: number) => boolean;
|
||||
|
||||
export default class Aggrow {
|
||||
data: AggrowData;
|
||||
expander: AggrowExpander;
|
||||
|
||||
constructor(aggrowData: AggrowData) {
|
||||
aggrowData.flattenStacks();
|
||||
this.data = aggrowData;
|
||||
this.expander = new AggrowExpander(aggrowData.rowCount);
|
||||
}
|
||||
|
||||
addSumAggregator(aggregatorName: string, columnName: string): number {
|
||||
const column = this.data.getColumn(columnName);
|
||||
|
||||
invariant(column, `Column ${columnName} does not exist.`);
|
||||
invariant(column instanceof AggrowIntColumn || column instanceof AggrowDoubleColumn,
|
||||
`Sum aggregator does not support ${column.constructor.name} columns!`);
|
||||
return this.expander.addAggregator(
|
||||
aggregatorName,
|
||||
(indices: Int32Array): number => {
|
||||
let size = 0;
|
||||
indices.forEach((i: number) => { size += column.get(i); });
|
||||
return size;
|
||||
},
|
||||
(value: any): string => value.toLocaleString(),
|
||||
(a: number, b: number): number => b - a,
|
||||
);
|
||||
}
|
||||
|
||||
addCountAggregator(aggregatorName: string): number {
|
||||
return this.expander.addAggregator(
|
||||
aggregatorName,
|
||||
(indices: Int32Array): number => indices.length,
|
||||
(value: any): string => value.toLocaleString(),
|
||||
(a: number, b: number): number => b - a,
|
||||
);
|
||||
}
|
||||
|
||||
addStringExpander(expanderName: string, columnName: string): number {
|
||||
const column = this.data.getColumn(columnName);
|
||||
invariant(column, `Column ${columnName} does not exist.`);
|
||||
invariant(column instanceof AggrowStringColumn, 'String expander needs a string column.');
|
||||
const strings = column.strings;
|
||||
return this.expander.addFieldExpander(
|
||||
expanderName,
|
||||
(rowA: number, rowB: number): number => column.get(rowA) - column.get(rowB),
|
||||
(row: number): string => strings.get(column.get(row)),
|
||||
(s: string): string => s,
|
||||
);
|
||||
}
|
||||
|
||||
addNumberExpander(expanderName: string, columnName: string): number {
|
||||
const column = this.data.getColumn(columnName);
|
||||
invariant(column, `Column ${columnName} does not exist.`);
|
||||
invariant(
|
||||
column instanceof AggrowIntColumn || column instanceof AggrowDoubleColumn,
|
||||
`Number expander does not support ${column.constructor.name} columns.`);
|
||||
return this.expander.addFieldExpander(
|
||||
expanderName,
|
||||
(rowA: number, rowB: number): number => column.get(rowA) - column.get(rowB),
|
||||
(row: number): number => column.get(row),
|
||||
(n: any): string => n.toLocaleString(),
|
||||
);
|
||||
}
|
||||
|
||||
addPointerExpander(expanderName: string, columnName: string): number {
|
||||
const column = this.data.getColumn(columnName);
|
||||
invariant(column, `Column ${columnName} does not exist.`);
|
||||
invariant(
|
||||
column instanceof AggrowIntColumn,
|
||||
`Pointer expander does not support ${column.constructor.name} columns.`);
|
||||
return this.expander.addFieldExpander(
|
||||
expanderName,
|
||||
(rowA: number, rowB: number): number => column.get(rowA) - column.get(rowB),
|
||||
(row: number): number => column.get(row),
|
||||
(p: number): string => `0x${(p >>> 0).toString(16)}`, // eslint-disable-line no-bitwise
|
||||
);
|
||||
}
|
||||
|
||||
addStackExpander(
|
||||
expanderName: string,
|
||||
columnName: string,
|
||||
reverse: boolean,
|
||||
focus: ?FocusConfig): number {
|
||||
const column = this.data.getColumn(columnName);
|
||||
invariant(column, `Column ${columnName} does not exist.`);
|
||||
invariant(
|
||||
column instanceof AggrowStackColumn,
|
||||
`Stack expander does not support ${column.constructor.name} columns.`);
|
||||
let stacks = column.stacks;
|
||||
const getter = column.getter;
|
||||
const formatter = column.formatter;
|
||||
if (focus) {
|
||||
const re = focus.pattern;
|
||||
const predicate = (frameId: number): boolean => re.test(formatter(getter(frameId)));
|
||||
stacks = focusStacks(stacks, predicate, focus.firstMatch, focus.leftSide);
|
||||
}
|
||||
return this.expander.addStackExpander(
|
||||
expanderName,
|
||||
stacks.maxDepth,
|
||||
(row: number): FlattenedStack => stacks.get(column.get(row)),
|
||||
getter,
|
||||
formatter,
|
||||
!!reverse,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function focusStacks(
|
||||
stacks: StackRegistry,
|
||||
predicate: FocusPredicate,
|
||||
firstMatch: boolean,
|
||||
leftSide: boolean): FocusedStackRegistry {
|
||||
let stackMapper;
|
||||
if (firstMatch && leftSide) {
|
||||
stackMapper = (stack: FlattenedStack): FlattenedStack => {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
if (predicate(stack[i])) {
|
||||
return stack.subarray(0, i + 1);
|
||||
}
|
||||
}
|
||||
return stack.subarray(0, 0);
|
||||
};
|
||||
} else if (firstMatch && !leftSide) {
|
||||
stackMapper = (stack: FlattenedStack): FlattenedStack => {
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
if (predicate(stack[i])) {
|
||||
return stack.subarray(i, stack.length);
|
||||
}
|
||||
}
|
||||
return stack.subarray(0, 0);
|
||||
};
|
||||
} else if (!firstMatch && leftSide) {
|
||||
stackMapper = (stack: FlattenedStack): FlattenedStack => {
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
if (predicate(stack[i])) {
|
||||
return stack.subarray(0, i + 1);
|
||||
}
|
||||
}
|
||||
return stack.subarray(0, 0);
|
||||
};
|
||||
} else { // !firstMatch && !leftSide
|
||||
stackMapper = (stack: FlattenedStack): FlattenedStack => {
|
||||
for (let i = stack.length - 1; i >= 0; i--) {
|
||||
if (predicate(stack[i])) {
|
||||
return stack.subarray(i, stack.length);
|
||||
}
|
||||
}
|
||||
return stack.subarray(0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
invariant(stacks.stackIdMap, 'Stacks were not flattened.');
|
||||
return new FocusedStackRegistry(
|
||||
stacks.stackIdMap.map(stackMapper),
|
||||
stacks.maxDepth);
|
||||
}
|
||||
|
||||
class FocusedStackRegistry {
|
||||
maxDepth: number;
|
||||
stackIdMap: Array<FlattenedStack>;
|
||||
|
||||
constructor(stackIdMap: Array<FlattenedStack>, maxDepth: number) {
|
||||
this.maxDepth = maxDepth;
|
||||
this.stackIdMap = stackIdMap;
|
||||
}
|
||||
|
||||
get(id: number): FlattenedStack {
|
||||
return this.stackIdMap[id];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
// @flow
|
||||
|
||||
import invariant from 'invariant';
|
||||
|
||||
import type { FrameGetter, FrameFormatter } from './AggrowExpander';
|
||||
import type { Stack } from './StackRegistry';
|
||||
import StackRegistry from './StackRegistry';
|
||||
import StringInterner from './StringInterner';
|
||||
|
||||
export type AggrowColumnDef =
|
||||
AggrowStringColumnDef |
|
||||
AggrowIntColumnDef |
|
||||
AggrowDoubleColumnDef |
|
||||
AggrowStackColumnDef;
|
||||
|
||||
type AggrowStringColumnDef = {
|
||||
type: 'string';
|
||||
name: string;
|
||||
strings: StringInterner;
|
||||
}
|
||||
|
||||
type AggrowIntColumnDef = {
|
||||
type: 'int';
|
||||
name: string;
|
||||
}
|
||||
|
||||
type AggrowDoubleColumnDef = {
|
||||
type: 'double';
|
||||
name: string;
|
||||
}
|
||||
|
||||
type AggrowStackColumnDef = {
|
||||
type: 'stack';
|
||||
name: string;
|
||||
stacks: StackRegistry,
|
||||
getter: FrameGetter,
|
||||
formatter: FrameFormatter,
|
||||
}
|
||||
|
||||
export interface AggrowColumn {
|
||||
name: string;
|
||||
get(row: number): number;
|
||||
insert(row: number, s: any): void;
|
||||
extend(count: number): void;
|
||||
}
|
||||
|
||||
class AggrowColumnBase {
|
||||
name: string;
|
||||
|
||||
constructor(def: AggrowColumnDef) {
|
||||
this.name = def.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class AggrowStringColumn extends AggrowColumnBase {
|
||||
strings: StringInterner;
|
||||
data: Int32Array = new Int32Array(0);
|
||||
|
||||
constructor(def: AggrowStringColumnDef) {
|
||||
super(def);
|
||||
this.strings = def.strings;
|
||||
}
|
||||
|
||||
get(row: number): number {
|
||||
return this.data[row];
|
||||
}
|
||||
|
||||
insert(row: number, s: string) {
|
||||
this.data[row] = this.strings.intern(s);
|
||||
}
|
||||
|
||||
extend(count: number) {
|
||||
const newData = new Int32Array(this.data.length + count);
|
||||
newData.set(this.data);
|
||||
this.data = newData;
|
||||
}
|
||||
}
|
||||
|
||||
export class AggrowIntColumn extends AggrowColumnBase {
|
||||
data: Int32Array = new Int32Array(0);
|
||||
|
||||
get(row: number): number {
|
||||
return this.data[row];
|
||||
}
|
||||
|
||||
insert(row: number, i: number) {
|
||||
this.data[row] = i;
|
||||
}
|
||||
|
||||
extend(count: number) {
|
||||
const newData = new Int32Array(this.data.length + count);
|
||||
newData.set(this.data);
|
||||
this.data = newData;
|
||||
}
|
||||
}
|
||||
|
||||
export class AggrowDoubleColumn extends AggrowColumnBase {
|
||||
data: Float64Array = new Float64Array(0);
|
||||
|
||||
get(row: number): number {
|
||||
return this.data[row];
|
||||
}
|
||||
|
||||
insert(row: number, d: number) {
|
||||
this.data[row] = d;
|
||||
}
|
||||
|
||||
extend(count: number) {
|
||||
const newData = new Float64Array(this.data.length + count);
|
||||
newData.set(this.data);
|
||||
this.data = newData;
|
||||
}
|
||||
}
|
||||
|
||||
export class AggrowStackColumn extends AggrowColumnBase {
|
||||
data: Int32Array = new Int32Array(0);
|
||||
stacks: StackRegistry;
|
||||
getter: FrameGetter;
|
||||
formatter: FrameFormatter;
|
||||
|
||||
constructor(def: AggrowStackColumnDef) {
|
||||
super(def);
|
||||
this.stacks = def.stacks;
|
||||
this.getter = def.getter;
|
||||
this.formatter = def.formatter;
|
||||
}
|
||||
|
||||
get(row: number): number {
|
||||
return this.data[row];
|
||||
}
|
||||
|
||||
insert(row: number, s: Stack) {
|
||||
this.data[row] = s.id;
|
||||
}
|
||||
|
||||
extend(count: number) {
|
||||
const newData = new Int32Array(this.data.length + count);
|
||||
newData.set(this.data);
|
||||
this.data = newData;
|
||||
}
|
||||
}
|
||||
|
||||
function newColumn(def: AggrowColumnDef): AggrowColumn {
|
||||
switch (def.type) {
|
||||
case 'string':
|
||||
return new AggrowStringColumn(def);
|
||||
case 'int':
|
||||
return new AggrowIntColumn(def);
|
||||
case 'double':
|
||||
return new AggrowDoubleColumn(def);
|
||||
case 'stack':
|
||||
return new AggrowStackColumn(def);
|
||||
default:
|
||||
throw new Error(`Unknown column type: ${def.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class AggrowData {
|
||||
columns: Array<AggrowColumn>;
|
||||
rowCount = 0;
|
||||
|
||||
constructor(columnDefs: Array<AggrowColumnDef>) {
|
||||
this.columns = columnDefs.map(newColumn);
|
||||
}
|
||||
|
||||
rowInserter(numRows: number): RowInserter {
|
||||
const columns = this.columns;
|
||||
columns.forEach((c: AggrowColumn): void => c.extend(numRows));
|
||||
const currRow = this.rowCount;
|
||||
const endRow = currRow + numRows;
|
||||
this.rowCount = endRow;
|
||||
|
||||
return new RowInserter(columns, { currRow, endRow });
|
||||
}
|
||||
|
||||
getColumn(name: string): ?AggrowColumn {
|
||||
return this.columns.find((c: AggrowColumn): boolean => c.name === name);
|
||||
}
|
||||
|
||||
flattenStacks() {
|
||||
this.columns.forEach((c: AggrowColumn) => {
|
||||
if (c instanceof AggrowStackColumn) {
|
||||
c.stacks.flatten();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RowInserter {
|
||||
columns: Array<AggrowColumn>;
|
||||
currRow: number;
|
||||
endRow: number;
|
||||
|
||||
constructor(
|
||||
columns: Array<AggrowColumn>,
|
||||
params: { currRow: number, endRow: number }) {
|
||||
this.columns = columns;
|
||||
this.currRow = params.currRow;
|
||||
this.endRow = params.endRow;
|
||||
}
|
||||
|
||||
insertRow(...args: Array<number | string | Stack>) {
|
||||
invariant(this.currRow < this.endRow, 'Tried to insert data off end of added range!');
|
||||
invariant(
|
||||
args.length === this.columns.length,
|
||||
`Expected data for ${this.columns.length} columns, got ${args.length} columns`);
|
||||
|
||||
args.forEach((arg: number | string | Stack, i: number): void =>
|
||||
this.columns[i].insert(this.currRow, arg));
|
||||
this.currRow += 1;
|
||||
}
|
||||
|
||||
done() {
|
||||
invariant(this.currRow === this.endRow, 'Unfilled rows!');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,694 @@
|
|||
// @flow
|
||||
import invariant from 'invariant';
|
||||
|
||||
import type { FlattenedStack } from './StackRegistry';
|
||||
|
||||
// expander ID definitions
|
||||
const FIELD_EXPANDER_ID_MIN = 0x0000;
|
||||
const FIELD_EXPANDER_ID_MAX = 0x7fff;
|
||||
const STACK_EXPANDER_ID_MIN = 0x8000;
|
||||
const STACK_EXPANDER_ID_MAX = 0xffff;
|
||||
|
||||
// used for row.expander which reference state.activeExpanders (with frame index masked in)
|
||||
const INVALID_ACTIVE_EXPANDER = -1;
|
||||
const ACTIVE_EXPANDER_MASK = 0xffff;
|
||||
const ACTIVE_EXPANDER_FRAME_SHIFT = 16;
|
||||
|
||||
// aggregator ID definitions
|
||||
const AGGREGATOR_ID_MAX = 0xffff;
|
||||
|
||||
// active aggragators can have sort order changed in the reference
|
||||
const ACTIVE_AGGREGATOR_MASK = 0xffff;
|
||||
const ACTIVE_AGGREGATOR_ASC_BIT = 0x10000;
|
||||
|
||||
// tree node state definitions
|
||||
const NODE_EXPANDED_BIT = 0x0001; // this row is expanded
|
||||
const NODE_REAGGREGATE_BIT = 0x0002; // children need aggregates
|
||||
const NODE_REORDER_BIT = 0x0004; // children need to be sorted
|
||||
const NODE_REPOSITION_BIT = 0x0008; // children need position
|
||||
const NODE_INDENT_SHIFT = 16;
|
||||
|
||||
function _calleeFrameIdGetter(stack: FlattenedStack, depth: number): number {
|
||||
return stack[depth];
|
||||
}
|
||||
|
||||
function _callerFrameIdGetter(stack: FlattenedStack, depth: number): number {
|
||||
return stack[stack.length - depth - 1];
|
||||
}
|
||||
|
||||
function _createStackComparers(
|
||||
stackGetter: StackGetter,
|
||||
frameIdGetter: FrameIdGetter,
|
||||
maxStackDepth: number): Array<Comparer<number>> {
|
||||
const comparers = new Array(maxStackDepth);
|
||||
for (let depth = 0; depth < maxStackDepth; depth++) {
|
||||
const captureDepth = depth; // NB: to capture depth per loop iteration
|
||||
comparers[depth] = function calleeStackComparer(rowA: number, rowB: number): number {
|
||||
const a = stackGetter(rowA);
|
||||
const b = stackGetter(rowB);
|
||||
// NB: we put the stacks that are too short at the top,
|
||||
// so they can be grouped into the '<exclusive>' bucket
|
||||
if (a.length <= captureDepth && b.length <= captureDepth) {
|
||||
return 0;
|
||||
} else if (a.length <= captureDepth) {
|
||||
return -1;
|
||||
} else if (b.length <= captureDepth) {
|
||||
return 1;
|
||||
}
|
||||
return frameIdGetter(a, captureDepth) - frameIdGetter(b, captureDepth);
|
||||
};
|
||||
}
|
||||
return comparers;
|
||||
}
|
||||
|
||||
function _createTreeNode(
|
||||
parent: Row | null,
|
||||
label: string,
|
||||
indices: Int32Array,
|
||||
expander: number): Row {
|
||||
const indent = parent === null ? 0 : (parent.state >>> NODE_INDENT_SHIFT) + 1; // eslint-disable-line no-bitwise, max-len
|
||||
const state = NODE_REPOSITION_BIT | // eslint-disable-line no-bitwise
|
||||
NODE_REAGGREGATE_BIT |
|
||||
NODE_REORDER_BIT |
|
||||
(indent << NODE_INDENT_SHIFT); // eslint-disable-line no-bitwise
|
||||
return {
|
||||
parent, // null if root
|
||||
children: null, // array of children nodes
|
||||
label, // string to show in UI
|
||||
indices, // row indices under this node
|
||||
aggregates: null, // result of aggregate on indices
|
||||
expander, // index into state.activeExpanders
|
||||
top: 0, // y position of top row (in rows)
|
||||
height: 1, // number of rows including children
|
||||
state, // see NODE_* definitions above
|
||||
};
|
||||
}
|
||||
|
||||
const NO_SORT_ORDER: Comparer<*> = (): number => 0;
|
||||
|
||||
type Comparer<T> = (a: T, b: T) => number;
|
||||
|
||||
type Aggregator = {
|
||||
name: string, // name for column
|
||||
aggregator: (indexes: Int32Array) => number, // index array -> aggregate value
|
||||
formatter: (value: number) => string, // aggregate value -> display string
|
||||
sorter: Comparer<number>, // compare two aggregate values
|
||||
}
|
||||
|
||||
type FieldExpander = {
|
||||
name: string,
|
||||
comparer: Comparer<number>,
|
||||
getter: (rowIndex: number) => any,
|
||||
formatter: (value: any) => string,
|
||||
}
|
||||
|
||||
type StackGetter = (rowIndex: number) => FlattenedStack; // (row) => [frameId int]
|
||||
type FrameIdGetter = (stack: FlattenedStack, depth: number) => number; // (stack,depth) -> frame id
|
||||
export type FrameGetter = (id: number) => any; // (frameId int) => frame obj
|
||||
export type FrameFormatter = (frame: any) => string; // (frame obj) => display string
|
||||
|
||||
type StackExpander = {
|
||||
name: string, // display name of expander
|
||||
comparers: Array<Comparer<number>>, // depth -> comparer
|
||||
stackGetter: StackGetter,
|
||||
frameIdGetter: FrameIdGetter,
|
||||
frameGetter: FrameGetter,
|
||||
frameFormatter: FrameFormatter,
|
||||
}
|
||||
|
||||
export type Row = {
|
||||
top: number,
|
||||
height: number,
|
||||
state: number,
|
||||
parent: Row | null,
|
||||
indices: Int32Array,
|
||||
aggregates: Array<number> | null,
|
||||
children: Array<Row> | null,
|
||||
expander: number,
|
||||
label: string,
|
||||
}
|
||||
|
||||
type State = {
|
||||
fieldExpanders: Array<FieldExpander>, // tree expanders that expand on simple values
|
||||
stackExpanders: Array<StackExpander>, // tree expanders that expand stacks
|
||||
activeExpanders: Array<number>, // index into field or stack expanders, hierarchy of tree
|
||||
aggregators: Array<Aggregator>, // all available aggregators, might not be used
|
||||
activeAggregators: Array<number>, // index into aggregators, to actually compute
|
||||
sorter: Comparer<*>,
|
||||
root: Row,
|
||||
}
|
||||
|
||||
export default class AggrowExpander { // eslint-disable-line no-unused-vars
|
||||
indices: Int32Array;
|
||||
state: State;
|
||||
|
||||
constructor(numRows: number) {
|
||||
this.indices = new Int32Array(numRows);
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
this.indices[i] = i;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
fieldExpanders: [],
|
||||
stackExpanders: [],
|
||||
activeExpanders: [],
|
||||
aggregators: [],
|
||||
activeAggregators: [],
|
||||
sorter: NO_SORT_ORDER,
|
||||
root: _createTreeNode(null, '<root>', this.indices, INVALID_ACTIVE_EXPANDER),
|
||||
};
|
||||
}
|
||||
|
||||
_evaluateAggregate(row: Row) {
|
||||
const activeAggregators = this.state.activeAggregators;
|
||||
const aggregates = new Array(activeAggregators.length);
|
||||
for (let j = 0; j < activeAggregators.length; j++) {
|
||||
const aggregator = this.state.aggregators[activeAggregators[j]];
|
||||
aggregates[j] = aggregator.aggregator(row.indices);
|
||||
}
|
||||
row.aggregates = aggregates; // eslint-disable-line no-param-reassign
|
||||
row.state |= NODE_REAGGREGATE_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
|
||||
_evaluateAggregates(row: Row) {
|
||||
if ((row.state & NODE_EXPANDED_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
const children = row.children;
|
||||
invariant(children, 'Expected non-null children');
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
this._evaluateAggregate(children[i]);
|
||||
}
|
||||
row.state |= NODE_REORDER_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
row.state ^= NODE_REAGGREGATE_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
|
||||
_evaluateOrder(row: Row) {
|
||||
if ((row.state & NODE_EXPANDED_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
const children = row.children;
|
||||
invariant(children, 'Expected non-null children');
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
child.state |= NODE_REORDER_BIT; // eslint-disable-line no-bitwise
|
||||
}
|
||||
children.sort(this.state.sorter);
|
||||
row.state |= NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
row.state ^= NODE_REORDER_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
|
||||
_evaluatePosition(row: Row) { // eslint-disable-line class-methods-use-this
|
||||
if ((row.state & NODE_EXPANDED_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
const children = row.children;
|
||||
invariant(children, 'Expected a children array');
|
||||
let childTop = row.top + 1;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
if (child.top !== childTop) {
|
||||
child.top = childTop;
|
||||
child.state |= NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise
|
||||
}
|
||||
childTop += child.height;
|
||||
}
|
||||
}
|
||||
row.state ^= NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
|
||||
_getRowsImpl(row: Row, top: number, height: number, result: Array<Row | null>) {
|
||||
if ((row.state & NODE_REAGGREGATE_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._evaluateAggregates(row);
|
||||
}
|
||||
if ((row.state & NODE_REORDER_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._evaluateOrder(row);
|
||||
}
|
||||
if ((row.state & NODE_REPOSITION_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._evaluatePosition(row);
|
||||
}
|
||||
|
||||
if (row.top >= top && row.top < top + height) {
|
||||
invariant(
|
||||
result[row.top - top] === null,
|
||||
`getRows put more than one row at position ${row.top} into result`);
|
||||
result[row.top - top] = row; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
if ((row.state & NODE_EXPANDED_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
const children = row.children;
|
||||
invariant(children, 'Expected non-null children');
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
if (child.top < top + height && top < child.top + child.height) {
|
||||
this._getRowsImpl(child, top, height, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_updateHeight(row: Row | null, heightChange: number) { // eslint-disable-line class-methods-use-this, max-len
|
||||
while (row !== null) {
|
||||
row.height += heightChange; // eslint-disable-line no-param-reassign
|
||||
row.state |= NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
row = row.parent; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
}
|
||||
|
||||
_addChildrenWithFieldExpander(row: Row, expander: FieldExpander, nextActiveIndex: number) { // eslint-disable-line class-methods-use-this, max-len
|
||||
const rowIndices = row.indices;
|
||||
const comparer = expander.comparer;
|
||||
const formatter = expander.formatter;
|
||||
const getter = expander.getter;
|
||||
rowIndices.sort(comparer);
|
||||
let begin = 0;
|
||||
let end = 1;
|
||||
row.children = []; // eslint-disable-line no-param-reassign
|
||||
while (end < rowIndices.length) {
|
||||
if (comparer(rowIndices[begin], rowIndices[end]) !== 0) {
|
||||
invariant(row.children, 'Expected a children array');
|
||||
row.children.push(_createTreeNode(
|
||||
row,
|
||||
`${expander.name}: ${formatter(getter(rowIndices[begin]))}`,
|
||||
rowIndices.subarray(begin, end),
|
||||
nextActiveIndex));
|
||||
begin = end;
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
row.children.push(_createTreeNode(
|
||||
row,
|
||||
`${expander.name}: ${formatter(getter(rowIndices[begin]))}`,
|
||||
rowIndices.subarray(begin, end),
|
||||
nextActiveIndex));
|
||||
}
|
||||
|
||||
_addChildrenWithStackExpander( // eslint-disable-line class-methods-use-this
|
||||
row: Row,
|
||||
expander: StackExpander,
|
||||
activeIndex: number,
|
||||
depth: number,
|
||||
nextActiveIndex: number) {
|
||||
const rowIndices = row.indices;
|
||||
const stackGetter = expander.stackGetter;
|
||||
const frameIdGetter = expander.frameIdGetter;
|
||||
const frameGetter = expander.frameGetter;
|
||||
const frameFormatter = expander.frameFormatter;
|
||||
const comparer = expander.comparers[depth];
|
||||
const expandNextFrame = activeIndex | ((depth + 1) << ACTIVE_EXPANDER_FRAME_SHIFT); // eslint-disable-line no-bitwise, max-len
|
||||
rowIndices.sort(comparer);
|
||||
let columnName = '';
|
||||
if (depth === 0) {
|
||||
columnName = `${expander.name}: `;
|
||||
}
|
||||
|
||||
// put all the too-short stacks under <exclusive>
|
||||
let begin = 0;
|
||||
let beginStack = null;
|
||||
row.children = []; // eslint-disable-line no-param-reassign
|
||||
while (begin < rowIndices.length) {
|
||||
beginStack = stackGetter(rowIndices[begin]);
|
||||
if (beginStack.length > depth) {
|
||||
break;
|
||||
}
|
||||
begin += 1;
|
||||
}
|
||||
invariant(beginStack !== null, 'Expected beginStack at this point');
|
||||
if (begin > 0) {
|
||||
row.children.push(_createTreeNode(
|
||||
row,
|
||||
`${columnName}<exclusive>`,
|
||||
rowIndices.subarray(0, begin),
|
||||
nextActiveIndex));
|
||||
}
|
||||
// aggregate the rest under frames
|
||||
if (begin < rowIndices.length) {
|
||||
let end = begin + 1;
|
||||
while (end < rowIndices.length) {
|
||||
const endStack = stackGetter(rowIndices[end]);
|
||||
if (frameIdGetter(beginStack, depth) !== frameIdGetter(endStack, depth)) {
|
||||
invariant(row.children, 'Expected a children array');
|
||||
row.children.push(_createTreeNode(
|
||||
row,
|
||||
columnName + frameFormatter(frameGetter(frameIdGetter(beginStack, depth))),
|
||||
rowIndices.subarray(begin, end),
|
||||
expandNextFrame));
|
||||
begin = end;
|
||||
beginStack = endStack;
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
row.children.push(_createTreeNode(
|
||||
row,
|
||||
columnName + frameFormatter(frameGetter(frameIdGetter(beginStack, depth))),
|
||||
rowIndices.subarray(begin, end),
|
||||
expandNextFrame));
|
||||
}
|
||||
}
|
||||
|
||||
_contractRow(row: Row) {
|
||||
invariant(
|
||||
(row.state & NODE_EXPANDED_BIT) !== 0, // eslint-disable-line no-bitwise
|
||||
'Cannot contract row; already contracted!');
|
||||
row.state ^= NODE_EXPANDED_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
const heightChange = 1 - row.height;
|
||||
this._updateHeight(row, heightChange);
|
||||
}
|
||||
|
||||
_pruneExpanders(row: Row, oldExpander: number, newExpander: number) {
|
||||
row.state |= NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
if (row.expander === oldExpander) {
|
||||
row.state |= NODE_REAGGREGATE_BIT | NODE_REORDER_BIT | NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign, max-len
|
||||
if ((row.state & NODE_EXPANDED_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._contractRow(row);
|
||||
}
|
||||
row.children = null; // eslint-disable-line no-param-reassign
|
||||
row.expander = newExpander; // eslint-disable-line no-param-reassign
|
||||
} else {
|
||||
row.state |= NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
const children = row.children;
|
||||
if (children != null) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
this._pruneExpanders(child, oldExpander, newExpander);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addFieldExpander(
|
||||
name: string,
|
||||
comparer: Comparer<number>,
|
||||
getter: (rowIndex: number) => any,
|
||||
formatter: (value: any) => string): number {
|
||||
invariant(
|
||||
FIELD_EXPANDER_ID_MIN + this.state.fieldExpanders.length < FIELD_EXPANDER_ID_MAX,
|
||||
'too many field expanders!');
|
||||
this.state.fieldExpanders.push({ name, comparer, getter, formatter });
|
||||
return FIELD_EXPANDER_ID_MIN + this.state.fieldExpanders.length - 1;
|
||||
}
|
||||
|
||||
addStackExpander(
|
||||
name: string,
|
||||
maxStackDepth: number,
|
||||
stackGetter: StackGetter,
|
||||
frameGetter: FrameGetter,
|
||||
frameFormatter: FrameFormatter,
|
||||
reverse: boolean): number {
|
||||
invariant(
|
||||
STACK_EXPANDER_ID_MIN + this.state.fieldExpanders.length < STACK_EXPANDER_ID_MAX,
|
||||
'Too many stack expanders!');
|
||||
const idGetter = reverse ? _callerFrameIdGetter : _calleeFrameIdGetter;
|
||||
this.state.stackExpanders.push({
|
||||
name,
|
||||
stackGetter,
|
||||
comparers: _createStackComparers(stackGetter, idGetter, maxStackDepth),
|
||||
frameIdGetter: idGetter,
|
||||
frameGetter,
|
||||
frameFormatter,
|
||||
});
|
||||
return STACK_EXPANDER_ID_MIN + this.state.stackExpanders.length - 1;
|
||||
}
|
||||
|
||||
getExpanders(): Array<number> {
|
||||
const expanders = [];
|
||||
for (let i = 0; i < this.state.fieldExpanders.length; i++) {
|
||||
expanders.push(FIELD_EXPANDER_ID_MIN + i);
|
||||
}
|
||||
for (let i = 0; i < this.state.stackExpanders.length; i++) {
|
||||
expanders.push(STACK_EXPANDER_ID_MIN + i);
|
||||
}
|
||||
return expanders;
|
||||
}
|
||||
|
||||
getExpanderName(id: number): string {
|
||||
if (id >= FIELD_EXPANDER_ID_MIN && id <= FIELD_EXPANDER_ID_MAX) {
|
||||
return this.state.fieldExpanders[id - FIELD_EXPANDER_ID_MIN].name;
|
||||
} else if (id >= STACK_EXPANDER_ID_MIN && id <= STACK_EXPANDER_ID_MAX) {
|
||||
return this.state.stackExpanders[id - STACK_EXPANDER_ID_MIN].name;
|
||||
}
|
||||
throw new Error(`Unknown expander ID ${id.toString()}`);
|
||||
}
|
||||
|
||||
setActiveExpanders(ids: Array<number>) {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
if (id >= FIELD_EXPANDER_ID_MIN && id <= FIELD_EXPANDER_ID_MAX) {
|
||||
invariant(
|
||||
id - FIELD_EXPANDER_ID_MIN < this.state.fieldExpanders.length,
|
||||
`field expander for id ${id.toString()} does not exist!`);
|
||||
} else if (id >= STACK_EXPANDER_ID_MIN && id <= STACK_EXPANDER_ID_MAX) {
|
||||
invariant(id - STACK_EXPANDER_ID_MIN < this.state.stackExpanders.length,
|
||||
`stack expander for id ${id.toString()} does not exist!`);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
if (this.state.activeExpanders.length <= i) {
|
||||
this._pruneExpanders(this.state.root, INVALID_ACTIVE_EXPANDER, i);
|
||||
break;
|
||||
} else if (ids[i] !== this.state.activeExpanders[i]) {
|
||||
this._pruneExpanders(this.state.root, i, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: if ids is prefix of activeExpanders, we need to make an expander invalid
|
||||
this.state.activeExpanders = ids.slice();
|
||||
}
|
||||
|
||||
getActiveExpanders(): Array<number> {
|
||||
return this.state.activeExpanders.slice();
|
||||
}
|
||||
|
||||
addAggregator(
|
||||
name: string,
|
||||
aggregator: (indexes: Int32Array) => number,
|
||||
formatter: (value: number) => string,
|
||||
sorter: Comparer<number>): number {
|
||||
invariant(this.state.aggregators.length < AGGREGATOR_ID_MAX, 'too many aggregators!');
|
||||
this.state.aggregators.push({ name, aggregator, formatter, sorter });
|
||||
return this.state.aggregators.length - 1;
|
||||
}
|
||||
|
||||
getAggregators(): Array<number> {
|
||||
const aggregators = [];
|
||||
for (let i = 0; i < this.state.aggregators.length; i++) {
|
||||
aggregators.push(i);
|
||||
}
|
||||
return aggregators;
|
||||
}
|
||||
|
||||
getAggregatorName(id: number): string {
|
||||
return this.state.aggregators[id & ACTIVE_AGGREGATOR_MASK].name; // eslint-disable-line no-bitwise, max-len
|
||||
}
|
||||
|
||||
setActiveAggregators(ids: Array<number>) {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i] & ACTIVE_AGGREGATOR_MASK; // eslint-disable-line no-bitwise
|
||||
invariant(
|
||||
id >= 0 && id < this.state.aggregators.length,
|
||||
`aggregator id ${id.toString()} not valid`);
|
||||
}
|
||||
this.state.activeAggregators = ids.slice();
|
||||
// NB: evaluate root here because dirty bit is for children
|
||||
// so someone has to start with root, and it might as well be right away
|
||||
this._evaluateAggregate(this.state.root);
|
||||
let sorter = NO_SORT_ORDER;
|
||||
for (let i = ids.length - 1; i >= 0; i--) {
|
||||
const ascending = (ids[i] & ACTIVE_AGGREGATOR_ASC_BIT) !== 0; // eslint-disable-line no-bitwise, max-len
|
||||
const id = ids[i] & ACTIVE_AGGREGATOR_MASK; // eslint-disable-line no-bitwise
|
||||
const comparer = this.state.aggregators[id].sorter;
|
||||
const captureSorter = sorter;
|
||||
const captureIndex = i;
|
||||
sorter = (a: Row, b: Row): number => {
|
||||
invariant(a.aggregates && b.aggregates, 'Expected aggregates.');
|
||||
const c = comparer(a.aggregates[captureIndex], b.aggregates[captureIndex]);
|
||||
if (c === 0) {
|
||||
return captureSorter(a, b);
|
||||
}
|
||||
return ascending ? -c : c;
|
||||
};
|
||||
}
|
||||
this.state.sorter = sorter; // eslint-disable-line no-param-reassign
|
||||
this.state.root.state |= NODE_REORDER_BIT; // eslint-disable-line no-bitwise, no-param-reassign
|
||||
}
|
||||
|
||||
getActiveAggregators(): Array<number> {
|
||||
return this.state.activeAggregators.slice();
|
||||
}
|
||||
|
||||
getRows(top: number, height: number): Array<Row | null> {
|
||||
const result = new Array(height);
|
||||
for (let i = 0; i < height; i++) {
|
||||
result[i] = null;
|
||||
}
|
||||
this._getRowsImpl(this.state.root, top, height, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_findRowImpl(fromRow: number, predicate: (row: Row) => boolean, row: Row): number {
|
||||
if (row.top > fromRow && predicate(row)) {
|
||||
return row.top; // this row is a match!
|
||||
}
|
||||
|
||||
// remember how to clean up after ourselves so we only expand as little as possible
|
||||
const contractChildren = this.canExpand(row);
|
||||
const cleanUpChildren = row.children === null;
|
||||
if (contractChildren) {
|
||||
this.expand(row);
|
||||
}
|
||||
|
||||
// evaluate position so we search in the correct order
|
||||
if ((row.state & NODE_REAGGREGATE_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._evaluateAggregates(row);
|
||||
}
|
||||
if ((row.state & NODE_REORDER_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._evaluateOrder(row);
|
||||
}
|
||||
if ((row.state & NODE_REPOSITION_BIT) !== 0) { // eslint-disable-line no-bitwise
|
||||
this._evaluatePosition(row);
|
||||
}
|
||||
// TODO: encapsulate row state management somewhere so logic can be shared with _getRowsImpl
|
||||
|
||||
// search in children
|
||||
const children = row.children;
|
||||
if (children !== null) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
if (child.top + child.height > fromRow) {
|
||||
const find = this._findRowImpl(fromRow, predicate, child);
|
||||
if (find >= 0) {
|
||||
return find;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// clean up to leave the tree how it was if we didn't find anything
|
||||
// this also saves memory
|
||||
if (contractChildren) {
|
||||
this.contract(row);
|
||||
}
|
||||
if (cleanUpChildren) {
|
||||
row.children = null;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// findRow - find the first row that matches a predicate
|
||||
// parameters
|
||||
// predicate: returns true when row is found
|
||||
// fromRow: start search from after this row index (negative for start at beginning)
|
||||
// returns: index of first row that matches, -1 if no match found
|
||||
findRow(predicate: (row: Row) => boolean, fromRow: ?number): number {
|
||||
return this._findRowImpl(!fromRow ? -1 : fromRow, predicate, this.state.root);
|
||||
}
|
||||
|
||||
getRowLabel(row: Row): string { // eslint-disable-line class-methods-use-this
|
||||
return row.label;
|
||||
}
|
||||
|
||||
getRowIndent(row: Row): number { // eslint-disable-line class-methods-use-this
|
||||
return row.state >>> NODE_INDENT_SHIFT; // eslint-disable-line no-bitwise
|
||||
}
|
||||
|
||||
getRowExpanderIndex(row: Row): number { // eslint-disable-line class-methods-use-this
|
||||
if (row.parent) {
|
||||
return row.parent.expander & ACTIVE_EXPANDER_MASK; // eslint-disable-line no-bitwise
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
getRowExpansionPath(row: Row | null): Array<any> {
|
||||
const path = [];
|
||||
invariant(row, 'Expected non-null row here');
|
||||
const index = row.indices[0];
|
||||
row = row.parent; // eslint-disable-line no-param-reassign
|
||||
while (row) {
|
||||
const exIndex = row.expander & ACTIVE_EXPANDER_MASK; // eslint-disable-line no-bitwise
|
||||
const exId = this.state.activeExpanders[exIndex];
|
||||
if (exId >= FIELD_EXPANDER_ID_MIN &&
|
||||
exId < FIELD_EXPANDER_ID_MIN + this.state.fieldExpanders.length) {
|
||||
const expander = this.state.fieldExpanders[exId - FIELD_EXPANDER_ID_MIN]; // eslint-disable-line no-bitwise, max-len
|
||||
path.push(expander.getter(index));
|
||||
row = row.parent; // eslint-disable-line no-param-reassign
|
||||
} else if (exId >= STACK_EXPANDER_ID_MIN &&
|
||||
exId < STACK_EXPANDER_ID_MIN + this.state.stackExpanders.length) {
|
||||
const expander = this.state.stackExpanders[exId - STACK_EXPANDER_ID_MIN];
|
||||
const stackGetter = expander.stackGetter;
|
||||
const frameIdGetter = expander.frameIdGetter;
|
||||
const frameGetter = expander.frameGetter;
|
||||
const stack = [];
|
||||
while (row && (row.expander & ACTIVE_EXPANDER_MASK) === exIndex) { // eslint-disable-line no-bitwise, max-len
|
||||
const depth = row.expander >>> ACTIVE_EXPANDER_FRAME_SHIFT; // eslint-disable-line no-bitwise, max-len
|
||||
const rowStack = stackGetter(index);
|
||||
if (depth >= rowStack.length) {
|
||||
stack.push('<exclusive>');
|
||||
} else {
|
||||
stack.push(frameGetter(frameIdGetter(rowStack, depth)));
|
||||
}
|
||||
row = row.parent; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
path.push(stack.reverse());
|
||||
}
|
||||
}
|
||||
return path.reverse();
|
||||
}
|
||||
|
||||
getRowAggregate(row: Row, index: number): string {
|
||||
const aggregator = this.state.aggregators[this.state.activeAggregators[index]];
|
||||
invariant(row.aggregates, 'Expected aggregates');
|
||||
return aggregator.formatter(row.aggregates[index]);
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this.state.root.height;
|
||||
}
|
||||
|
||||
canExpand(row: Row): boolean { // eslint-disable-line class-methods-use-this
|
||||
return (row.state & NODE_EXPANDED_BIT) === 0 && (row.expander !== INVALID_ACTIVE_EXPANDER); // eslint-disable-line no-bitwise, max-len
|
||||
}
|
||||
|
||||
canContract(row: Row): boolean { // eslint-disable-line class-methods-use-this
|
||||
return (row.state & NODE_EXPANDED_BIT) !== 0; // eslint-disable-line no-bitwise
|
||||
}
|
||||
|
||||
expand(row: Row) {
|
||||
invariant(
|
||||
(row.state & NODE_EXPANDED_BIT) === 0, // eslint-disable-line no-bitwise
|
||||
'can not expand row, already expanded');
|
||||
invariant(row.height === 1, `unexpanded row has height ${row.height.toString()} != 1`);
|
||||
if (row.children === null) { // first expand, generate children
|
||||
const activeIndex = row.expander & ACTIVE_EXPANDER_MASK; // eslint-disable-line no-bitwise
|
||||
let nextActiveIndex = activeIndex + 1; // NB: if next is stack, frame is 0
|
||||
if (nextActiveIndex >= this.state.activeExpanders.length) {
|
||||
nextActiveIndex = INVALID_ACTIVE_EXPANDER;
|
||||
}
|
||||
invariant(
|
||||
activeIndex < this.state.activeExpanders.length,
|
||||
`invalid active expander index ${activeIndex.toString()}`);
|
||||
const exId = this.state.activeExpanders[activeIndex];
|
||||
if (exId >= FIELD_EXPANDER_ID_MIN &&
|
||||
exId < FIELD_EXPANDER_ID_MIN + this.state.fieldExpanders.length) {
|
||||
const expander = this.state.fieldExpanders[exId - FIELD_EXPANDER_ID_MIN];
|
||||
this._addChildrenWithFieldExpander(row, expander, nextActiveIndex);
|
||||
} else if (exId >= STACK_EXPANDER_ID_MIN &&
|
||||
exId < STACK_EXPANDER_ID_MIN + this.state.stackExpanders.length) {
|
||||
const depth = row.expander >>> ACTIVE_EXPANDER_FRAME_SHIFT; // eslint-disable-line no-bitwise, max-len
|
||||
const expander = this.state.stackExpanders[exId - STACK_EXPANDER_ID_MIN];
|
||||
this._addChildrenWithStackExpander(row, expander, activeIndex, depth, nextActiveIndex);
|
||||
} else {
|
||||
throw new Error(`state.activeIndex ${activeIndex} has invalid expander${exId}`);
|
||||
}
|
||||
}
|
||||
row.state |= NODE_EXPANDED_BIT | NODE_REAGGREGATE_BIT | NODE_REORDER_BIT | NODE_REPOSITION_BIT; // eslint-disable-line no-bitwise, no-param-reassign, max-len
|
||||
let heightChange = 0;
|
||||
invariant(row.children, 'Expected a children array');
|
||||
for (let i = 0; i < row.children.length; i++) {
|
||||
heightChange += row.children[i].height;
|
||||
}
|
||||
this._updateHeight(row, heightChange);
|
||||
// if children only contains one node, then expand it as well
|
||||
invariant(row.children, 'Expected a children array');
|
||||
if (row.children.length === 1 && this.canExpand(row.children[0])) {
|
||||
this.expand(row.children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
contract(row: Row) {
|
||||
this._contractRow(row);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,439 @@
|
|||
// @flow
|
||||
|
||||
import invariant from 'invariant';
|
||||
import React from 'react';
|
||||
|
||||
import Aggrow from './Aggrow';
|
||||
import type { Row } from './AggrowExpander';
|
||||
import TableConfiguration from './TableConfiguration';
|
||||
import TableHeader from './TableHeader';
|
||||
|
||||
const rowHeight = 20;
|
||||
const treeIndent = 16;
|
||||
|
||||
type Props = {
|
||||
aggrow: Aggrow,
|
||||
enableConfigurationPane: boolean,
|
||||
onSelectionChange?: (row: Row) => void,
|
||||
}
|
||||
|
||||
type State = {
|
||||
aggrow: Aggrow,
|
||||
viewport: {
|
||||
top: number,
|
||||
height: number,
|
||||
},
|
||||
cursor: number,
|
||||
searchValue: string,
|
||||
}
|
||||
|
||||
export default class AggrowTable extends React.Component {
|
||||
static defaultProps = {
|
||||
enableConfigurationPane: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
aggrow: props.aggrow,
|
||||
viewport: { top: 0, height: 100 },
|
||||
cursor: 0,
|
||||
searchValue: '',
|
||||
};
|
||||
}
|
||||
|
||||
props: Props;
|
||||
|
||||
state: State;
|
||||
|
||||
componentDidMount() {
|
||||
document.body.addEventListener('keydown', this.keydown);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.aggrow !== nextProps.aggrow) {
|
||||
this.setState({
|
||||
aggrow: nextProps.aggrow,
|
||||
viewport: { top: 0, height: 100 },
|
||||
cursor: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.body.removeEventListener('keydown', this.keydown);
|
||||
}
|
||||
|
||||
scroll = (e: SyntheticUIEvent) => {
|
||||
const viewport = e.target;
|
||||
invariant(viewport instanceof HTMLElement, 'Expected an HTML element');
|
||||
const top = Math.floor((viewport.scrollTop - (viewport.clientHeight * 1.0)) / rowHeight);
|
||||
const height = Math.ceil(viewport.clientHeight * 3.0 / rowHeight);
|
||||
if (top !== this.state.viewport.top || height !== this.state.viewport.height) {
|
||||
this.setState({ viewport: { top, height } });
|
||||
}
|
||||
}
|
||||
|
||||
_updateCursor(position: number) {
|
||||
this.setState({ cursor: position });
|
||||
const onSelectionChange = this.props.onSelectionChange;
|
||||
if (onSelectionChange) {
|
||||
const row = this.state.aggrow.expander.getRows(position, 1)[0];
|
||||
invariant(row, 'Expected a row');
|
||||
onSelectionChange(row);
|
||||
}
|
||||
}
|
||||
|
||||
_contractRow(row: Row) {
|
||||
let newCursor = this.state.cursor;
|
||||
if (newCursor > row.top && newCursor < row.top + row.height) { // in contracted section
|
||||
newCursor = row.top;
|
||||
} else if (newCursor >= row.top + row.height) { // below contracted section
|
||||
newCursor -= row.height - 1;
|
||||
}
|
||||
this.state.aggrow.expander.contract(row);
|
||||
this._updateCursor(newCursor);
|
||||
}
|
||||
|
||||
_expandRow(row: Row) {
|
||||
let newCursor = this.state.cursor;
|
||||
this.state.aggrow.expander.expand(row);
|
||||
if (newCursor > row.top) { // below expanded section
|
||||
newCursor += row.height - 1;
|
||||
}
|
||||
this._updateCursor(newCursor);
|
||||
}
|
||||
|
||||
_scrollDiv: ?HTMLDivElement = null;
|
||||
|
||||
_setScrollDiv = (div: ?HTMLDivElement) => {
|
||||
this._scrollDiv = div;
|
||||
}
|
||||
|
||||
_keepCursorInViewport() {
|
||||
if (this._scrollDiv) {
|
||||
const cursor = this.state.cursor;
|
||||
const scrollDiv = this._scrollDiv;
|
||||
if (cursor * rowHeight < scrollDiv.scrollTop + (scrollDiv.clientHeight * 0.1)) {
|
||||
scrollDiv.scrollTop = (cursor * rowHeight) - (scrollDiv.clientHeight * 0.1);
|
||||
} else if ((cursor + 1) * rowHeight > scrollDiv.scrollTop + (scrollDiv.clientHeight * 0.9)) {
|
||||
scrollDiv.scrollTop = ((cursor + 1) * rowHeight) - (scrollDiv.clientHeight * 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keydown = (e: KeyboardEvent) => {
|
||||
const expander = this.state.aggrow.expander;
|
||||
let cursor = this.state.cursor;
|
||||
let row = expander.getRows(cursor, 1)[0];
|
||||
invariant(row, 'Expected a row');
|
||||
switch (e.keyCode) {
|
||||
case 38: // up
|
||||
if (cursor > 0) {
|
||||
this._updateCursor(cursor - 1);
|
||||
this._keepCursorInViewport();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 40: // down
|
||||
if (cursor < expander.getHeight() - 1) {
|
||||
this._updateCursor(cursor + 1);
|
||||
this._keepCursorInViewport();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 37: // left
|
||||
if (expander.canContract(row)) {
|
||||
this._contractRow(row);
|
||||
} else if (expander.getRowIndent(row) > 0) {
|
||||
const indent = expander.getRowIndent(row) - 1;
|
||||
while (expander.getRowIndent(row) > indent) {
|
||||
cursor -= 1;
|
||||
row = expander.getRows(cursor, 1)[0];
|
||||
}
|
||||
this._updateCursor(cursor);
|
||||
this._keepCursorInViewport();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 39: // right
|
||||
if (expander.canExpand(row)) {
|
||||
this._expandRow(row);
|
||||
} else if (cursor < expander.getHeight() - 1) {
|
||||
this._updateCursor(cursor + 1);
|
||||
this._keepCursorInViewport();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dropAction = (s: string, d: string) => {
|
||||
const expander = this.state.aggrow.expander;
|
||||
if (s.startsWith('aggregate:active:')) {
|
||||
const sIndex = parseInt(s.substr(17), 10);
|
||||
let dIndex = -1;
|
||||
const active = expander.getActiveAggregators();
|
||||
const dragged = active[sIndex];
|
||||
if (d.startsWith('aggregate:insert:')) {
|
||||
dIndex = parseInt(d.substr(17), 10);
|
||||
} else if (d === 'divider:insert') {
|
||||
dIndex = active.length;
|
||||
} else {
|
||||
throw new Error(`not allowed to drag ${s} to ${d}`);
|
||||
}
|
||||
if (dIndex > sIndex) {
|
||||
dIndex -= 1;
|
||||
}
|
||||
active.splice(sIndex, 1);
|
||||
active.splice(dIndex, 0, dragged);
|
||||
expander.setActiveAggregators(active);
|
||||
this._updateCursor(0);
|
||||
} else if (s.startsWith('expander:active:')) {
|
||||
const sIndex = parseInt(s.substr(16), 10);
|
||||
let dIndex = -1;
|
||||
const active = expander.getActiveExpanders();
|
||||
const dragged = active[sIndex];
|
||||
if (d.startsWith('expander:insert:')) {
|
||||
dIndex = parseInt(d.substr(16), 10);
|
||||
} else if (d === 'divider:insert') {
|
||||
dIndex = 0;
|
||||
} else {
|
||||
throw new Error(`not allowed to drag ${s} to ${d}`);
|
||||
}
|
||||
if (dIndex > sIndex) {
|
||||
dIndex -= 1;
|
||||
}
|
||||
active.splice(sIndex, 1);
|
||||
active.splice(dIndex, 0, dragged);
|
||||
expander.setActiveExpanders(active);
|
||||
this._updateCursor(0);
|
||||
} else if (s.startsWith('expander:add:')) {
|
||||
let dIndex = -1;
|
||||
const sExpander = parseInt(s.substring(13), 10);
|
||||
if (d.startsWith('expander:insert:')) {
|
||||
dIndex = parseInt(d.substr(16), 10);
|
||||
} else if (d === 'divider:insert') {
|
||||
dIndex = 0;
|
||||
} else {
|
||||
throw new Error(`not allowed to drag ${s} to ${d}`);
|
||||
}
|
||||
const active = expander.getActiveExpanders();
|
||||
active.splice(dIndex, 0, sExpander);
|
||||
expander.setActiveExpanders(active);
|
||||
this._updateCursor(0);
|
||||
}
|
||||
}
|
||||
|
||||
_handleUpdate = () => {
|
||||
this.setState({ aggrow: this.state.aggrow });
|
||||
}
|
||||
|
||||
renderVirtualizedRows(): React.Element<*> {
|
||||
const expander = this.state.aggrow.expander;
|
||||
const viewport = this.state.viewport;
|
||||
const rows = expander.getRows(viewport.top, viewport.height);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: `${(rowHeight * (expander.getHeight() + 20))}px`,
|
||||
}}>
|
||||
{ rows.map((child: Row | null): ?React.Element<*> => this.renderRow(child)) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRow(toRender: Row | null): ?React.Element<*> {
|
||||
if (toRender === null) {
|
||||
return null;
|
||||
}
|
||||
const row = toRender;
|
||||
let bg = 'white';
|
||||
const expander = this.state.aggrow.expander;
|
||||
const columns = [];
|
||||
let rowText = '';
|
||||
const indent = 4 + (expander.getRowIndent(row) * treeIndent);
|
||||
const aggregates = expander.getActiveAggregators();
|
||||
if (expander.getRowExpanderIndex(row) % 2 === 1) {
|
||||
bg = '#f0f0f0';
|
||||
}
|
||||
if (row.top === this.state.cursor) {
|
||||
bg = '#dfe3ee';
|
||||
}
|
||||
for (let i = 0; i < aggregates.length; i++) {
|
||||
const aggregate = expander.getRowAggregate(row, i);
|
||||
columns.push((
|
||||
<div
|
||||
key={`ag${i}`}
|
||||
style={{
|
||||
width: '16px',
|
||||
height: 'inherit',
|
||||
backgroundColor: '#8b9dc3',
|
||||
flexShrink: '0',
|
||||
}}
|
||||
/>
|
||||
));
|
||||
columns.push((
|
||||
<div
|
||||
key={`agsep${i}`}
|
||||
style={{
|
||||
width: '128px',
|
||||
textAlign: 'right',
|
||||
backgroundColor: bg,
|
||||
flexShrink: '0',
|
||||
}}>
|
||||
{aggregate}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
columns.push((
|
||||
<div
|
||||
key="sep"
|
||||
style={{
|
||||
width: '16px',
|
||||
height: 'inherit',
|
||||
backgroundColor: '#3b5998',
|
||||
flexShrink: '0',
|
||||
}}
|
||||
/>
|
||||
));
|
||||
if (expander.canExpand(row)) {
|
||||
columns.push((
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
key="indent"
|
||||
// TODO: Fix this to not need an arrow function
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={(): void => this._expandRow(row)}
|
||||
style={{
|
||||
marginLeft: `${indent}px`,
|
||||
flexShrink: '0',
|
||||
width: '12px',
|
||||
textAlign: 'center',
|
||||
border: '1px solid gray',
|
||||
}}>
|
||||
+
|
||||
</div>
|
||||
));
|
||||
} else if (expander.canContract(row)) {
|
||||
columns.push((
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
key="indent"
|
||||
// TODO: Fix this to not need an arrow function
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={(): void => this._contractRow(row)}
|
||||
style={{
|
||||
marginLeft: `${indent}px`,
|
||||
flexShrink: '0',
|
||||
width: '12px',
|
||||
textAlign: 'center',
|
||||
border: '1px solid gray',
|
||||
}}>
|
||||
-
|
||||
</div>
|
||||
));
|
||||
} else {
|
||||
columns.push((
|
||||
<div
|
||||
key="indent"
|
||||
style={{
|
||||
marginLeft: `${indent}px`,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
rowText += expander.getRowLabel(row);
|
||||
columns.push((
|
||||
<div
|
||||
key="data"
|
||||
style={{
|
||||
flexShrink: '0',
|
||||
whiteSpace: 'nowrap',
|
||||
marginRight: '20px',
|
||||
}}>
|
||||
{rowText}
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
key={row.top}
|
||||
// TODO: Fix this to not need an arrow function
|
||||
onClick={() => { // eslint-disable-line react/jsx-no-bind
|
||||
this._updateCursor(row.top);
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: `${(rowHeight - 1)}px`,
|
||||
top: `${(rowHeight * row.top)}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: bg,
|
||||
borderBottom: '1px solid gray',
|
||||
}}>
|
||||
{columns}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
const expander = this.state.aggrow.expander;
|
||||
const cursor = this.state.cursor;
|
||||
const row = expander.getRows(cursor, 1)[0];
|
||||
invariant(row, 'Expected a row');
|
||||
const selectedExpander = expander.getRowExpanderIndex(row);
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div>
|
||||
<input type="text" value={this.state.searchValue} onChange={(event) => {this.setState({searchValue: event.target.value});}} />
|
||||
<input type="button" value="search!" onClick={() => {
|
||||
const re = new RegExp(this.state.searchValue);
|
||||
const i = this.state.aggrow.expander.findRow((row) => re.test(row.label), this.state.cursor);
|
||||
if (i >= 0) {
|
||||
this._updateCursor(i);
|
||||
this._keepCursorInViewport();
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
<TableHeader
|
||||
aggrow={this.state.aggrow}
|
||||
dropAction={this.dropAction}
|
||||
selectedExpander={selectedExpander}
|
||||
/>
|
||||
<div
|
||||
onScroll={this.scroll}
|
||||
ref={this._setScrollDiv}
|
||||
style={{
|
||||
width: '100%',
|
||||
flexGrow: '1',
|
||||
overflow: 'scroll',
|
||||
}}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{ this.renderVirtualizedRows() }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.props.enableConfigurationPane ?
|
||||
<TableConfiguration
|
||||
aggrow={this.state.aggrow}
|
||||
onUpdate={this._handleUpdate}
|
||||
/> :
|
||||
undefined
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import invariant from 'invariant';
|
||||
import React from 'react';
|
||||
|
||||
import AggrowData from './AggrowData';
|
||||
import type { AggrowColumn } from './AggrowData';
|
||||
|
||||
type Props = {
|
||||
aggrow: AggrowData,
|
||||
filter: (column: AggrowColumn) => boolean,
|
||||
onSelect: (columnName: string) => void,
|
||||
selected?: string,
|
||||
}
|
||||
|
||||
export default class DataColumnSelector extends React.Component {
|
||||
static defaultProps = {
|
||||
filter: (): boolean => true,
|
||||
};
|
||||
|
||||
props: Props;
|
||||
|
||||
_handleChange = (e: SyntheticEvent) => {
|
||||
invariant(e.target instanceof HTMLSelectElement, 'Expected element');
|
||||
const changed = Number.parseInt(e.target.value, 10);
|
||||
this.props.onSelect(this.props.aggrow.columns[changed].name);
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
const columns = this.props.aggrow.columns.filter(this.props.filter);
|
||||
const selected = columns.findIndex(
|
||||
(c: AggrowColumn): boolean => c.name === this.props.selected);
|
||||
return (
|
||||
<select
|
||||
onChange={this._handleChange}
|
||||
value={selected.toString()}>
|
||||
{columns.map((c: AggrowColumn, i: number): React.Element<*> =>
|
||||
<option key={`${c.name}-${i}`} value={i.toString()}>{c.name}</option>)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
children?: any,
|
||||
}
|
||||
|
||||
export default class Draggable extends React.Component {
|
||||
props: Props;
|
||||
|
||||
_handleDragStart = (e: SyntheticDragEvent) => {
|
||||
e.dataTransfer.setData('text', this.props.id);
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
return React.cloneElement(
|
||||
React.Children.only(this.props.children),
|
||||
{
|
||||
draggable: 'true',
|
||||
onDragStart: this._handleDragStart,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
dropAction: (sourceId: string, thisId: string) => void,
|
||||
children?: any,
|
||||
}
|
||||
|
||||
export default class DropTarget extends React.Component {
|
||||
props: Props;
|
||||
|
||||
_handleDragOver = (e: SyntheticDragEvent) => {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
_handleDrop = (e: SyntheticDragEvent) => {
|
||||
const sourceId = e.dataTransfer.getData('text');
|
||||
e.preventDefault();
|
||||
this.props.dropAction(sourceId, this.props.id);
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
return React.cloneElement(
|
||||
React.Children.only(this.props.children),
|
||||
{
|
||||
onDragOver: this._handleDragOver,
|
||||
onDrop: this._handleDrop,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AggrowExpander from './AggrowExpander';
|
||||
import Draggable from './Draggable';
|
||||
|
||||
type Props = {
|
||||
expander: AggrowExpander;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default function ExpanderConfiguration(props: Props): React.Element<*> {
|
||||
const expander = props.expander;
|
||||
const id = props.id;
|
||||
return (
|
||||
<Draggable id={`expander:add:${id}`}>
|
||||
<div
|
||||
style={{
|
||||
width: 'auto',
|
||||
height: '26px',
|
||||
border: '1px solid darkGray',
|
||||
margin: '2px',
|
||||
}}>
|
||||
{expander.getExpanderName(id)}
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// @flow
|
||||
/* eslint-disable jsx-a11y/label-has-for */
|
||||
|
||||
import invariant from 'invariant';
|
||||
import React from 'react';
|
||||
|
||||
import Aggrow from './Aggrow';
|
||||
import { AggrowStackColumn } from './AggrowData';
|
||||
import type { AggrowColumn } from './AggrowData';
|
||||
import type { FocusConfig } from './Aggrow';
|
||||
|
||||
type Props = {
|
||||
aggrow: Aggrow,
|
||||
onCreate: (expanderId: number) => void,
|
||||
}
|
||||
|
||||
type State = {
|
||||
column: string,
|
||||
pattern: string,
|
||||
reverse: boolean,
|
||||
leftSide: boolean,
|
||||
firstMatch: boolean,
|
||||
}
|
||||
|
||||
export default class StackExpanderCreator extends React.Component {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const data = this.props.aggrow.data;
|
||||
const firstColumn = data.columns.find(isStackColumn);
|
||||
this.state = {
|
||||
column: firstColumn ? firstColumn.name : '',
|
||||
pattern: '',
|
||||
reverse: false,
|
||||
leftSide: false,
|
||||
firstMatch: true,
|
||||
};
|
||||
}
|
||||
props: Props;
|
||||
|
||||
state: State;
|
||||
|
||||
_handleColumnSelected = (e: SyntheticEvent) => {
|
||||
invariant(e.target instanceof HTMLSelectElement, 'Expected select element');
|
||||
this.setState({ column: e.target.value });
|
||||
}
|
||||
|
||||
_handlePatternSelected = (e: SyntheticEvent) => {
|
||||
invariant(e.target instanceof HTMLInputElement, 'Expected input element');
|
||||
this.setState({ pattern: e.target.value });
|
||||
}
|
||||
|
||||
_handleReverseSelected = (e: SyntheticEvent) => {
|
||||
invariant(e.target instanceof HTMLInputElement, 'Expected input element');
|
||||
this.setState({ reverse: e.target.checked });
|
||||
}
|
||||
|
||||
_handleFirstMatchSelected = (e: SyntheticEvent) => {
|
||||
invariant(e.target instanceof HTMLInputElement, 'Expected input element');
|
||||
this.setState({ firstMatch: e.target.checked });
|
||||
}
|
||||
|
||||
_handleLeftSideSelected = (e: SyntheticEvent) => {
|
||||
invariant(e.target instanceof HTMLInputElement, 'Expected input element');
|
||||
this.setState({ leftSide: e.target.checked });
|
||||
}
|
||||
|
||||
_handleCreateClicked = () => {
|
||||
let focus: FocusConfig;
|
||||
let expanderName = this.state.column;
|
||||
if (this.state.pattern !== '') {
|
||||
focus = {
|
||||
pattern: new RegExp(this.state.pattern),
|
||||
firstMatch: this.state.firstMatch,
|
||||
leftSide: this.state.leftSide,
|
||||
};
|
||||
expanderName += this.state.reverse ? ' reversed' : '';
|
||||
expanderName += this.state.leftSide ? ' before' : ' after';
|
||||
expanderName += this.state.firstMatch ? ' first ' : ' last ';
|
||||
expanderName += this.state.pattern;
|
||||
}
|
||||
|
||||
this.props.onCreate(
|
||||
this.props.aggrow.addStackExpander(
|
||||
expanderName,
|
||||
this.state.column,
|
||||
this.state.reverse,
|
||||
focus,
|
||||
));
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
const data = this.props.aggrow.data;
|
||||
const stackColumns = data.columns.filter(isStackColumn);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<select
|
||||
key="columnselect"
|
||||
onChange={this._handleColumnSelected}
|
||||
value={this.state.column}>
|
||||
{stackColumns.map((c: AggrowColumn): React.Element<*> =>
|
||||
<option key={c.name} value={c.name}>{c.name}</option>
|
||||
)}
|
||||
</select>
|
||||
<input
|
||||
key="pattern"
|
||||
onChange={this._handlePatternSelected}
|
||||
type="text"
|
||||
value={this.state.pattern}
|
||||
/>
|
||||
<label key="reverse">
|
||||
<input
|
||||
checked={this.state.reverse}
|
||||
onChange={this._handleReverseSelected}
|
||||
type="checkbox"
|
||||
/>
|
||||
Reverse
|
||||
</label>
|
||||
<label key="firstmatch">
|
||||
<input
|
||||
checked={this.state.firstMatch}
|
||||
onChange={this._handleFirstMatchSelected}
|
||||
type="checkbox"
|
||||
/>
|
||||
First Match
|
||||
</label>
|
||||
<label key="leftside">
|
||||
<input
|
||||
checked={this.state.leftSide}
|
||||
onChange={this._handleLeftSideSelected}
|
||||
type="checkbox"
|
||||
/>
|
||||
Left of Match
|
||||
</label>
|
||||
<button
|
||||
key="create"
|
||||
onClick={this._handleCreateClicked}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isStackColumn(c: AggrowColumn): boolean {
|
||||
return c instanceof AggrowStackColumn;
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// @flow
|
||||
|
||||
import invariant from 'invariant';
|
||||
|
||||
export type Stack = {
|
||||
id: number,
|
||||
[frameId: number]: Stack,
|
||||
}
|
||||
|
||||
export type FlattenedStack = Int32Array;
|
||||
|
||||
type StackIdMap = Array<FlattenedStack>;
|
||||
|
||||
export default class StackRegistry {
|
||||
root: ?Stack = { id: 0 };
|
||||
nodeCount: number = 1;
|
||||
maxDepth: number = -1;
|
||||
stackIdMap: ?StackIdMap = null;
|
||||
|
||||
insert(parent: Stack, frameId: number): Stack {
|
||||
invariant(this.stackIdMap === null, 'Stacks already flattened!');
|
||||
let node = parent[frameId];
|
||||
if (node === undefined) {
|
||||
node = { id: this.nodeCount };
|
||||
this.nodeCount += 1;
|
||||
|
||||
// TODO: make a builder instead of mutating the array?
|
||||
parent[frameId] = node; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
get(id: number): FlattenedStack {
|
||||
invariant(this.stackIdMap, 'Stacks not flattened!');
|
||||
return this.stackIdMap[id];
|
||||
}
|
||||
|
||||
flatten() {
|
||||
if (this.stackIdMap !== null) {
|
||||
return;
|
||||
}
|
||||
let stackFrameCount = 0;
|
||||
function countStacks(tree: Stack, depth: number): boolean {
|
||||
let leaf = true;
|
||||
Object.keys(tree).forEach((frameId: any) => {
|
||||
if (frameId !== 'id') {
|
||||
leaf = countStacks(tree[Number(frameId)], depth + 1);
|
||||
}
|
||||
});
|
||||
if (leaf) {
|
||||
stackFrameCount += depth;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const root = this.root;
|
||||
invariant(root, 'Stacks already flattened');
|
||||
countStacks(root, 0);
|
||||
const stackIdMap = new Array(this.nodeCount);
|
||||
const stackArray = new Int32Array(stackFrameCount);
|
||||
let maxStackDepth = 0;
|
||||
stackFrameCount = 0;
|
||||
function flattenStacksImpl(tree: Stack, stack: Array<number>): void {
|
||||
let childStack;
|
||||
maxStackDepth = Math.max(maxStackDepth, stack.length);
|
||||
Object.keys(tree).forEach((frameId: any) => {
|
||||
if (frameId !== 'id') {
|
||||
stack.push(Number(frameId));
|
||||
childStack = flattenStacksImpl(tree[frameId], stack);
|
||||
stack.pop();
|
||||
}
|
||||
});
|
||||
|
||||
const id = tree.id;
|
||||
invariant(
|
||||
id >= 0 && id < stackIdMap.length && stackIdMap[id] === undefined,
|
||||
'Invalid stack ID!');
|
||||
|
||||
if (childStack !== undefined) {
|
||||
// each child must have our stack as a prefix, so just use that
|
||||
stackIdMap[id] = childStack.subarray(0, stack.length);
|
||||
} else {
|
||||
const newStack = stackArray.subarray(stackFrameCount, stackFrameCount + stack.length);
|
||||
stackFrameCount += stack.length;
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
newStack[i] = stack[i];
|
||||
}
|
||||
stackIdMap[id] = newStack;
|
||||
}
|
||||
return stackIdMap[id];
|
||||
}
|
||||
flattenStacksImpl(root, []);
|
||||
this.root = null;
|
||||
this.stackIdMap = stackIdMap;
|
||||
this.maxDepth = maxStackDepth;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
|
||||
type InternedStringsTable = {
|
||||
[key: string]: number,
|
||||
}
|
||||
|
||||
export default class StringInterner {
|
||||
strings: Array<string> = [];
|
||||
ids: InternedStringsTable = {};
|
||||
|
||||
intern(s: string): number {
|
||||
const find = this.ids[s];
|
||||
if (find === undefined) {
|
||||
const id = this.strings.length;
|
||||
this.ids[s] = id;
|
||||
this.strings.push(s);
|
||||
return id;
|
||||
}
|
||||
|
||||
return find;
|
||||
}
|
||||
|
||||
get(id: number): string {
|
||||
return this.strings[id];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Aggrow from './Aggrow';
|
||||
import ExpanderConfiguration from './ExpanderConfiguration';
|
||||
import StackExpanderCreator from './StackExpanderCreator';
|
||||
|
||||
type State = {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
aggrow: Aggrow,
|
||||
onUpdate: () => void,
|
||||
}
|
||||
|
||||
export default class TableConfiguration extends React.Component {
|
||||
props: Props;
|
||||
|
||||
state: State = {
|
||||
expanded: false,
|
||||
}
|
||||
|
||||
_handleUpdate = () => {
|
||||
this.props.onUpdate();
|
||||
}
|
||||
|
||||
_toggleExpanded = () => {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
}
|
||||
|
||||
renderExpander(id: number): React.Element<*> {
|
||||
return (<ExpanderConfiguration expander={this.props.aggrow.expander} id={id} />);
|
||||
}
|
||||
|
||||
render(): React.Element<*> {
|
||||
const expanderText = this.state.expanded ? '>>' : '<<';
|
||||
const expander = this.props.aggrow.expander;
|
||||
let config = [];
|
||||
if (this.state.expanded) {
|
||||
config = expander.getExpanders().map(
|
||||
(ex: number): React.Element<*> => this.renderExpander(ex));
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: this.state.expanded ? '512px' : '26px',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderLeft: '2px solid black',
|
||||
}}>
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
onClick={this._toggleExpanded}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '26px',
|
||||
border: '1px solid darkGray',
|
||||
}}>
|
||||
{ expanderText }
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '26px',
|
||||
flexGrow: '1',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
{ config }
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '256px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderTop: '1px solid darkGray',
|
||||
}}>
|
||||
<StackExpanderCreator aggrow={this.props.aggrow} onCreate={this._handleUpdate} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Aggrow from './Aggrow';
|
||||
import Draggable from './Draggable';
|
||||
import DropTarget from './DropTarget';
|
||||
|
||||
type Props = {
|
||||
aggrow: Aggrow,
|
||||
dropAction: (sourceId: string, thisId: string) => void,
|
||||
selectedExpander: ?number,
|
||||
}
|
||||
|
||||
export default function TableHeader(props: Props): React.Element<*> {
|
||||
const expander = props.aggrow.expander;
|
||||
const aggregators = expander.getActiveAggregators();
|
||||
const expanders = expander.getActiveExpanders();
|
||||
const headers = [];
|
||||
for (let i = 0; i < aggregators.length; i++) {
|
||||
const name = expander.getAggregatorName(aggregators[i]);
|
||||
headers.push((
|
||||
<DropTarget
|
||||
dropAction={props.dropAction}
|
||||
id={`aggregate:insert:${i}`}
|
||||
key={`aggregate:insert:${i}`}>
|
||||
<div
|
||||
style={{
|
||||
width: '16px',
|
||||
height: 'inherit',
|
||||
backgroundColor: '#8b9dc3',
|
||||
flexShrink: '0',
|
||||
}}
|
||||
/>
|
||||
</DropTarget>));
|
||||
headers.push((
|
||||
<Draggable
|
||||
id={`aggregate:active:${i}`}
|
||||
key={`aggregate:active:${i}`}>
|
||||
<div style={{ width: '128px', textAlign: 'center', flexShrink: '0' }}>{name}</div>
|
||||
</Draggable>));
|
||||
}
|
||||
headers.push((
|
||||
<DropTarget
|
||||
dropAction={props.dropAction}
|
||||
id="divider:insert"
|
||||
key="divider:insert">
|
||||
<div
|
||||
style={{
|
||||
width: '16px',
|
||||
height: 'inherit',
|
||||
backgroundColor: '#3b5998',
|
||||
flexShrink: '0',
|
||||
}}
|
||||
/>
|
||||
</DropTarget>));
|
||||
for (let i = 0; i < expanders.length; i++) {
|
||||
const name = expander.getExpanderName(expanders[i]);
|
||||
headers.push((
|
||||
<Draggable
|
||||
id={`expander:active:${i}`}
|
||||
key={`expander:active:${i}`}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
flexShrink: '0',
|
||||
padding: '4px',
|
||||
backgroundColor: i === props.selectedExpander ? '#dfe3ee' : 'white',
|
||||
}}>
|
||||
{name}
|
||||
</div>
|
||||
</Draggable>));
|
||||
const sep = i + 1 < expanders.length ? '->' : '...';
|
||||
headers.push((
|
||||
<DropTarget
|
||||
dropAction={props.dropAction}
|
||||
id={`expander:insert:${i + 1}`}
|
||||
key={`expander:insert:${i + 1}`}>
|
||||
<div
|
||||
style={{
|
||||
height: 'inherit',
|
||||
backgroundColor: '#8b9dc3',
|
||||
flexShrink: '0',
|
||||
}}>
|
||||
{sep}
|
||||
</div>
|
||||
</DropTarget>)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '26px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottom: '2px solid black',
|
||||
}}>
|
||||
{headers}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,389 @@
|
|||
/**
|
||||
* Copyright (c) 2016-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
'use strict';
|
||||
/*eslint no-console-disallow: "off"*/
|
||||
/*global preLoadedCapture:true*/
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import {
|
||||
Aggrow,
|
||||
AggrowData,
|
||||
AggrowTable,
|
||||
StringInterner,
|
||||
StackRegistry,
|
||||
} from './index.js';
|
||||
|
||||
function RefVisitor(refs, id) {
|
||||
this.refs = refs;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
RefVisitor.prototype = {
|
||||
moveToEdge: function moveToEdge(name) {
|
||||
const ref = this.refs[this.id];
|
||||
if (ref && ref.edges) {
|
||||
const edges = ref.edges;
|
||||
for (const edgeId in edges) {
|
||||
if (edges[edgeId] === name) {
|
||||
this.id = edgeId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.id = undefined;
|
||||
return this;
|
||||
},
|
||||
moveToFirst: function moveToFirst(callback) {
|
||||
const ref = this.refs[this.id];
|
||||
if (ref && ref.edges) {
|
||||
const edges = ref.edges;
|
||||
for (const edgeId in edges) {
|
||||
this.id = edgeId;
|
||||
if (callback(edges[edgeId], this)) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.id = undefined;
|
||||
return this;
|
||||
},
|
||||
forEachEdge: function forEachEdge(callback) {
|
||||
const ref = this.refs[this.id];
|
||||
if (ref && ref.edges) {
|
||||
const edges = ref.edges;
|
||||
const visitor = new RefVisitor(this.refs, undefined);
|
||||
for (const edgeId in edges) {
|
||||
visitor.id = edgeId;
|
||||
callback(edges[edgeId], visitor);
|
||||
}
|
||||
}
|
||||
},
|
||||
getType: function getType() {
|
||||
const ref = this.refs[this.id];
|
||||
if (ref) {
|
||||
return ref.type;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
getRef: function getRef() {
|
||||
return this.refs[this.id];
|
||||
},
|
||||
clone: function clone() {
|
||||
return new RefVisitor(this.refs, this.id);
|
||||
},
|
||||
isDefined: function isDefined() {
|
||||
return !!this.id;
|
||||
},
|
||||
getValue: function getValue() {
|
||||
const ref = this.refs[this.id];
|
||||
if (ref) {
|
||||
if (ref.type === 'string') {
|
||||
if (ref.value) {
|
||||
return ref.value;
|
||||
} else {
|
||||
const rope = [];
|
||||
this.forEachEdge((name, visitor) => {
|
||||
if (name && name.startsWith('[') && name.endsWith(']')) {
|
||||
const index = parseInt(name.substring(1, name.length - 1), 10);
|
||||
rope[index] = visitor.getValue();
|
||||
}
|
||||
});
|
||||
return rope.join('');
|
||||
}
|
||||
} else if (ref.type === 'ScriptExecutable'
|
||||
|| ref.type === 'EvalExecutable'
|
||||
|| ref.type === 'ProgramExecutable') {
|
||||
return ref.value.url + ':' + ref.value.line + ':' + ref.value.col;
|
||||
} else if (ref.type === 'FunctionExecutable') {
|
||||
return ref.value.name + '@' + ref.value.url + ':' + ref.value.line + ':' + ref.value.col;
|
||||
} else if (ref.type === 'NativeExecutable') {
|
||||
return ref.value.function + ' ' + ref.value.constructor + ' ' + ref.value.name;
|
||||
} else if (ref.type === 'Function') {
|
||||
const executable = this.clone().moveToEdge('@Executable');
|
||||
if (executable.id) {
|
||||
return executable.getRef().type + ' ' + executable.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return '#none';
|
||||
}
|
||||
};
|
||||
|
||||
function forEachRef(refs, callback) {
|
||||
const visitor = new RefVisitor(refs, undefined);
|
||||
for (const id in refs) {
|
||||
visitor.id = id;
|
||||
callback(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
function firstRef(refs, callback) {
|
||||
for (const id in refs) {
|
||||
const ref = refs[id];
|
||||
if (callback(id, ref)) {
|
||||
return new RefVisitor(refs, id);
|
||||
}
|
||||
}
|
||||
return new RefVisitor(refs, undefined);
|
||||
}
|
||||
|
||||
function getInternalInstanceName(visitor) {
|
||||
const type = visitor.clone().moveToEdge('_currentElement').moveToEdge('type');
|
||||
if (type.getType() === 'string') { // element.type is string
|
||||
return type.getValue();
|
||||
} else if (type.getType() === 'Function') { // element.type is function
|
||||
const displayName = type.clone().moveToEdge('displayName');
|
||||
if (displayName.isDefined()) {
|
||||
return displayName.getValue(); // element.type.displayName
|
||||
}
|
||||
const name = type.clone().moveToEdge('name');
|
||||
if (name.isDefined()) {
|
||||
return name.getValue(); // element.type.name
|
||||
}
|
||||
type.moveToEdge('@Executable');
|
||||
if (type.getType() === 'FunctionExecutable') {
|
||||
return type.getRef().value.name; // element.type symbolicated name
|
||||
}
|
||||
}
|
||||
return '#unknown';
|
||||
}
|
||||
|
||||
function buildReactComponentTree(visitor, registry, strings) {
|
||||
const ref = visitor.getRef();
|
||||
if (ref.reactTree || ref.reactParent === undefined) {
|
||||
return; // has one or doesn't need one
|
||||
}
|
||||
const parentVisitor = ref.reactParent;
|
||||
if (parentVisitor === null) {
|
||||
ref.reactTree = registry.insert(registry.root, strings.intern(getInternalInstanceName(visitor)));
|
||||
} else if (parentVisitor) {
|
||||
const parentRef = parentVisitor.getRef();
|
||||
buildReactComponentTree(parentVisitor, registry, strings);
|
||||
let relativeName = getInternalInstanceName(visitor);
|
||||
if (ref.reactKey) {
|
||||
relativeName = ref.reactKey + ': ' + relativeName;
|
||||
}
|
||||
ref.reactTree = registry.insert(parentRef.reactTree, strings.intern(relativeName));
|
||||
} else {
|
||||
throw 'non react instance parent of react instance';
|
||||
}
|
||||
}
|
||||
|
||||
function markReactComponentTree(refs, registry, strings) {
|
||||
// annotate all refs that are react internal instances with their parent and name
|
||||
// ref.reactParent = visitor that points to parent instance,
|
||||
// null if we know it's an instance, but don't have a parent yet
|
||||
// ref.reactKey = if a key is used to distinguish siblings
|
||||
forEachRef(refs, (visitor) => {
|
||||
const visitorClone = visitor.clone(); // visitor will get stomped on next iteration
|
||||
const ref = visitor.getRef();
|
||||
visitor.forEachEdge((edgeName, edgeVisitor) => {
|
||||
const edgeRef = edgeVisitor.getRef();
|
||||
if (edgeRef) {
|
||||
if (edgeName === '_renderedChildren') {
|
||||
if (ref.reactParent === undefined) {
|
||||
// ref is react component, even if we don't have a parent yet
|
||||
ref.reactParent = null;
|
||||
}
|
||||
edgeVisitor.forEachEdge((childName, childVisitor) => {
|
||||
const childRef = childVisitor.getRef();
|
||||
if (childRef && childName.startsWith('.')) {
|
||||
childRef.reactParent = visitorClone;
|
||||
childRef.reactKey = childName;
|
||||
}
|
||||
});
|
||||
} else if (edgeName === '_renderedComponent') {
|
||||
if (ref.reactParent === undefined) {
|
||||
ref.reactParent = null;
|
||||
}
|
||||
edgeRef.reactParent = visitorClone;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// build tree of react internal instances (since that's what has the structure)
|
||||
// fill in ref.reactTree = path registry node
|
||||
forEachRef(refs, (visitor) => {
|
||||
buildReactComponentTree(visitor, registry, strings);
|
||||
});
|
||||
// hook in components by looking at their _reactInternalInstance fields
|
||||
forEachRef(refs, (visitor) => {
|
||||
const ref = visitor.getRef();
|
||||
const instanceRef = visitor.moveToEdge('_reactInternalInstance').getRef();
|
||||
if (instanceRef) {
|
||||
ref.reactTree = instanceRef.reactTree;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function functionUrlFileName(visitor) {
|
||||
const executable = visitor.clone().moveToEdge('@Executable');
|
||||
const ref = executable.getRef();
|
||||
if (ref && ref.value && ref.value.url) {
|
||||
const url = ref.value.url;
|
||||
let file = url.substring(url.lastIndexOf('/') + 1);
|
||||
if (file.endsWith('.js')) {
|
||||
file = file.substring(0, file.length - 3);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function markModules(refs) {
|
||||
const modules = firstRef(refs, (id, ref) => ref.type === 'CallbackGlobalObject');
|
||||
modules.moveToEdge('require');
|
||||
modules.moveToFirst((name, visitor) => visitor.getType() === 'JSActivation');
|
||||
modules.moveToEdge('modules');
|
||||
modules.forEachEdge((name, visitor) => {
|
||||
const ref = visitor.getRef();
|
||||
visitor.moveToEdge('exports');
|
||||
if (visitor.getType() === 'Object') {
|
||||
visitor.moveToFirst((memberName, member) => member.getType() === 'Function');
|
||||
if (visitor.isDefined()) {
|
||||
ref.module = functionUrlFileName(visitor);
|
||||
}
|
||||
} else if (visitor.getType() === 'Function') {
|
||||
const displayName = visitor.clone().moveToEdge('displayName');
|
||||
if (displayName.isDefined()) {
|
||||
ref.module = displayName.getValue();
|
||||
}
|
||||
ref.module = functionUrlFileName(visitor);
|
||||
}
|
||||
if (ref && !ref.module) {
|
||||
ref.module = '#unknown ' + name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerPathToRoot(refs, registry, strings) {
|
||||
markReactComponentTree(refs, registry, strings);
|
||||
markModules(refs);
|
||||
let breadth = [];
|
||||
forEachRef(refs, (visitor) => {
|
||||
const ref = visitor.getRef();
|
||||
if (ref.type === 'CallbackGlobalObject') {
|
||||
ref.rootPath = registry.insert(registry.root, strings.intern(ref.type));
|
||||
breadth.push(visitor.clone());
|
||||
}
|
||||
});
|
||||
while (breadth.length > 0) {
|
||||
const nextBreadth = [];
|
||||
for (let i = 0; i < breadth.length; i++) {
|
||||
const visitor = breadth[i];
|
||||
const ref = visitor.getRef();
|
||||
visitor.forEachEdge((edgeName, edgeVisitor) => {
|
||||
const edgeRef = edgeVisitor.getRef();
|
||||
if (edgeRef && edgeRef.rootPath === undefined) {
|
||||
let pathName = edgeRef.type;
|
||||
if (edgeName) {
|
||||
pathName = edgeName + ': ' + pathName;
|
||||
}
|
||||
edgeRef.rootPath = registry.insert(ref.rootPath, strings.intern(pathName));
|
||||
nextBreadth.push(edgeVisitor.clone());
|
||||
// copy module and react tree forward
|
||||
if (edgeRef.module === undefined) {
|
||||
edgeRef.module = ref.module;
|
||||
}
|
||||
if (edgeRef.reactTree === undefined) {
|
||||
edgeRef.reactTree = ref.reactTree;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
breadth = nextBreadth;
|
||||
}
|
||||
}
|
||||
|
||||
function registerCapture(data, captureId, capture, stacks, strings) {
|
||||
// NB: capture.refs is potentially VERY large, so we try to avoid making
|
||||
// copies, even if iteration is a bit more annoying.
|
||||
let rowCount = 0;
|
||||
for (const id in capture.refs) { // eslint-disable-line no-unused-vars
|
||||
rowCount++;
|
||||
}
|
||||
for (const id in capture.markedBlocks) { // eslint-disable-line no-unused-vars
|
||||
rowCount++;
|
||||
}
|
||||
const inserter = data.rowInserter(rowCount);
|
||||
registerPathToRoot(capture.refs, stacks, strings);
|
||||
const noneString = strings.intern('#none');
|
||||
const noneStack = stacks.insert(stacks.root, noneString);
|
||||
forEachRef(capture.refs, (visitor) => {
|
||||
// want to data.append(value, value, value), not IDs
|
||||
const ref = visitor.getRef();
|
||||
const id = visitor.id;
|
||||
inserter.insertRow(
|
||||
parseInt(id, 16),
|
||||
ref.type,
|
||||
ref.size,
|
||||
captureId,
|
||||
ref.rootPath === undefined ? noneStack : ref.rootPath,
|
||||
ref.reactTree === undefined ? noneStack : ref.reactTree,
|
||||
visitor.getValue(),
|
||||
ref.module === undefined ? '#none' : ref.module,
|
||||
);
|
||||
});
|
||||
for (const id in capture.markedBlocks) {
|
||||
const block = capture.markedBlocks[id];
|
||||
inserter.insertRow(
|
||||
parseInt(id, 16),
|
||||
'Marked Block Overhead',
|
||||
block.capacity - block.size,
|
||||
captureId,
|
||||
noneStack,
|
||||
noneStack,
|
||||
'capacity: ' + block.capacity + ', size: ' + block.size + ', granularity: ' + block.cellSize,
|
||||
'#none',
|
||||
);
|
||||
}
|
||||
inserter.done();
|
||||
}
|
||||
|
||||
if (preLoadedCapture) {
|
||||
const strings = new StringInterner();
|
||||
const stacks = new StackRegistry();
|
||||
const columns = [
|
||||
{ name: 'id', type: 'int' },
|
||||
{ name: 'type', type: 'string', strings: strings },
|
||||
{ name: 'size', type: 'int' },
|
||||
{ name: 'trace', type: 'string', strings: strings },
|
||||
{ name: 'path', type: 'stack', stacks: stacks, getter: x => strings.get(x), formatter: x => x },
|
||||
{ name: 'react', type: 'stack', stacks: stacks, getter: x => strings.get(x), formatter: x => x },
|
||||
{ name: 'value', type: 'string', strings: strings },
|
||||
{ name: 'module', type: 'string', strings: strings },
|
||||
];
|
||||
const data = new AggrowData(columns);
|
||||
registerCapture(data, 'trace', preLoadedCapture, stacks, strings);
|
||||
preLoadedCapture = undefined; // let GG clean up the capture
|
||||
const aggrow = new Aggrow(data);
|
||||
aggrow.addPointerExpander('Id', 'id');
|
||||
const typeExpander = aggrow.addStringExpander('Type', 'type');
|
||||
aggrow.addNumberExpander('Size', 'size');
|
||||
aggrow.addStringExpander('Trace', 'trace');
|
||||
const pathExpander = aggrow.addStackExpander('Path', 'path');
|
||||
const reactExpander = aggrow.addStackExpander('React Tree', 'react');
|
||||
const valueExpander = aggrow.addStringExpander('Value', 'value');
|
||||
const moduleExpander = aggrow.addStringExpander('Module', 'module');
|
||||
aggrow.expander.setActiveExpanders([
|
||||
pathExpander,
|
||||
reactExpander,
|
||||
moduleExpander,
|
||||
typeExpander,
|
||||
valueExpander,
|
||||
]);
|
||||
const sizeAggregator = aggrow.addSumAggregator('Size', 'size');
|
||||
const countAggregator = aggrow.addCountAggregator('Count');
|
||||
aggrow.expander.setActiveAggregators([
|
||||
sizeAggregator,
|
||||
countAggregator,
|
||||
]);
|
||||
ReactDOM.render(<AggrowTable aggrow={aggrow} />, document.body);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// @flow
|
||||
|
||||
import Aggrow from './Aggrow';
|
||||
import AggrowData from './AggrowData';
|
||||
import type { AggrowColumnDef } from './AggrowData';
|
||||
import AggrowExpander from './AggrowExpander';
|
||||
import AggrowTable from './AggrowTable';
|
||||
import StackRegistry from './StackRegistry';
|
||||
import type { Stack } from './StackRegistry';
|
||||
import StringInterner from './StringInterner';
|
||||
|
||||
export type {
|
||||
AggrowColumnDef,
|
||||
Stack,
|
||||
};
|
||||
|
||||
export {
|
||||
Aggrow,
|
||||
AggrowData,
|
||||
AggrowExpander,
|
||||
AggrowTable,
|
||||
StackRegistry,
|
||||
StringInterner,
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
devtool: 'inline-source-map',
|
||||
entry: './src/heapCapture.js',
|
||||
resolve: {
|
||||
extensions: ["", ".js", ".jsx"],
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
include: /\/src\//,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: [ 'react', 'es2015' ],
|
||||
plugins: [ 'transform-class-properties' ]
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.BannerPlugin('\n// @generated\n', { raw: true }),
|
||||
],
|
||||
output: {
|
||||
path: './',
|
||||
filename: 'bundle.js',
|
||||
},
|
||||
};
|
|
@ -19,6 +19,8 @@ var sharedBlacklist = [
|
|||
|
||||
// TODO(jkassens, #9876132): Remove this rule when it's no longer needed.
|
||||
'Libraries/Relay/relay/tools/relayUnstableBatchedUpdates.js',
|
||||
|
||||
/heapCapture\/bundle\.js/,
|
||||
];
|
||||
|
||||
function escapeRegExp(pattern) {
|
||||
|
|
Loading…
Reference in New Issue