From 4f6a5dabcf4d88d3cb2d76ec27504b643db773be Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sun, 16 Nov 2025 20:28:46 -0500 Subject: [PATCH] Add BasicType system with type-safe storage and configurable locking --- .github/workflows/benchmarks.yml | 149 +++- .github/workflows/test.yml | 33 + .gitignore | 3 + README.md | 117 ++-- Sources/MemoryMap/BasicType.swift | 436 ++++++++++++ Sources/MemoryMap/ByteStorage.swift | 230 ++++++ .../MemoryMap/KeyValueStore+Internal.swift | 378 ++++++++++ Sources/MemoryMap/KeyValueStore.swift | 553 +-------------- Sources/MemoryMap/Locks.swift | 61 ++ Sources/MemoryMap/MemoryMap.swift | 23 +- .../MemoryMapTests/BasicTypeBenchmarks.swift | 551 +++++++++++++++ Tests/MemoryMapTests/BasicTypeTests.swift | 654 ++++++++++++++++++ .../CacheLocalityBenchmark.swift | 70 ++ Tests/MemoryMapTests/CapacityComparison.swift | 102 +++ Tests/MemoryMapTests/KeyValueStoreTests.swift | 481 ++++++------- .../MixedOperationsProfile.swift | 131 ++++ Tests/MemoryMapTests/ResizingBenchmark.swift | 66 ++ Tests/MemoryMapTests/TupleStorageTests.swift | 142 ++++ 18 files changed, 3312 insertions(+), 868 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 Sources/MemoryMap/BasicType.swift create mode 100644 Sources/MemoryMap/ByteStorage.swift create mode 100644 Sources/MemoryMap/KeyValueStore+Internal.swift create mode 100644 Sources/MemoryMap/Locks.swift create mode 100644 Tests/MemoryMapTests/BasicTypeBenchmarks.swift create mode 100644 Tests/MemoryMapTests/BasicTypeTests.swift create mode 100644 Tests/MemoryMapTests/CacheLocalityBenchmark.swift create mode 100644 Tests/MemoryMapTests/CapacityComparison.swift create mode 100644 Tests/MemoryMapTests/MixedOperationsProfile.swift create mode 100644 Tests/MemoryMapTests/ResizingBenchmark.swift create mode 100644 Tests/MemoryMapTests/TupleStorageTests.swift diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 1baf51c..d189851 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -2,13 +2,15 @@ name: Benchmarks on: pull_request: - push: - branches: main permissions: contents: read pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: benchmark: runs-on: macos-latest @@ -53,12 +55,12 @@ jobs: matches = re.findall(pattern, content) # Start markdown output - output = ["# 🚀 KeyValueStore Performance Benchmarks\n"] - output.append("*Optimized with double hashing, memcmp equality, and derived hash2*\n") + output = ["# 🚀 MemoryMap Performance Benchmarks\n"] + output.append("*256-entry capacity with double hashing, `@inline(__always)` optimizations*\n") output.append(f"**Test Hardware:** {system_info}\n") # Core operations section - output.append("## Core Operations (100 ops)\n") + output.append("## Core Operations\n") output.append("| Operation | Time | Per-Op | Main Thread |") output.append("|-----------|------|--------|-------------|") @@ -67,8 +69,9 @@ jobs: "LookupHit": ("Lookup (hit)", 100), "LookupMiss": ("Lookup (miss)", 100), "Update": ("Update", 100), - "Remove": ("Remove", 200), + "Remove": ("Remove (insert+delete)", 200), "Contains": ("Contains", 100), + "MixedOperations": ("Mixed operations", 200), } for test_name, avg_time in matches: @@ -98,43 +101,42 @@ jobs: output.append(f"| {op_name} | {total_ms:.1f}ms | {per_op} | {status} |") # Load factor performance - output.append("\n## Load Factor Performance (10,000 lookups)\n") - output.append("| Load % | Time | Degradation | Status |") - output.append("|--------|------|-------------|--------|") + output.append("\n## Load Factor Performance\n") + output.append("| Load % | Lookups | Time | Per-Lookup | Status |") + output.append("|--------|---------|------|------------|--------|") load_factors = { - "LoadFactor25Percent": ("25%", None), - "LoadFactor50Percent": ("50%", None), - "LoadFactor75Percent": ("75%", None), - "LoadFactor90Percent": ("90%", None), - "LoadFactor99Percent": ("99%", None), + "LoadFactor25Percent": ("25%", 64, 6400), + "LoadFactor50Percent": ("50%", 128, 12800), + "LoadFactor75Percent": ("75%", 192, 19200), + "LoadFactor90Percent": ("90%", 230, 23000), + "LoadFactor99Percent": ("99%", 253, 25300), } - baseline = None + baseline_per_lookup = None for test_name, avg_time in matches: if test_name in load_factors: avg_time_f = float(avg_time) - load_name = load_factors[test_name][0] - - if baseline is None: - baseline = avg_time_f - degradation = "baseline" - else: - ratio = avg_time_f / baseline - degradation = f"{ratio:.1f}x" + load_name, keys, lookups = load_factors[test_name] total_ms = avg_time_f * 1000 + per_lookup_us = (avg_time_f * 1_000_000) / lookups - if avg_time_f < 0.050: + if baseline_per_lookup is None: + baseline_per_lookup = per_lookup_us + + # Status based on per-lookup time + if per_lookup_us < 15: status = "✅ Excellent" - elif avg_time_f < 0.100: + elif per_lookup_us < 30: status = "✅ Good" - elif avg_time_f < 0.150: + elif per_lookup_us < 50: status = "⚠️ OK" else: status = "❌ Slow" - output.append(f"| {load_name} | {total_ms:.0f}ms | {degradation} | {status} |") + lookups_str = f"{lookups:,}" + output.append(f"| {load_name} | {lookups_str} | {total_ms:.0f}ms | {per_lookup_us:.1f} μs | {status} |") # Key length impact output.append("\n## Key Length Impact (100 ops)\n") @@ -157,13 +159,100 @@ jobs: output.append(f"| {key_name} | {total_ms:.1f}ms | {per_op_us:.1f} μs |") - # Main thread budget - output.append("\n## Main Thread Guidelines") + # Bulk operations + output.append("\n## Bulk Operations\n") + output.append("| Operation | Time | Description |") + output.append("|-----------|------|-------------|") + + bulk_ops = { + "Count": ("Count (100 entries)", None), + "Keys": ("Keys iteration (100 entries)", None), + "ToDictionary": ("Convert to Dictionary (100 entries)", None), + "RemoveAll": ("Remove all entries", None), + "LargeBatchWrite": ("Large batch write", None), + } + + for test_name, avg_time in matches: + if test_name in bulk_ops: + avg_time_f = float(avg_time) + op_name = bulk_ops[test_name][0] + total_ms = avg_time_f * 1000 + + if total_ms < 10: + status = "✅ Excellent" + elif total_ms < 50: + status = "✅ Good" + elif total_ms < 100: + status = "⚠️ OK" + else: + status = "❌ Review" + + output.append(f"| {op_name} | {total_ms:.1f}ms | {status} |") + + # Stress tests + output.append("\n## Stress & Edge Cases\n") + output.append("| Test | Time | Status |") + output.append("|------|------|--------|") + + stress_tests = { + "WorstCaseProbeChain": "Worst-case probe chain", + "ManyTombstones": "Many tombstones", + "SequentialVsRandom": "Sequential vs random access", + "RandomAccess": "Random access pattern", + } + + for test_name, avg_time in matches: + if test_name in stress_tests: + avg_time_f = float(avg_time) + test_desc = stress_tests[test_name] + total_ms = avg_time_f * 1000 + + if total_ms < 50: + status = "✅ Good" + elif total_ms < 100: + status = "⚠️ OK" + else: + status = "❌ Slow" + + output.append(f"| {test_desc} | {total_ms:.0f}ms | {status} |") + + # Persistence operations + output.append("\n## Persistence\n") + output.append("| Operation | Time | Status |") + output.append("|-----------|------|--------|") + + persistence_ops = { + "WriteCloseReopen": "Write, close, reopen", + } + + for test_name, avg_time in matches: + if test_name in persistence_ops: + avg_time_f = float(avg_time) + op_name = persistence_ops[test_name] + total_ms = avg_time_f * 1000 + + if total_ms < 50: + status = "✅ Excellent" + elif total_ms < 100: + status = "✅ Good" + else: + status = "⚠️ OK" + + output.append(f"| {op_name} | {total_ms:.0f}ms | {status} |") + + # Main thread budget and capacity info + output.append("\n## Performance Characteristics") + output.append("### Main Thread Budget") output.append("- ✅ **Excellent**: <10ms - Perfect for UI interactions") output.append("- ✅ **Good**: 10-50ms - Acceptable for most operations") output.append("- ⚠️ **OK**: 50-100ms - Use with caution on main thread") output.append("- ❌ **Review**: >100ms - Consider background thread") - output.append("\n*Target: 16.67ms per frame @ 60fps, 8.33ms @ 120fps*") + output.append("\n*Target: 16.67ms/frame @ 60fps, 8.33ms/frame @ 120fps*") + output.append("\n### Capacity & Optimization") + output.append("- **Fixed capacity**: 256 entries") + output.append("- **Recommended usage**: ≤200 keys for optimal performance") + output.append("- **Memory footprint**: ~306KB per store") + output.append("- **Key optimizations**: Double hashing, `@inline(__always)`, direct buffer access via `withUnsafeBytes`") # Summary total_tests = len(re.findall(r"Test Case.*passed", content)) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b07916f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + workflow_call: {} + workflow_dispatch: {} + pull_request: {} + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: ${{ matrix.platform }} + runs-on: macos-latest + + strategy: + fail-fast: false + matrix: + platform: [macOS, iOS, tvOS, watchOS, visionOS] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build and Test + uses: mxcl/xcodebuild@v3 + with: + platform: ${{ matrix.platform }} + action: test diff --git a/.gitignore b/.gitignore index 3cf4821..fa967b0 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ fastlane/screenshots/**/*.png fastlane/test_output .DS_Store .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +PERFORMANCE_OPTIMIZATIONS.md +.claude/settings.local.json +CLAUDE.md diff --git a/README.md b/README.md index c7c3a97..517d3a9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Welcome to MemoryMap! 🚀 +# MemoryMap [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT) [![Swift](https://img.shields.io/badge/Swift-6.0-orange.svg)](https://swift.org/) [![Platform](https://img.shields.io/badge/platform-iOS%2016%2B%20%7C%20macOS%2013%2B%20%7C%20tvOS%2016%2B%20%7C%20watchOS%209%2B%20%7C%20visionOS%202%2B-lightgrey.svg)]() -MemoryMap is a Swift utility class designed for efficient persistence and crash-resilient storage of Plain Old Data (POD) structs using memory-mapped files. It provides thread-safe access to the stored data, ensuring integrity and performance for applications requiring low-latency storage solutions. +A Swift library for crash-resilient, memory-mapped file storage. Provides direct file-backed persistence with thread-safe access and microsecond-latency operations. ## Requirements @@ -14,13 +14,12 @@ MemoryMap is a Swift utility class designed for efficient persistence and crash- ## Features -- **Memory-mapped file support**: Back a POD struct with a memory-mapped file for direct memory access -- **Thread-safe access**: Read and write operations protected by OSAllocatedUnfairLock for optimal performance -- **Crash resilience**: Changes immediately reflected in the memory-mapped file -- **Data integrity validation**: File validation using magic numbers -- **KeyValueStore**: High-performance hash table with Dictionary-like API (10-20μs per operation) -- **Main-thread safe**: All operations optimized for UI thread usage -- **Double hashing**: Eliminates clustering for consistent performance even at high load factors +- **Direct file-backed storage**: Memory-mapped POD structs with crash resilience +- **Thread-safe**: Configurable locking (OSAllocatedUnfairLock, NoLock, NSLock, or custom) +- **KeyValueStore**: 256-entry hash table with Dictionary-like API +- **Fast lookups**: ~11-12μs per operation, consistent at all load factors +- **Type-safe**: BasicType wrappers for Int, String, Double, Bool +- **Main-thread safe**: Microsecond operations suitable for UI thread ## Installation @@ -50,13 +49,43 @@ let memoryMap = try MemoryMap(fileURL: fileURL) memoryMap.get.counter = 42 memoryMap.get.flag = true -// Closure-based access with lock +// Batch operations with closure memoryMap.withLockedStorage { storage in storage.counter += 1 storage.flag = !storage.flag } ``` +### BasicType Sizes + +MemoryMap provides three size variants for type-safe storage: + +- **BasicType8** (alias: `BasicTypeNumber`): 10 bytes total + - Optimized for numeric types (Int, UInt, Double, Float, Bool) + - Minimal memory footprint for compact keys or values + +- **BasicType64**: 62 bytes total + - Suitable for short strings (~60 characters) + - Good balance between size and flexibility + +- **BasicType1024**: 1022 bytes total + - For longer strings (~1000 characters) + - Use when you need to store larger text values + +```swift +// Compact numeric storage +let numericStore = try KeyValueStore(fileURL: url) +numericStore[12345] = userData // Integer literal works directly + +// General purpose with short strings +let generalStore = try KeyValueStore(fileURL: url) +generalStore["user:123"] = 42 // String and integer literals work + +// Large string storage +let largeStore = try KeyValueStore(fileURL: url) +largeStore["config"] = "very long configuration string..." +``` + ### KeyValueStore - High-Performance Hash Table ```swift @@ -67,59 +96,63 @@ struct UserData { var loginCount: Int } -let store = try KeyValueStore(fileURL: fileURL) +let fileURL = URL(fileURLWithPath: "/path/to/store.map") +let store = try KeyValueStore(fileURL: fileURL) -// Dictionary-like subscript access +// Set values using dictionary-like subscript store["user:123"] = UserData(lastSeen: Date().timeIntervalSince1970, loginCount: 1) // Read with default value let data = store["user:456", default: UserData(lastSeen: 0, loginCount: 0)] -// Explicit error handling -try store.setValue( - UserData(lastSeen: Date().timeIntervalSince1970, loginCount: 5), - for: "user:123" -) - -// Iterate over keys +// Iterate over all entries for key in store.keys { if let value = store[key] { print("\(key): \(value.loginCount) logins") } } -// Compact to remove tombstones and improve performance +// Remove tombstones to improve performance store.compact() - -// Convert to Dictionary for advanced operations -let dict = store.dictionaryRepresentation() ``` -## Performance +### Advanced: Custom Locks -KeyValueStore uses **double hashing** with optimized comparisons for excellent main-thread performance: +Both MemoryMap and KeyValueStore support custom lock implementations for specialized use cases: -**Test Hardware:** Apple M3, 24 GB RAM +```swift +// Single-threaded (no locking overhead) +let store = try KeyValueStore(fileURL: url, lock: NoLock()) -| Operation | Time (100 ops) | Per-Op | Main Thread | -|-----------|----------------|--------|-------------| -| Insert | 1.0ms | 10 μs | ✅ Excellent | -| Lookup (hit) | 1.0ms | 10 μs | ✅ Excellent | -| Lookup (miss) | 2.0ms | 20 μs | ✅ Excellent | -| Update | 2.0ms | 20 μs | ✅ Excellent | -| Remove | 3.0ms | 15 μs | ✅ Excellent | +// Use NSLock instead of OSAllocatedUnfairLock +let store = try KeyValueStore(fileURL: url, lock: NSLock()) -**Load Factor Performance:** -- 25% load: 15ms (baseline) -- 50% load: 31ms (2.1x) -- 75% load: 53ms (3.5x) -- 99% load: 97ms (6.5x) ⚠️ +// Default (OSAllocatedUnfairLock) +let store = try KeyValueStore(fileURL: url) +``` -**Main Thread Budget:** -- 60fps: 16.67ms per frame -- 120fps: 8.33ms per frame +## Performance -All operations are well within budget for smooth UI performance. Even at 99% capacity, performance remains acceptable for main thread usage. +*Benchmarks measured on Apple M3, 24 GB RAM, macOS 15.1* + +**KeyValueStore:** +- **Lookup speed:** ~11-12μs per lookup, consistent across all load factors (25%-99%) +- **Insert/Update:** ~10-20μs per operation +- **Capacity:** 256 entries, recommended ≤200 keys for optimal performance (~78% load) +- **Memory footprint:** ~306 KB per store (BasicType64 + BasicType1024) +- **Main thread safe:** Well within 60fps budget (<2.5ms for 200 lookups) +- **Load factor performance:** Remains consistent even at 99% capacity due to efficient double hashing + +**Load Factor Benchmarks (10,000+ lookups):** +- 25% load (64 keys): ~11.4μs per lookup +- 50% load (128 keys): ~11.6μs per lookup +- 75% load (192 keys): ~11.6μs per lookup +- 90% load (230 keys): ~11.6μs per lookup +- 99% load (253 keys): ~12.1μs per lookup + +**BasicType operations:** +- Int/Double/Bool: Sub-microsecond +- Hashing/equality checks: ~0.5μs ## Development diff --git a/Sources/MemoryMap/BasicType.swift b/Sources/MemoryMap/BasicType.swift new file mode 100644 index 0000000..5903c7c --- /dev/null +++ b/Sources/MemoryMap/BasicType.swift @@ -0,0 +1,436 @@ +/// MIT License +/// +/// Copyright (c) 2025 Alexander Cohen +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. + +import Foundation + +/// Protocol conformance required for types that can be used as keys in KeyValueStore. +/// +/// Combines Hashable, Sendable, and custom equality/hashing protocols for efficient +/// memory-mapped storage with double hashing. +public typealias BasicTypeCompliance = Hashable & Sendable & _BasicTypeEquatable & _BasicTypeHashable + +/// A type-safe wrapper for storing basic Swift types with 1020 bytes of storage. +/// +/// Use this for keys or values that need to store longer strings (up to ~1000 characters). +/// Size: ~1023 bytes (1020 bytes storage + 3 bytes metadata). Stride may be slightly larger due to alignment. +public typealias BasicType1024 = _BasicType + +/// A type-safe wrapper for storing basic Swift types with 60 bytes of storage. +/// +/// Use this for keys or values with shorter strings (up to ~60 characters). +/// Size: ~63 bytes (60 bytes storage + 3 bytes metadata). Stride may be slightly larger due to alignment. +public typealias BasicType64 = _BasicType + +/// A type-safe wrapper for storing basic Swift types with 8 bytes of storage. +/// +/// Use this for compact keys or values with numeric types. +/// Size: ~11 bytes (8 bytes storage + 3 bytes metadata). Stride may be slightly larger due to alignment. +/// Suitable for Int, UInt, Double, Float, Bool and their sized variants. +public typealias BasicType8 = _BasicType +public typealias BasicTypeNumber = BasicType8 + +/// A type-safe container for basic Swift types optimized for memory-mapped storage. +/// +/// Stores Int, UInt, Double, Float, Bool, and String values in a fixed-size buffer. +/// Designed to be a POD (Plain Old Data) type for safe persistence in memory-mapped files. +/// +/// ## Supported Types +/// - Integers: Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64 +/// - Floating point: Double, Float +/// - Boolean: Bool +/// - String: UTF-8 encoded, length limited by storage size +/// +/// ## Example +/// ```swift +/// let key = BasicType64("user:123") +/// let value = BasicType64(42) +/// store[key] = myData +/// ``` +@frozen +public struct _BasicType: BasicTypeCompliance { + public typealias StorageInfo = Storage + + /// The type of value stored in this BasicType instance. + public enum Kind: UInt8, Sendable { + case int + case int8 + case int16 + case int32 + case int64 + case uint + case uint8 + case uint16 + case uint32 + case uint64 + case double + case float + case bool + case string + } + + /// The type of value stored + public let kind: Kind + + /// The number of bytes used in storage + public let length: UInt16 + + /// Raw storage buffer + let value: StorageInfo.Storage + + public init(kind: Kind, length: UInt16, value: StorageInfo.Storage) { + self.kind = kind + self.length = length + self.value = value + } +} + +// MARK: - Initializers + +public extension _BasicType { + @inline(__always) + init(_ integer: Int) { + self.init( + kind: .int, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: Int8) { + self.init( + kind: .int8, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: Int16) { + self.init( + kind: .int16, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: Int32) { + self.init( + kind: .int32, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: Int64) { + self.init( + kind: .int64, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: UInt) { + self.init( + kind: .uint, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: UInt8) { + self.init( + kind: .uint8, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: UInt16) { + self.init( + kind: .uint16, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: UInt32) { + self.init( + kind: .uint32, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ integer: UInt64) { + self.init( + kind: .uint64, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(integer) + ) + } + + @inline(__always) + init(_ double: Double) { + self.init( + kind: .double, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(double) + ) + } + + @inline(__always) + init(_ float: Float) { + self.init( + kind: .float, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(float) + ) + } + + @inline(__always) + init(_ bool: Bool) { + self.init( + kind: .bool, + length: UInt16(MemoryLayout.stride), + value: StorageInfo.store(bool) + ) + } + + @inline(__always) + init(throwing string: String) throws { + let info = StorageInfo.store(string) + guard info.fits else { + throw KeyValueStoreError.tooLarge + } + self.init( + kind: .string, + length: UInt16(info.length), + value: info.storage + ) + } + + @inline(__always) + init(_ string: String) { + let info = StorageInfo.store(string) + self.init( + kind: .string, + length: UInt16(info.length), + value: info.storage + ) + } +} + +// MARK: - ExpressibleBy ... + +extension _BasicType: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value) + } +} + +extension _BasicType: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self.init(value) + } +} + +extension _BasicType: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self.init(value) + } +} + +extension _BasicType: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self.init(value) + } +} + +// MARK: - Value Extraction + +public extension _BasicType { + /// Extracts the value as an Int. + /// + /// - Returns: The stored Int value, or `0` if this instance doesn't contain an Int + @inline(__always) + var intValue: Int { + guard kind == .int else { return 0 } + return StorageInfo.extract(Int.self, from: value) + } + + @inline(__always) + var int8Value: Int8 { + guard kind == .int8 else { return 0 } + return StorageInfo.extract(Int8.self, from: value) + } + + @inline(__always) + var int16Value: Int16 { + guard kind == .int16 else { return 0 } + return StorageInfo.extract(Int16.self, from: value) + } + + @inline(__always) + var int32Value: Int32 { + guard kind == .int32 else { return 0 } + return StorageInfo.extract(Int32.self, from: value) + } + + @inline(__always) + var int64Value: Int64 { + guard kind == .int64 else { return 0 } + return StorageInfo.extract(Int64.self, from: value) + } + + @inline(__always) + var uintValue: UInt { + guard kind == .uint else { return 0 } + return StorageInfo.extract(UInt.self, from: value) + } + + @inline(__always) + var uint8Value: UInt8 { + guard kind == .uint8 else { return 0 } + return StorageInfo.extract(UInt8.self, from: value) + } + + @inline(__always) + var uint16Value: UInt16 { + guard kind == .uint16 else { return 0 } + return StorageInfo.extract(UInt16.self, from: value) + } + + @inline(__always) + var uint32Value: UInt32 { + guard kind == .uint32 else { return 0 } + return StorageInfo.extract(UInt32.self, from: value) + } + + @inline(__always) + var uint64Value: UInt64 { + guard kind == .uint64 else { return 0 } + return StorageInfo.extract(UInt64.self, from: value) + } + + @inline(__always) + var doubleValue: Double { + guard kind == .double else { return 0.0 } + return StorageInfo.extract(Double.self, from: value) + } + + @inline(__always) + var floatValue: Float { + guard kind == .float else { return 0.0 } + return StorageInfo.extract(Float.self, from: value) + } + + @inline(__always) + var boolValue: Bool { + guard kind == .bool else { return false } + return StorageInfo.extract(Bool.self, from: value) + } + + /// Extracts the value as a String. + /// + /// - Returns: The stored String value, or an empty string if this instance doesn't contain a String + @inline(__always) + var stringValue: String { + guard kind == .string else { return "" } + return StorageInfo.extractString(from: value, length: length) + } +} + +/// Protocol for BasicType equality comparison optimized for memory-mapped storage. +public protocol _BasicTypeEquatable: Equatable {} + +/// Protocol for BasicType hash computation supporting double hashing. +public protocol _BasicTypeHashable { + /// Computes two hash values for double hashing collision resolution. + /// + /// - Returns: A tuple of (hash1, hash2) where hash2 is derived from hash1 and guaranteed to be odd + func hashes() -> (Int, Int) +} + +// MARK: - Compliance + +public extension _BasicType { + static func == (lhs: _BasicType, rhs: _BasicType) -> Bool { + guard lhs.length == rhs.length, lhs.kind == rhs.kind else { + return false + } + // Optimize: use withUnsafeBytes once instead of per-byte subscript calls + return withUnsafeBytes(of: lhs.value) { lhsPtr in + withUnsafeBytes(of: rhs.value) { rhsPtr in + let length = Int(lhs.length) + return memcmp(lhsPtr.baseAddress, rhsPtr.baseAddress, length) == 0 + } + } + } +} + +extension _BasicType { + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: value) { ptr in + hasher.combine(bytes: ptr) + } + } + + public func hashes() -> (Int, Int) { + let hash1 = _hash1() + let hash2 = _hash2(from: hash1) + return (hash1, hash2) + } + + private func _hash1() -> Int { + // FNV-1a hash algorithm (better distribution than djb2) + // Optimize: use withUnsafeBytes once instead of per-byte subscript calls + withUnsafeBytes(of: value) { ptr in + let buffer = ptr.bindMemory(to: UInt8.self) + var hash: UInt64 = 14_695_981_039_346_656_037 // FNV offset basis + let length = Int(length) + for i in 0 ..< length { + hash ^= UInt64(buffer[i]) + hash = hash &* 1_099_511_628_211 // FNV prime + } + // Ensure non-negative result + return Int(hash & UInt64(Int.max)) + } + } + + /// Derive second hash from first hash (much faster than computing separately) + /// Use bit mixing to create independent distribution + /// Must be odd (coprime with capacity=256) and never zero + private func _hash2(from h1: Int) -> Int { + // Mix bits: rotate and XOR to decorrelate from hash1 + let mixed = ((h1 >> 17) ^ (h1 << 15)) &+ (h1 >> 7) + // Ensure odd by setting lowest bit + return mixed | 1 + } +} diff --git a/Sources/MemoryMap/ByteStorage.swift b/Sources/MemoryMap/ByteStorage.swift new file mode 100644 index 0000000..d430c7a --- /dev/null +++ b/Sources/MemoryMap/ByteStorage.swift @@ -0,0 +1,230 @@ +/// MIT License +/// +/// Copyright (c) 2025 Alexander Cohen +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. + +import Foundation + +/// Protocol for fixed-size byte storage using tuples for POD compliance. +/// +/// Provides a way to create fixed-size storage buffers that remain POD (Plain Old Data) types, +/// which is required for safe memory-mapped file storage. Uses tuples instead of arrays to +/// maintain trivial type guarantees. +/// +/// You typically won't implement this yourself - use `ByteStorage60` or `ByteStorage1020` instead. +public protocol ByteStorage { + /// The underlying storage type (a tuple of Int8 values) + associatedtype Storage: Sendable + + /// The storage capacity in bytes + static var capacity: Int { get } + + /// Creates a new zero-initialized storage instance + static func make() -> Storage +} + +public extension ByteStorage { + /// Capacity of the storage in bytes + @inline(__always) + static var capacity: Int { + MemoryLayout.size + } + + @inline(__always) + static func make() -> Storage { + withUnsafeTemporaryAllocation(of: UInt8.self, capacity: capacity) { buffer in + buffer.initialize(repeating: 0) + return UnsafeRawPointer(buffer.baseAddress!).loadUnaligned(as: Storage.self) + } + } + + // MARK: - Generic Storage Methods + + /// Stores a fixed-size value in storage + @inline(__always) + static func store(_ value: T) -> Storage { + precondition( + MemoryLayout.size <= capacity, + "Value too large for \(Storage.self) (capacity: \(capacity))" + ) + var storage = make() + withUnsafeMutableBytes(of: &storage) { ptr in + ptr.storeBytes(of: value, as: T.self) + } + return storage + } + + /// Extracts a fixed-size value from storage + @inline(__always) + static func extract(_: T.Type = T.self, from storage: Storage) -> T { + precondition( + MemoryLayout.size <= capacity, + "Type \(T.self):\(MemoryLayout.size) too large for \(Storage.self):\(capacity)" + ) + return withUnsafePointer(to: storage) { ptr in + UnsafeRawPointer(ptr).loadUnaligned(as: T.self) + } + } + + // MARK: - String Storage Methods + + /// Stores a String in storage with length prefix + @inline(__always) + static func store(_ string: String) -> (storage: Storage, length: UInt16, fits: Bool) { + var storage = make() + + var fits = true + var written = 0 + withUnsafeMutableBytes(of: &storage) { ptr in + let buffer = ptr.bindMemory(to: UInt8.self) + + // Start writing after the length prefix + for scalar in string.unicodeScalars { + let utf8 = UTF8.encode(scalar)! + let count = utf8.count + + // Only write if entire code point fits + guard written + count <= capacity else { + fits = false + break + } + + for byte in utf8 { + buffer[written] = byte + written += 1 + } + } + } + + return (storage, UInt16(written), fits) + } + + /// Extracts a String from storage + @inline(__always) + static func extractString(from storage: Storage, length: UInt16) -> String { + withUnsafeBytes(of: storage) { ptr in + let length = min(Int(length), capacity) + let bytes = UnsafeBufferPointer( + start: ptr.baseAddress?.assumingMemoryBound(to: UInt8.self), + count: length + ) + return String(decoding: bytes, as: UTF8.self) + } + } +} + +// MARK: - Storage Size Constants + +/// 8-byte fixed storage using a tuple-based buffer. +/// +/// Provides minimal storage suitable for numeric types only (Int, UInt, Double, Float, etc.). +/// The largest numeric types (Int64, UInt64, Double) require exactly 8 bytes. +/// With 3 bytes of metadata in BasicType, total size is ~11 bytes (stride may be larger due to alignment). +public struct ByteStorage8: ByteStorage { + public typealias Storage = ( + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 + ) +} + +/// 60-byte fixed storage using a tuple-based buffer. +/// +/// Provides compact storage suitable for short strings (~60 characters) or small data. +/// Used by `BasicType64` which adds 3 bytes of metadata (size: ~63 bytes, stride may be larger). +public struct ByteStorage60: ByteStorage { + public typealias Storage = ( + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 + ) +} + +/// 1020-byte fixed storage using a tuple-based buffer. +/// +/// Provides large storage suitable for longer strings (~1000 characters) or larger data. +/// Used by `BasicType1024` which adds 3 bytes of metadata (size: ~1023 bytes, stride may be larger). +public struct ByteStorage1020: ByteStorage { + public typealias Storage = ( + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, + Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 + ) +} diff --git a/Sources/MemoryMap/KeyValueStore+Internal.swift b/Sources/MemoryMap/KeyValueStore+Internal.swift new file mode 100644 index 0000000..15d8441 --- /dev/null +++ b/Sources/MemoryMap/KeyValueStore+Internal.swift @@ -0,0 +1,378 @@ +/// MIT License +/// +/// Copyright (c) 2025 Alexander Cohen +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. + +import Foundation +import os + +/// Default capacity for the key-value store +let KeyValueStoreDefaultCapacity = 256 + +// MARK: - Private KeyValueStore + +extension KeyValueStore { + @inline(__always) + func forEachOccupiedEntry( + in storage: inout KeyValueStoreStorage, + _ body: (Key, Value) -> Void + ) { + // Access raw entry buffer once to avoid repeated subscript overhead + storage.entries.withUnsafeEntries { entries, count in + for i in 0 ..< count { + let entry = entries[i] + if entry.state == .occupied { + body(entry.key, entry.value) + } + } + } + } + + @inline(__always) + func probeSlot(for key: Key, in storage: inout KeyValueStoreStorage) -> ProbeResult { + storage.entries.withUnsafeEntries { entries, count in + + let hashes = key.hashes() + let hash1 = hashes.0 + let step = hashes.1 + let startIndex = hash1 & (count - 1) + var index = startIndex + var probeCount = 0 + var firstTombstoneIndex: Int? + + while probeCount < count { + let entry = entries[index] + + switch entry.state { + case .occupied: + if entry.key == key { + return .found(index) + } + // Robin Hood: check if our probe distance exceeds the resident's + // If so, this is a good insertion point (we'd swap in actual insertion) + case .tombstone: + if firstTombstoneIndex == nil { + firstTombstoneIndex = index + } + case .empty: + return .available(firstTombstoneIndex ?? index) + } + + // Double hashing: increment by step (eliminates multiplication) + probeCount += 1 + index = (index &+ step) & (count - 1) + } + + if let tombstoneIndex = firstTombstoneIndex { + return .available(tombstoneIndex) + } + + return .full + } + } + + func _setValue(_ value: Value?, for key: Key) throws { + try memoryMap.withLockedStorage { storage in + switch self.probeSlot(for: key, in: &storage) { + case let .found(index): + storage.entries.withUnsafeMutableEntries { entries, _ in + if let value { + entries[index].value = value + } else { + entries[index].state = .tombstone + } + } + case let .available(index): + if let value { + storage.entries.withUnsafeMutableEntries { entries, _ in + entries[index] = KeyValueEntry( + key: key, + value: value, + state: .occupied + ) + } + } + case .full: + if value != nil { + throw KeyValueStoreError.storeFull + } + } + } + } + + func _value(for key: Key) -> Value? { + memoryMap.withLockedStorage { storage in + guard case let .found(index) = self.probeSlot(for: key, in: &storage) else { + return nil + } + return storage.entries.withUnsafeEntries { entries, _ in + entries[index].value + } + } + } + + func compactInternal(storage: inout KeyValueStoreStorage) { + // Collect all active entries + var activeEntries: [(key: Key, value: Value)] = [] + + // Access raw entry buffer once to avoid repeated subscript overhead + storage.entries.withUnsafeEntries { entries, count in + for i in 0 ..< count { + let entry = entries[i] + if entry.state == .occupied { + activeEntries.append((key: entry.key, value: entry.value)) + } + } + } + + // Clear the entire table + storage.entries.reset() + + // Reinsert all entries with fresh hash positions using double hashing + storage.entries.withUnsafeMutableEntries { entries, count in + + for (key, value) in activeEntries { + let hashes = key.hashes() + let hash1 = hashes.0 + let step = hashes.1 + var index = hash1 & (count - 1) + var probeCount = 0 + + // Find the first empty slot (no tombstones after clearing) + var inserted = false + while probeCount < count { + let entry = entries[index] + if entry.state == .empty { + entries[index] = KeyValueEntry( + key: key, + value: value, + state: .occupied + ) + inserted = true + break + } + // Double hashing: increment by step (eliminates multiplication) + probeCount += 1 + index = (index &+ step) & (count - 1) + } + + // This should never fail since we just cleared the table and are reinserting + // the same number of entries, but guard against data loss + precondition(inserted, "Failed to reinsert entry during compaction") + } + } + } + + /// The number of key-value pairs in the store (calculated on-demand) + var count: Int { + memoryMap.withLockedStorage { storage in + var result = 0 + storage.entries.withUnsafeEntries { entries, count in + for i in 0 ..< count { + if entries[i].state == .occupied { + result += 1 + } + } + } + return result + } + } + + /// A Boolean value indicating whether the store is empty (calculated on-demand) + var isEmpty: Bool { + memoryMap.withLockedStorage { storage in + storage.entries.withUnsafeEntries { entries, count in + for i in 0 ..< count { + if entries[i].state == .occupied { + return false + } + } + return true + } + } + } +} + +enum ProbeResult { + case found(Int) + case available(Int) + case full +} + +// MARK: - POD Types + +/// Storage container for the key-value store. +/// +/// This structure holds the hash table entries. +/// It is designed to be a POD (Plain Old Data) type for efficient memory-mapped storage. +struct KeyValueStoreStorage { + /// Fixed-size array of hash table entries + var entries: KeyValueStoreEntries +} + +/// A single entry in the key-value store. +/// +/// Each entry contains a key, value, and state indicator. This structure is designed +/// to be a POD type for efficient memory-mapped storage in a hash table using double hashing. +struct KeyValueEntry { + /// The key for this entry + var key: Key + /// The value stored in this entry + var value: Value + + /// State of a slot in the key-value store + enum SlotState: UInt8 { + /// Never used or fully cleared + case empty = 0 + /// Contains a valid key-value pair + case occupied = 1 + /// Previously occupied but deleted (maintains probe chains for double hashing) + case tombstone = 2 + } + + /// The current state of this hash table slot + var state: SlotState + + init(key: Key, value: Value, state: SlotState) { + self.key = key + self.value = value + self.state = state + } +} + +/// Fixed-size array of 256 entries with subscript access. +/// +/// This structure provides a fixed-capacity hash table storage using a tuple for the +/// underlying representation. The tuple ensures the struct remains a POD type, which is +/// required for safe memory-mapped storage. +struct KeyValueStoreEntries { + /// Tuple-based storage for 256 entries (ensures POD/trivial type for memory mapping) + private var storage: ( + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, + KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry + ) + + mutating func withUnsafeMutableEntries( + _ block: (UnsafeMutableBufferPointer>, Int) + -> Void + ) { + withUnsafeMutableBytes(of: &storage) { ptr in + let entries = ptr.bindMemory(to: KeyValueEntry.self) + block(entries, KeyValueStoreDefaultCapacity) + } + } + + func withUnsafeEntries(_ block: (UnsafeBufferPointer>, Int) -> Result) -> + Result + { + withUnsafeBytes(of: storage) { ptr in // No & here + let entries = ptr.bindMemory(to: KeyValueEntry.self) + return block(entries, KeyValueStoreDefaultCapacity) + } + } + + /// Resets all entries to empty state. + /// + /// This method zeroes out the entire storage buffer, effectively marking all entries + /// as empty and clearing any previously stored data. + mutating func reset() { + _ = withUnsafeMutableBytes(of: &storage) { ptr in + ptr.initializeMemory(as: UInt8.self, repeating: 0) + } + } +} diff --git a/Sources/MemoryMap/KeyValueStore.swift b/Sources/MemoryMap/KeyValueStore.swift index 5953bd9..209d66a 100644 --- a/Sources/MemoryMap/KeyValueStore.swift +++ b/Sources/MemoryMap/KeyValueStore.swift @@ -21,15 +21,7 @@ /// SOFTWARE. import Foundation - -/// Maximum length for string keys (in bytes) -public let KeyValueStoreMaxKeyLength = 64 - -/// Default capacity for the key-value store -public let KeyValueStoreDefaultCapacity = 128 - -/// Default maximum size for POD values (in bytes) -public let KeyValueStoreDefaultMaxValueSize = 1024 +import os /// A persistent key-value store backed by memory-mapped files. /// @@ -37,10 +29,7 @@ public let KeyValueStoreDefaultMaxValueSize = 1024 /// All data is automatically persisted to disk via memory mapping. /// /// ## Requirements -/// - **Keys**: Strings up to 64 bytes (UTF-8). Longer keys are truncated. -/// - **Values**: Must be POD (Plain Old Data) types with no references or classes. -/// Maximum value size is 1024 bytes. -/// - **Capacity**: Fixed at 128 entries. Not resizable after creation. +/// - **Capacity**: Fixed at 256 entries. Not resizable after creation. /// /// ## Storage Details /// Uses a hash table with double hashing for collision resolution. Deleted entries @@ -74,26 +63,7 @@ public let KeyValueStoreDefaultMaxValueSize = 1024 /// } /// ``` @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) -public class KeyValueStore: @unchecked Sendable { - /// A validated string key for KeyValueStore. - /// - /// Keys are limited to 64 bytes of UTF-8 data. Longer strings are handled differently - /// in debug vs release builds for safety. - public struct Key: Sendable { - /// Low-level storage representation - let keyArray: KeyStorage - - /// Creates a key from a string. - /// - /// Keys are limited to 64 bytes (UTF-8). If the string exceeds this limit, - /// it will be silently truncated to fit (respecting UTF-8 character boundaries). - /// - /// - Parameter string: The string to use as a key (max 64 bytes UTF-8) - public init(_ string: String) { - keyArray = KeyStorage(truncating: string) - } - } - +public class KeyValueStore: @unchecked Sendable { /// The URL of the memory-mapped file backing this store public var url: URL { memoryMap.url @@ -112,20 +82,18 @@ public class KeyValueStore: @unchecked Sendable { /// Creates or opens a key-value store at the specified file location. /// /// The store is backed by a memory-mapped file that persists data to disk. - /// Capacity is fixed at 128 entries and cannot be changed. + /// Capacity is fixed at 256 entries and cannot be changed. /// - /// - Parameter fileURL: The file location for the memory-mapped store + /// - Parameters: + /// - fileURL: The file location for the memory-mapped store + /// - lock: The lock implementation to use (defaults to OSAllocatedUnfairLock) /// /// - Throws: - /// - `KeyValueStoreError.valueTooLarge` if the Value type exceeds 1024 bytes /// - File system errors if the file cannot be created or opened /// /// - Note: Value must be a POD (Plain Old Data) type with no references or object pointers. - public init(fileURL: URL) throws { - guard MemoryLayout.stride <= KeyValueStoreDefaultMaxValueSize else { - throw KeyValueStoreError.valueTooLarge - } - memoryMap = try MemoryMap>(fileURL: fileURL) + public init(fileURL: URL, lock: MemoryMapLock = DefaultMemoryMapLock()) throws { + memoryMap = try MemoryMap>(fileURL: fileURL, lock: lock) } /// Returns all keys currently stored. @@ -133,11 +101,11 @@ public class KeyValueStore: @unchecked Sendable { /// The order of keys is not guaranteed and may change between calls. /// /// - Returns: An array of all keys in the store - public var keys: [String] { + public var keys: [Key] { memoryMap.withLockedStorage { storage in - var keys: [String] = [] + var keys: [Key] = [] self.forEachOccupiedEntry(in: &storage) { key, _ in - keys.append(key.string) + keys.append(key) } return keys } @@ -189,7 +157,7 @@ public class KeyValueStore: @unchecked Sendable { /// - Returns: `true` if the key exists, `false` otherwise public func contains(_ key: Key) -> Bool { memoryMap.withLockedStorage { storage in - if case .found = self.probeSlot(for: key.keyArray, in: &storage) { + if case .found = self.probeSlot(for: key, in: &storage) { return true } return false @@ -209,7 +177,7 @@ public class KeyValueStore: @unchecked Sendable { /// can cause long probe chains. It collects all active entries, clears the /// table, and reinserts them with fresh hash positions. /// - /// - Warning: This operation writes to all 128 hash table slots, resulting + /// - Warning: This operation writes to all 256 hash table slots, resulting /// in significant disk I/O since the store is backed by a memory-mapped file. /// Call this method deliberately when you can afford the I/O cost (e.g., /// during maintenance windows or when the store is idle). @@ -228,507 +196,26 @@ public class KeyValueStore: @unchecked Sendable { /// and collection operations. /// /// - Returns: A Dictionary with all keys and values from the store - public func dictionaryRepresentation() -> [String: Value] { + public func dictionaryRepresentation() -> [Key: Value] { memoryMap.withLockedStorage { storage in - var dict: [String: Value] = [:] + var dict: [Key: Value] = [:] self.forEachOccupiedEntry(in: &storage) { key, value in - dict[key.string] = value + dict[key] = value } return dict } } /// Private storage - private let memoryMap: MemoryMap> -} - -// MARK: - String Convenience API - -@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) -public extension KeyValueStore { - /// Accesses the value associated with the given key for reading and writing. - /// - /// This is a convenience method that accepts a String. - /// - /// - Note: Keys longer than 64 bytes will be truncated in release builds (assertion in debug). - /// Assignment failures (e.g., when the store is full) fail silently. - /// Use `setValue(_:for:)` if you need explicit error handling. - subscript(key: String) -> Value? { - get { - self[Key(key)] - } - set { - self[Key(key)] = newValue - } - } - - /// Accesses the value for the given key, or returns a default value if the key isn't found. - /// - /// Convenience method that accepts a String instead of a Key. - /// - /// - Parameters: - /// - key: The key to look up (max 64 bytes UTF-8) - /// - defaultValue: The value to return if the key doesn't exist - /// - Returns: The stored value, or the default value if the key isn't found - /// - /// - Note: Keys longer than 64 bytes will be truncated in release builds (assertion in debug). - subscript(key: String, default defaultValue: @autoclosure () -> Value) -> Value { - self[Key(key), default: defaultValue()] - } - - /// Returns whether the store contains the given key. - /// - /// Convenience method that accepts a String instead of a Key. - /// - /// - Parameter key: The key to check (max 64 bytes UTF-8) - /// - Returns: `true` if the key exists, `false` otherwise - /// - /// - Note: Keys longer than 64 bytes will be truncated in release builds (assertion in debug). - func contains(_ key: String) -> Bool { - contains(Key(key)) - } -} - -// MARK: - ExpressibleByStringLiteral - -@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) -extension KeyValueStore.Key: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - // String literals are compile-time constants - // Assert will catch invalid literals during development - self.init(value) - } -} - -// MARK: - Private KeyValueStore - -extension KeyValueStore { - private func forEachOccupiedEntry( - in storage: inout KeyValueStoreStorage, - _ body: (KeyStorage, Value) -> Void - ) { - for i in 0 ..< KeyValueStoreDefaultCapacity { - let entry = storage.entries[i] - if entry.state == .occupied { - body(entry.key, entry.value) - } - } - } - - private func probeSlot(for key: KeyStorage, in storage: inout KeyValueStoreStorage) -> ProbeResult { - precondition( - KeyValueStoreDefaultCapacity & (KeyValueStoreDefaultCapacity - 1) == 0, - "Capacity must be a power of two" - ) - let hash1 = key.hashKey - let step = key.hash2(from: hash1) - let startIndex = hash1 & (KeyValueStoreDefaultCapacity - 1) - var index = startIndex - var probeCount = 0 - var firstTombstoneIndex: Int? - - while probeCount < KeyValueStoreDefaultCapacity { - let entry = storage.entries[index] - - switch entry.state { - case .occupied: - if entry.key == key { - return .found(index) - } - // Robin Hood: check if our probe distance exceeds the resident's - // If so, this is a good insertion point (we'd swap in actual insertion) - case .tombstone: - if firstTombstoneIndex == nil { - firstTombstoneIndex = index - } - case .empty: - return .available(firstTombstoneIndex ?? index) - } - - // Double hashing: increment by step (eliminates multiplication) - probeCount += 1 - index = (index &+ step) & (KeyValueStoreDefaultCapacity - 1) - } - - if let tombstoneIndex = firstTombstoneIndex { - return .available(tombstoneIndex) - } - - return .full - } - - private func _setValue(_ value: Value?, for key: Key) throws { - let keyArray = key.keyArray - - return try memoryMap.withLockedStorage { storage in - switch self.probeSlot(for: keyArray, in: &storage) { - case let .found(index): - var updatedEntry = storage.entries[index] - if let value { - updatedEntry.value = value - } else { - updatedEntry.state = .tombstone - } - storage.entries[index] = updatedEntry - case let .available(index): - if let value { - storage.entries[index] = KeyValueEntry( - key: keyArray, - value: value, - state: .occupied - ) - } - case .full: - if value != nil { - throw KeyValueStoreError.storeFull - } - } - } - } - - private func _value(for key: Key) -> Value? { - memoryMap.withLockedStorage { storage in - guard case let .found(index) = self.probeSlot(for: key.keyArray, in: &storage) else { - return nil - } - let entry = storage.entries[index] - return entry.value - } - } - - private func compactInternal(storage: inout KeyValueStoreStorage) { - // Collect all active entries - var activeEntries: [(key: KeyStorage, value: Value)] = [] - - for i in 0 ..< KeyValueStoreDefaultCapacity { - let entry = storage.entries[i] - if entry.state == .occupied { - activeEntries.append((key: entry.key, value: entry.value)) - } - } - - // Clear the entire table - storage.entries.reset() - - // Reinsert all entries with fresh hash positions using double hashing - for (key, value) in activeEntries { - let hash1 = key.hashKey - let step = key.hash2(from: hash1) - var index = hash1 & (KeyValueStoreDefaultCapacity - 1) - var probeCount = 0 - - // Find the first empty slot (no tombstones after clearing) - var inserted = false - while probeCount < KeyValueStoreDefaultCapacity { - let entry = storage.entries[index] - if entry.state == .empty { - storage.entries[index] = KeyValueEntry( - key: key, - value: value, - state: .occupied - ) - inserted = true - break - } - // Double hashing: increment by step (eliminates multiplication) - probeCount += 1 - index = (index &+ step) & (KeyValueStoreDefaultCapacity - 1) - } - - // This should never fail since we just cleared the table and are reinserting - // the same number of entries, but guard against data loss - precondition(inserted, "Failed to reinsert entry during compaction") - } - } - - /// The number of key-value pairs in the store (calculated on-demand) - var count: Int { - memoryMap.withLockedStorage { storage in - var count = 0 - for i in 0 ..< KeyValueStoreDefaultCapacity { - if storage.entries[i].state == .occupied { - count += 1 - } - } - return count - } - } - - /// A Boolean value indicating whether the store is empty (calculated on-demand) - var isEmpty: Bool { - memoryMap.withLockedStorage { storage in - for i in 0 ..< KeyValueStoreDefaultCapacity { - if storage.entries[i].state == .occupied { - return false - } - } - return true - } - } -} - -private enum ProbeResult { - case found(Int) - case available(Int) - case full -} - -// MARK: - POD Types - -/// Storage container for the key-value store. -/// -/// This structure holds the hash table entries. -/// It is designed to be a POD (Plain Old Data) type for efficient memory-mapped storage. -struct KeyValueStoreStorage { - /// Fixed-size array of hash table entries - var entries: KeyValueStoreEntries -} - -/// Fixed-size key storage (64 bytes) plus tracked length -struct KeyStorage: @unchecked Sendable, Equatable { - // Public storage ensures the struct remains trivial/POD - typealias Storage64 = ( - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, - Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 - ) - let storage: Storage64 - let length: UInt8 - - /// Default initializer - creates a zero-filled key - init() { - storage = ( - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ) - length = 0 - } - - /// Creates a KeyStorage from a String, truncating if necessary - init(truncating string: String) { - let result = Self.packString(string) - // Silently truncate - no assertion for truncating init - storage = result.storage - length = result.usedBytes - } - - init(_ string: String) throws { - let result = Self.packString(string) - guard result.fits else { - throw KeyValueStoreError.keyTooLong - } - storage = result.storage - length = result.usedBytes - } - - /// Packs a string into a 64-byte storage tuple, preserving UTF-8 character boundaries - static func packString(_ string: String) -> (storage: Storage64, usedBytes: UInt8, fits: Bool) { - var storage = KeyStorage().storage - var fits = true - - let written = withUnsafeMutableBytes(of: &storage) { buffer in - let dest = buffer.bindMemory(to: UInt8.self) - var written = 0 - - for scalar in string.unicodeScalars { - let utf8 = UTF8.encode(scalar)! - let count = utf8.count - - // Only write if entire code point fits - guard written + count <= 64 else { - fits = false - break - } - - for byte in utf8 { - dest[written] = byte - written += 1 - } - } - - return written - } - - return (storage, UInt8(written), fits) - } - - /// Provides read-only byte access to the underlying storage - /// - /// - Parameter index: The byte index (must be 0..<64) - /// - Returns: The byte at the specified index - subscript(index: Int) -> Int8 { - precondition(index >= 0 && index < 64, "Index out of bounds") - return withUnsafeBytes(of: storage) { ptr in - Int8(bitPattern: ptr[index]) - } - } - - static func == (lhs: KeyStorage, rhs: KeyStorage) -> Bool { - guard lhs.length == rhs.length else { - return false - } - // Optimize: use withUnsafeBytes once instead of per-byte subscript calls - return withUnsafeBytes(of: lhs.storage) { lhsPtr in - withUnsafeBytes(of: rhs.storage) { rhsPtr in - let length = Int(lhs.length) - return memcmp(lhsPtr.baseAddress, rhsPtr.baseAddress, length) == 0 - } - } - } - - var hashKey: Int { - // FNV-1a hash algorithm (better distribution than djb2) - // Optimize: use withUnsafeBytes once instead of per-byte subscript calls - withUnsafeBytes(of: storage) { ptr in - let buffer = ptr.bindMemory(to: UInt8.self) - var hash: UInt64 = 14_695_981_039_346_656_037 // FNV offset basis - let length = Int(length) - for i in 0 ..< length { - hash ^= UInt64(buffer[i]) - hash = hash &* 1_099_511_628_211 // FNV prime - } - // Ensure non-negative result - return Int(hash & UInt64(Int.max)) - } - } - - /// Derive second hash from first hash (much faster than computing separately) - /// Use bit mixing to create independent distribution - /// Must be odd (coprime with capacity=128) and never zero - func hash2(from h1: Int) -> Int { - // Mix bits: rotate and XOR to decorrelate from hash1 - let mixed = ((h1 >> 17) ^ (h1 << 15)) &+ (h1 >> 7) - // Ensure odd by setting lowest bit - return mixed | 1 - } - - var string: String { - withUnsafeBytes(of: storage) { ptr in - let base = ptr.bindMemory(to: UInt8.self) - let slice = UnsafeBufferPointer(start: base.baseAddress, count: Int(length)) - return String(decoding: slice, as: UTF8.self) - } - } -} - -/// A single entry in the key-value store. -/// -/// Each entry contains a key, value, and state indicator. This structure is designed -/// to be a POD type for efficient memory-mapped storage in a hash table using double hashing. -struct KeyValueEntry { - /// The key for this entry - var key: KeyStorage - /// The value stored in this entry - var value: Value - - /// State of a slot in the key-value store - enum SlotState: UInt8 { - /// Never used or fully cleared - case empty = 0 - /// Contains a valid key-value pair - case occupied = 1 - /// Previously occupied but deleted (maintains probe chains for double hashing) - case tombstone = 2 - } - - /// The current state of this hash table slot - var state: SlotState - - init(key: KeyStorage = KeyStorage(), value: Value, state: SlotState = .empty) { - self.key = key - self.value = value - self.state = state - } -} - -/// Fixed-size array of 128 entries with subscript access. -/// -/// This structure provides a fixed-capacity hash table storage using a tuple for the -/// underlying representation. The tuple ensures the struct remains a POD type, which is -/// required for safe memory-mapped storage. -struct KeyValueStoreEntries { - /// Tuple-based storage for 128 entries (ensures POD/trivial type for memory mapping) - var storage: ( - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry, - KeyValueEntry, KeyValueEntry, KeyValueEntry, KeyValueEntry - ) - - /// Provides array-like subscript access to entries - /// - /// - Parameter index: The index of the entry (must be 0..<128) - /// - Returns: The entry at the specified index - subscript(index: Int) -> KeyValueEntry { - get { - precondition(index >= 0 && index < 128, "Index out of bounds") - return withUnsafeBytes(of: storage) { ptr in - ptr.bindMemory(to: KeyValueEntry.self)[index] - } - } - set { - precondition(index >= 0 && index < 128, "Index out of bounds") - withUnsafeMutableBytes(of: &storage) { ptr in - ptr.bindMemory(to: KeyValueEntry.self)[index] = newValue - } - } - } - - /// Resets all entries to empty state. - /// - /// This method zeroes out the entire storage buffer, effectively marking all entries - /// as empty and clearing any previously stored data. - public mutating func reset() { - _ = withUnsafeMutableBytes(of: &storage) { ptr in - ptr.initializeMemory(as: UInt8.self, repeating: 0) - } - } + let memoryMap: MemoryMap> } // MARK: - Errors /// Errors that can occur during KeyValueStore operations public enum KeyValueStoreError: Error { - /// The provided key exceeds the maximum allowed length (64 bytes) - case keyTooLong /// The store has reached its maximum capacity and cannot accept new entries case storeFull - /// The value type's size exceeds the maximum allowed size (1024 bytes) - case valueTooLarge + /// The items type's size exceeds the maximum allowed size + case tooLarge } diff --git a/Sources/MemoryMap/Locks.swift b/Sources/MemoryMap/Locks.swift new file mode 100644 index 0000000..076f6ef --- /dev/null +++ b/Sources/MemoryMap/Locks.swift @@ -0,0 +1,61 @@ +/// MIT License +/// +/// Copyright (c) 2024 Alexander Cohen +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. + +import Foundation +import os + +/// Protocol for lock implementations used by MemoryMap. +/// +/// Allows choosing different locking strategies: +/// - `OSAllocatedUnfairLock` (default): Fast unfair lock for typical use +/// - `NoLock`: No-op lock for single-threaded scenarios +/// - `NSLock`: Standard Foundation lock +/// - Custom implementations for specialized needs +public protocol MemoryMapLock { + func lock() + func unlock() +} + +/// A no-op lock implementation for single-threaded use cases. +/// +/// Use this when you know MemoryMap will only be accessed from a single thread +/// to avoid lock overhead. Methods are inlined for zero-cost abstraction. +/// +/// ## Example +/// ```swift +/// let memoryMap = try MemoryMap(fileURL: url, lock: NoLock()) +/// ``` +public class NoLock: MemoryMapLock { + public init() {} + + @inline(__always) + public func lock() {} + + @inline(__always) + public func unlock() {} +} + +extension NSLock: MemoryMapLock {} +extension OSAllocatedUnfairLock: MemoryMapLock where State == () {} + +/// The default lock implementation for MemoryMap +public typealias DefaultMemoryMapLock = OSAllocatedUnfairLock diff --git a/Sources/MemoryMap/MemoryMap.swift b/Sources/MemoryMap/MemoryMap.swift index 0679443..6ac5537 100644 --- a/Sources/MemoryMap/MemoryMap.swift +++ b/Sources/MemoryMap/MemoryMap.swift @@ -28,7 +28,7 @@ import os /// crash-resilient storage, with thread-safe access. @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) public class MemoryMap: @unchecked Sendable { - /// The URL of the memory-mapped file. + /// The URL of the memory-mapped file backing this storage public let url: URL /// Initializes a memory-mapped file for the given POD type `T`. @@ -41,7 +41,7 @@ public class MemoryMap: @unchecked Sendable { /// - fileURL: The file's location on disk. /// /// Note: `T` must be a Plain Old Data (POD) type. This is validated at runtime. - public init(fileURL: URL) throws { + public init(fileURL: URL, lock: MemoryMapLock = DefaultMemoryMapLock()) throws { // only POD types are allowed, so basically // structs with built-in types (aka. trivial). assert(_isPOD(T.self), "\(type(of: T.self)) is a non-trivial Type.") @@ -53,6 +53,7 @@ public class MemoryMap: @unchecked Sendable { throw MemoryMapError.invalidSize } + self.lock = lock url = fileURL container = try Self._mmap(url, size: MemoryLayout.stride) _s = withUnsafeMutablePointer(to: &container.pointee._s) { @@ -201,24 +202,30 @@ public class MemoryMap: @unchecked Sendable { var _s: T } - private let lock = OSAllocatedUnfairLock() + private let lock: MemoryMapLock private let container: UnsafeMutablePointer private let _s: UnsafeMutablePointer } +/// Errors that can occur during memory-mapped file operations public enum MemoryMapError: Error { - /// A unix error of some sort (open, mmap, ...). + /// A Unix system call failed (e.g., open, mmap, fstat, ftruncate) + /// + /// - Parameters: + /// - errno: The Unix error code + /// - operation: The name of the operation that failed + /// - url: The file URL that was being accessed case unix(Int32, String, URL) - /// Memory layout alignment is incorrect. + /// The memory-mapped region has incorrect alignment for the data type case alignment - /// `.bindMemory` failed. + /// Failed to bind the memory-mapped region to the expected type case failedBind - /// The header magic number is wrong. + /// The file doesn't have the expected magic number header (not a valid MemoryMap file) case notMemoryMap - /// The struct backing the map is too big. + /// The struct size exceeds the maximum allowed size (1MB) case invalidSize } diff --git a/Tests/MemoryMapTests/BasicTypeBenchmarks.swift b/Tests/MemoryMapTests/BasicTypeBenchmarks.swift new file mode 100644 index 0000000..208111f --- /dev/null +++ b/Tests/MemoryMapTests/BasicTypeBenchmarks.swift @@ -0,0 +1,551 @@ +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class BasicType1024Benchmarks: XCTestCase { + // MARK: - Integer Benchmarks + + func testBenchmarkIntStorage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(i) + } + } + } + + func testBenchmarkIntExtraction() { + let values = (0 ..< 10000).map { BasicType1024($0) } + measure { + for value in values { + let _ = value.intValue + } + } + } + + func testBenchmarkInt64Storage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(Int64(i)) + } + } + } + + func testBenchmarkInt64Extraction() { + let values = (0 ..< 10000).map { BasicType1024(Int64($0)) } + measure { + for value in values { + let _ = value.int64Value + } + } + } + + // MARK: - Floating Point Benchmarks + + func testBenchmarkDoubleStorage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(Double(i) * 3.14159) + } + } + } + + func testBenchmarkDoubleExtraction() { + let values = (0 ..< 10000).map { BasicType1024(Double($0) * 3.14159) } + measure { + for value in values { + let _ = value.doubleValue + } + } + } + + func testBenchmarkFloatStorage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(Float(i) * 3.14) + } + } + } + + func testBenchmarkFloatExtraction() { + let values = (0 ..< 10000).map { BasicType1024(Float($0) * 3.14) } + measure { + for value in values { + let _ = value.floatValue + } + } + } + + // MARK: - Boolean Benchmarks + + func testBenchmarkBoolStorage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(i % 2 == 0) + } + } + } + + func testBenchmarkBoolExtraction() { + let values = (0 ..< 10000).map { BasicType1024($0 % 2 == 0) } + measure { + for value in values { + let _ = value.boolValue + } + } + } + + // MARK: - String Benchmarks + + func testBenchmarkShortStringStorage() { + let testString = "Hello" + measure { + for _ in 0 ..< 10000 { + let _ = BasicType1024(testString) + } + } + } + + func testBenchmarkShortStringExtraction() { + let testString = "Hello" + let values = (0 ..< 10000).map { _ in BasicType1024(testString) } + measure { + for value in values { + let _ = value.stringValue + } + } + } + + func testBenchmarkMediumStringStorage() { + let testString = String(repeating: "Test ", count: 50) // ~250 chars + measure { + for _ in 0 ..< 1000 { + let _ = BasicType1024(testString) + } + } + } + + func testBenchmarkMediumStringExtraction() { + let testString = String(repeating: "Test ", count: 50) + let values = (0 ..< 1000).map { _ in BasicType1024(testString) } + measure { + for value in values { + let _ = value.stringValue + } + } + } + + func testBenchmarkLongStringStorage() { + let testString = String(repeating: "A", count: 1000) // ~1KB + measure { + for _ in 0 ..< 1000 { + let _ = BasicType1024(testString) + } + } + } + + func testBenchmarkLongStringExtraction() { + let testString = String(repeating: "A", count: 1000) + let values = (0 ..< 1000).map { _ in BasicType1024(testString) } + measure { + for value in values { + let _ = value.stringValue + } + } + } + + func testBenchmarkUnicodeStringStorage() { + let testString = "Hello 👋 World 🌍 Testing 🧪 " + String(repeating: "日本語", count: 10) + measure { + for _ in 0 ..< 1000 { + let _ = BasicType1024(testString) + } + } + } + + func testBenchmarkUnicodeStringExtraction() { + let testString = "Hello 👋 World 🌍 Testing 🧪 " + String(repeating: "日本語", count: 10) + let values = (0 ..< 1000).map { _ in BasicType1024(testString) } + measure { + for value in values { + let _ = value.stringValue + } + } + } + + // MARK: - KeyValueStore Integration Benchmarks + + func testBenchmarkKeyValueStoreIntOperations() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + let store = try KeyValueStore(fileURL: url) + + measure { + for i in 0 ..< 100 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + for i in 0 ..< 100 { + let _ = store[BasicType64("key\(i)")]?.intValue + } + } + } + + func testBenchmarkKeyValueStoreStringOperations() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + let store = try KeyValueStore(fileURL: url) + let testString = "Test String Value" + + measure { + for i in 0 ..< 100 { + store[BasicType64("str\(i)")] = BasicType1024(testString) + } + for i in 0 ..< 100 { + let _ = store[BasicType64("str\(i)")]?.stringValue + } + } + } + + func testBenchmarkKeyValueStoreMixedOperations() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + let store = try KeyValueStore(fileURL: url) + + measure { + for i in 0 ..< 50 { + store[BasicType64("int\(i)")] = BasicType1024(i) + store[BasicType64("str\(i)")] = BasicType1024("Value \(i)") + store[BasicType64("bool\(i)")] = BasicType1024(i % 2 == 0) + store[BasicType64("double\(i)")] = BasicType1024(Double(i) * 3.14) + } + } + } + + // MARK: - Memory Efficiency Benchmarks + + func testBenchmarkMemoryEfficiency() { + // Create many BasicType1024 instances and measure memory usage + let iterations = 10000 + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(iterations) + for i in 0 ..< iterations { + values.append(BasicType1024(i)) + } + // Force retention + XCTAssertEqual(values.count, iterations) + } + } + + func testBenchmarkStringMemoryEfficiency() { + let testString = String(repeating: "Test", count: 100) + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(1000) + for _ in 0 ..< 1000 { + values.append(BasicType1024(testString)) + } + XCTAssertEqual(values.count, 1000) + } + } + + // MARK: - Round-Trip Benchmarks + + func testBenchmarkCompleteRoundTripInt() { + measure { + for i in 0 ..< 10000 { + let stored = BasicType1024(i) + let _ = stored.intValue + } + } + } + + func testBenchmarkCompleteRoundTripString() { + let testString = "Hello, World!" + measure { + for _ in 0 ..< 10000 { + let stored = BasicType1024(testString) + let _ = stored.stringValue + } + } + } + + // MARK: - Equality Comparison Benchmarks + + func testBenchmarkEqualityInt() { + let values = (0 ..< 1000).map { BasicType1024($0) } + measure { + var result = true + for i in 0 ..< 1000 { + result = result && (values[i] == values[i]) + } + XCTAssertTrue(result) + } + } + + func testBenchmarkEqualityString() { + let testString = "Test String Value" + let values = (0 ..< 1000).map { _ in BasicType1024(testString) } + measure { + var result = true + for i in 0 ..< 1000 { + result = result && (values[i] == values[i]) + } + XCTAssertTrue(result) + } + } + + func testBenchmarkEqualityLongString() { + let testString = String(repeating: "A", count: 500) + let values = (0 ..< 1000).map { _ in BasicType1024(testString) } + measure { + var result = true + for i in 0 ..< 1000 { + result = result && (values[i] == values[i]) + } + XCTAssertTrue(result) + } + } + + func testBenchmarkInequalityDifferentKind() { + let intValues = (0 ..< 1000).map { BasicType1024($0) } + let stringValues = (0 ..< 1000).map { BasicType1024("value\($0)") } + measure { + var result = true + for i in 0 ..< 1000 { + result = result && (intValues[i] != stringValues[i]) + } + XCTAssertTrue(result) + } + } + + // MARK: - Hashing Benchmarks + + func testBenchmarkHashInt() { + let values = (0 ..< 10000).map { BasicType1024($0) } + measure { + var hash = 0 + for value in values { + hash ^= value.hashValue + } + XCTAssertNotEqual(hash, 0) + } + } + + func testBenchmarkHashString() { + let values = (0 ..< 10000).map { BasicType1024("value\($0)") } + measure { + var hash = 0 + for value in values { + hash ^= value.hashValue + } + XCTAssertNotEqual(hash, 0) + } + } + + func testBenchmarkDoubleHashInt() { + let values = (0 ..< 10000).map { BasicType1024($0) } + measure { + var hash1 = 0 + var hash2 = 0 + for value in values { + let hashes = value.hashes() + hash1 ^= hashes.0 + hash2 ^= hashes.1 + } + XCTAssertNotEqual(hash1, 0) + XCTAssertNotEqual(hash2, 0) + } + } + + func testBenchmarkDoubleHashString() { + let values = (0 ..< 10000).map { BasicType1024("str\($0)") } + measure { + var hash1 = 0 + var hash2 = 0 + for value in values { + let hashes = value.hashes() + hash1 ^= hashes.0 + hash2 ^= hashes.1 + } + XCTAssertNotEqual(hash1, 0) + XCTAssertNotEqual(hash2, 0) + } + } + + // MARK: - Unsigned Integer Benchmarks + + func testBenchmarkUIntStorage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(UInt(i)) + } + } + } + + func testBenchmarkUIntExtraction() { + let values = (0 ..< 10000).map { BasicType1024(UInt($0)) } + measure { + for value in values { + let _ = value.uintValue + } + } + } + + func testBenchmarkUInt8Storage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(UInt8(i % 256)) + } + } + } + + func testBenchmarkUInt8Extraction() { + let values = (0 ..< 10000).map { BasicType1024(UInt8($0 % 256)) } + measure { + for value in values { + let _ = value.uint8Value + } + } + } + + // MARK: - Type Discrimination Benchmarks + + func testBenchmarkTypeChecking() { + let values: [BasicType1024] = (0 ..< 1000).flatMap { i in + [ + BasicType1024(i), + BasicType1024(Double(i)), + BasicType1024("value\(i)"), + BasicType1024(i % 2 == 0), + ] + } + measure { + var intCount = 0 + var doubleCount = 0 + var stringCount = 0 + var boolCount = 0 + for value in values { + switch value.kind { + case .int: intCount += 1 + case .double: doubleCount += 1 + case .string: stringCount += 1 + case .bool: boolCount += 1 + default: break + } + } + XCTAssertEqual(intCount + doubleCount + stringCount + boolCount, 4000) + } + } + + func testBenchmarkWrongTypeExtraction() { + let values = (0 ..< 10000).map { BasicType1024($0) } + measure { + for value in values { + // Try to extract as wrong types - all should return nil + let _ = value.stringValue + let _ = value.doubleValue + let _ = value.boolValue + } + } + } + + // MARK: - Small Integer Type Benchmarks + + func testBenchmarkInt8Storage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(Int8(i % 128)) + } + } + } + + func testBenchmarkInt16Storage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(Int16(i)) + } + } + } + + func testBenchmarkInt32Storage() { + measure { + for i in 0 ..< 10000 { + let _ = BasicType1024(Int32(i)) + } + } + } + + // MARK: - Direct Construction vs Literal Benchmarks + + func testBenchmarkIntLiteralConstruction() { + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(10000) + for i in 0 ..< 10000 { + values.append(BasicType1024(integerLiteral: i)) + } + } + } + + func testBenchmarkStringLiteralInline() { + // Test if literal construction has different performance + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(100) + for _ in 0 ..< 100 { + values.append("test") + } + } + } + + // MARK: - Comparison with Swift Native Types + + func testBenchmarkBaselineIntArray() { + // Baseline: just creating Int values in an array + measure { + var values: [Int] = [] + values.reserveCapacity(10000) + for i in 0 ..< 10000 { + values.append(i) + } + } + } + + func testBenchmarkBaselineStringArray() { + // Baseline: just creating String values in an array + let testString = "Hello" + measure { + var values: [String] = [] + values.reserveCapacity(10000) + for _ in 0 ..< 10000 { + values.append(testString) + } + } + } + + func testBenchmarkBasicTypeIntArray() { + // Compare: creating BasicType values in an array + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(10000) + for i in 0 ..< 10000 { + values.append(BasicType1024(i)) + } + } + } + + func testBenchmarkBasicTypeStringArray() { + // Compare: creating BasicType values in an array + let testString = "Hello" + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(10000) + for _ in 0 ..< 10000 { + values.append(BasicType1024(testString)) + } + } + } +} diff --git a/Tests/MemoryMapTests/BasicTypeTests.swift b/Tests/MemoryMapTests/BasicTypeTests.swift new file mode 100644 index 0000000..3f1f22c --- /dev/null +++ b/Tests/MemoryMapTests/BasicTypeTests.swift @@ -0,0 +1,654 @@ +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class BasicType1024Tests: XCTestCase { + // MARK: - Integer Types Tests + + func testIntRoundTrip() { + let value = Int.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.intValue, value) + XCTAssertEqual(basic.int8Value, 0) + XCTAssertEqual(basic.stringValue, "") + } + + func testInt8RoundTrip() { + let value = Int8.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.int8Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testInt16RoundTrip() { + let value = Int16.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.int16Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testInt32RoundTrip() { + let value = Int32.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.int32Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testInt64RoundTrip() { + let value = Int64.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.int64Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUIntRoundTrip() { + let value = UInt.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.uintValue, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt8RoundTrip() { + let value = UInt8.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.uint8Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt16RoundTrip() { + let value = UInt16.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.uint16Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt32RoundTrip() { + let value = UInt32.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.uint32Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt64RoundTrip() { + let value = UInt64.max + let basic = BasicType1024(value) + XCTAssertEqual(basic.uint64Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + // MARK: - Floating Point Tests + + func testDoubleRoundTrip() { + let value = Double.pi + let basic = BasicType1024(value) + XCTAssertEqual(basic.doubleValue, value) + XCTAssertEqual(basic.floatValue, 0.0) + XCTAssertEqual(basic.intValue, 0) + } + + func testFloatRoundTrip() { + let value = Float.pi + let basic = BasicType1024(value) + XCTAssertEqual(basic.floatValue, value) + XCTAssertEqual(basic.doubleValue, 0.0) + XCTAssertEqual(basic.intValue, 0) + } + + func testDoubleSpecialValues() { + // Test infinity + let inf = BasicType1024(Double.infinity) + XCTAssertEqual(inf.doubleValue, Double.infinity) + + // Test negative infinity + let negInf = BasicType1024(-Double.infinity) + XCTAssertEqual(negInf.doubleValue, -Double.infinity) + + // Test NaN + let nan = BasicType1024(Double.nan) + XCTAssertTrue(nan.doubleValue.isNaN) + + // Test zero + let zero = BasicType1024(0.0) + XCTAssertEqual(zero.doubleValue, 0.0) + } + + // MARK: - Boolean Tests + + func testBoolTrue() { + let basic = BasicType1024(true) + XCTAssertEqual(basic.boolValue, true) + XCTAssertEqual(basic.intValue, 0) + } + + func testBoolFalse() { + let basic = BasicType1024(false) + XCTAssertEqual(basic.boolValue, false) + XCTAssertEqual(basic.intValue, 0) + } + + // MARK: - String Tests + + func testEmptyString() { + let basic = BasicType1024("") + XCTAssertEqual(basic.stringValue, "") + } + + func testShortString() { + let value = "Hello, World!" + let basic = BasicType1024(value) + XCTAssertEqual(basic.stringValue, value) + } + + func testMediumString() { + let value = String(repeating: "A", count: 500) + let basic = BasicType1024(value) + XCTAssertEqual(basic.stringValue, value) + } + + func testMaxLengthString() { + let value = String(repeating: "X", count: 1020) + let basic = BasicType1024(value) + let foundValue = basic.stringValue + XCTAssertEqual(foundValue, value) + } + + func testStringTruncation() { + // String longer than max length should be truncated + let value = String(repeating: "Y", count: 2000) + let basic = BasicType1024(value) + let retrieved = basic.stringValue + XCTAssertLessThanOrEqual(retrieved.count, 1020) + } + + func testUnicodeString() { + let value = "Hello 👋 World 🌍 Testing 🧪" + let basic = BasicType1024(value) + XCTAssertEqual(basic.stringValue, value) + } + + func testMultiByteUnicodeCharacters() { + // Test various Unicode scripts + let tests = [ + "こんにちは世界", // Japanese + "مرحبا بالعالم", // Arabic + "Здравствуй мир", // Russian + "你好世界", // Chinese + "🎉🎊🎈🎁🎀", // Emojis + "café résumé naïve", // Accented Latin + ] + + for value in tests { + let basic = BasicType1024(value) + XCTAssertEqual(basic.stringValue, value, "Failed for: \(value)") + } + } + + func testUnicodeBoundaryTruncation() { + // Create a string that will be truncated at a multi-byte character boundary + // Fill with single-byte chars, then add multi-byte emoji near the end + let prefix = String(repeating: "A", count: 1016) + let emoji = "👨‍👩‍👧‍👦" // Family emoji (multi-byte) + let value = prefix + emoji + + let basic = BasicType1024(value) + let retrieved = basic.stringValue + + // Should truncate without corrupting UTF-8 + XCTAssertTrue(retrieved.count <= 1020) + XCTAssertTrue(retrieved.unicodeScalars.allSatisfy { $0.isASCII || $0.value > 127 }) + } + + // MARK: - Throwing Initializer Tests + + func testThrowingInitWithValidString() throws { + let value = "Hello, World!" + let basic = try BasicType1024(throwing: value) + XCTAssertEqual(basic.stringValue, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testThrowingInitWithEmptyString() throws { + let basic = try BasicType1024(throwing: "") + XCTAssertEqual(basic.stringValue, "") + } + + func testThrowingInitWithMaxLengthString() throws { + let value = String(repeating: "X", count: 1020) + let basic = try BasicType1024(throwing: value) + XCTAssertEqual(basic.stringValue, value) + } + + func testThrowingInitWithTooLargeString() { + // String that exceeds the storage capacity should throw + let value = String(repeating: "Y", count: 2000) + XCTAssertThrowsError(try BasicType1024(throwing: value)) { error in + XCTAssertEqual(error as? KeyValueStoreError, KeyValueStoreError.tooLarge) + } + } + + func testThrowingInitWithUnicodeString() throws { + let value = "Hello 👋 World 🌍 Testing 🧪" + let basic = try BasicType1024(throwing: value) + XCTAssertEqual(basic.stringValue, value) + } + + // MARK: - KeyValueStore Integration Tests + + func testBasicType1024InKeyValueStore() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + let store = try KeyValueStore(fileURL: url) + + // Test different types + store["int"] = BasicType1024(42) + store["string"] = BasicType1024("Hello, World!") + store["bool"] = BasicType1024(true) + store["double"] = BasicType1024(3.14159) + + XCTAssertEqual(store["int"]?.intValue, 42) + XCTAssertEqual(store["string"]?.stringValue, "Hello, World!") + XCTAssertEqual(store["bool"]?.boolValue, true) + XCTAssertEqual(store["double"]?.doubleValue, 3.14159) + } + + func testBasicType1024Persistence() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + let testString = "Persistence Test 🚀" + + // Write data + do { + let store = try KeyValueStore(fileURL: url) + store["str"] = BasicType1024(testString) + store["num"] = BasicType1024(12345) + } + + // Read data in new instance + do { + let store = try KeyValueStore(fileURL: url) + XCTAssertEqual(store["str"]?.stringValue, testString) + XCTAssertEqual(store["num"]?.intValue, 12345) + } + } + + // MARK: - Type Safety Tests + + func testTypeSafety() { + let intValue = BasicType1024(42) + XCTAssertEqual(intValue.intValue, 42) + XCTAssertEqual(intValue.stringValue, "") + XCTAssertEqual(intValue.doubleValue, 0.0) + XCTAssertEqual(intValue.boolValue, false) + + let stringValue = BasicType1024("test") + XCTAssertEqual(stringValue.intValue, 0) + XCTAssertEqual(stringValue.stringValue, "test") + XCTAssertEqual(stringValue.doubleValue, 0.0) + } + + // MARK: - Edge Cases + + func testNegativeIntegers() { + let int8 = BasicType1024(Int8.min) + XCTAssertEqual(int8.int8Value, Int8.min) + + let int64 = BasicType1024(Int64.min) + XCTAssertEqual(int64.int64Value, Int64.min) + } + + func testZeroValues() { + XCTAssertEqual(BasicType1024(Int(0)).intValue, 0) + XCTAssertEqual(BasicType1024(Double(0.0)).doubleValue, 0.0) + XCTAssertEqual(BasicType1024(Float(0.0)).floatValue, 0.0) + XCTAssertEqual(BasicType1024(UInt(0)).uintValue, 0) + } +} + +// MARK: - BasicType8 Tests + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class BasicType8Tests: XCTestCase { + // MARK: - Integer Types Tests + + func testIntRoundTrip() { + let value = Int.max + let basic = BasicType8(value) + XCTAssertEqual(basic.intValue, value) + XCTAssertEqual(basic.int8Value, 0) + } + + func testInt8RoundTrip() { + let value = Int8.max + let basic = BasicType8(value) + XCTAssertEqual(basic.int8Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testInt16RoundTrip() { + let value = Int16.max + let basic = BasicType8(value) + XCTAssertEqual(basic.int16Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testInt32RoundTrip() { + let value = Int32.max + let basic = BasicType8(value) + XCTAssertEqual(basic.int32Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testInt64RoundTrip() { + let value = Int64.max + let basic = BasicType8(value) + XCTAssertEqual(basic.int64Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUIntRoundTrip() { + let value = UInt.max + let basic = BasicType8(value) + XCTAssertEqual(basic.uintValue, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt8RoundTrip() { + let value = UInt8.max + let basic = BasicType8(value) + XCTAssertEqual(basic.uint8Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt16RoundTrip() { + let value = UInt16.max + let basic = BasicType8(value) + XCTAssertEqual(basic.uint16Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt32RoundTrip() { + let value = UInt32.max + let basic = BasicType8(value) + XCTAssertEqual(basic.uint32Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + func testUInt64RoundTrip() { + let value = UInt64.max + let basic = BasicType8(value) + XCTAssertEqual(basic.uint64Value, value) + XCTAssertEqual(basic.intValue, 0) + } + + // MARK: - Floating Point Tests + + func testDoubleRoundTrip() { + let value = Double.pi + let basic = BasicType8(value) + XCTAssertEqual(basic.doubleValue, value) + XCTAssertEqual(basic.floatValue, 0.0) + XCTAssertEqual(basic.intValue, 0) + } + + func testFloatRoundTrip() { + let value = Float.pi + let basic = BasicType8(value) + XCTAssertEqual(basic.floatValue, value) + XCTAssertEqual(basic.doubleValue, 0.0) + XCTAssertEqual(basic.intValue, 0) + } + + func testDoubleSpecialValues() { + // Test infinity + let inf = BasicType8(Double.infinity) + XCTAssertEqual(inf.doubleValue, Double.infinity) + + // Test negative infinity + let negInf = BasicType8(-Double.infinity) + XCTAssertEqual(negInf.doubleValue, -Double.infinity) + + // Test NaN + let nan = BasicType8(Double.nan) + XCTAssertTrue(nan.doubleValue.isNaN) + + // Test zero + let zero = BasicType8(0.0) + XCTAssertEqual(zero.doubleValue, 0.0) + } + + // MARK: - Boolean Tests + + func testBoolTrue() { + let basic = BasicType8(true) + XCTAssertEqual(basic.boolValue, true) + XCTAssertEqual(basic.intValue, 0) + } + + func testBoolFalse() { + let basic = BasicType8(false) + XCTAssertEqual(basic.boolValue, false) + XCTAssertEqual(basic.intValue, 0) + } + + // MARK: - Literal Tests + + func testIntegerLiteral() { + let basic: BasicType8 = 42 + XCTAssertEqual(basic.intValue, 42) + } + + func testFloatLiteral() { + let basic: BasicType8 = 3.14159 + XCTAssertEqual(basic.doubleValue, 3.14159) + } + + func testBooleanLiteral() { + let basic: BasicType8 = true + XCTAssertEqual(basic.boolValue, true) + } + + // MARK: - BasicTypeNumber Alias Tests + + func testBasicTypeNumberAlias() { + let number: BasicTypeNumber = 12345 + XCTAssertEqual(number.intValue, 12345) + } + + // MARK: - KeyValueStore Integration Tests + + func testBasicType8InKeyValueStore() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + struct NumericValue { + var value: Double + var count: Int + } + + let store = try KeyValueStore(fileURL: url) + + // Test with integer keys + store[42] = NumericValue(value: 3.14, count: 1) + store[100] = NumericValue(value: 2.71, count: 2) + + XCTAssertEqual(store[42]?.value, 3.14) + XCTAssertEqual(store[42]?.count, 1) + XCTAssertEqual(store[100]?.value, 2.71) + XCTAssertEqual(store[100]?.count, 2) + } + + func testBasicType8Persistence() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + struct TestData { + var x: Int + var y: Int + } + + // Write data + do { + let store = try KeyValueStore(fileURL: url) + store[1] = TestData(x: 10, y: 20) + store[2] = TestData(x: 30, y: 40) + } + + // Read data in new instance + do { + let store = try KeyValueStore(fileURL: url) + XCTAssertEqual(store[1]?.x, 10) + XCTAssertEqual(store[1]?.y, 20) + XCTAssertEqual(store[2]?.x, 30) + XCTAssertEqual(store[2]?.y, 40) + } + } + + // MARK: - Type Safety Tests + + func testTypeSafety() { + let intValue = BasicType8(42) + XCTAssertEqual(intValue.intValue, 42) + XCTAssertEqual(intValue.doubleValue, 0.0) + XCTAssertEqual(intValue.boolValue, false) + + let doubleValue = BasicType8(3.14) + XCTAssertEqual(doubleValue.intValue, 0) + XCTAssertEqual(doubleValue.doubleValue, 3.14) + } + + // MARK: - Edge Cases + + func testNegativeIntegers() { + let int8 = BasicType8(Int8.min) + XCTAssertEqual(int8.int8Value, Int8.min) + + let int64 = BasicType8(Int64.min) + XCTAssertEqual(int64.int64Value, Int64.min) + } + + func testZeroValues() { + XCTAssertEqual(BasicType8(Int(0)).intValue, 0) + XCTAssertEqual(BasicType8(Double(0.0)).doubleValue, 0.0) + XCTAssertEqual(BasicType8(Float(0.0)).floatValue, 0.0) + XCTAssertEqual(BasicType8(UInt(0)).uintValue, 0) + } + + // MARK: - Hashing and Equality Tests + + func testEquality() { + let a = BasicType8(42) + let b = BasicType8(42) + let c = BasicType8(43) + + XCTAssertEqual(a, b) + XCTAssertNotEqual(a, c) + } + + func testHashing() { + let a = BasicType8(42) + let b = BasicType8(42) + + XCTAssertEqual(a.hashValue, b.hashValue) + // Note: hash values for different values are not guaranteed to be different + // but they usually are for simple cases + } + + func testDoubleHashing() { + let value = BasicType8(12345) + let (hash1, hash2) = value.hashes() + + // hash2 must be odd (for double hashing to work with power-of-2 capacity) + XCTAssertTrue(hash2 & 1 == 1, "hash2 must be odd") + + // Both hashes should be computed (non-zero or valid) + // Note: Hashes can be negative in Swift, that's fine + XCTAssertNotEqual(hash1, 0) + XCTAssertNotEqual(hash2, 0) + } +} + +// MARK: - BasicType Size Verification Tests + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class BasicTypeSizeTests: XCTestCase { + func testBasicType8Size() { + // BasicType8: 8 bytes storage + 3 bytes metadata (1 byte kind + 2 bytes length) + // Expected size: 11 bytes, but stride may be larger due to alignment + let size = MemoryLayout.size + let stride = MemoryLayout.stride + + print("BasicType8 - size: \(size), stride: \(stride)") + + // Size should be at least 11 bytes (8 storage + 1 kind + 2 length) + XCTAssertGreaterThanOrEqual(size, 11, "BasicType8 size should be at least 11 bytes") + + // Stride is usually aligned, so it might be larger + XCTAssertGreaterThanOrEqual(stride, size, "Stride should be >= size") + + // Verify storage capacity + XCTAssertEqual(ByteStorage8.capacity, 8, "ByteStorage8 capacity should be 8 bytes") + } + + func testBasicType64Size() { + // BasicType64: 60 bytes storage + 3 bytes metadata + // Expected size: 63 bytes, but stride may be larger due to alignment + let size = MemoryLayout.size + let stride = MemoryLayout.stride + + print("BasicType64 - size: \(size), stride: \(stride)") + + // Size should be at least 63 bytes (60 storage + 1 kind + 2 length) + XCTAssertGreaterThanOrEqual(size, 63, "BasicType64 size should be at least 63 bytes") + + // Stride is usually aligned + XCTAssertGreaterThanOrEqual(stride, size, "Stride should be >= size") + + // Verify storage capacity + XCTAssertEqual(ByteStorage60.capacity, 60, "ByteStorage60 capacity should be 60 bytes") + } + + func testBasicType1024Size() { + // BasicType1024: 1020 bytes storage + 3 bytes metadata + // Expected size: 1023 bytes, but stride may be larger due to alignment + let size = MemoryLayout.size + let stride = MemoryLayout.stride + + print("BasicType1024 - size: \(size), stride: \(stride)") + + // Size should be at least 1023 bytes (1020 storage + 1 kind + 2 length) + XCTAssertGreaterThanOrEqual(size, 1023, "BasicType1024 size should be at least 1023 bytes") + + // Stride is usually aligned + XCTAssertGreaterThanOrEqual(stride, size, "Stride should be >= size") + + // Verify storage capacity + XCTAssertEqual(ByteStorage1020.capacity, 1020, "ByteStorage1020 capacity should be 1020 bytes") + } + + func testByteStorageSizes() { + // Verify ByteStorage capacities match their type sizes + XCTAssertEqual(MemoryLayout.size, 8) + XCTAssertEqual(MemoryLayout.size, 60) + XCTAssertEqual(MemoryLayout.size, 1020) + } + + func testBasicTypeSizeComparison() { + // Verify relative sizes are correct + let size8 = MemoryLayout.size + let size64 = MemoryLayout.size + let size1024 = MemoryLayout.size + + XCTAssertLessThan(size8, size64, "BasicType8 should be smaller than BasicType64") + XCTAssertLessThan(size64, size1024, "BasicType64 should be smaller than BasicType1024") + } + + func testBasicTypeIsPOD() { + // Verify all BasicType variants are POD (Plain Old Data) + // This is critical for memory-mapped storage + XCTAssertTrue(_isPOD(BasicType8.self), "BasicType8 must be POD") + XCTAssertTrue(_isPOD(BasicType64.self), "BasicType64 must be POD") + XCTAssertTrue(_isPOD(BasicType1024.self), "BasicType1024 must be POD") + } +} diff --git a/Tests/MemoryMapTests/CacheLocalityBenchmark.swift b/Tests/MemoryMapTests/CacheLocalityBenchmark.swift new file mode 100644 index 0000000..ce23caf --- /dev/null +++ b/Tests/MemoryMapTests/CacheLocalityBenchmark.swift @@ -0,0 +1,70 @@ +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class CacheLocalityBenchmark: XCTestCase { + // MARK: - Cache Locality Hypothesis Testing + + func testBenchmark50InsertThen50Lookup() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + // 50 inserts (working set: ~50 entries = ~100KB < L2 cache) + for i in 0 ..< 50 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + // 50 lookups (same entries, should be L2 cache hits!) + for i in 0 ..< 50 { + let _ = store[BasicType64("key\(i)")] + } + } + } + + func testBenchmark150InsertThen150Lookup() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + // 150 inserts (working set: ~128 entries due to hash table size = ~263KB > L2 cache) + for i in 0 ..< 150 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + // 150 lookups (entries may be evicted from L2, cache misses) + for i in 0 ..< 150 { + let _ = store[BasicType64("key\(i)")] + } + } + } + + func testBenchmark100InsertWithoutLookup() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + // Just 100 inserts, no lookups (should be reasonably fast due to smaller working set) + for i in 0 ..< 100 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + } + } + + func testBenchmark200InsertDifferentKeysEachTime() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + var iterationCounter = 0 + measure { + // Use different keys each iteration to force real inserts, not updates + let base = iterationCounter * 200 + for i in 0 ..< 200 { + store[BasicType64("key\(base + i)")] = BasicType1024(i) + } + iterationCounter += 1 + } + } +} diff --git a/Tests/MemoryMapTests/CapacityComparison.swift b/Tests/MemoryMapTests/CapacityComparison.swift new file mode 100644 index 0000000..af0e6c1 --- /dev/null +++ b/Tests/MemoryMapTests/CapacityComparison.swift @@ -0,0 +1,102 @@ +import Foundation +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class CapacityComparison: XCTestCase { + func testMemoryAndFileSize() throws { + print("\n=== Capacity 256 vs 128 Comparison ===\n") + + // Current capacity (256) + let currentCapacity = KeyValueStoreDefaultCapacity + print("Current capacity: \(currentCapacity)") + + // Memory footprint + let entrySize = MemoryLayout>.stride + let storageSize = MemoryLayout>.size + + print("\n--- Memory Footprint ---") + print("Per entry (stride): \(entrySize) bytes") + print("Current (\(currentCapacity) entries): \(storageSize) bytes = \(storageSize / 1024) KB") + + // Calculate what 128 would be + let entries128 = 128 + let estimated128Size = entries128 * entrySize + print("With 128 entries: ~\(estimated128Size) bytes = ~\(estimated128Size / 1024) KB") + print("Memory increase: \(storageSize - estimated128Size) bytes = \(storageSize / estimated128Size)x") + + // Create actual file and measure size + print("\n--- File Size on Disk ---") + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + + let store = try KeyValueStore(fileURL: url) + + // Insert some data to ensure file is flushed + store[BasicType64("test")] = BasicType1024(42) + + // Get file size + let attributes = try FileManager.default.attributesOfItem(atPath: url.path) + let fileSize = attributes[.size] as! UInt64 + + print("Actual file size (256 capacity): \(fileSize) bytes = \(fileSize / 1024) KB") + + // The file includes: + // - Magic number (8 bytes) + // - KeyValueStoreStorage struct (contains the entries tuple) + + print("\nBreakdown:") + print(" Magic number: 8 bytes") + print(" Storage: \(fileSize - 8) bytes") + print(" Per entry: \(entrySize) bytes") + print(" Total entries: \(currentCapacity)") + + // Estimate 128 file size + let estimated128FileSize = UInt64(estimated128Size + 8) + print( + "\nEstimated file size with 128 capacity: \(estimated128FileSize) bytes = \(estimated128FileSize / 1024) KB" + ) + print( + "File size increase: \(fileSize - estimated128FileSize) bytes = ~\((Double(fileSize) / Double(estimated128FileSize) * 10).rounded() / 10)x" + ) + + // Cost-benefit analysis + print("\n--- Cost-Benefit Analysis ---") + print( + "Memory cost: +\((storageSize - estimated128Size) / 1024) KB (\(((Double(storageSize) / Double(estimated128Size) - 1) * 100).rounded())% increase)" + ) + print("Performance gain: 7-10x faster for 200 operations") + print("Collision rate: ~0.78 keys/slot (vs ~1.56 with 128)") + print("\nConclusion: 2x memory cost for 7-10x performance gain = Excellent trade-off!") + } + + func testCacheFootprint() { + print("\n=== CPU Cache Impact ===\n") + + let entryStride = MemoryLayout>.stride + + print("L1 cache (typical): 32-64 KB") + print("L2 cache (typical): 256 KB - 1 MB") + print("L3 cache (typical): 2-16 MB") + + print("\nEntries that fit in cache:") + print(" L1 (64 KB): ~\(64 * 1024 / entryStride) entries") + print(" L2 (256 KB): ~\(256 * 1024 / entryStride) entries") + print(" L3 (8 MB): ~\(8 * 1024 * 1024 / entryStride) entries") + + print("\nWith 256 capacity:") + let size256 = 256 * entryStride + print(" Total size: \(size256 / 1024) KB") + print(" Fits in L2: \(size256 < 256 * 1024 ? "Yes ✅" : "No ⚠️")") + print(" Fits in L3: Yes ✅") + + print("\nWith 128 capacity:") + let size128 = 128 * entryStride + print(" Total size: \(size128 / 1024) KB") + print(" Fits in L2: Yes ✅") + print(" Fits in L3: Yes ✅") + + print("\nNote: Even with 256 capacity, the table still fits in most L2 caches.") + print("The performance gain is primarily from reduced collisions, not cache locality.") + } +} diff --git a/Tests/MemoryMapTests/KeyValueStoreTests.swift b/Tests/MemoryMapTests/KeyValueStoreTests.swift index c48888b..265934e 100644 --- a/Tests/MemoryMapTests/KeyValueStoreTests.swift +++ b/Tests/MemoryMapTests/KeyValueStoreTests.swift @@ -18,7 +18,7 @@ final class KeyValueStoreTests: XCTestCase { var flag: Bool } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(counter: 42, flag: true) @@ -33,7 +33,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let value = store["nonexistent"] XCTAssertNil(value) @@ -44,7 +44,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 10) store["key1"] = TestValue(value: 20) @@ -58,7 +58,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 42) let removed = store["key1"] @@ -72,7 +72,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let removed = store["nonexistent"] store["nonexistent"] = nil @@ -86,39 +86,39 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let keys = collidingKeys // Insert keys that will create a probe chain (all map to same slot) - store[keys[0]] = TestValue(value: 1) - store[keys[1]] = TestValue(value: 2) - store[keys[2]] = TestValue(value: 3) - store[keys[3]] = TestValue(value: 4) + store[BasicType64(keys[0])] = TestValue(value: 1) + store[BasicType64(keys[1])] = TestValue(value: 2) + store[BasicType64(keys[2])] = TestValue(value: 3) + store[BasicType64(keys[3])] = TestValue(value: 4) // Verify all keys are accessible before deletion - XCTAssertEqual(store[keys[0]]?.value, 1) - XCTAssertEqual(store[keys[1]]?.value, 2) - XCTAssertEqual(store[keys[2]]?.value, 3) - XCTAssertEqual(store[keys[3]]?.value, 4) + XCTAssertEqual(store[BasicType64(keys[0])]?.value, 1) + XCTAssertEqual(store[BasicType64(keys[1])]?.value, 2) + XCTAssertEqual(store[BasicType64(keys[2])]?.value, 3) + XCTAssertEqual(store[BasicType64(keys[3])]?.value, 4) // Remove the first key - let removed = store[keys[0]] - store[keys[0]] = nil + let removed = store[BasicType64(keys[0])] + store[BasicType64(keys[0])] = nil XCTAssertEqual(removed?.value, 1) - XCTAssertNil(store[keys[0]], "Removed key should not be found") + XCTAssertNil(store[BasicType64(keys[0])], "Removed key should not be found") // CRITICAL: These lookups should still work! // If deletion just marks as unoccupied without rehashing, // any keys that were in the probe chain after the deleted key // will become unreachable because the probe chain is broken. - XCTAssertEqual(store[keys[1]]?.value, 2, "key b should still be accessible after deleting a") - XCTAssertEqual(store[keys[2]]?.value, 3, "key c should still be accessible after deleting a") - XCTAssertEqual(store[keys[3]]?.value, 4, "key d should still be accessible after deleting a") + XCTAssertEqual(store[BasicType64(keys[1])]?.value, 2, "key b should still be accessible after deleting a") + XCTAssertEqual(store[BasicType64(keys[2])]?.value, 3, "key c should still be accessible after deleting a") + XCTAssertEqual(store[BasicType64(keys[3])]?.value, 4, "key d should still be accessible after deleting a") // Try removing another and verify remaining keys - store[keys[2]] = nil - XCTAssertEqual(store[keys[1]]?.value, 2, "key b should still be accessible after deleting c") - XCTAssertEqual(store[keys[3]]?.value, 4, "key d should still be accessible after deleting c") + store[BasicType64(keys[2])] = nil + XCTAssertEqual(store[BasicType64(keys[1])]?.value, 2, "key b should still be accessible after deleting c") + XCTAssertEqual(store[BasicType64(keys[3])]?.value, 4, "key d should still be accessible after deleting c") } func testDeletionBreaksProbeChainScenario() throws { @@ -126,26 +126,26 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let keys = Array(collidingKeys.prefix(3)) // Fill colliding slots to guarantee a probe chain exists - store[keys[0]] = TestValue(value: 10) - store[keys[1]] = TestValue(value: 20) - store[keys[2]] = TestValue(value: 30) + store[BasicType64(keys[0])] = TestValue(value: 10) + store[BasicType64(keys[1])] = TestValue(value: 20) + store[BasicType64(keys[2])] = TestValue(value: 30) // Verify all are accessible - XCTAssertEqual(store[keys[0]]?.value, 10) - XCTAssertEqual(store[keys[1]]?.value, 20) - XCTAssertEqual(store[keys[2]]?.value, 30) + XCTAssertEqual(store[BasicType64(keys[0])]?.value, 10) + XCTAssertEqual(store[BasicType64(keys[1])]?.value, 20) + XCTAssertEqual(store[BasicType64(keys[2])]?.value, 30) // Remove middle element (most likely to break chain) - store[keys[1]] = nil + store[BasicType64(keys[1])] = nil // CRITICAL: x and z should still be findable - XCTAssertEqual(store[keys[0]]?.value, 10, "x should be findable after removing y") - XCTAssertEqual(store[keys[2]]?.value, 30, "z should be findable after removing y") - XCTAssertNil(store[keys[1]], "y should not be findable after removal") + XCTAssertEqual(store[BasicType64(keys[0])]?.value, 10, "x should be findable after removing y") + XCTAssertEqual(store[BasicType64(keys[2])]?.value, 30, "z should be findable after removing y") + XCTAssertNil(store[BasicType64(keys[1])], "y should not be findable after removal") } func testDeletionAndReinsertion() throws { @@ -153,24 +153,24 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let keys = Array(collidingKeys.prefix(3)) // Create a probe chain scenario - store[keys[0]] = TestValue(value: 1) - store[keys[1]] = TestValue(value: 2) - store[keys[2]] = TestValue(value: 3) + store[BasicType64(keys[0])] = TestValue(value: 1) + store[BasicType64(keys[1])] = TestValue(value: 2) + store[BasicType64(keys[2])] = TestValue(value: 3) // Remove middle element - store[keys[1]] = nil + store[BasicType64(keys[1])] = nil // Reinsert with different value - store[keys[1]] = TestValue(value: 20) + store[BasicType64(keys[1])] = TestValue(value: 20) // All should be accessible - XCTAssertEqual(store[keys[0]]?.value, 1) - XCTAssertEqual(store[keys[1]]?.value, 20) - XCTAssertEqual(store[keys[2]]?.value, 3) + XCTAssertEqual(store[BasicType64(keys[0])]?.value, 1) + XCTAssertEqual(store[BasicType64(keys[1])]?.value, 20) + XCTAssertEqual(store[BasicType64(keys[2])]?.value, 3) } func testContains() throws { @@ -178,7 +178,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 42) @@ -193,7 +193,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 1) store["key2"] = TestValue(value: 2) @@ -209,7 +209,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 1) store["key2"] = TestValue(value: 2) @@ -227,7 +227,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 1) store["key2"] = TestValue(value: 2) @@ -249,7 +249,7 @@ final class KeyValueStoreTests: XCTestCase { var timestamp: Double } - var store: KeyValueStore? = try KeyValueStore(fileURL: url) + var store: KeyValueStore? = try KeyValueStore(fileURL: url) store?["user:123"] = TestValue(counter: 100, timestamp: 12345.67) store?["user:456"] = TestValue(counter: 200, timestamp: 67890.12) @@ -276,7 +276,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Keys longer than 64 bytes are truncated in release builds // (assertion in debug builds) @@ -284,12 +284,12 @@ final class KeyValueStoreTests: XCTestCase { // Use a key that's exactly at the limit (64 bytes) let maxKey = String(repeating: "a", count: 64) - store[maxKey] = TestValue(value: 42) - XCTAssertEqual(store[maxKey]?.value, 42) + store[BasicType64(maxKey)] = TestValue(value: 42) + XCTAssertEqual(store[BasicType64(maxKey)]?.value, 42) // Clean up - store[maxKey] = nil - XCTAssertNil(store[maxKey]) + store[BasicType64(maxKey)] = nil + XCTAssertNil(store[BasicType64(maxKey)]) } func testMaxKeyLength() throws { @@ -297,13 +297,13 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Create a key exactly 64 bytes let maxKey = String(repeating: "a", count: 64) - store[maxKey] = TestValue(value: 42) - XCTAssertEqual(store[maxKey]?.value, 42) + store[BasicType64(maxKey)] = TestValue(value: 42) + XCTAssertEqual(store[BasicType64(maxKey)]?.value, 42) } // MARK: - Hash Collisions @@ -313,16 +313,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Add many entries to increase likelihood of hash collisions for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Verify all entries are retrievable for i in 0 ..< 100 { - let value = store["key\(i)"] + let value = store[BasicType64("key\(i)")] XCTAssertEqual(value?.value, i, "Failed to retrieve key\(i)") } } @@ -334,18 +334,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let capacity = KeyValueStoreDefaultCapacity // Fill the store for i in 0 ..< capacity { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Subscript silently ignores when full let overflowKey = "overflow" - store[overflowKey] = TestValue(value: 999) - XCTAssertNil(store[overflowKey], "Store should be full and unable to add new keys") + store[BasicType64(overflowKey)] = TestValue(value: 999) + XCTAssertNil(store[BasicType64(overflowKey)], "Store should be full and unable to add new keys") } // MARK: - Special Characters in Keys @@ -355,7 +355,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let specialKeys = [ "key:with:colons", @@ -368,11 +368,11 @@ final class KeyValueStoreTests: XCTestCase { ] for (index, key) in specialKeys.enumerated() { - store[key] = TestValue(value: index) + store[BasicType64(key)] = TestValue(value: index) } for (index, key) in specialKeys.enumerated() { - let value = store[key] + let value = store[BasicType64(key)] XCTAssertEqual(value?.value, index, "Failed for key: \(key)") } } @@ -384,7 +384,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key"] = TestValue(value: Int.max) XCTAssertEqual(store["key"]?.value, Int.max) } @@ -394,7 +394,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Double } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key"] = TestValue(value: 3.14159) let retrieved = store["key"] @@ -418,7 +418,7 @@ final class KeyValueStoreTests: XCTestCase { var bool2: Bool } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let complex = ComplexValue( int8: -128, @@ -460,7 +460,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store[""] = TestValue(value: 42) XCTAssertEqual(store[""]?.value, 42) @@ -471,7 +471,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["a"] = TestValue(value: 1) store["b"] = TestValue(value: 2) @@ -480,35 +480,6 @@ final class KeyValueStoreTests: XCTestCase { XCTAssertEqual(store["b"]?.value, 2) } - func testValueTooLarge() throws { - // Create a large struct that exceeds default max (1KB) - struct LargeValue { - var data: ( - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, - Int, Int, Int, Int, Int, Int, Int, Int, Int, Int - ) // 130 * 8 = 1040 bytes - } - - // Should fail with default max value size (1KB) - XCTAssertThrowsError(try KeyValueStore(fileURL: url)) { error in - guard case KeyValueStoreError.valueTooLarge = error else { - XCTFail("Expected KeyValueStoreError.valueTooLarge, got \(error)") - return - } - } - } - // MARK: - Dictionary-like API func testSubscriptAccess() throws { @@ -516,7 +487,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Test setting and getting store["key"] = TestValue(value: 42) @@ -536,7 +507,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Non-existent key returns default let value1 = store["missing", default: TestValue(value: 99)] @@ -553,7 +524,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Set initial value store["key"] = TestValue(value: 42) @@ -571,7 +542,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) store["key1"] = TestValue(value: 1) store["key2"] = TestValue(value: 2) @@ -596,11 +567,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) measure { for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } } } @@ -610,16 +581,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for i in 0 ..< 100 { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -629,16 +600,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate with different keys for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for i in 0 ..< 100 { - _ = store["missing\(i)"] + _ = store[BasicType64("missing\(i)")] } } } @@ -648,16 +619,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i * 2) + store[BasicType64("key\(i)")] = TestValue(value: i * 2) } } } @@ -667,17 +638,17 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) measure { // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Remove all for i in 0 ..< 100 { - store["key\(i)"] = nil + store[BasicType64("key\(i)")] = nil } } } @@ -687,11 +658,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { @@ -706,11 +677,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { @@ -723,11 +694,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { @@ -740,24 +711,24 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) measure { // Mix of operations that simulate real-world usage for i in 0 ..< 50 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } for i in 0 ..< 50 { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } for i in 0 ..< 25 { - store["key\(i)"] = TestValue(value: i * 2) + store[BasicType64("key\(i)")] = TestValue(value: i * 2) } for i in 0 ..< 10 { - store["key\(i)"] = nil + store[BasicType64("key\(i)")] = nil } _ = store.keys @@ -771,11 +742,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } let iterations = 1000 @@ -786,7 +757,7 @@ final class KeyValueStoreTests: XCTestCase { DispatchQueue.global().async { for _ in 0 ..< iterations { let index = Int.random(in: 0 ..< 100) - let value = store["key\(index)"] + let value = store[BasicType64("key\(index)")] XCTAssertEqual(value?.value, index) } expectation.fulfill() @@ -801,7 +772,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let iterations = 30 // 4 threads * 30 items = 120 total (within 128 capacity) let expectation = XCTestExpectation(description: "Concurrent writes") @@ -811,7 +782,7 @@ final class KeyValueStoreTests: XCTestCase { DispatchQueue.global().async { for i in 0 ..< iterations { let key = "thread\(threadId)_key\(i)" - store[key] = TestValue(value: i) + store[BasicType64(key)] = TestValue(value: i) } expectation.fulfill() } @@ -823,7 +794,7 @@ final class KeyValueStoreTests: XCTestCase { for threadId in 0 ..< 4 { for i in 0 ..< iterations { let key = "thread\(threadId)_key\(i)" - XCTAssertEqual(store[key]?.value, i, "Failed for \(key)") + XCTAssertEqual(store[BasicType64(key)]?.value, i, "Failed for \(key)") } } } @@ -833,11 +804,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate some data for i in 0 ..< 30 { - store["shared\(i)"] = TestValue(value: i) + store[BasicType64("shared\(i)")] = TestValue(value: i) } let expectation = XCTestExpectation(description: "Concurrent mixed operations") @@ -848,7 +819,7 @@ final class KeyValueStoreTests: XCTestCase { DispatchQueue.global().async { for _ in 0 ..< 200 { let index = Int.random(in: 0 ..< 30) - _ = store["shared\(index)"] + _ = store[BasicType64("shared\(index)")] } expectation.fulfill() } @@ -859,7 +830,7 @@ final class KeyValueStoreTests: XCTestCase { DispatchQueue.global().async { for i in 0 ..< 20 { let key = "writer\(threadId)_\(i)" - store[key] = TestValue(value: i * threadId) + store[BasicType64(key)] = TestValue(value: i * threadId) } expectation.fulfill() } @@ -875,9 +846,9 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) - let key = KeyValueStore.Key("mykey") + let key = BasicType64("mykey") store[key] = TestValue(value: 42) XCTAssertEqual(store[key]?.value, 42) @@ -888,9 +859,9 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) - let key: KeyValueStore.Key = "literal_key" + let key: BasicType64 = "literal_key" store[key] = TestValue(value: 99) XCTAssertEqual(store[key]?.value, 99) @@ -901,9 +872,9 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) - let key: KeyValueStore.Key = "test" + let key: BasicType64 = "test" let value = store[key, default: TestValue(value: 100)] XCTAssertEqual(value.value, 100) @@ -919,7 +890,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let keys = [ "🎉🌟💯🚀", // Emoji (4 bytes each) @@ -929,11 +900,11 @@ final class KeyValueStoreTests: XCTestCase { ] for (index, key) in keys.enumerated() { - store[key] = TestValue(value: index) + store[BasicType64(key)] = TestValue(value: index) } for (index, key) in keys.enumerated() { - XCTAssertEqual(store[key]?.value, index, "Failed for key: \(key)") + XCTAssertEqual(store[BasicType64(key)]?.value, index, "Failed for key: \(key)") } } @@ -942,20 +913,20 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Create a key with emoji that will be truncated at 64 bytes // Each emoji is 4 bytes, so 16 emoji = 64 bytes exactly let exactKey = String(repeating: "🎉", count: 16) // Exactly 64 bytes - store[exactKey] = TestValue(value: 1) - XCTAssertEqual(store[exactKey]?.value, 1) + store[BasicType64(exactKey)] = TestValue(value: 1) + XCTAssertEqual(store[BasicType64(exactKey)]?.value, 1) // 17 emoji = 68 bytes, should truncate to 16 emoji let overKey = String(repeating: "🎉", count: 17) - store[overKey] = TestValue(value: 2) + store[BasicType64(overKey)] = TestValue(value: 2) // The truncated version should match the 16-emoji key - XCTAssertEqual(store[exactKey]?.value, 2, "Truncation should respect UTF-8 boundaries") + XCTAssertEqual(store[BasicType64(exactKey)]?.value, 2, "Truncation should respect UTF-8 boundaries") } func testUTF8MixedASCIIAndMultibyte() throws { @@ -963,7 +934,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let keys = [ "user:123:🎉", @@ -972,11 +943,11 @@ final class KeyValueStoreTests: XCTestCase { ] for (index, key) in keys.enumerated() { - store[key] = TestValue(value: index) + store[BasicType64(key)] = TestValue(value: index) } for (index, key) in keys.enumerated() { - XCTAssertEqual(store[key]?.value, index) + XCTAssertEqual(store[BasicType64(key)]?.value, index) } } @@ -987,7 +958,7 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - var store: KeyValueStore? = try KeyValueStore(fileURL: url) + var store: KeyValueStore? = try KeyValueStore(fileURL: url) // Add some data store?["key1"] = TestValue(value: 1) @@ -1013,27 +984,27 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - var store: KeyValueStore? = try KeyValueStore(fileURL: url) + var store: KeyValueStore? = try KeyValueStore(fileURL: url) // Add data with colliding keys to create tombstones let keys = Array(collidingKeys.prefix(4)) for (index, key) in keys.enumerated() { - store?[key] = TestValue(value: index) + store?[BasicType64(key)] = TestValue(value: index) } // Delete some - store?[keys[1]] = nil - store?[keys[3]] = nil + store?[BasicType64(keys[1])] = nil + store?[BasicType64(keys[3])] = nil // Close and reopen store = nil store = try KeyValueStore(fileURL: url) // Verify persistence - XCTAssertEqual(store?[keys[0]]?.value, 0) - XCTAssertNil(store?[keys[1]]) - XCTAssertEqual(store?[keys[2]]?.value, 2) - XCTAssertNil(store?[keys[3]]) + XCTAssertEqual(store?[BasicType64(keys[0])]?.value, 0) + XCTAssertNil(store?[BasicType64(keys[1])]) + XCTAssertEqual(store?[BasicType64(keys[2])]?.value, 2) + XCTAssertNil(store?[BasicType64(keys[3])]) } func testMultipleReopenCycles() throws { @@ -1042,7 +1013,7 @@ final class KeyValueStoreTests: XCTestCase { } for cycle in 0 ..< 5 { - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) if cycle == 0 { // First cycle: add data @@ -1061,12 +1032,12 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Insert many keys and verify distribution for i in 0 ..< 100 { let key = "key\(i)" - store[key] = TestValue(value: i) + store[BasicType64(key)] = TestValue(value: i) // We can't directly access the slot, but we can infer distribution // by checking how many collisions occur @@ -1074,7 +1045,7 @@ final class KeyValueStoreTests: XCTestCase { // All keys should be retrievable (this tests distribution indirectly) for i in 0 ..< 100 { - XCTAssertEqual(store["key\(i)"]?.value, i) + XCTAssertEqual(store[BasicType64("key\(i)")]?.value, i) } } @@ -1083,19 +1054,19 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) let testKey = "consistency_test" - store[testKey] = TestValue(value: 123) + store[BasicType64(testKey)] = TestValue(value: 123) // Retrieve multiple times - should always work for _ in 0 ..< 100 { - XCTAssertEqual(store[testKey]?.value, 123) + XCTAssertEqual(store[BasicType64(testKey)]?.value, 123) } // Reopen and verify - let store2 = try KeyValueStore(fileURL: url) - XCTAssertEqual(store2[testKey]?.value, 123) + let store2 = try KeyValueStore(fileURL: url) + XCTAssertEqual(store2[BasicType64(testKey)]?.value, 123) } // MARK: - Load Factor Performance @@ -1105,18 +1076,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) - let itemCount = 32 // 25% of 128 + let store = try KeyValueStore(fileURL: url) + let itemCount = 64 // 25% of 256 // Prepopulate for i in 0 ..< itemCount { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for _ in 0 ..< 100 { for i in 0 ..< itemCount { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1127,18 +1098,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) - let itemCount = 64 // 50% of 128 + let store = try KeyValueStore(fileURL: url) + let itemCount = 128 // 50% of 256 // Prepopulate for i in 0 ..< itemCount { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for _ in 0 ..< 100 { for i in 0 ..< itemCount { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1149,18 +1120,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) - let itemCount = 96 // 75% of 128 + let store = try KeyValueStore(fileURL: url) + let itemCount = 192 // 75% of 256 // Prepopulate for i in 0 ..< itemCount { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for _ in 0 ..< 100 { for i in 0 ..< itemCount { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1171,18 +1142,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) - let itemCount = 115 // 90% of 128 + let store = try KeyValueStore(fileURL: url) + let itemCount = 230 // 90% of 256 // Prepopulate for i in 0 ..< itemCount { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for _ in 0 ..< 100 { for i in 0 ..< itemCount { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1193,18 +1164,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) - let itemCount = 127 // 99% of 128 + let store = try KeyValueStore(fileURL: url) + let itemCount = 253 // 99% of 256 // Prepopulate for i in 0 ..< itemCount { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for _ in 0 ..< 100 { for i in 0 ..< itemCount { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1217,17 +1188,17 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Use colliding keys to create maximum probe chain for (index, key) in collidingKeys.enumerated() { - store[key] = TestValue(value: index) + store[BasicType64(key)] = TestValue(value: index) } measure { for _ in 0 ..< 1000 { for key in collidingKeys { - _ = store[key] + _ = store[BasicType64(key)] } } } @@ -1238,21 +1209,21 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) measure { // Insert and delete to create tombstones for i in 0 ..< 50 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } for i in 0 ..< 25 { - store["key\(i)"] = nil + store[BasicType64("key\(i)")] = nil } // Now lookups have to traverse tombstones for i in 25 ..< 50 { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } // Clean up for next iteration @@ -1265,17 +1236,17 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Sequential access measure { for i in 0 ..< 100 { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1285,11 +1256,11 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Generate random sequence @@ -1298,7 +1269,7 @@ final class KeyValueStoreTests: XCTestCase { // Random access measure { for i in indices { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } } @@ -1310,16 +1281,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate with 1-5 char keys for i in 0 ..< 100 { - store["k\(i)"] = TestValue(value: i) + store[BasicType64("k\(i)")] = TestValue(value: i) } measure { for i in 0 ..< 100 { - _ = store["k\(i)"] + _ = store[BasicType64("k\(i)")] } } } @@ -1329,16 +1300,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate with ~25 char keys for i in 0 ..< 100 { - store["medium_length_key_\(i)_test"] = TestValue(value: i) + store[BasicType64("medium_length_key_\(i)_test")] = TestValue(value: i) } measure { for i in 0 ..< 100 { - _ = store["medium_length_key_\(i)_test"] + _ = store[BasicType64("medium_length_key_\(i)_test")] } } } @@ -1348,18 +1319,18 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate with 64 char keys for i in 0 ..< 100 { let key = String(format: "very_long_key_name_with_lots_of_characters_item_%04d_end", i) - store[key] = TestValue(value: i) + store[BasicType64(key)] = TestValue(value: i) } measure { for i in 0 ..< 100 { let key = String(format: "very_long_key_name_with_lots_of_characters_item_%04d_end", i) - _ = store[key] + _ = store[BasicType64(key)] } } } @@ -1372,10 +1343,10 @@ final class KeyValueStoreTests: XCTestCase { } measure { - var store: KeyValueStore? = try? KeyValueStore(fileURL: url) + var store: KeyValueStore? = try? KeyValueStore(fileURL: url) for i in 0 ..< 50 { - store?["key\(i)"] = TestValue(value: i) + store?[BasicType64("key\(i)")] = TestValue(value: i) } store = nil @@ -1393,10 +1364,10 @@ final class KeyValueStoreTests: XCTestCase { } measure { - let store = try! KeyValueStore(fileURL: url) + let store = try! KeyValueStore(fileURL: url) for i in 0 ..< 128 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } try? FileManager.default.removeItem(at: url) @@ -1410,16 +1381,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } measure { for i in 0 ..< 100 { - _ = store.contains("key\(i)") + _ = store.contains(BasicType64("key\(i)")) } } } @@ -1429,12 +1400,12 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) measure { // Prepopulate for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } store.removeAll() @@ -1448,16 +1419,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Fill store for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Delete half to create tombstones for i in 0 ..< 50 { - store["key\(i)"] = nil + store[BasicType64("key\(i)")] = nil } // Compact should clean up tombstones @@ -1465,7 +1436,7 @@ final class KeyValueStoreTests: XCTestCase { // Verify remaining items are still accessible for i in 50 ..< 100 { - XCTAssertEqual(store["key\(i)"]?.value, i) + XCTAssertEqual(store[BasicType64("key\(i)")]?.value, i) } } @@ -1474,25 +1445,25 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Insert and delete many times to trigger auto-compact for cycle in 0 ..< 10 { for i in 0 ..< 20 { - store["temp\(i)"] = TestValue(value: cycle) + store[BasicType64("temp\(i)")] = TestValue(value: cycle) } for i in 0 ..< 20 { - store["temp\(i)"] = nil + store[BasicType64("temp\(i)")] = nil } } // Store should still work correctly after auto-compaction for i in 0 ..< 50 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } for i in 0 ..< 50 { - XCTAssertEqual(store["key\(i)"]?.value, i) + XCTAssertEqual(store[BasicType64("key\(i)")]?.value, i) } } @@ -1501,14 +1472,14 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - var store: KeyValueStore? = try KeyValueStore(fileURL: url) + var store: KeyValueStore? = try KeyValueStore(fileURL: url) // Create data with tombstones for i in 0 ..< 80 { - store?["key\(i)"] = TestValue(value: i) + store?[BasicType64("key\(i)")] = TestValue(value: i) } for i in 0 ..< 40 { - store?["key\(i)"] = nil + store?[BasicType64("key\(i)")] = nil } // Compact @@ -1520,7 +1491,7 @@ final class KeyValueStoreTests: XCTestCase { // Verify data survived compaction for i in 40 ..< 80 { - XCTAssertEqual(store?["key\(i)"]?.value, i, "Failed for key\(i)") + XCTAssertEqual(store?[BasicType64("key\(i)")]?.value, i, "Failed for key\(i)") } } @@ -1529,23 +1500,23 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Fill with items for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: i) + store[BasicType64("key\(i)")] = TestValue(value: i) } // Delete many to create tombstones for i in 0 ..< 80 { - store["key\(i)"] = nil + store[BasicType64("key\(i)")] = nil } // Measure lookup time with tombstones let startWithTombstones = Date() for _ in 0 ..< 1000 { for i in 80 ..< 100 { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } let timeWithTombstones = Date().timeIntervalSince(startWithTombstones) @@ -1557,7 +1528,7 @@ final class KeyValueStoreTests: XCTestCase { let startAfterCompact = Date() for _ in 0 ..< 1000 { for i in 80 ..< 100 { - _ = store["key\(i)"] + _ = store[BasicType64("key\(i)")] } } let timeAfterCompact = Date().timeIntervalSince(startAfterCompact) @@ -1574,16 +1545,16 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) // Rapid insert/delete cycles for cycle in 0 ..< 10 { for i in 0 ..< 50 { - store["key\(i)"] = TestValue(value: cycle * 100 + i) + store[BasicType64("key\(i)")] = TestValue(value: cycle * 100 + i) } for i in 0 ..< 50 { - store["key\(i)"] = nil + store[BasicType64("key\(i)")] = nil } } } @@ -1593,12 +1564,12 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) for cycle in 0 ..< 5 { // Fill for i in 0 ..< 100 { - store["key\(i)"] = TestValue(value: cycle * 100 + i) + store[BasicType64("key\(i)")] = TestValue(value: cycle * 100 + i) } // Clear @@ -1611,15 +1582,15 @@ final class KeyValueStoreTests: XCTestCase { var value: Int } - let store = try KeyValueStore(fileURL: url) + let store = try KeyValueStore(fileURL: url) for i in 0 ..< 1000 { if i % 3 == 0 { - store["key\(i % 100)"] = TestValue(value: i) + store[BasicType64("key\(i % 100)")] = TestValue(value: i) } else if i % 3 == 1 { - _ = store["key\(i % 100)"] + _ = store[BasicType64("key\(i % 100)")] } else { - _ = store.contains("key\(i % 100)") + _ = store.contains(BasicType64("key\(i % 100)")) } } } @@ -1640,17 +1611,17 @@ final class KeyValueStoreTests: XCTestCase { var c: Int32 } - let store1 = try KeyValueStore(fileURL: url.appendingPathExtension("align1")) + let store1 = try KeyValueStore(fileURL: url.appendingPathExtension("align1")) store1["key"] = AlignedValue1(a: 42) XCTAssertEqual(store1["key"]?.a, 42) try? FileManager.default.removeItem(at: url.appendingPathExtension("align1")) - let store8 = try KeyValueStore(fileURL: url.appendingPathExtension("align8")) + let store8 = try KeyValueStore(fileURL: url.appendingPathExtension("align8")) store8["key"] = AlignedValue8(a: 12_345_678) XCTAssertEqual(store8["key"]?.a, 12_345_678) try? FileManager.default.removeItem(at: url.appendingPathExtension("align8")) - let storeMixed = try KeyValueStore(fileURL: url.appendingPathExtension("mixed")) + let storeMixed = try KeyValueStore(fileURL: url.appendingPathExtension("mixed")) storeMixed["key"] = AlignedValueMixed(a: 1, b: 2, c: 3) let val = storeMixed["key"] XCTAssertEqual(val?.a, 1) diff --git a/Tests/MemoryMapTests/MixedOperationsProfile.swift b/Tests/MemoryMapTests/MixedOperationsProfile.swift new file mode 100644 index 0000000..78a8df6 --- /dev/null +++ b/Tests/MemoryMapTests/MixedOperationsProfile.swift @@ -0,0 +1,131 @@ +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class MixedOperationsProfile: XCTestCase { + // MARK: - Baseline: Homogeneous Operations + + func testBenchmarkHomogeneousIntOnly() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + for i in 0 ..< 200 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + } + } + + // MARK: - Mixed Type Operations (200 total) + + func testBenchmarkMixed200Operations() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + for i in 0 ..< 50 { + store[BasicType64("int\(i)")] = BasicType1024(i) + store[BasicType64("str\(i)")] = BasicType1024("Value \(i)") + store[BasicType64("bool\(i)")] = BasicType1024(i % 2 == 0) + store[BasicType64("double\(i)")] = BasicType1024(Double(i) * 3.14) + } + } + } + + // MARK: - Isolate Key Creation + + func testBenchmarkKeyCreationOnly() { + measure { + var keys: [BasicType64] = [] + keys.reserveCapacity(200) + for i in 0 ..< 50 { + keys.append(BasicType64("int\(i)")) + keys.append(BasicType64("str\(i)")) + keys.append(BasicType64("bool\(i)")) + keys.append(BasicType64("double\(i)")) + } + } + } + + // MARK: - Isolate Value Creation + + func testBenchmarkValueCreationMixed() { + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(200) + for i in 0 ..< 50 { + values.append(BasicType1024(i)) + values.append(BasicType1024("Value \(i)")) + values.append(BasicType1024(i % 2 == 0)) + values.append(BasicType1024(Double(i) * 3.14)) + } + } + } + + func testBenchmarkValueCreationHomogeneous() { + measure { + var values: [BasicType1024] = [] + values.reserveCapacity(200) + for i in 0 ..< 200 { + values.append(BasicType1024(i)) + } + } + } + + // MARK: - Isolate Store Insert + + func testBenchmarkStoreInsertPrebuilt() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + // Prebuild keys and values + var keys: [BasicType64] = [] + var values: [BasicType1024] = [] + for i in 0 ..< 50 { + keys.append(BasicType64("int\(i)")) + values.append(BasicType1024(i)) + keys.append(BasicType64("str\(i)")) + values.append(BasicType1024("Value \(i)")) + keys.append(BasicType64("bool\(i)")) + values.append(BasicType1024(i % 2 == 0)) + keys.append(BasicType64("double\(i)")) + values.append(BasicType1024(Double(i) * 3.14)) + } + + measure { + for i in 0 ..< 200 { + store[keys[i]] = values[i] + } + } + } + + // MARK: - Test Hash Distribution + + func testMixedTypeHashDistribution() { + var hashCounts: [Int: Int] = [:] + + for i in 0 ..< 50 { + let int = BasicType1024(i).hashValue & 127 + let str = BasicType1024("Value \(i)").hashValue & 127 + let bool = BasicType1024(i % 2 == 0).hashValue & 127 + let double = BasicType1024(Double(i) * 3.14).hashValue & 127 + + hashCounts[int, default: 0] += 1 + hashCounts[str, default: 0] += 1 + hashCounts[bool, default: 0] += 1 + hashCounts[double, default: 0] += 1 + } + + let maxCollisions = hashCounts.values.max() ?? 0 + let avgCollisions = Double(hashCounts.values.reduce(0, +)) / Double(hashCounts.count) + + print("\nHash distribution (128 buckets, 200 values):") + print(" Unique buckets used: \(hashCounts.count)") + print(" Max collisions in one bucket: \(maxCollisions)") + print(" Average per bucket: \(String(format: "%.2f", avgCollisions))") + print(" Ideal (uniform): \(String(format: "%.2f", 200.0 / 128.0))") + } +} diff --git a/Tests/MemoryMapTests/ResizingBenchmark.swift b/Tests/MemoryMapTests/ResizingBenchmark.swift new file mode 100644 index 0000000..fdcf70b --- /dev/null +++ b/Tests/MemoryMapTests/ResizingBenchmark.swift @@ -0,0 +1,66 @@ +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class ResizingBenchmark: XCTestCase { + // MARK: - Hash Table Resizing Investigation + + func testBenchmark200InsertsNoPrealloc() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + for i in 0 ..< 200 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + } + } + + func testBenchmark100InsertThen100Lookup() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + // Insert phase + for i in 0 ..< 100 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + // Lookup phase + for i in 0 ..< 100 { + let _ = store[BasicType64("key\(i)")] + } + } + } + + func testBenchmark200Lookups() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + // Prepopulate + for i in 0 ..< 200 { + store[BasicType64("key\(i)")] = BasicType1024(i) + } + + measure { + for i in 0 ..< 200 { + let _ = store[BasicType64("key\(i)")] + } + } + } + + func testBenchmarkInterleavedInsertLookup() throws { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at: url) } + let store = try KeyValueStore(fileURL: url) + + measure { + for i in 0 ..< 100 { + store[BasicType64("key\(i)")] = BasicType1024(i) + let _ = store[BasicType64("key\(i)")] + } + } + } +} diff --git a/Tests/MemoryMapTests/TupleStorageTests.swift b/Tests/MemoryMapTests/TupleStorageTests.swift new file mode 100644 index 0000000..b6c5bd2 --- /dev/null +++ b/Tests/MemoryMapTests/TupleStorageTests.swift @@ -0,0 +1,142 @@ +@testable import MemoryMap +import XCTest + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, visionOS 2.0, *) +final class TupleStorageTests: XCTestCase { + // MARK: - Storage Size Tests + + func testByteStorage60Size() { + let stride = MemoryLayout.stride + let size = MemoryLayout.size + + XCTAssertEqual(stride, 60, "Storage stride should be exactly 60 bytes") + XCTAssertEqual(size, 60, "Storage size should be exactly 60 bytes") + } + + func testByteStorage1020Size() { + let stride = MemoryLayout.stride + let size = MemoryLayout.size + + XCTAssertEqual(stride, 1020, "Storage stride should be exactly 1020 bytes") + XCTAssertEqual(size, 1020, "Storage size should be exactly 1020 bytes") + } + + func testCapacityProperty() { + XCTAssertEqual(ByteStorage60.capacity, 60) + XCTAssertEqual(ByteStorage1020.capacity, 1020) + } + + // MARK: - Storage Creation Tests + + func testMakeCreatesZeroFilledStorage() { + let storage = ByteStorage1020.make() + + // Verify all bytes are zero + withUnsafeBytes(of: storage) { ptr in + let bytes = ptr.bindMemory(to: Int8.self) + for i in 0 ..< 1020 { + XCTAssertEqual(bytes[i], 0, "Byte at index \(i) should be zero") + } + } + } + + func testMakeCreatesCorrectSize() { + let storage = ByteStorage1020.make() + let size = MemoryLayout.size(ofValue: storage) + XCTAssertEqual(size, 1020) + } + + // MARK: - Storage Manipulation Tests + + func testStorageCanStoreAndRetrieveData() { + var storage = ByteStorage1020.make() + + // Write some test data directly + withUnsafeMutableBytes(of: &storage) { ptr in + let buffer = ptr.bindMemory(to: UInt8.self) + // Write some data + for i in 0 ..< 10 { + buffer[i] = UInt8(i) + } + } + + // Read and verify + withUnsafeBytes(of: storage) { ptr in + let buffer = ptr.bindMemory(to: UInt8.self) + for i in 0 ..< 10 { + XCTAssertEqual(buffer[i], UInt8(i)) + } + } + } + + func testStorageCanStoreMaximumContent() { + var storage = ByteStorage1020.make() + let capacity = ByteStorage1020.capacity + + // Write maximum content + withUnsafeMutableBytes(of: &storage) { ptr in + let buffer = ptr.bindMemory(to: UInt8.self) + for i in 0 ..< capacity { + buffer[i] = UInt8(i % 256) + } + } + + // Verify + withUnsafeBytes(of: storage) { ptr in + let buffer = ptr.bindMemory(to: UInt8.self) + for i in 0 ..< capacity { + XCTAssertEqual(buffer[i], UInt8(i % 256)) + } + } + } + + // MARK: - BasicType1024 Integration Tests + + func testBasicType1024UsesCorrectStorageSize() { + // BasicType1024 has: kind (UInt8, 1 byte) + length (UInt16, 2 bytes) + value (1020 bytes) + // With alignment, total should be around 1023-1024 bytes + let basicTypeSize = MemoryLayout.size + + // Verify the storage component is 1020 bytes + XCTAssertEqual(MemoryLayout.size, 1020) + + // BasicType1024 size should be at least kind + length + storage + let minimumSize = MemoryLayout.size + MemoryLayout.size + 1020 + XCTAssertGreaterThanOrEqual(basicTypeSize, minimumSize) + } + + func testBasicType1024Alignment() { + // Verify BasicType1024 is properly aligned + let alignment = MemoryLayout.alignment + XCTAssertGreaterThanOrEqual(alignment, 1) + } + + // MARK: - Edge Case Tests + + func testStorageIsValueType() { + let storage1 = ByteStorage1020.make() + var storage2 = storage1 + + // Modify storage2 + withUnsafeMutableBytes(of: &storage2) { ptr in + ptr.storeBytes(of: UInt16(42), toByteOffset: 0, as: UInt16.self) + } + + // Verify storage1 is unchanged (value semantics) + withUnsafeBytes(of: storage1) { ptr in + let value = ptr.loadUnaligned(fromByteOffset: 0, as: UInt16.self) + XCTAssertEqual(value, 0, "Original storage should be unchanged") + } + } + + func testMultipleStorageInstances() { + // Create multiple independent storage instances + let storage1 = ByteStorage1020.make() + let storage2 = ByteStorage1020.make() + let storage3 = ByteStorage1020.make() + + XCTAssertEqual(MemoryLayout.size(ofValue: storage1), 1020) + XCTAssertEqual(MemoryLayout.size(ofValue: storage2), 1020) + XCTAssertEqual(MemoryLayout.size(ofValue: storage3), 1020) + } +}