Skip to content

Commit 3083a76

Browse files
committed
[implementation] back to always-task-local
1 parent b4c71be commit 3083a76

8 files changed

Lines changed: 233 additions & 418 deletions

File tree

Sources/Instrumentation/InstrumentationSystem.swift

Lines changed: 69 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import ServiceContextModule
2121
/// Set up the instrumentation using ``bootstrap(_:)``, and access the globally available instrument using ``instrument``.
2222
/// If you need to use more that one cross-cutting tool you can do so by using ``MultiplexInstrument``.
2323
///
24-
/// To enable scoped overrides (per-test, per-subsystem), use ``withInstrument(_:_:)``, which installs a
25-
/// dedicated task-local instrument as the bootstrap on first use and sets the active instrument on it.
24+
/// To override the active instrument for a scope — a test, a subsystem — without touching the process-wide
25+
/// bootstrap, use ``withInstrument(_:_:)``. It binds a task-local instrument that ``instrument`` and discovery
26+
/// resolve ahead of the bootstrapped one, for the duration of a closure.
2627
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal ServiceContext
2728
public enum InstrumentationSystem {
2829
/// Marked as @unchecked Sendable due to the synchronization being
@@ -52,64 +53,57 @@ public enum InstrumentationSystem {
5253
}
5354
}
5455

55-
/// Ensure a ``TaskLocalInstrument`` is installed so a scope can be entered, installing one lazily
56-
/// over the default ``NoOpInstrument`` when nothing has been bootstrapped.
57-
///
58-
/// Uses double-checked locking: the common case — a ``TaskLocalInstrument`` is already installed —
59-
/// is satisfied under a reader lock, matching the cost of reading ``instrument``. Only the one-time
60-
/// install (or the crash) takes the writer lock.
61-
///
62-
/// - NoOp bootstrap (nothing installed): install an empty `TaskLocalInstrument()` so the wrapper is
63-
/// reachable from discovery and propagation for the duration of any scope.
64-
/// - ``TaskLocalInstrument`` already installed: nothing to do.
65-
/// - Any other bootstrapped instrument: crash. The task-local instrument can only be installed when
66-
/// the system is otherwise un-bootstrapped. It cannot replace an instrument the application chose to
67-
/// bootstrap. `withInstrument` is an application-level facility and must not be called against a
68-
/// non-scoping bootstrap (typically from library code).
69-
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
70-
func installTaskLocalInstrumentIfNeeded() {
71-
// Fast path: already scoping-capable, no write needed.
72-
if self.lock.withReaderLock({ self._instrument is TaskLocalInstrument }) {
73-
return
74-
}
75-
// Slow path: install over NoOp, or crash. Re-check under the writer lock.
76-
self.lock.withWriterLock {
77-
if self._instrument is TaskLocalInstrument {
78-
return
79-
}
80-
guard self._instrument is NoOpInstrument else {
81-
fatalError(
82-
"""
83-
withInstrument(_:_:) requires either an un-bootstrapped InstrumentationSystem or one \
84-
already using task-local scoping, but a \(type(of: self._instrument)) was bootstrapped. \
85-
It cannot replace a plain bootstrapped instrument. withInstrument(_:_:) is an \
86-
application-level facility and must not be called from library code.
87-
"""
88-
)
89-
}
90-
self._instrument = TaskLocalInstrument()
91-
}
92-
}
93-
9456
var instrument: Instrument {
9557
self.lock.withReaderLock { self._instrument }
9658
}
9759

9860
func _findInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
9961
self.lock.withReaderLock {
100-
if let container = self._instrument as? _InstrumentContainer {
101-
return container.firstInstrument(where: predicate)
102-
} else if predicate(self._instrument) {
103-
return self._instrument
104-
} else {
105-
return nil
106-
}
62+
InstrumentationSystem.firstInstrument(in: self._instrument, where: predicate)
10763
}
10864
}
10965
}
11066

11167
private static let shared = Storage()
11268

69+
/// Task-local instrument override set by ``withInstrument(_:_:)``.
70+
///
71+
/// Resolved ahead of the bootstrapped instrument by ``instrument`` and ``_findInstrument(where:)`` for the
72+
/// duration of a scope. Internal storage — callers set it by calling ``withInstrument(_:_:)``.
73+
@TaskLocal
74+
@usableFromInline
75+
internal static var _taskLocalInstrument: (any Instrument)?
76+
77+
/// Runs `operation` with `instrument` bound to the task-local override. Backs ``withInstrument(_:_:)``.
78+
@usableFromInline
79+
static func withTaskLocalInstrument<Result>(
80+
_ instrument: any Instrument,
81+
operation: () throws -> Result
82+
) rethrows -> Result {
83+
try Self.$_taskLocalInstrument.withValue(instrument, operation: operation)
84+
}
85+
86+
#if compiler(>=6.2)
87+
/// Async variant of ``withTaskLocalInstrument(_:operation:)``.
88+
@usableFromInline
89+
nonisolated(nonsending) static func withTaskLocalInstrument<Result>(
90+
_ instrument: any Instrument,
91+
operation: nonisolated(nonsending) () async throws -> Result
92+
) async rethrows -> Result {
93+
try await Self.$_taskLocalInstrument.withValue(instrument, operation: operation)
94+
}
95+
#else
96+
/// Async variant of ``withTaskLocalInstrument(_:operation:)``.
97+
@usableFromInline
98+
static func withTaskLocalInstrument<Result>(
99+
_ instrument: any Instrument,
100+
isolation: isolated (any Actor)? = #isolation,
101+
operation: () async throws -> Result
102+
) async rethrows -> Result {
103+
try await Self.$_taskLocalInstrument.withValue(instrument, operation: operation)
104+
}
105+
#endif
106+
113107
/// Globally select the desired ``Instrument`` implementation.
114108
///
115109
/// - Parameter instrument: The ``Instrument`` you want to share globally within your system.
@@ -126,38 +120,42 @@ public enum InstrumentationSystem {
126120
self.shared.bootstrapInternal(instrument)
127121
}
128122

129-
/// Ensures the system can enter a task-local scope, installing a ``TaskLocalInstrument`` over the
130-
/// default ``NoOpInstrument`` if nothing has been bootstrapped. Backs ``withInstrument(_:_:)``.
131-
///
132-
/// Crashes if a plain (non-scoping) instrument was bootstrapped. See
133-
/// ``Storage/installTaskLocalInstrumentIfNeeded()`` for the full semantics.
134-
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
135-
internal static func installTaskLocalInstrumentIfNeeded() {
136-
self.shared.installTaskLocalInstrumentIfNeeded()
137-
}
138-
139-
/// Returns the currently bootstrapped ``Instrument``, or a ``NoOpInstrument`` if none was set.
123+
/// The currently active ``Instrument``.
140124
///
141-
/// When an override has been set via ``withInstrument(_:_:)``, it participates when the returned
142-
/// instrument's methods are invoked — `inject` / `extract` and discovery (``tracer``,
143-
/// ``_findInstrument(where:)``) resolve through it.
144-
///
145-
/// > Warning: Do not pass this value to ``withInstrument(_:_:)`` (directly or inside a
146-
/// > ``MultiplexInstrument``). It is task-local-backed, so re-installing it would resolve a scope that
147-
/// > contains itself; `withInstrument` rejects it with a crash.
125+
/// This is the instrument bound by the innermost enclosing ``withInstrument(_:_:)`` scope, if any,
126+
/// otherwise the one set with ``bootstrap(_:)`` — and a ``NoOpInstrument`` if neither was set.
148127
public static var instrument: Instrument {
149-
shared.instrument
128+
Self._taskLocalInstrument ?? self.shared.instrument
150129
}
151130
}
152131

153132
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) // for TaskLocal ServiceContext
154133
extension InstrumentationSystem {
155134
/// INTERNAL API: Do Not Use
156135
///
157-
/// Walks the bootstrapped instrument for the first member matching `predicate`. Recurses into
158-
/// `_InstrumentContainer` members, including ``MultiplexInstrument`` members and the task-local override
159-
/// installed by ``withInstrument(_:_:)``.
136+
/// Finds the first instrument matching `predicate` in the currently active instrument — the
137+
/// ``withInstrument(_:_:)`` override if one is in scope, otherwise the bootstrapped instrument. If the
138+
/// active instrument is a ``MultiplexInstrument``, its direct members are checked in order.
160139
public static func _findInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
161-
self.shared._findInstrument(where: predicate)
140+
if let scoped = Self._taskLocalInstrument {
141+
return Self.firstInstrument(in: scoped, where: predicate)
142+
}
143+
return self.shared._findInstrument(where: predicate)
144+
}
145+
146+
/// Returns the first match for `predicate`: `instrument` itself, or — when it is a ``MultiplexInstrument``
147+
/// — its first direct member satisfying `predicate`. This is not recursive: a ``MultiplexInstrument``
148+
/// nested inside another is tested as a whole, not descended into.
149+
fileprivate static func firstInstrument(
150+
in instrument: Instrument,
151+
where predicate: (Instrument) -> Bool
152+
) -> Instrument? {
153+
if let multiplex = instrument as? MultiplexInstrument {
154+
return multiplex.firstInstrument(where: predicate)
155+
} else if predicate(instrument) {
156+
return instrument
157+
} else {
158+
return nil
159+
}
162160
}
163161
}

Sources/Instrumentation/MultiplexInstrument.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift Distributed Tracing open source project
44
//
5-
// Copyright (c) 2020-2025 Apple Inc. and the Swift Distributed Tracing project authors
5+
// Copyright (c) 2020-2023 Apple Inc. and the Swift Distributed Tracing project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -17,9 +17,7 @@ import ServiceContextModule
1717
/// A pseudo instrument to use to instrument using multiple instruments across a
1818
/// common service context.
1919
public struct MultiplexInstrument {
20-
/// The members this multiplex fans out to. Readable within the module so the scoping guard in
21-
/// ``withInstrument(_:_:)`` can walk for a nested ``TaskLocalInstrument``.
22-
var instruments: [Instrument]
20+
private var instruments: [Instrument]
2321

2422
/// Create a multiplex instrument.
2523
///
@@ -31,9 +29,7 @@ public struct MultiplexInstrument {
3129
}
3230
}
3331

34-
extension MultiplexInstrument: _InstrumentContainer {
35-
/// Returns the first member that satisfies `predicate`. Conforming to `_InstrumentContainer` lets
36-
/// ``InstrumentationSystem`` discovery (and a scoped ``TaskLocalInstrument``) descend into the multiplex.
32+
extension MultiplexInstrument {
3733
func firstInstrument(where predicate: (Instrument) -> Bool) -> Instrument? {
3834
self.instruments.first(where: predicate)
3935
}

0 commit comments

Comments
 (0)