Process heap capture into trace html

Reviewed By: bestander

Differential Revision: D3642188

fbshipit-source-id: c9a4699b2a0d60eb5961333dec45941085e19324
This commit is contained in:
Charles Dick 2016-08-08 04:27:53 -07:00 committed by Facebook Github Bot 7
parent cb59264e73
commit 54f867f0d6
10 changed files with 2509 additions and 5 deletions

View File

@ -0,0 +1,2 @@
/captures/*
preLoadedCapture.js

View File

@ -0,0 +1,5 @@
all:
NODE_PATH="../../../../node_modules/" babel --presets babel-preset-react-native -d out src
watch:
NODE_PATH="../../../../node_modules/" babel --watch --presets babel-preset-react-native -d out src

View File

@ -0,0 +1,16 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" style="height: 100%;">
<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="out/aggrow.js"></script>
<script src="out/table.js"></script>
</head>
<body style="margin:0px; height: 100%">
Loading... This could take a while depending on how big the profile is. Check devtools console for errors.
</body>
<script src="preLoadedCapture.js"></script>
<script src="out/heapCapture.js"></script>
</html>

View File

@ -0,0 +1,616 @@
/**
* 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-bitwise: "off"*/
/*eslint no-console-disallow: "off"*/
// TODO: future features
// put in a module.exports
// filtering / search
// pivot around frames in the middle of a stack by callers / callees
// graphing?
function stringInterner(){ // eslint-disable-line no-unused-vars
var strings=[];
var ids={};
return {
intern:function internString(s){
var find=ids[s];
if(find===undefined){
var id=strings.length;
ids[s]=id;
strings.push(s);
return id;}else
{
return find;}},
get:function getString(id){
return strings[id];}};}
function stackData(stackIdMap,maxDepth){ // eslint-disable-line no-unused-vars
return {
maxDepth:maxDepth,
get:function getStack(id){
return stackIdMap[id];}};}
function stackRegistry(interner){ // eslint-disable-line no-unused-vars
return {
root:{id:0},
nodeCount:1,
insert:function insertNode(parent,label){
var labelId=interner.intern(label);
var node=parent[labelId];
if(node===undefined){
node={id:this.nodeCount};
this.nodeCount++;
parent[labelId]=node;}
return node;},
flatten:function flattenStacks(){
var stackFrameCount=0;
function countStacks(tree,depth){
var leaf=true;
for(var frameId in tree){
if(frameId!=='id'){
leaf=countStacks(tree[frameId],depth+1);}}
if(leaf){
stackFrameCount+=depth;}
return false;}
countStacks(this.root,0);
console.log('size needed to store stacks: '+(stackFrameCount*4).toString()+'B');
var stackIdMap=new Array(this.nodeCount);
var stackArray=new Int32Array(stackFrameCount);
var maxStackDepth=0;
stackFrameCount=0;
function flattenStacksImpl(tree,stack){
var childStack=void 0;
maxStackDepth=Math.max(maxStackDepth,stack.length);
for(var frameId in tree){
if(frameId!=='id'){
stack.push(Number(frameId));
childStack=flattenStacksImpl(tree[frameId],stack);
stack.pop();}}
var id=tree.id;
if(id<0||id>=stackIdMap.length||stackIdMap[id]!==undefined){
throw '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
{
var newStack=stackArray.subarray(stackFrameCount,stackFrameCount+stack.length);
stackFrameCount+=stack.length;
for(var i=0;i<stack.length;i++){
newStack[i]=stack[i];}
stackIdMap[id]=newStack;}
return stackIdMap[id];}
flattenStacksImpl(this.root,[]);
return new stackData(stackIdMap,maxStackDepth);}};}
function aggrow(strings,stacks,numRows){ // eslint-disable-line no-unused-vars
// expander ID definitions
var FIELD_EXPANDER_ID_MIN=0x0000;
var FIELD_EXPANDER_ID_MAX=0x7fff;
var STACK_EXPANDER_ID_MIN=0x8000;
var STACK_EXPANDER_ID_MAX=0xffff;
// used for row.expander which reference state.activeExpanders (with frame index masked in)
var INVALID_ACTIVE_EXPANDER=-1;
var ACTIVE_EXPANDER_MASK=0xffff;
var ACTIVE_EXPANDER_FRAME_SHIFT=16;
// aggregator ID definitions
var AGGREGATOR_ID_MAX=0xffff;
// active aggragators can have sort order changed in the reference
var ACTIVE_AGGREGATOR_MASK=0xffff;
var ACTIVE_AGGREGATOR_ASC_BIT=0x10000;
// tree node state definitions
var NODE_EXPANDED_BIT=0x0001; // this row is expanded
var NODE_REAGGREGATE_BIT=0x0002; // children need aggregates
var NODE_REORDER_BIT=0x0004; // children need to be sorted
var NODE_REPOSITION_BIT=0x0008; // children need position
var NODE_INDENT_SHIFT=16;
function calleeFrameGetter(stack,depth){
return stack[depth];}
function callerFrameGetter(stack,depth){
return stack[stack.length-depth-1];}
function createStackComparers(stackGetter,frameGetter){
var comparers=new Array(stacks.maxDepth);var _loop=function _loop(
depth){
var captureDepth=depth; // NB: to capture depth per loop iteration
comparers[depth]=function calleeStackComparer(rowA,rowB){
var a=stackGetter(rowA);
var 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 frameGetter(a,captureDepth)-frameGetter(b,captureDepth);};};for(var depth=0;depth<stacks.maxDepth;depth++){_loop(depth);}
return comparers;}
function createTreeNode(parent,label,indices,expander){
var indent=parent===null?0:(parent.state>>>NODE_INDENT_SHIFT)+1;
var state=NODE_REPOSITION_BIT|
NODE_REAGGREGATE_BIT|
NODE_REORDER_BIT|
indent<<NODE_INDENT_SHIFT;
return {
parent:parent, // null if root
children:null, // array of children nodes
label:label, // string to show in UI
indices:indices, // row indices under this node
aggregates:null, // result of aggregate on indices
expander:expander, // index into state.activeExpanders
top:0, // y position of top row (in rows)
height:1, // number of rows including children
state:state};} // see NODE_* definitions above
function noSortOrder(a,b){
return 0;}
var indices=new Int32Array(numRows);
for(var i=0;i<numRows;i++){
indices[i]=i;}
var state={
fieldExpanders:[], // tree expanders that expand on simple values
stackExpanders:[], // tree expanders that expand stacks
activeExpanders:[], // index into field or stack expanders, hierarchy of tree
aggregators:[], // all available aggregators, might not be used
activeAggregators:[], // index into aggregators, to actually compute
sorter:noSortOrder, // compare function that uses sortOrder to sort row.children
root:createTreeNode(null,'<root>',indices,INVALID_ACTIVE_EXPANDER)};
function evaluateAggregate(row){
var activeAggregators=state.activeAggregators;
var aggregates=new Array(activeAggregators.length);
for(var j=0;j<activeAggregators.length;j++){
var aggregator=state.aggregators[activeAggregators[j]];
aggregates[j]=aggregator.aggregator(row.indices);}
row.aggregates=aggregates;
row.state|=NODE_REAGGREGATE_BIT;}
function evaluateAggregates(row){
if((row.state&NODE_EXPANDED_BIT)!==0){
var children=row.children;
for(var _i=0;_i<children.length;_i++){
evaluateAggregate(children[_i]);}
row.state|=NODE_REORDER_BIT;}
row.state^=NODE_REAGGREGATE_BIT;}
function evaluateOrder(row){
if((row.state&NODE_EXPANDED_BIT)!==0){
var children=row.children;
for(var _i2=0;_i2<children.length;_i2++){
var child=children[_i2];
child.state|=NODE_REORDER_BIT;}
children.sort(state.sorter);
row.state|=NODE_REPOSITION_BIT;}
row.state^=NODE_REORDER_BIT;}
function evaluatePosition(row){
if((row.state&NODE_EXPANDED_BIT)!==0){
var children=row.children;
var childTop=row.top+1;
for(var _i3=0;_i3<children.length;_i3++){
var child=children[_i3];
if(child.top!==childTop){
child.top=childTop;
child.state|=NODE_REPOSITION_BIT;}
childTop+=child.height;}}
row.state^=NODE_REPOSITION_BIT;}
function getRowsImpl(row,top,height,result){
if((row.state&NODE_REAGGREGATE_BIT)!==0){
evaluateAggregates(row);}
if((row.state&NODE_REORDER_BIT)!==0){
evaluateOrder(row);}
if((row.state&NODE_REPOSITION_BIT)!==0){
evaluatePosition(row);}
if(row.top>=top&&row.top<top+height){
if(result[row.top-top]!=null){
throw 'getRows put more than one row at position '+row.top+' into result';}
result[row.top-top]=row;}
if((row.state&NODE_EXPANDED_BIT)!==0){
var children=row.children;
for(var _i4=0;_i4<children.length;_i4++){
var child=children[_i4];
if(child.top<top+height&&top<child.top+child.height){
getRowsImpl(child,top,height,result);}}}}
function updateHeight(row,heightChange){
while(row!==null){
row.height+=heightChange;
row.state|=NODE_REPOSITION_BIT;
row=row.parent;}}
function addChildrenWithFieldExpander(row,expander,nextActiveIndex){
var rowIndices=row.indices;
var comparer=expander.comparer;
rowIndices.sort(comparer);
var begin=0;
var end=1;
row.children=[];
while(end<rowIndices.length){
if(comparer(rowIndices[begin],rowIndices[end])!==0){
row.children.push(createTreeNode(
row,
expander.name+': '+expander.formatter(rowIndices[begin]),
rowIndices.subarray(begin,end),
nextActiveIndex));
begin=end;}
end++;}
row.children.push(createTreeNode(
row,
expander.name+': '+expander.formatter(rowIndices[begin]),
rowIndices.subarray(begin,end),
nextActiveIndex));}
function addChildrenWithStackExpander(row,expander,activeIndex,depth,nextActiveIndex){
var rowIndices=row.indices;
var stackGetter=expander.stackGetter;
var frameGetter=expander.frameGetter;
var comparer=expander.comparers[depth];
var expandNextFrame=activeIndex|depth+1<<ACTIVE_EXPANDER_FRAME_SHIFT;
rowIndices.sort(comparer);
var columnName='';
if(depth===0){
columnName=expander.name+': ';}
// put all the too-short stacks under <exclusive>
var begin=0;
var beginStack=null;
row.children=[];
while(begin<rowIndices.length){
beginStack=stackGetter(rowIndices[begin]);
if(beginStack.length>depth){
break;}
begin++;}
if(begin>0){
row.children.push(createTreeNode(
row,
columnName+'<exclusive>',
rowIndices.subarray(0,begin),
nextActiveIndex));}
// aggregate the rest under frames
if(begin<rowIndices.length){
var end=begin+1;
while(end<rowIndices.length){
var endStack=stackGetter(rowIndices[end]);
if(frameGetter(beginStack,depth)!==frameGetter(endStack,depth)){
row.children.push(createTreeNode(
row,
columnName+strings.get(frameGetter(beginStack,depth)),
rowIndices.subarray(begin,end),
expandNextFrame));
begin=end;
beginStack=endStack;}
end++;}
row.children.push(createTreeNode(
row,
columnName+strings.get(frameGetter(beginStack,depth)),
rowIndices.subarray(begin,end),
expandNextFrame));}}
function contractRow(row){
if((row.state&NODE_EXPANDED_BIT)===0){
throw 'can not contract row, already contracted';}
row.state^=NODE_EXPANDED_BIT;
var heightChange=1-row.height;
updateHeight(row,heightChange);}
function pruneExpanders(row,oldExpander,newExpander){
row.state|=NODE_REPOSITION_BIT;
if(row.expander===oldExpander){
row.state|=NODE_REAGGREGATE_BIT|NODE_REORDER_BIT|NODE_REPOSITION_BIT;
if((row.state&NODE_EXPANDED_BIT)!==0){
contractRow(row);}
row.children=null;
row.expander=newExpander;}else
{
row.state|=NODE_REPOSITION_BIT;
var children=row.children;
if(children!=null){
for(var _i5=0;_i5<children.length;_i5++){
var child=children[_i5];
pruneExpanders(child,oldExpander,newExpander);}}}}
return {
addFieldExpander:function addFieldExpander(name,formatter,comparer){
if(FIELD_EXPANDER_ID_MIN+state.fieldExpanders.length>=FIELD_EXPANDER_ID_MAX){
throw 'too many field expanders!';}
state.fieldExpanders.push({
name:name, // name for column
formatter:formatter, // row index -> display string
comparer:comparer}); // compares by two row indices
return FIELD_EXPANDER_ID_MIN+state.fieldExpanders.length-1;},
addCalleeStackExpander:function addCalleeStackExpander(name,stackGetter){
if(STACK_EXPANDER_ID_MIN+state.fieldExpanders.length>=STACK_EXPANDER_ID_MAX){
throw 'too many stack expanders!';}
state.stackExpanders.push({
name:name, // name for column
stackGetter:stackGetter, // row index -> stack array
comparers:createStackComparers(stackGetter,calleeFrameGetter), // depth -> comparer
frameGetter:calleeFrameGetter}); // (stack, depth) -> string id
return STACK_EXPANDER_ID_MIN+state.stackExpanders.length-1;},
addCallerStackExpander:function addCallerStackExpander(name,stackGetter){
if(STACK_EXPANDER_ID_MIN+state.fieldExpanders.length>=STACK_EXPANDER_ID_MAX){
throw 'too many stack expanders!';}
state.stackExpanders.push({
name:name,
stackGetter:stackGetter,
comparers:createStackComparers(stackGetter,callerFrameGetter),
frameGetter:callerFrameGetter});
return STACK_EXPANDER_ID_MIN+state.stackExpanders.length-1;},
getExpanders:function getExpanders(){
var expanders=[];
for(var _i6=0;_i6<state.fieldExpanders.length;_i6++){
expanders.push(FIELD_EXPANDER_ID_MIN+_i6);}
for(var _i7=0;_i7<state.stackExpanders.length;_i7++){
expanders.push(STACK_EXPANDER_ID_MIN+_i7);}
return expanders;},
getExpanderName:function getExpanderName(id){
if(id>=FIELD_EXPANDER_ID_MIN&&id<=FIELD_EXPANDER_ID_MAX){
return state.fieldExpanders[id-FIELD_EXPANDER_ID_MIN].name;}else
if(id>=STACK_EXPANDER_ID_MIN&&id<=STACK_EXPANDER_ID_MAX){
return state.stackExpanders[id-STACK_EXPANDER_ID_MIN].name;}
throw 'Unknown expander ID '+id.toString();},
setActiveExpanders:function setActiveExpanders(ids){
for(var _i8=0;_i8<ids.length;_i8++){
var id=ids[_i8];
if(id>=FIELD_EXPANDER_ID_MIN&&id<=FIELD_EXPANDER_ID_MAX){
if(id-FIELD_EXPANDER_ID_MIN>=state.fieldExpanders.length){
throw 'field expander for id '+id.toString()+' does not exist!';}}else
if(id>=STACK_EXPANDER_ID_MIN&&id<=STACK_EXPANDER_ID_MAX){
if(id-STACK_EXPANDER_ID_MIN>=state.stackExpanders.length){
throw 'stack expander for id '+id.toString()+' does not exist!';}}}
for(var _i9=0;_i9<ids.length;_i9++){
if(state.activeExpanders.length<=_i9){
pruneExpanders(state.root,INVALID_ACTIVE_EXPANDER,_i9);
break;}else
if(ids[_i9]!==state.activeExpanders[_i9]){
pruneExpanders(state.root,_i9,_i9);
break;}}
// TODO: if ids is prefix of activeExpanders, we need to make an expander invalid
state.activeExpanders=ids.slice();},
getActiveExpanders:function getActiveExpanders(){
return state.activeExpanders.slice();},
addAggregator:function addAggregator(name,aggregator,formatter,sorter){
if(state.aggregators.length>=AGGREGATOR_ID_MAX){
throw 'too many aggregators!';}
state.aggregators.push({
name:name, // name for column
aggregator:aggregator, // index array -> aggregate value
formatter:formatter, // aggregate value -> display string
sorter:sorter}); // compare two aggregate values
return state.aggregators.length-1;},
getAggregators:function getAggregators(){
var aggregators=[];
for(var _i10=0;_i10<state.aggregators.length;_i10++){
aggregators.push(_i10);}
return aggregators;},
getAggregatorName:function getAggregatorName(id){
return state.aggregators[id&ACTIVE_AGGREGATOR_MASK].name;},
setActiveAggregators:function setActiveAggregators(ids){
for(var _i11=0;_i11<ids.length;_i11++){
var id=ids[_i11]&ACTIVE_AGGREGATOR_MASK;
if(id<0||id>state.aggregators.length){
throw 'aggregator id '+id.toString()+' not valid';}}
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
evaluateAggregate(state.root);
var sorter=noSortOrder;var _loop2=function _loop2(
_i12){
var ascending=(ids[_i12]&ACTIVE_AGGREGATOR_ASC_BIT)!==0;
var id=ids[_i12]&ACTIVE_AGGREGATOR_MASK;
var comparer=state.aggregators[id].sorter;
var captureSorter=sorter;
var captureIndex=_i12;
sorter=function sorter(a,b){
var c=comparer(a.aggregates[captureIndex],b.aggregates[captureIndex]);
if(c===0){
return captureSorter(a,b);}
return ascending?-c:c;};};for(var _i12=ids.length-1;_i12>=0;_i12--){_loop2(_i12);}
state.sorter=sorter;
state.root.state|=NODE_REORDER_BIT;},
getActiveAggregators:function getActiveAggregators(){
return state.activeAggregators.slice();},
getRows:function getRows(top,height){
var result=new Array(height);
for(var _i13=0;_i13<height;_i13++){
result[_i13]=null;}
getRowsImpl(state.root,top,height,result);
return result;},
getRowLabel:function getRowLabel(row){
return row.label;},
getRowIndent:function getRowIndent(row){
return row.state>>>NODE_INDENT_SHIFT;},
getRowAggregate:function getRowAggregate(row,index){
var aggregator=state.aggregators[state.activeAggregators[index]];
return aggregator.formatter(row.aggregates[index]);},
getHeight:function getHeight(){
return state.root.height;},
canExpand:function canExpand(row){
return (row.state&NODE_EXPANDED_BIT)===0&&row.expander!==INVALID_ACTIVE_EXPANDER;},
canContract:function canContract(row){
return (row.state&NODE_EXPANDED_BIT)!==0;},
expand:function expand(row){
if((row.state&NODE_EXPANDED_BIT)!==0){
throw 'can not expand row, already expanded';}
if(row.height!==1){
throw 'unexpanded row has height '+row.height.toString()+' != 1';}
if(row.children===null){ // first expand, generate children
var activeIndex=row.expander&ACTIVE_EXPANDER_MASK;
var nextActiveIndex=activeIndex+1; // NB: if next is stack, frame is 0
if(nextActiveIndex>=state.activeExpanders.length){
nextActiveIndex=INVALID_ACTIVE_EXPANDER;}
if(activeIndex>=state.activeExpanders.length){
throw 'invalid active expander index '+activeIndex.toString();}
var exId=state.activeExpanders[activeIndex];
if(exId>=FIELD_EXPANDER_ID_MIN&&
exId<FIELD_EXPANDER_ID_MIN+state.fieldExpanders.length){
var expander=state.fieldExpanders[exId-FIELD_EXPANDER_ID_MIN];
addChildrenWithFieldExpander(row,expander,nextActiveIndex);}else
if(exId>=STACK_EXPANDER_ID_MIN&&
exId<STACK_EXPANDER_ID_MIN+state.stackExpanders.length){
var depth=row.expander>>>ACTIVE_EXPANDER_FRAME_SHIFT;
var _expander=state.stackExpanders[exId-STACK_EXPANDER_ID_MIN];
addChildrenWithStackExpander(row,_expander,activeIndex,depth,nextActiveIndex);}else
{
throw 'state.activeIndex '+activeIndex.toString()+
' has invalid expander'+exId.toString();}}
row.state|=NODE_EXPANDED_BIT|
NODE_REAGGREGATE_BIT|NODE_REORDER_BIT|NODE_REPOSITION_BIT;
var heightChange=0;
for(var _i14=0;_i14<row.children.length;_i14++){
heightChange+=row.children[_i14].height;}
updateHeight(row,heightChange);
// if children only contains one node, then expand it as well
if(row.children.length===1&&this.canExpand(row.children[0])){
this.expand(row.children[0]);}},
contract:function contract(row){
contractRow(row);}};}

View File

@ -0,0 +1,284 @@
/**
* 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 React ReactDOM Table stringInterner stackRegistry aggrow preLoadedCapture:true*/var _jsxFileName='src/heapCapture.js';
function registerReactComponentTreeImpl(refs,registry,parents,inEdgeNames,trees,id){
if(parents[id]===undefined){
// not a component
}else if(parents[id]===null){
trees[id]=registry.insert(registry.root,'<internalInstance>');}else
{
var parent=parents[id];
var inEdgeName=inEdgeNames[id];
var parentTree=trees[parent];
if(parentTree===undefined){
parentTree=registerReactComponentTreeImpl(
refs,
registry,
parents,
inEdgeNames,
trees,
parent);}
trees[id]=registry.insert(parentTree,inEdgeName);}
return trees[id];}
// TODO: make it easier to query the heap graph, it's super annoying to deal with edges directly
function registerReactComponentTree(refs,registry){
// build list of parents for react interal instances, so we can connect a tree
var parents={};
var inEdgeNames={};
for(var id in refs){
var ref=refs[id];
for(var linkId in ref.edges){
if(linkId!=='0x0'){
var name=ref.edges[linkId];
if(name==='_renderedChildren'){
if(parents[id]===undefined){
// mark that we are a react component, even if we don't have a parent
parents[id]=null;}
var childrenRef=refs[linkId];
for(var childId in childrenRef.edges){
var linkName=childrenRef.edges[childId];
if(linkName.startsWith('.')){
parents[childId]=id;
inEdgeNames[childId]=linkName;}}}else
if(name==='_renderedComponent'){
if(parents[id]===undefined){
parents[id]=null;}
parents[linkId]=id;
inEdgeNames[linkId]='_renderedComponent';}}}}
// build tree of react internal instances (since that's what has the structure)
var trees={};
for(var _id in refs){
registerReactComponentTreeImpl(refs,registry,parents,inEdgeNames,trees,_id);}
// hook in components by looking at their _reactInternalInstance fields
for(var _id2 in refs){
var _ref=refs[_id2];
for(var _linkId in _ref.edges){
var _name=_ref.edges[_linkId];
if(_name==='_reactInternalInstance'){
if(trees[_linkId]!==undefined){
trees[_id2]=registry.insert(trees[_linkId],'<component>');}}}}
return trees;}
function registerPathToRoot(roots,refs,registry,reactComponentTree){
var visited={};
var breadth=[];
for(var i=0;i<roots.length;i++){
var id=roots[i];
if(visited[id]===undefined){
var ref=refs[id];
visited[id]=registry.insert(registry.root,ref.type);
breadth.push(id);}}
while(breadth.length>0){
var nextBreadth=[];var _loop=function _loop(
_i){
var id=breadth[_i];
var ref=refs[id];
var node=visited[id];
// TODO: make edges map id -> name, (empty for none) seems that would be better
var edges=Object.getOwnPropertyNames(ref.edges);
edges.sort(function putUnknownLast(a,b){
var aName=ref.edges[a];
var bName=ref.edges[b];
if(aName===null&&bName!==null){
return 1;}else
if(aName!==null&&bName===null){
return -1;}else
if(aName===null&&bName===null){
return 0;}else
{
return a.localeCompare(b);}});
for(var j=0;j<edges.length;j++){
var edgeId=edges[j];
var edgeName='';
if(ref.edges[edgeId]){
edgeName=ref.edges[edgeId]+': ';}
if(visited[edgeId]===undefined){
var edgeRef=refs[edgeId];
if(edgeRef===undefined){
// TODO: figure out why we have edges that point to things not JSCell
//console.log('registerPathToRoot unable to follow edge from ' + id + ' to ' + edgeId);
}else {
visited[edgeId]=registry.insert(node,edgeName+edgeRef.type);
nextBreadth.push(edgeId);
if(reactComponentTree[edgeId]===undefined){
reactComponentTree[edgeId]=reactComponentTree[id];}}}}};for(var _i=0;_i<breadth.length;_i++){_loop(_i);}
breadth=nextBreadth;}
return visited;}
function captureRegistry(){
var strings=stringInterner();
var stacks=stackRegistry(strings);
var data=new Int32Array(0);
var idField=0;
var typeField=1;
var sizeField=2;
var traceField=3;
var pathField=4;
var reactField=5;
var numFields=6;
return {
strings:strings,
stacks:stacks,
data:data,
register:function registerCapture(captureId,capture){
var rowCount=0;
for(var id in capture.refs){
rowCount++;}
console.log(
'increasing row data from '+(this.data.length*4).toString()+'B to '+
(this.data.length*4+rowCount*numFields*4).toString()+'B');
var newData=new Int32Array(this.data.length+rowCount*numFields);
newData.set(data);
var dataOffset=this.data.length;
this.data=null;
var reactComponentTreeMap=registerReactComponentTree(capture.refs,this.stacks);
var rootPathMap=registerPathToRoot(
capture.roots,
capture.refs,
this.stacks,
reactComponentTreeMap);
var internedCaptureId=this.strings.intern(captureId);
for(var _id3 in capture.refs){
var ref=capture.refs[_id3];
newData[dataOffset+idField]=parseInt(_id3,16);
newData[dataOffset+typeField]=this.strings.intern(ref.type);
newData[dataOffset+sizeField]=ref.size;
newData[dataOffset+traceField]=internedCaptureId;
var pathNode=rootPathMap[_id3];
if(pathNode===undefined){
throw 'did not find path for ref!';}
newData[dataOffset+pathField]=pathNode.id;
var reactTree=reactComponentTreeMap[_id3];
if(reactTree===undefined){
newData[dataOffset+reactField]=
this.stacks.insert(this.stacks.root,'<not-under-tree>').id;}else
{
newData[dataOffset+reactField]=reactTree.id;}
dataOffset+=numFields;}
this.data=newData;},
getAggrow:function getAggrow(){
var agStrings=this.strings;
var agStacks=this.stacks.flatten();
var agData=this.data;
var agNumRows=agData.length/numFields;
var ag=new aggrow(agStrings,agStacks,agNumRows);
var idExpander=ag.addFieldExpander('Id',
function getId(row){
var 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];});
var typeExpander=ag.addFieldExpander('Type',
function getSize(row){return agStrings.get(agData[row*numFields+typeField]);},
function compareSize(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];});
var traceExpander=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];});
var pathExpander=ag.addCalleeStackExpander('Path',
function getStack(row){return agStacks.get(agData[row*numFields+pathField]);});
var reactExpander=ag.addCalleeStackExpander('React Tree',
function getStack(row){return agStacks.get(agData[row*numFields+reactField]);});
var sizeAggregator=ag.addAggregator('Size',
function aggregateSize(indices){
var size=0;
for(var i=0;i<indices.length;i++){
var row=indices[i];
size+=agData[row*numFields+sizeField];}
return size;},
function formatSize(value){return value.toString();},
function sortSize(a,b){return b-a;});
var 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,typeExpander,idExpander,traceExpander]);
ag.setActiveAggregators([sizeAggregator,countAggregator]);
return ag;}};}
if(preLoadedCapture){
var r=new captureRegistry();
r.register('trace',preLoadedCapture);
preLoadedCapture=undefined; // let GG clean up the capture
ReactDOM.render(React.createElement(Table,{aggrow:r.getAggrow(),__source:{fileName:_jsxFileName,lineNumber:284}}),document.body);}

View File

@ -0,0 +1,326 @@
/**
* 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 React:true*/
// TODO:
// selection and arrow keys for navigating
var _jsxFileName='src/table.js';var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value" in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor);}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor;};}();function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function");}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return call&&(typeof call==="object"||typeof call==="function")?call:self;}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass);}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass;}
var rowHeight=20;
var treeIndent=16;var
Draggable=function(_React$Component){_inherits(Draggable,_React$Component); // eslint-disable-line no-unused-vars
function Draggable(props){_classCallCheck(this,Draggable);return _possibleConstructorReturn(this,Object.getPrototypeOf(Draggable).call(this,
props));}_createClass(Draggable,[{key:'render',value:function render()
{
var id=this.props.id;
function dragStart(e){
e.dataTransfer.setData('text/plain',id);}
return React.cloneElement(
this.props.children,
{draggable:'true',onDragStart:dragStart});}}]);return Draggable;}(React.Component);
Draggable.propTypes={
children:React.PropTypes.element.isRequired,
id:React.PropTypes.string.isRequired};var
DropTarget=function(_React$Component2){_inherits(DropTarget,_React$Component2); // eslint-disable-line no-unused-vars
function DropTarget(props){_classCallCheck(this,DropTarget);return _possibleConstructorReturn(this,Object.getPrototypeOf(DropTarget).call(this,
props));}_createClass(DropTarget,[{key:'render',value:function render()
{
var thisId=this.props.id;
var dropFilter=this.props.dropFilter;
var dropAction=this.props.dropAction;
return React.cloneElement(
this.props.children,
{
onDragOver:function onDragOver(e){
var sourceId=e.dataTransfer.getData('text/plain');
if(dropFilter(sourceId)){
e.preventDefault();}},
onDrop:function onDrop(e){
var sourceId=e.dataTransfer.getData('text/plain');
if(dropFilter(sourceId)){
e.preventDefault();
dropAction(sourceId,thisId);}}});}}]);return DropTarget;}(React.Component);
DropTarget.propTypes={
children:React.PropTypes.element.isRequired,
id:React.PropTypes.string.isRequired,
dropFilter:React.PropTypes.func.isRequired,
dropAction:React.PropTypes.func.isRequired};var
Table=function(_React$Component3){_inherits(Table,_React$Component3); // eslint-disable-line no-unused-vars
function Table(props){_classCallCheck(this,Table);var _this3=_possibleConstructorReturn(this,Object.getPrototypeOf(Table).call(this,
props));
_this3.state={
aggrow:props.aggrow,
viewport:{top:0,height:100}};return _this3;}_createClass(Table,[{key:'scroll',value:function scroll(
e){
var viewport=e.target;
var top=Math.floor((viewport.scrollTop-viewport.clientHeight*1.0)/rowHeight);
var height=Math.ceil(viewport.clientHeight*3.0/rowHeight);
this.state.viewport.top=top;
this.state.viewport.height=height;
this.forceUpdate();}},{key:'dropAggregator',value:function dropAggregator(
s,d){
var aggrow=this.state.aggrow;
console.log('dropped '+s+' to '+d);
if(s.startsWith('aggregate:active:')){
var sIndex=parseInt(s.substr(17),10);
var dIndex=-1;
var active=aggrow.getActiveAggregators();
var dragged=active[sIndex];
if(d.startsWith('aggregate:insert:')){
dIndex=parseInt(d.substr(17),10);}else
if(d==='divider:insert'){
dIndex=active.length;}else
{
throw 'not allowed to drag '+s+' to '+d;}
if(dIndex>sIndex){
dIndex--;}
active.splice(sIndex,1);
active.splice(dIndex,0,dragged);
aggrow.setActiveAggregators(active);
this.forceUpdate();}else
if(s.startsWith('expander:active:')){
var _sIndex=parseInt(s.substr(16),10);
var _dIndex=-1;
var _active=aggrow.getActiveExpanders();
var _dragged=_active[_sIndex];
if(d.startsWith('expander:insert:')){
_dIndex=parseInt(d.substr(16),10);}else
if(d==='divider:insert'){
_dIndex=0;}else
{
throw 'not allowed to drag '+s+' to '+d;}
if(_dIndex>_sIndex){
_dIndex--;}
_active.splice(_sIndex,1);
_active.splice(_dIndex,0,_dragged);
aggrow.setActiveExpanders(_active);
this.forceUpdate();}}},{key:'render',value:function render()
{var _this4=this;
var headers=[];
var aggrow=this.state.aggrow;
var aggregators=aggrow.getActiveAggregators();
var expanders=aggrow.getActiveExpanders();
// aggregators
for(var i=0;i<aggregators.length;i++){
var name=aggrow.getAggregatorName(aggregators[i]);
headers.push(
React.createElement(DropTarget,{
id:'aggregate:insert:'+i.toString(),
dropFilter:function dropFilter(){return true;},
dropAction:function dropAction(s,d){_this4.dropAggregator(s,d);},__source:{fileName:_jsxFileName,lineNumber:149}},
React.createElement('div',{style:{
width:'16px',
height:'inherit',
backgroundColor:'darkGray',
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:154}})));
headers.push(React.createElement(Draggable,{id:'aggregate:active:'+i.toString(),__source:{fileName:_jsxFileName,lineNumber:161}},
React.createElement('div',{style:{width:'128px',textAlign:'center',flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:162}},name)));}
headers.push(
React.createElement(DropTarget,{
id:'divider:insert',
dropFilter:function dropFilter(){return true;},
dropAction:function dropAction(s,d){_this4.dropAggregator(s,d);},__source:{fileName:_jsxFileName,lineNumber:166}},
React.createElement('div',{style:{
width:'16px',
height:'inherit',
backgroundColor:'gold',
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:171}})));
for(var _i=0;_i<expanders.length;_i++){
var _name=aggrow.getExpanderName(expanders[_i]);
var bg=_i%2===0?'white':'lightGray';
headers.push(React.createElement(Draggable,{id:'expander:active:'+_i.toString(),__source:{fileName:_jsxFileName,lineNumber:181}},
React.createElement('div',{style:{
width:'128px',
textAlign:'center',
backgroundColor:bg,
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:182}},
_name)));
var sep=_i+1<expanders.length?'->':'...';
headers.push(
React.createElement(DropTarget,{
id:'expander:insert:'+(_i+1).toString(),
dropFilter:function dropFilter(){return true;},
dropAction:function dropAction(s,d){_this4.dropAggregator(s,d);},__source:{fileName:_jsxFileName,lineNumber:193}},
React.createElement('div',{style:{
height:'inherit',
backgroundColor:'darkGray',
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:198}},
sep)));}
return (
React.createElement('div',{style:{width:'100%',height:'100%',display:'flex',flexDirection:'column'},__source:{fileName:_jsxFileName,lineNumber:210}},
React.createElement('div',{style:{
width:'100%',
height:'26px',
display:'flex',
flexDirection:'row',
alignItems:'center',
borderBottom:'2px solid black'},__source:{fileName:_jsxFileName,lineNumber:211}},
headers),
React.createElement('div',{style:{
width:'100%',
flexGrow:'1',
overflow:'scroll'},
onScroll:function onScroll(e){return _this4.scroll(e);},__source:{fileName:_jsxFileName,lineNumber:221}},
React.createElement('div',{style:{position:'relative'},__source:{fileName:_jsxFileName,lineNumber:226}},
this.renderVirtualizedRows()))));}},{key:'renderVirtualizedRows',value:function renderVirtualizedRows()
{var _this5=this;
var aggrow=this.state.aggrow;
var viewport=this.state.viewport;
var rows=aggrow.getRows(viewport.top,viewport.height);
return (
React.createElement('div',{style:{
position:'absolute',
width:'100%',
height:(rowHeight*(aggrow.getHeight()+20)).toString()+'px'},__source:{fileName:_jsxFileName,lineNumber:239}},
rows.map(function(child){return _this5.renderRow(child);})));}},{key:'renderRow',value:function renderRow(
row){var _this6=this;
if(row===null){
return null;}
var bg='lightGray';
var aggrow=this.state.aggrow;
var columns=[];
var rowText='';
var indent=4+aggrow.getRowIndent(row)*treeIndent;
var aggregates=aggrow.getActiveAggregators();
if(row.parent!==null&&row.parent.expander%2===0){
bg='white';}
for(var i=0;i<aggregates.length;i++){
var aggregate=aggrow.getRowAggregate(row,i);
columns.push(
React.createElement('div',{style:{
width:'16px',
height:'inherit',
backgroundColor:'darkGray',
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:265}}));
columns.push(
React.createElement('div',{style:{
width:'128px',
textAlign:'right',
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:273}},
aggregate));}
columns.push(
React.createElement('div',{style:{
width:'16px',
height:'inherit',
backgroundColor:'gold',
flexShrink:'0'},__source:{fileName:_jsxFileName,lineNumber:283}}));
if(aggrow.canExpand(row)){
rowText+='+';}else
if(aggrow.canContract(row)){
rowText+='-';}else
{
rowText+=' ';}
rowText+=aggrow.getRowLabel(row);
columns.push(
React.createElement('div',{style:{
marginLeft:indent.toString()+'px',
flexShrink:'0',
whiteSpace:'nowrap'},__source:{fileName:_jsxFileName,lineNumber:299}},
rowText));
return (
React.createElement('div',{style:{
position:'absolute',
height:(rowHeight-1).toString()+'px',
top:(rowHeight*row.top).toString()+'px',
display:'flex',
flexDirection:'row',
backgroundColor:bg,
borderBottom:'1px solid gray'},
onClick:function onClick(){
if(aggrow.canExpand(row)){
aggrow.expand(row);
_this6.forceUpdate();}else
if(aggrow.canContract(row)){
aggrow.contract(row);
_this6.forceUpdate();}},__source:{fileName:_jsxFileName,lineNumber:308}},
columns));}}]);return Table;}(React.Component);

View File

@ -0,0 +1,619 @@
/**
* 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-bitwise: "off"*/
/*eslint no-console-disallow: "off"*/
// TODO: future features
// put in a module.exports
// filtering / search
// pivot around frames in the middle of a stack by callers / callees
// graphing?
function stringInterner() { // eslint-disable-line no-unused-vars
const strings = [];
const ids = {};
return {
intern: function internString(s) {
const find = ids[s];
if (find === undefined) {
const id = strings.length;
ids[s] = id;
strings.push(s);
return id;
} else {
return find;
}
},
get: function getString(id) {
return strings[id];
},
};
}
function stackData(stackIdMap, maxDepth) { // eslint-disable-line no-unused-vars
return {
maxDepth: maxDepth,
get: function getStack(id) {
return stackIdMap[id];
},
};
}
function stackRegistry(interner) { // eslint-disable-line no-unused-vars
return {
root: { id: 0 },
nodeCount: 1,
insert: function insertNode(parent, label) {
const labelId = interner.intern(label);
let node = parent[labelId];
if (node === undefined) {
node = { id: this.nodeCount };
this.nodeCount++;
parent[labelId] = node;
}
return node;
},
flatten: function flattenStacks() {
let stackFrameCount = 0;
function countStacks(tree, depth) {
let leaf = true;
for (const frameId in tree) {
if (frameId !== 'id') {
leaf = countStacks(tree[frameId], depth + 1);
}
}
if (leaf) {
stackFrameCount += depth;
}
return false;
}
countStacks(this.root, 0);
console.log('size needed to store stacks: ' + (stackFrameCount * 4).toString() + 'B');
const stackIdMap = new Array(this.nodeCount);
const stackArray = new Int32Array(stackFrameCount);
let maxStackDepth = 0;
stackFrameCount = 0;
function flattenStacksImpl(tree, stack) {
let childStack;
maxStackDepth = Math.max(maxStackDepth, stack.length);
for (const frameId in tree) {
if (frameId !== 'id') {
stack.push(Number(frameId));
childStack = flattenStacksImpl(tree[frameId], stack);
stack.pop();
}
}
const id = tree.id;
if (id < 0 || id >= stackIdMap.length || stackIdMap[id] !== undefined) {
throw '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(this.root, []);
return new stackData(stackIdMap, maxStackDepth);
},
};
}
function aggrow(strings, stacks, numRows) { // eslint-disable-line no-unused-vars
// 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 calleeFrameGetter(stack, depth) {
return stack[depth];
}
function callerFrameGetter(stack, depth) {
return stack[stack.length - depth - 1];
}
function createStackComparers(stackGetter, frameGetter) {
const comparers = new Array(stacks.maxDepth);
for (let depth = 0; depth < stacks.maxDepth; depth++) {
const captureDepth = depth; // NB: to capture depth per loop iteration
comparers[depth] = function calleeStackComparer(rowA, rowB) {
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 frameGetter(a, captureDepth) - frameGetter(b, captureDepth);
};
}
return comparers;
}
function createTreeNode(parent, label, indices, expander) {
const indent = parent === null ? 0 : (parent.state >>> NODE_INDENT_SHIFT) + 1;
const state = NODE_REPOSITION_BIT |
NODE_REAGGREGATE_BIT |
NODE_REORDER_BIT |
(indent << NODE_INDENT_SHIFT);
return {
parent: parent, // null if root
children: null, // array of children nodes
label: label, // string to show in UI
indices: indices, // row indices under this node
aggregates: null, // result of aggregate on indices
expander: expander, // index into state.activeExpanders
top: 0, // y position of top row (in rows)
height: 1, // number of rows including children
state: state, // see NODE_* definitions above
};
}
function noSortOrder(a, b) {
return 0;
}
const indices = new Int32Array(numRows);
for (let i = 0; i < numRows; i++) {
indices[i] = i;
}
const state = {
fieldExpanders: [], // tree expanders that expand on simple values
stackExpanders: [], // tree expanders that expand stacks
activeExpanders: [], // index into field or stack expanders, hierarchy of tree
aggregators: [], // all available aggregators, might not be used
activeAggregators: [], // index into aggregators, to actually compute
sorter: noSortOrder, // compare function that uses sortOrder to sort row.children
root: createTreeNode(null, '<root>', indices, INVALID_ACTIVE_EXPANDER),
};
function evaluateAggregate(row) {
const activeAggregators = state.activeAggregators;
const aggregates = new Array(activeAggregators.length);
for (let j = 0; j < activeAggregators.length; j++) {
const aggregator = state.aggregators[activeAggregators[j]];
aggregates[j] = aggregator.aggregator(row.indices);
}
row.aggregates = aggregates;
row.state |= NODE_REAGGREGATE_BIT;
}
function evaluateAggregates(row) {
if ((row.state & NODE_EXPANDED_BIT) !== 0) {
const children = row.children;
for (let i = 0; i < children.length; i++) {
evaluateAggregate(children[i]);
}
row.state |= NODE_REORDER_BIT;
}
row.state ^= NODE_REAGGREGATE_BIT;
}
function evaluateOrder(row) {
if ((row.state & NODE_EXPANDED_BIT) !== 0) {
const children = row.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.state |= NODE_REORDER_BIT;
}
children.sort(state.sorter);
row.state |= NODE_REPOSITION_BIT;
}
row.state ^= NODE_REORDER_BIT;
}
function evaluatePosition(row) {
if ((row.state & NODE_EXPANDED_BIT) !== 0) {
const children = row.children;
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;
}
childTop += child.height;
}
}
row.state ^= NODE_REPOSITION_BIT;
}
function getRowsImpl(row, top, height, result) {
if ((row.state & NODE_REAGGREGATE_BIT) !== 0) {
evaluateAggregates(row);
}
if ((row.state & NODE_REORDER_BIT) !== 0) {
evaluateOrder(row);
}
if ((row.state & NODE_REPOSITION_BIT) !== 0) {
evaluatePosition(row);
}
if (row.top >= top && row.top < top + height) {
if (result[row.top - top] != null) {
throw 'getRows put more than one row at position ' + row.top + ' into result';
}
result[row.top - top] = row;
}
if ((row.state & NODE_EXPANDED_BIT) !== 0) {
const children = row.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.top < top + height && top < child.top + child.height) {
getRowsImpl(child, top, height, result);
}
}
}
}
function updateHeight(row, heightChange) {
while (row !== null) {
row.height += heightChange;
row.state |= NODE_REPOSITION_BIT;
row = row.parent;
}
}
function addChildrenWithFieldExpander(row, expander, nextActiveIndex) {
const rowIndices = row.indices;
const comparer = expander.comparer;
rowIndices.sort(comparer);
let begin = 0;
let end = 1;
row.children = [];
while (end < rowIndices.length) {
if (comparer(rowIndices[begin], rowIndices[end]) !== 0) {
row.children.push(createTreeNode(
row,
expander.name + ': ' + expander.formatter(rowIndices[begin]),
rowIndices.subarray(begin, end),
nextActiveIndex));
begin = end;
}
end++;
}
row.children.push(createTreeNode(
row,
expander.name + ': ' + expander.formatter(rowIndices[begin]),
rowIndices.subarray(begin, end),
nextActiveIndex));
}
function addChildrenWithStackExpander(row, expander, activeIndex, depth, nextActiveIndex) {
const rowIndices = row.indices;
const stackGetter = expander.stackGetter;
const frameGetter = expander.frameGetter;
const comparer = expander.comparers[depth];
const expandNextFrame = activeIndex | ((depth + 1) << ACTIVE_EXPANDER_FRAME_SHIFT);
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 = [];
while (begin < rowIndices.length) {
beginStack = stackGetter(rowIndices[begin]);
if (beginStack.length > depth) {
break;
}
begin++;
}
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 (frameGetter(beginStack, depth) !== frameGetter(endStack, depth)) {
row.children.push(createTreeNode(
row,
columnName + strings.get(frameGetter(beginStack, depth)),
rowIndices.subarray(begin, end),
expandNextFrame));
begin = end;
beginStack = endStack;
}
end++;
}
row.children.push(createTreeNode(
row,
columnName + strings.get(frameGetter(beginStack, depth)),
rowIndices.subarray(begin, end),
expandNextFrame));
}
}
function contractRow(row) {
if ((row.state & NODE_EXPANDED_BIT) === 0) {
throw 'can not contract row, already contracted';
}
row.state ^= NODE_EXPANDED_BIT;
const heightChange = 1 - row.height;
updateHeight(row, heightChange);
}
function pruneExpanders(row, oldExpander, newExpander) {
row.state |= NODE_REPOSITION_BIT;
if (row.expander === oldExpander) {
row.state |= NODE_REAGGREGATE_BIT | NODE_REORDER_BIT | NODE_REPOSITION_BIT;
if ((row.state & NODE_EXPANDED_BIT) !== 0) {
contractRow(row);
}
row.children = null;
row.expander = newExpander;
} else {
row.state |= NODE_REPOSITION_BIT;
const children = row.children;
if (children != null) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
pruneExpanders(child, oldExpander, newExpander);
}
}
}
}
return {
addFieldExpander: function addFieldExpander(name, formatter, comparer) {
if (FIELD_EXPANDER_ID_MIN + state.fieldExpanders.length >= FIELD_EXPANDER_ID_MAX) {
throw 'too many field expanders!';
}
state.fieldExpanders.push({
name: name, // name for column
formatter: formatter, // row index -> display string
comparer: comparer, // compares by two row indices
});
return FIELD_EXPANDER_ID_MIN + state.fieldExpanders.length - 1;
},
addCalleeStackExpander: function addCalleeStackExpander(name, stackGetter) {
if (STACK_EXPANDER_ID_MIN + state.fieldExpanders.length >= STACK_EXPANDER_ID_MAX) {
throw 'too many stack expanders!';
}
state.stackExpanders.push({
name: name, // name for column
stackGetter: stackGetter, // row index -> stack array
comparers: createStackComparers(stackGetter, calleeFrameGetter), // depth -> comparer
frameGetter: calleeFrameGetter, // (stack, depth) -> string id
});
return STACK_EXPANDER_ID_MIN + state.stackExpanders.length - 1;
},
addCallerStackExpander: function addCallerStackExpander(name, stackGetter) {
if (STACK_EXPANDER_ID_MIN + state.fieldExpanders.length >= STACK_EXPANDER_ID_MAX) {
throw 'too many stack expanders!';
}
state.stackExpanders.push({
name: name,
stackGetter: stackGetter,
comparers: createStackComparers(stackGetter, callerFrameGetter),
frameGetter: callerFrameGetter,
});
return STACK_EXPANDER_ID_MIN + state.stackExpanders.length - 1;
},
getExpanders: function getExpanders() {
const expanders = [];
for (let i = 0; i < state.fieldExpanders.length; i++) {
expanders.push(FIELD_EXPANDER_ID_MIN + i);
}
for (let i = 0; i < state.stackExpanders.length; i++) {
expanders.push(STACK_EXPANDER_ID_MIN + i);
}
return expanders;
},
getExpanderName: function getExpanderName(id) {
if (id >= FIELD_EXPANDER_ID_MIN && id <= FIELD_EXPANDER_ID_MAX) {
return state.fieldExpanders[id - FIELD_EXPANDER_ID_MIN].name;
} else if (id >= STACK_EXPANDER_ID_MIN && id <= STACK_EXPANDER_ID_MAX) {
return state.stackExpanders[id - STACK_EXPANDER_ID_MIN].name;
}
throw 'Unknown expander ID ' + id.toString();
},
setActiveExpanders: function setActiveExpanders(ids) {
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (id >= FIELD_EXPANDER_ID_MIN && id <= FIELD_EXPANDER_ID_MAX) {
if (id - FIELD_EXPANDER_ID_MIN >= state.fieldExpanders.length) {
throw 'field expander for id ' + id.toString() + ' does not exist!';
}
} else if (id >= STACK_EXPANDER_ID_MIN && id <= STACK_EXPANDER_ID_MAX) {
if (id - STACK_EXPANDER_ID_MIN >= state.stackExpanders.length) {
throw 'stack expander for id ' + id.toString() + ' does not exist!';
}
}
}
for (let i = 0; i < ids.length; i++) {
if (state.activeExpanders.length <= i) {
pruneExpanders(state.root, INVALID_ACTIVE_EXPANDER, i);
break;
} else if (ids[i] !== state.activeExpanders[i]) {
pruneExpanders(state.root, i, i);
break;
}
}
// TODO: if ids is prefix of activeExpanders, we need to make an expander invalid
state.activeExpanders = ids.slice();
},
getActiveExpanders: function getActiveExpanders() {
return state.activeExpanders.slice();
},
addAggregator: function addAggregator(name, aggregator, formatter, sorter) {
if (state.aggregators.length >= AGGREGATOR_ID_MAX) {
throw 'too many aggregators!';
}
state.aggregators.push({
name: name, // name for column
aggregator: aggregator, // index array -> aggregate value
formatter: formatter, // aggregate value -> display string
sorter: sorter, // compare two aggregate values
});
return state.aggregators.length - 1;
},
getAggregators: function getAggregators() {
const aggregators = [];
for (let i = 0; i < state.aggregators.length; i++) {
aggregators.push(i);
}
return aggregators;
},
getAggregatorName: function getAggregatorName(id) {
return state.aggregators[id & ACTIVE_AGGREGATOR_MASK].name;
},
setActiveAggregators: function setActiveAggregators(ids) {
for (let i = 0; i < ids.length; i++) {
const id = ids[i] & ACTIVE_AGGREGATOR_MASK;
if (id < 0 || id > state.aggregators.length) {
throw 'aggregator id ' + id.toString() + ' not valid';
}
}
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
evaluateAggregate(state.root);
let sorter = noSortOrder;
for (let i = ids.length - 1; i >= 0; i--) {
const ascending = (ids[i] & ACTIVE_AGGREGATOR_ASC_BIT) !== 0;
const id = ids[i] & ACTIVE_AGGREGATOR_MASK;
const comparer = state.aggregators[id].sorter;
const captureSorter = sorter;
const captureIndex = i;
sorter = function (a, b) {
const c = comparer(a.aggregates[captureIndex], b.aggregates[captureIndex]);
if (c === 0) {
return captureSorter(a, b);
}
return ascending ? -c : c;
};
}
state.sorter = sorter;
state.root.state |= NODE_REORDER_BIT;
},
getActiveAggregators: function getActiveAggregators() {
return state.activeAggregators.slice();
},
getRows: function getRows(top, height) {
const result = new Array(height);
for (let i = 0; i < height; i++) {
result[i] = null;
}
getRowsImpl(state.root, top, height, result);
return result;
},
getRowLabel: function getRowLabel(row) {
return row.label;
},
getRowIndent: function getRowIndent(row) {
return row.state >>> NODE_INDENT_SHIFT;
},
getRowAggregate: function getRowAggregate(row, index) {
const aggregator = state.aggregators[state.activeAggregators[index]];
return aggregator.formatter(row.aggregates[index]);
},
getHeight: function getHeight() {
return state.root.height;
},
canExpand: function canExpand(row) {
return (row.state & NODE_EXPANDED_BIT) === 0 && (row.expander !== INVALID_ACTIVE_EXPANDER);
},
canContract: function canContract(row) {
return (row.state & NODE_EXPANDED_BIT) !== 0;
},
expand: function expand(row) {
if ((row.state & NODE_EXPANDED_BIT) !== 0) {
throw 'can not expand row, already expanded';
}
if (row.height !== 1) {
throw 'unexpanded row has height ' + row.height.toString() + ' != 1';
}
if (row.children === null) { // first expand, generate children
const activeIndex = row.expander & ACTIVE_EXPANDER_MASK;
let nextActiveIndex = activeIndex + 1; // NB: if next is stack, frame is 0
if (nextActiveIndex >= state.activeExpanders.length) {
nextActiveIndex = INVALID_ACTIVE_EXPANDER;
}
if (activeIndex >= state.activeExpanders.length) {
throw 'invalid active expander index ' + activeIndex.toString();
}
const exId = state.activeExpanders[activeIndex];
if (exId >= FIELD_EXPANDER_ID_MIN &&
exId < FIELD_EXPANDER_ID_MIN + state.fieldExpanders.length) {
const expander = state.fieldExpanders[exId - FIELD_EXPANDER_ID_MIN];
addChildrenWithFieldExpander(row, expander, nextActiveIndex);
} else if (exId >= STACK_EXPANDER_ID_MIN &&
exId < STACK_EXPANDER_ID_MIN + state.stackExpanders.length) {
const depth = row.expander >>> ACTIVE_EXPANDER_FRAME_SHIFT;
const expander = state.stackExpanders[exId - STACK_EXPANDER_ID_MIN];
addChildrenWithStackExpander(row, expander, activeIndex, depth, nextActiveIndex);
} else {
throw 'state.activeIndex ' + activeIndex.toString()
+ ' has invalid expander' + exId.toString();
}
}
row.state |= NODE_EXPANDED_BIT
| NODE_REAGGREGATE_BIT | NODE_REORDER_BIT | NODE_REPOSITION_BIT;
let heightChange = 0;
for (let i = 0; i < row.children.length; i++) {
heightChange += row.children[i].height;
}
updateHeight(row, heightChange);
// if children only contains one node, then expand it as well
if (row.children.length === 1 && this.canExpand(row.children[0])) {
this.expand(row.children[0]);
}
},
contract: function contract(row) {
contractRow(row);
},
};
}

View File

@ -0,0 +1,287 @@
/**
* 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 React ReactDOM Table stringInterner stackRegistry aggrow preLoadedCapture:true*/
function registerReactComponentTreeImpl(refs, registry, parents, inEdgeNames, trees, id) {
if (parents[id] === undefined) {
// not a component
} else if (parents[id] === null) {
trees[id] = registry.insert(registry.root, '<internalInstance>');
} else {
const parent = parents[id];
const inEdgeName = inEdgeNames[id];
let parentTree = trees[parent];
if (parentTree === undefined) {
parentTree = registerReactComponentTreeImpl(
refs,
registry,
parents,
inEdgeNames,
trees,
parent);
}
trees[id] = registry.insert(parentTree, inEdgeName);
}
return trees[id];
}
// TODO: make it easier to query the heap graph, it's super annoying to deal with edges directly
function registerReactComponentTree(refs, registry) {
// build list of parents for react interal instances, so we can connect a tree
const parents = {};
const inEdgeNames = {};
for (const id in refs) {
const ref = refs[id];
for (const linkId in ref.edges) {
if (linkId !== '0x0') {
const name = ref.edges[linkId];
if (name === '_renderedChildren') {
if (parents[id] === undefined) {
// mark that we are a react component, even if we don't have a parent
parents[id] = null;
}
const childrenRef = refs[linkId];
for (const childId in childrenRef.edges) {
const linkName = childrenRef.edges[childId];
if (linkName.startsWith('.')) {
parents[childId] = id;
inEdgeNames[childId] = linkName;
}
}
} else if (name === '_renderedComponent') {
if (parents[id] === undefined) {
parents[id] = null;
}
parents[linkId] = id;
inEdgeNames[linkId] = '_renderedComponent';
}
}
}
}
// build tree of react internal instances (since that's what has the structure)
const trees = {};
for (const id in refs) {
registerReactComponentTreeImpl(refs, registry, parents, inEdgeNames, trees, id);
}
// hook in components by looking at their _reactInternalInstance fields
for (const id in refs) {
const ref = refs[id];
for (const linkId in ref.edges) {
const name = ref.edges[linkId];
if (name === '_reactInternalInstance') {
if (trees[linkId] !== undefined) {
trees[id] = registry.insert(trees[linkId], '<component>');
}
}
}
}
return trees;
}
function registerPathToRoot(roots, refs, registry, reactComponentTree) {
const visited = {};
let breadth = [];
for (let i = 0; i < roots.length; i++) {
const id = roots[i];
if (visited[id] === undefined) {
const ref = refs[id];
visited[id] = registry.insert(registry.root, ref.type);
breadth.push(id);
}
}
while (breadth.length > 0) {
const nextBreadth = [];
for (let i = 0; i < breadth.length; i++) {
const id = breadth[i];
const ref = refs[id];
const node = visited[id];
// TODO: make edges map id -> name, (empty for none) seems that would be better
const edges = Object.getOwnPropertyNames(ref.edges);
edges.sort(function putUnknownLast(a, b) {
const aName = ref.edges[a];
const bName = ref.edges[b];
if (aName === null && bName !== null) {
return 1;
} else if (aName !== null && bName === null) {
return -1;
} else if (aName === null && bName === null) {
return 0;
} else {
return a.localeCompare(b);
}
});
for (let j = 0; j < edges.length; j++) {
const edgeId = edges[j];
let edgeName = '';
if (ref.edges[edgeId]) {
edgeName = ref.edges[edgeId] + ': ';
}
if (visited[edgeId] === undefined) {
const edgeRef = refs[edgeId];
if (edgeRef === undefined) {
// TODO: figure out why we have edges that point to things not JSCell
//console.log('registerPathToRoot unable to follow edge from ' + id + ' to ' + edgeId);
} else {
visited[edgeId] = registry.insert(node, edgeName + edgeRef.type);
nextBreadth.push(edgeId);
if (reactComponentTree[edgeId] === undefined) {
reactComponentTree[edgeId] = reactComponentTree[id];
}
}
}
}
}
breadth = nextBreadth;
}
return visited;
}
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 numFields = 6;
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++;
}
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;
const reactComponentTreeMap = registerReactComponentTree(capture.refs, this.stacks);
const rootPathMap = registerPathToRoot(
capture.roots,
capture.refs,
this.stacks,
reactComponentTreeMap
);
const internedCaptureId = this.strings.intern(captureId);
for (const id in capture.refs) {
const ref = capture.refs[id];
newData[dataOffset + idField] = parseInt(id, 16);
newData[dataOffset + typeField] = this.strings.intern(ref.type);
newData[dataOffset + sizeField] = ref.size;
newData[dataOffset + traceField] = internedCaptureId;
const pathNode = rootPathMap[id];
if (pathNode === undefined) {
throw 'did not find path for ref!';
}
newData[dataOffset + pathField] = pathNode.id;
const reactTree = reactComponentTreeMap[id];
if (reactTree === undefined) {
newData[dataOffset + reactField] =
this.stacks.insert(this.stacks.root, '<not-under-tree>').id;
} else {
newData[dataOffset + reactField] = reactTree.id;
}
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(agStrings, agStacks, agNumRows);
const idExpander = 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 getSize(row) { return agStrings.get(agData[row * numFields + typeField]); },
function compareSize(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];
});
const traceExpander = 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',
function getStack(row) { return agStacks.get(agData[row * numFields + pathField]); });
const reactExpander = ag.addCalleeStackExpander('React Tree',
function getStack(row) { return agStacks.get(agData[row * numFields + reactField]); });
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, typeExpander, idExpander, traceExpander]);
ag.setActiveAggregators([sizeAggregator, countAggregator]);
return ag;
},
};
}
if (preLoadedCapture) {
const r = new captureRegistry();
r.register('trace', preLoadedCapture);
preLoadedCapture = undefined; // let GG clean up the capture
ReactDOM.render(<Table aggrow={r.getAggrow()} />, document.body);
}

View File

@ -0,0 +1,330 @@
/**
* 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 React:true*/
// TODO:
// selection and arrow keys for navigating
const rowHeight = 20;
const treeIndent = 16;
class Draggable extends React.Component { // eslint-disable-line no-unused-vars
constructor(props) {
super(props);
}
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.propTypes = {
children: React.PropTypes.element.isRequired,
id: React.PropTypes.string.isRequired,
};
class DropTarget extends React.Component { // eslint-disable-line no-unused-vars
constructor(props) {
super(props);
}
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();
}
},
onDrop: (e) => {
const sourceId = e.dataTransfer.getData('text/plain');
if (dropFilter(sourceId)) {
e.preventDefault();
dropAction(sourceId, thisId);
}
},
}
);
}
}
DropTarget.propTypes = {
children: React.PropTypes.element.isRequired,
id: React.PropTypes.string.isRequired,
dropFilter: React.PropTypes.func.isRequired,
dropAction: React.PropTypes.func.isRequired,
};
class Table extends React.Component { // eslint-disable-line no-unused-vars
constructor(props) {
super(props);
this.state = {
aggrow: props.aggrow,
viewport: { top: 0, height: 100 },
};
}
scroll(e) {
const viewport = e.target;
const top = Math.floor((viewport.scrollTop - viewport.clientHeight * 1.0) / rowHeight);
const height = Math.ceil(viewport.clientHeight * 3.0 / rowHeight);
this.state.viewport.top = top;
this.state.viewport.height = height;
this.forceUpdate();
}
dropAggregator(s, d) {
const aggrow = this.state.aggrow;
console.log('dropped ' + s + ' to ' + d);
if (s.startsWith('aggregate:active:')) {
const sIndex = parseInt(s.substr(17), 10);
let dIndex = -1;
const active = aggrow.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 'not allowed to drag ' + s + ' to ' + d;
}
if (dIndex > sIndex) {
dIndex--;
}
active.splice(sIndex, 1);
active.splice(dIndex, 0, dragged);
aggrow.setActiveAggregators(active);
this.forceUpdate();
} else if (s.startsWith('expander:active:')) {
const sIndex = parseInt(s.substr(16), 10);
let dIndex = -1;
const active = aggrow.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 'not allowed to drag ' + s + ' to ' + d;
}
if (dIndex > sIndex) {
dIndex--;
}
active.splice(sIndex, 1);
active.splice(dIndex, 0, dragged);
aggrow.setActiveExpanders(active);
this.forceUpdate();
}
}
render() {
const headers = [];
const aggrow = this.state.aggrow;
const aggregators = aggrow.getActiveAggregators();
const expanders = aggrow.getActiveExpanders();
// aggregators
for (let i = 0; i < aggregators.length; i++) {
const name = aggrow.getAggregatorName(aggregators[i]);
headers.push((
<DropTarget
id={'aggregate:insert:' + i.toString()}
dropFilter={()=>{return true; }}
dropAction={(s, d)=>{ this.dropAggregator(s, d); }}
>
<div style={{
width: '16px',
height: 'inherit',
backgroundColor: 'darkGray',
flexShrink: '0' }}
></div>
</DropTarget>));
headers.push((<Draggable id={'aggregate:active:' + i.toString()}>
<div style={{ width: '128px', textAlign: 'center', flexShrink: '0' }}>{name}</div>
</Draggable>));
}
headers.push((
<DropTarget
id="divider:insert"
dropFilter={()=>{return true; }}
dropAction={(s, d)=>{ this.dropAggregator(s, d); }}
>
<div style={{
width: '16px',
height: 'inherit',
backgroundColor: 'gold',
flexShrink: '0'
}}></div>
</DropTarget>));
for (let i = 0; i < expanders.length; i++) {
const name = aggrow.getExpanderName(expanders[i]);
const bg = (i % 2 === 0) ? 'white' : 'lightGray';
headers.push((<Draggable id={'expander:active:' + i.toString()}>
<div style={{
width: '128px',
textAlign: 'center',
backgroundColor: bg,
flexShrink: '0'
}}>
{name}
</div>
</Draggable>));
const sep = i + 1 < expanders.length ? '->' : '...';
headers.push((
<DropTarget
id={'expander:insert:' + (i + 1).toString()}
dropFilter={()=>{return true; }}
dropAction={(s, d)=>{ this.dropAggregator(s, d);}}
>
<div style={{
height: 'inherit',
backgroundColor: 'darkGray',
flexShrink: '0'
}}>
{sep}
</div>
</DropTarget>)
);
}
return (
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{
width: '100%',
height: '26px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderBottom: '2px solid black',
}}>
{headers}
</div>
<div style={{
width: '100%',
flexGrow: '1',
overflow: 'scroll'
}} onScroll={ (e) => this.scroll(e) }>
<div style={{ position: 'relative' }}>
{ this.renderVirtualizedRows() }
</div>
</div>
</div>
);
}
renderVirtualizedRows() {
const aggrow = this.state.aggrow;
const viewport = this.state.viewport;
const rows = aggrow.getRows(viewport.top, viewport.height);
return (
<div style={{
position: 'absolute',
width: '100%',
height: (rowHeight * (aggrow.getHeight() + 20)).toString() + 'px'
}}>
{ rows.map(child => this.renderRow(child)) }
</div>
);
}
renderRow(row) {
if (row === null) {
return null;
}
let bg = 'lightGray';
const aggrow = this.state.aggrow;
const columns = [];
let rowText = '';
const indent = 4 + aggrow.getRowIndent(row) * treeIndent;
const aggregates = aggrow.getActiveAggregators();
if (row.parent !== null && (row.parent.expander % 2 === 0)) {
bg = 'white';
}
for (let i = 0; i < aggregates.length; i++) {
var aggregate = aggrow.getRowAggregate(row, i);
columns.push((
<div style={{
width: '16px',
height: 'inherit',
backgroundColor: 'darkGray',
flexShrink: '0'
}}></div>
));
columns.push((
<div style={{
width: '128px',
textAlign: 'right',
flexShrink: '0'
}}>
{aggregate}
</div>
));
}
columns.push((
<div style={{
width: '16px',
height: 'inherit',
backgroundColor: 'gold',
flexShrink: '0'
}}></div>
));
if (aggrow.canExpand(row)) {
rowText += '+';
} else if (aggrow.canContract(row)) {
rowText += '-';
} else {
rowText += ' ';
}
rowText += aggrow.getRowLabel(row);
columns.push((
<div style={{
marginLeft: indent.toString() + 'px',
flexShrink: '0',
whiteSpace: 'nowrap'
}}>
{rowText}
</div>
));
return (
<div style={{
position: 'absolute',
height: (rowHeight - 1).toString() + 'px',
top: (rowHeight * row.top).toString() + 'px',
display: 'flex',
flexDirection: 'row',
backgroundColor: bg,
borderBottom: '1px solid gray',
}}
onClick={ () => {
if (aggrow.canExpand(row)) {
aggrow.expand(row);
this.forceUpdate();
} else if (aggrow.canContract(row)) {
aggrow.contract(row);
this.forceUpdate();
}
}}>
{columns}
</div>
);
}
}

View File

@ -7,8 +7,9 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
/*eslint no-console-disallow: "off"*/
const exec = require('child_process').exec;
const spawn = require('child_process').spawn;
const fs = require('fs');
const path = require('path');
@ -18,9 +19,27 @@ module.exports = function(req, res, next) {
return;
}
console.log('Receiving heap capture...');
var captureName = '/tmp/capture_' + Date.now() + '.json';
fs.writeFileSync(captureName, req.rawBody);
console.log('Capture written to ' + captureName);
console.log('Downloading Heap Capture');
var preload = path.join(__dirname, 'heapCapture/preLoadedCapture.js');
fs.writeFileSync(preload, 'var preLoadedCapture = ');
fs.appendFileSync(preload, req.rawBody);
fs.appendFileSync(preload, ';');
res.end();
console.log('Packaging Trace');
var captureHtml = path.join(__dirname, 'heapCapture/captures/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('exit', (code, signal) => {
if (code === 0) {
console.log('Heap capture written to: ' + captureHtml);
} else {
console.error('Error processing heap capture, inliner returned code: ' + code);
}
});
};