├── .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 |
--------------------------------------------------------------------------------