// 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();