Skip to content

Commit 4874679

Browse files
authored
Clean up locks in record events readable spans (#799)
* Cleaned up locks * Added performance tests
1 parent dafdf11 commit 4874679

File tree

2 files changed

+176
-83
lines changed

2 files changed

+176
-83
lines changed

Sources/OpenTelemetrySdk/Trace/RecordEventsReadableSpan.swift

Lines changed: 62 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,49 @@ import OpenTelemetryApi
88

99
/// Implementation for the Span class that records trace events.
1010
public class RecordEventsReadableSpan: ReadableSpan {
11-
public var isRecording = true
11+
private let lock: ReadWriteLock = .init()
12+
13+
fileprivate var internalIsRecording = true
14+
public var isRecording: Bool {
15+
get { lock.withReaderLock { internalIsRecording } }
16+
set {
17+
lock.withWriterLock {
18+
if !internalEnd {
19+
internalIsRecording = newValue
20+
}
21+
}
22+
}
23+
}
1224

13-
fileprivate let internalStatusQueue = DispatchQueue(label: "org.opentelemetry.RecordEventsReadableSpan.internalStatusQueue", attributes: .concurrent)
14-
/// The displayed name of the span.
1525
fileprivate var internalName: String
1626
public var name: String {
17-
get {
18-
var value = ""
19-
internalStatusQueue.sync {
20-
value = internalName
27+
get { lock.withReaderLock { internalName } }
28+
set {
29+
lock.withWriterLock {
30+
if !internalEnd {
31+
internalName = newValue
32+
}
2133
}
22-
return value
2334
}
35+
}
36+
37+
/// The status of the span.
38+
fileprivate var internalStatus: Status = .unset
39+
public var status: Status {
40+
get { lock.withReaderLock { internalStatus } }
2441
set {
25-
internalStatusQueue.sync(flags: .barrier) {
42+
lock.withWriterLock {
2643
if !internalEnd {
27-
internalName = newValue
44+
internalStatus = newValue
2845
}
2946
}
3047
}
3148
}
3249

50+
/// True if the span is ended.
51+
fileprivate var internalEnd = false
52+
public var hasEnded: Bool { lock.withReaderLock { internalEnd } }
53+
3354
// The config used when constructing this Span.
3455
public private(set) var spanLimits: SpanLimits
3556
/// Contains the identifiers associated with this Span.
@@ -70,44 +91,13 @@ public class RecordEventsReadableSpan: ReadableSpan {
7091
/// Number of events recorded.
7192
public private(set) var totalRecordedEvents = 0
7293

73-
/// The status of the span.
74-
fileprivate var internalStatus: Status = .unset
75-
public var status: Status {
76-
get {
77-
var value: Status = .unset
78-
internalStatusQueue.sync {
79-
value = internalStatus
80-
}
81-
return value
82-
}
83-
set {
84-
internalStatusQueue.sync(flags: .barrier) {
85-
if !internalEnd {
86-
internalStatus = newValue
87-
}
88-
}
89-
}
90-
}
91-
9294
/// Returns the latency of the Span in seconds. If still active then returns now() - start time.
9395
public var latency: TimeInterval {
9496
return endTime?.timeIntervalSince(startTime) ?? clock.now.timeIntervalSince(startTime)
9597
}
9698

9799
/// The end time of the span.
98100
public private(set) var endTime: Date?
99-
/// True if the span is ended.
100-
fileprivate var internalEnd = false
101-
public var hasEnded: Bool {
102-
var value = false
103-
internalStatusQueue.sync {
104-
value = internalEnd
105-
}
106-
return value
107-
}
108-
109-
private let eventsSyncLock = Lock()
110-
private let attributesSyncLock = Lock()
111101

112102
private init(context: SpanContext,
113103
name: String,
@@ -193,43 +183,39 @@ public class RecordEventsReadableSpan: ReadableSpan {
193183
}
194184

195185
public func toSpanData() -> SpanData {
196-
attributesSyncLock.withLock {
197-
return SpanData(traceId: context.traceId,
198-
spanId: context.spanId,
199-
traceFlags: context.traceFlags,
200-
traceState: context.traceState,
201-
parentSpanId: parentContext?.spanId,
202-
resource: resource,
203-
instrumentationScope: instrumentationScopeInfo,
204-
name: name,
205-
kind: kind,
206-
startTime: startTime,
207-
attributes: attributes.attributes,
208-
events: adaptEvents(),
209-
links: adaptLinks(),
210-
status: status,
211-
endTime: endTime ?? clock.now,
212-
hasRemoteParent: hasRemoteParent,
213-
hasEnded: hasEnded,
214-
totalRecordedEvents: getTotalRecordedEvents(),
215-
totalRecordedLinks: totalRecordedLinks,
216-
totalAttributeCount: totalAttributeCount)
186+
lock.withReaderLock {
187+
SpanData(traceId: context.traceId,
188+
spanId: context.spanId,
189+
traceFlags: context.traceFlags,
190+
traceState: context.traceState,
191+
parentSpanId: parentContext?.spanId,
192+
resource: resource,
193+
instrumentationScope: instrumentationScopeInfo,
194+
name: internalName,
195+
kind: kind,
196+
startTime: startTime,
197+
attributes: attributes.attributes,
198+
events: lockedAdaptEvents(),
199+
links: lockedAdaptLinks(),
200+
status: internalStatus,
201+
endTime: endTime ?? clock.now,
202+
hasRemoteParent: hasRemoteParent,
203+
hasEnded: internalEnd,
204+
totalRecordedEvents: totalRecordedEvents,
205+
totalRecordedLinks: totalRecordedLinks,
206+
totalAttributeCount: totalAttributeCount)
217207
}
218208
}
219209

220-
private func adaptEvents() -> [SpanData.Event] {
221-
var sourceEvents = [SpanData.Event]()
222-
eventsSyncLock.withLockVoid {
223-
sourceEvents = events.array
224-
}
210+
private func lockedAdaptEvents() -> [SpanData.Event] {
225211
var result = [SpanData.Event]()
226-
sourceEvents.forEach {
212+
events.array.forEach {
227213
result.append(SpanData.Event(name: $0.name, timestamp: $0.timestamp, attributes: $0.attributes))
228214
}
229215
return result
230216
}
231217

232-
private func adaptLinks() -> [SpanData.Link] {
218+
private func lockedAdaptLinks() -> [SpanData.Link] {
233219
var result = [SpanData.Link]()
234220
let linksRef = links
235221
linksRef.forEach {
@@ -239,8 +225,8 @@ public class RecordEventsReadableSpan: ReadableSpan {
239225
}
240226

241227
public func setAttribute(key: String, value: AttributeValue?) {
242-
attributesSyncLock.withLockVoid {
243-
if !isRecording {
228+
lock.withWriterLock {
229+
if !internalIsRecording {
244230
return
245231
}
246232

@@ -285,8 +271,8 @@ public class RecordEventsReadableSpan: ReadableSpan {
285271
}
286272

287273
private func addEvent(event: SpanData.Event) {
288-
eventsSyncLock.withLockVoid {
289-
if !isRecording {
274+
lock.withWriterLock {
275+
if !internalIsRecording {
290276
return
291277
}
292278
events.append(event)
@@ -299,24 +285,19 @@ public class RecordEventsReadableSpan: ReadableSpan {
299285
}
300286

301287
public func end(time: Date) {
302-
let alreadyEnded = internalStatusQueue.sync(flags: .barrier) { () -> Bool in
288+
let alreadyEnded = lock.withWriterLock {
303289
if internalEnd {
304290
return true
305291
}
306292

307293
internalEnd = true
294+
internalIsRecording = false
308295
return false
309296
}
310-
311297
if alreadyEnded {
312298
return
313299
}
314300

315-
eventsSyncLock.withLockVoid {
316-
attributesSyncLock.withLockVoid {
317-
isRecording = false
318-
}
319-
}
320301
endTime = time
321302
OpenTelemetry.instance.contextProvider.removeContextForSpan(self)
322303
spanProcessor.onEnd(span: self)
@@ -327,9 +308,7 @@ public class RecordEventsReadableSpan: ReadableSpan {
327308
}
328309

329310
func getTotalRecordedEvents() -> Int {
330-
eventsSyncLock.withLock {
331-
totalRecordedEvents
332-
}
311+
lock.withReaderLock { totalRecordedEvents }
333312
}
334313

335314
/// For testing purposes
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//
2+
// Copyright The OpenTelemetry Authors
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
7+
8+
import XCTest
9+
@testable import OpenTelemetryApi
10+
@testable import OpenTelemetrySdk
11+
12+
final class RecordEventsReadableSpanPerformanceTests: XCTestCase {
13+
var tracerSdkFactory = TracerProviderSdk()
14+
var tracerSdk: Tracer!
15+
16+
let iterations = 10_000
17+
18+
override func setUp() {
19+
super.setUp()
20+
tracerSdk = tracerSdkFactory.get(instrumentationName: "SpanBuilderSdkTest")
21+
}
22+
23+
private func createTestSpan() -> RecordEventsReadableSpan {
24+
tracerSdk.spanBuilder(spanName: name).startSpan() as! RecordEventsReadableSpan
25+
}
26+
27+
func testAddEventPerformance() {
28+
let span = createTestSpan()
29+
30+
measure(metrics: [XCTClockMetric()]) {
31+
for _ in 0 ..< iterations {
32+
span.addEvent(name: UUID().uuidString)
33+
}
34+
}
35+
}
36+
37+
func testSetAttributePerformance() {
38+
let span = createTestSpan()
39+
40+
measure(metrics: [XCTClockMetric()]) {
41+
for i in 0 ..< iterations {
42+
span.setAttribute(key: "key\(i)", value: .string("value"))
43+
}
44+
}
45+
}
46+
47+
func testSetStatusPerformance() {
48+
let span = createTestSpan()
49+
50+
measure(metrics: [XCTClockMetric()]) {
51+
for _ in 0 ..< iterations {
52+
span.status = Int.random(in: 0 ... 10) % 2 == 0 ? .ok : .unset
53+
}
54+
}
55+
}
56+
57+
func testAllOperationsTogetherPerformance() {
58+
let span = createTestSpan()
59+
60+
measure(metrics: [XCTClockMetric()]) {
61+
for i in 0 ..< iterations {
62+
span.setAttribute(key: "key\(i)", value: .string("value"))
63+
span.addEvent(name: UUID().uuidString)
64+
span.status = Int.random(in: 0 ... 10) % 2 == 0 ? .ok : .unset
65+
_ = span.toSpanData()
66+
}
67+
}
68+
}
69+
70+
func testAddEventPerformance_concurrent() {
71+
let span = createTestSpan()
72+
73+
measure(metrics: [XCTClockMetric()]) {
74+
DispatchQueue.concurrentPerform(iterations: iterations) { _ in
75+
span.addEvent(name: UUID().uuidString)
76+
}
77+
}
78+
}
79+
80+
func testSetAttributePerformance_concurrent() {
81+
let span = createTestSpan()
82+
83+
measure(metrics: [XCTClockMetric()]) {
84+
DispatchQueue.concurrentPerform(iterations: iterations) { i in
85+
span.setAttribute(key: "key\(i)", value: .string("value"))
86+
}
87+
}
88+
}
89+
90+
func testSetStatusPerformance_concurrent() {
91+
let span = createTestSpan()
92+
93+
measure(metrics: [XCTClockMetric()]) {
94+
DispatchQueue.concurrentPerform(iterations: iterations) { _ in
95+
span.status = Int.random(in: 0 ... 10) % 2 == 0 ? .ok : .unset
96+
}
97+
}
98+
}
99+
100+
func testAllOperationsTogetherPerformance_concurrent() {
101+
let span = createTestSpan()
102+
103+
measure(metrics: [XCTClockMetric()]) {
104+
DispatchQueue.concurrentPerform(iterations: iterations) { i in
105+
span.setAttribute(key: "key\(i)", value: .string("value"))
106+
span.addEvent(name: UUID().uuidString)
107+
span.status = Int.random(in: 0 ... 10) % 2 == 0 ? .ok : .unset
108+
_ = span.toSpanData()
109+
}
110+
}
111+
}
112+
}
113+
114+
#endif

0 commit comments

Comments
 (0)