Skip to content

Major performance optimizations for HFS+ DMG extraction - #76

Open
statico wants to merge 6 commits into
blacktop:mainfrom
northpolesec:ian/upstream-pr
Open

Major performance optimizations for HFS+ DMG extraction#76
statico wants to merge 6 commits into
blacktop:mainfrom
northpolesec:ian/upstream-pr

Conversation

@statico

@statico statico commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

Important

This is stacked on #75

Authored with Claude Code, this PR dramatically improves the performance of HFS+ filesystem extraction from DMG files. An internal operation has gone from taking 2+ minutes to 1.83s with these changes.

Changes

  • Added LRU caching to Partition.ReadAt to avoid re-decompressing the same chunks
  • Fixed a bug in DMG.ReadAt where cache entries were never stored (cache.Add was in wrong branch)
  • Added buffer pooling (sync.Pool) for decompression buffers to reduce GC pressure
  • Removed debug logging in hot paths that caused allocations even when disabled
  • Replaced encoding/binary.Read with direct byte parsing in performance-critical paths
  • Added comprehensive benchmarks for DMG and HFS+ operations

Performance Improvements

Benchmark Before After Improvement
HFS+ Mount 10.2s / 46GB 1.5ms / 1MB 6,800x faster, 46,000x less memory
HFS+ List Files 10s / 44GB 1.6ms / 1.1MB 6,250x faster
Full Extraction 24s / 94GB 41ms / 18MB 585x faster, 5,200x less memory

Root Cause

The primary issue was Partition.ReadAt (used by HFS+ code) had no caching - every read of a B-tree node would re-decompress the same DMG chunks repeatedly. A typical HFS+ mount would decompress the same blocks thousands of times, causing massive memory churn and 46GB of allocations for a 46MB DMG.

Testing

All existing tests pass. Added new benchmarks using Fork.dmg to validate performance.

statico and others added 6 commits January 8, 2026 16:44
- dmg.go: Add LZMA compression support, fix partition offset handling
- hfsplus.go: Rewrite Files() for efficiency, add infinite loop protection
- types.go: Fix multi-extent file reading, fix String() recursion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added LRU cache field to Partition struct
- Implemented initCache() for lazy cache initialization
- Added findChunkIndex() for binary search chunk lookup
- Rewrote Partition.ReadAt to use caching and binary search
- Fixed DMG.ReadAt cache bug (cache.Add was in wrong branch)
- Added benchmark tests for performance measurement
- Added sync.Pool for bytes.Buffer output buffers
- Added sized input buffer pools (64K, 128K, 256K, 512K, 1M)
- Pre-size output buffers to avoid growth allocations
- Memory for full extraction: 64MB → 19MB (3.3x reduction)
- Removed log.Debugf in B-tree traversal (caused allocations)
- Added bytes.Reader pool for zlib decompression
- Allocations dropped from 72K to 51K
- readBTRecordAt: direct uint16 reads for keyLength and recordType
- readBTreeNodeAtOffset: direct byte buffer read for record offsets
- Allocations: 51K → 47K
- Use ReadAt for keyLength and recordType instead of SectionReader with seeks
- Cleaner code path, same performance
@blacktop

Copy link
Copy Markdown
Owner

Sorry I'm taking so long to respond 😩 this is a great PR; but with AI stuff it's hard to quickly review because it so much code, but I'm certain it's better code than I could write myself, but being the owner I owe it to my users to thoroughly review it. Just letting you know I've seen this, I think it looks great, but in need some time to real read it.

@blacktop

Copy link
Copy Markdown
Owner

please resolve merge conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants