Performance engineering · measured implementation · July 2026

How fast can Archivist load a large collection?

222,242 filesystem entries applied in 420 ms.

Archivist is a desktop frontend for curating, verifying and launching large video-game collections. Its catalogue compiler, storage-side Historian service, hashing pipeline and desktop backend are native Rust. Tauri provides the current embedded-browser interface, while collection processing remains in native code. The measured snapshot contained 210,840 files and 11,402 directories.

01 / the familiar desktop model

Most game-collection managers are conventional desktop applications.

Some are built with Electron, which combines a Node.js main process with Chromium renderers. Others are C# applications running as managed code on .NET. These runtimes provide mature interface libraries, broad platform support and a productive way to build a frontend.

A conventional game-collection manager usually reads its collection through ordinary filesystem interfaces and stores the result in a general-purpose database or in-memory object model. That works well for a collection on a local disk. As the library grows, every search, list and filter has more records to traverse, while box art, screenshots and videos add thousands of files to the media cache.

Degradation arrives gradually. Startup takes longer, scrolling becomes less consistent and searches lose their snap. A network filesystem adds latency to every cache miss and small-file access. Running an entire collection and its media cache from network-attached storage over a file share is aspirational, to put it lightly.

02 / Archivist’s starting point

Archivist starts with native Rust.

Archivist's catalogue compiler builds the playable catalogue and verification index. Its Historian service inventories storage, hashes files and matches them to catalogue evidence. The snapshot encoder and Tauri desktop backend are also native Rust, giving Archivist direct control over allocation, memory layout, concurrency and filesystem access. The remaining gains come from deciding where the data lives and how each consumer reads it.

A player can move from a platform timeline to a genre family tree, inspect release evidence, browse cover art and launch a game. Delayed searches or stuttering lists make a large collection feel cumbersome, however quickly the underlying query completed.

Our target is 144 frames per second on every screen. Each rendered frame gets 6.94 milliseconds. Process creation may exceed that budget during startup, but every interaction after startup must fit within it.

Archivist keeps the selected catalogue native and resident, and it loads the storage snapshot before browsing begins. The renderer creates only the rows currently on screen while every catalogue record remains immediately addressable.

03 / architecture

The Historian service runs beside the storage.

Archivist moves collection discovery from the desktop to the network-attached storage. The Historian service reads file metadata, hashes logical members inside archives and matches those digests to preservation manifests beside the storage pool. The desktop receives one compact, immutable snapshot instead of pulling every game image across the network share.

01 / storageVault + IntakeLocal reads, archive members and server-side renames.
02 / historianNative RustCombined hashes, matching, watchers and WAL state.
03 / snapshotAligned field columnsMessagePack metadata, custom structure-of-arrays columns and one name blob.
04 / desktopTauri backendOne authenticated HTTP snapshot plus WebSocket change notices.
05 / presentationTyped-array viewsRust-side gzip, one UTF-8 decode and a resident browser view.
Appliance deployment

One static musl binary

The storage service is a statically linked x86-64 executable that can be copied onto Linux NAS appliances such as TrueNAS SCALE.

Protocol split

HTTP bulk, WebSocket propagation

Bulk snapshots use one authenticated compressed response. A persistent Rust-to-Rust WebSocket carries status and generation notices without exposing historian credentials to JavaScript.

Mutable evidence

Restartable SQLite journal

A SQLite write-ahead log stores restartable digest records for each file. Hashing happens outside short batched transactions. Immutable interface snapshots are cached separately.

Snapshot semantics

The scan time is explicit

The UI reports when storage was last scanned. Changes made while the filesystem was offline appear after the next scan.

04 / frontend loading

Reducing frontend loading from 4.9 seconds to 420 milliseconds.

The earliest measured Historian snapshot reproduced the JavaScript frontend's object graph in MessagePack, including a full path in every entry. That representation repeated strings throughout the graph and spent 3.0 seconds in JavaScript decoding. We replaced named maps with tuples, full paths with parent indices and row objects with aligned field columns. The Rust backend then compressed the complete snapshot with gzip. Together, those changes cut the interval from HTTP request to completed frontend application from 4,911 to 420 milliseconds.

The optimisation sequence
RepresentationWire payloadRequest → appliedCumulative gain
Named-map MessagePack56.43 MB4,911 msbaseline
Struct tuple arrays31.96 MB1,096 ms4.48×
Parent-index MessagePack14.23 MB549 ms8.95×
Parent index + native gzip5.46 MB491 ms10.0×
Hybrid aligned SoA + gzip4.70 MB420 ms11.7×

The current request spent 51 milliseconds fetching the snapshot and 28 milliseconds crossing the Tauri boundary. Browser decoding and path reconstruction took 227 milliseconds, and the frontend finished applying the snapshot 420 milliseconds after the request began. Without restarting Archivist, that measured process reached the same applied state 1.167 seconds after process launch. A parallel Rust thread produced the backend's resident copy in 174 milliseconds without delaying the interface.

05 / resident catalogue

Compiling 2.15 million manifest rows.

Archivist’s catalogue compiler selects the strongest available file digest from each manifest row and builds the in-memory hash index used for verification. It extracts the required evidence, normalises each binary digest key and emits the resident structure used to match collection files.

503 identical manifests · wall time

Logarithmic time scale. All three tools received the same copied set of 503 compatible manifests totalling 487,201,103 bytes, although each tool produced a different output described in the comparison section.

1 s 10 s 100 s Archivist1.314 s Igir 5.3.1125.757 s OxyROMon200.337 s
Parallel manifest compiler scaling
WorkersWall timeSpeedupObservation
16.001 s1.00×single-thread reference
23.139 s1.91×near-linear
41.625 s3.69×near-linear
81.405 s4.27×memory/allocation pressure begins
161.314 s4.57×best measured result
241.557 s3.85×oversubscribed regression
321.360 s4.41×no useful gain over 16

Across the benchmark’s 1,221-manifest policy snapshot, the raw pass took 6.548 seconds. It parsed 909,779,869 bytes, produced 842,486 releases and 2,764,072 media rows, and rejected one malformed 303 MB TDC input. Bounded parallelism reduced the heavier static selection build from 383.289 to 196.547 seconds. Normal startup uses the versioned resident artefact, which memory-maps and hydrates in 0.557 seconds.

06 / format tournament

Choosing the snapshot format.

Archivist uses MessagePack for the snapshot's small, irregular header. The repetitive rows use a custom binary, padded structure-of-arrays (SoA) layout. Each field occupies a contiguous column, and the encoder pads each column boundary to its required alignment. A single zero-terminated UTF-8 blob holds the file basenames. JavaScript can create typed-array views over the numeric columns directly without allocating millions of row objects.

The format gives us control over where every bit lands. A pass reads only the columns it needs, and the next value is already beside the current one in memory. There are no per-row object headers or pointers to chase through the heap. That layout maximises cache locality and keeps the decoder in the same tight loop for every row.

The regular loop is also friendly to branch prediction. Tagged object fields, optional values and lexical token streams introduce data-dependent decisions. Fixed columns keep the hot path predictable. Our benchmark measures total decode time, so we don’t assign a separate number to branch prediction, but it is one reason we prefer the simpler layout when two formats compress to a similar size.

Isolated catalogue encoding experiment
EncodingRawgzipVerdict
JSON19.495 MiB4.526 MiBportable, much too much parsing
CBOR13.590 MiB4.504 MiBno advantage over MessagePack
Parent-index MessagePack13.567 MiB4.512 MiBgood baseline
Protobuf-style rows10.082 MiB4.053 MiBsmaller, still row-oriented
Custom varint rows9.256 MiB3.752 MiBsmallest raw row format
FlatBuffer-style aligned13.779 MiB3.458 MiBdirect access, extra structure
Custom binary padded SoA12.296 MiB3.222 MiBbest compressed + typed views

The live 4.70 MB payload is larger than the isolated 3.22 MiB codec fixture because it also carries the real roots, match projection, vault files, timestamps and status metadata.

Testing a filename dictionary

The path experiment included a dictionary for common components such as “Disk”. The top 128 tokens covered 69.4% of component occurrences. Parent indices already captured repeated directories, however, while leaf names were mostly unique. The second dictionary saved few bytes and added pointer chasing during reconstruction.

Path representation experiment
RepresentationRaw sizezstd level 1Decode observation
Original tuples30.477 MiBrepeated full paths
Parent index + basename13.567 MiB4.331 MiB~126.8 ms proxy, simplest
Full component arrays13.728 MiB4.442 MiBextra indices, no size win
Parent + name dictionary13.508 MiBalmost no raw saving
Parent + front coding12.527 MiBsmaller, more reconstruction
Parent + front coding + extension11.926 MiBmore branches and tables
Parent + lexical tokens11.591 MiB4.379 MiB~182.5 ms proxy, 44% slower

gzip already captures repeated byte strings through backreferences, and the embedded browser provides a native streaming DecompressionStream. Zstandard produced excellent sizes but would require a JavaScript or WebAssembly decoder in the current interface. gzip had the best measured request-to-applied result.

07 / disks, digests and joins

Hashing each file once.

Game-preservation catalogues commonly provide CRC32, MD5 and SHA-1 file digests: compact values used to identify exact file contents. The Historian service advances all three digest calculations during one sequential read, including for logical files inside ZIP containers.

OxyROMon-compatible 1 GiB TrueNAS microbenchmark
OperationArchivistOxyROMon 0.21.0Difference
CRC32796 MiB/s867 MiB/s−8.2%
MD5486 MiB/s376 MiB/s+29.4%
SHA-1462 MiB/s373 MiB/s+24.0%
ROM write278 MiB/s252 MiB/s+10.3%
Temporary write325 MiB/s276 MiB/s+17.9%

This was a single run on our hardware. We excluded read rates because ZFS ARC and run order materially changed them. OxyROMon recorded the faster CRC32 result. Archivist led MD5, SHA-1 and writes on this machine.

Real copied 1.77 GB file set · combined CRC32 + MD5 + SHA-1
WorkersWall timeThroughputObservation
113.928 s~121 MiB/sfirst/cold run
23.801–3.867 s436–444 MiB/sbest result
34.152 s~407 MiB/scontention begins
44.312 s~392 MiB/smore workers made it worse

Inventory took between 110 and 386 ms depending on cache state. Joining 12,297 computed digests against the resident manifest evidence took about 1.17 ms. Exact digest matching uses fixed-width binary keys in an FxHashMap. Ordered B-tree traversal offers no useful operation here.

08 / comparison

Comparing different jobs.

Our friends behind OxyROMon and Igir have built mature, capable tools, and OxyROMon is the existing state of the art for this work. The shared-corpus timings measure different outputs. Archivist builds a deduplicated resident digest index, Igir writes a report, and OxyROMon imports normalised relational records.

Exact 503-manifest shared corpus
ToolRuntimeWall timeMeasured output
Archivistnative Rust1.314 s2,151,769 rows → 1,877,162 strongest digests
Igir 5.3.1Bun binary125.757 s503-manifest report
OxyROMon 0.21.0native Rust200.337 s2,148,554 ROM rows, 1.1 GB SQLite

On the shared 503-manifest corpus, Archivist’s purpose-built digest compiler is approximately 96× faster than Igir’s report generation and 152× faster than OxyROMon’s relational import. Their additional database and reporting work explains part of the difference.

Catalogue-family compatibility observed in the copied corpus
ToolTOSECMAME software-list/listxmlRetroAchievements
Archivist
RomVault 3.7.6
Igir 5.3.1△ metadata failure
OxyROMon 0.21.0— native XML rejected△ duplicate-entry failure
clrmame 0.7.1— not batch-verified✓ documented— not batch-verified
Full 1,221-manifest selected policy
Tool/pathResultScope note
Archivist raw digest pass6.548 s2,764,072 rows, one malformed 303 MB input rejected
Archivist cold static policy196.547 s1.95× faster than baseline, normally bypassed
Archivist warm catalogue0.557 s50 MiB resident artefact
RomVault 3.7.695.253 scompleted all DAT updates
Igir 5.3.1failed after 583.919 s>14 GB RSS before MAME metadata exception
OxyROMon 0.21.0not acceptedMAME, duplicate RA and malformed inputs failed sequential attempts
clrmame 0.7.1not batch-comparedscan CLI accepts one XML per invocation
09 / current implementation

What the current build uses.

  • StorageNAS-side inventory, logical archive hashing, matching, filesystem notifications and guarded same-filesystem rename.
  • DigestsCRC32, MD5 and SHA-1 advance during one read through reusable buffers. Assembly dispatch selects available CPU instructions, and fixed-width binary digests feed the FxHashMap joins.
  • CatalogueStatic policy pruning reduces the benchmark’s 1,221 sources to 292 contributing sources, 313,914 retained releases and 1,239,634 unique strongest digests. Warm startup memory-maps the versioned 50 MiB artefact.
  • StateA SQLite write-ahead log stores restartable file evidence. Hashing stays outside short write transactions, while an immutable snapshot cache and WebSocket generations serve the frontend.
  • SnapshotMessagePack metadata, custom padded binary SoA columns, parent indices and one basename blob travel through native gzip. The browser reads typed-array views and performs one UTF-8 decode.
  • MeasurementEnd-to-end timestamps cover process creation, HTTP transfer, Tauri interprocess communication, decoding and frontend application.
10 / CPU feature audit

The TrueNAS virtual machine hides useful CPU instructions.

The physical Intel N100 processor exposes vector, carry-less multiplication and hash acceleration through instruction-set extensions including AVX2, PCLMULQDQ and SHA-NI. The TrueNAS virtual machine presents an x86-64-v2-AES virtual CPU that hides those extensions. Archivist's portable static binary already contains runtime-selected assembly implementations, but software inside a virtual machine can't select instructions that its virtual CPU doesn't advertise.

One-run CPU-feature experiment · same static binary · hot 1 GiB sparse file
Execution environmentSHA-1CRC32 + MD5 + SHA-1Status
TrueNAS virtual machine · x86-64-v2-AES465.63 MiB/s240.32 MiB/scurrent deployment
Proxmox host · full Intel N100 ISA1,767.81 MiB/s366.31 MiB/scandidate: +52% combined

The one-run host-versus-guest comparison identifies a likely opportunity, but it doesn't support a production claim. The host and virtual machine are different execution environments, and each result comes from one run. The 3.8× SHA-1 difference is consistent with SHA-NI becoming available; the 52% combined-digest difference is more representative of Archivist's workload. The next test is to expose the host CPU to the TrueNAS virtual machine during a maintenance window and rerun the controlled benchmark.

Acceleration opportunities
TechniqueCurrent stateExpected valueDecision
SHA/MD5 assembly dispatchcompiled inlarge when ISA is exposedkeep and expose host CPU flags
CRC vector/PCLMUL dispatchcrate support compiled inmoderateretest after passthrough
GPU hashingnot implementedpoor on HDD, possible on large NVMe batchesnot a priority
io_uring batched readsnot implementedsmall on HDD, plausible on SSD/NVMebenchmark behind a provider
posix_fadvise / readaheadnot implementedworkload and ZFS dependentcheap controlled experiment
WebView GPU compositorplatform-managedlimited to compositinginsufficient for retained 144 Hz goal
Native wgpu renderernot implementedlargest remaining frame-time controlproposed next architecture

The production NAS is currently bound by spinning disks and file scheduling. GPU hashing would also add a copy from host memory to the device. It becomes worth testing for large contiguous NVMe batches or when a GPU already consumes the same bytes.

11 / further experiments

Experiments worth running next.

The current measurements leave a short list of useful experiments. CPU passthrough has the clearest immediate case. Filesystem-native change detection and faster Linux I/O providers need separate HDD, SATA SSD and NVMe results before they earn a place in the production path.

  • NextExpose the N100 host instruction set to the TrueNAS virtual machine and rerun hashing, Historian startup and power measurements.
  • CandidateZFS snapshot-delta inventory, so a new scan can identify changed paths in one filesystem-native sweep.
  • CandidateLinux statx identity reuse for hard links, fs-verity digests where the filesystem can prove them, and optional digest xattrs for portable rehash avoidance.
  • Candidategetdents64/statx scanner and io_uring reader providers, with separate HDD, SATA SSD and NVMe benchmarks.
  • CandidatePatch snapshots over WebSocket for small live changes. Startup should retain the single dense snapshot. A one-file mutation shouldn’t require another full projection.
  • CandidateSame-pool reflinks and copy_file_range for moves that can’t use rename(2).
  • RejectedWholesale JSON, CBOR, protobuf rows, FlatBuffer-style rows, lexical path tokens and a global filename dictionary. Each lost on total bytes, decode time or complexity.
  • RejectedA B-tree digest index. Exact digest lookup favours the existing hash map, and this workload gains nothing from ordered traversal.
  • DeferredPortable zero-copy shared memory into the WebView. Tauri doesn’t provide a cross-platform borrowed Rust buffer to JavaScript. Custom platform bridges would still leave JavaScript object materialisation.
12 / native rendering

What a native renderer would change.

The embedded-browser path now applies the live snapshot in 420 milliseconds. Its remaining costs include Tauri interprocess communication, JavaScript decoding and object materialisation, garbage collection, document styling and layout, and a second catalogue representation.

A native renderer would keep the compressed snapshot and presentation models in one Rust process. Storage and network costs would remain the same, but Archivist would gain direct control over allocation, layout, text shaping, GPU uploads, draw submission, input latency and frame pacing.

A native-renderer experiment should begin with one vertical slice of Archivist's largest timeline or verification tree, using the existing interface as its visual contract.

  1. Freeze the visual contract.Extract colours, type scale, spacing, borders, shadows, animation curves and responsive rules into shared design tokens. Capture reference screenshots for every state and viewport.
  2. Separate presentation models from DOM models.Move sorting, grouping, selection and row projection into Rust read models backed by the existing resident SoA catalogue. Keep full data resident and virtualise only visual nodes.
  3. Build one native performance screen.Use the largest timeline or verification tree as the vertical slice. Run it beside the current screen behind a feature flag and compare frame time, memory, input latency and pixel output.
  4. Render with winit + wgpu.Use persistent GPU buffers, instanced rectangles and images, a glyph atlas with cosmic-text/swash, explicit clipping and no per-frame heap allocation in steady state.
  5. Retain and invalidate.Keep a scene graph and layout cache. Recompute only dirty subtrees, update only changed GPU ranges, and render only visible rows while every catalogue record remains immediately addressable.
  6. Preserve desktop quality.Add AccessKit accessibility, IME and clipboard support, keyboard/gamepad focus, high-DPI scaling and platform window semantics before replacing the corresponding WebView screen.
  7. Gate on measurable parity.Require perceptual screenshot parity, 144 Hz frame pacing on target hardware, bounded allocations and equal-or-better accessibility. Migrate screen by screen and keep the proven web implementation until each replacement wins.

egui is excellent, although its immediate-mode model differs from the retained renderer we want to test. Slint and Iced are credible candidates, with Slint preserving declarative layout. A custom wgpu layer offers the most direct control over Archivist’s data-heavy interface. The vertical slice must justify that additional work before we commit the whole product.

13 / measurement method

How the measurements were taken.

All results in this article are development measurements from the copied corpora and hardware described beside each table. Comparative runs used release binaries. The 2.15 million figure counts manifest media rows; the 222,242 figure counts files and directories in the Historian snapshot. Neither figure counts playable games.

Benchmark mirror

12,297 catalogue files · 1.77 GB copied locally

Archivist's selected catalogue policy contains 1,221 manifests. The exact subset accepted by Archivist, OxyROMon and Igir contains 503 files and 487,201,103 bytes.

Live storage corpus

222,242 filesystem entries

210,840 files and 11,402 directories in the measured historian snapshot. These entries aren’t all distinct playable games.

Output semantics

Purpose-built compiler output

Archivist builds the resident strongest-digest evidence it needs. Other tools may perform additional relational, reporting or repair work.

Cache effects

Reported with each result

ZFS ARC, Linux page cache and run order materially affect disk tests. Cold, warm and one-run figures are labelled where known.

The benchmark tools live with Archivist so future changes can use the same inventory, combined-hash, matching and catalogue workloads. We’ll update this page when a later implementation changes the result.

Privacy, plainly.

This site uses no analytics, advertising trackers, or cookies. It stores only your theme preference in your browser. The early-access button is currently inactive, so this site collects no contact details. When signups open, we’ll use supplied addresses only to send one email when Archivist is ready to accept early-access signups. We won’t add you to any other marketing list.

Your collection, your software.

Archivist doesn’t provide copyrighted material. It’s strictly a collection manager for users to sort, verify, view, and launch their own legally obtained game software.