Pull aggrow from facebookincubator/tracery-prerelease

Reviewed By: bnham

Differential Revision: D4250937

fbshipit-source-id: b5f2cfdeb06c04399670e463b8b2498e2fe0074b
This commit is contained in:
Charles Dick 2016-11-30 12:43:46 -08:00 committed by Facebook Github Bot
parent 3094c36c81
commit 48d3cd7d26
20 changed files with 25271 additions and 5 deletions

File diff suppressed because one or more lines are too long

View File

@ -3,14 +3,10 @@
<head>
<meta charset="utf-8">
<title>JSC Heap Capture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react-dom.js"></script>
<script src="out/aggrow.js"></script>
<script src="out/table.js"></script>
</head>
<body style="margin:0px; height: 100%">
Loading... This could take a while depending on how big the profile is. Check devtools console for errors.
</body>
<script src="preLoadedCapture.js"></script>
<script src="out/heapCapture.js"></script>
<script src="bundle.js"></script>
</html>

View File

@ -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"
}
}

View File

@ -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];
}
}

View File

@ -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!');
}
}

View File

@ -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);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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,
}
);
}
}

View File

@ -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,
}
);
}
}

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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];
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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',
},
};

View File

@ -19,6 +19,8 @@ var sharedBlacklist = [
// TODO(jkassens, #9876132): Remove this rule when it's no longer needed.
'Libraries/Relay/relay/tools/relayUnstableBatchedUpdates.js',
/heapCapture\/bundle\.js/,
];
function escapeRegExp(pattern) {