evtx 0.12.2

A Fast (and safe) parser for the Windows XML Event Log (EVTX) format
Documentation
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
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
<!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&nbsp;2, pinned core)</span></div>
  <div class="stat"><b>1.2M rec/s</b><span>records per second on one Apple M3&nbsp;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&nbsp;MB benchmark log</span></div>
</div>

<h2>The shape of the problem</h2>

<p>An EVTX file is a sequence of 64&nbsp;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>&lt;Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"&gt;
  &lt;System&gt;
    &lt;Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-…}"/&gt;
    &lt;EventID&gt;4688&lt;/EventID&gt;
    …
    &lt;TimeCreated SystemTime="2016-10-06T01:47:07.166302Z"/&gt;
    &lt;Computer&gt;WIN-WFBHIBE5GXZ.example.co.jp&lt;/Computer&gt;
  &lt;/System&gt;
  &lt;EventData&gt;
    &lt;Data Name="NewProcessName"&gt;C:\Windows\System32\cmd.exe&lt;/Data&gt;
    …
  &lt;/EventData&gt;
&lt;/Event&gt;</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&nbsp;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>&lt;Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"&gt;
  &lt;System&gt;
    &lt;Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-…}"/&gt;
    &lt;EventID&gt;<span class="hole val">⟨value 3 · UInt16⟩</span>&lt;/EventID&gt;
    &lt;TimeCreated SystemTime="<span class="hole attr">⟨value 7 · FileTime⟩</span>"/&gt;
    &lt;EventRecordID&gt;<span class="hole val">⟨value 10 · UInt64⟩</span>&lt;/EventRecordID&gt;
    &lt;Execution ProcessID="<span class="hole attr">⟨value 8 · UInt32⟩</span>" ThreadID="<span class="hole attr">⟨value 9 · UInt32⟩</span>"/&gt;
    &lt;Computer&gt;<span class="hole val">⟨value 12 · UTF-16 string⟩</span>&lt;/Computer&gt;
  &lt;/System&gt;
  <span class="hole child">⟨value 19 · embedded binary XML — the EventData section⟩</span>
&lt;/Event&gt;</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">  &lt;EventID&gt;</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">&lt;/EventID&gt;⏎ … &lt;EventRecordID&gt;</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">&lt;/EventRecordID&gt;⏎ … &lt;Computer&gt;</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">&lt;/Computer&gt;⏎ … &lt;/System&gt;⏎</span><br>
<span class="n">8</span> <span class="kw">run</span> value 19's own compiled template → <span class="fmt19">&lt;EventData&gt;…&lt;/EventData&gt;</span><br>
<span class="n">9</span> <span class="kw">copy</span> <span class="copy">&lt;/Event&gt;⏎</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">    &lt;EventID&gt;</span><span class="v">4688</span><span class="c">&lt;/EventID&gt;</span>
<span class="c">    ⋮</span>
<span class="c">    &lt;EventRecordID&gt;</span><span class="v">227127</span><span class="c">&lt;/EventRecordID&gt;</span>
<span class="c">    ⋮</span>
<span class="c">    &lt;Computer&gt;</span><span class="v">WIN-WFBHIBE5GXZ.example.co.jp</span><span class="c">&lt;/Computer&gt;</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>&lt;Data&gt;a&lt;/Data&gt;&lt;Data&gt;b&lt;/Data&gt;</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&nbsp;2 core that took Application.evtx from 22.8&nbsp;ms to 11.0&nbsp;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&nbsp;MB, 62k records — single-threaded (<code>-t 1</code>), hyperfine, 10 runs. Three releases on the same Apple M3&nbsp;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&nbsp;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 &lt;fmt&gt;</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>