Cross-platform memory allocation observability for Swift
A focused library providing memory allocation tracking, leak detection, peak memory profiling, and allocation histograms across macOS and Linux.
- Allocation Tracking - Measure memory allocations in code blocks
- Leak Detection - Detect memory leaks by tracking net allocations
- Peak Memory Tracking - Monitor peak memory usage over time
- Allocation Profiling - Profile allocations with statistics and histograms
- Cross-Platform - Works on macOS and Linux with platform-optimized implementations
- Thread-Safe - All trackers are Sendable and safe for concurrent use
- Zero Dependencies - Pure Swift using only Darwin/Glibc platform primitives (no Foundation)
Add swift-memory-allocation to your Package.swift:
dependencies: [
.package(url: "https://github.com/coenttb/swift-memory-allocation", from: "0.1.0")
]Then add it to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "MemoryAllocation", package: "swift-memory-allocation")
]
)import MemoryAllocation
// Measure allocations in a code block
let (result, stats) = AllocationTracker.measure {
let array = Array(repeating: 0, count: 1000)
return array.count
}
print("Allocated \(stats.bytesAllocated) bytes")
print("Allocations: \(stats.allocations)")
print("Deallocations: \(stats.deallocations)")let detector = LeakDetector()
// Your code here
var leaked: [[Int]] = []
for i in 0..<100 {
leaked.append(Array(repeating: i, count: 100))
}
if detector.hasLeaks() {
print("Warning: \(detector.netAllocations) leaked allocations")
print("Net bytes: \(detector.netBytes)")
}
// Or assert no leaks
try detector.assertNoLeaks() // Throws if leaks detectedlet tracker = PeakMemoryTracker()
for i in 0..<100 {
let array = Array(repeating: 0, count: i * 100)
tracker.sample() // Record current memory
}
print("Peak memory: \(tracker.peakBytes) bytes")
print("Peak allocations: \(tracker.peakAllocations)")let profiler = AllocationProfiler()
// Profile multiple runs
for _ in 0..<100 {
profiler.profile {
let array = Array(repeating: 0, count: 1000)
_ = array.reduce(0, +)
}
}
// Analyze results
print("Mean: \(profiler.meanBytes) bytes")
print("Median: \(profiler.medianBytes) bytes")
print("P95: \(profiler.percentileBytes(95)) bytes")
print("P99: \(profiler.percentileBytes(99)) bytes")
// Generate histogram
let histogram = profiler.histogram(buckets: 10)
for bucket in histogram.buckets {
print("\(bucket.lowerBound)-\(bucket.upperBound): \(bucket.count) (\(bucket.frequency)%)")
}Uses malloc_zone_statistics which provides:
- Current memory snapshot
- Blocks in use
- Total bytes in use
Note: On Darwin platforms, allocation deltas represent snapshots and can be negative due to background cleanup. This is expected behavior.
Uses LD_PRELOAD malloc/free hooks which provide:
- Accurate allocation counting
- Deallocation tracking
- Per-thread statistics
Note: On Linux, you need to start tracking explicitly:
#if os(Linux)
AllocationStats.startTracking()
#endifCore type representing allocation statistics.
public struct AllocationStats: Sendable, Equatable {
public let allocations: Int
public let deallocations: Int
public let bytesAllocated: Int
public var netAllocations: Int { allocations - deallocations }
public static func capture() -> AllocationStats
public static func delta(from start: AllocationStats, to end: AllocationStats) -> AllocationStats
}Measure allocations in code blocks.
public enum AllocationTracker {
public static func measure<T>(_ operation: () throws -> T) rethrows -> (result: T, stats: AllocationStats)
public static func measure<T>(_ operation: () async throws -> T) async rethrows -> (result: T, stats: AllocationStats)
public static func measure(_ operation: () throws -> Void) rethrows -> AllocationStats
public static func measure(_ operation: () async throws -> Void) async rethrows -> AllocationStats
}Detect memory leaks by tracking net allocations.
public final class LeakDetector: Sendable {
public init()
public func hasLeaks() -> Bool
public var netAllocations: Int { get }
public var netBytes: Int { get }
public func delta() -> AllocationStats
public func assertNoLeaks(file: StaticString = #file, line: UInt = #line) throws
}Track peak memory usage.
public final class PeakMemoryTracker: Sendable {
public init()
public func sample()
public var peakBytes: Int { get }
public var peakAllocations: Int { get }
public var samples: [AllocationStats] { get }
public var current: AllocationStats { get }
public func reset()
public static func track<T>(_ operation: (PeakMemoryTracker) throws -> T) rethrows -> (result: T, peak: AllocationStats)
public static func track<T>(_ operation: (PeakMemoryTracker) async throws -> T) async rethrows -> (result: T, peak: AllocationStats)
}Profile allocations with statistics and histograms.
public final class AllocationProfiler: Sendable {
public init()
public func profile<T>(_ operation: () throws -> T) rethrows -> T
public func profile<T>(_ operation: () async throws -> T) async rethrows -> T
public var count: Int { get }
public var allMeasurements: [AllocationStats] { get }
public var meanBytes: Double { get }
public var medianBytes: Int { get }
public func percentileBytes(_ percentile: Int) -> Int
public var meanAllocations: Double { get }
public var medianAllocations: Int { get }
public func percentileAllocations(_ percentile: Int) -> Int
public func histogram(buckets: Int = 10) -> AllocationHistogram
public func reset()
}Track memory allocations in your test suite:
func testMemoryEfficiency() throws {
let stats = AllocationTracker.measure {
myExpensiveOperation()
}
// Assert maximum allocation budget
#expect(stats.bytesAllocated <= 1024 * 1024) // Max 1MB
}Profile allocations across builds:
let profiler = AllocationProfiler()
for _ in 0..<1000 {
profiler.profile {
criticalPath()
}
}
// Compare with baseline
let p95 = profiler.percentileBytes(95)
#expect(p95 <= baselineP95)Detect leaks in long-running services:
let detector = LeakDetector()
while running {
handleRequest()
if detector.netAllocations > threshold {
logger.warning("Potential leak: \(detector.netBytes) bytes")
}
}Understand allocation patterns:
let tracker = PeakMemoryTracker()
for batch in batches {
processBatch(batch)
tracker.sample()
}
print("Peak during processing: \(tracker.peakBytes) bytes")- Swift 6.2+
- macOS 13+, iOS 16+, watchOS 9+, tvOS 16+, or Linux
This library is released under the Apache License 2.0. See LICENSE for details.
- swift-testing-performance - Performance measurement traits for Swift Testing