mirror of
https://github.com/status-im/react-native.git
synced 2025-01-14 19:44:13 +00:00
remove the old heap profiler visualization code
Differential Revision: D4650983 fbshipit-source-id: 1f791acdd3e2d96e7881ea037045fafa2c6d781a
This commit is contained in:
parent
7098c450d8
commit
af590b0c74
@ -1,3 +0,0 @@
|
|||||||
/captures/*
|
|
||||||
preLoadedCapture.js
|
|
||||||
bundle.js
|
|
@ -1,8 +0,0 @@
|
|||||||
SHELL := /bin/bash
|
|
||||||
|
|
||||||
all:
|
|
||||||
NODE_PATH="../../../../node_modules/" babel --presets babel-preset-react-native --source-maps inline -d out src
|
|
||||||
for f in out/*.js; do echo -e "\n// @generated" >> $$f; done
|
|
||||||
|
|
||||||
watch:
|
|
||||||
NODE_PATH="../../../../node_modules/" babel --watch --presets babel-preset-react-native -d out src
|
|
@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" style="height: 100%;">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>JSC Heap Capture</title>
|
|
||||||
</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="bundle.js"></script>
|
|
||||||
</html>
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,190 +0,0 @@
|
|||||||
// @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];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,216 +0,0 @@
|
|||||||
// @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!');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,694 +0,0 @@
|
|||||||
// @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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,439 +0,0 @@
|
|||||||
// @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 && 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 && 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// @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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// @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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
// @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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
// @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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,152 +0,0 @@
|
|||||||
// @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;
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
// @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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
// @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];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
// @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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
// @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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,412 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 registerPathToRootBFS(breadth, registry, strings) {
|
|
||||||
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 registerPathToRoot(capture, registry, strings) {
|
|
||||||
const refs = capture.refs;
|
|
||||||
const roots = capture.roots;
|
|
||||||
markReactComponentTree(refs, registry, strings);
|
|
||||||
markModules(refs);
|
|
||||||
let breadth = [];
|
|
||||||
// BFS from global objects first
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerPathToRootBFS(breadth, registry, strings);
|
|
||||||
breadth = [];
|
|
||||||
// lower priority, BFS from other roots
|
|
||||||
for (const id of roots) {
|
|
||||||
const visitor = new RefVisitor(refs, id);
|
|
||||||
const ref = visitor.getRef();
|
|
||||||
if (ref.rootPath === undefined) {
|
|
||||||
ref.rootPath = registry.insert(registry.root, strings.intern(`root ${id}: ${ref.type}`));
|
|
||||||
breadth.push(visitor.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registerPathToRootBFS(breadth, registry, strings);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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,
|
|
||||||
ref.cellSize,
|
|
||||||
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,
|
|
||||||
0,
|
|
||||||
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: 'cell', 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 cellAggregator = aggrow.addSumAggregator('Cell Size', 'cell');
|
|
||||||
const countAggregator = aggrow.addCountAggregator('Count');
|
|
||||||
aggrow.expander.setActiveAggregators([
|
|
||||||
cellAggregator,
|
|
||||||
sizeAggregator,
|
|
||||||
countAggregator,
|
|
||||||
]);
|
|
||||||
ReactDOM.render(<AggrowTable aggrow={aggrow} />, document.body);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
// @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,
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
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',
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,171 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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"*/
|
|
||||||
|
|
||||||
const spawn = require('child_process').spawn;
|
|
||||||
const fs = require('fs');
|
|
||||||
const http = require('http');
|
|
||||||
const path = require('path');
|
|
||||||
const urlLib = require('url');
|
|
||||||
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
|
||||||
|
|
||||||
|
|
||||||
// url: string
|
|
||||||
// onSuccess: function (SourceMapConsumer)
|
|
||||||
// onFailure: function (string)
|
|
||||||
function getSourceMapForUrl(url, onFailure, onSuccess) {
|
|
||||||
if (!url) {
|
|
||||||
onFailure('must provide a URL');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url === 'assets://default_bundle') {
|
|
||||||
onFailure('Don\'t know how to symbolicate in-app bundle, please load from server');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedUrl = urlLib.parse(url);
|
|
||||||
const mapPath = parsedUrl.pathname.replace(/\.bundle$/, '.map');
|
|
||||||
const options = {
|
|
||||||
host: 'localhost',
|
|
||||||
port: parsedUrl.port,
|
|
||||||
path: mapPath + parsedUrl.search + '&babelSourcemap=true',
|
|
||||||
};
|
|
||||||
|
|
||||||
http.get(options, (res) => {
|
|
||||||
res.setEncoding('utf8');
|
|
||||||
let sawEnd = false;
|
|
||||||
let resBody = '';
|
|
||||||
res.on('data', (chunk) => {
|
|
||||||
resBody += chunk;
|
|
||||||
}).on('end', () => {
|
|
||||||
sawEnd = true;
|
|
||||||
onSuccess(new SourceMapConsumer(resBody));
|
|
||||||
}).on('close', (err) => {
|
|
||||||
if (!sawEnd) {
|
|
||||||
onFailure('Connection terminated prematurely because of: '
|
|
||||||
+ err.code + ' for url: ' + url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).on('error', (err) => {
|
|
||||||
onFailure('Could not get response from: ' + url + ', error: ' + err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// capture: capture object
|
|
||||||
// onSuccess: function (Map of url -> SourceMapConsumer)
|
|
||||||
// onFailure: function (string)
|
|
||||||
function getSourceMapsForCapture(capture, onFailure, onSuccess) {
|
|
||||||
const urls = new Set();
|
|
||||||
const sourcemaps = new Map();
|
|
||||||
for (const id in capture.refs) {
|
|
||||||
const ref = capture.refs[id];
|
|
||||||
if ((ref.type === 'ScriptExecutable' ||
|
|
||||||
ref.type === 'EvalExecutable' ||
|
|
||||||
ref.type === 'ProgramExecutable' ||
|
|
||||||
ref.type === 'FunctionExecutable') && ref.value.url) {
|
|
||||||
urls.add(ref.value.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
urls.forEach((url) => {
|
|
||||||
getSourceMapForUrl(url, onFailure, (sourcemap) => {
|
|
||||||
sourcemaps.set(url, sourcemap);
|
|
||||||
urls.delete(url);
|
|
||||||
if (urls.size === 0) {
|
|
||||||
onSuccess(sourcemaps);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (urls.size === 0) {
|
|
||||||
console.warn('No source information found in capture');
|
|
||||||
onSuccess(sourcemaps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// capture: capture object
|
|
||||||
// onSuccess: function (capture object)
|
|
||||||
// onFailure: function (string)
|
|
||||||
function symbolicateHeapCaptureFunctions(capture, onFailure, onSuccess) {
|
|
||||||
getSourceMapsForCapture(capture, onFailure, (sourcemaps) => {
|
|
||||||
for (const id in capture.refs) {
|
|
||||||
const ref = capture.refs[id];
|
|
||||||
if (ref.type === 'ScriptExecutable' ||
|
|
||||||
ref.type === 'EvalExecutable' ||
|
|
||||||
ref.type === 'ProgramExecutable' ||
|
|
||||||
ref.type === 'FunctionExecutable') {
|
|
||||||
const sourcemap = sourcemaps.get(ref.value.url);
|
|
||||||
if (sourcemap) {
|
|
||||||
const original = sourcemap.originalPositionFor({
|
|
||||||
line: ref.value.line,
|
|
||||||
column: ref.value.col,
|
|
||||||
});
|
|
||||||
if (original.name) {
|
|
||||||
ref.value.name = original.name;
|
|
||||||
} else if (!ref.value.name) {
|
|
||||||
ref.value.name = path.posix.basename(original.source || '') + ':' + original.line;
|
|
||||||
}
|
|
||||||
ref.value.url = original.source;
|
|
||||||
ref.value.line = original.line;
|
|
||||||
ref.value.col = original.column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onSuccess(capture);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function(req, res, next) {
|
|
||||||
if (req.url !== '/jscheapcaptureupload') {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('symbolicating Heap Capture');
|
|
||||||
symbolicateHeapCaptureFunctions(JSON.parse(req.rawBody), (err) => {
|
|
||||||
console.error('Error when symbolicating: ' + err);
|
|
||||||
},
|
|
||||||
(capture) => {
|
|
||||||
const preload = path.join(__dirname, 'heapCapture/preLoadedCapture.js');
|
|
||||||
fs.writeFileSync(preload, 'var preLoadedCapture = ');
|
|
||||||
fs.appendFileSync(preload, JSON.stringify(capture));
|
|
||||||
fs.appendFileSync(preload, ';');
|
|
||||||
const captureDir = path.join(__dirname, 'heapCapture/captures');
|
|
||||||
if (!fs.existsSync(captureDir)) {
|
|
||||||
fs.mkdirSync(captureDir);
|
|
||||||
}
|
|
||||||
console.log('Packaging Trace');
|
|
||||||
var captureHtml = captureDir + '/capture_' + Date.now() + '.html';
|
|
||||||
var capture = fs.createWriteStream(captureHtml);
|
|
||||||
var inliner = spawn(
|
|
||||||
'inliner',
|
|
||||||
['--nocompress', 'heapCapture.html'],
|
|
||||||
{ cwd: path.join(__dirname, '/heapCapture/'),
|
|
||||||
stdio: [ process.stdin, 'pipe', process.stderr ],
|
|
||||||
});
|
|
||||||
inliner.stdout.pipe(capture);
|
|
||||||
inliner.on('error', (err) => {
|
|
||||||
console.error('Error processing heap capture: ' + err.message);
|
|
||||||
console.error('make sure you have installed inliner with \'npm install inliner -g\'');
|
|
||||||
});
|
|
||||||
inliner.on('exit', (code, signal) => {
|
|
||||||
if (code === 0) {
|
|
||||||
var response = captureHtml;
|
|
||||||
console.log('Heap capture written to: ' + response);
|
|
||||||
res.end(response);
|
|
||||||
} else {
|
|
||||||
var response = 'Error processing heap capture, inliner returned code: ' + code;
|
|
||||||
console.error(response);
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end(response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
@ -20,7 +20,6 @@ const defaultAssetExts = require('../../packager/defaults').assetExts;
|
|||||||
const defaultPlatforms = require('../../packager/defaults').platforms;
|
const defaultPlatforms = require('../../packager/defaults').platforms;
|
||||||
const defaultProvidesModuleNodeModules = require('../../packager/defaults').providesModuleNodeModules;
|
const defaultProvidesModuleNodeModules = require('../../packager/defaults').providesModuleNodeModules;
|
||||||
const getDevToolsMiddleware = require('./middleware/getDevToolsMiddleware');
|
const getDevToolsMiddleware = require('./middleware/getDevToolsMiddleware');
|
||||||
const heapCaptureMiddleware = require('./middleware/heapCaptureMiddleware.js');
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const indexPageMiddleware = require('./middleware/indexPage');
|
const indexPageMiddleware = require('./middleware/indexPage');
|
||||||
const loadRawBodyMiddleware = require('./middleware/loadRawBodyMiddleware');
|
const loadRawBodyMiddleware = require('./middleware/loadRawBodyMiddleware');
|
||||||
@ -46,7 +45,6 @@ function runServer(args, config, readyCallback) {
|
|||||||
.use(copyToClipBoardMiddleware)
|
.use(copyToClipBoardMiddleware)
|
||||||
.use(statusPageMiddleware)
|
.use(statusPageMiddleware)
|
||||||
.use(systraceProfileMiddleware)
|
.use(systraceProfileMiddleware)
|
||||||
.use(heapCaptureMiddleware)
|
|
||||||
.use(cpuProfilerMiddleware)
|
.use(cpuProfilerMiddleware)
|
||||||
.use(indexPageMiddleware)
|
.use(indexPageMiddleware)
|
||||||
.use(unless('/inspector', inspectorProxy.processRequest.bind(inspectorProxy)))
|
.use(unless('/inspector', inspectorProxy.processRequest.bind(inspectorProxy)))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user