Dev/Mod/HeapSpy.odc

MODULE DevHeapSpy;
(**
	project	= "BlackBox"
	organization	= "www.oberon.ch"
	contributors	= "Oberon microsystems"
	version	= "System/Rsrc/About"
	copyright	= "System/Rsrc/About"
	license	= "Docu/BB-License"
	changes	= "
	- YYYYMMDD, nn, ...
	"
	issues	= "
	- ...
	"

**)

	IMPORT
		S := SYSTEM, Kernel, Strings,
		Ports, Models, Views, Services, Stores, Properties, Dialog, Controllers, Documents, DevDebug;

	CONST
		resolution = 8; (* one pixel represents resolution bytes *)
		lineW = 512; (* one line is 512 pixels long, i.e. represents 512 * resolution bytes *)
		lineH = 5; (* one line is lineH pixels high *)
		lineVertM = 3; (* margin between two lines *)
		lineHorM = 2; (* margin between vertical cluster edge and lines *)
		cluW = lineW + 2*lineHorM;
		cluHorM = 4; (* cluHorM pixels between left view edge and vertical cluster edge *)
		cluTopM = 8; (* cluTopM pixels between clusters *)
		
		W = (2*cluHorM + cluW) * Ports.point;

	TYPE
		Block = POINTER TO RECORD [untagged]
			tag: Kernel.Type;
			size: INTEGER;		(* size of free blocks *)
			actual: INTEGER;
			first: INTEGER
		END;
		Cluster = POINTER TO RECORD [untagged]
			size: INTEGER;	(* total size *)
			next: Cluster;
		END;

		View = POINTER TO RECORD (Views.View)
			height: INTEGER
		END;
		
		Model = POINTER TO RECORD (Models.Model)
			alloc: INTEGER
		END;
		
		Action = POINTER TO RECORD (Services.Action) END;

		Msg = RECORD (Models.Message) END;
		
	VAR
		mem: Model;
		par-: RECORD
			allocated-: INTEGER; (* number of bytes currently allocated *)
			clusters-: INTEGER; (* number of clusters currently allocated *)
			heapsize-: INTEGER (* total bytes currently allocated for heap *)
		END;

	PROCEDURE Append (VAR s: ARRAY OF CHAR; t: ARRAY OF CHAR);
		VAR len, i, j: INTEGER; ch: CHAR;
	BEGIN
		len := LEN(s);
		i := 0; WHILE s[i] # 0X DO INC(i) END;
		j := 0; REPEAT ch := t[j]; s[i] := ch; INC(j); INC(i) UNTIL (ch = 0X) OR (i = len);
		s[len - 1] := 0X
	END Append;

	PROCEDURE SAppend (VAR s: ARRAY OF CHAR; t: ARRAY OF SHORTCHAR);
		VAR str: ARRAY 256 OF CHAR;
	BEGIN
		str := t$; Append(s, str)
	END SAppend;

	PROCEDURE SizeOf (f: SHORTCHAR; t: Kernel.Type): INTEGER;
		VAR x: INTEGER;
	BEGIN
		CASE f OF
		| 0BX: RETURN 0
		| 1X, 2X, 4X: RETURN 1
		| 3X, 5X: RETURN 2
		| 8X, 0AX: RETURN 8
		| 11X: RETURN t.size
		| 12X:
			x := S.VAL(INTEGER, t.base[0]);
			IF x DIV 256 # 0 THEN x := t.base[0].id MOD 4 + 16 END;
			RETURN t.size * SizeOf(SHORT(CHR(x)), t.base[0])
		ELSE RETURN 4
		END
	END SizeOf;

	PROCEDURE FormOf (t: Kernel.Type): SHORTCHAR;
	BEGIN
		IF S.VAL(INTEGER, t) DIV 256 = 0 THEN
			RETURN SHORT(CHR(S.VAL(INTEGER, t)))
		ELSE
			RETURN SHORT(CHR(16 + t.id MOD 4))
		END
	END FormOf;
		
	PROCEDURE WriteName (t: Kernel.Type; VAR s: ARRAY OF CHAR);
		VAR name: Kernel.Name; f: SHORTCHAR;
	BEGIN
		f := FormOf(t);
		CASE f OF
		| 0X: Dialog.MapString("#Dev:Undefined", s)
		| 1X: s := "BOOLEAN"
		| 2X: s := "SHORTCHAR"
		| 3X: s := "CHAR"
		| 4X: s := "BYTE"
		| 5X: s := "SHORTINT"
		| 6X: s := "INTEGER"
		| 7X: s := "SHORTREAL"
		| 8X: s := "REAL"
		| 9X: s := "SET"
		| 0AX: s := "LONGINT"
		| 0BX: s := "ANYREC"
		| 0CX: s := "ANYPTR"
		| 0DX: s := "POINTER"
		| 0EX: s := "PROCEDURE"
		| 0FX: s := "STRING"
		| 10X, 11X, 13X:
			IF (t.id DIV 256 # 0) & (t.mod.refcnt >= 0) THEN
				s := t.mod.name$; Append(s, ".");
				Kernel.GetTypeName(t, name); SAppend(s, name)
			ELSIF f = 11X THEN
				s := t.mod.name$; Append(s, ".RECORD"); 
			ELSIF f = 13X THEN
				s := "POINTER"
			ELSE
				s := "PROCEDURE"
			END
		| 20X: s := "COM.IUnknown"
		| 21X: s := "COM.GUID"
		| 22X: s := "COM.RESULT"
		ELSE Dialog.MapString("#Dev:UnknownFormat", s)
		END
	END WriteName;
		
	PROCEDURE Next (b: Block): Block;	(* next block in same cluster *)
		VAR size: INTEGER; tag: Kernel.Type;
	BEGIN
		tag := S.VAL(Kernel.Type, S.VAL(INTEGER, b.tag) DIV 4 * 4);
		size := tag.size + 4;
		IF ODD(S.VAL(INTEGER, b.tag) DIV 2) THEN size := b.size - S.ADR(b.size) + size END;
		size := (size + 15) DIV 16 * 16;
		RETURN S.VAL(Block, S.VAL(INTEGER, b) + size)
	END Next;

	PROCEDURE ClusterHeight (size: INTEGER): INTEGER;
		(* height of a cluster in pixels *)
		VAR noflines: INTEGER;
	BEGIN
		noflines := ((size DIV resolution) + (lineW-1)) DIV lineW;
		RETURN noflines * (lineH + lineVertM) + lineVertM;
	END ClusterHeight;

	PROCEDURE ViewHeight(): INTEGER;
		(* height of view in universal units *)
		VAR cluster: Cluster; h: INTEGER;
	BEGIN
		cluster := S.VAL(Cluster, Kernel.Root()); h := 0;
		WHILE cluster # NIL DO
			INC(h, (cluTopM + ClusterHeight(cluster.size)) * Ports.point);
			cluster := cluster.next;
		END;
		INC(h, cluTopM * Ports.point);
		RETURN h;
	END ViewHeight;

	PROCEDURE PaintMem (f: Views.Frame; top, bottom: INTEGER);
		VAR cluster: Cluster; b0, blk, next: Block; end: INTEGER;
			clusterTop, clusterH, x, x1, y: INTEGER; (* units *)
			dot, painted, runlen: INTEGER;
			c, c0: INTEGER;
	BEGIN
		dot := (Ports.point DIV f.dot) * f.dot;
		IF dot = 0 THEN dot := f.dot END;
		clusterTop := 0;
		cluster := S.VAL(Cluster, Kernel.Root());
		WHILE cluster # NIL DO
			INC(clusterTop, cluTopM*dot);
			clusterH := ClusterHeight(cluster.size) * dot;
			IF (clusterTop - cluTopM < bottom) OR (clusterTop + clusterH > top) THEN
				f.DrawRect(cluHorM*dot, clusterTop, (cluHorM+cluW)*dot, clusterTop + clusterH,
							Ports.fill, Ports.grey25);
				(* scan cluster and draw blocks *)
				blk := S.VAL(Block, S.VAL(INTEGER, cluster) + 12);
				end := S.VAL(INTEGER, blk) + (cluster.size - 12) DIV 16 * 16;
				b0 := blk; c0 := -1;
				y := clusterTop + lineVertM*dot;
				painted := 16 DIV resolution; (* nof pixels already painted on current line *)
				x := (cluHorM+lineHorM+painted)*dot;
				WHILE S.VAL(INTEGER, blk) < end DO
					IF S.VAL(INTEGER, blk.tag) = S.ADR(blk.size) THEN (* free block *)
						c := -1; next := S.VAL(Block, S.VAL(INTEGER, blk) + blk.size + 4)
					ELSIF 1 IN S.VAL(SET, blk.tag) THEN (* array *)
						c := Ports.blue; next := Next(blk)
					ELSE (* record *)
						c := Ports.red; next := S.VAL(Block, S.VAL(INTEGER, blk) + (blk.tag.size + 19) DIV 16 * 16)
					END;
					IF c # c0 THEN
						runlen := (S.VAL(INTEGER, blk) - S.VAL(INTEGER, b0)) DIV resolution;
						WHILE runlen > lineW-painted DO
							x1 := (cluHorM+lineHorM+lineW)*dot;
							IF (c0 # -1) & (x < x1) THEN
								f.DrawRect(x, y, x1, y + lineH * dot, Ports.fill, c0)
							END;
							DEC(runlen, lineW-painted); painted := 0;
							x := (cluHorM+lineHorM)*dot; INC(y, (lineH+lineVertM) * dot);
						END;
						IF runlen > 0 THEN
							IF c0 # -1 THEN f.DrawRect(x, y, x + runlen*dot, y + lineH * dot, Ports.fill, c0) END;
							INC(painted, runlen); INC(x, runlen*dot);
						END;
						b0 := blk; c0 := c
					END;
					blk := next
				END;
				IF c0 # -1 THEN
					runlen := (S.VAL(INTEGER, end) - S.VAL(INTEGER, b0)) DIV resolution;
					WHILE runlen > lineW-painted DO
						f.DrawRect(x, y, (cluHorM+lineHorM+lineW)*dot, y + lineH * dot, Ports.fill, c0);
						DEC(runlen, lineW-painted); painted := 0;
						x := (cluHorM+lineHorM)*dot; INC(y, (lineH+lineVertM) * dot);
					END;
					IF runlen > 0 THEN f.DrawRect(x, y, x + runlen*dot, y + lineH * dot, Ports.fill, c0) END;
				END
			END;
			cluster := cluster.next;
			INC(clusterTop, clusterH);
		END
	END PaintMem;

	PROCEDURE MarkBlock (f: Views.Frame; sel: Block; on: BOOLEAN);
		VAR cluster: Cluster; next: Block; end, offs, dot, col: INTEGER; found: BOOLEAN;
			clusterTop, x, y, r, e: INTEGER; (* units *)
	BEGIN
		dot := (Ports.point DIV f.dot) * f.dot;
		IF dot = 0 THEN dot := f.dot END;
		clusterTop := 0;
		cluster := S.VAL(Cluster, Kernel.Root()); found := FALSE;
		WHILE (cluster # NIL) & ~found DO
			end := S.VAL(INTEGER, cluster) + 12 + (cluster.size - 12) DIV 16 * 16;
			INC(clusterTop, cluTopM * dot);
			IF (S.VAL(INTEGER, cluster) <= S.VAL(INTEGER, sel)) & (S.VAL(INTEGER, sel) < end) THEN
				found := TRUE;
			ELSE
				INC(clusterTop, ClusterHeight(cluster.size) * dot);
				cluster := cluster.next;
			END
		END;
		IF found THEN (* sel is contained in cluster *)
			IF on THEN col := Ports.green
			ELSIF 1 IN S.VAL(SET, sel.tag) THEN col := Ports.blue
			ELSE col := Ports.red
			END;
			next := Next(sel);
			r := (cluHorM + lineHorM + lineW) * dot;
			offs := (S.VAL(INTEGER, sel) + 4 (* tag *) - S.VAL(INTEGER, cluster)) DIV resolution;
			y := clusterTop + ((offs DIV lineW) * (lineH+lineVertM) + lineVertM) * dot;
			x := (cluHorM + lineHorM + (offs MOD lineW)) * dot;
			e := x + (S.VAL(INTEGER, next) - S.VAL(INTEGER, sel)) DIV resolution * dot;
			WHILE e >= r DO
				f.DrawRect(x, y, r, y + lineH * dot, Ports.fill, col);
				INC(y, (lineH + lineVertM) * dot);
				x := (cluHorM + lineHorM) * dot;
				e := x + e - r;
			END;
			IF e > x THEN f.DrawRect(x, y, e, y + lineH * dot, Ports.fill, col) END;
		END
	END MarkBlock;

	PROCEDURE ThisCluster (f: Views.Frame; sx, sy: INTEGER): Cluster;
		VAR cluster: Cluster; dot, clusterTop, clusterH: INTEGER;
	BEGIN
		dot := (Ports.point DIV f.dot) * f.dot;
		IF dot = 0 THEN dot := f.dot END;
		cluster := NIL;
		IF (cluHorM * dot <= sx) & (sx < (cluHorM + 2*lineHorM + lineW) * dot) THEN
			cluster := S.VAL(Cluster, Kernel.Root());
			clusterTop := 0;
			WHILE cluster # NIL DO
				INC(clusterTop, cluTopM * dot);
				clusterH := ClusterHeight(cluster.size) * dot;
				IF (clusterTop <= sy) & (sy < clusterTop + clusterH) THEN RETURN cluster
				ELSE INC(clusterTop, clusterH); cluster := cluster.next;
				END
			END
		END;
		RETURN cluster;
	END ThisCluster;

	PROCEDURE ThisBlock (f: Views.Frame; sx, sy: INTEGER): Block;
		VAR cluster: Cluster; blk, next: Block; found: BOOLEAN;
			dot, lineno, offs, end, adr: INTEGER;
			clusterTop, clusterH: INTEGER;
	BEGIN
		dot := (Ports.point DIV f.dot) * f.dot;
		IF dot = 0 THEN dot := f.dot END;
		IF (sx < (cluHorM + lineHorM) * dot) OR ((cluHorM + lineHorM + lineW) * dot <= sx) THEN
			RETURN NIL
		END;
		cluster := S.VAL(Cluster, Kernel.Root());
		clusterTop :=0; found := FALSE;
		WHILE (cluster # NIL) & ~found DO
			INC(clusterTop, cluTopM * dot);
			clusterH := ClusterHeight(cluster.size) * dot;
			IF (clusterTop <= sy) & (sy < clusterTop + clusterH) THEN found := TRUE
			ELSE INC(clusterTop, clusterH); cluster := cluster.next;
			END
		END;
		IF found THEN (* (sx, sy) points into cluster *)
			lineno := ((sy - clusterTop) DIV dot) DIV (lineH + lineVertM);
			offs := ((sy - clusterTop) DIV dot) MOD (lineH + lineVertM); (* vertical offset in line *)
			IF (lineVertM <= offs) & (offs < lineH + lineVertM) THEN
				offs := (sx DIV dot) - cluHorM - lineHorM; (* hor offset from left into line *)
				adr := S.VAL(INTEGER, cluster) + (lineno * lineW + offs) * resolution;
				(* we're looking for the block that contains address adr *)
				blk := S.VAL(Block, S.VAL(INTEGER, cluster) + 12);
				end := S.VAL(INTEGER, blk) + (cluster.size - 12) DIV 16 * 16;
				WHILE S.VAL(INTEGER, blk) < end DO
					next := Next(blk);
					IF adr < S.VAL(INTEGER, next) THEN
						IF S.VAL(INTEGER, blk.tag) # S.ADR(blk.size) THEN RETURN blk
						ELSE RETURN NIL (* blk is a free block *)
						END
					ELSE blk := next
					END
				END;
				RETURN NIL
			ELSE (* (sx, sy) points between two lines *) RETURN NIL
			END
		ELSE RETURN NIL
		END
	END ThisBlock;

	PROCEDURE SearchPath (this, that: Block; VAR path: ARRAY OF INTEGER; VAR len: INTEGER);
		VAR father, son: Block; tag: Kernel.Type; flag, offset, actual, i: INTEGER; found: BOOLEAN;
	BEGIN
		i := 1; len := 0; found := FALSE;
		IF ~ODD(S.VAL(INTEGER, this.tag)) THEN
			father := NIL;
			LOOP
				INC(S.VAL(INTEGER, this.tag));
				flag := S.VAL(INTEGER, this.tag) MOD 4;
				tag := S.VAL(Kernel.Type, S.VAL(INTEGER, this.tag) - flag);
				IF flag >= 2 THEN actual := this.first; this.actual := actual
				ELSE actual := S.ADR(this.size)
				END;
				LOOP
					IF (this = that) & ~found THEN len := i; found := TRUE END;
					offset := tag.ptroffs[0];
					IF offset < 0 THEN
						INC(S.VAL(INTEGER, tag), offset + 4); (* restore tag *)
						IF (flag >= 2) & (actual < this.size) & (offset < -4) THEN (* next array element *)
							INC(actual, tag.size); this.actual := actual
						ELSE (* up *)
							this.tag := S.VAL(Kernel.Type, S.VAL(INTEGER, tag) + flag);
							IF father = NIL THEN RETURN END;
							son := this; this := father; DEC(i);
							flag := S.VAL(INTEGER, this.tag) MOD 4;
							tag := S.VAL(Kernel.Type, S.VAL(INTEGER, this.tag) - flag);
							offset := tag.ptroffs[0];
							IF flag >= 2 THEN actual := this.actual ELSE actual := S.ADR(this.size) END;
							S.GET(actual + offset, father); S.PUT(actual + offset, S.ADR(son.size));
							INC(S.VAL(INTEGER, tag), 4)
						END
					ELSE
						S.GET(actual + offset, son);
						IF (son # NIL) & ~found THEN
							DEC(S.VAL(INTEGER, son), 4);
							IF ~ODD(S.VAL(INTEGER, son.tag)) THEN (* down *)
								IF i < LEN(path) THEN
									IF flag < 2 THEN path[i] := offset
									ELSE path[i] := actual - S.ADR(this.size) + offset
									END
								END;
								INC(i);
								this.tag := S.VAL(Kernel.Type, S.VAL(INTEGER, tag) + flag);
								S.PUT(actual + offset, father); father := this; this := son;
								EXIT
							END
						END;
						INC(S.VAL(INTEGER, tag), 4)
					END
				END
			END
		END
	END SearchPath;

	PROCEDURE ResetMarks;
		VAR cluster: Cluster; blk: Block; end: INTEGER;
	BEGIN
		cluster := S.VAL(Cluster, Kernel.Root());
		WHILE cluster # NIL DO
			blk := S.VAL(Block, S.VAL(INTEGER, cluster) + 12);
			end := S.VAL(INTEGER, blk) + (cluster.size - 12) DIV 16 * 16;
			WHILE S.VAL(INTEGER, blk) < end DO EXCL(S.VAL(SET, blk.tag), 0); blk := Next(blk) END;
			cluster := cluster.next
		END
	END ResetMarks;

	PROCEDURE SearchAnchor (blk: Block; VAR path: ARRAY OF CHAR);
		VAR m, mod: Kernel.Module; ref, offs, i, j, k, p, x, a, n: INTEGER; offsets: ARRAY 1 OF INTEGER;
			 t, f: SHORTCHAR; desc: Kernel.Type; name, title: Kernel.Name; tag, typ: Kernel.Type;
	BEGIN
		mod := NIL; offs := 0;
		m := Kernel.ThisLoadedMod("HostWindows");
		IF m # NIL THEN
			ref := m.refs; Kernel.GetRefProc(ref, x, name);
			IF x # 0 THEN
				REPEAT
					Kernel.GetRefVar(ref, t, f, desc, a, name)
				UNTIL (t # 1X) OR (name = "winAnchor");
				IF t = 1X THEN
					S.GET(m.data + a, p);
					IF p # 0 THEN
						SearchPath(S.VAL(Block, p - 4), blk, offsets, n);
						IF n > 0 THEN offs := 1 END
					END
				END
			END
		END;
		m := Kernel.modList;
		WHILE (mod = NIL) & (offs = 0) & (m # NIL) DO
			IF m.refcnt >= 0 THEN
				i := 0;
				WHILE (mod = NIL) & (i < m.nofptrs) DO
					S.GET(m.data + m.ptrs[i], p);
					IF p # 0 THEN
						SearchPath(S.VAL(Block, p - 4), blk, offsets, n);
						IF n > 0 THEN mod := m; offs := m.ptrs[i] END
					END;
					INC(i)
				END
			END;
			m := m.next
		END;
		ResetMarks;
		IF offs # 0 THEN
			IF mod # NIL THEN
				path := mod.name$; Append(path, ".");
				ref := mod.refs; Kernel.GetRefProc(ref, x, name);
				IF x # 0 THEN
					REPEAT
						Kernel.GetRefVar(ref, t, f, desc, a, name)
					UNTIL (t # 1X) OR (offs >= a) & (offs < a + SizeOf(f, desc));
					IF t = 1X THEN SAppend(path, name)
					ELSE Append(path, "???")
					END
				END
			ELSE path := "window list"
			END;
			i := 1;
			WHILE (i < n) & (i < LEN(offsets)) DO
				S.GET(p - 4, tag);
				IF 1 IN S.VAL(SET, tag) THEN Append(path, "[]")
				ELSE
					k := 0;
					REPEAT
						typ := tag.base[k]; INC(k); j := 0;
						WHILE (j < typ.fields.num) & (typ.fields.obj[j].offs # offsets[i]) DO INC(j) END;
					UNTIL (j < typ.fields.num) OR (k > tag.id DIV 16 MOD 16);
					IF j < typ.fields.num THEN
						Kernel.GetObjName(typ.mod, S.ADR(typ.fields.obj[j]), name);
						Append(path, "."); SAppend(path, name)
					ELSE
						Append(path, ".?")
					END
				END;
				S.GET(p + offsets[i], p); INC(i)
			END
		ELSE path := ""
		END
	END SearchAnchor;

	PROCEDURE ShowBlock (blk: Block);
		VAR  title: ARRAY 1024 OF CHAR; path: ARRAY 1024 OF CHAR;
	BEGIN
		SearchAnchor(blk, path);
		IF path # "" THEN
			title := "Object anchored in ";
			Append(title, path);
		ELSE title := "Object not globally anchored"
		END;
		DevDebug.ShowHeapObject(S.ADR(blk.size), title)
	END ShowBlock;

	PROCEDURE BlockInfo (blk: Block; VAR s: ARRAY OF CHAR);
		VAR tag: Kernel.Type; str: ARRAY 256 OF CHAR;
	BEGIN
		tag := blk.tag;
		IF ODD(S.VAL(INTEGER, tag) DIV 2) THEN	(* array *)
			DEC(S.VAL(INTEGER, tag), 2);
			IF (tag.mod.name = "Kernel") & (tag.fields.num = 1) THEN
				tag := tag.fields.obj[0].struct
			END;
			WriteName(tag, str);
			s := "ARRAY OF "; Append(s, str)
		ELSE	(* record *)
			WriteName(tag, s)
		END
	END BlockInfo;

	PROCEDURE HeapInfo (VAR size, nofclusters: INTEGER);
		VAR cluster: Cluster;
	BEGIN
		nofclusters := 0; size := 0; cluster := S.VAL(Cluster, Kernel.Root());
		WHILE cluster # NIL DO
			INC(nofclusters); INC(size, cluster.size);
			cluster := cluster.next
		END;
	END HeapInfo;

	PROCEDURE (v: View) ThisModel (): Models.Model;
	BEGIN
		RETURN mem
	END ThisModel;

	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
		VAR h: INTEGER;
	BEGIN
		PaintMem(f, t, b);
		h := ViewHeight();
		IF h # v.height THEN v.context.SetSize(W, h) END;
	END Restore;

	PROCEDURE (v: View) HandleCtrlMsg (
		f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View
	);
		VAR x, y: INTEGER; isDown: BOOLEAN; blk, b: Block; m: SET; c, lastC: Cluster;
			s: ARRAY 100 OF CHAR; num: ARRAY 16 OF CHAR;
	BEGIN
		WITH msg: Controllers.TrackMsg DO
			blk := NIL; lastC := NIL;
			REPEAT
				f.Input(x, y, m, isDown);
				c := ThisCluster(f, x, y); b := ThisBlock(f, x, y);
				IF (b # blk) OR (c # lastC) THEN
					IF b # NIL THEN BlockInfo(b, s);
					ELSIF c # NIL THEN s := "cluster of length ";
						IF c.size MOD 1024 = 0 THEN
							Strings.IntToString(c.size DIV 1024, num); Append(s, num); Append(s, " KB")
						ELSE Strings.IntToString(c.size, num); Append(s, num); Append(s, " bytes")
						END;
						Append(s, " at address ");
						Strings.IntToStringForm(S.VAL(INTEGER, c), Strings.hexadecimal, 9, "0", TRUE, num);
						Append(s, num)
					ELSE s := ""
					END;
					Dialog.ShowStatus(s)
				END;
				lastC := c;
				IF b # blk THEN
					IF blk # NIL THEN MarkBlock(f, blk, FALSE) END;
					blk := b;
					IF blk # NIL THEN MarkBlock(f, blk, TRUE) END
				END
			UNTIL ~isDown;
			(* IF ~Dialog.showsStatus & (s # "") THEN Dialog.ShowMsg(s) END; *)
			IF blk # NIL THEN
				MarkBlock(f, blk, FALSE);
				ShowBlock(blk)
			END;
			Dialog.ShowStatus("");
		ELSE
		END
	END HandleCtrlMsg;

	PROCEDURE (v: View) HandleModelMsg (VAR msg: Models.Message);
	BEGIN
		WITH msg: Msg DO Views.Update(v, Views.keepFrames)
		ELSE
		END
	END HandleModelMsg;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.Preference DO
			WITH msg: Properties.ResizePref DO msg.fixed := TRUE
			| msg: Properties.FocusPref DO msg.hotFocus := TRUE
			| msg: Properties.SizePref DO msg.w := W; msg.h := ViewHeight();
			ELSE
			END
		ELSE
		END
	END HandlePropMsg;


	PROCEDURE (m: Model) CopyFrom (source: Stores.Store), EMPTY;


	PROCEDURE (a: Action) Do;
		VAR alloc, size, nofclusters: INTEGER; msg: Msg;
	BEGIN
		alloc := Kernel.Allocated();
		IF mem.alloc # alloc THEN
			mem.alloc := alloc;
			Models.Broadcast(mem, msg);
			par.allocated := alloc;
			HeapInfo(size, nofclusters);
			IF nofclusters # par.clusters THEN par.clusters := nofclusters END;
			IF size # par.heapsize THEN par.heapsize := size END;
			Dialog.Update(par)
		END;
		Services.DoLater(a, Services.Ticks() + Services.resolution)
	END Do;

	PROCEDURE ShowHeap*;
		VAR v: View; action: Action; d: Documents.Document;
	BEGIN
		NEW(v); v.height := ViewHeight();
		d := Documents.dir.New(v, W, v.height);
		Views.OpenAux(d, "Heap Layout")
	END ShowHeap;

	PROCEDURE GetAnchor* (adr: INTEGER; OUT anchor: ARRAY OF CHAR);
	BEGIN
		SearchAnchor(S.VAL(Block, adr - 4), anchor);
	END GetAnchor;

	PROCEDURE ShowAnchor* (adr: INTEGER);
	BEGIN
		ShowBlock(S.VAL(Block, adr - 4))
	END ShowAnchor;

	PROCEDURE Init;
		VAR action: Action;
	BEGIN
		NEW(mem); mem.alloc := 0;
		Stores.InitDomain(mem);
		NEW(action); Services.DoLater(action, Services.now)
	END Init;

BEGIN Init
END DevHeapSpy.