<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://hnawc.com/">
  <title>Curio Log</title>
  <subtitle>Short notes on software, systems, and things worth remembering.</subtitle>
  <link href="https://hnawc.com/feed.xml" rel="self"/>
  <link href="https://hnawc.com/blog.html"/>
  <id>https://hnawc.com/blog.html</id>
  <updated>2026-06-16T16:03:34+08:00</updated>
  <author><name>Henry Nugraha</name></author>
  <entry>
    <title>When UI localization is only half the battle</title>
    <link href="https://hnawc.com/blog/when-ui-localization-is-only-half-the-battle.html"/>
    <id>https://hnawc.com/blog/when-ui-localization-is-only-half-the-battle.html</id>
    <published>2026-06-16T16:03:34+08:00</published>
    <updated>2026-06-16T16:03:34+08:00</updated>
    <summary>In the recent Hestia survey, many users asked me to add Chinese localization to Hestia. That's no problem with the current technology, so why not? I thought the hardest part was wiring all the hardcoded strings, and that everything would be downhill from there. Turns out I was so wrong... I first translate it from English to Indonesian, my native language. Then I approach the Chinese localization using both as references, tackling it from two angles simultaneously. It resulted in a decent local</summary>
    <content type="html">&lt;p&gt;In the recent Hestia &lt;a href="https://docs.google.com/spreadsheets/d/e/2PACX-1vR-WEv_8-Hkc5XqCNTT0R1JE9RI1LoWO67cqVrZYAyQgvgK5vtyD0nCb7iOJGxYl15cHBxCn6RkJuuA/pubhtml?gid=1243420515&amp;single=true" rel="noreferrer"&gt;survey&lt;/a&gt;, many users asked me to add Chinese localization to Hestia. That's no problem with the current technology, so why not? I thought the hardest part was wiring all the hardcoded strings, and that everything would be downhill from there. Turns out I was so wrong...&lt;/p&gt;&lt;p&gt;I first translate it from English to Indonesian, my native language. Then I approach the Chinese localization using both as references, tackling it from two angles simultaneously. It resulted in a decent localization that is neither stiff nor feels out of place.&lt;/p&gt;&lt;p&gt;Then a revelation came to me when I tried using it. All the menus, buttons, and settings are now in pinyin, except the content itself:&lt;/p&gt;&lt;figure class="kg-card kg-image-card"&gt;&lt;img src="/asset/blog/2026/06/Hestia-zh-CN.webp" class="kg-image" alt="" loading="lazy" width="1920" height="1080" srcset="/asset/blog/size/w600/2026/06/Hestia-zh-CN.webp 600w, /asset/blog/size/w1000/2026/06/Hestia-zh-CN.webp 1000w, /asset/blog/size/w1600/2026/06/Hestia-zh-CN.webp 1600w, /asset/blog/2026/06/Hestia-zh-CN.webp 1920w" sizes="(min-width: 720px) 720px"&gt;&lt;/figure&gt;&lt;p&gt;Most mods are in English, so even with a Chinese interface, they would still be staring at walls of English text. This only solves half the problem!&lt;/p&gt;&lt;p&gt;Some online games I played over a decade ago had a translation button in their chat window, it was helpful. I figured Hestia needed the same button. The problem now is, who will translate it? I explored some options, but they were either too expensive or had unacceptable translation quality.&lt;/p&gt;&lt;p&gt;I decided to build myself one with Rust + Actix and deploy it on my own server. An LLM now handles the translation, and with the right instructions, it can produce contextual translations. It's still taking 15-60s to complete a single translation, but once done, it's aggressively cached on Redis for speed and on MySQL for persistence.&lt;/p&gt;&lt;p&gt;The tricky part was in preventing abuse. The service must be publicly available, without any authentication or an API key, because Hestia's users will access the translation service from their own PCs. So I had to hardcode it to process only GameBanana mods along with a strict rate limiter.&lt;/p&gt;&lt;p&gt;While translation quality isn't the best, it's actually solid enough. Now, even I can read what mod does, regardless of what language it's written in.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Rust as a web app? Introducing an experimental Altcha server</title>
    <link href="https://hnawc.com/blog/rust-as-a-web-app-introducing-an-experimental-altcha-server.html"/>
    <id>https://hnawc.com/blog/rust-as-a-web-app-introducing-an-experimental-altcha-server.html</id>
    <published>2026-06-09T00:10:08+08:00</published>
    <updated>2026-06-11T21:51:22+08:00</updated>
    <summary>Link: https://cerberus.hnawc.com/demo</summary>
    <content type="html">&lt;p&gt;With Bun transitioning from Zig to Rust, why aren't we just using Rust directly on the web?&lt;/p&gt;&lt;p&gt;And so I did.&lt;/p&gt;&lt;p&gt;Currently in experimental phase, I deployed an &lt;a href="https://altcha.org/" rel="noreferrer"&gt;Altcha&lt;/a&gt; challenge/verify server built on Rust &amp;amp; Actix. It runs as a multi-instance binary backed by an Nginx reverse proxy, with each instance binding to its own internal port, and all communicate via Redis. I could freely set how many instances were running from a few commands, which I later simplified into a script. Nginx load-balances everything and adds several layers of abuse protection, including an additional rate limiter built into the server itself.&lt;/p&gt;&lt;h2 id="api-endpoints"&gt;API Endpoints&lt;/h2&gt;&lt;div class="kg-card kg-callout-card kg-callout-card-yellow"&gt;&lt;div class="kg-callout-text"&gt;All API endpoints are currently public. If you try this, expect rough edges. Treat it as a proof of concept for Rust on the web behind Nginx. Reach out to me for issues or suggestions!&lt;/div&gt;&lt;/div&gt;&lt;h3 id="1-configuration"&gt;&lt;strong&gt;1. Configuration&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Returns the server configuration, including all supported algorithms.&lt;/p&gt;&lt;p&gt;&lt;code&gt;GET https://cerberus.hnawc.com/config&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Response:&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-JSON"&gt;{
  "algorithms": [
    "ARGON2ID",
    "SCRYPT",
    "PBKDF2/SHA-256",
    "PBKDF2/SHA-384",
    "PBKDF2/SHA-512"
  ],
  "maxDifficulty": 0,
  "pow": "v2",
  "server": "cerberus.hnawc.com"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="2-challenge"&gt;&lt;strong&gt;2. Challenge&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Returns a signed proof-of-work challenge. The client's widget uses this to generate a solution.&lt;/p&gt;&lt;p&gt;&lt;code&gt;GET https://cerberus.hnawc.com/challenge&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Query Parameter:&lt;/sub&gt;&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;algo&lt;/code&gt; OR &lt;code&gt;algorithm&lt;/code&gt;&lt;br&gt;
Possible values are &lt;code&gt;ARGON2ID&lt;/code&gt;, &lt;code&gt;SCRYPT&lt;/code&gt;, &lt;code&gt;PBKDF2/SHA-256&lt;/code&gt;, &lt;code&gt;PBKDF2/SHA-384&lt;/code&gt;, and &lt;code&gt;PBKDF2/SHA-512&lt;/code&gt; with all case-insensitive and support common shorthand. The default algorithm is Argon2id as it is the current strongest one to fight bots, but that may change in the future as technology develops.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GET https://cerberus.hnawc.com/challenge?algo=argon2id&lt;/code&gt;&lt;br&gt;
&lt;code&gt;GET https://cerberus.hnawc.com/challenge?algo=scrypt&lt;/code&gt;&lt;br&gt;
&lt;code&gt;GET https://cerberus.hnawc.com/challenge?algo=PBKDF2/SHA-256&lt;/code&gt;&lt;br&gt;
&lt;code&gt;GET https://cerberus.hnawc.com/challenge?algo=SHA-384&lt;/code&gt;&lt;br&gt;
&lt;code&gt;GET https://cerberus.hnawc.com/challenge?algo=sha512&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Response:&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-JSON"&gt;{
  "parameters": {
    "algorithm": "ARGON2ID",
    "cost": 1,
    "expiresAt": 1781181120,
    "keyLength": 32,
    "keyPrefix": "476d20ac5f2a5361870125da64c75f65",
    "keySignature": "22f9787b3db56caa4cca8f2ac19d2bce09ef2942caa78c0272e766cbee0db046",
    "memoryCost": 131072,
    "nonce": "f45992822a154214d5e53193451aac07",
    "parallelism": 1,
    "salt": "58beee892019246b9c7d45d3f6b2520c"
  },
  "signature": "420c55a9918a77df2b133e5b12cacf2ec4075ceb74cfe691784302e9587188fc"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="3-verify"&gt;&lt;strong&gt;3. Verify&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;Submits a completed proof-of-work solution. The client's widget generates this after the challenge is solved. &lt;/p&gt;&lt;p&gt;&lt;code&gt;POST https://cerberus.hnawc.com/verify&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Example:&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-BASH"&gt;curl -X POST https://cerberus.hnawc.com/verify \
  -H "Content-Type: application/json" \
  -d '
{
  "challenge": {
    "parameters": {
      "algorithm": "ARGON2ID",
      "cost": 1,
      "expiresAt": 1781181935,
      "keyLength": 32,
      "keyPrefix": "8f58a823e137a04ebde45c7ec0a98bb1",
      "keySignature": "e32d295c093e2174d4ffd617de6d865f445b1bda95b89d2326aefc941504c048",
      "memoryCost": 131072,
      "nonce": "c7085f021f0b05342619c0be1d530dfa",
      "parallelism": 1,
      "salt": "16d9426feb30b0c969c2f8a7210dfefa"
    },
    "signature": "e22e447110e4f8e1ee43ef36a86eb5c640afec149454a991475fadedd49a7d2f"
  },
  "solution": {
    "counter": 12,
    "derivedKey": "8f58a823e137a04ebde45c7ec0a98bb1f5c1f8c5d8ed79017dce5b8a945faebc",
    "time": 2167.6
  }
}
'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Response (success):&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-JSON"&gt;{"success": true}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Response (failure):&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-JSON"&gt;{"success": false, "error": "expired"}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Possible error messages:&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;malformed_json&lt;/code&gt; = Payload body couldn't be parsed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expired&lt;/code&gt; = The challenge's &lt;code&gt;expiresAt&lt;/code&gt; is in the past.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replay&lt;/code&gt; = Identical solved nonce exists, indicating a replay-attack.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invalid_payload&lt;/code&gt; = Solution doesn't validate or payload was tampered.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-to-use"&gt;How to Use&lt;/h2&gt;&lt;p&gt;Use the new Altcha &lt;a href="https://altcha.org/docs/v2/widget-v3/" rel="noreferrer"&gt;Widget v3&lt;/a&gt; that supports POW v2 to enable Argon2id algorithm.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Example:&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;head&amp;gt;
  &amp;lt;!-- Loads Altcha --&amp;gt;
  &amp;lt;script async defer src="https://cdn.jsdelivr.net/npm/altcha@3.0.11/dist/main/altcha.min.js" type="module"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;!-- Render widget &amp;amp; fetch the challenge --&amp;gt;
  &amp;lt;altcha-widget challenge="https://cerberus.hnawc.com/challenge" workers="1"&amp;gt;&amp;lt;/altcha-widget&amp;gt;

  &amp;lt;!-- Loads Argon2id to solve the challenge --&amp;gt;
  &amp;lt;script&amp;gt;
  window.addEventListener('load', async () =&amp;gt; {
    const blob = await fetch('https://cdn.jsdelivr.net/npm/altcha@3.0.11/dist/workers/argon2id.js').then(r =&amp;gt; r.blob());
    $altcha.algorithms.set('ARGON2ID', () =&amp;gt; new Worker(URL.createObjectURL(blob)));
  });
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Demo:&lt;/sub&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;!--kg-card-begin: html--&gt;
&lt;head&gt;
  &lt;!-- Loads Altcha --&gt;
  &lt;script async defer src="https://cdn.jsdelivr.net/npm/altcha@3.0.11/dist/main/altcha.min.js" type="module"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- Render widget &amp; fetch the challenge --&gt;
  &lt;altcha-widget challenge="https://cerberus.hnawc.com/challenge" workers="1"&gt;&lt;/altcha-widget&gt;
  &lt;sup&gt;&lt;small&gt;Link: &lt;a href="https://cerberus.hnawc.com/demo" target="_blank"&gt;https://cerberus.hnawc.com/demo&lt;/a&gt;&lt;/small&gt;&lt;/sup&gt;

  &lt;!-- Loads Argon2id to solve the challenge --&gt;
  &lt;script&gt;
  window.addEventListener('load', async () =&gt; {
    const blob = await fetch('https://cdn.jsdelivr.net/npm/altcha@3.0.11/dist/workers/argon2id.js').then(r =&gt; r.blob());
    $altcha.algorithms.set('ARGON2ID', () =&gt; new Worker(URL.createObjectURL(blob)));
  });
  &lt;/script&gt;
&lt;/body&gt;
&lt;!--kg-card-end: html--&gt;
&lt;p&gt;Once solved and the user submits the form, the payload is ready to be verified to &lt;code&gt;/verify&lt;/code&gt; via a &lt;code&gt;POST&lt;/code&gt; request from your end. If &lt;code&gt;{"success": true}&lt;/code&gt;, process the form.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;sub&gt;Practical Usage:&lt;/sub&gt;&lt;/strong&gt;&lt;br&gt;
Altcha invisibly protects my own &lt;a href="https://hnawc.com/contact.html"&gt;contact&lt;/a&gt; page. It auto-loads the challenge, silently solves it, then immediately submits to &lt;code&gt;/verify&lt;/code&gt; and fetches my email address. Altcha Widget v3's &lt;code&gt;auto=onload&lt;/code&gt; and &lt;code&gt;display="invisible"&lt;/code&gt; make this frictionless setup possible without needing user interaction.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Inside the survey: Anonymous &amp; Autonomous</title>
    <link href="https://hnawc.com/blog/inside-the-survey-anonymous-autonomous.html"/>
    <id>https://hnawc.com/blog/inside-the-survey-anonymous-autonomous.html</id>
    <published>2026-06-07T17:58:20+08:00</published>
    <updated>2026-06-16T15:59:34+08:00</updated>
    <summary>With the release of Hestia version 1.5.0, the feedback survey for version 1.4.1 has officially come to a close. It was exciting to read everyone's feedback and respond to it. Survey results • Current Version: https://hestia-survey.hnawc.com/ongoing • Previous Version: https://hestia-survey.hnawc.com/previous • Version specific: 1.4.1 Whether you're curious about the backend or not, I insist on sharing how it works. The survey form is hardcoded into the app with minimal payload and transmitted</summary>
    <content type="html">&lt;p&gt;With the release of &lt;a href="https://github.com/HenryNugraha/Hestia" rel="noreferrer"&gt;Hestia&lt;/a&gt; version 1.5.0, the feedback survey for version 1.4.1 has officially come to a close. It was exciting to read everyone's feedback and respond to it.&lt;/p&gt;&lt;div class="kg-card kg-callout-card kg-callout-card-yellow"&gt;&lt;div class="kg-callout-text"&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Survey results&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;• Current Version: &lt;a href="https://hestia-survey.hnawc.com/ongoing" rel="noreferrer"&gt;https://hestia-survey.hnawc.com/ongoing&lt;/a&gt;&lt;br&gt;• Previous Version: &lt;a href="https://hestia-survey.hnawc.com/previous" rel="noreferrer"&gt;https://hestia-survey.hnawc.com/previous&lt;/a&gt;&lt;br&gt;• Version specific: &lt;a href="https://docs.google.com/spreadsheets/d/e/2PACX-1vR-WEv_8-Hkc5XqCNTT0R1JE9RI1LoWO67cqVrZYAyQgvgK5vtyD0nCb7iOJGxYl15cHBxCn6RkJuuA/pubhtml?gid=301316372&amp;single=true" rel="noreferrer"&gt;1.4.1&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;figure class="kg-card kg-image-card"&gt;&lt;img src="/asset/blog/2026/06/image-2.png" class="kg-image" alt="" loading="lazy" width="588" height="938"&gt;&lt;/figure&gt;&lt;p&gt;Whether you're curious about the backend or not, I insist on sharing how it works. The survey form is &lt;a href="https://github.com/HenryNugraha/Hestia/blob/71933349f254623969789745a471baf9b5f6efe8/src/main.rs#L46" rel="noreferrer"&gt;hardcoded&lt;/a&gt; into the app with minimal payload and transmitted anonymously to a Cloudflare worker instance, which in turn verifies integrity and rate limit before writing into the D1 database. The summary table is automatically generated in Google Sheets, which I scripted to periodically fetch new data from the database and notify me of any new submissions. That way, I stay on top of everything without needing AI to take part in this automation.&lt;/p&gt;&lt;figure class="kg-card kg-image-card"&gt;&lt;img src="/asset/blog/2026/06/image-3.png" class="kg-image" alt="" loading="lazy" width="423" height="569"&gt;&lt;/figure&gt;&lt;p&gt;p.s. Among many incoming bug reports, feature requests, and words of support, there were also some jokes thrown in:&lt;/p&gt;&lt;figure class="kg-card kg-image-card"&gt;&lt;img src="/asset/blog/2026/06/image-1.png" class="kg-image" alt="" loading="lazy" width="1038" height="484" srcset="/asset/blog/size/w600/2026/06/image-1.png 600w, /asset/blog/size/w1000/2026/06/image-1.png 1000w, /asset/blog/2026/06/image-1.png 1038w" sizes="(min-width: 720px) 720px"&gt;&lt;/figure&gt;</content>
  </entry>
  <entry>
    <title>Reading a file in chunks? Swallow it whole!</title>
    <link href="https://hnawc.com/blog/reading-a-file-in-chunks-swallow-it-whole.html"/>
    <id>https://hnawc.com/blog/reading-a-file-in-chunks-swallow-it-whole.html</id>
    <published>2026-05-30T14:48:32+08:00</published>
    <updated>2026-05-30T22:01:00+08:00</updated>
    <summary>Recently, I had to render multiple high-resolution images on a Rust + Egui stack for Hestia. The bottleneck was obviously the process being choked by decoding and rendering, but let's shift our focus elsewhere: reading the image files. For nearly 2 decades, I have been reading files by chunking them into smaller buffers. Nothing is wrong with that, really... use std::fs::File; use std::io::{self, Read}; fn main() { // Open the file let mut file = File::open("image.png").unwrap();</summary>
    <content type="html">&lt;p&gt;Recently, I had to render multiple high-resolution images on a Rust + Egui stack for &lt;a href="https://github.com/HenryNugraha/Hestia" rel="noreferrer"&gt;Hestia&lt;/a&gt;. The bottleneck was obviously the process being choked by decoding and rendering, but let's shift our focus elsewhere: reading the image files.&lt;/p&gt;&lt;p&gt;For nearly 2 decades, I have been reading files by chunking them into smaller buffers. Nothing is wrong with that, really...&lt;/p&gt;&lt;pre&gt;&lt;code class="language-rust"&gt;use std::fs::File;
use std::io::{self, Read};

fn main() {
    // Open the file
    let mut file = File::open("image.png").unwrap();

    // Collect all bytes into 'image'
    let mut image = Vec::new();

    // Prepare chunk size of 4kb
    let mut buffer = [0; 4096];

    // Loop-read operation
    loop {
        let b = file.read(&amp;amp;mut buffer).unwrap();
        if b == 0 {
          break; 
        }
        image.extend_from_slice(&amp;amp;buffer[..b]);
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this scenario, I needed to squeeze performance wherever possible. This led me to &lt;a href="https://info.varnish-software.com/blog/how-memory-maps-mmap-deliver-25x-faster-file-access-in-go" rel="noreferrer"&gt;memory mapping&lt;/a&gt;, which is also available in Rust under the &lt;a href="https://github.com/RazrFalcon/memmap2-rs" rel="noreferrer"&gt;memmap2&lt;/a&gt; crate.&lt;/p&gt;&lt;pre&gt;&lt;code class="language-rust"&gt;use std::fs::File;
use memmap2::MmapOptions;

fn main() {
    // Open the file
    let file = File::open("image.png").unwrap();

    // Map the file into memory &amp;amp; assign to 'image'
    let image = unsafe{ 
        MmapOptions::new().map(&amp;amp;file).unwrap()
    };
}
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;The block is wrapped in &lt;em&gt;unsafe&lt;/em&gt; because it contradicts Rust's memory-safe philosophy, and it won't compile unless we explicitly mark it as unsafe.&lt;/blockquote&gt;&lt;p&gt;Adopting this technique has significantly improved speed. Under the hood, it doesn't actually read the whole file &lt;em&gt;yet&lt;/em&gt;, it only registers the file's location in memory and loads chunks of the file when needed, and the OS can efficiently unload it if memory is tight. And the whole thing is done with zero-copy, avoiding wasted operations and unnecessary memory overhead. Beautiful.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>It seems I was still living in 2004...</title>
    <link href="https://hnawc.com/blog/it-seems-i-was-still-living-in-2004.html"/>
    <id>https://hnawc.com/blog/it-seems-i-was-still-living-in-2004.html</id>
    <published>2026-05-30T10:27:56+08:00</published>
    <updated>2026-05-30T22:01:07+08:00</updated>
    <summary>I was writing a small tool in Rust to scan folders recursively on my PC and generate a hash for each file, but it was too slow. Wondering if it was due to the PC limitations, I naturally went to Task Manager. That was not the actual image (I didn't take a screenshot at the time), but you get the gist... The CPU was practically idle, and this tool didn't even reach over 10% cpu usage. Did I mention that the tool is small? Very small, it only does a WalkDir function, generates a hash, and stores</summary>
    <content type="html">&lt;p&gt;I was writing a small tool in Rust to scan folders recursively on my PC and generate a hash for each file, but it was too slow. Wondering if it was due to the PC limitations, I naturally went to Task Manager.&lt;/p&gt;&lt;figure class="kg-card kg-image-card kg-card-hascaption"&gt;&lt;img src="/asset/blog/2026/05/image-2.png" class="kg-image" alt="" loading="lazy" width="562" height="370"&gt;&lt;figcaption&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Processes list via &lt;/em&gt;&lt;/i&gt;&lt;a href="https://systeminformer.sourceforge.io/" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;System Informer&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;That was not the actual image (I didn't take a screenshot at the time), but you get the gist... The CPU was practically idle, and this tool didn't even reach over 10% cpu usage.&lt;/p&gt;&lt;p&gt;Did I mention that the tool is small? Very small, it only does a WalkDir function, generates a hash, and stores it in memory.&lt;/p&gt;&lt;p&gt;Suddenly, an idea struck me: what if I fire up multiple scanners concurrently and merge the results? After a little research, apparently it was a thing! By using both &lt;a href="https://tokio.rs/" rel="noreferrer"&gt;Tokio&lt;/a&gt; for concurrent tasks and &lt;a href="https://github.com/rayon-rs/rayon" rel="noreferrer"&gt;Rayon&lt;/a&gt; for multi-core processing, I was able to achieve near-100% CPU usage and significantly improve the scanning speed.&lt;/p&gt;&lt;p&gt;Turns out I have only been using a single core, like a primitive person time-traveled from the Pentium III era in early 2000. Coming from web development, I never had to deal with such low-level stuff, and it felt great to be in control. After all, I paid for the whole PC, so it's only natural I use the whole CPU, right?&lt;/p&gt;</content>
  </entry>
</feed>
