memscope-rs 0.2.6

A memory tracking library for Rust applications.
Documentation
    // === Variable Dependency Graph (pure SVG radial layout — no external dep) ===
    // Renders variable nodes on a circle with curved edges. Fully interactive:
    // hover shows a detailed tooltip + highlights the node's neighbourhood,
    // click selects the node and populates the SELECTED_NODE detail panel.
    // Pure SVG guarantees rendering even if the D3 CDN is unreachable.
    function renderVariableGraph() {
        const container = document.getElementById('variableGraphContainer');
        if (!container) return;
        const rels = DATA.relationships || [];
        if (!rels.length) {
            container.innerHTML = '<div class="text-outline text-xs px-2 py-4">No relationship data.</div>';
            return;
        }
        // Build node metadata: in/out degree, type, pointer, cycle flag.
        const nodeMeta = {};
        rels.forEach(r => {
            const s = r.source_var_name || r.source_ptr || '?';
            const t = r.target_var_name || r.target_ptr || '?';
            if (!nodeMeta[s]) nodeMeta[s] = { name: s, type: r.type_name || '?', ptr: r.source_ptr || '', inDeg: 0, outDeg: 0, cycle: false };
            if (!nodeMeta[t]) nodeMeta[t] = { name: t, type: r.type_name || '?', ptr: r.target_ptr || '', inDeg: 0, outDeg: 0, cycle: false };
            nodeMeta[s].outDeg++;
            nodeMeta[t].inDeg++;
            if (r.is_part_of_cycle) { nodeMeta[s].cycle = true; nodeMeta[t].cycle = true; }
        });
        // Cap visible nodes for readability; keep highest-degree ones.
        const nodeNames = Object.keys(nodeMeta)
            .sort((a, b) => (nodeMeta[b].inDeg + nodeMeta[b].outDeg) - (nodeMeta[a].inDeg + nodeMeta[a].outDeg))
            .slice(0, 36);
        const visSet = {};
        nodeNames.forEach(n => { visSet[n] = true; });

        const W = container.clientWidth || 640;
        const H = 380;
        const cx = W / 2, cy = H / 2;
        const radius = Math.min(W, H) * 0.38;
        const pos = {};
        nodeNames.forEach((n, i) => {
            const angle = (i / nodeNames.length) * Math.PI * 2 - Math.PI / 2;
            pos[n] = { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius };
        });
        // Edges between visible nodes only.
        const edges = rels.filter(r => {
            const s = r.source_var_name || r.source_ptr || '?';
            const t = r.target_var_name || r.target_ptr || '?';
            return visSet[s] && visSet[t] && s !== t;
        }).slice(0, 140);

        let html = '<svg width="100%" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" id="varGraphSvg">';
        html += '<defs><marker id="vgArrow" viewBox="0 -5 10 10" refX="10" refY="0" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,-5L10,0,L0,5" fill="#948e9c"/></marker></defs>';
        // Edges as quadratic curves for a cleaner look than straight lines.
        edges.forEach(e => {
            const s = e.source_var_name || e.source_ptr || '?';
            const t = e.target_var_name || e.target_ptr || '?';
            const sp = pos[s], tp = pos[t];
            if (!sp || !tp) return;
            const mx = (sp.x + tp.x) / 2, my = (sp.y + tp.y) / 2;
            const dx = tp.x - sp.x, dy = tp.y - sp.y;
            const ctrlX = mx - dy * 0.12, ctrlY = my + dx * 0.12;
            const color = e.color || '#494551';
            const sw = Math.max(0.8, (e.strength || 0.5) * 2.2).toFixed(2);
            html += '<path class="vg-edge" data-source="' + s + '" data-target="' + t + '" d="M' + sp.x + ',' + sp.y + ' Q' + ctrlX + ',' + ctrlY + ' ' + tp.x + ',' + tp.y + '" stroke="' + color + '" stroke-width="' + sw + '" fill="none" opacity="0.45" marker-end="url(#vgArrow)" style="cursor:pointer"/>';
        });
        // Nodes — radius scales with degree; cycle nodes get a red ring.
        nodeNames.forEach(n => {
            const p = pos[n];
            const m = nodeMeta[n];
            const deg = m.inDeg + m.outDeg;
            const r = Math.max(4, Math.min(11, 4 + deg * 0.45));
            const ring = m.cycle ? '#ef4444' : 'var(--accent-amber)';
            html += '<g class="vg-node" data-name="' + n + '" data-type="' + m.type + '" data-ptr="' + m.ptr + '" data-in="' + m.inDeg + '" data-out="' + m.outDeg + '" data-cycle="' + (m.cycle ? 1 : 0) + '" style="cursor:pointer">';
            html += '<circle cx="' + p.x + '" cy="' + p.y + '" r="' + r + '" fill="var(--primary)" stroke="' + ring + '" stroke-width="1.5"/>';
            const label = n.length > 14 ? n.slice(0, 14) + '…' : n;
            html += '<text x="' + p.x + '" y="' + (p.y - r - 4) + '" fill="#cbc4d2" font-family="JetBrains Mono" font-size="9" text-anchor="middle">' + label + '</text>';
            html += '</g>';
        });
        html += '</svg>';
        container.innerHTML = html;

        const svgEl = container.querySelector('#varGraphSvg');
        // Highlight a node and its incident edges; dim the rest.
        function highlightNode(name) {
            svgEl.querySelectorAll('.vg-edge').forEach(e => {
                const active = e.getAttribute('data-source') === name || e.getAttribute('data-target') === name;
                e.setAttribute('opacity', active ? '0.95' : '0.10');
            });
            svgEl.querySelectorAll('.vg-node').forEach(g => {
                g.style.opacity = (g.getAttribute('data-name') === name) ? '1' : '0.30';
            });
        }
        function clearHighlight() {
            svgEl.querySelectorAll('.vg-edge').forEach(e => { e.setAttribute('opacity', '0.45'); });
            svgEl.querySelectorAll('.vg-node').forEach(g => { g.style.opacity = '1'; });
        }
        // Node hover + click — full detail tooltip and SELECTED_NODE panel sync.
        svgEl.querySelectorAll('.vg-node').forEach(g => {
            const name = g.getAttribute('data-name');
            const type = g.getAttribute('data-type');
            const ptr = g.getAttribute('data-ptr');
            const inDeg = g.getAttribute('data-in');
            const outDeg = g.getAttribute('data-out');
            const isCycle = g.getAttribute('data-cycle') === '1';
            g.addEventListener('mouseenter', function(ev) {
                showChartTooltip(ev, 'VARIABLE_NODE', [
                    ['name', name], ['type', type], ['ptr', ptr || '—'],
                    ['in_degree', inDeg], ['out_degree', outDeg],
                    ['total_rels', (parseInt(inDeg) || 0) + (parseInt(outDeg) || 0)],
                    ['cycle', isCycle ? 'YES' : 'no']
                ]);
                highlightNode(name);
            });
            g.addEventListener('mousemove', moveChartTooltip);
            g.addEventListener('mouseleave', function() { hideChartTooltip(); clearHighlight(); });
            g.addEventListener('click', function() {
                const detail = document.getElementById('detailNodeName');
                const status = document.getElementById('detailNodeStatus');
                const typeEl = document.getElementById('detailNodeType');
                const cycleEl = document.getElementById('detailNodeCycle');
                const execEl = document.getElementById('detailNodeExecTime');
                const upEl = document.getElementById('detailNodeUpstream');
                const traceEl = document.getElementById('detailNodeTrace');
                const uuidEl = document.getElementById('detailNodeUuid');
                if (detail) detail.textContent = name;
                if (status) status.textContent = 'in:' + inDeg + ' · out:' + outDeg;
                if (typeEl) typeEl.textContent = 'TYPE: ' + (type || '—');
                if (cycleEl) {
                    cycleEl.textContent = isCycle ? '⚠ PART OF CYCLE (leak risk)' : '✓ not in cycle';
                    cycleEl.className = 'font-data-mono text-[10px] mt-1 ' + (isCycle ? 'text-error' : 'text-success');
                }
                if (execEl) execEl.textContent = ptr ? ptr : '—';
                if (upEl) upEl.textContent = (parseInt(inDeg) || 0) + (parseInt(outDeg) || 0) + ' total';
                if (uuidEl) uuidEl.textContent = 'PTR: ' + (ptr || '—');
                // Build a synthetic trace from incident relationships.
                if (traceEl) {
                    const incidents = rels.filter(r => (r.source_var_name || r.source_ptr) === name || (r.target_var_name || r.target_ptr) === name).slice(0, 8);
                    if (incidents.length) {
                        traceEl.innerHTML = incidents.map(r => {
                            const s = r.source_var_name || r.source_ptr || '?';
                            const t = r.target_var_name || r.target_ptr || '?';
                            const cyc = r.is_part_of_cycle ? ' ⚠' : '';
                            return '<div class="text-on-surface-variant exec-trace-item" data-source="' + s + '" data-target="' + t + '" style="cursor:pointer">' + s + ' → ' + t + ' · ' + (r.relationship_type || 'link') + ' · ' + (r.strength || 0).toFixed(2) + cyc + '</div>';
                        }).join('');
                        // Bind hover + click to each freshly-created trace row so the
                        // detail panel's exec trace is itself interactive.
                        traceEl.querySelectorAll('.exec-trace-item').forEach((item, i) => {
                            const r = incidents[i] || {};
                            const sName = item.getAttribute('data-source');
                            const tName = item.getAttribute('data-target');
                            item.classList.add('chart-interactive');
                            item.addEventListener('mousemove', function(ev) {
                                showChartTooltip(ev, 'EXEC_TRACE', [
                                    ['from', sName],
                                    ['to', tName],
                                    ['type', r.relationship_type || 'link'],
                                    ['strength', (r.strength || 0).toFixed(2)],
                                    ['type_name', r.type_name || '—'],
                                    ['cycle', r.is_part_of_cycle ? 'YES ⚠' : 'no'],
                                    ['container_src', r.is_container_source ? 'yes' : 'no'],
                                    ['container_tgt', r.is_container_target ? 'yes' : 'no']
                                ]);
                            });
                            item.addEventListener('mouseleave', hideChartTooltip);
                            item.addEventListener('click', function() {
                                drillKpi('exec_trace', 'RELATIONSHIP', sName + ' → ' + tName + ' · ' + (r.relationship_type || 'link') + ' · ' + (r.strength || 0).toFixed(2));
                            });
                        });
                    } else {
                        traceEl.innerHTML = '<div class="text-outline">No incident relationships.</div>';
                    }
                }
                drillKpi('var_node', 'VARIABLE_NODE', name + ' · ' + type + ' · in ' + inDeg + ' / out ' + outDeg + (isCycle ? ' · CYCLE' : ''));
            });
        });
        // Edge hover + click — look up the originating relationship for full detail.
        svgEl.querySelectorAll('.vg-edge').forEach(e => {
            const s = e.getAttribute('data-source');
            const t = e.getAttribute('data-target');
            const rel = rels.find(r => (r.source_var_name || r.source_ptr) === s && (r.target_var_name || r.target_ptr) === t) || {};
            e.addEventListener('mouseenter', function(ev) {
                showChartTooltip(ev, 'RELATIONSHIP', [
                    ['from', s], ['to', t],
                    ['type', rel.relationship_type || 'link'],
                    ['strength', (rel.strength != null ? rel.strength.toFixed(2) : '?')],
                    ['type_name', rel.type_name || '—'],
                    ['cycle', rel.is_part_of_cycle ? 'yes' : 'no']
                ]);
            });
            e.addEventListener('mousemove', moveChartTooltip);
            e.addEventListener('mouseleave', hideChartTooltip);
            e.addEventListener('click', function() {
                drillKpi('var_edge', 'RELATIONSHIP', s + ' → ' + t + ' · ' + (rel.relationship_type || 'link'));
            });
        });
    }
    registerThemeAware('variableGraph', renderVariableGraph);
    renderVariableGraph();

    // === Task Topology DAG (SVG) ===
    (function renderTaskTopology() {
        const canvas = document.getElementById('taskTopologyCanvas');
        if (!canvas) return;
        const nodes = DATA.task_topology_nodes || [];
        const edges = DATA.task_topology_edges || [];
        if (nodes.length === 0) {
            canvas.innerHTML = '<div class="text-outline text-xs px-4 py-8">No topology data.</div>';
            return;
        }
        const W = canvas.clientWidth || 600;
        const H = 410;
        let html = '<svg width="100%" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '">';
        // Build lookup map by task_id for edge resolution
        const nodeMap = {};
        nodes.forEach(n => { nodeMap[n.task_id] = n; });
        // Draw edges
        edges.forEach(edge => {
            const src = nodeMap[edge.source];
            const tgt = nodeMap[edge.target];
            if (!src || !tgt) return;
            const x1 = (src.x_pct / 100) * W;
            const y1 = (src.y_pct / 100) * H;
            const x2 = (tgt.x_pct / 100) * W;
            const y2 = (tgt.y_pct / 100) * H;
            const edgeStroke = edge.is_active === true ? 'var(--accent-amber)' : '#494551';
            html += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" style="stroke: ' + edgeStroke + ';cursor:pointer" stroke-width="1.5" opacity="0.5" class="topo-edge" data-active="' + (edge.is_active == null ? 'Unknown' : edge.is_active ? 'yes' : 'no') + '"/>';
        });
        // Draw nodes
        nodes.forEach(node => {
            const x = (node.x_pct / 100) * W;
            const y = (node.y_pct / 100) * H;
            // RUNNING → semantic green; POLLING → theme accent; COMPLETED → theme primary; else muted.
            const fillVar = node.status === 'RUNNING' ? '#10B981' : node.status === 'POLLING' ? 'var(--accent-amber)' : node.status === 'COMPLETED' ? 'var(--primary)' : '#948e9c';
            html += '<circle cx="' + x + '" cy="' + y + '" r="8" style="fill: ' + fillVar + '" opacity="0.85" class="topo-node chart-interactive" data-id="' + node.task_id + '" data-name="' + node.name + '" data-status="' + node.status + '" data-duration="' + (node.duration_ms == null ? 'Unknown' : node.duration_ms + 'ms') + '"/>';
            html += '<text x="' + x + '" y="' + (y + 18) + '" fill="#cbc4d2" font-family="JetBrains Mono" font-size="9" text-anchor="middle" paint-order="stroke" stroke="#141218" stroke-width="3">' + (node.name || '').slice(0, 18) + '</text>';
        });
        html += '</svg>';
        canvas.innerHTML = html;
        canvas.querySelectorAll('.topo-node').forEach(n => {
            const nm = n.getAttribute('data-name');
            const id = n.getAttribute('data-id');
            const st = n.getAttribute('data-status');
            const dur = n.getAttribute('data-duration');
            n.addEventListener('mousemove', function(ev) {
                showChartTooltip(ev, 'TASK_NODE', [['name', nm], ['task_id', id], ['status', st], ['duration', dur]]);
            });
            n.addEventListener('mouseleave', hideChartTooltip);
            n.addEventListener('click', function() {
                drillKpi('topo_node', 'TASK_NODE', nm + ' [' + id + '] — ' + st + ' (' + dur + ')');
            });
        });
        canvas.querySelectorAll('.topo-edge').forEach(e => {
            const act = e.getAttribute('data-active');
            e.addEventListener('mousemove', function(ev) {
                showChartTooltip(ev, 'TASK_EDGE', [['active', act]]);
            });
            e.addEventListener('mouseleave', hideChartTooltip);
        });
    })();

    // === FFI Thread Execution Timeline (HTML segments) — inline styles for CSS var colors ===
    (function renderFfiThreadTimeline() {
        const container = document.getElementById('ffiThreadTimeline');
        if (!container) return;
        const rows = DATA.thread_timeline || [];
        if (rows.length === 0) {
            container.innerHTML = '<div class="text-outline text-xs">No timeline data.</div>';
            return;
        }
        let html = '';
        rows.forEach((row, ri) => {
            html += '<div class="flex items-center gap-3">';
            html += '<span class="font-data-mono text-xs text-accent-amber w-32 truncate">' + (row.thread_name || '?') + '</span>';
            html += '<div class="flex-1 h-5 bg-surface-container-low rounded overflow-hidden relative">';
            (row.segments || []).forEach((seg, si) => {
                const w = Math.max(1, seg.width_pct);
                // Use inline style background so CSS variables like var(--primary) resolve correctly
                html += '<div class="absolute h-full opacity-70 chart-interactive" style="left:' + seg.start_pct + '%;width:' + w + '%;background:' + (seg.color || 'var(--primary)') + '" title="' + (row.thread_name || '') + ' seg #' + si + ' · ' + seg.width_pct + '%"></div>';
            });
            html += '</div></div>';
        });
        container.innerHTML = html;
        // Add click handlers to segments
        container.querySelectorAll('.chart-interactive').forEach((seg, i) => {
            seg.addEventListener('click', function() {
                drillKpi('timeline_seg', 'TIMELINE_SEG', this.getAttribute('title'));
            });
        });
    })();