← Back to list

FastDevFs

1. Introduction

Mohit Gupta · 2026-04-08 19:30 · 4 claps · 8.0 min read
#technology #filesystem #fuse #operating-systems #fastdevfs
Open on Medium ↗

FastDevFs

1. Introduction

Modern software development has a storage problem hiding in plain sight. A single Node.js project with a few dependencies can produce a node_modules directory with 30,000+ files, consuming 200–400 MB of disk. Multiply that across 10–20 active repositories on a developer workstation, and you're looking at 4–8 GB — overwhelmingly filled with identical copies of the same library code.

This is not unique to JavaScript. Python’s venv, Go's module cache, Rust's target/ directory, and PHP's vendor/ all exhibit similar redundancy. Existing solutions like pnpm's content-addressable store or symlink strategies are ecosystem-specific and fragile across toolchains.

FastDevFs is a filesystem-level solution: a FUSE-based virtual filesystem, written in C++, that intercepts all file I/O and transparently deduplicates content at both the file level (via SHA-256 hardlinking) and the folder level (via directory subtree node sharing). It operates below application code — requiring zero changes to package managers, build tools, or workflows.

This post is a deep technical walkthrough of its architecture, data structures, deduplication pipeline, and the engineering tradeoffs behind each decision.

2. System Architecture

FastDevFs consists of five major subsystems, connected via shared memory and Unix domain sockets:

Figure 1: High-level architecture of FastDevFs. The FUSE daemon runs as a single process with multiple threads. File content is stored on the host filesystem in /tmp/fastdevfs_data/, while the directory namespace is managed entirely in-memory via the ADT.

Why this architecture?

The FUSE (Filesystem in Userspace) layer lets us intercept every filesystem operation without needing kernel module development. By decoupling the directory namespace (in-memory tree) from actual file content (host-backed data files), we can manipulate metadata and sharing semantics without touching file bytes. The dedup server runs asynchronously to avoid blocking latency-sensitive FUSE operations.

3. The In-Memory Directory Tree

Why it’s needed

A virtual filesystem needs its own directory structure because it’s not backed by a real on-disk filesystem hierarchy. FastDevFs maintains an N-ary tree of 100,000 pre-allocated nodes representing every file and directory in the virtual namespace.

Data structure: First-Child, Next-Sibling representation

Each treenode contains exactly three pointers (as integer indices into a flat array):

struct treenode {
    int parent;        // Index of parent node
    int firstchild;    // Index of first child (-1 if leaf)
    int nextsibling;   // Index of next sibling (-1 if last)
    int nextfree;      // -1 if allocated; else next free node index
    bool isdeleted;    // true if in free list
    metadate metadata; // inode number, filename (char[256]), mode, size
};

Figure 2: Tree node layout with sibling linking. Every node has a fixed size regardless of the number of children, which is critical for the mmap persistence strategy.

Why first-child/next-sibling?

The alternative — storing a vector<int> children per node — would make nodes variable-sized, breaking the fixed-array layout needed for mmap persistence. With sibling pointers, every node is the same size, and the entire 100,000-node array can be serialized with a single memcpy().

Free list allocator

Instead of malloc/free, nodes are managed via an intrusive free list:

Allocation (O(1)):
    free = head.firstfree          // e.g., 5
    head.firstfree = arr[5].nextfree  // advance head
    arr[5].isdeleted = false       // mark as live

Deallocation (O(1)):
    arr[idx].nextfree = head.firstfree  // link to current head
    head.firstfree = idx               // new head
    arr[idx].isdeleted = true

No heap fragmentation, no system calls, O(1) allocation and deallocation.

Hash map for O(1) path lookups

Walking the tree from root for every path lookup would be O(depth). Instead, a statically-allocated hash map (150,000 entries, ~67% load factor) maps filename → node_index:

  • Hash function: Polynomial rolling hash (hash = hash * 131 + char), 64-bit
  • Collision resolution: Linear probing with cached hash values for fast skipping
  • Deletion: Full cluster rehashing to maintain probe chain integrity.

4. Persistence via mmap

Why it’s needed

A filesystem that forgets everything on daemon restart is not a filesystem. FastDevFs persists the entire directory tree and dedup index to disk using memory-mapped I/O.

Serialization strategy

The core insight: because all data structures use fixed-size POD types with integer indices (not pointers), the entire state can be persisted as a raw memory dump.

// Save: map a file, memcpy the struct, msync
void* mapped = mmap(NULL, sizeof(treefile_serializable),
                    PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(mapped, &treefile_data, sizeof(treefile_serializable));
msync(mapped, sizeof(treefile_serializable), MS_SYNC);

// Load: map the file, memcpy back
void* mapped = mmap(NULL, expected_size, PROT_READ, MAP_PRIVATE, fd, 0);
memcpy(&treefile_data, mapped, expected_size);

No serialization library. No format parsing. No pointer fixups. The file is a byte-for-byte image of the in-memory structure.

Why not dynamic allocation?

// This would BREAK after save/load:
struct treenode_dynamic {
    treenode* firstchild;  // Raw pointer → invalid after reload
    std::string name;      // std::string uses heap → pointer invalidated
};

// This WORKS after save/load:
struct treenode_static {
    int firstchild;        // Array index → valid at any base address
    char name[256];        // Inline data → no external pointers
};

Pointers are absolute virtual addresses — meaningless after a process restart. Integer indices are relative offsets into a known array, valid regardless of where the array is mapped.

Cost

The fixed layout consumes ~18 GB of virtual address space (100K nodes × ~180 KB each). On 64-bit systems this is not a problem — virtual address space is 128 TB, and only touched pages consume physical RAM (demand paging).

5. File-Level Deduplication

Why it’s needed

This is the core value proposition. Two projects with identical copies of lodash@4.17.21 contain the same bytes in hundreds of files. Storing them once and hardlinking the rest saves real disk space with zero user-visible impact.

Pipeline

igure 3: File-level deduplication data flow. The debounce timer prevents redundant hashing during burst writes (e.g., npm install writing thousands of files in rapid succession).

Why debounced?

During npm install, a single file might be created, written, closed, then immediately rewritten by a post-install script. Without debouncing, we'd hash and potentially hardlink a file that's about to change again — wasting CPU and potentially requiring an immediate CoW break. The 500ms debounce timer coalesces rapid mutations into a single hash computation.

Dedup Index: dual-map architecture

The DedupIndex maintains two mmap-backed maps:

  1. Forward map (content_hash → DedupEntry): open-addressing hash table with 150K slots. Maps content hashes to their canonical file index and refcount.
  2. Reverse map (tree_index → InodeHashMapping): flat array indexed by node index. Given any file, instantly retrieve its current content hash.

Both are POD structures in a single mmap’d file, persisted exactly like the directory tree.

6. Copy-on-Write (CoW) for Data Integrity

Why it’s needed

Hardlinking creates a data integrity hazard: modifying one hardlinked file modifies all files sharing that inode. Without CoW, editing a file in Project A would silently corrupt the same file in Project B.

Mechanism

CoW is triggered synchronously in the FUSE write() path — before any bytes are modified:

Critical property: The CoW break is synchronous. The FUSE write() callback blocks until the break is complete, ensuring no write ever touches shared data.

Canonical reassignment

If the file being broken was the canonical copy (the one other inodes hardlinked to), the DedupIndex reassigns canonicality to another inode sharing the same content hash. This prevents dangling references.

7. Library-Level (Folder) Deduplication

Why it’s needed

File-level dedup handles individual files, but a library like react@18.2.0 consists of hundreds of files in a directory tree. Even after file-level dedup eliminates duplicate content, each project still maintains its own tree nodes for every file in that library's subtree — consuming tree node slots (from the fixed 100K pool) and metadata memory.

Folder-level dedup goes further: it collapses entire duplicate subtrees so that multiple projects share the same directory tree nodes.

Pipeline

Figure 4: Library-level deduplication pipeline. The 3-second settlement timer waits for npm install to finish populating a directory before evaluating it.

Node sharing via dedup_link

The key operation dedup_link(target, canonical, treefile) simply sets target.firstchild = canonical.firstchild. Both folders now point to the same child chain in the tree array. No data is copied — the subtree is structurally shared.

CoW guards in the FUSE mutation paths (write, unlink, rename) call dedup_break() before modifying any node in a shared subtree, ensuring modifications create independent copies.

8. Thread Safety & Concurrency Model

Why it’s needed

FUSE dispatches filesystem operations from arbitrary threads. Without synchronization, concurrent mkdir + rm on the same parent could corrupt sibling chains, or a dedup worker modifying the DedupIndex while a FUSE thread reads it could cause data races.

Approach: coarse-grained recursive mutex

struct treefile {
    header head;
    treenode arr[100000];
    recursive_mutex mtx;  // Single lock for entire tree
};

Every tree operation acquires this mutex via lock_guard<recursive_mutex>. The recursive variant is required because operations like delete1() call hashindex() internally — both of which acquire the lock.

Why not fine-grained locking?

For a development filesystem where the bottleneck is disk I/O (not lock contention), coarse-grained locking is the right tradeoff. Correctness is non-negotiable in a filesystem.

9. The CLI Tool (fdfs)

Why it’s needed

A background FUSE daemon is opaque. Developers need observability and control: Is the daemon healthy? How much space has dedup saved? Can I force a dedup sweep? Can I change the dedup policy without unmounting?

IPC via Unix Domain Socket

The daemon spawns a lightweight listener thread on /tmp/fastdevfs_ctrl.sock. The fdfs CLI tool connects to this socket and exchanges JSON or binary-serialized structs.

# Mount management
fdfs mount /mnt/dev --foreground --log DEBUG
fdfs unmount /mnt/dev

# Monitoring
fdfs stats --json          # Dedup metrics: space saved, hardlinked files, etc.
fdfs status                # PID, uptime, queue size

# Dedup control
fdfs dedup run             # Force immediate evaluation sweep
fdfs dedup queue           # Pending items in debounce queue
fdfs dedup flush           # Drop all pending items

# Runtime configuration
fdfs config get SETTLEMENT_TIMEOUT_MS
fdfs config set POLICY LIBRARIES_ONLY

Why not a virtual file interface?

An alternative design would expose stats via a virtual file like <mountpoint>/.fdfs/stats.json. While elegant (accessible via cat and jq), it requires knowing the mountpoint and depends on the FUSE mount being responsive. The socket-based approach works even when the mount is hanging — critical for debugging hung filesystems.

10. Design Tradeoffs & Future Work

Tradeoff summary

11. Conclusion

FastDevFs demonstrates that significant storage savings can be achieved transparently at the filesystem layer, without requiring changes to application tooling. By combining FUSE, mmap-backed persistence, SHA-256 content addressing, hardlink-based deduplication, and folder-level subtree sharing, it eliminates redundant storage across development projects while maintaining full POSIX semantics and data integrity through Copy-on-Write.

The engineering philosophy throughout is simplicity over cleverness: fixed-size arrays over dynamic allocation, coarse locking over fine-grained, snapshots over journals. Each of these is a deliberate tradeoff — one that can be revisited as the system matures, but that provides a correct and maintainable foundation today.

FastDevFs is open source and available at: https://github.com/devlup-labs/FastDevFs

11. Project Team

FastDevFs is built and maintained by:

  • Mohit Gupta
  • Gokul Bansal
  • Diya Limbani

메타데이터
post_id
08899dbc2566
slug
fastdevfs-08899dbc2566
url
https://medium.com/@mannugupta2005/fastdevfs-08899dbc2566
canonical_url
https://medium.com/@mannugupta2005/fastdevfs-08899dbc2566
author_url
https://medium.com/@mannugupta2005
status
ok
fetched_at
2026-07-11 10:59:41