├── .gitignore ├── Package.swift ├── README.md ├── Sources ├── Chronicle │ ├── Buffer.swift │ ├── Chronicle.swift │ ├── Entry.swift │ ├── EntrySequence.swift │ ├── Epilog.swift │ ├── Loggable.swift │ ├── Logger.swift │ ├── Macro.swift │ └── StringCollector.swift └── Macros │ └── Macro.swift └── Tests └── ChronicleTests └── ChronicleTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .swiftpm/ 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chronicle 2 | 3 | Chronicle is an experiment built to answer a single question: what would an [os_log](https://developer.apple.com/documentation/os/logging) designed for third-party developers, by third-party developers, look like? It turns out, kind of like this: 4 | 5 | ```swift 6 | let chronicle = Chronicle(url: URL(fileURLWithPath: "log.chronicle"), bufferSize: 1 << 20) 7 | let logger = try chronicle.logger(name: "test") 8 | 9 | #log(logger, "Invocation: \(String(cString: getprogname())) [\(CommandLine.argc) arguments]") 10 | #log(logger, "It's been \(Date.now.timeIntervalSince1970) seconds since the epoch") 11 | ``` 12 | 13 | Chronicle is: 14 | 15 | * **Fast.** You can throw millions of messages a second at Chronicle and it won't break a sweat. With logging this cheap (just a few dozen nanoseconds per message!) there's no reason to turn it off. 16 | * **Persistent.** A memory-mapped ring buffer puts you in control with where you keep your logs, and provides durability for the harshest environments when you need your logs the most. 17 | * **Structured.** Embed any data you want (with a few exceptions) into your logs. Extract it out later and operate on it with the analysis tooling of your choice. Or don't: if you like strings, Chronicle is fine with that. 18 | 19 | Chronicle is *not*: 20 | 21 | * A general purpose logger. It won't also make network requests to your backend or track your user across multiple app sessions. 22 | * Available for other languages. It's Swift-only and will likely stay that way. 23 | * Complete or ready for use. See below. 24 | 25 | ## Why not os_log? 26 | os_log is part of Apple's high-performance unified logging solution for their platforms. Inside the company, it powers many analysis tools that engineers use to diagnose and investigate problems in their code. It has a number of neat features: it collects logs in a central location from all parts of the system, has relatively low overhead, is highly configurable on a level/subsystem/category basis, and has integration with software such as Console and Instruments. Apple would like you to use it, too, which is why they make the API available to third-party developers. Unfortunately, sometimes it's not particularly well suited to the job. 27 | 28 | ### It's "unified" 29 | App developers care first and foremost about *their* logs. A unified log, while helpful when trying to track down issues across components, is less useful to someone trying to diagnose issues in their own code. For logging, the shared log means that system services often spew "spam" into it: messages that are rarely useful, except to the immediate developers of a system API. While there are some ways to silence logs from chatty sources (at least, if the developers use an appropriate subsystem and category) even then the sheer *number* of sources is hard to grapple with. And as the OS gets larger, the number of clients continues to grow with it. 30 | 31 | ### It's system-managed 32 | While you have some control over your log persistence, by and large logs are saved for as long as the system would like to keep them. And getting them back is a pain: the [public APIs](https://developer.apple.com/documentation/oslog/oslogstore) (which are not what Apple uses!) are limited and broken. Asking users to take a sysdiagnose to get your logs is a pain and privacy-invading, since it contains data from other apps in it. 33 | 34 | This design also limits the implementation: any buffers in your process are sized by the system, and if you overrun them or log too quickly messages will be dropped. Messages that are persisted to disk or streamed necessarily incur the cost of XPC. Even the logging configuration is system-wide, and while significant work has been put into making accesses efficient (commpage, etc.) it's still overhead if you don't need it. 35 | 36 | ### It has a constrained API 37 | os_log is designed for what Apple needs it to do. This means that they run special compiler optimization passes on your code to try to optimize log buffers into the shape the API wants. It means that activity IDs are carried across processes but not between different computers. Information you stick in a log message is stuck there unless you use Apple's tooling to extract it out, and if you wanted to find the 99- and 90-percentile values and Apple only shows you a median, you're out of luck. If you want to backdeploy to Catalina and the system doesn't support what you want to do, there's not much you can do. Apple's official solution to wrapping os_log, of course, is "don't". 38 | 39 | ## Design 40 | Chronicle is designed a bit like os_log, but local to your process. By default it opens a file as a ring buffer and maps it in, so that it doesn't have to ever explicitly flush any data. This means it can persist through a crash without any special handlers being installed, and it doesn't hit the disk for each message (which would be both slow and terrible for your flash storage). The log format is carefully designed as a linear doubly-linked list so that it can be recovered even if interrupted part of the way through a write. 41 | 42 | Logs in Chronicle are typed and, like os_log, the in-memory format writes as little as possible as a performance optimization. For example, constant strings in your log message are not written out, but instead noted down as an offset into the originating binary. The types of data you can write to a log are likewise limited: simple integral and floating point types, booleans, and strings. This information can be extracted for later analysis, or formatted into a human-readable string log message. 43 | 44 | While Chronicle has typed logs, it does very little to dictate the "schema" of your log messages. In particular, there is no special support for things like logging subsystems, categories, activities, or event IDs. The only metadata that is written alongside your message is a high-resolution timestamp and the originating logger's ID. A logger in Chronicle likewise is just something that is named and can be disabled selectively. Any organization on top of this is up to you. It is supported and recommended to layer your own organization in-line by e.g. logging something like this: 45 | 46 | ```swift 47 | let category: StaticString = "ImageDecoder" 48 | #log(logger, "\(category): Started decode of \(image)") 49 | ``` 50 | 51 | Unlike os_log, Chronicle does not use special compiler optimization passes to reduce logging overhead. Instead, it uses the `#log` macro and careful inlining to collapse the code into direct writes to the logging buffer. Since it is a library that you ship with your app, it has no ABI concerns beyond the format of the log itself. 52 | 53 | ## Log format 54 | By default, Chronicle logs to a ".chronicle" bundle. Inside it are a directory and two files. The first file is metadata.json, which contains data needed to reconstruct the logs. In particular, it contains the log version, logger names, timing information, and some string table information. The string tables (currently, a wholesale dump of `__TEXT,__cstring` from images outside of the shared cache) themselves are stored in the directory called strings, with a filename that represents the load address for the section and the contents of the file being the strings data. 55 | 56 | ### Buffer format 57 | The actual logs themselves are stored in the other file (called buffer) and it has the following high-level format: 58 | 59 | ``` 60 | [array of log messages] 61 | [unused space] 62 | [trailer] 63 | ``` 64 | 65 | Log messages always start at the start of the file. If the buffer was never filled completely, then they will use as much as the buffer as needed. If the buffer has wrapped around, then messages will still begin from the very start of the file, but the topmost message will no longer be the first message chronologically. Instead, the messages from the time at which the log wrapped going forward can be read going forward in the file from the start, walking the chain of in-use messages (see below). Messages from earlier can be read going backwards in the file, traversing the other end of the linked list. The trailer at the very bottom of the file indicates an "offset distance" to the end of the last message, encoded in reversed ULEB-128 bytes (least significant byte at the very end of the buffer, second least significant second-to-last byte, etc.). This trailer is necessary because log messages may not fill the entire buffer. There will always be at least one trailer byte at the end of the file. The "offset distance" is the true distance from the end of the last message to the end of the buffer, but with one subtracted from it. This is because a full log buffer only extends to the second-to-last byte in the file, because of the necessity of at least one trailer byte. Note that the trailer encodes the number of bytes of unused space when small, but it diverges as the trailer gets larger to incorporate more bytes. 66 | 67 |
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 | -------------------------------------------------------------------------------- /Sources/Chronicle/Buffer.swift: -------------------------------------------------------------------------------- 1 | class Buffer { 2 | typealias Size = UInt32 3 | 4 | enum Progress: UInt8 { 5 | case unused 6 | case preparing 7 | case prepared 8 | case completing 9 | case completed 10 | case used 11 | } 12 | 13 | let buffer: UnsafeMutableRawBufferPointer 14 | var offset: Int 15 | var checkpoint: Int 16 | 17 | init(buffer: UnsafeMutableRawBufferPointer) { 18 | let minSize = MemoryLayout.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/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 | -------------------------------------------------------------------------------- /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.. 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.. 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/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/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/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/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/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.. 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 | --------------------------------------------------------------------------------