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>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>JSC Heap Capture</title>
|
<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>
|
</head>
|
||||||
<body style="margin:0px; height: 100%">
|
<body style="margin:0px; height: 100%">
|
||||||
Loading... This could take a while depending on how big the profile is. Check devtools console for errors.
|
Loading... This could take a while depending on how big the profile is. Check devtools console for errors.
|
||||||
</body>
|
</body>
|
||||||
<script src="preLoadedCapture.js"></script>
|
<script src="preLoadedCapture.js"></script>
|
||||||
<script src="out/heapCapture.js"></script>
|
<script src="bundle.js"></script>
|
||||||
</html>
|
</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.
|
// TODO(jkassens, #9876132): Remove this rule when it's no longer needed.
|
||||||
'Libraries/Relay/relay/tools/relayUnstableBatchedUpdates.js',
|
'Libraries/Relay/relay/tools/relayUnstableBatchedUpdates.js',
|
||||||
|
|
||||||
|
/heapCapture\/bundle\.js/,
|
||||||
];
|
];
|
||||||
|
|
||||||
function escapeRegExp(pattern) {
|
function escapeRegExp(pattern) {
|
||||||
|
|
Loading…
Reference in New Issue