Selaa lähdekoodia

memory-profiler: Stats & start of hierarchical view

lviguier 1 vuosi sitten
vanhempi
commit
11a64e2e21
8 muutettua tiedostoa jossa 2364 lisäystä ja 0 poistoa
  1. 6 0
      bin/app.html
  2. 158 0
      bin/style.css
  3. 194 0
      bin/style.less
  4. 6 0
      hide/Ide.hx
  5. 208 0
      hide/tools/memory/Block.hx
  6. 1128 0
      hide/tools/memory/Memory.hx
  7. 188 0
      hide/tools/memory/TType.hx
  8. 476 0
      hide/view/Profiler.hx

+ 6 - 0
bin/app.html

@@ -100,6 +100,12 @@
 		<menu label="Save As..." class="saveas"></menu>
 		<menu label="Manage" class="manage" disabled="disabled"></menu>
 	</menu>
+	<menu label="Profiler" class="prof">
+		<div class="content">
+		</div>
+		<separator></separator>
+		<menu label="Show" class="show"></menu>
+	</menu>
 </xml>
 <script src="hide.js"></script>
 <script>

+ 158 - 0
bin/style.css

@@ -2951,3 +2951,161 @@ div.gradient-box {
   width: 20;
   background: #111;
 }
+.profiler {
+  width: 100%;
+  height: 100%;
+  display: flex;
+}
+.profiler .left-panel {
+  height: 100%;
+  width: 80%;
+  display: flex;
+  flex-direction: column;
+}
+.profiler .left-panel .tree-map {
+  background-color: #4c00ff;
+  width: 100%;
+  flex-grow: 1;
+}
+.profiler .left-panel .hierarchy {
+  background-color: #303030;
+  width: 100%;
+  max-height: 1200px;
+  height: 30%;
+}
+.profiler .left-panel .hierarchy table {
+  overflow-y: scroll;
+}
+.profiler .left-panel .hierarchy table thead {
+  position: sticky;
+  top: 0;
+  background-color: #151515;
+}
+.profiler .left-panel .hierarchy table thead td {
+  font-weight: bold;
+}
+.profiler .left-panel .hierarchy table tr:hover {
+  background-color: #272727;
+}
+.profiler .left-panel .hierarchy table thead tr:hover {
+  background-color: #151515;
+}
+.profiler .left-panel .hierarchy table td:last-child {
+  width: 100%;
+}
+.profiler .left-panel .hierarchy table td:nth-child(3) {
+  width: 70%;
+  max-width: 700px;
+}
+.profiler .left-panel .hierarchy table td {
+  padding: 3px 30px 3px 1px;
+  overflow: hidden;
+  white-space: nowrap;
+}
+.profiler .left-panel .hierarchy table td .folder {
+  cursor: pointer;
+  margin-right: 10px;
+  margin-left: 5px;
+  font-size: 12pt;
+  text-align: center;
+  vertical-align: text-top;
+}
+.profiler .left-panel .hierarchy table td .outer-gauge {
+  background-color: #535353;
+  width: 100%;
+  height: 15px;
+}
+.profiler .left-panel .hierarchy table td .inner-gauge {
+  background-color: #d6d6d6;
+  height: 15px;
+}
+.profiler .left-panel .hierarchy table .inspect-row td {
+  width: 100px;
+}
+.profiler .right-panel {
+  height: 100%;
+  width: 20%;
+  padding: 10px;
+}
+.profiler .right-panel dl {
+  overflow-x: hidden;
+  margin: 0px;
+}
+.profiler .right-panel dt {
+  width: 80px;
+  text-align: right;
+  font-size: 11px;
+  color: #aaa;
+  user-select: none;
+  text-transform: capitalize;
+  cursor: pointer;
+  font-weight: normal;
+  vertical-align: middle;
+  display: inline-block;
+  text-wrap: wrap;
+  word-break: break-word;
+  margin-top: 4px;
+}
+.profiler .right-panel dd {
+  position: relative;
+  width: 200px;
+  margin-left: 10px;
+  vertical-align: middle;
+  display: inline-block;
+  text-wrap: wrap;
+  word-break: break-word;
+  margin-top: 4px;
+}
+.profiler .right-panel .outer-gauge {
+  background-color: #535353;
+  width: 100%;
+  height: 20px;
+}
+.profiler .right-panel .inner-gauge {
+  background-color: #d6d6d6;
+  height: 20px;
+}
+.profiler .right-panel h4 {
+  margin-bottom: 2px;
+}
+.profiler .right-panel .title {
+  text-align: center;
+  font-size: 9pt;
+  background-color: #151515;
+  margin-bottom: 5px;
+  margin-top: 20px;
+  vertical-align: middle;
+}
+.profiler .right-panel .title:first-child {
+  margin-top: 5px;
+}
+.profiler .right-panel .drop-zone {
+  background-color: #111111;
+  border-style: dashed;
+  border-width: 5px;
+  height: 100px;
+  padding-top: 20px;
+}
+.profiler .right-panel .drop-zone .icon {
+  text-align: center;
+  font-size: 30pt;
+  margin: 0;
+  font-weight: bold;
+  pointer-events: none;
+}
+.profiler .right-panel .drop-zone .label {
+  text-align: center;
+  pointer-events: none;
+}
+@keyframes zoomIn {
+  from {
+    transform: scale(0);
+  }
+  to {
+    transform: scale(1);
+  }
+}
+.profiler .right-panel .files-input #process-btn {
+  width: 100%;
+  margin-top: 15px;
+}

+ 194 - 0
bin/style.less

@@ -3370,4 +3370,198 @@ div.gradient-box {
 		width : 20;
 		background : #111;
 	}
+}
+
+.profiler {
+	width: 100%;
+	height: 100%;
+	display: flex;
+
+	.left-panel {
+		height: 100%;
+		width: 80%;
+		display: flex;
+		flex-direction: column;
+
+		.tree-map {
+			background-color: #4c00ff;
+			width: 100%;
+			flex-grow: 1;
+		}
+
+		.hierarchy {
+			background-color: #303030;
+			width: 100%;
+			max-height: 1200px;
+			height: 30%;
+
+			table {
+				overflow-y: scroll;
+
+				thead {
+					position: sticky;
+					top: 0;
+					background-color: #151515;
+
+					td {
+						font-weight: bold;
+					}
+				}
+
+				tr:hover{
+					background-color: #272727;
+				}
+
+				thead {
+					tr:hover {
+						background-color: #151515;
+					}
+				}
+
+				td:last-child {
+					width: 100%;
+				}
+
+				td:nth-child(3) {
+					width: 70%;
+					max-width: 700px;
+				}
+
+				td {
+					padding: 3px 30px 3px 1px;
+					overflow: hidden;
+					white-space: nowrap;
+
+					.folder {
+						cursor: pointer;
+						margin-right: 10px;
+						margin-left: 5px;
+						font-size: 12pt;
+						text-align: center;
+						vertical-align: text-top;
+					}
+
+					.outer-gauge {
+						background-color: #535353;
+						width: 100%;
+						height: 15px;
+					}
+
+					.inner-gauge {
+						background-color: #d6d6d6;
+						height: 15px;
+					}
+				}
+
+				.inspect-row {
+					td {
+						width: 100px;
+					}
+				}
+			}
+		}
+	}
+
+	.right-panel {
+		height: 100%;
+		width: 20%;
+		padding: 10px;
+
+		dl {
+			overflow-x: hidden;
+			margin: 0px;
+		}
+
+		dt {
+			width: 80px;
+			text-align: right;
+			font-size: 11px;
+			color: #aaa;
+			user-select: none;
+			text-transform: capitalize;
+			cursor: pointer;
+			font-weight: normal;
+			vertical-align: middle;
+			display: inline-block;
+			text-wrap: wrap;
+			word-break: break-word;
+			margin-top: 4px;
+		}
+
+		dd {
+			position: relative;
+			width: 200px;
+			margin-left: 10px;
+			vertical-align: middle;
+			display: inline-block;
+			text-wrap: wrap;
+			word-break: break-word;
+			margin-top: 4px;
+		}
+
+		.outer-gauge {
+			background-color: #535353;
+			width: 100%;
+			height: 20px;
+		}
+
+		.inner-gauge {
+			background-color: #d6d6d6;
+			height: 20px;
+		}
+
+		h4 {
+			margin-bottom: 2px;
+		}
+
+		.title {
+			text-align: center;
+			font-size: 9pt;
+			background-color: #151515;
+			margin-bottom: 5px;
+			margin-top: 20px;
+			vertical-align: middle;
+		}
+
+		.title:first-child {
+			margin-top: 5px;
+		}
+
+		.drop-zone {
+			background-color: #111111;
+			border-style: dashed;
+			border-width: 5px;
+			height: 100px;
+			padding-top: 20px;
+
+			.icon {
+				text-align: center;
+				font-size: 30pt;
+				margin: 0;
+				font-weight: bold;
+				pointer-events: none;
+			}
+
+			.label {
+				text-align: center;
+				pointer-events: none;
+			}
+
+			@keyframes zoomIn {
+				from {
+					transform: scale(0);
+				}
+				to {
+					transform: scale(1);
+				}
+			}
+		}
+
+		.files-input {
+			#process-btn {
+				width: 100%;
+				margin-top: 15px;
+			}
+		}
+	}
 }

+ 6 - 0
hide/Ide.hx

@@ -1114,6 +1114,12 @@ class Ide extends hide.tools.IdeData {
 			config.global.save();
 		});
 
+		// profilers
+		var profilers = menu.find(".prof");
+		profilers.find(".show").click(function(_) {
+			open("hide.view.Profiler",{});
+		});
+
 		window.menu = new hide.ui.Menu(menu).root;
 	}
 

+ 208 - 0
hide/tools/memory/Block.hx

@@ -0,0 +1,208 @@
+package hide.tools.memory;
+
+abstract Pointer(haxe.Int64) {
+
+	public var value(get,never) : haxe.Int64;
+
+	inline function get_value() return this;
+
+	inline function new(v) this = v;
+
+	public inline function isNull() {
+		return this == 0;
+	}
+	public inline function offset( i : Int ) : Pointer {
+		return cast (this + i);
+	}
+	public inline function sub( p : Pointer ) : Int {
+		return haxe.Int64.toInt(this - p.value);
+	}
+	public inline function toString() {
+		return "0x"+(this.high == 0 ? StringTools.hex(this.high, 8) : "")+StringTools.hex(this.low,8);
+	}
+	public inline function shift( k : Int ) : haxe.Int64 {
+		return haxe.Int64.shr(this,k);
+	}
+
+	public static var NULL(get,never) : Pointer;
+	inline static function get_NULL() return new Pointer(0);
+
+}
+
+enum abstract PageKind(Int) {
+	var PDynamic = 0;
+	var PRaw = 1;
+	var PNoPtr = 2;
+	var PFinalizer = 3;
+}
+
+class Page {
+	public var addr : Pointer;
+	public var kind : PageKind;
+	public var size : Int;
+	public var reserved : Int;
+	public var dataPosition : Int = -1;
+
+	public function new() {
+	}
+
+	public inline function memHasPtr() {
+		return kind == PDynamic || kind == PRaw;
+	}
+
+}
+
+class Stack {
+	public var base : Pointer;
+	public var contents : Array<Pointer>;
+	public function new() {
+	}
+}
+
+enum BlockTypeKind {
+	KHeader;
+	KRoot;
+	KAbstractData;
+	KDynObjData;
+	KInferred( t : TType, k : BlockTypeKind );
+}
+
+class BlockSub {
+	public var b : Block;
+	public var fid : Int;
+	public function new(b,fid) {
+		this.b = b;
+		this.fid = fid;
+	}
+}
+
+class Block {
+	public static var MARK_UID = 0;
+
+	public var page : Page;
+	public var addr : Pointer;
+	public var size : Int;
+	public var typePtr : Pointer;
+	public var owner : Block;
+	public var type(default, set) : TType;
+	public var typeKind : BlockTypeKind;
+
+	public var depth : Int = -1;
+	public var mark : Int = -1;
+
+	public var subs : Array<BlockSub>; // can be null
+	public var parents : Array<Block>; // if multiple owners
+
+	public function new() {
+	}
+
+	inline function set_type( t : TType ) {
+		return type = t;
+	}
+
+	public function addParent(b:Block,fid:Int=0) {
+		if( owner == null ) {
+			owner = b;
+		} else {
+			if( parents == null ) parents = [owner];
+			parents.push(b);
+		}
+		if( b.subs == null ) b.subs = [];
+		b.subs.push(new BlockSub(this,fid));
+	}
+
+	public function makeTID( prev : Block, withField : Bool ) {
+		if( type == null )
+			return 0;
+		if( withField )
+			for( s in subs )
+				if( s.b == prev )
+					return type.tid | (s.fid << 24);
+		return type.tid;
+	}
+
+	public function getParents() {
+		return parents != null ? parents : owner == null ? [] : [owner];
+	}
+
+	public function markDepth() {
+		var d = depth + 1;
+		var all = subs;
+		if( all == null ) return;
+		while( all.length > 0 ) {
+			var out = [];
+			for( b in all ) {
+				var b = b.b;
+				if( b.depth < 0 || b.depth > d ) {
+					b.depth = d;
+					if( b.subs != null ) for( s in b.subs ) out.push(s);
+				}
+			}
+			all = out;
+			d++;
+		}
+	}
+
+	public function finalize() {
+		if( parents == null ) return;
+
+		inline function getPriority(b:Block) {
+			var d = -b.depth * 5;
+			if( b.type == null ) return d-2;
+			if( !b.type.hasPtr ) return d-1; // false positive
+			return switch( b.type.t ) {
+			case HFun(_): d;
+			case HVirtual(_): d+1;
+			default: d+2;
+			}
+		}
+
+		parents.sort(function(p1, p2) return getPriority(p2) - getPriority(p1));
+		owner = parents[0];
+	}
+
+	function removeParent( p : Block ) {
+		if( parents != null ) {
+			parents.remove(p);
+			if( parents.length == 0 ) parents = null;
+		}
+		if( owner == p )
+			owner = parents == null ? null : parents[0];
+	}
+
+	public function removeChildren() {
+		if( subs != null ) {
+			for( s in subs )
+				s.b.removeParent(this);
+			subs = null;
+		}
+	}
+
+}
+
+@:generic
+class PointerMap<T> {
+
+	var lookup : Map<Int,Map<Int,T>>;
+
+	public function new() {
+		lookup = new Map();
+	}
+
+	public function set( p : Pointer, v : T ) {
+		var c = lookup.get(p.value.high);
+		if( c == null ) {
+			c = new Map();
+			lookup.set(p.value.high, c);
+		}
+		c.set(p.value.low, v);
+	}
+
+	public function get( p : Pointer ) : T {
+		if( p.isNull() ) return null;
+		var c = lookup.get(p.value.high);
+		if( c == null ) return null;
+		return c.get(p.value.low);
+	}
+
+}

+ 1128 - 0
hide/tools/memory/Memory.hx

@@ -0,0 +1,1128 @@
+package hide.tools.memory;
+
+import format.hl.Data;
+using format.hl.Tools;
+import hide.tools.memory.Block;
+
+typedef TypeValue = { tl : Array<Int>, count : Int, mem : Int }
+
+class Stats {
+	var mem : Memory;
+	var byT = new Map();
+	var allT = [];
+
+	public function new(mem) {
+		this.mem = mem;
+	}
+
+	public function add( t : TType, mem : Int ) {
+		return addPath([t == null ? 0 : t.tid], mem);
+	}
+
+	public inline function makeID( t : TType, field : Int ) {
+		return t.tid | (field << 24);
+	}
+
+	public function addPath( tl : Array<Int>, mem : Int ) {
+		var key = tl.join(" ");
+		var inf = byT.get(key);
+		if( inf == null ) {
+			inf = { tl : tl, count : 0, mem : 0 };
+			byT.set(key, inf);
+			allT.push(inf);
+		}
+		inf.count++;
+		inf.mem += mem;
+	}
+
+	public function print( withSum = false ) {
+		sort( @:privateAccess mem.sortByCount );
+		var totCount = 0;
+		var totMem = 0;
+		var max = @:privateAccess mem.maxLines;
+		if( max > 0 && allT.length > max ) {
+			mem.log("<ignore "+(allT.length - max)+" lines>");
+			allT = allT.slice(allT.length - max);
+		}
+		for( i in allT ) {
+			totCount += i.count;
+			totMem += i.mem;
+			var tpath = getPathStrings(mem, i.tl);
+			mem.log(Memory.withColor(i.count + " count, " + Memory.MB(i.mem) + " ", 33) + tpath.join(${Memory.withColor(' > ', 36)}));
+		}
+		if( withSum )
+			mem.log("Total: "+totCount+" count, "+Memory.MB(totMem));
+	}
+
+	public static function getTypeString(mem : Memory, id : Int){
+		var t = mem.types[id & 0xFFFFFF];
+		var tstr = t.toString();
+		var fid = id >>> 24;
+		if( fid > 0 ) {
+			var f = t.memFieldsNames[fid-1];
+			if( f != null ) tstr += "." + f;
+		}
+		return tstr;
+	}
+
+	public static function getPathStrings(mem : Memory, i : Array<Int>){
+		var tpath = [];
+		for( tid in i )
+			tpath.push(getTypeString(mem, tid));
+
+		return tpath;
+	}
+
+	public function sort( byCount = true , asc = true) {
+		if( byCount )
+			allT.sort(function(i1, i2) return asc ? i1.count - i2.count : i2.count - i1.count);
+		else
+			allT.sort(function(i1, i2) return asc ? i1.mem - i2.mem : i2.mem - i1.mem);
+	}
+}
+
+
+enum FieldsMode {
+	Full;
+	Parents;
+	None;
+}
+
+enum FilterMode {
+	None;
+	Intersect;	// Will display only blocks present in all memories
+	Unique;		// Will display only blocks not present in other memories
+}
+
+class Memory {
+
+	public static var readIntCpt = 0;
+	public static var readPointerCpt = 0;
+	public var memoryDump : sys.io.FileInput;
+	public var memoryDumpBytes : haxe.io.Bytes;
+	public var pos : Int;
+
+	public var is64 : Bool;
+	public var bool32 : Bool;
+	public var ptrBits : Int;
+
+	public var types : Array<TType>;
+
+	var otherMems : Array<Memory>;
+	var filterMode: FilterMode = None;
+
+	var memFile : String;
+
+	var privateData : Int;
+	var markData : Int;
+
+	var sortByCount : Bool;
+	var displayFields : FieldsMode = Full;
+	var displayProgress = true;
+
+	var code : format.hl.Data;
+	var pages : Array<Page>;
+	var roots : Array<Pointer>;
+	var stacks : Array<Stack>;
+	var typesPointers : Array<Pointer>;
+	var closuresPointers : Array<Pointer>;
+	var blocks : Array<Block>;
+	var filteredBlocks : Array<Block> = [];
+	var baseTypes : Array<{ t : HLType, p : Pointer }>;
+	var all : Block;
+
+	var toProcess : Array<Block>;
+	var tdynObj : TType;
+	var tdynObjData : TType;
+	var pointerBlock : PointerMap<Block>;
+	var pointerType : PointerMap<TType>;
+	var falseCandidates : Array<{ b : Block, f : Block, idx : Int }>;
+
+	var currentTypeIndex = 0;
+	var resolveCache : Map<String,TType> = new Map();
+	var maxLines : Int = 100;
+
+	function new() {
+	}
+
+	public function typeSize( t : HLType ) {
+		return switch( t ) {
+		case HVoid: 0;
+		case HUi8: 1;
+		case HUi16: 2;
+		case HI32, HF32: 4;
+		case HI64, HF64: 8;
+		case HBool:
+			return bool32 ? 4 : 1;
+		case HPacked(_), HAt(_):
+			throw "assert";
+		default:
+			return is64 ? 8 : 4;
+		}
+	}
+
+	public function getType( t : HLType, isNull = false ) : TType {
+		// this is quite slow, but we can't use a Map, maybe try a more per-type specific approach ?
+		for( t2 in types )
+			if( t == t2.t )
+				return t2;
+		if( isNull )
+			return null;
+		throw "Type not found " + t.toString();
+		return null;
+	}
+
+	function loadBytecode( arg : String ) {
+		if( code != null ) throw "Duplicate code";
+		code = new format.hl.Reader(false).read(new haxe.io.BytesInput(sys.io.File.getBytes(arg)));
+		log(arg + " code loaded");
+	}
+
+	// function readInt() {
+	// 	return memoryDump.readInt32();
+	// }
+
+	// inline function readPointer() : Pointer {
+	// 	var low = memoryDump.readInt32();
+	// 	var high = is64 ? memoryDump.readInt32() : 0;
+	// 	return cast haxe.Int64.make(high,low);
+	// }
+
+	inline function readPointer() : Pointer {
+		readPointerCpt++;
+		var low = readInt();
+		readIntCpt--;
+		var high = 0;
+		if (is64) {
+			high = readInt();
+			readIntCpt--;
+		}
+		return cast haxe.Int64.make(high,low);
+	}
+
+	function readInt() {
+		readIntCpt++;
+		var ch1 = memoryDumpBytes.get(pos);
+		var ch2 = memoryDumpBytes.get(pos + 1);
+		var ch3 = memoryDumpBytes.get(pos + 2);
+		var ch4 = memoryDumpBytes.get(pos + 3);
+		pos += 4;
+		return memoryDump.bigEndian ? ch4 | (ch3 << 8) | (ch2 << 16) | (ch1 << 24) : ch1 | (ch2 << 8) | (ch3 << 16) | (ch4 << 24);
+	}
+
+	public static function MB( v : Float ) {
+		if( v < 1000 )
+			return Std.int(v) + "B";
+		if( v < 1024 * 1000 )
+			return (Math.round(v * 10 / 1024) / 10)+"KB";
+		return (Math.round(v * 10 / (1024 * 1024)) / 10)+"MB";
+	}
+
+	function loadMemory( arg : String ) {
+		memFile = arg;
+
+		memoryDump = sys.io.File.read(arg);
+		memoryDumpBytes = sys.io.File.getBytes(arg);
+
+		pos = 0;
+		if( memoryDumpBytes.getString(pos, 3) != "HMD" )
+			throw "Invalid memory dump file";
+		pos += 3;
+
+		var version = memoryDumpBytes.get(pos) - "0".code;
+		pos += 1;
+
+		if( version != 1 )
+			throw "Unsupported format version "+version;
+
+		var flags = readInt();
+		is64 = (flags & 1) != 0;
+		bool32 = (flags & 2) != 0;
+		ptrBits = is64 ? 3 : 2;
+		var ptrSize = 1 << ptrBits;
+
+		privateData = readInt();
+		markData = readInt();
+
+		// load pages
+		var count = readInt();
+		pages = [];
+		blocks = [];
+		for( i in 0...count ) {
+			var addr = readPointer();
+			var p = new Page();
+			p.addr = addr;
+			p.kind = cast readInt();
+			p.size = readInt();
+			p.reserved = readInt();
+
+			var readPtr = !p.memHasPtr();
+			while( true ) {
+				var ptr = readPointer();
+				if( ptr.isNull() ) break;
+				var size = readInt();
+				var b = new Block();
+				b.page = p;
+				b.addr = ptr;
+				b.size = size;
+				b.typePtr = @:privateAccess new hide.tools.memory.Pointer(0);
+				b.owner = null;
+				b.typeKind = null;
+				b.subs = null;
+				b.parents = null;
+				if( readPtr && size >= ptrSize ) b.typePtr = readPointer();
+				blocks.push(b);
+			}
+
+			if( p.memHasPtr() ) {
+				p.dataPosition = pos;
+				this.pos += p.size;
+			}
+
+			pages.push(p);
+		}
+
+		// load roots
+		roots = [for( i in 0...readInt() ) readPointer()];
+
+		// load stacks
+		stacks = [];
+		for( i in 0...readInt() ) {
+			var s = new Stack();
+			s.base = readPointer();
+			s.contents = [for( i in 0...readInt() ) readPointer()];
+			stacks.push(s);
+		}
+
+		// load types
+
+		baseTypes = [];
+		while( true ) {
+			var tid = readInt();
+			if( tid < 0 ) break;
+			var ptr = readPointer();
+			baseTypes.push({ t : Type.createEnumIndex(HLType, tid), p : ptr });
+		}
+
+		typesPointers = [for( i in 0...readInt() ) readPointer()];
+		closuresPointers = [for( i in 0...readInt() ) readPointer()];
+	}
+
+	public function getStats() {
+		var pagesSize = 0, reserved = 0;
+		var used = 0, gc = 0;
+		var fUsed = 0;
+		for( p in pages ) {
+			pagesSize += p.size;
+			reserved += p.reserved;
+		}
+		for( b in blocks )
+			used += b.size;
+		for (b in filteredBlocks)
+			fUsed += b.size;
+
+		var strings = [];
+		var string = "--- " + memFile + " ---\n";
+		string += pages.length + " pages, " + MB(pagesSize) + " memory\n";
+		string += roots.length + " roots, "+ stacks.length + " stacks\n";
+		string += code.types.length + " types, " + closuresPointers.length + " closures\n";
+		string += blocks.length + " live blocks " + MB(used) + " used, " + MB(pagesSize - used - reserved) + " free, "+MB(privateData + markData)+" gc\n";
+		if (filterMode != None)
+			string += filteredBlocks.length + " blocks in filter " + MB(fUsed) + " used\n";
+
+		strings.push(string);
+		for (m in otherMems??[]) strings = strings.concat(m.getStats());
+
+		return strings;
+	}
+
+	public function getStatsObj() {
+		var pagesSize = 0, reserved = 0;
+		var used = 0, gc = 0;
+		var fUsed = 0;
+		for( p in pages ) {
+			pagesSize += p.size;
+			reserved += p.reserved;
+		}
+		for( b in blocks )
+			used += b.size;
+		for (b in filteredBlocks)
+			fUsed += b.size;
+
+		return {
+			free: pagesSize - used - reserved,
+			used: used,
+			totalAllocated: pagesSize - reserved,
+			gc : privateData + markData,
+			pagesCount : pages.length,
+			pagesSize : pagesSize,
+			rootsCount : roots.length,
+			stackCount : stacks.length,
+			typesCount : code.types.length,
+			closuresCount : closuresPointers.length,
+			blockCount : blocks.length
+		};
+	}
+
+	function printStats() {
+		var pagesSize = 0, reserved = 0;
+		var used = 0, gc = 0;
+		var fUsed = 0;
+		for( p in pages ) {
+			pagesSize += p.size;
+			reserved += p.reserved;
+		}
+		for( b in blocks )
+			used += b.size;
+		for (b in filteredBlocks)
+			fUsed += b.size;
+		log(withColor("--- " + memFile + " ---", 36));
+		log(pages.length + " pages, " + MB(pagesSize) + " memory");
+		log(roots.length + " roots, "+ stacks.length + " stacks");
+		log(code.types.length + " types, " + closuresPointers.length + " closures");
+		log(blocks.length + " live blocks " + MB(used) + " used, " + MB(pagesSize - used - reserved) + " free, "+MB(privateData + markData)+" gc");
+		if (filterMode != None)
+			log(filteredBlocks.length + " blocks in filter " + MB(fUsed) + " used");
+	}
+
+	function getTypeNull( t : TType ) {
+		if( t.nullWrap != null )
+			return t.nullWrap;
+		for( t2 in types )
+			switch( t2.t ) {
+			case HNull(base) if( base == t.t ):
+				t.nullWrap = t2;
+				return t2;
+			default:
+			}
+		var r = new TType(types.length, HNull(t.t));
+		t.nullWrap = r;
+		types.push(r);
+		return r;
+	}
+
+	function goto( b : Block ) {
+		var p = b.page.dataPosition;
+		if( p < 0 ) throw "assert";
+		pos = p + b.addr.sub(b.page.addr);
+		//memoryDump.seek(p + b.addr.sub(b.page.addr), SeekBegin);
+	}
+
+	function check() {
+		if( code == null ) throw "Missing .hl file";
+		if( memoryDump == null ) throw "Missing .dump file";
+		if( code.types.length != this.typesPointers.length ) throw "Types count mismatch";
+
+		pointerType = new PointerMap();
+		var cid = 0;
+		types = [for( i in 0...code.types.length ) new TType(i, code.types[i])];
+		for( i in 0...typesPointers.length ) {
+			pointerType.set(typesPointers[i], types[i]);
+			switch( code.types[i] ) {
+			case HFun(f):
+				var tid = types.length;
+				var args = f.args.copy();
+				var clparam = args.shift();
+				if( clparam == null ) {
+					cid++;
+					continue;
+				}
+				switch( clparam ) {
+				case HEnum(p) if( p.name == "" ):
+					p.name = '<closure$i context>';
+				default:
+				}
+				var ct = new TType(tid, HFun({ args : args, ret : f.ret }), clparam);
+				types.push(ct);
+				var pt = closuresPointers[cid++];
+				if( !pt.isNull() )
+					pointerType.set(pt, ct);
+			case HObj(o), HStruct(o):
+				if( o.tsuper != null ) {
+					var found = false;
+					for( j in 0...types.length )
+						if( types[j].t == o.tsuper ) {
+							types[i].parentClass = types[j];
+							found = true;
+							break;
+						}
+					if( !found ) throw "Missing parent class";
+				}
+			default:
+			}
+		}
+
+		for( b in baseTypes ) {
+			var t = getType(b.t, true);
+			if( t == null ) {
+				t = new TType(types.length, b.t);
+				types.push(t);
+			}
+			pointerType.set(b.p, t);
+		}
+
+		var progress = 0;
+		pointerBlock = new PointerMap();
+
+		var missingTypes = 0;
+		for( b in blocks ) {
+			progress++;
+			if( displayProgress && progress % 1000 == 0 )
+				Sys.print((Std.int((progress / blocks.length) * 1000.0) / 10) + "%  \r");
+			if( b.page.kind == PDynamic ) {
+				goto(b);
+				b.typePtr = readPointer();
+			}
+			b.type = pointerType.get(b.typePtr);
+			if( b.page.kind == PDynamic && b.type == null && b.typePtr != Pointer.NULL )
+				missingTypes++; // types that we don't have in our dump
+			b.typePtr = Pointer.NULL;
+			if( b.type != null ) {
+				switch( b.page.kind ) {
+				case PDynamic:
+				case PNoPtr:
+					if( b.type.hasPtr ) {
+						if( b.type.t.match(HEnum(_)) ) {
+							// most likely one of the constructor without pointer parameter
+						} else
+							b.type = null; // false positive
+					}
+				case PRaw, PFinalizer:
+					if( b.type.isDyn )
+						b.type = null; // false positive
+				}
+			}
+			if( b.type != null && !b.type.isDyn )
+				b.type = getTypeNull(b.type);
+			if( b.type != null )
+				b.typeKind = KHeader;
+			pointerBlock.set(b.addr, b);
+		}
+
+		//Sys.println(missingTypes+" blocks with unresolved type");
+
+		printStats();
+
+		// look in roots (higher ownership priority)
+		all = new Block();
+
+		var broot = new Block();
+		broot.type = new TType(types.length, HAbstract("roots"));
+		types.push(broot.type);
+		broot.depth = 0;
+		broot.addParent(all);
+
+		for( r in roots ) {
+			var b = pointerBlock.get(r);
+			if( b == null ) continue;
+			b.addParent(broot);
+			if( b.type == null )
+				b.typeKind = KRoot;
+		}
+
+		var tinvalid = new TType(types.length, HAbstract("invalid"));
+		types.push(tinvalid);
+		for( t in types )
+			t.buildTypes(this, tinvalid);
+
+		var tunknown = new TType(types.length, HAbstract("unknown"));
+		types.push(tunknown);
+
+		tdynObj = getType(HDynObj);
+		tdynObjData = new TType(types.length, HAbstract("dynobjdata"));
+		types.push(tdynObjData);
+
+		toProcess = blocks.copy();
+		falseCandidates = [];
+
+
+		while( toProcess.length > 0 )
+			buildHierarchy();
+
+		var i = hide.tools.memory.Memory.readIntCpt;
+		var v = hide.tools.memory.Memory.readPointerCpt;
+		trace(i + " // " + v);
+		// look in stacks (low priority of ownership)
+		var tstacks = new TType(types.length, HAbstract("stack"));
+		var bstacks = [];
+		types.push(tstacks);
+		for( s in stacks ) {
+			var bstack = new Block();
+			bstack.depth = 10000;
+			bstack.type = tstacks;
+			bstack.addParent(all);
+			bstacks.push(bstack);
+			for( r in s.contents ) {
+				var b = pointerBlock.get(r);
+				if( b != null )
+					b.addParent(bstack);
+			}
+		}
+
+		for( f in falseCandidates )
+			if( f.f.owner == null ) {
+				f.f.addParent(f.b);
+				f.b.type.falsePositive++;
+				f.b.type.falsePositiveIndexes[f.idx]++;
+			}
+
+		// precompute Arrays (no NativeArray intermediate)
+		function shortCircuit( native, haxe ) {
+			var tnat = resolveType(native, false);
+			var tarr = resolveType(haxe, false);
+			if( tnat == null || tarr == null ) return;
+			for( b in blocks ) {
+				if( b.type == tnat && b.owner != null && b.owner.type == tarr && b.subs != null ) {
+					for( s in b.subs )
+						s.b.addParent(b.owner);
+				}
+			}
+		}
+		shortCircuit("hl.NativeArray","Array<T>");
+		// disable for now, this generates unknowns and "Void" links
+		//shortCircuit("hl_bytes_map","Map<String,Dynamic>");
+		//shortCircuit("hl_int_map","Map<Int,Dynamic>");
+		//shortCircuit("hl_obj_map","Map<{},Dynamic>");
+
+		// assign depths
+
+		Sys.println("Computing depths...");
+		broot.markDepth();
+		for( b in bstacks ) b.markDepth();
+
+		var changed = -1;
+		while( changed != 0 ) {
+			changed = 0;
+			for( b in blocks ) {
+				var minD = -1;
+				if( b.parents == null ) {
+					if( b.owner != null && b.owner.depth >= 0 )
+						minD = b.owner.depth;
+				} else {
+					for( p in b.parents )
+						if( p.depth >= 0 && (minD < 0 || p.depth < minD) )
+							minD = p.depth;
+				}
+				if( minD >= 0 ) {
+					minD++;
+					if( b.depth < 0 || b.depth > minD ) {
+						b.depth = minD;
+						changed++;
+					}
+				}
+			}
+		}
+
+		for( b in blocks )
+			b.finalize();
+
+		var unk = 0, unkMem = 0, unRef = 0;
+		for( b in blocks ) {
+			if( b.owner == null ) {
+				unRef++;
+				if( unRef < 100 )
+					log("  "+b.addr.toString()+"["+b.size+"] is not referenced");
+				continue;
+			}
+
+			if( b.type != null )
+				continue;
+
+			var o = b.owner;
+			while( o != null && o.type == null )
+				o = o.owner;
+			if( o != null )
+				switch( o.type.t ) {
+				case HAbstract(_):
+					b.type = o.type; // data inside this
+					b.typeKind = KAbstractData;
+					continue;
+				default:
+				}
+
+			b.type = tunknown;
+		}
+
+		var falseCount = 0;
+		for( t in types )
+			falseCount += t.falsePositive;
+
+		log("Hierarchy built, "+falseCount+" false positives, "+unRef+" unreferenced");
+	}
+
+	function printFalsePositives( ?typeStr : String ) {
+		var falses = [for( t in types ) if( t.falsePositive > 0 && (typeStr == null || t.toString().indexOf(typeStr) >= 0) ) t];
+		falses.sort(function(t1, t2) return t1.falsePositive - t2.falsePositive);
+		for( f in falses )
+			log(f.falsePositive+" count " + f + " "+f.falsePositiveIndexes+"\n    "+[for( f in f.memFields ) f.t.toString()]);
+	}
+
+	function printUnknown() {
+		var byT = new Map();
+		for( b in blocks ) {
+			if( b.type != null ) continue;
+
+			var o = b;
+			while( o != null && o.type == null )
+				o = o.owner;
+
+			var t = o == null ? null : o.type;
+			var tid = t == null ? -1 : t.tid;
+			var inf = byT.get(tid);
+			if( inf == null ) {
+				inf = { t : t, count : 0, mem : 0 };
+				byT.set(tid, inf);
+			}
+			inf.count++;
+			inf.mem += b.size;
+		}
+		var all = [for( k in byT ) k];
+		all.sort(function(i1, i2) return i1.count - i2.count);
+		for( a in all )
+			log("Unknown "+a.count + " count, " + MB(a.mem)+" "+(a.t == null ? "" : a.t.toString()));
+	}
+
+	function buildHierarchy() {
+		var progress = 0;
+		var blocks = toProcess;
+		toProcess = [];
+
+		for( b in blocks )
+			b.removeChildren();
+
+		for( b in blocks ) {
+			progress++;
+			if( displayProgress && progress % 10000 == 0 )
+				Sys.print((Std.int(progress * 1000.0 / blocks.length) / 10) + "%  \r");
+
+			if( !b.page.memHasPtr() )
+				continue;
+
+			if( b.type != null && !b.type.hasPtr )
+				log("  Scanning "+b.type+" "+b.addr.toString());
+
+			goto(b);
+			var fields = null;
+			var start = 0;
+			var ptrTags = null;
+			var hasFieldNames = false;
+			if( b.type != null ) {
+				hasFieldNames = b.type.memFieldsNames != null;
+				fields = b.type.memFields;
+				ptrTags = b.type.ptrTags;
+				// enum
+				if( b.type.constructs != null ) {
+					readPointer(); // type
+					var index = readInt();
+					fields = b.type.constructs[index];
+					if( is64 ) readInt(); // skip, not a pointer anyway
+					start += 2;
+				}
+			}
+
+			for( i in start...(b.size >> ptrBits) ) {
+				var r = readPointer();
+
+				var bs = pointerBlock.get(r);
+				if( bs == null ) continue;
+				var ft = fields != null ? fields[i] : null;
+
+				if( ptrTags != null && ((ptrTags.get(i >> 3) >>> (i & 7)) & 1) == 0 && !b.type.t.match(HVirtual(_)) )
+					continue;
+
+				if( b.type == tdynObj && (i == 1 || i == 2 || i == 3) ) {
+					if( bs.typeKind != KHeader && (bs.typeKind != null || bs.type != null) )
+						trace(bs.typeKind, bs.type);
+					else {
+						bs.type = tdynObjData;
+						bs.typeKind = KDynObjData;
+					}
+				}
+
+				if( ft != null && !ft.t.isPtr() ) {
+					falseCandidates.push({ b : b, f:bs, idx : i });
+					continue;
+				}
+				bs.addParent(b,hasFieldNames ? (i+1) : 0);
+
+				if( bs.type == null && ft != null ) {
+					if( ft.t.match(HDyn | HObj(_)) ) {
+						// we can't infer with a polymorph type
+						continue;
+					}
+					bs.type = ft;
+					bs.typeKind = KInferred(b.type, b.typeKind);
+					if( bs.subs != null )
+						toProcess.push(bs);
+				}
+			}
+		}
+	}
+
+	function printByType() {
+		var ctx = new Stats(this);
+		for( b in filteredBlocks )
+			ctx.add(b.type, b.size);
+		ctx.print();
+	}
+
+	function resolveType( str, showError = true ) {
+		var t = resolveCache.get(str);
+		if( t != null )
+			return t;
+		for( i in currentTypeIndex...types.length ) {
+			var t = types[i];
+			var tstr = t.toString();
+			if (tstr != null) {
+				resolveCache.set(tstr, t);
+				currentTypeIndex = i + 1;
+				if( tstr == str )
+					return t;
+			}
+		}
+		if( showError )
+			log("Type not found '"+str+"'");
+		return null;
+	}
+
+	public function getLocate(tstr : String, up = 0) {
+		var ctx = new Stats(this);
+
+		var lt = resolveType(tstr);
+		if( lt == null ) return ctx;
+
+		inline function isVirtualField(t) { t >>>= 24; return t == 1 || t == 2; }
+
+		for( b in filteredBlocks )
+			if( b.type != null && b.type.match(lt) ) {
+				var tl = [];
+				var owner = b.owner;
+				// skip first virtual field
+				if( lt.t != HDynObj && owner != null && owner.type != null && owner.type.t.match(HVirtual(_)) && isVirtualField(owner.makeTID(b,true)) )
+					owner = owner.owner;
+
+				if( owner != null ) {
+					tl.push(owner.makeTID(b,displayFields == Full));
+					var k : Int = up;
+					while( owner.owner != null && k-- > 0 && owner.owner != all ) {
+						var tag = owner.owner.makeTID(owner,displayFields != None);
+						owner = owner.owner;
+						// remove recursive sequence
+						for( i => tag2 in tl )
+							if( tag2 == tag ) {
+								var seq = true;
+								for( n in 0...i ) {
+									if( tl[n] != tl[i+1+n] )
+										seq = false;
+								}
+								if( seq ) {
+									for( k in 0...i ) tl.shift();
+									tag = -1;
+									k += i + 1;
+								}
+								break;
+							}
+						// don't display virtual wrappers
+						if( displayFields != None && owner.type != null && isVirtualField(tag) && owner.type.t.match(HVirtual(_)) ) {
+							tag = -1;
+							k++;
+						}
+						if( tag != -1 )
+							tl.unshift(tag);
+					}
+				}
+				ctx.addPath(tl, b.size);
+			}
+
+		return ctx;
+	}
+
+	function locate( tstr : String, up = 0 ) {
+		var ctx = getLocate(tstr, up);
+		ctx.print();
+	}
+
+	function count( tstr : String, excludes : Array<String> ) {
+		var t = resolveType(tstr);
+		if( t == null ) return;
+		var texclude = [];
+		for( e in excludes ) {
+			var t = resolveType(e);
+			if( t == null ) return;
+			texclude.push(t);
+		}
+		var ctx = new Stats(this);
+		Block.MARK_UID++;
+		var mark = [];
+		for( b in filteredBlocks )
+			if( b.type == t )
+				visitRec(b,ctx,[],mark);
+		while( mark.length > 0 ) {
+			var b = mark.pop();
+			for( s in b.subs )
+				visitRec(s.b,ctx,texclude,mark);
+		}
+		ctx.print(true);
+	}
+
+	function visitRec( b : Block, ctx : Stats, exclude : Array<TType>, mark : Array<Block> ) {
+		if( b.mark == Block.MARK_UID ) return;
+		b.mark = Block.MARK_UID;
+		if( b.type != null ) for( t in exclude ) if( b.type.match(t) ) return;
+		ctx.addPath(b.type == null ? [] : [b.type.tid],b.size);
+		if( b.subs != null )
+			mark.push(b);
+	}
+
+	function parents( tstr : String, up = 0 ) {
+		var lt = null;
+		for( t in types )
+			if( t.t.toString() == tstr ) {
+				lt = t;
+				break;
+			}
+		if( lt == null ) {
+			log("Type not found");
+			return;
+		}
+
+		var ctx = new Stats(this);
+		for( b in filteredBlocks )
+			if( b.type == lt )
+				for( b in b.getParents() )
+					ctx.addPath([if( b.type == null ) 0 else b.type.tid], 0);
+		ctx.print();
+	}
+
+	function subs( tstr : String, down = 0 ) {
+		var lt = null;
+		for( t in types )
+			if( t.t.toString() == tstr ) {
+				lt = t;
+				break;
+			}
+		if( lt == null ) {
+			log("Type not found");
+			return;
+		}
+
+		var ctx = new Stats(this);
+		var mark = new Map();
+		for( b in filteredBlocks )
+			if( b.type == lt ) {
+				function addRec(tl:Array<Int>,b:Block, k:Int) {
+					if( k < 0 ) return;
+					if( mark.exists(b) )
+						return;
+					mark.set(b, true);
+					tl.push(b.type == null ? 0 : b.type.tid);
+					ctx.addPath(tl, b.size);
+					if( b.subs != null ) {
+							k--;
+						for( s in b.subs )
+							addRec(tl.copy(),s.b, k);
+					}
+				}
+				addRec([], b, down);
+			}
+		ctx.print();
+	}
+
+	public function setFilterMode(m: FilterMode) {
+		filterMode = m;
+		switch( m ) {
+		case None:
+			filteredBlocks = blocks.copy();
+		default:
+			filteredBlocks = [];
+			var progress = 0;
+			for( b in blocks ) {
+				progress++;
+				if( displayProgress && progress % 1000 == 0 )
+					Sys.print((Std.int((progress / blocks.length) * 1000.0) / 10) + "%  \r");
+				if( !isBlockIgnored(b, m) )
+					filteredBlocks.push(b);
+			}
+			if( displayProgress )
+				Sys.print("       \r");
+		}
+	}
+	public function isBlockIgnored(b: Block, m: FilterMode) {
+		switch( m ) {
+		case None:
+			return false;
+		case Intersect:
+			for( m in otherMems ) {
+				if( m.pointerBlock.get(b.addr ) == null )
+					return true;
+			}
+		case Unique:
+			for( m in otherMems ) {
+				if( m.pointerBlock.get(b.addr ) != null )
+					return true;
+			}
+		}
+		return false;
+	}
+
+	public function log(msg:String) {
+		Sys.println(msg);
+	}
+
+	static function parseArgs(str: String) {
+		str = StringTools.trim(str);
+		var i = 0;
+		var tok = "";
+		var args = [];
+		var escape = false;
+		while(i != str.length) {
+			var c = str.charAt(i++);
+			if(c == '"') {
+				escape = !escape;
+			}
+			else {
+				if(c == " " && !escape) {
+					if(tok.length > 0) args.push(tok);
+					tok = "";
+				}
+				else
+					tok += c;
+			}
+		}
+		if(tok.length > 0) args.push(tok);
+		return args;
+	}
+
+	static var useColor = false;
+	static function main() {
+		var m = new Memory();
+		var others: Array<Memory> = [];
+		var filterMode: FilterMode = None;
+
+		//hl.Gc.dumpMemory(); Sys.command("cp memory.hl test.hl");
+
+		var code = null, memory = null;
+		var args = Sys.args();
+		while( args.length > 0 ) {
+			var arg = args.shift();
+			if( StringTools.endsWith(arg, ".hl") ) {
+				code = arg;
+				m.loadBytecode(arg);
+				continue;
+			}
+			if( arg == "-c" || arg == "--color" ) {
+				useColor = true;
+				continue;
+			}
+			if( arg == "--args" ) {
+				m.displayProgress = false;
+				break;
+			}
+			if (memory == null) {
+				memory = arg;
+				m.loadMemory(arg);
+			} else {
+				var m2 = new Memory();
+				m2.loadMemory(arg);
+				others.push(m2);
+			}
+		}
+		if( code != null && memory == null ) {
+			var dir = new haxe.io.Path(code).dir;
+			if( dir == null ) dir = ".";
+			memory = dir+"/hlmemory.dump";
+			if( sys.FileSystem.exists(memory) ) m.loadMemory(memory);
+		}
+
+		m.check();
+		for (m2 in others) {
+			m2.code = m.code;
+			m2.check();
+		}
+		m.otherMems = [for (i in others) i];
+		m.setFilterMode(filterMode);
+
+		var stdin = Sys.stdin();
+		while( true ) {
+			Sys.print(withColor("> ", 31));
+			var args = parseArgs(args.length > 0 ? args.shift() : stdin.readLine());
+			var cmd = args.shift();
+			switch( cmd ) {
+			case "exit", "quit", "q":
+				break;
+			case "types":
+				m.printByType();
+			case "stats":
+				m.printStats();
+			case "false":
+				m.printFalsePositives(args.shift());
+			case "unknown":
+				m.printUnknown();
+			case "locate":
+				m.locate(args.shift(), Std.parseInt(args.shift()));
+			case "count":
+				m.count(args.shift(), args);
+			case "parents":
+				m.parents(args.shift());
+			case "subs":
+				m.subs(args.shift(), Std.parseInt(args.shift()));
+			case "sort":
+				switch( args.shift() ) {
+				case "mem":
+					m.sortByCount = false;
+				case "count":
+					m.sortByCount = true;
+				case mode:
+					Sys.println("Unknown sort mode " + mode);
+				}
+			case "fields":
+				switch( args.shift() ) {
+				case "full":
+					m.displayFields = Full;
+				case "none":
+					m.displayFields = None;
+				case "parents":
+					m.displayFields = Parents;
+				case mode:
+					Sys.println("Unknown fields mode " + mode);
+				}
+			case "filter":
+				switch( args.shift() ) {
+				case "none":
+					filterMode = None;
+				case "intersect":
+					filterMode = Intersect;
+				case "unique":
+					filterMode = Unique;
+				case mode:
+					Sys.println("Unknown filter mode " + mode);
+				}
+				m.setFilterMode(filterMode);
+			case "nextDump":
+				others.push(m);
+				m = others.shift();
+				m.otherMems = [for (i in others) i];
+				m.setFilterMode(filterMode);
+				var ostr = others.length > 0 ? (" (others are " + others.map(m -> m.memFile) + ")") : "";
+				Sys.println("Using dump " + m.memFile + ostr);
+			case "lines":
+				var v = args.shift();
+				if( v != null )
+					m.maxLines = Std.parseInt(v);
+				Sys.println(m.maxLines == 0 ? "Lines limit disabled" : m.maxLines + " maximum lines displayed");
+			case null:
+				Sys.println("");
+			default:
+				Sys.println("Unknown command " + cmd);
+			}
+		}
+	}
+
+	// A list of ansi colors is available at
+	// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#8-16-colors
+	public static function withColor(str: String, ansiCol: Int) {
+		if (!useColor)
+			return str;
+		return "\x1B[" + ansiCol + "m" + str + "\x1B[0m";
+	}
+}

+ 188 - 0
hide/tools/memory/TType.hx

@@ -0,0 +1,188 @@
+package hide.tools.memory;
+
+import format.hl.Data;
+using format.hl.Tools;
+
+class TType {
+	public var tid : Int;
+	public var t : HLType;
+	public var closure : HLType;
+	public var bmp : haxe.io.Bytes;
+	public var hasPtr : Bool;
+	public var isDyn : Bool;
+
+	public var memFields : Array<TType>;
+	public var memFieldsNames : Array<String>;
+
+	public var constructs : Array<Array<TType>>;
+	public var nullWrap : TType;
+	public var ptrTags : haxe.io.Bytes;
+	public var parentClass : TType;
+
+	public var falsePositive = 0;
+	public var falsePositiveIndexes = [];
+
+	public function new(tid, t, ?cl) {
+		this.tid = tid;
+		this.t = t;
+		this.closure = cl;
+		isDyn = t.isDynamic();
+		switch( t ) {
+		case HFun(_):
+			hasPtr = cl != null && cl.isPtr();
+		default:
+			hasPtr = t.containsPointer();
+		}
+	}
+
+	public function match( t : TType ) {
+		if( t == this ) return true;
+		if( parentClass != null ) return parentClass.match(t);
+		return false;
+	}
+
+	function tagPtr( pos : Int ) {
+		var p = pos >> 3;
+		if( ptrTags == null || ptrTags.length <= p ) {
+			var nc = haxe.io.Bytes.alloc(p + 1);
+			if( ptrTags != null ) nc.blit(0, ptrTags, 0, ptrTags.length);
+			ptrTags = nc;
+		}
+		ptrTags.set(p, ptrTags.get(p) | (1 << (pos & 7)));
+	}
+
+	function appendField( m : Memory, pos : Int, name : String, t : HLType ) {
+		switch( t ) {
+		case HPacked({ v : HStruct(p) }):
+			var all = [p];
+			while( p.tsuper != null ) {
+				switch( p.tsuper ) {
+				case HStruct(p2):
+					p = p2;
+					all.unshift(p2);
+				default:
+					throw "assert";
+				}
+			}
+			for( p in all )
+				for( f in p.fields )
+					pos = appendField(m, pos, name+"."+f.name, f.t);
+		case HPacked(_):
+			throw "assert";
+		default:
+			var size = m.typeSize(t);
+			pos = align(pos, size);
+			memFields[pos >> m.ptrBits] = m.getType(t);
+			memFieldsNames[pos >> m.ptrBits] = name;
+			if( t.isPtr() ) tagPtr(pos >> m.ptrBits);
+			pos += size;
+		}
+		return pos;
+	}
+
+	public function buildTypes( m : Memory, tvoid : TType ) {
+		if( !hasPtr ) return;
+
+		// layout of data inside memory
+		inline function fill(fields:Array<TType>, pos:Int) {
+			if( m.is64 ) {
+				for( i in 0...pos >> m.ptrBits )
+					if( fields[i] == null )
+						fields[i] = tvoid;
+			} else {
+				// fill two slots for 64bit data
+				var i = pos >> m.ptrBits;
+				while( --i >= 0 )
+					if( fields[i] == null )
+						fields[i] = (fields[i-1] == null || fields[i-1].t != HF64) ? tvoid : fields[i-1];
+			}
+		}
+
+		switch( t ) {
+		case HObj(p), HStruct(p):
+			var protos = [p];
+			while( p.tsuper != null )
+				switch( p.tsuper ) {
+				case HObj(p2), HStruct(p2):
+					protos.unshift(p2);
+					p = p2;
+				default:
+				}
+
+			memFields = [];
+			memFieldsNames = [];
+
+			var pos = t.match(HStruct(_)) ? 0 : 1 << m.ptrBits; // type
+			for( p in protos )
+				for( f in p.fields )
+					pos = appendField(m, pos, f.name, f.t);
+			fill(memFields, pos);
+		case HEnum(e):
+			constructs = [];
+			for( c in e.constructs ) {
+				var pos = (1<<m.ptrBits) + 4; // type + index
+				var fields = [];
+				for( t in c.params ) {
+					var size = m.typeSize(t);
+					pos = align(pos, size);
+					fields[pos>>m.ptrBits] = m.getType(t);
+					if( t.isPtr() ) tagPtr(pos >> m.ptrBits);
+					pos += size;
+				}
+				fill(fields, pos);
+				constructs.push(fields);
+			}
+		case HVirtual(fl):
+			memFields = [
+				tvoid, // type
+				m.getType(HDyn), // obj
+				m.getType(HDyn), // next
+			];
+			memFieldsNames = [];
+			var pos = (fl.length + 3) << m.ptrBits;
+			tagPtr(1);
+			tagPtr(2);
+			for( f in fl ) {
+				var size = m.typeSize(f.t);
+				pos = align(pos, size);
+				memFields[pos >> m.ptrBits] = m.getType(f.t);
+				memFieldsNames[pos >> m.ptrBits] = f.name;
+				if( f.t.isPtr() ) tagPtr(pos >> m.ptrBits);
+				pos += size;
+			}
+
+			fill(memFields, pos);
+			// keep null our fields pointers since they might point to a DynObj data head
+			for( i in 0...fl.length )
+				memFields[i+3] = null;
+		case HNull(t):
+			if( m.is64 )
+				memFields = [tvoid, m.getType(t)];
+			else
+				memFields = [tvoid, tvoid, m.getType(t), tvoid];
+		case HFun(_):
+			memFields = [tvoid, tvoid, tvoid, m.getType(closure)];
+		default:
+		}
+	}
+
+	inline function align(pos, size) {
+		var d = pos & (size - 1);
+		if( d != 0 ) pos += size - d;
+		return pos;
+	}
+
+	public function toString() {
+		switch( t ) {
+		case HAbstract("roots"):
+			return Memory.withColor("roots", 32);
+		case HAbstract(p):
+			return p;
+		case HFun(_), HMethod(_):
+			return 'Function(${t.toString()})';
+		default:
+			return t.toString();
+		}
+	}
+
+}

+ 476 - 0
hide/view/Profiler.hx

@@ -0,0 +1,476 @@
+package hide.view;
+
+typedef LineData = {
+	count : Int,
+	size : Int,
+	tid : Array<Int>,
+	name : String,
+}
+
+typedef Path = {
+	v: Int,
+	children: Array<Path>,
+	line: LineData,
+	total : {count: Int, mem: Int},
+};
+
+enum DumpViewerPage {
+	None;
+	Stats;
+	Dump;
+}
+
+enum SortType {
+	ByMemory;
+	ByCount;
+}
+
+enum Filter {
+	None;
+	Unique;			// Blocks only present in current memory
+	Difference;		// Blocks only present in other memory
+	Intersected; 	// Blocks present in both memories
+}
+
+class Profiler extends hide.ui.View<{}> {
+
+	var tabContents : Array<Element>;
+	var editor : hide.comp.cdb.Editor;
+	var currentSheet : String;
+	var tabCache : String;
+	var tabs : hide.comp.Tabs;
+	var view : cdb.DiffFile.ConfigView;
+
+
+	public var mainMemory : hide.tools.memory.Memory = null;
+	public var currentMemory : hide.tools.memory.Memory = null;
+	public var names(default, null) : Array<String> = [];
+
+	public var lines(default, null) : Array<LineData> = [];
+	public var locationData(default, null) : Map<String, Array<LineData>> = [];
+
+	var hlPath = "";//"C:/Projects/wartales/trunk/wartales.hl";
+	var dumpPath = "";//"C:/Projects/wartales/trunk/capture.dump";
+
+	var error : String = "";
+
+	var sort : SortType = ByCount;
+	var sortOrderAscending = true;
+	var currentFilter : Filter = None;
+	var stats : Array<String> = [];
+	var statsObj : Dynamic;
+
+	public function new( ?state ) {
+		super(state);
+	}
+
+	override function onDisplay() {
+		new Element('
+		<div class="profiler">
+			<div class="left-panel">
+				<div class="tree-map"></div>
+				<div class="hierarchy"></div>
+			</div>
+			<div class="right-panel">
+				<div class="title">Files input</div>
+					<div class="files-input">
+						<div class="drop-zone hidden">
+							<p class="icon">+</p>
+							<p class="label">Drop .hl and .dump files here</p>
+						</div>
+						<div class="inputs">
+							<dl>
+								<dt>HL file</dt><dd><input class="hl-fileselect" type="fileselect" extensions="hl"/></dd>
+								<dt>Dump file</dt><dd><input class="dump-fileselect" type="fileselect" extension="dump"/></dd>
+							</dl>
+						</div>
+						<input type="button" value="Process Files" id="process-btn"/>
+					</div>
+				</div>
+			</div>
+		</div>'
+		).appendTo(element);
+
+		var hlSelect = new hide.comp.FileSelect(["hl"], null, element.find(".hl-fileselect"));
+		hlSelect.onChange = function() { hlPath = Ide.inst.getPath(hlSelect.path); };
+
+		var dumpSelect = new hide.comp.FileSelect(["dump"], null, element.find(".dump-fileselect"));
+		dumpSelect.onChange = function() { dumpPath = Ide.inst.getPath(dumpSelect.path); };
+
+		var dropZone = element.find(".drop-zone");
+		dropZone.css({display:'none'});
+
+		var inputs = element.find(".inputs");
+		inputs.css({display:'block'});
+
+		var isDragging = false;
+		var wait = false;
+		var fileInput = element.find(".files-input");
+		fileInput.on('dragenter', function(e) {
+			var dt : js.html.DataTransfer = e.originalEvent.dataTransfer;
+			if (!wait && !isDragging && dt.files != null && dt.files.length > 0) {
+				dropZone.css({display:'block'});
+				inputs.css({display:'none'});
+				dropZone.css({animation:'zoomIn .25s'});
+				isDragging = true;
+				wait = true;
+				haxe.Timer.delay(function() wait = false, 500);
+			}
+		});
+
+		fileInput.on('drop', function(e) {
+			var dt : js.html.DataTransfer = e.originalEvent.dataTransfer;
+			if (dt.files != null && dt.files.length > 0) {
+				dropZone.css({display:'none'});
+				inputs.css({display:'block'});
+				isDragging = false;
+
+				for (f in dt.files) {
+					var arrSplit = Reflect.getProperty(f, "name").split('.');
+					var ext = arrSplit[arrSplit.length - 1];
+					var p = Reflect.getProperty(f, "path");
+					p = StringTools.replace(p, "\\", "/");
+
+					if (ext == "hl") {
+						hlPath = p;
+						hlSelect.path = p;
+						continue;
+					}
+
+					if (ext == "dump") {
+						dumpPath = p;
+						dumpSelect.path = p;
+						continue;
+					}
+
+					Ide.inst.error('File ${p} is not supported, please provide .dump file or .hl file');
+				}
+			}
+		});
+
+		fileInput.on('dragleave', function(e) {
+			if (!wait && isDragging) {
+				dropZone.css({display:'none'});
+				inputs.css({display:'block'});
+				isDragging = false;
+				wait = true;
+				haxe.Timer.delay(function() wait = false, 500);
+			}
+		});
+
+		var processBtn = element.find("#process-btn");
+		processBtn.on('click', function() {
+			if (hlPath == null || hlPath == '' || dumpPath == null || dumpPath == '') {
+				Ide.inst.quickMessage('.hl or/and .dump files are missing. Please provide both files before hit the process button');
+				return;
+			}
+
+			clear();
+			load();
+			refresh();
+		});
+
+		var hierarchyPanel = new hide.comp.ResizablePanel(Vertical, element.find(".hierarchy"));
+		hierarchyPanel.saveDisplayKey = "hierarchyPanel";
+	}
+
+	override function getTitle() {
+		return "Memory profiler";
+	}
+
+	function load() {
+		names = [ dumpPath ];
+
+		var result = loadAll();
+		if ( result != null) {
+			error = result;
+		} else {
+			error = "";
+			if (names.length > 0) {
+				filter(None);
+				displayTypes(sort, sortOrderAscending);
+				stats = mainMemory?.getStats();
+				statsObj = mainMemory?.getStatsObj();
+			}
+		}
+	}
+
+	function loadAll() @:privateAccess{
+		if (names.length < 1) return null;
+		for (i in 0...names.length) {
+			var newMem = new hide.tools.memory.Memory();
+			try {
+				if (i == 0) { // setup main Memory
+					newMem.loadBytecode(hlPath);
+					currentMemory = mainMemory = newMem;
+				} else {
+					mainMemory.otherMems.push(newMem);
+					newMem.code = mainMemory.code;
+				}
+
+				newMem.otherMems = [];
+				newMem.loadMemory(names[i]);
+				newMem.check();
+			} catch(e) {
+				names.remove(names[i]);
+				if (names.length < 1)
+					mainMemory = currentMemory = null;
+
+				return e.toString();
+			}
+		}
+
+		mainMemory.setFilterMode(None);
+		for (mem in mainMemory.otherMems)
+			mem.setFilterMode(None);
+
+		return null;
+	}
+
+	function clear() {
+		mainMemory = currentMemory = null;
+		lines = [];
+		locationData.clear();
+	}
+
+	function filter(f : Filter) {
+		switch (f) {
+			case None :
+				currentMemory = mainMemory;
+				mainMemory.setFilterMode(None);
+			/*case Unique :
+				currentMemory = mainMemory;
+				mainMemory.setFilterMode(Unique);
+			case Difference :
+				mainMemory.setFilterMode(None);
+				if (mainMemory.otherMems.length > 0){
+					var other = mainMemory.otherMems[0];
+					other.otherMems = [mainMemory];
+					other.setFilterMode(Unique);
+					other.otherMems = [];
+					currentMemory = other;
+				}
+			case Intersected :
+				var other = mainMemory.otherMems[0];
+				other.setFilterMode(None);
+				currentMemory = mainMemory;
+				mainMemory.setFilterMode(Intersect);*/
+			default:
+				currentMemory = mainMemory;
+				mainMemory.setFilterMode(None);
+		}
+
+		locationData.clear();
+	}
+
+	public function displayTypes(sort : SortType = ByCount, asc : Bool = true) @:privateAccess{
+		if (currentMemory == null) throw "memory not loaded";
+
+		lines = [];
+
+		var ctx = new hide.tools.memory.Memory.Stats(currentMemory);
+		for ( b in currentMemory.filteredBlocks)
+			ctx.add(b.type, b.size);
+
+		ctx.sort(sort == ByCount, asc);
+
+		for (i in ctx.allT){
+			lines.push({count : i.count, size : i.mem, tid : i.tl, name : getNameString(i.tl)});
+		}
+	}
+
+	public function getNameString(tid : Array<Int>) {
+		var path = hide.tools.memory.Memory.Stats.getPathStrings(mainMemory, tid);
+		return path[path.length-1];
+	}
+
+	public function getPathString(tid : Array<Int>) {
+		return hide.tools.memory.Memory.Stats.getPathStrings(currentMemory, tid).join(" > ");
+	}
+
+	public function refresh() {
+		// Update memory statistics on the right panel
+		element.find('.stats').remove();
+
+		new Element ('
+		<div class="stats">
+			<div class="title">Stats</div>
+			<h4>Memory usage on device</h4>
+			<div class="outer-gauge"><div class="inner-gauge" title="${hide.tools.memory.Memory.MB(statsObj?.used)} used (${ 100 * statsObj?.used / statsObj?.totalAllocated}% of total)" style="width:${ 100 * statsObj?.used / statsObj?.totalAllocated}%;"></div></div>
+			<dl>
+				<dt>Allocated</dt><dd>${hide.tools.memory.Memory.MB(statsObj?.totalAllocated)}</dd>
+				<dt>Used</dt><dd>${hide.tools.memory.Memory.MB(statsObj?.used)}</dd>
+				<dt>Free</dt><dd>${hide.tools.memory.Memory.MB(statsObj?.free)}</dd>
+				<dt>GC</dt><dd>${hide.tools.memory.Memory.MB(statsObj?.gc)}</dd>
+				<dt>&nbsp</dt><dd></dd>
+				<dt>Pages</dt><dd>${statsObj?.pagesCount} (${hide.tools.memory.Memory.MB(statsObj?.pagesSize)})</dd>
+				<dt>Roots</dt><dd>${statsObj?.rootsCount}</dd>
+				<dt>Stacks</dt><dd>${statsObj?.stackCount}</dd>
+				<dt>Types</dt><dd>${statsObj?.typesCount}</dd>
+				<dt>Closures</dt><dd>${statsObj?.closuresCount}</dd>
+				<dt>Live blocks</dt><dd>${statsObj?.blockCount}</dd>
+			</dl>
+		</div>
+		').appendTo(element.find('.right-panel'));
+
+		// Update memory hierarchical view
+		element.find('table').parent().remove();
+		var tab = new Element('
+		<div class="hide-scroll">
+			<table rules=none>
+				<thead>
+					<td>Count</td>
+					<td>Size</td>
+					<td>Name</td>
+					<td>% Impact</td>
+				</thead>
+				<tbody>
+				</tbody>
+			</table>
+		</div>'
+		).appendTo(element.find(".hierarchy"));
+
+		var body = tab.find('tbody');
+		for (l in lines)
+			new ProfilerElement(this, l, null, null).element.appendTo(body);
+	}
+
+	public function locate(str : String) @:privateAccess {
+		var datas = [];
+		if (str == "null" || locationData.exists(str)) return;
+
+		var ctx = currentMemory.getLocate(str, 30);
+		ctx.sort();
+		for (i in ctx.allT)
+			datas.push({count : i.count, size : i.mem, tid : i.tl, name : null, state: Unique});
+
+		locationData.set(str, datas);
+	}
+
+	public function getChildren(depth : Int, parent : Int, valid : Array<LineData>) : Array<Path> {
+		var valid = valid.filter(p ->  {
+			var isCurrentPath = depth <= 0 || p.tid[depth-1] == parent;
+			return p.tid.length > depth && isCurrentPath;
+		});
+
+		var children : Array<Dynamic> = [];
+		for (path in valid) {
+			if (parent == -1 || path.tid[depth - 1] == parent) {
+				var copy = children.filter((c) -> c.p == path.tid[depth]);
+				if (copy.length == 0) {
+					children.push({p : path.tid[depth],
+						count : path.count, size : path.size,
+						line : depth == path.tid.length - 1 ? path : null});
+				} else {
+					copy[0].count += path.count;
+					copy[0].size += path.size;
+				}
+			}
+
+		}
+
+		children.sort((a, b) -> b.count - a.count);
+		return children.map(c -> {v : c.p, children : getChildren(depth+1, c.p, valid), line : c.line, total : {count : c.count, mem : c.size}});
+	}
+
+	static var _ = hide.ui.View.register(Profiler);
+
+}
+
+class ProfilerElement extends hide.comp.Component{
+	public var profiler : Profiler;
+	public var line : LineData;
+	public var path : Path;
+	public var parent : ProfilerElement;
+	public var depth : Int = 0;
+	public var isOpen = false;
+
+	// Cached values
+	var foldBtn : Element;
+	var children : Array<ProfilerElement> = null;
+
+	public function new(profiler : Profiler, line: LineData, path : Path, parent : ProfilerElement = null) @:privateAccess {
+        super(null, null);
+
+		this.profiler = profiler;
+		this.line = line;
+		this.path = path;
+		this.parent = parent;
+		this.depth = parent != null ? parent.depth + 1 : 0;
+
+		var name = path == null ? line.name : hide.tools.memory.Memory.Stats.getTypeString(profiler.currentMemory, path.v);
+		var count = path == null ? line.count : path.total.count;
+		var mem = path == null ? line.size : path.total.mem;
+
+		this.element = new Element('<tr><td><div class="folder icon ico ico-caret-right"></div>${count}</td><td>${hide.tools.memory.Memory.MB(mem)}</td><td title="${name}">${name}</td><td><div title="Allocated ${mem} (${100 * mem / Reflect.getProperty(profiler.statsObj, "totalAllocated")}% of total)" class="outer-gauge"><div class="inner-gauge" style="width:${100 * mem / Reflect.getProperty(profiler.statsObj, "totalAllocated")}%;"></div></div></td></tr>');
+		this.element.find('td').first().css({'padding-left':'${10 * depth}px'});
+
+		this.foldBtn = this.element.find('.folder');
+
+		// Build children profiler
+		if (this.path != null) {
+			for (p in this.path.children??[]) {
+				if (children == null)
+					children = [];
+
+				var pe = new ProfilerElement(this.profiler, null, p, this);
+
+				if (pe.children == null) {
+					pe.foldBtn.css({ opacity : 0 });
+					pe.foldBtn.off();
+				}
+
+				children.push(pe);
+			}
+		}
+
+		// Manage line folding / unfolding to show / unshow details
+		foldBtn.on('click', function(e) {
+			if (!isOpen) {
+				this.open();
+			}
+			else {
+				this.close();
+			}
+		});
+    }
+
+	public function open() {
+		this.isOpen = true;
+		this.foldBtn.removeClass('ico-caret-right').addClass('ico-caret-down');
+
+		// Only root nodes without children will generate their children
+		if (children == null && parent == null) {
+			var d = profiler.locationData.get(line.name);
+			if (d == null) {
+				profiler.locate(line.name);
+				d = profiler.locationData.get(line.name);
+			}
+
+			var pathData = profiler.getChildren(depth, path != null ? path.v : -1, d);
+
+			if (this.children == null)
+				this.children = [];
+
+			for (p in pathData) {
+				var pe = new ProfilerElement(this.profiler, null, p, this);
+				this.children.push(pe);
+				pe.element.insertAfter(this.element);
+			}
+		}
+
+		for (c in children??[]) {
+			c.element.insertAfter(this.element);
+		}
+	}
+
+	public function close() {
+		this.isOpen = false;
+		this.foldBtn.removeClass('ico-caret-down').addClass('ico-caret-right');
+
+		for (c in children??[]) {
+			c.close();
+			c.element.detach();
+		}
+	}
+}