refactor aggrow to make adding new sources of data easier

Reviewed By: michalgr

Differential Revision: D3961648

fbshipit-source-id: 3c77d3c1352fd89e12163eee393ffcebe09ea8e3
This commit is contained in:
Charles Dick 2016-10-25 07:06:11 -07:00 committed by Facebook Github Bot
parent cd6f9f95d2
commit 6ddf8a8795
7 changed files with 509 additions and 453 deletions

View File

@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<title>JSC Heap Capture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react-dom.js"></script>
<script src="out/aggrow.js"></script>
<script src="out/table.js"></script>
</head>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@
// pivot around frames in the middle of a stack by callers / callees
// graphing?
function stringInterner() { // eslint-disable-line no-unused-vars
function StringInterner() { // eslint-disable-line no-unused-vars
const strings = [];
const ids = {};
return {
@ -37,20 +37,16 @@ function stringInterner() { // eslint-disable-line no-unused-vars
};
}
function stackData(stackIdMap, maxDepth) { // eslint-disable-line no-unused-vars
return {
maxDepth: maxDepth,
get: function getStack(id) {
return stackIdMap[id];
},
};
}
function stackRegistry() { // eslint-disable-line no-unused-vars
function StackRegistry() { // eslint-disable-line no-unused-vars
return {
root: { id: 0 },
nodeCount: 1,
maxDepth: -1,
stackIdMap: null,
insert: function insertNode(parent, frameId) {
if (this.stackIdMap !== null) {
throw 'stacks already flattened';
}
let node = parent[frameId];
if (node === undefined) {
node = { id: this.nodeCount };
@ -59,7 +55,13 @@ function stackRegistry() { // eslint-disable-line no-unused-vars
}
return node;
},
get: function getStackArray(id) {
return this.stackIdMap[id];
},
flatten: function flattenStacks() {
if (this.stackIdMap !== null) {
return;
}
let stackFrameCount = 0;
function countStacks(tree, depth) {
let leaf = true;
@ -109,13 +111,150 @@ function stackRegistry() { // eslint-disable-line no-unused-vars
return stackIdMap[id];
}
flattenStacksImpl(this.root, []);
return new stackData(stackIdMap, maxStackDepth);
this.root = null;
this.stackIdMap = stackIdMap;
this.maxDepth = maxStackDepth;
},
};
}
function aggrow(numRows) { // eslint-disable-line no-unused-vars
function AggrowData(columns) { // eslint-disable-line no-unused-vars
const columnCount = columns.length;
const columnConverter = columns.map(c => {
switch (c.type) {
case 'int': // stores raw value
return (i) => i;
case 'string': // stores interned id of string
return (s) => c.strings.intern(s);
case 'stack': // stores id of stack node
return (s) => s.id;
default:
throw 'unknown AggrowData column type';
}
});
return {
data: new Int32Array(0),
columns: columns,
rowCount: 0,
rowInserter: function rowInserter(numRows) {
console.log(
'increasing row data from ' + (this.data.length * 4).toLocaleString() + ' B to ' +
(this.data.length * 4 + numRows * columnCount * 4).toLocaleString() + ' B'
);
const newData = new Int32Array(this.data.length + numRows * columnCount);
newData.set(this.data);
let currOffset = this.data.length;
const endOffset = newData.length;
this.data = newData;
this.rowCount = newData.length / columnCount;
return {
insertRow: function insertRow() {
if (currOffset >= endOffset) {
throw 'tried to insert data off end of added range';
}
if (arguments.length !== columnCount) {
throw 'expected data for ' + columnCount.toString() + ' columns, got' +
arguments.length.toString() + ' columns';
}
for (let i = 0; i < arguments.length; i++) {
newData[currOffset + i] = columnConverter[i](arguments[i]);
}
currOffset += columnCount;
},
done: function done() {
if (currOffset !== endOffset) {
throw 'unfilled rows';
}
},
};
},
};
}
function Aggrow(aggrowData) {
const columns = aggrowData.columns;
const columnCount = columns.length;
const data = aggrowData.data;
function columnIndex(columnName, columnType) {
const index = columns.findIndex(c => c.name === columnName && c.type === columnType);
if (index < 0) {
throw 'did not find data column ' + columnName + ' with type ' + columnType;
}
return index;
}
for (let i = 0; i < columns.length; i++) {
if (columns[i].type === 'stack') {
columns[i].stacks.flatten();
}
}
return {
expander: new AggrowExpander(aggrowData.rowCount),
addSumAggregator: function addSumAggregator(aggregatorName, columnName) {
const index = columnIndex(columnName, 'int');
return this.expander.addAggregator(
aggregatorName,
function aggregateSize(indices) {
let size = 0;
for (let i = 0; i < indices.length; i++) {
const row = indices[i];
size += data[row * columnCount + index];
}
return size;
},
(value) => value.toLocaleString(),
(a, b) => b - a,
);
},
addCountAggregator: function addCountAggregator(aggregatorName) {
return this.expander.addAggregator(
aggregatorName,
function aggregateCount(indices) {
return indices.length;
},
(value) => value.toLocaleString(),
(a, b) => b - a,
);
},
addStringExpander: function addStringExpander(expanderName, columnName) {
const index = columnIndex(columnName, 'string');
const strings = columns[index].strings;
return this.expander.addFieldExpander(
expanderName,
(row) => strings.get(data[row * columnCount + index]),
(rowA, rowB) => data[rowA * columnCount + index] - data[rowB * columnCount + index],
);
},
addNumberExpander: function addNumberExpander(expanderName, columnName) {
const index = columnIndex(columnName, 'int');
return this.expander.addFieldExpander(
expanderName,
(row) => data[row * columnCount + index].toLocaleString(),
(rowA, rowB) => data[rowA * columnCount + index] - data[rowB * columnCount + index],
);
},
addPointerExpander: function addPointerExpander(expanderName, columnName) {
const index = columnIndex(columnName, 'int');
return this.expander.addFieldExpander(
expanderName,
(row) => '0x' + (data[row * columnCount + index] >>> 0).toString(),
(rowA, rowB) => data[rowA * columnCount + index] - data[rowB * columnCount + index],
);
},
addStackExpander: function addStackExpander(expanderName, columnName, formatter) {
// TODO: options for caller/callee, pivoting
const index = columnIndex(columnName, 'stack');
const stacks = columns[index].stacks;
return this.expander.addCalleeStackExpander(
expanderName,
stacks.maxDepth,
(row) => stacks.get(data[row * columnCount + index]),
formatter,
);
},
};
}
function AggrowExpander(numRows) { // eslint-disable-line no-unused-vars
// expander ID definitions
const FIELD_EXPANDER_ID_MIN = 0x0000;
const FIELD_EXPANDER_ID_MAX = 0x7fff;

View File

@ -8,7 +8,7 @@
*/
'use strict';
/*eslint no-console-disallow: "off"*/
/*global React ReactDOM Table stringInterner stackRegistry aggrow preLoadedCapture:true*/
/*global React ReactDOM Table StringInterner StackRegistry AggrowData Aggrow preLoadedCapture:true*/
function RefVisitor(refs, id) {
this.refs = refs;
@ -292,189 +292,88 @@ function registerPathToRoot(refs, registry, strings) {
}
}
function captureRegistry() {
const strings = stringInterner();
const stacks = stackRegistry(strings);
const data = new Int32Array(0);
const idField = 0;
const typeField = 1;
const sizeField = 2;
const traceField = 3;
const pathField = 4;
const reactField = 5;
const valueField = 6;
const moduleField = 7;
const numFields = 8;
return {
strings: strings,
stacks: stacks,
data: data,
register: function registerCapture(captureId, capture) {
// NB: capture.refs is potentially VERY large, so we try to avoid making
// copies, even of 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++;
}
console.log(
'increasing row data from ' + (this.data.length * 4).toString() + 'B to ' +
(this.data.length * 4 + rowCount * numFields * 4).toString() + 'B'
);
const newData = new Int32Array(this.data.length + rowCount * numFields);
newData.set(data);
let dataOffset = this.data.length;
this.data = null;
registerPathToRoot(capture.refs, this.stacks, this.strings);
const internedCaptureId = this.strings.intern(captureId);
const noneString = this.strings.intern('#none');
const noneStack = this.stacks.insert(this.stacks.root, noneString);
forEachRef(capture.refs, (visitor) => {
const ref = visitor.getRef();
const id = visitor.id;
newData[dataOffset + idField] = parseInt(id, 16);
newData[dataOffset + typeField] = this.strings.intern(ref.type);
newData[dataOffset + sizeField] = ref.size;
newData[dataOffset + traceField] = internedCaptureId;
if (ref.rootPath === undefined) {
newData[dataOffset + pathField] = noneStack.id;
} else {
newData[dataOffset + pathField] = ref.rootPath.id;
}
if (ref.reactTree === undefined) {
newData[dataOffset + reactField] = noneStack.id;
} else {
newData[dataOffset + reactField] = ref.reactTree.id;
}
newData[dataOffset + valueField] = this.strings.intern(visitor.getValue());
if (ref.module) {
newData[dataOffset + moduleField] = this.strings.intern(ref.module);
} else {
newData[dataOffset + moduleField] = noneString;
}
dataOffset += numFields;
});
for (const id in capture.markedBlocks) {
const block = capture.markedBlocks[id];
newData[dataOffset + idField] = parseInt(id, 16);
newData[dataOffset + typeField] = this.strings.intern('Marked Block Overhead');
newData[dataOffset + sizeField] = block.capacity - block.size;
newData[dataOffset + traceField] = internedCaptureId;
newData[dataOffset + pathField] = noneStack.id;
newData[dataOffset + reactField] = noneStack.id;
newData[dataOffset + valueField] = this.strings.intern(
'capacity: ' + block.capacity +
', size: ' + block.size +
', granularity: ' + block.cellSize
);
newData[dataOffset + moduleField] = noneString;
dataOffset += numFields;
}
this.data = newData;
},
getAggrow: function getAggrow() {
const agStrings = this.strings;
const agStacks = this.stacks.flatten();
const agData = this.data;
const agNumRows = agData.length / numFields;
const ag = new aggrow(agNumRows);
ag.addFieldExpander('Id',
function getId(row) {
let id = agData[row * numFields + idField];
if (id < 0) {
id += 0x100000000; // data is int32, id is uint32
}
return '0x' + id.toString(16);
},
function compareAddress(rowA, rowB) {
return agData[rowA * numFields + idField] - agData[rowB * numFields + idField];
});
const typeExpander = ag.addFieldExpander('Type',
function getType(row) { return agStrings.get(agData[row * numFields + typeField]); },
function compareType(rowA, rowB) {
return agData[rowA * numFields + typeField] - agData[rowB * numFields + typeField];
});
ag.addFieldExpander('Size',
function getSize(row) { return agData[row * numFields + sizeField].toString(); },
function compareSize(rowA, rowB) {
return agData[rowA * numFields + sizeField] - agData[rowB * numFields + sizeField];
});
ag.addFieldExpander('Trace',
function getSize(row) { return agStrings.get(agData[row * numFields + traceField]); },
function compareSize(rowA, rowB) {
return agData[rowA * numFields + traceField] - agData[rowB * numFields + traceField];
});
const pathExpander = ag.addCalleeStackExpander(
'Path',
agStacks.maxDepth,
function getStack(row) { return agStacks.get(agData[row * numFields + pathField]); },
function getFrame(id) { return agStrings.get(id); },
);
const reactExpander = ag.addCalleeStackExpander(
'React Tree',
agStacks.maxDepth,
function getStack(row) { return agStacks.get(agData[row * numFields + reactField]); },
function getFrame(id) { return agStrings.get(id); },
);
const valueExpander = ag.addFieldExpander('Value',
function getValue(row) { return agStrings.get(agData[row * numFields + valueField]); },
function compareValue(rowA, rowB) {
return agData[rowA * numFields + valueField] - agData[rowB * numFields + valueField];
});
const moduleExpander = ag.addFieldExpander('Module',
function getModule(row) { return agStrings.get(agData[row * numFields + moduleField]); },
function compareModule(rowA, rowB) {
return agData[rowA * numFields + moduleField] - agData[rowB * numFields + moduleField];
});
const sizeAggregator = ag.addAggregator('Size',
function aggregateSize(indices) {
let size = 0;
for (let i = 0; i < indices.length; i++) {
const row = indices[i];
size += agData[row * numFields + sizeField];
}
return size;
},
function formatSize(value) { return value.toString(); },
function sortSize(a, b) { return b - a; } );
const countAggregator = ag.addAggregator('Count',
function aggregateCount(indices) {
return indices.length;
},
function formatCount(value) { return value.toString(); },
function sortCount(a, b) { return b - a; } );
ag.setActiveExpanders([
pathExpander,
reactExpander,
moduleExpander,
typeExpander,
valueExpander,
]);
ag.setActiveAggregators([sizeAggregator, countAggregator]);
return ag;
},
};
function registerCapture(data, captureId, capture, stacks, strings) {
// NB: capture.refs is potentially VERY large, so we try to avoid making
// copies, even if iteration is a bit more annoying.
let rowCount = 0;
for (const id in capture.refs) { // eslint-disable-line no-unused-vars
rowCount++;
}
for (const id in capture.markedBlocks) { // eslint-disable-line no-unused-vars
rowCount++;
}
const inserter = data.rowInserter(rowCount);
registerPathToRoot(capture.refs, stacks, strings);
const noneString = strings.intern('#none');
const noneStack = stacks.insert(stacks.root, noneString);
forEachRef(capture.refs, (visitor) => {
// want to data.append(value, value, value), not IDs
const ref = visitor.getRef();
const id = visitor.id;
inserter.insertRow(
parseInt(id, 16),
ref.type,
ref.size,
captureId,
ref.rootPath === undefined ? noneStack : ref.rootPath,
ref.reactTree === undefined ? noneStack : ref.reactTree,
visitor.getValue(),
ref.module === undefined ? '#none' : ref.module,
);
});
for (const id in capture.markedBlocks) {
const block = capture.markedBlocks[id];
inserter.insertRow(
parseInt(id, 16),
'Marked Block Overhead',
block.capacity - block.size,
captureId,
noneStack,
noneStack,
'capacity: ' + block.capacity + ', size: ' + block.size + ', granularity: ' + block.cellSize,
'#none',
);
}
inserter.done();
}
if (preLoadedCapture) {
const r = new captureRegistry();
r.register('trace', preLoadedCapture);
const strings = StringInterner();
const stacks = new StackRegistry();
const columns = [
{ name: 'id', type: 'int' },
{ name: 'type', type: 'string', strings: strings },
{ name: 'size', type: 'int' },
{ name: 'trace', type: 'string', strings: strings },
{ name: 'path', type: 'stack', stacks: stacks },
{ name: 'react', type: 'stack', stacks: stacks },
{ 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
ReactDOM.render(<Table aggrow={r.getAggrow()} />, document.body);
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', strings.get);
const reactExpander = aggrow.addStackExpander('React Tree', 'react', strings.get);
const valueExpander = aggrow.addStringExpander('Value', 'value');
const moduleExpander = aggrow.addStringExpander('Module', 'module');
aggrow.expander.setActiveExpanders([
pathExpander,
reactExpander,
moduleExpander,
typeExpander,
valueExpander,
]);
const sizeAggregator = aggrow.addSumAggregator('Size', 'size');
const countAggregator = aggrow.addCountAggregator('Count');
aggrow.expander.setActiveAggregators([
sizeAggregator,
countAggregator,
]);
ReactDOM.render(<Table aggrow={aggrow.expander} />, document.body);
}

View File

@ -24,12 +24,14 @@ class Draggable extends React.Component { // eslint-disable-line no-unused-vars
render() {
const id = this.props.id;
function dragStart(e) {
e.dataTransfer.setData('text/plain', id);
}
return React.cloneElement(
this.props.children,
{ draggable: 'true', onDragStart: dragStart }
{
draggable: 'true',
onDragStart: (e) => {
e.dataTransfer.setData('text', id);
},
}
);
}
}
@ -45,23 +47,15 @@ class DropTarget extends React.Component { // eslint-disable-line no-unused-vars
render() {
const thisId = this.props.id;
const dropFilter = this.props.dropFilter;
const dropAction = this.props.dropAction;
return React.cloneElement(
this.props.children,
{
onDragOver: (e) => {
const sourceId = e.dataTransfer.getData('text/plain');
if (dropFilter(sourceId)) {
e.preventDefault();
}
},
onDragOver: (e) => e.preventDefault(),
onDrop: (e) => {
const sourceId = e.dataTransfer.getData('text/plain');
if (dropFilter(sourceId)) {
e.preventDefault();
dropAction(sourceId, thisId);
}
const sourceId = e.dataTransfer.getData('text');
e.preventDefault();
dropAction(sourceId, thisId);
},
}
);
@ -71,7 +65,6 @@ class DropTarget extends React.Component { // eslint-disable-line no-unused-vars
DropTarget.propTypes = {
children: React.PropTypes.element.isRequired,
id: React.PropTypes.string.isRequired,
dropFilter: React.PropTypes.func.isRequired,
dropAction: React.PropTypes.func.isRequired,
};
@ -156,7 +149,6 @@ class TableHeader extends React.Component {
headers.push((
<DropTarget
id={'aggregate:insert:' + i.toString()}
dropFilter={(s) => s.startsWith('aggregate')}
dropAction={this.props.dropAction}
>
<div style={{
@ -173,7 +165,6 @@ class TableHeader extends React.Component {
headers.push((
<DropTarget
id="divider:insert"
dropFilter={(s) => s.startsWith('aggregate') || s.startsWith('expander')}
dropAction={this.props.dropAction}
>
<div style={{
@ -200,7 +191,6 @@ class TableHeader extends React.Component {
headers.push((
<DropTarget
id={'expander:insert:' + (i + 1).toString()}
dropFilter={()=>{return true; }}
dropAction={this.props.dropAction}
>
<div style={{