<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bill Edenfield</title>
    <description>The latest articles on DEV Community by Bill Edenfield (@baldheadbill).</description>
    <link>https://dev.to/baldheadbill</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909603%2F57a81891-6313-4421-8d3d-3808dec9bc73.jpg</url>
      <title>DEV Community: Bill Edenfield</title>
      <link>https://dev.to/baldheadbill</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/baldheadbill"/>
    <language>en</language>
    <item>
      <title>My first Hackathon - SpeedDemon 2026 by Hackathon Raptors</title>
      <dc:creator>Bill Edenfield</dc:creator>
      <pubDate>Thu, 05 Nov 2026 23:02:34 +0000</pubDate>
      <link>https://dev.to/baldheadbill/my-first-hackathon-speeddemon-2026-by-hackathon-raptors-lmc</link>
      <guid>https://dev.to/baldheadbill/my-first-hackathon-speeddemon-2026-by-hackathon-raptors-lmc</guid>
      <description>&lt;h2&gt;
  
  
  I optimised for time. Speed Demon scored me on something else.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;My Speed Demon 2026 write-up — 72 hours, four tracks, zero-crate Rust, and the three lessons that actually moved the score.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I entered &lt;a href="https://speeddemon.dev" rel="noopener noreferrer"&gt;Speed Demon 2026&lt;/a&gt;, run by &lt;strong&gt;Hackathon Raptors&lt;/strong&gt;, as team BaldheadBill (my team of me and the voices in my head). Four tracks, 72 hours (Oct 30 – Nov 2, 2026), one rule that shaped everything: Rust, standard library only, zero external crates. No shared code between track folders except by copying it in.&lt;/p&gt;

&lt;p&gt;The code is here: &lt;strong&gt;&lt;a href="https://github.com/BaldheadBill/speed-demon-2026" rel="noopener noreferrer"&gt;https://github.com/BaldheadBill/speed-demon-2026&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Final board, Nov 2:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Board time&lt;/th&gt;
&lt;th&gt;Board memory&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;K&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: Ingest&lt;/td&gt;
&lt;td&gt;0.177 s&lt;/td&gt;
&lt;td&gt;64 MB&lt;/td&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;12.477476&lt;/td&gt;
&lt;td&gt;54.806&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: Index&lt;/td&gt;
&lt;td&gt;1.409 s&lt;/td&gt;
&lt;td&gt;501 MB&lt;/td&gt;
&lt;td&gt;741&lt;/td&gt;
&lt;td&gt;6.206545&lt;/td&gt;
&lt;td&gt;39.642&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: Join&lt;/td&gt;
&lt;td&gt;0.377 s&lt;/td&gt;
&lt;td&gt;275 MB&lt;/td&gt;
&lt;td&gt;634&lt;/td&gt;
&lt;td&gt;5.031920&lt;/td&gt;
&lt;td&gt;35.087&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D: Traverse&lt;/td&gt;
&lt;td&gt;2.062 s&lt;/td&gt;
&lt;td&gt;524 MB&lt;/td&gt;
&lt;td&gt;356&lt;/td&gt;
&lt;td&gt;4.810890&lt;/td&gt;
&lt;td&gt;34.111&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Best three tracks (A, B, C) give &lt;strong&gt;42.401 points overall&lt;/strong&gt;. Final ranks: overall 103, A 111, B 95, C 102, D 107.&lt;/p&gt;

&lt;p&gt;That rank is not the story. The story is that I spent the first part of the sprint optimising the wrong thing, and the score formula kept telling me so — I just had to stop reading it as a stopwatch.  My fault entirely because in my industry, i am rather known for creating lightening fast software; call me cocky or whatever but I honestly thought i was going to flat out smoke the competition on this one.... I was SO very wrong.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; **The score is not time. It is K.**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Speed Demon's K blends time, memory, code size, and dependencies. Time carries the heaviest exponent (0.50), memory is next (0.20), and code size / dependencies are 0.15 each. Points are &lt;code&gt;50 × log10(K)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That matters because a change can win on the clock and lose on the board. Once I internalised that, most of my decisions got simpler: bank a hash-verified score early, then only push a change that beats it on K — not on one axis.&lt;/p&gt;

&lt;p&gt;And correctness is not a phase, it is the gate. A run that fails correctness, the timeout, or the sandbox scores nothing. Every version below was byte-exact against the board's expected output hash before it went anywhere near a push. Wrong output is zero, however fast it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;_ 1. The profiler reading I misread: mmap, memory, and the hot cache_&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Track A is ingest: 3.1M log lines, per-endpoint counts, p50/p95/p99 latencies, hourly error rates. Reference: 32.05 s, 228 MB. My progression on the board:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Board time&lt;/th&gt;
&lt;th&gt;Board memory&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;K&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parallel, whole file in memory&lt;/td&gt;
&lt;td&gt;0.319 s&lt;/td&gt;
&lt;td&gt;309 MB&lt;/td&gt;
&lt;td&gt;177&lt;/td&gt;
&lt;td&gt;7.941&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming ranges, per-thread buffered line reader&lt;/td&gt;
&lt;td&gt;0.259 s&lt;/td&gt;
&lt;td&gt;73 MB&lt;/td&gt;
&lt;td&gt;204&lt;/td&gt;
&lt;td&gt;11.519&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged final: 1024-shard mutex table, inline small-group latency storage, SWAR parse&lt;/td&gt;
&lt;td&gt;0.181 s&lt;/td&gt;
&lt;td&gt;67 MB&lt;/td&gt;
&lt;td&gt;468&lt;/td&gt;
&lt;td&gt;12.350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot-cache variant of merged final&lt;/td&gt;
&lt;td&gt;0.170 s&lt;/td&gt;
&lt;td&gt;68 MB&lt;/td&gt;
&lt;td&gt;539&lt;/td&gt;
&lt;td&gt;12.427&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Champion: mmap variant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.177 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;503&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.477476&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two misreads in that table:&lt;/p&gt;

&lt;p&gt;1st, the biggest single K jump was not a faster parser. Going from whole-file-in-memory to streaming ranges improved time by 19% — and cut memory from 309 MB to 73 MB. K went from 7.941 to 11.519, up 45%. I had been staring at parse speed. The binding axis was memory.&lt;br&gt;
2nd, the hot-cache variant was faster on the clock (0.170 s vs 0.181 s) and I nearly banked it as the winner. Then the mmap variant came in &lt;em&gt;slower&lt;/em&gt; than the hot cache (0.177 s) — and won on K anyway, because it was 36 lines shorter and 4 MB lighter. A 0.007 s time win lost to lines and memory. If you only watch time, you ship the wrong champion.&lt;/p&gt;

&lt;p&gt;There was yet another, dumber misread underneath the mmap decision: I expected memory-mapped pages to count toward the board's memory number. The input is 239 MB; if mmap pages counted, the total should have blown up toward ~290 MB. It did not. The board showed 64 MB total. mmap pages do not count toward the board memory number, and that settled the question — no madvise fallback, no hedging. The measurement beat my mental model of the profiler, which is exactly what a measurement is for.&lt;/p&gt;

&lt;p&gt;Lesson: profile the score, not the stopwatch. Find the binding axis first, and distrust any optimisation whose win lives entirely on the axis you happen to be watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;_ 2. The rewrite that made it slower — and I shipped it anyway_&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That mmap champion is also my "rewrite that made it slower" story, and it is the one I am glad I kept. It was slower than the hot-cache version I already had banked. In a normal sprint you do not replace a faster build with a slower one. Here, the slower build scored higher, used less memory, and was shorter. I shipped the slower program because the score beat out my pride in the stopwatch.&lt;/p&gt;

&lt;p&gt;The same discipline killed several other "improvements" that never got pushed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Track B, memory variant:&lt;/strong&gt; exact-size reads, a Bloom pre-filter, and CSR phrase assignments. Hash-verified locally. Its memory gain was about cancelled by the added lines, time was about the same, and pushing it risked a banked 39.587 points for a wash. Kept the champion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track D, +7-line variant:&lt;/strong&gt; 4.398 s → 4.314 s locally, hash-verified. Not pushed — after Track C's v2 result, D was the dropped track, so a fraction of a point on memory had no upside to the overall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track A, a compression stage:&lt;/strong&gt; I build compression algorithms (DEAD-Q is mine and published) so the temptation was real. I evaluated it and rejected it: the output must be bit-exact percentiles, latencies are only ~12 MB inside 67 MB, time is the binding axis, and compression adds CPU. Do not optimise an axis that is not binding, even if it is your favourite axis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a 72-hour competition, a banked, hash-verified score is an asset. A change that is merely "also good" is not an improvement; it is risk with no upside. That is a hard rule to follow when you are enjoying the work, and it is the rule that protected the final board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;_ 3. The change that more than halved it: parallelise the scan, don't rewrite the parser_&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Track C is a hostile-CSV join: 3.32M records, quoted fields, embedded commas and newlines, a join on &lt;code&gt;region_id&lt;/code&gt;, sorted aggregates. Reference: 14.44 s, 367 MB.&lt;/p&gt;

&lt;p&gt;Version 1 was a single-threaded byte state machine: &lt;strong&gt;1.272 s on the board, K 2.957&lt;/strong&gt;. V2 kept that exact parser: &lt;code&gt;scan_field&lt;/code&gt;, delimiter handling, integer-cents parsing, money formatting, all unchanged. This changed only how the work was split: the input is divided into chunks at record boundaries found by quote-parity, each thread runs the same state machine over its chunk, names are interned through a 64-way sharded table, and per-thread partial groups are merged at the end.&lt;/p&gt;

&lt;p&gt;Locally that went 5.445 s → 2.461 s. On the board it went &lt;strong&gt;1.272 s → 0.377 s, K 2.957 → 5.031920&lt;/strong&gt; — a 3.4× time cut, and the track that displaced D in my best three.  In retrospect, i wish i had chased Track C a little harder rather than as a last resort when i got stuck on A, B, and D.  Somewhere along the way i had made up my mind that C wasn't worth the trouble so I didn't bother with it until like hour 60ish. &lt;/p&gt;

&lt;p&gt;Track B rhymes with it. Version 1, single-threaded, ran 8.33 s locally (scan 5.89 s, BOOL 0.59 s, PHRASE 1.67 s, 22,240,530 postings). V2 parallelised the doc scan over line-aligned ranges, ran BOOL chunks in parallel, and gave PHRASE queries a parallel second scan. First board score: &lt;strong&gt;1.415 s, K 6.190689&lt;/strong&gt;; a re-run of the same commit gave 1.409 s, K 6.206545. The core insight was not a cleverer index, it was a smaller one: only terms that appear in the queries get postings at all. The reference builds a full index and stores raw text (2.29 GB); the wanted-terms-only index landed at 501 MB.&lt;/p&gt;

&lt;p&gt;The pattern across both tracks: the parser was already correct. The win was in the split — where chunk boundaries are safe, what state is per-thread, and what gets merged once at the end instead of locked on every line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;4. The spec traps were the real competition&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every track had a correctness trap that a fast-but-wrong solver walks straight into. These are the ones that cost time if you find them late:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Track A:&lt;/strong&gt; error is 5xx only. 499 is &lt;em&gt;not&lt;/em&gt; an error. My pre-sprint assumption was status ≥ 400; the sample data corrected it before kickoff. Percentiles are nearest-rank, duplicates kept, &lt;code&gt;error_ppm = floor(errors × 1,000,000 / count)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track B:&lt;/strong&gt; tokenisation is ASCII bytes only. &lt;code&gt;Zürich&lt;/code&gt; becomes &lt;code&gt;z&lt;/code&gt;, &lt;code&gt;rich&lt;/code&gt;, not &lt;code&gt;zurich&lt;/code&gt;. &lt;code&gt;007&lt;/code&gt; ≠ &lt;code&gt;7&lt;/code&gt;. BOOL is left-associative with no precedence: &lt;code&gt;a OR b AND c&lt;/code&gt; is &lt;code&gt;(a OR b) AND c&lt;/code&gt;. Get that wrong and the "optimised" BOOL evaluator is just a fast wrong answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track C:&lt;/strong&gt; a BOM is skipped only at file start or immediately after a record terminator. Quoted &lt;code&gt;""&lt;/code&gt; is one &lt;code&gt;"&lt;/code&gt;. Money is integer cents throughout, printed with exactly two decimals, and &lt;code&gt;0.00&lt;/code&gt; is never &lt;code&gt;-0.00&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track D:&lt;/strong&gt; 1,979 distinct sources over 2,000 queries, so per-source caching buys at most ~1% reuse — evaluated and rejected. A* was rejected too: the graph has no coordinates. Bidirectional Dijkstra over a CSR graph, parallel across queries with atomic work-stealing and per-thread distance stores with u16 generation stamps, was the answer. And &lt;code&gt;s != t&lt;/code&gt; can still be distance 0, via zero-weight paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four outputs were byte-exact against the expected hashes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;summary.txt&lt;/code&gt;: &lt;code&gt;495ca09682b9673ef836a506496177680bb4a3af54255174befddb1f83481cd3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;B &lt;code&gt;results.txt&lt;/code&gt;: &lt;code&gt;f86973c210228141bcb6f1f1b10188ad2a9b65f71e7cd7cf4590b38de4e80927&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;C &lt;code&gt;aggregates.txt&lt;/code&gt;: &lt;code&gt;d8088b1cd42ba1338b1345b107b55db7b96ef1f5066f3ff64fd26a8912a0f536&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;D &lt;code&gt;distances.txt&lt;/code&gt;: &lt;code&gt;a23dbdf0a3e49cc8901310a9ee1dc6a9a569a85297b6da75c2a7002cb2ed277e&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I would do differently&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three things, all cheap in hindsight:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start from K, not from time.&lt;/strong&gt; Write the score formula on a sticky note before the first line of code, and judge every variant against it. My best Track A decision, streaming ranges, looked modest on time and enormous on K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure on the board, not locally.&lt;/strong&gt; Local Docker times were for iteration only and are not comparable to board times. Same commit, re-run: Track B 1.415 s → 1.409 s, Track D 2.117 s → 2.062 s. That is box variance, and it is larger than some of the "improvements" I considered pushing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not trust an early rank.&lt;/strong&gt; On the evening of Oct 30 I was overall #41 with 41.985 points. By Nov 2 I was #103 with 42.401 points, a better score and a much worse rank, because the board filled up over the sprint weekend. Early position is mostly a measure of who has submitted yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is glamorous. It is the whole game, though: a correct program, a banked score, and the discipline to only replace it with something that beats it on the actual score.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;strong&gt;Hackathon Raptors&lt;/strong&gt; for running Speed Demon 2026.  The spec traps, the hash gates, and a scoring formula that punishes single-axis thinking are what made it worth writing about.  Now the wait is on for the releases so i can see how in the $#%&amp;amp;##&amp;amp;# blue-blazes did those teams pull off what I could not even come close to.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/BaldheadBill/speed-demon-2026" rel="noopener noreferrer"&gt;https://github.com/BaldheadBill/speed-demon-2026&lt;/a&gt;  Rust, zero crates, all four tracks, with a &lt;code&gt;PERF.md&lt;/code&gt; optimisation log in each track folder.&lt;/p&gt;

</description>
      <category>hackathonraptors</category>
      <category>beginners</category>
      <category>devchallenge</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
