├── 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 |
--------------------------------------------------------------------------------