import { Memory } from "../../core/runtime/Memory"; import { listenTo, MessageResponses, messageResponse, stopListeningTo } from "../../util/observe"; import * as SVG from "@svgdotjs/svg.js"; import { CPPObject } from "../../core/runtime/objects"; import { PointerToCompleteType } from "../../core/compilation/types"; import { Mutable } from "../../util/util"; import { StackFramesOutlet } from "./StackFramesOutlet"; import { TemporaryObjectsOutlet } from "./TemporaryObjectsOutlet"; import { HeapOutlet } from "./HeapOutlet"; import { MemoryObjectOutlet, ArrayMemoryObjectOutlet } from "./MemoryObjectOutlets"; export class MemoryOutlet { public readonly memory?: Memory; public readonly temporaryObjectsOutlet?: TemporaryObjectsOutlet; public readonly stackFramesOutlet?: StackFramesOutlet; public readonly heapOutlet?: HeapOutlet; private readonly element: JQuery; public readonly svgElem: JQuery; public readonly svg: SVG.Svg; public readonly SVG_DEFS: { [index: string]: SVG.Marker; }; public _act!: MessageResponses; /** * Maps from object ID to the outlet that represents that object. */ private objectOutlets: { [index: number]: MemoryObjectOutlet | undefined; } = {}; /** * Used to track SVG elements for pointer arrows. Maps from the object ID * for the pointer to the SVG element */ private pointerSVGElems: { [index: number]: SVGPointerArrowMemoryOverlay | undefined; } = {}; private svgOverlays: SVGMemoryOverlay[] = []; // public static updateArrows() { // this.instances = this.instances.filter((ptrMemObj) => { // if (jQuery.contains($("body")[0], ptrMemObj.element[0])) { // ptrMemObj.updateArrow(); // return true; // } // else{ //Element is detached // ptrMemObj.clearArrow(); // return false; // } // }); // } private svgUpdateThread: number; public constructor(element: JQuery) { this.element = element.addClass("lobster-memory"); this.svgElem = $('
'); this.svg = SVG.SVG().addTo(this.svgElem[0]); this.SVG_DEFS = { arrowStart: this.svg.marker(3, 3, function (add) { add.circle(3).fill({ color: '#fff' }); }), arrowEnd: this.svg.marker(6, 6, function (add) { add.path("M0,1 L0,5.5 L4,3 L0,1").fill({ color: '#fff' }); }) }; this.element.append(this.svgElem); this.svgUpdateThread = window.setInterval(() => this.updateSvg(), 20); } public dispose() { clearInterval(this.svgUpdateThread); } public setMemory(memory: Memory) { this.clearMemory(); (>this).memory = memory; listenTo(this, memory); (>this).temporaryObjectsOutlet = new TemporaryObjectsOutlet($("
").appendTo(this.element), memory, this); (>this).stackFramesOutlet = new StackFramesOutlet($("
").appendTo(this.element), memory, this); (>this).heapOutlet = new HeapOutlet($("
").appendTo(this.element), memory, this); // Since the simulation has already started, some objects will already be allocated memory.allLiveObjects().forEach(obj => this.onObjectAllocated(obj)); } public clearMemory() { delete (>this).temporaryObjectsOutlet; delete (>this).stackFramesOutlet; delete (>this).heapOutlet; this.element.children().filter((index, element) => element !== this.svgElem[0]).remove(); this.onReset(); if (this.memory) { stopListeningTo(this, this.memory); } delete (>this).memory; } public registerObjectOutlet(outlet: MemoryObjectOutlet) { this.objectOutlets[outlet.object.objectId] = outlet; } public disposeObjectOutlet(outlet: MemoryObjectOutlet) { delete this.objectOutlets[outlet.object.objectId]; } public getObjectOutletById(objectId: number) { return this.objectOutlets[objectId]; } public addSVGOverlay(overlay: SVGMemoryOverlay) { this.svgOverlays.push(overlay); } private updateSvg() { this.svgOverlays = this.svgOverlays.filter(svgOverlay => svgOverlay.update()); } @messageResponse("objectAllocated", "unwrap") private onObjectAllocated(object: CPPObject) { if (object.type.isPointerToCompleteObjectType()) { this.addSVGOverlay(new SVGPointerArrowMemoryOverlay( >object, this)); } } @messageResponse("reset") private onReset() { this.objectOutlets = {}; Object.values(this.pointerSVGElems).forEach(line => line?.remove()); this.pointerSVGElems = {}; this.svgOverlays.forEach(overlay => overlay.remove()); this.svgOverlays = []; } } abstract class SVGMemoryOverlay { protected memoryOutlet: MemoryOutlet; protected constructor(memoryOutlet: MemoryOutlet) { this.memoryOutlet = memoryOutlet; } public abstract update(): boolean; public abstract remove(): void; } export class SVGPointerArrowMemoryOverlay extends SVGMemoryOverlay { public readonly object: CPPObject; private line: SVG.Line; public constructor(object: CPPObject, memoryOutlet: MemoryOutlet) { super(memoryOutlet); this.object = object; this.line = memoryOutlet.svg.line(0, 0, 0, 0) .stroke({ color: '#fff', width: 1 }); this.line.marker("start", memoryOutlet.SVG_DEFS.arrowStart); this.line.marker("end", memoryOutlet.SVG_DEFS.arrowEnd); this.update(); } public update() { if (!this.object.isAlive) { this.line.remove(); return false; } let pointerElem = this.memoryOutlet.getObjectOutletById(this.object.objectId)?.objElem; let targetElem: JQuery | undefined; if (this.object.type.isArrayPointerType()) { let targetIndex = this.object.type.toIndex(this.object.rawValue()); let arr = this.object.type.arrayObject; let numElems = arr.type.numElems; let arrOutlet = this.memoryOutlet.getObjectOutletById(arr.objectId); if (0 <= targetIndex && targetIndex < numElems) { targetElem = arrOutlet?.elemOutlets[targetIndex].objElem; } else if (targetIndex === numElems) { targetElem = arrOutlet?.onePast; } } else if (this.object.type.isObjectPointerType()) { let targetObject = this.object.type.getPointedObject(); if (targetObject && targetObject.isAlive) { targetElem = this.memoryOutlet.getObjectOutletById(targetObject.objectId)?.objElem; } } if (!pointerElem || !targetElem) { this.line.hide(); return true; } let { startOffset, endOffset } = this.getPointerArrowOffsets(pointerElem, targetElem); this.line.plot(startOffset.left, startOffset.top, endOffset.left, endOffset.top); // this.line.marker("start", this.memoryOutlet.SVG_DEFS.arrowStart); // this.line.marker("end", this.memoryOutlet.SVG_DEFS.arrowEnd); this.line.show(); return true; } private getPointerArrowOffsets(pointerElem: JQuery, targetElem: JQuery) { let endOffset = targetElem.offset()!; endOffset.left += targetElem.outerWidth()! / 2; //endOffset.top += targetElem.outerHeight(); let startOffset = pointerElem.offset()!; startOffset.left += pointerElem.outerWidth()! / 2; // If start is below end (greater offset), we move top of end to bottom. if (startOffset.top > endOffset.top) { endOffset.top += targetElem.outerHeight()!; } else { startOffset.top += pointerElem.outerHeight()!; } let svgElemOffset = this.memoryOutlet.svgElem.offset()!; startOffset.left -= svgElemOffset.left; startOffset.top -= svgElemOffset.top; endOffset.left -= svgElemOffset.left; endOffset.top -= svgElemOffset.top; return { startOffset, endOffset }; } public remove(): void { this.line.remove(); } }