├── Package.swift ├── Sources ├── Chronicle │ ├── Macro.swift │ ├── Logger.swift │ ├── StringCollector.swift │ ├── EntrySequence.swift │ ├── Buffer.swift │ ├── Loggable.swift │ ├── Entry.swift │ ├── Epilog.swift │ └── Chronicle.swift └── Macros │ └── Macro.swift ├── .gitignore ├── Tests └── ChronicleTests │ └── ChronicleTests.swift └── README.md /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import CompilerPluginSupport 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Chronicle", 8 | products: [ 9 | .library(name: "Chronicle", targets: ["Chronicle"]) 10 | ], 11 | targets: [ 12 | .target(name: "Chronicle", dependencies: ["Macros"]), 13 | .macro(name: "Macros"), 14 | .testTarget(name: "ChronicleTests", dependencies: ["Chronicle"]), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Sources/Chronicle/Macro.swift: -------------------------------------------------------------------------------- 1 | // Unused save for type checking macro arguments 2 | public struct _ChronicleLogMessage: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { 3 | public struct StringInterpolation: StringInterpolationProtocol { 4 | public init(literalCapacity: Int, interpolationCount: Int) { 5 | } 6 | 7 | public func appendLiteral(_ literal: StaticString) { 8 | } 9 | 10 | public func appendInterpolation(_: some _Loggable) { 11 | } 12 | } 13 | 14 | public init(stringLiteral value: StaticString) { 15 | } 16 | 17 | public init(stringInterpolation: StringInterpolation) { 18 | } 19 | } 20 | 21 | @freestanding(expression) 22 | public macro log(_ log: Logger, _ message: _ChronicleLogMessage) = #externalMacro(module: "Macros", type: "") 23 | -------------------------------------------------------------------------------- /Sources/Chronicle/Logger.swift: -------------------------------------------------------------------------------- 1 | import Darwin 2 | 3 | public struct Logger { 4 | let chronicle: Chronicle 5 | let id: UInt16 6 | // TODO: figure out how this should be synchronized 7 | public var enabled = true 8 | 9 | public func __prepare(size: Int) -> _LogBuffer? { 10 | let timestamp = mach_continuous_time() 11 | guard let buffer = chronicle.__prepare_log(size: MemoryLayout.size(ofValue: timestamp) + MemoryLayout.size(ofValue: id) + size) else { 12 | return nil 13 | } 14 | buffer.storeBytes(of: timestamp, as: type(of: timestamp)) 15 | buffer.storeBytes(of: id, toByteOffset: MemoryLayout.size(ofValue: timestamp), as: type(of: id)) 16 | #if DEBUG 17 | return UnsafeMutableRawBufferPointer(rebasing: buffer[buffer.startIndex.advanced(by: MemoryLayout.size(ofValue: timestamp) + MemoryLayout.size(ofValue: id))...]) 18 | #else 19 | return buffer.advanced(by: MemoryLayout.size(ofValue: timestamp) + MemoryLayout.size(ofValue: id)) 20 | #endif 21 | } 22 | 23 | public func __complete() { 24 | chronicle.__complete_log() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Chronicle/StringCollector.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MachO 3 | 4 | let _dyld_get_shared_cache_range = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY), "_dyld_get_shared_cache_range"), to: (@convention(c) (UnsafeMutablePointer) -> UnsafeRawPointer?)?.self)! 5 | 6 | enum StringCollector { 7 | static var initialized = false 8 | static var strings = [UnsafeRawBufferPointer]() 9 | static var _strings = Set() 10 | 11 | static func initializeIfNeeded() { 12 | guard !initialized else { 13 | return 14 | } 15 | initialized = true 16 | 17 | for i in 0..<_dyld_image_count() { 18 | Self.addStrings(from: _dyld_get_image_header(i)) 19 | } 20 | 21 | _dyld_register_func_for_add_image { header, _ in 22 | Self.addStrings(from: header!) 23 | } 24 | } 25 | 26 | static func addStrings(from image: UnsafePointer) { 27 | #if _pointerBitWidth(_64) 28 | let image = UnsafeRawPointer(image).assumingMemoryBound(to: mach_header_64.self) 29 | let getter = getsectiondata 30 | var size: UInt = 0 31 | #elseif _pointerBitWidth(_32) 32 | let getter = getsectdatafromheader 33 | var size: UInt32 = 0 34 | #else 35 | #error("Only 32- and 64-bit platforms are supported") 36 | #endif 37 | // This is where StaticStrings go. Ideally we'd get our own section for 38 | // just our log messages but this would require the ability to move 39 | // things there: https://github.com/apple/swift/issues/73218 40 | guard let base = UnsafeRawPointer(getter(image, "__TEXT", "__cstring", &size)), 41 | !_strings.contains(base) 42 | else { 43 | return 44 | } 45 | var extent = 0 46 | guard let range = _dyld_get_shared_cache_range(&extent), 47 | base < range || base >= range + extent else { 48 | return 49 | } 50 | 51 | strings.append(UnsafeRawBufferPointer(start: base, count: Int(size))) 52 | _strings.insert(base) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Chronicle/EntrySequence.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct EntrySequence: Sequence, IteratorProtocol { 4 | enum Position { 5 | case forward(Data.Index) 6 | case backward(Data.Index) 7 | 8 | var index: Data.Index { 9 | get { 10 | switch self { 11 | case .backward(let index), .forward(let index): 12 | return index 13 | } 14 | } 15 | set { 16 | switch self { 17 | case .backward(_): 18 | self = .backward(newValue) 19 | case .forward(_): 20 | self = .forward(newValue) 21 | } 22 | } 23 | } 24 | } 25 | 26 | let epilog: Epilog 27 | var position: Position 28 | 29 | init(epilog: Epilog) { 30 | self.epilog = epilog 31 | position = .backward(epilog.backwardBuffer.startIndex) 32 | } 33 | 34 | public mutating func next() -> Entry? { 35 | var index: Data.Index 36 | let buffer: Data 37 | switch position { 38 | case .backward(let _index): 39 | guard _index < epilog.backwardBuffer.endIndex else { 40 | position = .forward(epilog.forwardBuffer.startIndex) 41 | return next() 42 | } 43 | index = _index 44 | buffer = epilog.backwardBuffer 45 | case .forward(let _index): 46 | guard _index < epilog.forwardBuffer.endIndex else { 47 | return nil 48 | } 49 | index = _index 50 | buffer = epilog.forwardBuffer 51 | } 52 | 53 | func advance(by size: Int) -> Data.Index { 54 | precondition(buffer.endIndex - index >= size) 55 | return index + size 56 | } 57 | 58 | index = advance(by: MemoryLayout.size) 59 | 60 | var _index = index 61 | index = advance(by: MemoryLayout.size) 62 | let size = Int( 63 | buffer[_index...size) 72 | } 73 | 74 | return Entry(data: buffer[_index...size 19 | 20 | precondition(buffer.count >= minSize) 21 | self.buffer = buffer 22 | checkpoint = buffer.startIndex // progress for current block 23 | offset = checkpoint.advanced(by: minSize) // for the next block header 24 | 25 | } 26 | 27 | func wraparound(size: Int) -> Bool { 28 | offset = buffer.endIndex 29 | var distance = UInt(offset - checkpoint - 1) 30 | 31 | checkpoint = offset - 1 32 | buffer[checkpoint] = 0 33 | 34 | repeat { 35 | let bits = UInt8(distance & 0b0111_1111) | 0b1000_0000 36 | offset -= 1 37 | buffer[offset] = bits 38 | distance >>= 7 39 | } while distance != 0 40 | 41 | buffer[offset] &= ~0b1000_0000 42 | 43 | checkpoint = buffer.startIndex 44 | offset = checkpoint.advanced(by: MemoryLayout.size) 45 | 46 | return offset + size < buffer.count 47 | } 48 | 49 | func reserve(size: Int) -> _LogBuffer? { 50 | updateProgress(.preparing) 51 | 52 | #if DEBUG 53 | let _size = Size(size) 54 | #else 55 | let _size = Size(UInt(bitPattern: size)) 56 | #endif 57 | 58 | let totalSize = 59 | MemoryLayout.size(ofValue: _size) // size 60 | + size // block data 61 | + MemoryLayout.size(ofValue: _size) // backwards size 62 | + MemoryLayout.size // progress for next block 63 | 64 | guard offset + totalSize <= buffer.count || wraparound(size: totalSize) else { 65 | // TODO: Note dropped oversize messages? 66 | return nil 67 | } 68 | 69 | buffer.storeBytes(of: _size, toByteOffset: offset, as: type(of: _size)) 70 | updateProgress(.prepared) 71 | 72 | #if DEBUG 73 | offset += MemoryLayout.size(ofValue: _size) 74 | defer { 75 | offset += size + MemoryLayout.size(ofValue: _size) + MemoryLayout.size 76 | } 77 | #else 78 | offset &+= MemoryLayout.size(ofValue: _size) 79 | defer { 80 | offset &+= size &+ MemoryLayout.size(ofValue: _size) &+ MemoryLayout.size 81 | } 82 | #endif 83 | 84 | let base = buffer.startIndex.advanced(by: offset) 85 | #if DEBUG 86 | return UnsafeMutableRawBufferPointer(rebasing: buffer[base.. Int = { $0 - $1 } 96 | #else 97 | let subtract: (Int, Int) -> Int = { $0 &- $1 } 98 | #endif 99 | let newCheckpoint = subtract(offset, 1) 100 | 101 | updateProgress(.completing) 102 | 103 | #if DEBUG 104 | let size = Size(subtract(newCheckpoint, oldCheckpoint)) 105 | #else 106 | let size = Size(truncatingIfNeeded: subtract(newCheckpoint, oldCheckpoint)) 107 | #endif 108 | buffer.storeBytes(of: size, toByteOffset: subtract(newCheckpoint, MemoryLayout.size(ofValue: size)), as: type(of: size)) 109 | 110 | updateProgress(.completed) 111 | 112 | checkpoint = newCheckpoint 113 | updateProgress(.unused) 114 | checkpoint = oldCheckpoint 115 | updateProgress(.used) 116 | checkpoint = newCheckpoint 117 | } 118 | 119 | func updateProgress(_ progress: Progress) { 120 | buffer[checkpoint] = progress.rawValue 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Chronicle/Loggable.swift: -------------------------------------------------------------------------------- 1 | public protocol __LogContext { 2 | associatedtype T 3 | init(value: T) 4 | } 5 | 6 | public struct _PassthroughLogContext: __LogContext { 7 | @usableFromInline 8 | let value: T 9 | 10 | public init(value: T) { 11 | self.value = value 12 | } 13 | } 14 | 15 | public protocol _Loggable where _LogContext: __LogContext, _LogContext.T == Self { 16 | associatedtype _LogContext 17 | 18 | static var __log_type: UInt8 { get } 19 | static func __log_size(context: _LogContext) -> Int 20 | 21 | static func __log(into buffer: _LogBuffer, context: _LogContext) 22 | } 23 | 24 | protocol __Loggable: _Loggable { 25 | static var ___log_type: StaticString { get } 26 | } 27 | 28 | extension __Loggable { 29 | // The optimizer seems to need this to coalesce writes of these together 30 | @_transparent 31 | public static var __log_type: UInt8 { 32 | ___log_type.utf8Start.pointee 33 | } 34 | } 35 | 36 | public protocol _TriviallyLoggable { 37 | } 38 | 39 | extension _Loggable where Self: _TriviallyLoggable { 40 | public static func __log_size(context: _LogContext) -> Int { 41 | MemoryLayout.size 42 | } 43 | 44 | // Otherwise this doesn't inline fully 45 | @_transparent 46 | public static func __log(into buffer: _LogBuffer, context: _PassthroughLogContext) { 47 | buffer.storeBytes(of: context.value, as: Self.self) 48 | } 49 | } 50 | 51 | extension Int8: __Loggable, _TriviallyLoggable { 52 | static let ___log_type: StaticString = "1" 53 | } 54 | 55 | extension Int16: __Loggable, _TriviallyLoggable { 56 | static let ___log_type: StaticString = "2" 57 | } 58 | 59 | extension Int32: __Loggable, _TriviallyLoggable { 60 | static let ___log_type: StaticString = "4" 61 | } 62 | 63 | extension Int64: __Loggable, _TriviallyLoggable { 64 | static let ___log_type: StaticString = "8" 65 | } 66 | 67 | extension Int: __Loggable, _TriviallyLoggable { 68 | static let ___log_type: StaticString = "i" 69 | } 70 | 71 | extension UInt8: __Loggable, _TriviallyLoggable { 72 | static let ___log_type: StaticString = "!" 73 | } 74 | 75 | extension UInt16: __Loggable, _TriviallyLoggable { 76 | static let ___log_type: StaticString = "@" 77 | } 78 | 79 | extension UInt32: __Loggable, _TriviallyLoggable { 80 | static let ___log_type: StaticString = "$" 81 | } 82 | 83 | extension UInt64: __Loggable, _TriviallyLoggable { 84 | static let ___log_type: StaticString = "*" 85 | } 86 | 87 | extension UInt: __Loggable, _TriviallyLoggable { 88 | static let ___log_type: StaticString = "I" 89 | } 90 | 91 | extension Float: __Loggable, _TriviallyLoggable { 92 | static let ___log_type: StaticString = "f" 93 | } 94 | 95 | extension Double: __Loggable, _TriviallyLoggable { 96 | static let ___log_type: StaticString = "F" 97 | } 98 | 99 | extension Bool: __Loggable, _TriviallyLoggable { 100 | static let ___log_type: StaticString = "b" 101 | } 102 | 103 | extension String: __Loggable { 104 | public struct _LogContext: __LogContext { 105 | @usableFromInline 106 | var utf8: String 107 | 108 | public init(value: String) { 109 | utf8 = value 110 | utf8.makeContiguousUTF8() 111 | } 112 | 113 | func ensureContiguousUTF8Optimizations() { 114 | guard utf8.isContiguousUTF8 else { 115 | // Poor man's Builtin.unreachable() 116 | unsafeBitCast((), to: Never.self) 117 | } 118 | } 119 | } 120 | 121 | static let ___log_type: StaticString = "s" 122 | 123 | public static func __log_size(context: _LogContext) -> Int { 124 | context.ensureContiguousUTF8Optimizations() 125 | let count = context.utf8.utf8.count 126 | return MemoryLayout.size(ofValue: count) + count 127 | } 128 | 129 | // This is intentionally not @_transparent so it gets outlined. It would 130 | // help performance a little bit but the code is kind of big. The inline 131 | // marker avoids a thunk from being generated for no reason. 132 | @inline(__always) 133 | public static func __log(into buffer: _LogBuffer, context: _LogContext) { 134 | context.ensureContiguousUTF8Optimizations() 135 | var copy = context 136 | copy.utf8.withUTF8 { 137 | buffer.storeBytes(of: $0.count, as: type(of: $0.count)) 138 | #if DEBUG 139 | buffer.baseAddress!.advanced(by: MemoryLayout.size(ofValue: $0.count)).copyMemory(from: $0.baseAddress!, byteCount: $0.count) 140 | #else 141 | buffer.advanced(by: MemoryLayout.size(ofValue: $0.count)).copyMemory(from: $0.baseAddress!, byteCount: $0.count) 142 | #endif 143 | } 144 | } 145 | } 146 | 147 | extension StaticString: __Loggable { 148 | static let ___log_type: StaticString = "S" 149 | 150 | public static func __log_size(context: _LogContext) -> Int { 151 | MemoryLayout.size 152 | } 153 | 154 | public static func __log(into buffer: _LogBuffer, context: _PassthroughLogContext) { 155 | // TODO: handle non-pointer StaticStrings 156 | buffer.storeBytes(of: UInt(bitPattern: context.value.utf8Start), as: UInt.self) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Sources/Chronicle/Entry.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Field: CustomStringConvertible { 4 | case int(Int64) 5 | case uint(UInt64) 6 | case float(Double) 7 | case bool(Bool) 8 | case string(String) 9 | case literal(String) 10 | 11 | public var description: String { 12 | switch self { 13 | case .int(let value): 14 | value.description 15 | case .uint(let value): 16 | value.description 17 | case .float(let value): 18 | value.description 19 | case .bool(let value): 20 | value.description 21 | case .string(let value): 22 | value.description 23 | case .literal(let value): 24 | value.description 25 | } 26 | } 27 | } 28 | 29 | public struct Entry: CustomStringConvertible { 30 | public let timestamp: Date 31 | public let logger: String 32 | public let fields: [Field] 33 | 34 | init(data: Data, epilog: Epilog) { 35 | var position = data.startIndex 36 | 37 | func read(size: Int) -> Data { 38 | precondition(data.endIndex - position >= size) 39 | let _position = position 40 | position += size 41 | return data[_position..(_ t: T.Type) -> T { 45 | read(size: MemoryLayout.size).withUnsafeBytes { 46 | $0.loadUnaligned(as: T.self) 47 | } 48 | } 49 | 50 | let timestamp = read(UInt64.self) 51 | let base = TimeInterval(epilog.timing.seconds) + TimeInterval(epilog.timing.nanoseconds) / 1e9 52 | let first: UInt64 53 | let second: UInt64 54 | let sign: TimeInterval 55 | if timestamp < epilog.timing.timestamp { 56 | first = timestamp 57 | second = epilog.timing.timestamp 58 | sign = -1 59 | } else { 60 | first = epilog.timing.timestamp 61 | second = timestamp 62 | sign = 1 63 | } 64 | let offset = sign * TimeInterval(UInt64(epilog.timing.numerator) * (second - first)) / TimeInterval(epilog.timing.denominator) / 1e9 65 | self.timestamp = Date(timeIntervalSince1970: base + offset) 66 | 67 | logger = epilog.loggers[Int(read(UInt16.self))] 68 | let count = Int(read(UInt8.self)) 69 | let types = read(size: count) 70 | 71 | var fields = [Field]() 72 | 73 | for i in 0.. Data) throws { 28 | let buffer = try Data(contentsOf: url.appendingPathComponent(Chronicle.bufferPath)) 29 | let metadata = try JSONDecoder().decode(Metadatav1.self, from: try Data(contentsOf: url.appendingPathComponent(Chronicle.metadataPath))) 30 | 31 | let _strings = url.appendingPathComponent(Chronicle.stringsPath) 32 | let strings: [(UInt64, Data)] = try FileManager.default.contentsOfDirectory(atPath: _strings.path).compactMap { 33 | guard let base = UInt64($0, radix: Metadatav1.radix) else { 34 | return nil 35 | } 36 | return (base, try stringsTransform(Data(contentsOf: _strings.appendingPathComponent($0)), metadata)) 37 | } 38 | self.init(buffer: buffer, metadata: metadata, strings: strings) 39 | } 40 | 41 | public init(url: URL) throws { 42 | try self.init(_url: url) { 43 | guard $1.compressedStrings else { 44 | return $0 45 | } 46 | guard #available(macOS 10.15, iOS 13, macCatalyst 13.1, tvOS 13, watchOS 6, *) else { 47 | preconditionFailure() 48 | } 49 | return try ($0 as NSData).decompressed(using: .lzfse) as Data 50 | } 51 | } 52 | 53 | static func readLogs(in buffer: Data) -> (Data, Data) { 54 | var position = buffer.startIndex 55 | 56 | var last: Data.Index! 57 | var forward: Data! 58 | 59 | while true { 60 | var _last: Data.Index = position.advanced(by: MemoryLayout.size) 61 | var advances = [() -> Void]() 62 | var next = false 63 | 64 | func advance(by size: @escaping @autoclosure () -> Int) { 65 | advances.append({ 66 | let size = size() 67 | precondition(buffer.endIndex - _last >= size) 68 | _last += size 69 | }) 70 | } 71 | 72 | switch Buffer.Progress(rawValue: buffer[position])! { 73 | case .used: 74 | next = true 75 | fallthrough 76 | case .completed: 77 | advance(by: MemoryLayout.size) 78 | fallthrough 79 | case .completing: 80 | advance(by: MemoryLayout.size) 81 | fallthrough 82 | case .prepared: 83 | advance( 84 | by: Int( 85 | buffer[_last...].withUnsafeBytes { 86 | $0.baseAddress!.loadUnaligned(fromByteOffset: -MemoryLayout.size, as: Buffer.Size.self) 87 | })) 88 | fallthrough 89 | case .preparing: 90 | advance(by: MemoryLayout.size) 91 | case .unused: 92 | advance(by: MemoryLayout.size) 93 | } 94 | 95 | for advance in advances.reversed() { 96 | advance() 97 | } 98 | 99 | guard next else { 100 | forward = buffer[...size 106 | } 107 | 108 | var distance = 0 109 | var trailer = buffer.endIndex 110 | 111 | var bits: UInt8 112 | repeat { 113 | trailer -= 1 114 | // We've clobbered the trailer. This means we've gone close enough 115 | // to the end of the buffer that we aren't going to have any 116 | // backward entries. (The largest distance we can represent in a 117 | // 64-bit ULEB-128 integer is not enough to fit an entry.) 118 | guard trailer >= last else { 119 | return (forward, Data()) 120 | } 121 | bits = buffer[trailer] 122 | distance |= Int(bits & 0b0111_1111) << (7 * (buffer.index(before: buffer.endIndex) - trailer)) 123 | } while bits & 0b1000_0000 != 0 124 | distance += 1 125 | 126 | var start = buffer.endIndex 127 | 128 | func retreat(size: Int) -> Int? { 129 | guard start - last >= size else { 130 | return nil 131 | } 132 | return start - size 133 | } 134 | 135 | start = retreat(size: distance)! 136 | let end = start 137 | 138 | while var _start = retreat(size: MemoryLayout.size) { 139 | let size = buffer[_start...].withUnsafeBytes { 140 | $0.loadUnaligned(as: Buffer.Size.self) 141 | } 142 | 143 | // If size is zero, we never wrote a backward chain. 144 | guard size != 0 else { 145 | return (forward, Data()) 146 | } 147 | 148 | swap(&start, &_start) 149 | guard let __start = retreat(size: Int(size) - MemoryLayout.size) else { 150 | return (forward, buffer[_start.. [(UInt64, String)] { 159 | strings.split(separator: 0, omittingEmptySubsequences: false).compactMap { 160 | guard let string = String(data: $0, encoding: .utf8) else { 161 | return nil 162 | } 163 | return (UInt64($0.startIndex - strings.startIndex) + base, string) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Sources/Chronicle/Chronicle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os 3 | 4 | // Enables intra-object bounds tracking in the log buffer 5 | #if DEBUG 6 | public typealias _LogBuffer = UnsafeMutableRawBufferPointer 7 | #else 8 | public typealias _LogBuffer = UnsafeMutableRawPointer 9 | #endif 10 | 11 | public struct Metadatav1: Codable { 12 | public struct Timing: Codable { 13 | public let numerator: UInt32 14 | public let denominator: UInt32 15 | public let timestamp: UInt64 16 | public let seconds: Int 17 | public let nanoseconds: Int 18 | } 19 | 20 | public struct Strings: Codable { 21 | public let start: UInt64 22 | public let size: UInt64 23 | } 24 | 25 | public var version = 1 26 | public var bitWidth = Int.bitWidth 27 | public var compressedStrings: Bool = { 28 | if #available(macOS 10.15, iOS 13, macCatalyst 13.1, tvOS 13, watchOS 6, *) { 29 | return true 30 | } else { 31 | return false 32 | } 33 | }() 34 | public let strings: [Strings] 35 | public let loggers: [String] 36 | public let timing: Timing 37 | 38 | static let radix = 0x10 39 | } 40 | 41 | public typealias Metadata = Metadatav1 42 | 43 | public struct Chronicle { 44 | class Guts { 45 | let lock: UnsafeMutablePointer 46 | let cleanup: (() -> Void)? 47 | var loggers = [String]() 48 | 49 | init(cleanup: (() -> Void)?) { 50 | lock = .allocate(capacity: 1) 51 | lock.initialize(to: .init()) 52 | self.cleanup = cleanup 53 | } 54 | 55 | func addLogger(named name: String) -> UInt16 { 56 | defer { 57 | loggers.append(name) 58 | } 59 | return UInt16(loggers.count) 60 | } 61 | 62 | deinit { 63 | lock.deallocate() 64 | cleanup?() 65 | } 66 | } 67 | 68 | static let metadataPath = "metadata.json" 69 | static let bufferPath = "buffer" 70 | static let stringsPath = "strings" 71 | 72 | let metadataUpdate: (Metadata, [UnsafeRawBufferPointer]) throws -> Void 73 | let buffer: Buffer 74 | let guts: Guts 75 | 76 | public init(buffer: UnsafeMutableRawBufferPointer, cleanup: (() -> Void)? = nil, metadataUpdate: @escaping (Metadata, [UnsafeRawBufferPointer]) throws -> Void) rethrows { 77 | StringCollector.initializeIfNeeded() 78 | 79 | self.buffer = Buffer(buffer: buffer) 80 | guts = Guts(cleanup: cleanup) 81 | self.metadataUpdate = metadataUpdate 82 | try metadataUpdate(updatedMetadata(), StringCollector.strings) 83 | } 84 | 85 | public init(url: URL, bufferSize: Int) throws { 86 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) 87 | 88 | let strings = url.appendingPathComponent(Self.stringsPath) 89 | try FileManager.default.createDirectory(at: strings, withIntermediateDirectories: false) 90 | 91 | let _metadata = open(url.appendingPathComponent(Self.metadataPath).path, O_RDWR | O_CREAT, 0o644) 92 | guard _metadata >= 0 else { 93 | throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) 94 | } 95 | let metadata = FileHandle(fileDescriptor: _metadata) 96 | 97 | let fd = open(url.appendingPathComponent(Self.bufferPath).path, O_RDWR | O_CREAT, 0o644) 98 | guard fd >= 0 else { 99 | throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) 100 | } 101 | guard ftruncate(fd, off_t(bufferSize)) >= 0 else { 102 | throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) 103 | } 104 | 105 | let _buffer = mmap(nil, bufferSize, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0) 106 | guard _buffer != MAP_FAILED else { 107 | throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) 108 | } 109 | close(fd) 110 | let buffer = UnsafeMutableRawBufferPointer(start: _buffer, count: bufferSize) 111 | 112 | try self.init( 113 | buffer: buffer, 114 | cleanup: { 115 | munmap(buffer.baseAddress, buffer.count) 116 | }, 117 | metadataUpdate: { 118 | metadata.seek(toFileOffset: 0) 119 | try metadata.write(JSONEncoder().encode($0)) 120 | let existing = try Set(FileManager.default.contentsOfDirectory(atPath: strings.path).compactMap { 121 | UInt($0, radix: Metadata.radix).map(UnsafeRawPointer.init) 122 | }) 123 | for data in $1 where !existing.contains(data.baseAddress) { 124 | var name = String(UInt(bitPattern: data.baseAddress), radix: Metadata.radix) 125 | while name.count < MemoryLayout.size * (1 << 8).trailingZeroBitCount / Metadata.radix.trailingZeroBitCount { 126 | name = "0\(name)" 127 | } 128 | if #available(macOS 10.15, iOS 13, macCatalyst 13.1, tvOS 13, watchOS 6, *) { 129 | try NSData(bytesNoCopy: UnsafeMutableRawPointer(mutating: data.baseAddress!), length: data.count, freeWhenDone: false).compressed(using: .lzfse).write(to: strings.appendingPathComponent(name)) 130 | } else { 131 | try Data(data).write(to: strings.appendingPathComponent(name)) 132 | } 133 | } 134 | }) 135 | } 136 | 137 | func updatedMetadata() -> Metadata { 138 | var timebase = mach_timebase_info() 139 | mach_timebase_info(&timebase) 140 | 141 | var time = timespec() 142 | clock_gettime(CLOCK_REALTIME, &time) 143 | 144 | return Metadata( 145 | strings: StringCollector.strings.map { 146 | .init(start: UInt64(UInt(bitPattern: $0.baseAddress)), size: UInt64($0.count)) 147 | }, 148 | loggers: guts.loggers, 149 | timing: .init( 150 | numerator: timebase.numer, 151 | denominator: timebase.denom, 152 | timestamp: mach_continuous_time(), 153 | seconds: time.tv_sec, 154 | nanoseconds: time.tv_nsec 155 | ) 156 | ) 157 | } 158 | 159 | public func logger(name: String) throws -> Logger { 160 | os_unfair_lock_lock(guts.lock) 161 | let id = guts.addLogger(named: name) 162 | let metadata = updatedMetadata() 163 | os_unfair_lock_unlock(guts.lock) 164 | 165 | try metadataUpdate(metadata, StringCollector.strings) 166 | return Logger(chronicle: self, id: id) 167 | } 168 | 169 | func __prepare_log(size: Int) -> _LogBuffer? { 170 | os_unfair_lock_lock(guts.lock) 171 | guard let buffer = buffer.reserve(size: size) else { 172 | os_unfair_lock_unlock(guts.lock) 173 | return nil 174 | } 175 | return buffer 176 | } 177 | 178 | func __complete_log() { 179 | buffer.complete() 180 | os_unfair_lock_unlock(guts.lock) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Tests/ChronicleTests/ChronicleTests.swift: -------------------------------------------------------------------------------- 1 | import Chronicle 2 | import Foundation 3 | import XCTest 4 | 5 | final class ChronicleTests: XCTestCase { 6 | static func inMemoryChronicleWithBuffer(size: Int, metadataUpdate: ((Metadata, [UnsafeRawBufferPointer]) -> Void)? = nil) -> (Chronicle, UnsafeMutableRawBufferPointer) { 7 | let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: size, alignment: 1) 8 | return ( 9 | Chronicle( 10 | buffer: buffer, 11 | cleanup: { 12 | buffer.deallocate() 13 | } 14 | ) { 15 | metadataUpdate?($0, $1) 16 | }, buffer 17 | ) 18 | } 19 | 20 | static func inMemoryChronicle(size: Int, metadataUpdate: ((Metadata, [UnsafeRawBufferPointer]) -> Void)? = nil) -> Chronicle { 21 | inMemoryChronicleWithBuffer(size: size, metadataUpdate: metadataUpdate).0 22 | } 23 | 24 | func testCreation() { 25 | let chronicle = Self.inMemoryChronicle(size: 1_000) 26 | _ = chronicle 27 | } 28 | 29 | func testBasicUsage() throws { 30 | let chronicle = Self.inMemoryChronicle(size: 1_000) 31 | let logger = try chronicle.logger(name: "test") 32 | #log(logger, "Invocation: \(String(cString: getprogname())) [\(CommandLine.argc) arguments]") 33 | #log(logger, "It's been \(Date.now.timeIntervalSince1970) seconds since the epoch") 34 | } 35 | 36 | func testMetadata() { 37 | var _metadata: Metadata? 38 | let chronicle = Self.inMemoryChronicle(size: 1) { metadata, _ in 39 | _metadata = metadata 40 | } 41 | _ = chronicle 42 | let metadata = _metadata! 43 | 44 | let string: StaticString = "test" 45 | 46 | XCTAssertNotNil(metadata.strings.first { 47 | memmem(UnsafeRawPointer(bitPattern: UInt($0.start)), Int($0.size), string.utf8Start, string.utf8CodeUnitCount) != nil 48 | }) 49 | 50 | XCTAssertEqual(TimeInterval(metadata.timing.seconds), Date().timeIntervalSince1970, accuracy: 1) 51 | let time = mach_continuous_time() 52 | XCTAssertEqual(TimeInterval(time - metadata.timing.timestamp) * TimeInterval(metadata.timing.numerator) / TimeInterval(metadata.timing.denominator) / TimeInterval(NSEC_PER_SEC), 0, accuracy: 1) 53 | } 54 | 55 | func testLoggers() throws { 56 | let loggers = ["LoggerA", "LoggerB"] 57 | 58 | var _metadata: Metadata? 59 | let chronicle = Self.inMemoryChronicle(size: 1) { metadata, _ in 60 | _metadata = metadata 61 | } 62 | for logger in loggers { 63 | _ = try chronicle.logger(name: logger) 64 | } 65 | XCTAssertEqual(loggers, _metadata!.loggers) 66 | } 67 | 68 | func testFormat() throws { 69 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: 1_000) 70 | let logger = try chronicle.logger(name: "test") 71 | let string = String(cString: getprogname()) 72 | let number = CommandLine.argc 73 | 74 | #log(logger, "Invocation: \(string) [\(number) arguments]") 75 | 76 | func bytes(of value: T) -> [UInt8] { 77 | withUnsafeBytes(of: value) { 78 | Array($0) 79 | } 80 | } 81 | 82 | let s1: StaticString = "Invocation: " 83 | let s2: StaticString = " [" 84 | let s3: StaticString = " arguments]" 85 | 86 | let types = [ 87 | StaticString.__log_type, 88 | String.__log_type, 89 | StaticString.__log_type, 90 | CInt.__log_type, 91 | StaticString.__log_type, 92 | ] 93 | 94 | let header = [ 95 | UInt8(0x04) // .used 96 | ] 97 | let nextHeader = [ 98 | UInt8(0x00) // .unused 99 | ] 100 | 101 | let timestamp: UInt64 = 0 102 | let loggerID: UInt16 = 0 103 | 104 | let payload = 105 | bytes(of: timestamp) 106 | + bytes(of: loggerID) 107 | + [ 108 | UInt8(types.count) 109 | ] + types 110 | + bytes(of: s1.utf8Start) 111 | + bytes(of: string.utf8.count) + Array(string.utf8) 112 | + bytes(of: s2.utf8Start) 113 | + bytes(of: number) 114 | + bytes(of: s3.utf8Start) 115 | let payloadCount = UInt32(payload.count) 116 | let trailingCount = UInt32(header.count + MemoryLayout.size(ofValue: payloadCount) + MemoryLayout.size(ofValue: payloadCount)) + payloadCount 117 | 118 | buffer.storeBytes(of: timestamp, toByteOffset: header.count + MemoryLayout.size(ofValue: payloadCount), as: type(of: timestamp)) 119 | 120 | let expected = 121 | header 122 | + bytes(of: payloadCount) 123 | + payload 124 | + bytes(of: trailingCount) 125 | + nextHeader 126 | XCTAssert(expected.elementsEqual(buffer.prefix(expected.count))) 127 | } 128 | 129 | func testOverflow() throws { 130 | let chronicle = Self.inMemoryChronicle(size: 1) 131 | let logger = try chronicle.logger(name: "test") 132 | #log(logger, "test") 133 | } 134 | 135 | func testTrailer() throws { 136 | let baseSize = 137 | MemoryLayout.size // header 138 | + MemoryLayout.size // payload size 139 | + MemoryLayout.size // timestamp 140 | + MemoryLayout.size // logger ID 141 | + MemoryLayout.size // argument count 142 | + 1 // types 143 | + MemoryLayout.size // string count 144 | + MemoryLayout.size // trailing size 145 | + MemoryLayout.size // next header 146 | 147 | var string = "" 148 | do { 149 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: baseSize) 150 | let logger = try chronicle.logger(name: "test") 151 | 152 | #log(logger, "\(string)") 153 | #log(logger, "\(string)") 154 | 155 | XCTAssertEqual(buffer.last, 0) 156 | } 157 | 158 | do { 159 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: baseSize + 1) 160 | let logger = try chronicle.logger(name: "test") 161 | 162 | #log(logger, "\(string)") 163 | #log(logger, "\(string)") 164 | 165 | XCTAssertEqual(buffer.last, 1) 166 | } 167 | 168 | string = "a" 169 | do { 170 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: baseSize + 1) 171 | let logger = try chronicle.logger(name: "test") 172 | 173 | #log(logger, "\(string)") 174 | #log(logger, "\(string)") 175 | 176 | XCTAssertEqual(buffer.last, 0) 177 | } 178 | 179 | string = String(repeating: "a", count: baseSize) 180 | do { 181 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: baseSize * 3) 182 | let logger = try chronicle.logger(name: "test") 183 | 184 | #log(logger, "\(string)") 185 | #log(logger, "\(string)") 186 | 187 | XCTAssertEqual(buffer.last, UInt8(baseSize)) 188 | } 189 | 190 | let count = 1_000 191 | string = String(repeating: "a", count: count) 192 | do { 193 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: baseSize + string.count * 2) 194 | let logger = try chronicle.logger(name: "test") 195 | 196 | #log(logger, "\(string)") 197 | #log(logger, "\(string)") 198 | 199 | var count = UInt(count) 200 | var _buffer = Array(buffer) 201 | while count != 0 { 202 | XCTAssertEqual(_buffer.popLast()! & ~0b1000_0000, UInt8(count & 0b111_1111)) 203 | count >>= 7 204 | } 205 | } 206 | } 207 | 208 | func testDisabled() throws { 209 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: 1_000) 210 | var logger = try chronicle.logger(name: "test") 211 | logger.enabled = false 212 | #log(logger, "Invocation: \(String(cString: getprogname())) [\(CommandLine.argc) arguments]") 213 | #log(logger, "It's been \(Date.now.timeIntervalSince1970) seconds since the epoch") 214 | XCTAssert(buffer.allSatisfy { 215 | $0 == 0 216 | }) 217 | } 218 | 219 | func testEpilog() throws { 220 | var metadata: Metadata? 221 | var strings: [UnsafeRawBufferPointer]? 222 | let (chronicle, buffer) = Self.inMemoryChronicleWithBuffer(size: 1_000) { 223 | metadata = $0 224 | strings = $1 225 | } 226 | 227 | let logger = try chronicle.logger(name: "test") 228 | #log(logger, "Invocation: \(String(cString: getprogname())) [\(CommandLine.argc) arguments]") 229 | 230 | let epilog = Epilog(buffer: Data(buffer), metadata: metadata!, strings: strings!.map { 231 | (UInt64(UInt(bitPattern: $0.baseAddress)), Data($0)) 232 | }) 233 | 234 | XCTAssert(["Invocation: \(String(cString: getprogname())) [\(CommandLine.argc) arguments]"].elementsEqual(epilog.entries.map { 235 | $0.fields.map(\.description).joined() 236 | })) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Sources/Macros/Macro.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct IO { 4 | enum HostToPluginMessage: Codable { 5 | struct Syntax: Codable { 6 | var source: String 7 | } 8 | 9 | case getCapability 10 | case expandFreestandingMacro(syntax: Syntax) 11 | } 12 | 13 | enum PluginToHostMessage: Codable { 14 | struct PluginCapability: Codable { 15 | var protocolVersion: Int = 1 16 | } 17 | 18 | case getCapabilityResult(capability: PluginCapability) 19 | case expandMacroResult(expandedSource: String?, diagnostics: [Never]) 20 | } 21 | 22 | func read() -> Data? { 23 | var size: UInt64 = 0 24 | let data = FileHandle.standardInput.readData(ofLength: MemoryLayout.size(ofValue: size)) 25 | guard data.count == MemoryLayout.size(ofValue: size) else { 26 | // Generally means stdin is closed, so nothing required of us 27 | return nil 28 | } 29 | size = data.withUnsafeBytes { 30 | $0.loadUnaligned(as: type(of: size)) 31 | } 32 | return FileHandle.standardInput.readData(ofLength: Int(size)) 33 | } 34 | 35 | func write(_ data: Data) { 36 | var count = data.count 37 | withUnsafeBytes(of: &count) { 38 | FileHandle.standardOutput.write(Data($0)) 39 | } 40 | FileHandle.standardOutput.write(data) 41 | } 42 | 43 | func receive() throws -> HostToPluginMessage? { 44 | try read().flatMap { 45 | try JSONDecoder().decode(HostToPluginMessage.self, from: $0) 46 | } 47 | } 48 | 49 | func reply(_ message: PluginToHostMessage) throws { 50 | try write(JSONEncoder().encode(message)) 51 | } 52 | } 53 | 54 | @main 55 | struct Main { 56 | static func stripComments(_ source: String) -> String { 57 | var stripped = [Character]() 58 | 59 | enum State { 60 | case code 61 | case start(Int) 62 | case comment(Int) 63 | case end(Int) 64 | } 65 | 66 | var state = State.code 67 | for c in source { 68 | switch (state, c) { 69 | case (.code, "/"): 70 | state = .start(0) 71 | case (.code, _): 72 | stripped.append(c) 73 | case let (.start(level), "*"): 74 | state = .comment(level + 1) 75 | case let (.start(level), _): 76 | if level == 0 { 77 | state = .code 78 | stripped.append("/") 79 | stripped.append(c) 80 | } 81 | case let (.comment(level), "*"): 82 | state = .end(level) 83 | case (.comment, _): 84 | break 85 | case let (.end(level), "/"): 86 | if level == 0 { 87 | state = .code 88 | } else { 89 | state = .comment(level - 1) 90 | } 91 | case let (.end(level), _): 92 | state = .comment(level) 93 | } 94 | } 95 | 96 | return String(stripped).split(separator: "\n").filter { 97 | !$0.trimmingCharacters(in: .whitespacesAndNewlines).starts(with: "//") 98 | }.joined(separator: "\n") 99 | } 100 | 101 | static func parseMessage(_ message: String) -> ([String], [String])? { 102 | var arguments = [String]() 103 | var strings = [String]() 104 | 105 | enum State { 106 | case literal([Character]) 107 | case escape([Character]) 108 | case interpolation(Int, [Character]) 109 | } 110 | 111 | var state = State.literal([]) 112 | for c in message { 113 | switch (state, c) { 114 | case (let .literal(string), "\\"): 115 | state = .escape(string) 116 | case (let .literal(string), _): 117 | state = .literal(string + [c]) 118 | case (let .escape(string), "("): 119 | if !string.isEmpty { 120 | arguments.append("string\(strings.count)") 121 | strings.append(String(string)) 122 | } 123 | state = .interpolation(1, []) 124 | case (let .escape(string), _): 125 | state = .literal(string + ["\\", c]) 126 | case (let .interpolation(level, string), "("): 127 | state = .interpolation(level + 1, string + [c]) 128 | case (let .interpolation(level, string), ")"): 129 | let newLevel = level - 1 130 | if newLevel == 0 { 131 | arguments.append(String(string)) 132 | state = .literal([]) 133 | } else { 134 | state = .interpolation(level - 1, string + [c]) 135 | } 136 | case (let .interpolation(level, string), _): 137 | state = .interpolation(level, string + [c]) 138 | } 139 | } 140 | 141 | switch state { 142 | case let .literal(string): 143 | if !string.isEmpty { 144 | arguments.append("string\(strings.count)") 145 | strings.append(String(string)) 146 | } 147 | return (arguments, strings) 148 | default: 149 | return nil 150 | } 151 | } 152 | 153 | static func expand(source: String) -> String? { 154 | var source = stripComments(source).trimmingCharacters(in: .whitespacesAndNewlines) 155 | 156 | let macro = "#log" 157 | 158 | guard source.hasPrefix(macro) else { 159 | return nil 160 | } 161 | 162 | source = String(source[source.index(source.startIndex, offsetBy: macro.count)...]) 163 | source = source.trimmingCharacters(in: .init("()".unicodeScalars)) 164 | 165 | guard let firstComma = source.firstIndex(of: ",") else { 166 | return nil 167 | } 168 | let logger = source[...size * \(arguments.count) 201 | let bodySize = \(arguments.indices.map { 202 | "argument\($0)Size" 203 | }.joined(separator: " + ")) 204 | let totalSize = headerSize + bodySize 205 | guard let buffer = \(logger).__prepare(size: totalSize) else { 206 | return 207 | } 208 | 209 | buffer.storeBytes(of: argumentsCount, as: Swift.type(of: argumentsCount)) 210 | 211 | \(arguments.indices.map { 212 | "buffer.storeBytes(of: Swift.type(of: argument\($0)).__log_type, toByteOffset: MemoryLayout.size(ofValue: argumentsCount) + \($0), as: UInt8.self)" 213 | }.joined(separator: "\n\t\t")) 214 | 215 | #if DEBUG 216 | var offset = buffer.startIndex + headerSize 217 | #else 218 | var offset = headerSize 219 | #endif 220 | 221 | \(arguments.indices.map { 222 | """ 223 | #if DEBUG 224 | Swift.type(of: argument\($0)).__log(into: UnsafeMutableRawBufferPointer(rebasing: buffer[offset.. 68 | An example 69 | Assume we have a 0x2000 byte file that is 0x1800 full of log messages (for example, if 0x1800 bytes were filled and a 0x1000 byte message came in). The first 0x1800 bytes are log messages. The space between 0x1800 and 0x1ffd is unused (garbage). At the very end we encode (0x2000 - 0x1800 - 1) in ULEB-128, as [0x0f, 0xff]. 70 | 71 | 72 | ``` 73 | 0x0000: [log messages] 74 | 0x0010: [log messages] 75 | ......: ............... 76 | 0x0ff0: [log messages] 77 | -Log messages end here- 78 | 0x1000: [unused space] 79 | 0x1010: [unused space] 80 | ......: ............... 81 | --Trailer starts here-- 82 | 0x1ffe: 0x0f 83 | 0x1fff: 0xff 84 | ``` 85 | 86 | 87 | ### Log Message format 88 | Log messages have a fixed format and are stored back-to-back with no padding. The general format of an in individual message (also stored with no padding) is: 89 | 90 | * `UInt8` header 91 | * `UInt32` payload size 92 | * `UInt64` timestamp 93 | * `UInt16` logger ID 94 | * `UInt8` log component count 95 | * `[UInt8]` component type string 96 | * `[[UInt8]]` log component data 97 | * `UInt32` message size 98 | 99 | #### Header 100 | The header is a number between 0 and 5 inclusive, and indicates how complete the message is. If the previous message is complete, then the next message will be guaranteed to have a valid header that can be read (i.e. the header for the next message is written before the last message is marked as complete). The meaning for these values are as follows: 101 | 102 | * 0: The message is in unused state. (All bytes past the header are untouched.) 103 | * 1: The message's payload size has started being written. (The bytes for the payload size may be trashed, but bytes past that are untouched.) 104 | * 2: The message's payload size is committed and the payload has started being written. (The payload size is valid, and the payload afterwards may be trashed.) 105 | * 3: The message's message size has started being written. (The payload is finished. The bytes for the message size may be trashed.) 106 | * 4: The message's message size has been written to completion. (The next message's header may be trashed.) 107 | * 5: The message is complete. (The header for the next message is valid to parse.) 108 | 109 | #### Payload size 110 | The payload size includes the size to encode the timestamp, logger ID, log component count, component type string, and log component data. 111 | 112 | #### Component type string 113 | The component type string has a length specified by the log component count. It specifies the type of each log component with a single character each, as follows: 114 | 115 | | Type | Character | 116 | |-----------------|-----------| 117 | | `Bool` | b | 118 | | `Int8` | 1 | 119 | | `Int16` | 2 | 120 | | `Int32` | 4 | 121 | | `Int64` | 8 | 122 | | `Int` | i | 123 | | `UInt8` | ! | 124 | | `UInt16` | @ | 125 | | `UInt32` | $ | 126 | | `UInt64` | * | 127 | | `UInt` | I | 128 | | `Float` | f | 129 | | `Double` | F | 130 | | `String` | s | 131 | | `StaticString` | S | 132 | 133 | #### Log component data 134 | The log component data includes the data for each log component, joined together with no padding. 135 | 136 | | Type | Encoding | 137 | |-----------------|-----------------------------------| 138 | | `Bool` | 1-byte data | 139 | | `Int8` | 1-byte data | 140 | | `Int16` | 2-byte data | 141 | | `Int32` | 4-byte data | 142 | | `Int64` | 8-byte data | 143 | | `Int` | Pointer-sized data | 144 | | `UInt8` | 1-byte data | 145 | | `UInt16` | 2-byte data | 146 | | `UInt32` | 4-byte data | 147 | | `UInt64` | 8-byte data | 148 | | `UInt` | Pointer-sized data | 149 | | `Float` | 4-byte data | 150 | | `Double` | 8-byte data | 151 | | `String` | Pointer-sized count + count bytes | 152 | | `StaticString` | Pointer-sized data | 153 | 154 | #### Message size 155 | The message size is the payload size, plus the size of the header, the size of the payload count, and the size of the message size (itself). In other words, it's the size of the whole message including itself. 156 | 157 | ### String interpolation macro 158 | 159 | The `#log` macro splits apart the string interpolation provided as the second argument into "components". Each literal part of the string is encoded as a `StaticString`. The interpolations are encoded as individual components. Thus, a string like `"Invocation: \(String(cString: getprogname())) [\(CommandLine.argc) arguments]"` is split into the following: 160 | 161 | ```swift 162 | let component1: StaticString = "Invocation: " 163 | let component2: String = String(cString: getprogname()) 164 | let component3: StaticString = " [" 165 | let component4: CInt = CommandLine.argc 166 | let component5: Staticstring = " arguments]" 167 | ``` 168 | 169 | The macro itself is vaguely structured like follows: 170 | 171 | ```swift 172 | let string1...N = /* Literal parts of the log message*/ 173 | 174 | if logger.enabled { 175 | let component1...N = /* Each component of the log message */ 176 | let totalSize = /* Sum up the sizes of each component */ 177 | let buffer = logger.prepare(totalSize) 178 | component1...N.log(into: buffer) 179 | logger.complete() 180 | } 181 | ``` 182 | 183 | This design means that all the writes and sizing happens inlined at the call site, so maximum optimizations can take place. In practice, each component is written directly to the log buffer, without any extra copies. This includes strings if they are already laid out as UTF-8 internally. 184 | 185 | ## Status 186 | 187 | **Chronicle is not yet ready for general-purpose use**. In fact it may never be ready for that. It was designed as a test, but also to support [Ensemble](https://github.com/saagarjha/Ensemble). It has many serious limitations: 188 | 189 | * It may evolve or break without warning. 190 | * Optimizations are done with underscored compiler attributes I barely understand. 191 | * Macros are parsed without [swift-syntax](https://github.com/apple/swift-syntax), using string replacement. 192 | 193 | Seriously, do not use it. It's there for you to think about what you might from your own logging framework. 194 | --------------------------------------------------------------------------------