From 54f867f0d6173cb0bb37644a7457310c5dc62402 Mon Sep 17 00:00:00 2001 From: Charles Dick Date: Mon, 8 Aug 2016 04:27:53 -0700 Subject: [PATCH] Process heap capture into trace html Reviewed By: bestander Differential Revision: D3642188 fbshipit-source-id: c9a4699b2a0d60eb5961333dec45941085e19324 --- .../server/middleware/heapCapture/.gitignore | 2 + .../server/middleware/heapCapture/Makefile | 5 + .../middleware/heapCapture/heapCapture.html | 16 + .../middleware/heapCapture/out/aggrow.js | 616 +++++++++++++++++ .../middleware/heapCapture/out/heapCapture.js | 284 ++++++++ .../middleware/heapCapture/out/table.js | 326 +++++++++ .../middleware/heapCapture/src/aggrow.js | 619 ++++++++++++++++++ .../middleware/heapCapture/src/heapCapture.js | 287 ++++++++ .../middleware/heapCapture/src/table.js | 330 ++++++++++ .../middleware/heapCaptureMiddleware.js | 29 +- 10 files changed, 2509 insertions(+), 5 deletions(-) create mode 100644 local-cli/server/middleware/heapCapture/.gitignore create mode 100644 local-cli/server/middleware/heapCapture/Makefile create mode 100644 local-cli/server/middleware/heapCapture/heapCapture.html create mode 100644 local-cli/server/middleware/heapCapture/out/aggrow.js create mode 100644 local-cli/server/middleware/heapCapture/out/heapCapture.js create mode 100644 local-cli/server/middleware/heapCapture/out/table.js create mode 100644 local-cli/server/middleware/heapCapture/src/aggrow.js create mode 100644 local-cli/server/middleware/heapCapture/src/heapCapture.js create mode 100644 local-cli/server/middleware/heapCapture/src/table.js diff --git a/local-cli/server/middleware/heapCapture/.gitignore b/local-cli/server/middleware/heapCapture/.gitignore new file mode 100644 index 00000000000000..70534df0aeb3ad --- /dev/null +++ b/local-cli/server/middleware/heapCapture/.gitignore @@ -0,0 +1,2 @@ +/captures/* +preLoadedCapture.js diff --git a/local-cli/server/middleware/heapCapture/Makefile b/local-cli/server/middleware/heapCapture/Makefile new file mode 100644 index 00000000000000..c0374ee6381956 --- /dev/null +++ b/local-cli/server/middleware/heapCapture/Makefile @@ -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 diff --git a/local-cli/server/middleware/heapCapture/heapCapture.html b/local-cli/server/middleware/heapCapture/heapCapture.html new file mode 100644 index 00000000000000..6cfbe840cc8d83 --- /dev/null +++ b/local-cli/server/middleware/heapCapture/heapCapture.html @@ -0,0 +1,16 @@ + + + + + JSC Heap Capture + + + + + + + Loading... This could take a while depending on how big the profile is. Check devtools console for errors. + + + + diff --git a/local-cli/server/middleware/heapCapture/out/aggrow.js b/local-cli/server/middleware/heapCapture/out/aggrow.js new file mode 100644 index 00000000000000..b7edd0fa316ce9 --- /dev/null +++ b/local-cli/server/middleware/heapCapture/out/aggrow.js @@ -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' 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>>NODE_INDENT_SHIFT)+1; +var state=NODE_REPOSITION_BIT| +NODE_REAGGREGATE_BIT| +NODE_REORDER_BIT| +indent<',indices,INVALID_ACTIVE_EXPANDER)}; + + +function evaluateAggregate(row){ +var activeAggregators=state.activeAggregators; +var aggregates=new Array(activeAggregators.length); +for(var j=0;j=top&&row.top +var begin=0; +var beginStack=null; +row.children=[]; +while(begindepth){ +break;} + +begin++;} + +if(begin>0){ +row.children.push(createTreeNode( +row, +columnName+'', +rowIndices.subarray(0,begin), +nextActiveIndex));} + +// aggregate the rest under frames +if(begin=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=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=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=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;_i10state.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>>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=STACK_EXPANDER_ID_MIN&& +exId>>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');}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],'');}}}} + + + + +return trees;} + + +function registerPathToRoot(roots,refs,registry,reactComponentTree){ +var visited={}; +var breadth=[]; +for(var i=0;i0){ +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').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;isIndex){ +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':'...'; +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= 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 '' 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, '', 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 + 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 + '', + 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); + }, + }; +} diff --git a/local-cli/server/middleware/heapCapture/src/heapCapture.js b/local-cli/server/middleware/heapCapture/src/heapCapture.js new file mode 100644 index 00000000000000..783542bfca25cf --- /dev/null +++ b/local-cli/server/middleware/heapCapture/src/heapCapture.js @@ -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, ''); + } 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], ''); + } + } + } + } + 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, '').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(, document.body); +} diff --git a/local-cli/server/middleware/heapCapture/src/table.js b/local-cli/server/middleware/heapCapture/src/table.js new file mode 100644 index 00000000000000..73d267c260370a --- /dev/null +++ b/local-cli/server/middleware/heapCapture/src/table.js @@ -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(( + {return true; }} + dropAction={(s, d)=>{ this.dropAggregator(s, d); }} + > +
+
)); + headers.push(( +
{name}
+
)); + } + headers.push(( + {return true; }} + dropAction={(s, d)=>{ this.dropAggregator(s, d); }} + > +
+
)); + for (let i = 0; i < expanders.length; i++) { + const name = aggrow.getExpanderName(expanders[i]); + const bg = (i % 2 === 0) ? 'white' : 'lightGray'; + headers.push(( +
+ {name} +
+
)); + const sep = i + 1 < expanders.length ? '->' : '...'; + headers.push(( + {return true; }} + dropAction={(s, d)=>{ this.dropAggregator(s, d);}} + > +
+ {sep} +
+
) + ); + } + + return ( +
+
+ {headers} +
+
this.scroll(e) }> +
+ { this.renderVirtualizedRows() } +
+
+
+ ); + } + + renderVirtualizedRows() { + const aggrow = this.state.aggrow; + const viewport = this.state.viewport; + const rows = aggrow.getRows(viewport.top, viewport.height); + return ( +
+ { rows.map(child => this.renderRow(child)) } +
+ ); + } + + 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(( +
+ )); + columns.push(( +
+ {aggregate} +
+ )); + } + columns.push(( +
+ )); + if (aggrow.canExpand(row)) { + rowText += '+'; + } else if (aggrow.canContract(row)) { + rowText += '-'; + } else { + rowText += ' '; + } + rowText += aggrow.getRowLabel(row); + columns.push(( +
+ {rowText} +
+ )); + return ( +
{ + if (aggrow.canExpand(row)) { + aggrow.expand(row); + this.forceUpdate(); + } else if (aggrow.canContract(row)) { + aggrow.contract(row); + this.forceUpdate(); + } + }}> + {columns} +
+ ); + } +} diff --git a/local-cli/server/middleware/heapCaptureMiddleware.js b/local-cli/server/middleware/heapCaptureMiddleware.js index cd1dcfc22ae2fd..1f660f65dcd390 100644 --- a/local-cli/server/middleware/heapCaptureMiddleware.js +++ b/local-cli/server/middleware/heapCaptureMiddleware.js @@ -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); + } + }); };