hprof-analyzer 0.2.0

Fast, low-memory Java HPROF heap-dump analyzer with Eclipse MAT-parity reports (System Overview, Leak Suspects, Top Consumers).
// React entry for the hprof-analyzer HTML report.
//
// The uncompressed bootstrap (in the HTML shell) has already run: it inflated
// this bundle and injected it, and exposed:
//   - window.__HPROF_DATA_B64__  : base64 of raw-DEFLATE'd report JSON
//   - window.hprofDecodeText(b64): Promise<string> (inflate + UTF-8 decode)
// We decode + inflate + JSON.parse the report data, then render.
import React from "react";
import { createRoot } from "react-dom/client";
import App, { DiffApp, ErrorBoundary } from "./App";
import type { Report, SeriesDiffEnvelope } from "./types";
import { FlatTreemap, QueryViz } from "./charts";
import { formatBytes, fmtCount } from "./format";
import type { QueryResult } from "./types";
import css from "./styles.css";

function fail(msg: string): void {
  const root = document.getElementById("root");
  if (root) root.textContent = msg;
}

function injectStyles(): void {
  const style = document.createElement("style");
  style.textContent = css as unknown as string;
  document.head.appendChild(style);
}

// ── Viz renderer exposed to shell.js ────────────────────────────────────────
// shell.js calls window.hprofRenderViz(container, slices, kind, fmt) to mount
// a React treemap / histogram into a plain DOM container. A Map tracks roots
// so the same container can be re-rendered or unmounted cleanly.
type VizSlice = { name: string; value: number };
const _vizRoots = new Map<Element, ReturnType<typeof createRoot>>();

declare global {
  interface Window {
    __HPROF_DATA_B64__?: string;
    hprofDecodeText?: (b64: string) => Promise<string>;
    hprofRenderViz: (
      container: Element,
      slices: VizSlice[],
      kind: "treemap" | "histogram" | "piechart",
      fmtKind: "bytes" | "count",
      height?: number,
    ) => void;
    hprofRenderQueryViz: (container: Element, query: QueryResult) => void;
    hprofUnmountViz: (container: Element) => void;
  }
}

window.hprofRenderViz = (container, slices, kind, fmtKind, height = 220) => {
  let root = _vizRoots.get(container);
  if (!root) {
    root = createRoot(container);
    _vizRoots.set(container, root);
  }
  const fmt = fmtKind === "bytes" ? formatBytes : fmtCount;
  root.render(
    <React.StrictMode>
      <FlatTreemap data={slices} fmt={fmt} height={height} />
    </React.StrictMode>,
  );
};

window.hprofRenderQueryViz = (container, query) => {
  let root = _vizRoots.get(container);
  if (!root) {
    root = createRoot(container);
    _vizRoots.set(container, root);
  }
  root.render(
    <React.StrictMode>
      <QueryViz query={query} />
    </React.StrictMode>,
  );
};

window.hprofUnmountViz = (container) => {
  const root = _vizRoots.get(container);
  if (root) {
    root.unmount();
    _vizRoots.delete(container);
  }
};

async function boot(): Promise<void> {
  const b64 = window.__HPROF_DATA_B64__ || "";
  const decode = window.hprofDecodeText;
  if (!decode) {
    fail("Report bootstrap missing (hprofDecodeText).");
    return;
  }
  let parsed: Report | SeriesDiffEnvelope;
  try {
    const json = await decode(b64);
    parsed = JSON.parse(json) as Report | SeriesDiffEnvelope;
  } catch (e) {
    fail("Failed to parse report data: " + e);
    return;
  }
  injectStyles();
  const el = document.getElementById("root");
  if (!el) {
    fail("Missing #root element.");
    return;
  }
  // A single-dump Report has no `kind` field; the diff view wraps its payload
  // in a {"kind":"series-diff", diff} envelope so we can dispatch here.
  const isDiff =
    parsed != null &&
    typeof parsed === "object" &&
    (parsed as SeriesDiffEnvelope).kind === "series-diff";
  // Client-side schema version guard: refuse reports generated by a newer
  // hprof-analyzer than this viewer knows about (same policy as the Rust CLI).
  const CURRENT_SCHEMA_VERSION = 11;
  if (!isDiff) {
    const v = (parsed as Report).schema_version;
    if (typeof v === "number" && v > CURRENT_SCHEMA_VERSION) {
      fail(
        `This report was generated by a newer hprof-analyzer (schema version ${v}); ` +
        `this viewer supports up to version ${CURRENT_SCHEMA_VERSION}. ` +
        `Please upgrade the viewer.`
      );
      return;
    }
  }
  // Do NOT clear #root up front: createRoot().render replaces the container's
  // contents itself, and an early clear would leave a blank page if render threw
  // before painting. The ErrorBoundary catches render exceptions and shows a
  // panel; a synchronous mount failure falls through to fail() below.
  try {
    createRoot(el).render(
      <React.StrictMode>
        <ErrorBoundary>
          {isDiff ? (
            <DiffApp diff={(parsed as SeriesDiffEnvelope).diff} />
          ) : (
            <App report={parsed as Report} />
          )}
        </ErrorBoundary>
      </React.StrictMode>,
    );
  } catch (e) {
    fail("Failed to render report: " + e);
  }
}

void boot();