├── README.md ├── Source ├── Analyzer.hx ├── Main.hx ├── ObjectGroup.hx ├── ObjectNode.hx └── ObjectTable.hx └── project.xml /README.md: -------------------------------------------------------------------------------- 1 | # hxcppObjectGraphViewer 2 | 3 | An experimental object graph viewer for hxcpp. 4 | 5 | Will need this object-graph branch: 6 | https://github.com/james4k/hxcpp/commits/object-graph 7 | 8 | To use, build your app with `-DHXCPP_GC_DUMP_OBJECT_GRAPH`, 9 | which will cause the program to dump an `object_graph.csv` file 10 | in the working directory after every GC at peak memory usage. Select this file in 11 | the file selection dialog when you run hxcppObjectGraphViewer. 12 | 13 | The user interface is extremely crude right now. Here is how to use it: 14 | 15 | * Click on a row in the overview table to see a list of each object in that 16 | group. 17 | * Click on an object address to browse via its references and see the object's roots. 18 | * Use the up and down arrow keys to adjust the `incl_walk_depth` value for 19 | computing `incl_size`. 20 | * `incl_walk_depth` is inclusive walk depth. `incl_size` is inclusive size, or 21 | the size of the object and any of the objects it refers to, recursively, up 22 | to a depth of `incl_walk_depth`. `excl_size` is exclusive size, or the size of 23 | the object alone. `n_inst` is number of instances. 24 | * There is no scrolling yet, sorry. Will be done when scrollRect is usable. 25 | -------------------------------------------------------------------------------- /Source/Analyzer.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | 4 | class Analyzer { 5 | 6 | 7 | public var depthLimit = 0; 8 | 9 | public var totalBytes = 0; 10 | public var totalObjects = 0; 11 | 12 | 13 | var table:ObjectTable; 14 | var classIDSets = new Map> (); 15 | 16 | 17 | public function new () {} 18 | 19 | 20 | public function open (path:String) { 21 | 22 | var f = sys.io.File.read (path); 23 | table = ObjectTable.read (f); 24 | f.close (); 25 | 26 | { 27 | var visited = new Map (); 28 | totalBytes = 0; 29 | totalObjects = 0; 30 | for (i in 0...(table.memberAddrs.length)) { 31 | var addr = table.memberAddrs[i]; 32 | if (visited.get (addr) != null) { 33 | continue; 34 | } 35 | visited.set (addr, 1); 36 | totalBytes += table.memberSizes[i]; 37 | totalObjects += 1; 38 | } 39 | } 40 | 41 | // precompute classIDSets 42 | for (i in 0...(table.thisAddrs.length)) { 43 | 44 | var set = classIDSets.get (table.memberClassIDs[i]); 45 | if (set == null) { 46 | set = new Map (); 47 | classIDSets.set (table.memberClassIDs[i], set); 48 | } 49 | var node = table.addrNodes.get (table.memberAddrs[i]); 50 | if (node == null) { 51 | throw 'ObjectNode not found for addr ${table.memberAddrs[i]}'; 52 | } 53 | set.set (node, true); 54 | 55 | } 56 | 57 | 58 | } 59 | 60 | 61 | public function lookupNode (addr:Int):ObjectNode { 62 | 63 | return table.addrNodes.get (addr); 64 | 65 | } 66 | 67 | 68 | public function groupByType ():Array { 69 | 70 | return aggregate (); 71 | 72 | } 73 | 74 | 75 | private function aggregate ():Array { 76 | 77 | var groups = new Array (); 78 | 79 | for (classID in classIDSets.keys ()) { 80 | var set = classIDSets.get (classID); 81 | var g = new ObjectGroup (); 82 | g.label = table.classNames[classID]; 83 | g.nodes = new Array (); 84 | for (n in set.keys()) { 85 | g.nodes.push (n); 86 | } 87 | g.compute (depthLimit); 88 | #if 0 89 | // nothing too interesting with refCounts. doesn't differ too much 90 | // from instance counts 91 | g.refCount = 0; 92 | for (id in memberClassIDs) { 93 | if (id == classID) { 94 | g.refCount += 1; 95 | } 96 | } 97 | #end 98 | groups.push (g); 99 | } 100 | 101 | return groups; 102 | 103 | } 104 | 105 | 106 | } 107 | 108 | -------------------------------------------------------------------------------- /Source/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | 4 | import openfl.display.DisplayObject; 5 | import openfl.display.DisplayObjectContainer; 6 | import openfl.display.Bitmap; 7 | import openfl.display.BitmapData; 8 | import openfl.display.Sprite; 9 | import openfl.events.Event; 10 | import openfl.events.KeyboardEvent; 11 | import openfl.events.MouseEvent; 12 | import openfl.text.TextField; 13 | import openfl.text.TextFormat; 14 | import openfl.text.TextFormatAlign; 15 | import openfl.ui.Keyboard; 16 | import openfl.Assets; 17 | 18 | 19 | class Main extends Sprite { 20 | 21 | 22 | var azr = new Analyzer (); 23 | var groups:Array; 24 | 25 | var splitView:HSplitViewControl; 26 | var mainView:TableControl; 27 | var objectListView:TableControl; 28 | var objectAnalysisView:TableControl; 29 | 30 | var selectedGroup:ObjectGroup; 31 | var selectedNode:ObjectNode; 32 | 33 | var mainViewTab = OverviewTab; 34 | 35 | 36 | public function new () { 37 | 38 | super (); 39 | 40 | splitView = new HSplitViewControl (this); 41 | 42 | mainView = new TableControl (this); 43 | objectListView = new TableControl (this); 44 | objectAnalysisView = new TableControl (this); 45 | splitView.addControl (mainView); 46 | splitView.addControl (objectListView); 47 | splitView.addControl (objectAnalysisView); 48 | splitView.firstLayout (0, 0, stage.stageWidth, stage.stageHeight); 49 | splitView.moveSplitter (0, stage.stageWidth/2); 50 | splitView.moveSplitter (1, stage.stageWidth/2 + 140); 51 | 52 | mainView.onRowClick = onMainRowClick; 53 | objectListView.onRowClick = onObjectListRowClick; 54 | objectListView.onRowHover = onObjectListRowHover; 55 | objectAnalysisView.onRowClick = onObjectAnalysisRowClick; 56 | 57 | //onResize (null); 58 | 59 | stage.addEventListener (Event.RESIZE, onResize); 60 | stage.addEventListener (KeyboardEvent.KEY_UP, onKeyUp); 61 | 62 | // TODO(james4k): show a menu of most recent files 63 | 64 | var fd = new lime.ui.FileDialog (); 65 | fd.onSelect.add (onSelect); 66 | fd.onCancel.add (function ():Void { Sys.exit (0); }); 67 | fd.browse (); 68 | 69 | } 70 | 71 | 72 | private function onSelect (path:String):Void { 73 | 74 | azr.open (path); 75 | update (); 76 | 77 | } 78 | 79 | 80 | private function onResize (event:Event):Void { 81 | 82 | splitView.updateLayout (0, 0, stage.stageWidth, stage.stageHeight); 83 | 84 | } 85 | 86 | 87 | private function onKeyUp (event:KeyboardEvent):Void { 88 | 89 | if (event.keyCode == Keyboard.UP) { 90 | azr.depthLimit += 1; 91 | update (); 92 | } else if (event.keyCode == Keyboard.DOWN) { 93 | azr.depthLimit -= 1; 94 | update (); 95 | } 96 | 97 | } 98 | 99 | 100 | private function onMainRowClick (event:MouseEvent, index:Int):Void { 101 | 102 | if (index == 0) { 103 | 104 | var word = mainView.getWordAt (event.stageX, event.stageY); 105 | if (word == "overview") { 106 | updateOverview (); 107 | } else if (word == "browser") { 108 | updateObjectBrowser (); 109 | } 110 | 111 | return; 112 | 113 | } 114 | 115 | if (mainViewTab == OverviewTab) { 116 | 117 | // account for header lines 118 | if (index >= 6) { 119 | selectedGroup = groups[index-6]; 120 | updateObjectList (); 121 | } else { 122 | var word = mainView.getWordAt (event.stageX, event.stageY); 123 | if (word == "incl_size") { 124 | ObjectGroup.sortBySize (groups); 125 | updateOverview (); 126 | } else if (word == "n_inst") { 127 | ObjectGroup.sortByInstances (groups); 128 | updateOverview (); 129 | } 130 | } 131 | 132 | } else if (mainViewTab == BrowserTab) { 133 | 134 | var word = mainView.getWordAt (event.localX, event.localY); 135 | var addr = Std.parseInt ("0x" + word); 136 | if (addr != null && addr != 0) { 137 | var node = azr.lookupNode (addr); 138 | if (node != null) { 139 | selectedNode = node; 140 | updateObjectBrowser (); 141 | updateObjectAnalysis (); 142 | } 143 | } 144 | 145 | } 146 | 147 | } 148 | 149 | 150 | private function onObjectListRowClick (event:MouseEvent, index:Int):Void { 151 | 152 | // account for header lines 153 | if (index >= 3) { 154 | selectedNode = selectedGroup.nodes[index-3]; 155 | updateObjectBrowser (); 156 | } 157 | 158 | } 159 | 160 | 161 | private function onObjectListRowHover (index:Int):Void { 162 | 163 | // account for header lines 164 | if (index >= 3) { 165 | selectedNode = selectedGroup.nodes[index-3]; 166 | updateObjectAnalysis (); 167 | if (mainViewTab == BrowserTab) { 168 | updateObjectBrowser (); 169 | } 170 | } 171 | 172 | } 173 | 174 | 175 | private function onObjectAnalysisRowClick (event:MouseEvent, index:Int):Void { 176 | 177 | var word = objectAnalysisView.getWordAt (event.localX, event.localY); 178 | var addr = Std.parseInt ("0x" + word); 179 | if (addr != null && addr != 0) { 180 | var node = azr.lookupNode (addr); 181 | if (node != null) { 182 | selectedNode = node; 183 | updateObjectBrowser (); 184 | updateObjectAnalysis (); 185 | } 186 | } 187 | 188 | } 189 | 190 | 191 | public function update ():Void { 192 | 193 | groups = azr.groupByType (); 194 | ObjectGroup.sortBySize (groups); 195 | updateOverview (); 196 | 197 | } 198 | 199 | 200 | private function updateOverview ():Void { 201 | 202 | mainViewTab = OverviewTab; 203 | 204 | var maxRows = 200; 205 | 206 | var str = new StringBuf (); 207 | str.add ('|| browser |\n\n'); 208 | str.add ('total_bytes=${azr.totalBytes} total_objects=${azr.totalObjects}\n'); 209 | str.add ('incl_walk_depth=${azr.depthLimit}'); 210 | str.add (" adjust with up/down keys\n\n"); 211 | str.add (StringTools.lpad ("incl_size", " ", 10)); 212 | str.add (StringTools.lpad ("n_inst", " ", 8)); 213 | str.add (" " + "class_name" + "\n"); 214 | var nrows = 0; 215 | for (g in groups) { 216 | str.add (StringTools.lpad ("" + g.inclusiveSize, " ", 10)); 217 | str.add (StringTools.lpad ("" + g.nodes.length, " ", 8)); 218 | str.add (" " + g.label + "\n"); 219 | nrows += 1; 220 | if (nrows > maxRows) { 221 | break; 222 | } 223 | } 224 | 225 | mainView.setText (str.toString ()); 226 | 227 | } 228 | 229 | 230 | private function updateObjectBrowser ():Void { 231 | 232 | mainViewTab = BrowserTab; 233 | 234 | var maxRows = 200; 235 | 236 | var str = new StringBuf (); 237 | str.add ('| overview ||\n\n'); 238 | 239 | if (selectedNode == null) { 240 | mainView.setText (str.toString ()); 241 | return; 242 | } 243 | 244 | var hexAddr = StringTools.hex (selectedNode.addr, 8); 245 | str.add (hexAddr); 246 | str.add (' field_name=${selectedNode.fieldName}'); 247 | str.add (' class_name=${selectedNode.className}'); 248 | str.add (' excl_size=${selectedNode.size}\n\n'); 249 | 250 | str.add ('referrers\n'); 251 | for (r in selectedNode.referrers) { 252 | hexAddr = StringTools.hex (r.addr, 8); 253 | str.add ('$hexAddr ${r.fieldName}:${r.className}\n'); 254 | } 255 | 256 | str.add ('\nmembers\n'); 257 | for (m in selectedNode.members) { 258 | hexAddr = StringTools.hex (m.addr, 8); 259 | str.add ('$hexAddr ${m.fieldName}:${m.className}\n'); 260 | } 261 | 262 | mainView.setText (str.toString ()); 263 | 264 | } 265 | 266 | 267 | private function updateObjectList ():Void { 268 | 269 | var maxRows = 200; 270 | 271 | var g = selectedGroup; 272 | if (g == null) { 273 | return; 274 | } 275 | 276 | var str = new StringBuf (); 277 | str.add ('${g.label}\n\n'); 278 | str.add (StringTools.lpad ("incl_size", " ", 9)); 279 | str.add (StringTools.lpad ("addr", " ", 9)); 280 | str.add ("\n"); 281 | //str.add (" " + "class_name" + "\n"); 282 | var nrows = 0; 283 | 284 | for (node in g.nodes) { 285 | var visited = new Map (); 286 | node.inclSize = node.inclusiveSize (visited, azr.depthLimit, 0); 287 | } 288 | g.nodes.sort (function (a:ObjectNode, b:ObjectNode):Int { 289 | return b.inclSize - a.inclSize; 290 | }); 291 | for (node in g.nodes) { 292 | str.add (StringTools.lpad ("" + node.inclSize, " ", 9)); 293 | str.add (StringTools.lpad ("" + StringTools.hex (node.addr, 8), " ", 9)); 294 | //str.add (" " + node.className); 295 | str.add ("\n"); 296 | nrows += 1; 297 | if (nrows > maxRows) { 298 | break; 299 | } 300 | } 301 | 302 | objectListView.setText (str.toString ()); 303 | 304 | } 305 | 306 | 307 | private function updateObjectAnalysis ():Void { 308 | 309 | var str = new StringBuf (); 310 | str.add ("roots\n\n"); 311 | 312 | var node = selectedNode; 313 | if (node == null) { 314 | return; 315 | } 316 | 317 | var rootPaths = new Array> (); 318 | { 319 | var visited = new Map (); 320 | node.findRoots (rootPaths, visited, new Array ()); 321 | } 322 | rootPaths.sort (function (a:Array, b:Array):Int { 323 | return a.length - b.length; 324 | }); 325 | for (i in 0...(rootPaths.length)) { 326 | var path = rootPaths[i]; 327 | path.reverse (); 328 | var root = rootPaths[i][0]; 329 | var addr = StringTools.hex (root.addr, 8); 330 | var prevArray = false; 331 | for (j in 0...(path.length)) { 332 | var node = path[j]; 333 | if (node.className == "Array") { 334 | if (node.fieldName != "") { 335 | if (j != 0) str.add ("."); 336 | str.add (node.fieldName); 337 | } 338 | prevArray = true; 339 | } else if (prevArray) { 340 | str.add ("[i]"); 341 | prevArray = false; 342 | } else { 343 | if (node.fieldName != "") { 344 | if (j != 0) str.add ("."); 345 | str.add (node.fieldName); 346 | } 347 | } 348 | } 349 | str.add ("\n"); 350 | for (j in 0...(path.length)) { 351 | var node = path[j]; 352 | addr = StringTools.hex (node.addr, 8); 353 | str.add (' ${addr} ${node.fieldName}:${node.className}\n'); 354 | } 355 | } 356 | 357 | objectAnalysisView.setText (str.toString ()); 358 | 359 | } 360 | 361 | 362 | } 363 | 364 | 365 | enum MainViewTab { 366 | 367 | OverviewTab; 368 | BrowserTab; 369 | 370 | } 371 | 372 | 373 | class Control { 374 | 375 | 376 | public var posX:Float; 377 | public var posY:Float; 378 | public var sizeX:Float; 379 | public var sizeY:Float; 380 | 381 | 382 | public function updateLayout (x:Float, y:Float, w:Float, h:Float):Void { 383 | 384 | posX = x; 385 | posY = y; 386 | sizeX = w; 387 | sizeY = h; 388 | 389 | } 390 | 391 | 392 | } 393 | 394 | 395 | class HSplitViewControl extends Control { 396 | 397 | static var splitterWidth (default, never) = 6; 398 | 399 | var display:DisplayObjectContainer; 400 | 401 | var controls = new Array (); 402 | var splitters = new Array (); 403 | var leftControls = new Array (); 404 | var rightControls = new Array (); 405 | 406 | var activeSplitter = -1; 407 | 408 | 409 | public function new (display:DisplayObjectContainer) { 410 | 411 | this.display = display; 412 | 413 | display.addEventListener (MouseEvent.MOUSE_MOVE, onMouseMove); 414 | display.addEventListener (MouseEvent.MOUSE_UP, onMouseUp); 415 | 416 | } 417 | 418 | 419 | public override function updateLayout (x:Float, y:Float, w:Float, h:Float):Void { 420 | 421 | super.updateLayout (x, y, w, h); 422 | 423 | for (i in 0...(controls.length)) { 424 | var c = controls[i]; 425 | if (i < controls.length - 1) { 426 | c.updateLayout (c.posX, c.posY, c.sizeX, h); 427 | } else { 428 | c.updateLayout (c.posX, c.posY, w - c.posX, h); 429 | } 430 | } 431 | 432 | for (i in 0...(splitters.length)) { 433 | splitters[i].x = rightControls[i].posX - splitterWidth; 434 | splitters[i].height = h; 435 | } 436 | 437 | } 438 | 439 | 440 | public function addControl (c:Control):Void { 441 | 442 | controls.push (c); 443 | 444 | if (controls.length > 1) { 445 | addSplitter (controls[controls.length-2], controls[controls.length-1]); 446 | } 447 | 448 | } 449 | 450 | 451 | public function firstLayout (x:Float, y:Float, w:Float, h:Float):Void { 452 | 453 | var x:Float = 0; 454 | var cw = Math.floor ((w - splitterWidth * splitters.length) / controls.length); 455 | for (c in controls) { 456 | c.updateLayout (x, c.posY, cw, h); 457 | x += cw + splitterWidth; 458 | } 459 | 460 | posX = x; 461 | posY = y; 462 | sizeX = w; 463 | sizeY = h; 464 | 465 | } 466 | 467 | 468 | private function addSplitter (left:Control, right:Control) { 469 | 470 | var splitter = new Sprite (); 471 | splitter.graphics.beginFill (0xff333333); 472 | splitter.graphics.drawRect (0, 0, splitterWidth, splitterWidth); 473 | display.addChild (splitter); 474 | 475 | var splitterIndex = splitters.length; 476 | splitters.push (splitter); 477 | leftControls.push (left); 478 | rightControls.push (right); 479 | 480 | var hovering = false; 481 | var sizing = false; 482 | splitter.addEventListener (MouseEvent.MOUSE_OVER, function (event:MouseEvent):Void { 483 | hovering = true; 484 | //lime.ui.Mouse.cursor = RESIZE_WE; 485 | }); 486 | 487 | splitter.addEventListener (MouseEvent.MOUSE_OUT, function (event:MouseEvent):Void { 488 | hovering = false; 489 | //lime.ui.Mouse.cursor = RESIZE_WE; 490 | }); 491 | 492 | splitter.addEventListener (MouseEvent.MOUSE_DOWN, function (event:MouseEvent):Void { 493 | activeSplitter = splitterIndex; 494 | }); 495 | 496 | splitter.addEventListener (MouseEvent.MOUSE_UP, onMouseUp); 497 | 498 | } 499 | 500 | 501 | private function onMouseMove (event:MouseEvent):Void { 502 | 503 | if (activeSplitter >= 0) { 504 | 505 | moveSplitter (activeSplitter, event.stageX); 506 | 507 | } 508 | 509 | } 510 | 511 | 512 | function onMouseUp (event:MouseEvent):Void { 513 | 514 | if (activeSplitter >= 0) { 515 | 516 | moveSplitter (activeSplitter, event.stageX); 517 | activeSplitter = -1; 518 | 519 | } 520 | 521 | } 522 | 523 | 524 | public function moveSplitter (index:Int, mouseX:Float):Void { 525 | 526 | var s = splitters[index]; 527 | var left = leftControls[index]; 528 | var right = rightControls[index]; 529 | 530 | var minX = left.posX + 120; 531 | var maxX = right.posX + right.sizeX - 120; 532 | 533 | var gap = s.width; 534 | var x = mouseX - gap/2; 535 | if (x < minX) { 536 | x = minX; 537 | } 538 | if (x > maxX) { 539 | x = maxX; 540 | } 541 | s.x = x; 542 | 543 | left.updateLayout (left.posX, left.posY, x - left.posX, left.sizeY); 544 | right.updateLayout (x + gap, right.posY, right.sizeX + right.posX - (x+gap), right.sizeY); 545 | 546 | } 547 | 548 | 549 | } 550 | 551 | 552 | // TableControl is not really what you expect for a table 553 | // control. It is more of a TextField that sort of acts like 554 | // one. 555 | class TableControl extends Control { 556 | 557 | 558 | public var onRowClick:MouseEvent->Int->Void; 559 | public var onRowHover:Int->Void; 560 | 561 | 562 | var tf:TextField; 563 | 564 | 565 | public function new (display:DisplayObjectContainer) { 566 | 567 | var textFormat = new TextFormat ("_typewriter", 12, 0x000000, false, false, false, "", "", TextFormatAlign.LEFT, 0, 0, 0, 0); 568 | tf = new TextField (); 569 | display.addChild (tf); 570 | tf.selectable = false; 571 | tf.defaultTextFormat = textFormat; 572 | tf.multiline = true; 573 | 574 | tf.addEventListener (MouseEvent.MOUSE_DOWN, onTextClick); 575 | tf.addEventListener (MouseEvent.MOUSE_OVER, onTextHover); 576 | 577 | } 578 | 579 | 580 | public override function updateLayout (x:Float, y:Float, w:Float, h:Float):Void { 581 | 582 | super.updateLayout (x, y, w, h); 583 | 584 | tf.x = x; 585 | tf.y = y; 586 | tf.width = (w > 0) ? w : 1; 587 | tf.height = (h > 0) ? h : 1; 588 | 589 | } 590 | 591 | 592 | public function setText (s:String):Void { 593 | 594 | tf.text = s; 595 | 596 | } 597 | 598 | 599 | private static function isValidWordChar (charCode:Int):Bool { 600 | 601 | if (charCode >= "0".code && charCode <= "9".code) { 602 | return true; 603 | } 604 | if (charCode >= "a".code && charCode <= "z".code) { 605 | return true; 606 | } 607 | if (charCode >= "A".code && charCode <= "Z".code) { 608 | return true; 609 | } 610 | if (charCode == "_".code) { 611 | return true; 612 | } 613 | return false; 614 | 615 | } 616 | 617 | public function getWordAt (x:Float, y:Float):String { 618 | 619 | var s = tf.text; 620 | var firstChar = tf.getCharIndexAtPoint (x, y); 621 | var line = tf.getLineIndexOfChar (firstChar); 622 | var minChar = tf.getLineOffset (line); 623 | var maxChar = minChar + tf.getLineLength (line); 624 | var lastChar = firstChar; 625 | while (true) { 626 | if (firstChar <= minChar) { 627 | break; 628 | } 629 | if (!isValidWordChar (s.charCodeAt (firstChar-1))) { 630 | break; 631 | } 632 | firstChar--; 633 | } 634 | while (true) { 635 | if (lastChar >= maxChar) { 636 | break; 637 | } 638 | if (!isValidWordChar (s.charCodeAt (lastChar+1))) { 639 | break; 640 | } 641 | lastChar++; 642 | } 643 | return s.substring (firstChar, lastChar+1); 644 | } 645 | 646 | 647 | private function onTextClick (event:MouseEvent):Void { 648 | 649 | if (onRowClick != null) { 650 | var lineIndex = tf.getLineIndexAtPoint (event.localX, event.localY); 651 | onRowClick (event, lineIndex); 652 | } 653 | 654 | } 655 | 656 | 657 | private function onTextHover (event:MouseEvent):Void { 658 | 659 | if (onRowHover != null) { 660 | var lineIndex = tf.getLineIndexAtPoint (event.localX, event.localY); 661 | onRowHover (lineIndex); 662 | } 663 | 664 | } 665 | 666 | 667 | } 668 | -------------------------------------------------------------------------------- /Source/ObjectGroup.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | 4 | class ObjectGroup { 5 | 6 | 7 | public var label:String; 8 | public var nodes:Array; 9 | 10 | public var inclusiveSize:Int; 11 | 12 | public function new () {} 13 | 14 | 15 | public static function sortByInstances (groups:Array):Void { 16 | 17 | groups.sort (function (a:ObjectGroup, b:ObjectGroup):Int { 18 | return b.nodes.length - a.nodes.length; 19 | }); 20 | 21 | } 22 | 23 | 24 | public static function sortBySize (groups:Array):Void { 25 | 26 | groups.sort (function (a:ObjectGroup, b:ObjectGroup):Int { 27 | return b.inclusiveSize - a.inclusiveSize; 28 | }); 29 | 30 | } 31 | 32 | 33 | public function compute (depthLimit:Int):Void { 34 | inclusiveSize = computeInclusiveSize (depthLimit); 35 | } 36 | 37 | 38 | private function computeInclusiveSize (depthLimit:Int):Int { 39 | var size = 0; 40 | var visited = new Map (); 41 | for (n in nodes) { 42 | size += n.inclusiveSize (visited, depthLimit, 0); 43 | } 44 | return size; 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Source/ObjectNode.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | 4 | class ObjectNode { 5 | 6 | 7 | public var addr:Int; 8 | public var className:String; 9 | public var fieldName:String; 10 | public var size:Int; 11 | public var members:Array; 12 | public var memberNames:Array; 13 | public var referrers:Array; 14 | 15 | // here for convenience. not computed by default. 16 | public var inclSize:Int; 17 | 18 | 19 | public function new () {} 20 | 21 | 22 | public function totalCount (visited:Map):Int { 23 | 24 | if (visited.get (addr) != null) { 25 | return 0; 26 | } 27 | visited.set (addr, true); 28 | 29 | var count = 1; 30 | 31 | for (m in members) { 32 | count += m.totalCount (visited); 33 | } 34 | 35 | return count; 36 | 37 | } 38 | 39 | 40 | public function inclusiveSize (visited:Map, depthLimit:Int, depth:Int):Int { 41 | 42 | if (visited.get (addr) != null) { 43 | return 0; 44 | } 45 | visited.set (addr, true); 46 | 47 | if (depth > depthLimit) { 48 | return 0; 49 | } 50 | 51 | var size = this.size; 52 | 53 | for (m in members) { 54 | size += m.inclusiveSize (visited, depthLimit, depth + 1); 55 | } 56 | 57 | return size; 58 | 59 | } 60 | 61 | 62 | public function findRoots ( 63 | rootPaths:Array>, 64 | visited:Map, 65 | stack:Array 66 | ):Void { 67 | 68 | if (visited.get (addr) != null) { 69 | return; 70 | } 71 | visited.set (addr, true); 72 | 73 | stack.push (this); 74 | 75 | var isRoot = true; 76 | for (refr in referrers) { 77 | if (refr.referrers.length > 0) { 78 | isRoot = false; 79 | } 80 | refr.findRoots (rootPaths, visited, stack); 81 | } 82 | 83 | // TODO(james4k): what are these guys that we have no names 84 | // for?? may be rooted by stack, but hard to tell 85 | //if (isRoot) { 86 | if (isRoot && className != "") { 87 | rootPaths.push (stack.copy ()); 88 | } 89 | 90 | stack.pop (); 91 | 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /Source/ObjectTable.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | 4 | class ObjectTable { 5 | 6 | 7 | public var classNameIDs = new Map (); 8 | public var fieldNameIDs = new Map (); 9 | public var classNames = new Array (); 10 | public var fieldNames = new Array (); 11 | public var lastClassID = 0; 12 | public var lastFieldID = 0; 13 | 14 | // table 15 | public var thisAddrs = new Array (); 16 | public var memberAddrs = new Array (); 17 | public var memberSizes = new Array (); 18 | public var memberClassIDs = new Array (); 19 | public var memberFieldIDs = new Array (); 20 | 21 | // graph 22 | public var addrNodes = new Map (); 23 | //public var roots = new Array (); 24 | 25 | 26 | public function new () {} 27 | 28 | 29 | public static function read (input:haxe.io.Input):ObjectTable { 30 | 31 | var table = new ObjectTable (); 32 | 33 | try { 34 | 35 | var tableHeader = input.readLine (); 36 | if (tableHeader != "this_addr,member_addr,member_size,member_class_name,member_field_name") { 37 | throw "unexpected table header"; 38 | } 39 | 40 | while (true) { 41 | 42 | var line = input.readLine (); 43 | if (line.length == 0) { 44 | continue; 45 | } 46 | 47 | var cells = line.split (","); 48 | if (cells.length != 5) { 49 | throw "unexpected table format"; 50 | } 51 | 52 | table.thisAddrs.push (Std.parseInt ("0x" + cells[0])); 53 | table.memberAddrs.push (Std.parseInt ("0x" + cells[1])); 54 | table.memberSizes.push (Std.parseInt (cells[2])); 55 | { 56 | var id = table.classNameIDs.get (cells[3]); 57 | if (id == null) { 58 | id = table.lastClassID++; 59 | table.classNameIDs.set(cells[3], id); 60 | table.classNames.push(cells[3]); 61 | } 62 | table.memberClassIDs.push (id); 63 | } 64 | { 65 | var id = table.fieldNameIDs.get (cells[4]); 66 | if (id == null) { 67 | id = table.lastFieldID++; 68 | table.fieldNameIDs.set(cells[4], id); 69 | table.fieldNames.push(cells[4]); 70 | } 71 | table.memberFieldIDs.push (id); 72 | } 73 | 74 | } 75 | 76 | } catch (e:haxe.io.Eof) { 77 | } 78 | 79 | table.buildGraph (); 80 | 81 | return table; 82 | 83 | } 84 | 85 | 86 | private function buildGraph () { 87 | 88 | for (i in 0...(thisAddrs.length)) { 89 | 90 | var thisNode = addrNodes.get (thisAddrs[i]); 91 | var memberNode = addrNodes.get (memberAddrs[i]); 92 | if (thisNode == null) { 93 | thisNode = new ObjectNode (); 94 | thisNode.addr = thisAddrs[i]; 95 | thisNode.className = ""; 96 | thisNode.members = new Array (); 97 | thisNode.memberNames = new Array (); 98 | thisNode.referrers = new Array (); 99 | addrNodes.set (thisAddrs[i], thisNode); 100 | } 101 | if (memberNode == null) { 102 | memberNode = new ObjectNode (); 103 | memberNode.addr = memberAddrs[i]; 104 | memberNode.className = classNames[memberClassIDs[i]]; 105 | memberNode.fieldName = fieldNames[memberFieldIDs[i]]; 106 | memberNode.size = memberSizes[i]; 107 | memberNode.members = new Array (); 108 | memberNode.memberNames = new Array (); 109 | memberNode.referrers = new Array (); 110 | addrNodes.set (memberAddrs[i], memberNode); 111 | } else if (memberNode.className == "") { 112 | memberNode.className = classNames[memberClassIDs[i]]; 113 | memberNode.fieldName = fieldNames[memberFieldIDs[i]]; 114 | memberNode.size = memberSizes[i]; 115 | } 116 | 117 | thisNode.members.push (memberNode); 118 | thisNode.memberNames.push (fieldNames[memberFieldIDs[i]]); 119 | memberNode.referrers.push (thisNode); 120 | 121 | } 122 | 123 | #if 0 124 | for (node in addrNodes) { 125 | if (node.className == "") { 126 | roots.push (node); 127 | } 128 | } 129 | #end 130 | 131 | } 132 | 133 | 134 | } 135 | 136 | -------------------------------------------------------------------------------- /project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------