hprof-analyzer 0.2.0

Fast, low-memory Java HPROF heap-dump analyzer with Eclipse MAT-parity reports (System Overview, Leak Suspects, Top Consumers).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Self-contained HTML report renderer.
//!
//! `render_html` emits a SINGLE self-contained HTML file with NO network
//! dependencies. Everything heavy is embedded COMPRESSED (raw-DEFLATE via
//! `flate2`, then base64) and inflated CLIENT-SIDE at load via the browser's
//! `DecompressionStream('deflate-raw')`:
//!
//!   - the report JSON (in a `<script type="application/octet-stream"
//!     id="report-data">` blob), and
//!   - the React app bundle (JS + inlined CSS).
//!
//! The ONLY uncompressed JS in the file is a tiny bootstrap that base64-decodes
//! and inflates the bundle blob, injects it as a `<script>` to boot the app,
//! which then decodes + inflates + parses the report-data blob and renders.
//!
//! flate2's `DeflateEncoder` produces RAW deflate (no zlib/gzip header), which
//! matches `DecompressionStream('deflate-raw')` end-to-end.

use std::io::Write;
use std::sync::OnceLock;

use base64::Engine as _;
use flate2::{Compression, write::DeflateEncoder};

use crate::diff_reports::SeriesDiffResult;
use crate::report::Report;
/// The React bundle pre-compressed as raw-deflate by `build.rs`.
/// base64-encoded directly into the HTML; the browser inflates it via
/// `DecompressionStream('deflate-raw')`.
static BUNDLE_DEFLATED: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bundle.deflate"));

/// The raw (unminified) React bundle for dev mode (`--dev`).
/// Embedded as plain text so the HTML report has a readable `<script>` tag.
static BUNDLE_RAW: &str = include_str!(concat!(env!("OUT_DIR"), "/bundle.js"));

fn bundle_b64() -> &'static str {
    static CACHED: OnceLock<String> = OnceLock::new();
    CACHED.get_or_init(|| base64::engine::general_purpose::STANDARD.encode(BUNDLE_DEFLATED))
}

/// Raw-DEFLATE (level 9) then base64-encode a byte slice. The codec matches the
/// analyzer's `--format json` Deflate9 and the browser's `deflate-raw`.
fn deflate_b64(bytes: &[u8]) -> String {
    let mut enc = DeflateEncoder::new(Vec::new(), Compression::new(9));
    enc.write_all(bytes)
        .expect("deflate write to Vec is infallible");
    let compressed = enc.finish().expect("deflate finish to Vec is infallible");
    base64::engine::general_purpose::STANDARD.encode(compressed)
}

/// Render a `Report` to a single self-contained HTML document.
///
/// Deterministic: for a given `Report` the output is byte-identical across
/// runs (serde_json preserves field order, the model carries only sorted
/// vectors, and deflate/base64 are pure functions of their input).
pub fn render_html(r: &Report) -> String {
    render_html_inner(r)
}

/// Like `render_html` but embeds the React bundle as a plain `<script>` tag
/// (no deflate/base64 wrapping) so it's human-readable in DevTools.
/// The JSON report data is still deflated. Output is much larger (~750 KB extra).
///
/// If `bundle_path` is `Some`, the bundle is read from that file at runtime
/// instead of using the compile-time embedded bytes, so JS/CSS changes take
/// effect without rebuilding the binary.
pub fn render_html_dev(r: &Report, bundle_path: Option<&std::path::Path>) -> String {
    let bundle_src: std::borrow::Cow<str> = match bundle_path {
        Some(p) => std::borrow::Cow::Owned(
            std::fs::read_to_string(p)
                .unwrap_or_else(|e| panic!("--bundle-path {}: {e}", p.display())),
        ),
        None => std::borrow::Cow::Borrowed(BUNDLE_RAW),
    };
    render_html_dev_inner(r, &bundle_src)
}

fn render_html_dev_inner(r: &Report, bundle_raw: &str) -> String {
    let json = serde_json::to_string(r).expect("Report serializes to JSON");
    let data_b64 = deflate_b64(json.as_bytes());
    let title = html_escape(&format!("Heap Dump Analysis: {}", r.overview.source_name));
    // Dev mode: embed the raw bundle as an inline <script> so browsers show
    // real source in DevTools. The globals (hprofInflate, __HPROF_DATA_B64__)
    // must be set up BEFORE the bundle script runs, so we emit DEV_PRELUDE_JS first.
    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
:root {{ color-scheme: light dark; }}
html, body {{ margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }}
#root {{ padding: 0; }}
#hprof-fallback {{ padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 160px; gap: 1rem; color: #666; font-size: 0.95rem; }}
</style>
</head>
<body>
<div id="root"><div id="hprof-fallback"><span>Loading heap dump report (dev mode)&hellip;</span></div></div>
<script type="application/octet-stream" id="report-data">{data_b64}</script>
<script>{dev_prelude}</script>
<script>
{bundle_raw}
</script>
</body>
</html>
"#,
        title = title,
        data_b64 = data_b64,
        dev_prelude = DEV_PRELUDE_JS,
        bundle_raw = bundle_raw,
    )
}

fn render_html_inner(r: &Report) -> String {
    let json = serde_json::to_string(r).expect("Report serializes to JSON");
    let data_b64 = deflate_b64(json.as_bytes());
    let title = html_escape(&format!("Heap Dump Analysis: {}", r.overview.source_name));
    let bundle_b64 = bundle_b64();
    // The bootstrap is the ONLY uncompressed JS. It reads the two base64 blobs
    // from the DOM, inflates the bundle blob (deflate-raw) to JS text, and
    // injects it as a <script> so the app boots; the app then reads the
    // compressed report blob, inflates + JSON.parses it, and renders. A
    // pure-JS inflate fallback covers browsers lacking DecompressionStream
    // (older Safari/Firefox), so the offline file always opens.
    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
:root {{ color-scheme: light dark; }}
html, body {{ margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }}
#root {{ padding: 0; }}
#hprof-fallback {{ padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 160px; gap: 1rem; color: #666; font-size: 0.95rem; }}
#hprof-pb-wrap {{ width: min(480px, 80vw); background: #e0e0e0; border-radius: 4px; overflow: hidden; height: 6px; }}
#hprof-pb {{ height: 100%; width: 0%; background: #4a90d9; border-radius: 4px; transition: width 0.08s linear; }}
#hprof-pb-label {{ font-size: 0.82rem; color: #888; }}
@media (prefers-color-scheme: dark) {{
  #hprof-fallback {{ color: #aaa; }}
  #hprof-pb-wrap {{ background: #333; }}
  #hprof-pb {{ background: #5ba3e8; }}
  #hprof-pb-label {{ color: #777; }}
}}
</style>
</head>
<body>
<div id="root"><div id="hprof-fallback"><span>Loading heap dump report&hellip;</span><div id="hprof-pb-wrap"><div id="hprof-pb"></div></div><span id="hprof-pb-label"></span></div></div>
<script type="application/octet-stream" id="report-data">{data_b64}</script>
<script type="application/octet-stream" id="app-bundle">{bundle_b64}</script>
<script>{bootstrap}</script>
</body>
</html>
"#,
        title = title,
        data_b64 = data_b64,
        bundle_b64 = bundle_b64,
        bootstrap = BOOTSTRAP_JS,
    )
}

/// Render an N-way cross-dump `SeriesDiffResult` to a single self-contained
/// HTML document. Reuses the SAME embedded React bundle and bootstrap as
/// `render_html`; the ONLY difference is the payload placed in `#report-data`.
///
/// Where a single-dump report embeds the RAW report JSON, this embeds a tagged
/// envelope `{"kind":"series-diff","diff": <SeriesDiffResult>}` so the shared
/// bundle can dispatch report-vs-diff at boot. A real single-dump `Report` has
/// no `kind` field, so the branch is unambiguous and backward-compatible.
///
/// Deterministic: for a given diff the output is byte-identical across runs.
pub fn render_diff_html(d: &SeriesDiffResult) -> String {
    // Tagged envelope so the shared bundle can tell a diff from a report.
    let envelope = serde_json::json!({ "kind": "series-diff", "diff": d });
    let json = serde_json::to_string(&envelope).expect("diff envelope serializes to JSON");
    let data_b64 = deflate_b64(json.as_bytes());
    let bundle_b64 = bundle_b64();

    let title = format!("Heap Dump Comparison ({} reports)", d.labels.len());
    let title = html_escape(&title);

    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
:root {{ color-scheme: light dark; }}
html, body {{ margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }}
#root {{ padding: 0; }}
#hprof-fallback {{ padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 160px; gap: 1rem; color: #666; font-size: 0.95rem; }}
#hprof-pb-wrap {{ width: min(480px, 80vw); background: #e0e0e0; border-radius: 4px; overflow: hidden; height: 6px; }}
#hprof-pb {{ height: 100%; width: 0%; background: #4a90d9; border-radius: 4px; transition: width 0.08s linear; }}
#hprof-pb-label {{ font-size: 0.82rem; color: #888; }}
@media (prefers-color-scheme: dark) {{
  #hprof-fallback {{ color: #aaa; }}
  #hprof-pb-wrap {{ background: #333; }}
  #hprof-pb {{ background: #5ba3e8; }}
  #hprof-pb-label {{ color: #777; }}
}}
</style>
</head>
<body>
<div id="root"><div id="hprof-fallback"><span>Loading heap dump comparison&hellip;</span><div id="hprof-pb-wrap"><div id="hprof-pb"></div></div><span id="hprof-pb-label"></span></div></div>
<script type="application/octet-stream" id="report-data">{data_b64}</script>
<script type="application/octet-stream" id="app-bundle">{bundle_b64}</script>
<script>{bootstrap}</script>
</body>
</html>
"#,
        title = title,
        data_b64 = data_b64,
        bundle_b64 = bundle_b64,
        bootstrap = BOOTSTRAP_JS,
    )
}

/// Minimal HTML text escaper for the `<title>` (the only place untrusted model
/// text lands in raw HTML; all other data flows through the JSON blob and is
/// rendered via the DOM API in the app, never as raw HTML).
fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
}

/// The uncompressed bootstrap loader. Kept tiny (~1-2 KB). Exposes
/// `window.hprofInflate(b64) -> Promise<Uint8Array>` and
/// `window.hprofDecodeText(b64) -> Promise<String>` used by both the bootstrap
/// (for the bundle) and the app (for the report data), then boots the bundle.
const BOOTSTRAP_JS: &str = r#"
// Patch localStorage to a silent no-op if the browser blocks access (e.g. strict
// Content-Security-Policy, sandboxed iframes, or Playwright headless mode).
(function () {
  try { localStorage.getItem("_"); } catch (_) {
    var _store = {};
    window.localStorage = { getItem: function(k){ return _store[k]??null; }, setItem: function(k,v){ _store[k]=String(v); }, removeItem: function(k){ delete _store[k]; }, clear: function(){ _store={}; }, key: function(i){ return Object.keys(_store)[i]??null; }, get length(){ return Object.keys(_store).length; } };
  }
})();
(function () {
  function b64ToBytes(b64) {
    var bin = atob(b64);
    var len = bin.length;
    var out = new Uint8Array(len);
    for (var i = 0; i < len; i++) out[i] = bin.charCodeAt(i);
    return out;
  }
  async function inflate(b64) {
    var bytes = b64ToBytes(b64);
    if (typeof DecompressionStream === "function") {
      var ds = new DecompressionStream("deflate-raw");
      var stream = new Response(new Blob([bytes]).stream().pipeThrough(ds));
      var buf = await stream.arrayBuffer();
      return new Uint8Array(buf);
    }
    return tinfl(bytes);
  }
  // Pure-JS raw-DEFLATE fallback for browsers without DecompressionStream.
  // Load-bearing: the offline file must always open.
  function tinfl(input) {
    var out = [], op = 0, ip = 0, bitBuf = 0, bitCnt = 0;
    function need(n){ while(bitCnt<n){ bitBuf|=input[ip++]<<bitCnt; bitCnt+=8; } }
    function bits(n){ need(n); var v=bitBuf&((1<<n)-1); bitBuf>>=n; bitCnt-=n; return v; }
    function build(lens){
      var max=0; for(var i=0;i<lens.length;i++) if(lens[i]>max) max=lens[i];
      var cnt=new Array(max+1).fill(0); for(i=0;i<lens.length;i++) cnt[lens[i]]++;
      cnt[0]=0; var next=new Array(max+1).fill(0), code=0;
      for(i=1;i<=max;i++){ code=(code+cnt[i-1])<<1; next[i]=code; }
      var codes={}; for(i=0;i<lens.length;i++){ var l=lens[i]; if(l){ codes[l+"_"+next[l]]=i; next[l]++; } }
      return {codes:codes,max:max};
    }
    function decode(t){ var code=0; for(var l=1;l<=t.max;l++){ code=(code<<1)|bits(1); var s=t.codes[l+"_"+code]; if(s!==undefined) return s; } throw "bad code"; }
    var LB=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258];
    var LE=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];
    var DB=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577];
    var DE=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];
    var CLO=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];
    while(true){
      var last=bits(1), type=bits(2);
      if(type===0){ bitBuf=0; bitCnt=0; var lenv=input[ip]|(input[ip+1]<<8); ip+=4; for(var k=0;k<lenv;k++) out[op++]=input[ip++]; }
      else {
        var lt, dt;
        if(type===1){
          var ll=[]; for(var i=0;i<288;i++) ll.push(i<144?8:i<256?9:i<280?7:8);
          var dl=[]; for(i=0;i<30;i++) dl.push(5);
          lt=build(ll); dt=build(dl);
        } else {
          var hlit=bits(5)+257, hdist=bits(5)+1, hclen=bits(4)+4;
          var cl=new Array(19).fill(0); for(i=0;i<hclen;i++) cl[CLO[i]]=bits(3);
          var ct=build(cl); var all=[]; while(all.length<hlit+hdist){ var s=decode(ct);
            if(s<16) all.push(s); else if(s===16){ var r=bits(2)+3, p=all[all.length-1]; while(r--) all.push(p); }
            else if(s===17){ var r2=bits(3)+3; while(r2--) all.push(0); }
            else { var r3=bits(7)+11; while(r3--) all.push(0); } }
          lt=build(all.slice(0,hlit)); dt=build(all.slice(hlit));
        }
        while(true){ var sym=decode(lt);
          if(sym===256) break;
          if(sym<256){ out[op++]=sym; }
          else { sym-=257; var length=LB[sym]+bits(LE[sym]); var ds2=decode(dt); var dist=DB[ds2]+bits(DE[ds2]);
            for(var c=0;c<length;c++){ out[op]=out[op-dist]; op++; } }
        }
      }
      if(last) break;
    }
    return new Uint8Array(out);
  }
  window.hprofInflate = inflate;
  var dec = new TextDecoder("utf-8");
  window.hprofDecodeText = function (b64) { return inflate(b64).then(function (u8) { return dec.decode(u8); }); };
  // Progress bar: estimate total load time from report data size (larger = longer inflate+parse+render).
  var _t0 = performance.now();
  var dataEl = document.getElementById("report-data");
  var _dataLen = dataEl ? dataEl.textContent.length : 0;
  // Empirical: ~300ms base + ~3ms per 1000 chars of base64 report data.
  var _estMs = 300 + _dataLen * 0.003;
  var _pb = document.getElementById("hprof-pb");
  var _pbl = document.getElementById("hprof-pb-label");
  var _timer = _pb ? setInterval(function () {
    var pct = Math.min(95, (performance.now() - _t0) / _estMs * 100);
    _pb.style.width = pct.toFixed(1) + "%";
    var elapsed = ((performance.now() - _t0) / 1000).toFixed(1);
    var est = (_estMs / 1000).toFixed(1);
    if (_pbl) _pbl.textContent = elapsed + "s / ~" + est + "s";
  }, 50) : null;
  window.__HPROF_DATA_B64__ = dataEl ? dataEl.textContent.trim() : "";
  var bundleEl = document.getElementById("app-bundle");
  var bundleB64 = bundleEl ? bundleEl.textContent.trim() : "";
  window.hprofDecodeText(bundleB64).then(function (src) {
    if (_timer) clearInterval(_timer);
    var s = document.createElement("script");
    s.textContent = src;
    document.body.appendChild(s);
  }).catch(function (e) {
    var fb = document.getElementById("hprof-fallback");
    if (fb) fb.textContent = "Failed to load report bundle: " + e;
  });
})();
"#;

/// Dev-mode prelude: sets up inflate helpers and `__HPROF_DATA_B64__` before
/// the inline bundle script runs. Omits the bundle-loading portion of the
/// normal bootstrap (bundle is already in the page as a `<script>` tag).
const DEV_PRELUDE_JS: &str = r#"
(function () {
  try { localStorage.getItem("_"); } catch (_) {
    var _store = {};
    window.localStorage = { getItem: function(k){ return _store[k]??null; }, setItem: function(k,v){ _store[k]=String(v); }, removeItem: function(k){ delete _store[k]; }, clear: function(){ _store={}; }, key: function(i){ return Object.keys(_store)[i]??null; }, get length(){ return Object.keys(_store).length; } };
  }
})();
(function () {
  function b64ToBytes(b64) {
    var bin = atob(b64);
    var len = bin.length;
    var out = new Uint8Array(len);
    for (var i = 0; i < len; i++) out[i] = bin.charCodeAt(i);
    return out;
  }
  async function inflate(b64) {
    var bytes = b64ToBytes(b64);
    if (typeof DecompressionStream === "function") {
      var ds = new DecompressionStream("deflate-raw");
      var stream = new Response(new Blob([bytes]).stream().pipeThrough(ds));
      var buf = await stream.arrayBuffer();
      return new Uint8Array(buf);
    }
    return tinfl(bytes);
  }
  function tinfl(input) {
    var out = [], op = 0, ip = 0, bitBuf = 0, bitCnt = 0;
    function need(n){ while(bitCnt<n){ bitBuf|=input[ip++]<<bitCnt; bitCnt+=8; } }
    function bits(n){ need(n); var v=bitBuf&((1<<n)-1); bitBuf>>=n; bitCnt-=n; return v; }
    function build(lens){
      var max=0; for(var i=0;i<lens.length;i++) if(lens[i]>max) max=lens[i];
      var cnt=new Array(max+1).fill(0); for(i=0;i<lens.length;i++) cnt[lens[i]]++;
      cnt[0]=0; var next=new Array(max+1).fill(0), code=0;
      for(i=1;i<=max;i++){ code=(code+cnt[i-1])<<1; next[i]=code; }
      var codes={}; for(i=0;i<lens.length;i++){ var l=lens[i]; if(l){ codes[l+"_"+next[l]]=i; next[l]++; } }
      return {codes:codes,max:max};
    }
    function decode(t){ var code=0; for(var l=1;l<=t.max;l++){ code=(code<<1)|bits(1); var s=t.codes[l+"_"+code]; if(s!==undefined) return s; } throw "bad code"; }
    var LB=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258];
    var LE=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];
    var DB=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577];
    var DE=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];
    var CLO=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];
    while(true){
      var last=bits(1), type=bits(2);
      if(type===0){ bitBuf=0; bitCnt=0; var lenv=input[ip]|(input[ip+1]<<8); ip+=4; for(var k=0;k<lenv;k++) out[op++]=input[ip++]; }
      else {
        var lt, dt;
        if(type===1){
          var ll=[]; for(var i=0;i<288;i++) ll.push(i<144?8:i<256?9:i<280?7:8);
          var dl=[]; for(i=0;i<30;i++) dl.push(5);
          lt=build(ll); dt=build(dl);
        } else {
          var hlit=bits(5)+257, hdist=bits(5)+1, hclen=bits(4)+4;
          var cl=new Array(19).fill(0); for(i=0;i<hclen;i++) cl[CLO[i]]=bits(3);
          var ct=build(cl); var all=[]; while(all.length<hlit+hdist){ var s=decode(ct);
            if(s<16) all.push(s); else if(s===16){ var r=bits(2)+3, p=all[all.length-1]; while(r--) all.push(p); }
            else if(s===17){ var r2=bits(3)+3; while(r2--) all.push(0); }
            else { var r3=bits(7)+11; while(r3--) all.push(0); } }
          lt=build(all.slice(0,hlit)); dt=build(all.slice(hlit));
        }
        while(true){ var sym=decode(lt);
          if(sym===256) break;
          if(sym<256){ out[op++]=sym; }
          else { sym-=257; var length=LB[sym]+bits(LE[sym]); var ds2=decode(dt); var dist=DB[ds2]+bits(DE[ds2]);
            for(var c=0;c<length;c++){ out[op]=out[op-dist]; op++; } }
        }
      }
      if(last) break;
    }
    return new Uint8Array(out);
  }
  window.hprofInflate = inflate;
  var dec = new TextDecoder("utf-8");
  window.hprofDecodeText = function (b64) { return inflate(b64).then(function (u8) { return dec.decode(u8); }); };
  var dataEl = document.getElementById("report-data");
  window.__HPROF_DATA_B64__ = dataEl ? dataEl.textContent.trim() : "";
})();
"#;