<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Compiling Windows Event Log templates — how evtx got ~3× faster</title>
<style>
:root {
--bg: #fdfdfb;
--fg: #1c2024;
--muted: #6b7280;
--accent: #4f46e5;
--card: #f4f4f0;
--border: #e2e2dc;
--lit-bg: #e8e8e2;
--lit-fg: #50555c;
--val-bg: #dbeafe;
--val-fg: #1d4ed8;
--attr-bg: #ede9fe;
--attr-fg: #6d28d9;
--child-bg: #dcfce7;
--child-fg: #15803d;
--bar1: #b9b9b0;
--bar2: #8e8ed6;
--bar3: #4f46e5;
--good: #15803d;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #16181d;
--fg: #e7e9ee;
--muted: #9aa1ac;
--accent: #8b85f4;
--card: #1f2229;
--border: #2e323b;
--lit-bg: #2a2d35;
--lit-fg: #b6bac2;
--val-bg: #1e3a5f;
--val-fg: #93c5fd;
--attr-bg: #3b2f63;
--attr-fg: #c4b5fd;
--child-bg: #14391f;
--child-fg: #86efac;
--bar1: #4a4d55;
--bar2: #6661c9;
--bar3: #8b85f4;
--good: #4ade80;
}
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0 auto;
padding: 3.5rem 1.4rem 6rem;
max-width: 860px;
background: var(--bg);
color: var(--fg);
font: 17px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
h1 { font-size: 2.1rem; line-height: 1.2; letter-spacing: -0.02em; margin: 0 0 0.4rem; }
h2 { font-size: 1.35rem; letter-spacing: -0.01em; margin: 2.8rem 0 0.8rem; }
p { margin: 0.8rem 0; }
.subtitle { color: var(--muted); font-size: 1.12rem; margin: 0 0 0.5rem; }
.byline { color: var(--muted); font-size: 0.88rem; margin-bottom: 2.2rem; }
a { color: var(--accent); }
code, pre {
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.86em;
}
code { background: var(--card); border-radius: 4px; padding: 0.1em 0.35em; }
pre {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.9rem 1.1rem;
overflow-x: auto;
line-height: 1.5;
}
pre code { background: none; padding: 0; }
figure { margin: 1.6rem 0; }
figcaption { color: var(--muted); font-size: 0.85rem; text-align: center; margin-top: 0.7rem; }
.statgrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 0.8rem;
margin: 1.8rem 0;
}
.stat {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.9rem 1rem 0.8rem;
}
.stat b { display: block; font-size: 1.55rem; letter-spacing: -0.02em; color: var(--accent); }
.stat span { color: var(--muted); font-size: 0.84rem; line-height: 1.35; display: block; margin-top: 0.15rem; }
.pipes { display: flex; flex-direction: column; gap: 1rem; }
.pipe {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.8rem 1rem;
}
.pipe h4 { margin: 0 0 0.5rem; font-size: 0.86rem; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.flow { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem; }
.stage {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.3rem 0.65rem;
font-size: 0.82rem;
white-space: nowrap;
}
.stage.hot { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.stage.dim { opacity: 0.65; }
.arr { color: var(--muted); font-size: 0.9rem; }
.per { font-size: 0.78rem; color: var(--muted); margin-top: 0.45rem; }
.tplbox pre { font-size: 0.8rem; }
.hole {
border-radius: 5px;
padding: 0.05em 0.3em;
font-weight: 600;
white-space: nowrap;
}
.hole.val { background: var(--val-bg); color: var(--val-fg); }
.hole.attr { background: var(--attr-bg); color: var(--attr-fg); }
.hole.child { background: var(--child-bg); color: var(--child-fg); }
/* worked example */
.vals { font-size: 0.84rem; }
.vals td, .vals th { padding: 0.35rem 0.7rem; }
.vals td:nth-child(3) { font-family: ui-monospace, Menlo, monospace; font-size: 0.78rem; color: var(--muted); }
.instr {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.7rem 1rem;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.8rem;
line-height: 2.05;
overflow-x: auto;
}
.instr .n { color: var(--muted); display: inline-block; width: 1.4em; }
.instr .kw { color: var(--muted); display: inline-block; width: 4.2em; }
.copy {
background: var(--lit-bg); color: var(--lit-fg);
border-radius: 5px; padding: 0.08em 0.4em; white-space: pre;
}
.fmt3, .fmt10, .fmt12 { border-radius: 5px; padding: 0.08em 0.4em; font-weight: 600; }
.fmt3, .fmt10, .fmt12 { background: var(--val-bg); color: var(--val-fg); }
.fmt19 { background: var(--child-bg); color: var(--child-fg); border-radius: 5px; padding: 0.08em 0.4em; font-weight: 600; }
.outbox {
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.7rem 1rem;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.8rem;
line-height: 1.9;
overflow-x: auto;
white-space: pre;
}
.outbox .c { color: var(--lit-fg); }
.outbox .v { background: var(--val-bg); color: var(--val-fg); border-radius: 4px; padding: 0.04em 0.2em; font-weight: 600; }
.lanes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8rem;
margin: 0.8rem 0;
}
.lane {
border: 1px solid var(--border);
border-radius: 12px;
padding: 0.8rem 1rem;
background: var(--card);
}
.lane h4 { margin: 0 0 0.4rem; font-size: 0.9rem; }
.lane.fast h4 { color: var(--good); }
.lane.slow h4 { color: var(--attr-fg); }
.lane p { font-size: 0.85rem; margin: 0.35rem 0; color: var(--muted); }
.converge {
text-align: center;
margin: 0.2rem 0 0;
font-size: 0.9rem;
color: var(--muted);
}
.barchart { display: flex; flex-direction: column; gap: 0.45rem; margin: 1rem 0; }
.barrow { display: grid; grid-template-columns: 9.5rem 1fr 4.5rem; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.barrow .lbl { text-align: right; color: var(--muted); }
.bartrack { background: var(--card); border-radius: 6px; overflow: hidden; }
.bar { height: 1.15rem; border-radius: 6px; }
.barrow .num { font-variant-numeric: tabular-nums; font-family: ui-monospace, Menlo, monospace; font-size: 0.8rem; }
.grouplbl { margin: 0.9rem 0 0.3rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: 0.9rem; }
th, td { text-align: right; padding: 0.45rem 0.7rem; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
th:first-child, td:first-child { text-align: left; }
td:first-child { text-align: left; }
table.vals td, table.vals th { text-align: left; }
th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
.small { font-size: 0.85rem; color: var(--muted); }
ul { padding-left: 1.3rem; }
li { margin: 0.3rem 0; }
@media (max-width: 640px) {
.lanes { grid-template-columns: 1fr; }
.barrow { grid-template-columns: 7rem 1fr 4rem; }
}
</style>
</head>
<body>
<h1>Compiling Windows Event Log templates</h1>
<p class="subtitle"><a href="https://github.com/omerbenamram/evtx">evtx</a> renders event logs about three times faster than it did two releases ago. The change: each event template is now compiled once into pre-rendered output text plus a short list of fill-in-the-blank instructions, instead of being re-walked for every record.</p>
<p class="byline">June 2026</p>
<div class="statgrid">
<div class="stat"><b>3.0× / 3.2×</b><span>JSON / XML single-thread speedup vs 0.12.1 (Zen 2, pinned core)</span></div>
<div class="stat"><b>1.2M rec/s</b><span>records per second on one Apple M3 Pro core, end to end</span></div>
<div class="stat"><b>0 bytes</b><span>of output changed — 27 sample logs × 5 output modes, verified on every commit</span></div>
<div class="stat"><b>32</b><span>distinct templates produce all 62,000 records in the 30 MB benchmark log</span></div>
</div>
<h2>The shape of the problem</h2>
<p>An EVTX file is a sequence of 64 KiB chunks, each holding a few hundred records. A record's payload is <em>binary XML</em>: a tokenized tree format in which Microsoft made one genuinely good decision — structure and data are stored separately. The structure lives in a <em>template definition</em> (an element tree with typed holes), stored once per chunk. Each record is then just a reference to a template plus an array of typed values for the holes.</p>
<p>Rendered, a record looks like this — a Security 4688, process creation:</p>
<pre><code><Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-…}"/>
<EventID>4688</EventID>
…
<TimeCreated SystemTime="2016-10-06T01:47:07.166302Z"/>
<Computer>WIN-WFBHIBE5GXZ.example.co.jp</Computer>
</System>
<EventData>
<Data Name="NewProcessName">C:\Windows\System32\cmd.exe</Data>
…
</EventData>
</Event></code></pre>
<p>Almost every byte of that output is constant per template — only the values change between records. And the workload is extremely repetitive: the 30 MB benchmark log holds 62,000 records across 481 chunks but only 32 distinct templates. Until this release, the parser re-parsed each template's structure once per chunk (about 4,500 times per file) and re-walked it once per record.</p>
<h2>What each version did per record</h2>
<figure>
<div class="pipes">
<div class="pipe">
<h4>v0.11 — build a tree for every record</h4>
<div class="flow">
<span class="stage">record bytes</span><span class="arr">→</span>
<span class="stage">token stream</span><span class="arr">→</span>
<span class="stage hot">clone template tree + fill holes</span><span class="arr">→</span>
<span class="stage hot">walk tree, escape, format</span><span class="arr">→</span>
<span class="stage">output</span>
</div>
<p class="per">Every record allocates and builds a full tree, then traverses it. Tree cloning alone was ~28% of cycles.</p>
</div>
<div class="pipe">
<h4>v0.12 — walk the cached template tree</h4>
<div class="flow">
<span class="stage">record bytes</span><span class="arr">→</span>
<span class="stage">decode values</span><span class="arr">→</span>
<span class="stage hot">walk the <em>cached</em> template tree, resolving holes on the fly</span><span class="arr">→</span>
<span class="stage">output</span>
</div>
<p class="per">No tree built, but still a full structural walk per record — every element re-classified (empty? one line? many?), every value decoded into an enum, every tag name re-escaped.</p>
</div>
<div class="pipe">
<h4>v0.13 — compile the template, run it per record</h4>
<div class="flow">
<span class="stage dim">once per template:</span>
<span class="stage">walk the definition</span><span class="arr">→</span>
<span class="stage">text buffer + instructions</span>
</div>
<div class="flow" style="margin-top:0.45rem">
<span class="stage dim">per record:</span>
<span class="stage">read the value table</span><span class="arr">→</span>
<span class="stage hot">copy text, format values</span><span class="arr">→</span>
<span class="stage">output</span>
</div>
<p class="per">Per record: N four-byte value descriptors, then a linear pass over the instructions. No tree, no token re-walk, no value enums, no layout decisions.</p>
</div>
</div>
<figcaption>The per-record pipeline across three releases, for each of the 62,000 records.</figcaption>
</figure>
<p>A profile of v0.12 showed where the time went: roughly a quarter of it decoded values into an intermediate enum, a sixth dispatched the tree walk, a sixth escaped and wrote strings, and a seventh ran layout classification — per record, for structure that is identical across thousands of records. All of that is computable once per template.</p>
<h2>Compiling a template</h2>
<p>The idea is the same as a <code>printf</code> format string, prepared ahead of time. The compiler walks a template definition once and renders everything constant — tag names, attributes, indentation, escaping — into a single flat text buffer. What's left is a short list of instructions: <em>copy this range of the buffer</em>, <em>format value N here</em>. Per record, rendering is just running that list.</p>
<figure class="tplbox">
<pre><code><Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-…}"/>
<EventID><span class="hole val">⟨value 3 · UInt16⟩</span></EventID>
<TimeCreated SystemTime="<span class="hole attr">⟨value 7 · FileTime⟩</span>"/>
<EventRecordID><span class="hole val">⟨value 10 · UInt64⟩</span></EventRecordID>
<Execution ProcessID="<span class="hole attr">⟨value 8 · UInt32⟩</span>" ThreadID="<span class="hole attr">⟨value 9 · UInt32⟩</span>"/>
<Computer><span class="hole val">⟨value 12 · UTF-16 string⟩</span></Computer>
</System>
<span class="hole child">⟨value 19 · embedded binary XML — the EventData section⟩</span>
</Event></code></pre>
<figcaption>The 4688 template, abridged. Everything outside the colored holes compiles to constant text.</figcaption>
</figure>
<p>In the source, the XML instruction set is one five-variant enum. Each variant carries everything its runtime decision needs, precomputed — including the alternative closing sequences for an element whose content might be empty, text, or a nested element:</p>
<pre><code>enum XOp {
/// Copy lits[range] to the output.
Lit(LitRange),
/// Escaped value text (element content or an always-present attribute).
Val { slot: u16, in_attr: bool },
/// ` name="value"` — the whole attribute is omitted when the value is empty.
AttrVal { slot: u16, pre: LitRange },
/// Element content with one hole: empty / text / nested-element each
/// get a precompiled closing sequence.
Body { slot: u16, optional: bool, indent: u16,
tail_text: LitRange, tail_empty: LitRange, tail_elem: LitRange },
/// A hole in child position: nothing, a nested instance, or indented text.
ChildSlot { slot: u16, optional: bool, indent: u16, ind: LitRange },
}</code></pre>
<p>JSON compiles from the same template with its own instruction set (<code>Lit</code>, <code>LeafVal</code>, <code>Elem</code>, <code>SlotChild</code>) because JSON output has its own rules, and they're decided at compile time too: <code>EventData</code> flattening, duplicate-key <code>_N</code> suffixes, which wire types print as bare numbers (integers and booleans; floats, hex, timestamps, GUIDs and SIDs stay quoted strings), and <code>null</code> versus <code>""</code> for empty values.</p>
<h2>Running one record through it</h2>
<p>At render time, a record contributes a value table: a count, then one (size, type) descriptor per value, then the value bytes packed back to back. A validation pass turns the descriptors into typed windows into the chunk buffer — no decoding, no copying:</p>
<figure>
<table class="vals">
<thead><tr><th>value</th><th>wire type</th><th>bytes in the chunk</th><th>formats as</th></tr></thead>
<tbody>
<tr><td>3</td><td>UInt16</td><td>50 12</td><td>4688</td></tr>
<tr><td>10</td><td>UInt64</td><td>37 77 03 00 00 00 00 00</td><td>227127</td></tr>
<tr><td>12</td><td>UTF-16 string</td><td>57 00 49 00 4E 00 2D 00 …</td><td>WIN-WFBHIBE5GXZ…</td></tr>
<tr><td>19</td><td>embedded binary XML</td><td>0F 01 01 00 0C …</td><td>the EventData section</td></tr>
</tbody>
</table>
<figcaption>Four of record 227127's twenty values. Until an instruction touches one, it stays raw bytes in the chunk.</figcaption>
</figure>
<p>Then the instructions run:</p>
<figure>
<div class="instr">
<span class="n">1</span> <span class="kw">copy</span> <span class="copy"> <EventID></span><br>
<span class="n">2</span> <span class="kw">format</span> value 3 → <span class="fmt3">4688</span><br>
<span class="n">3</span> <span class="kw">copy</span> <span class="copy"></EventID>⏎ … <EventRecordID></span><br>
<span class="n">4</span> <span class="kw">format</span> value 10 → <span class="fmt10">227127</span><br>
<span class="n">5</span> <span class="kw">copy</span> <span class="copy"></EventRecordID>⏎ … <Computer></span><br>
<span class="n">6</span> <span class="kw">format</span> value 12 → <span class="fmt12">WIN-WFBHIBE5GXZ.example.co.jp</span><br>
<span class="n">7</span> <span class="kw">copy</span> <span class="copy"></Computer>⏎ … </System>⏎</span><br>
<span class="n">8</span> <span class="kw">run</span> value 19's own compiled template → <span class="fmt19"><EventData>…</EventData></span><br>
<span class="n">9</span> <span class="kw">copy</span> <span class="copy"></Event>⏎</span>
</div>
</figure>
<p>And the output is assembled — gray bytes come straight out of the buffer with <code>memcpy</code>, colored bytes are formatted from the record:</p>
<figure>
<div class="outbox"><span class="c"> <EventID></span><span class="v">4688</span><span class="c"></EventID></span>
<span class="c"> ⋮</span>
<span class="c"> <EventRecordID></span><span class="v">227127</span><span class="c"></EventRecordID></span>
<span class="c"> ⋮</span>
<span class="c"> <Computer></span><span class="v">WIN-WFBHIBE5GXZ.example.co.jp</span><span class="c"></Computer></span></div>
</figure>
<p>Each value is formatted exactly once, directly from chunk bytes into the output buffer — UTF-16 to UTF-8 with escaping fused in a SIMD pass, integers through fixed-width decoders. Values never materialize into an intermediate representation at all. Nested templates (value 19 above) are themselves compiled, so the EventData section runs the same way.</p>
<p>The validation pass in front of this is what keeps it safe. Descriptor sizes are checked against a 256-entry table of wire-format facts:</p>
<pre><code>const TY_CLASS: [TyClass; 256] = { /* NULL→Sized, BOOL→Fixed(4),
GUID→Fixed(16), SID→Sid, UTF16_STRING→Utf16, …, everything else→Reject */ };</code></pre>
<p>A size that doesn't match its type, an unknown type, a malformed nested instance — anything irregular — and the record is rejected <em>before a single byte of output is written</em>. Rejected records take the slow path.</p>
<h2>The slow path</h2>
<p>Some records can't run on a compiled template: the compiler turns down templates whose output structure depends on values in ways the instruction set doesn't express, the per-record validation rejects irregular value tables, and the <code>--separate-json-attributes</code> output mode isn't compiled at all. Those records are parsed into a full tree and rendered from that, the way v0.12 rendered everything.</p>
<p>The part worth being careful about: the slow path is not a second rendering implementation. The compiler type itself renders trees. Walking a template, it emits an instruction wherever it finds a hole; walking a fully parsed record tree, there are no holes left, and the same walk writes its text directly to the output instead. Layout classification, escaping, emptiness rules — one copy of each. This matters because the path is chosen per record, and a record must render to the same bytes no matter which path it took.</p>
<figure>
<div class="lanes">
<div class="lane fast">
<h4>Fast path — compiled template</h4>
<p>Validate the value table, resolve nested instances, run the instructions.</p>
<p>Covers every record in Security logs and, with string-array support, nearly all of System and Application.</p>
</div>
<div class="lane slow">
<h4>Slow path — full parse</h4>
<p>Build the record's tree, then render it with the same walker in direct mode.</p>
<p>Handles whatever the fast path rejected: exotic value types, malformed tables, separate-attributes JSON, deeply nested structures.</p>
</div>
</div>
<p class="converge">Both paths produce identical bytes — checked by hashing 27 sample logs × 5 output modes against the previous release, on every commit.</p>
</figure>
<p>One case deserved real instructions instead of the slow path: array values. A substitution can hold a string array, and the spec says the <em>containing element</em> repeats once per item — <code><Data>a</Data><Data>b</Data></code> from a single two-item value. System and Application logs have these in 17–23% of records. They get a dedicated instruction that owns its element's opening tag and replays it per item; on a Zen 2 core that took Application.evtx from 22.8 ms to 11.0 ms.</p>
<h2>Compile once per file, not per chunk</h2>
<p>Compiled templates own their bytes, with no lifetime ties to the chunk they came from, so they can be cached at two levels: a per-chunk map keyed by definition offset for the common case, backed by a parser-wide store keyed by template content — GUID, size, and a hash of the definition bytes — behind an <code>RwLock</code> and shared across worker threads via <code>Arc</code>. The 481 chunks of the benchmark file used to trigger ~4,500 template parses per read; now each of the 32 templates compiles exactly once per run, and multithreaded workers reuse each other's work.</p>
<h2>Results</h2>
<p>security_big_sample.evtx — 30 MB, 62k records — single-threaded (<code>-t 1</code>), hyperfine, 10 runs. Three releases on the same Apple M3 Pro:</p>
<figure>
<div class="barchart">
<div class="grouplbl">JSON</div>
<div class="barrow"><span class="lbl">0.11.2 · tree/record</span><div class="bartrack"><div class="bar" style="width:88.8%;background:var(--bar1)"></div></div><span class="num">161.5 ms</span></div>
<div class="barrow"><span class="lbl">0.12.1 · tree walk</span><div class="bartrack"><div class="bar" style="width:72.8%;background:var(--bar2)"></div></div><span class="num">132.4 ms</span></div>
<div class="barrow"><span class="lbl">0.13 · compiled</span><div class="bartrack"><div class="bar" style="width:28.4%;background:var(--bar3)"></div></div><span class="num">51.7 ms</span></div>
<div class="grouplbl">XML</div>
<div class="barrow"><span class="lbl">0.11.2 · tree/record</span><div class="bartrack"><div class="bar" style="width:100%;background:var(--bar1)"></div></div><span class="num">181.8 ms</span></div>
<div class="barrow"><span class="lbl">0.12.1 · tree walk</span><div class="bartrack"><div class="bar" style="width:70.7%;background:var(--bar2)"></div></div><span class="num">128.5 ms</span></div>
<div class="barrow"><span class="lbl">0.13 · compiled</span><div class="bartrack"><div class="bar" style="width:29.6%;background:var(--bar3)"></div></div><span class="num">53.8 ms</span></div>
</div>
<figcaption>One M3 Pro core: ~580 MB/s, ~1.2M records/s end to end.</figcaption>
</figure>
<p>On an AMD Zen 2 box pinned to one core — the machine every change was measured on before landing — the gap against 0.12.1 is wider:</p>
<table>
<thead><tr><th>Zen 2, single core</th><th>0.12.1</th><th>0.13</th><th>speedup</th></tr></thead>
<tbody>
<tr><td>Security (30 MB), JSON</td><td>233.8 ms</td><td>76.8 ms</td><td>3.0×</td></tr>
<tr><td>Security (30 MB), XML</td><td>228.9 ms</td><td>71.6 ms</td><td>3.2×</td></tr>
<tr><td>Application (4 MB), JSON</td><td>22.8 ms</td><td>11.0 ms</td><td>2.1×</td></tr>
<tr><td>Application (4 MB), XML</td><td>22.0 ms</td><td>10.5 ms</td><td>2.1×</td></tr>
<tr><td>Security, multithreaded</td><td>36–38 ms</td><td>~16–17 ms</td><td>~2.2×</td></tr>
</tbody>
</table>
<h2>Verification</h2>
<ul>
<li>27 sample logs covering the corpus's odd shapes — ANSI codepages, provider-cached templates, zero-record files — are rendered in all five output modes and hashed against the previous release after every commit. This caught one real bug during development (generic binary-XML fragments in JSON output) before it landed.</li>
<li>99 integration tests plus snapshot tests, which also cover the per-record APIs.</li>
<li>A review pass over the new code found one genuine pre-existing bug — unbounded recursion on crafted nesting chains — now bounded by depth caps at build, validation, and scan time.</li>
<li>The crate is <code>#![forbid(unsafe_code)]</code> throughout, including the SIMD UTF-16 conversion.</li>
</ul>
<p class="small">Benchmarks: <code>hyperfine -w2 -r10</code>, <code>evtx_dump -t 1 -o <fmt></code> to <code>/dev/null</code>, fast-alloc feature, Apple M3 Pro (macOS) and AMD Zen 2 (Linux, <code>taskset</code>-pinned). The sample logs and output hashes are reproducible from the repository.</p>
</body>
</html>