Skip to content

Commit ba329da

Browse files
authored
move labels equality check out of critical section in summary/histogram (#61)
* move labels equality check out of critical section in summary/histogram * preconditions, addressing review comments
1 parent 196573e commit ba329da

File tree

4 files changed

+168
-54
lines changed

4 files changed

+168
-54
lines changed

Sources/Prometheus/MetricTypes/Histogram.swift

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import Dispatch
66
/// See https://prometheus.io/docs/concepts/metric_types/#Histogram
77
public struct Buckets: ExpressibleByArrayLiteral {
88
public typealias ArrayLiteralElement = Double
9-
9+
1010
public init(arrayLiteral elements: Double...) {
1111
self.init(elements)
1212
}
13-
14-
fileprivate init (_ r: [Double]) {
13+
14+
fileprivate init(_ r: [Double]) {
1515
if r.isEmpty {
1616
self = Buckets.defaultBuckets
1717
return
@@ -24,13 +24,13 @@ public struct Buckets: ExpressibleByArrayLiteral {
2424
assert(Array(Set(r)).sorted(by: <) == r.sorted(by: <), "Buckets contain duplicate values.")
2525
self.buckets = r
2626
}
27-
27+
2828
/// The upper bounds
2929
public let buckets: [Double]
30-
30+
3131
/// Default buckets used by Histograms
3232
public static let defaultBuckets: Buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
33-
33+
3434
/// Create linear buckets used by Histograms
3535
///
3636
/// - Parameters:
@@ -42,7 +42,7 @@ public struct Buckets: ExpressibleByArrayLiteral {
4242
let arr = (0..<count).map { Double(start) + Double($0) * Double(width) }
4343
return Buckets(arr)
4444
}
45-
45+
4646
/// Create exponential buckets used by Histograms
4747
///
4848
/// - Parameters:
@@ -88,28 +88,28 @@ public class PromHistogram<NumType: DoubleRepresentable, Labels: HistogramLabels
8888
public let name: String
8989
/// Help text of this Histogram, optional
9090
public let help: String?
91-
91+
9292
/// Type of the metric, used for formatting
9393
public let _type: PromMetricType = .histogram
94-
94+
9595
/// Bucketed values for this Histogram
9696
private var buckets: [PromCounter<NumType, EmptyLabels>] = []
97-
97+
9898
/// Buckets used by this Histogram
9999
internal let upperBounds: [Double]
100-
100+
101101
/// Labels for this Histogram
102102
internal let labels: Labels
103-
103+
104104
/// Sub Histograms for this Histogram
105105
fileprivate var subHistograms: [Labels: PromHistogram<NumType, Labels>] = [:]
106-
106+
107107
/// Total value of the Histogram
108108
private let sum: PromCounter<NumType, EmptyLabels>
109-
109+
110110
/// Lock used for thread safety
111111
private let lock: Lock
112-
112+
113113
/// Creates a new Histogram
114114
///
115115
/// - Parameters:
@@ -125,18 +125,18 @@ public class PromHistogram<NumType: DoubleRepresentable, Labels: HistogramLabels
125125
self.prometheus = p
126126

127127
self.sum = .init("\(self.name)_sum", nil, 0, p)
128-
128+
129129
self.labels = labels
130-
130+
131131
self.upperBounds = buckets.buckets
132-
132+
133133
self.lock = Lock()
134-
134+
135135
buckets.buckets.forEach { _ in
136136
self.buckets.append(.init("\(name)_bucket", nil, 0, p))
137137
}
138138
}
139-
139+
140140
/// Gets the metric string for this Histogram
141141
///
142142
/// - Returns:
@@ -147,6 +147,8 @@ public class PromHistogram<NumType: DoubleRepresentable, Labels: HistogramLabels
147147
}
148148

149149
var output = [String]()
150+
// HELP/TYPE + (histogram + subHistograms) * (buckets + sum + count)
151+
output.reserveCapacity(2 + (subHistograms.count + 1) * (buckets.count + 2))
150152

151153
if let help = self.help {
152154
output.append("# HELP \(self.name) \(help)")
@@ -200,11 +202,9 @@ public class PromHistogram<NumType: DoubleRepresentable, Labels: HistogramLabels
200202
/// - value: Value to observe
201203
/// - labels: Labels to attach to the observed value
202204
public func observe(_ value: NumType, _ labels: Labels? = nil) {
203-
if let labels = labels, type(of: labels) != type(of: EmptySummaryLabels()) {
204-
let his: PromHistogram<NumType, Labels> = self.lock.withLock {
205-
self.getOrCreateHistogram(with: labels)
206-
}
207-
his.observe(value)
205+
if let labels = labels, type(of: labels) != type(of: EmptyHistogramLabels()) {
206+
self.getOrCreateHistogram(with: labels)
207+
.observe(value)
208208
}
209209
self.sum.inc(value)
210210

@@ -215,7 +215,7 @@ public class PromHistogram<NumType: DoubleRepresentable, Labels: HistogramLabels
215215
}
216216
}
217217
}
218-
218+
219219
/// Time the duration of a closure and observe the resulting time in seconds.
220220
///
221221
/// - parameters:
@@ -233,14 +233,39 @@ public class PromHistogram<NumType: DoubleRepresentable, Labels: HistogramLabels
233233

234234
/// Helper for histograms & labels
235235
fileprivate func getOrCreateHistogram(with labels: Labels) -> PromHistogram<NumType, Labels> {
236-
let histogram = self.subHistograms[labels]
237-
if let histogram = histogram, histogram.name == self.name, histogram.help == self.help {
236+
let subHistograms = lock.withLock { self.subHistograms }
237+
if let histogram = subHistograms[labels] {
238+
precondition(histogram.name == self.name,
239+
"""
240+
Somehow got 2 subHistograms with the same data type / labels
241+
but different names: expected \(self.name), got \(histogram.name)
242+
""")
243+
precondition(histogram.help == self.help,
244+
"""
245+
Somehow got 2 subHistograms with the same data type / labels
246+
but different help messages: expected \(self.help ?? "nil"), got \(histogram.help ?? "nil")
247+
""")
238248
return histogram
239249
} else {
240-
guard let prometheus = prometheus else { fatalError("Lingering Histogram") }
241-
let newHistogram = PromHistogram(self.name, self.help, labels, Buckets(self.upperBounds), prometheus)
242-
self.subHistograms[labels] = newHistogram
243-
return newHistogram
250+
return lock.withLock {
251+
if let histogram = subHistograms[labels] {
252+
precondition(histogram.name == self.name,
253+
"""
254+
Somehow got 2 subHistograms with the same data type / labels
255+
but different names: expected \(self.name), got \(histogram.name)
256+
""")
257+
precondition(histogram.help == self.help,
258+
"""
259+
Somehow got 2 subHistograms with the same data type / labels
260+
but different help messages: expected \(self.help ?? "nil"), got \(histogram.help ?? "nil")
261+
""")
262+
return histogram
263+
}
264+
guard let prometheus = prometheus else { fatalError("Lingering Histogram") }
265+
let newHistogram = PromHistogram(self.name, self.help, labels, Buckets(self.upperBounds), prometheus)
266+
self.subHistograms[labels] = newHistogram
267+
return newHistogram
268+
}
244269
}
245270
}
246271
}

Sources/Prometheus/MetricTypes/Summary.swift

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class PromSummary<NumType: DoubleRepresentable, Labels: SummaryLabels>: P
5353
internal let quantiles: [Double]
5454

5555
/// Sub Summaries for this Summary
56-
fileprivate var subSummaries: [PromSummary<NumType, Labels>] = []
56+
fileprivate var subSummaries: [Labels: PromSummary<NumType, Labels>] = [:]
5757

5858
/// Lock used for thread safety
5959
private let lock: Lock
@@ -100,6 +100,8 @@ public class PromSummary<NumType: DoubleRepresentable, Labels: SummaryLabels>: P
100100
}
101101

102102
var output = [String]()
103+
// HELP/TYPE + (summary + subSummaries) * (quantiles + sum + count)
104+
output.reserveCapacity(2 + (subSummaries.count + 1) * (quantiles.count + 2))
103105

104106
if let help = self.help {
105107
output.append("# HELP \(self.name) \(help)")
@@ -117,7 +119,7 @@ public class PromSummary<NumType: DoubleRepresentable, Labels: SummaryLabels>: P
117119
output.append("\(self.name)_count\(labelsString) \(self.count.get())")
118120
output.append("\(self.name)_sum\(labelsString) \(format(self.sum.get().doubleValue))")
119121

120-
subSummaries.forEach { subSum in
122+
subSummaries.values.forEach { subSum in
121123
var subSumLabels = subSum.labels
122124
let subSumValues = lock.withLock { subSum.values }
123125
calculateQuantiles(quantiles: self.quantiles, values: subSumValues.map { $0.doubleValue }).sorted { $0.key < $1.key }.forEach { (arg) in
@@ -162,13 +164,13 @@ public class PromSummary<NumType: DoubleRepresentable, Labels: SummaryLabels>: P
162164
/// - value: Value to observe
163165
/// - labels: Labels to attach to the observed value
164166
public func observe(_ value: NumType, _ labels: Labels? = nil) {
167+
if let labels = labels, type(of: labels) != type(of: EmptySummaryLabels()) {
168+
let sum = self.getOrCreateSummary(withLabels: labels)
169+
sum.observe(value)
170+
}
171+
self.count.inc(1)
172+
self.sum.inc(value)
165173
self.lock.withLock {
166-
if let labels = labels, type(of: labels) != type(of: EmptySummaryLabels()) {
167-
guard let sum = self.prometheus?.getOrCreateSummary(withLabels: labels, forSummary: self) else { fatalError("Lingering Summary") }
168-
sum.observe(value)
169-
}
170-
self.count.inc(1)
171-
self.sum.inc(value)
172174
if self.values.count == self.capacity {
173175
_ = self.values.popFirst()
174176
}
@@ -190,22 +192,42 @@ public class PromSummary<NumType: DoubleRepresentable, Labels: SummaryLabels>: P
190192
}
191193
return try body()
192194
}
193-
}
194-
195-
extension PrometheusClient {
196-
/// Helper for summaries & labels
197-
fileprivate func getOrCreateSummary<T: Numeric, U: SummaryLabels>(withLabels labels: U, forSummary summary: PromSummary<T, U>) -> PromSummary<T, U> {
198-
let summaries = summary.subSummaries.filter { (metric) -> Bool in
199-
guard metric.name == summary.name, metric.help == summary.help, metric.labels == labels else { return false }
200-
return true
201-
}
202-
if summaries.count > 2 { fatalError("Somehow got 2 summaries with the same data type") }
203-
if let summary = summaries.first {
195+
fileprivate func getOrCreateSummary(withLabels labels: Labels) -> PromSummary<NumType, Labels> {
196+
let subSummaries = self.lock.withLock { self.subSummaries }
197+
if let summary = subSummaries[labels] {
198+
precondition(summary.name == self.name,
199+
"""
200+
Somehow got 2 subSummaries with the same data type / labels
201+
but different names: expected \(self.name), got \(summary.name)
202+
""")
203+
precondition(summary.help == self.help,
204+
"""
205+
Somehow got 2 subSummaries with the same data type / labels
206+
but different help messages: expected \(self.help ?? "nil"), got \(summary.help ?? "nil")
207+
""")
204208
return summary
205209
} else {
206-
let newSummary = PromSummary<T, U>(summary.name, summary.help, labels, summary.capacity, summary.quantiles, self)
207-
summary.subSummaries.append(newSummary)
208-
return newSummary
210+
return lock.withLock {
211+
if let summary = self.subSummaries[labels] {
212+
precondition(summary.name == self.name,
213+
"""
214+
Somehow got 2 subSummaries with the same data type / labels
215+
but different names: expected \(self.name), got \(summary.name)
216+
""")
217+
precondition(summary.help == self.help,
218+
"""
219+
Somehow got 2 subSummaries with the same data type / labels
220+
but different help messages: expected \(self.help ?? "nil"), got \(summary.help ?? "nil")
221+
""")
222+
return summary
223+
}
224+
guard let prometheus = prometheus else {
225+
fatalError("Lingering Summary")
226+
}
227+
let newSummary = PromSummary(self.name, self.help, labels, self.capacity, self.quantiles, prometheus)
228+
self.subSummaries[labels] = newSummary
229+
return newSummary
230+
}
209231
}
210232
}
211233
}

Tests/SwiftPrometheusTests/HistogramTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,40 @@ final class HistogramTests: XCTestCase {
3333
self.prom = nil
3434
try! self.group.syncShutdownGracefully()
3535
}
36+
37+
func testConcurrent() throws {
38+
let prom = PrometheusClient()
39+
let histogram = prom.createHistogram(forType: Double.self, named: "my_histogram",
40+
helpText: "Histogram for testing",
41+
buckets: Buckets.exponential(start: 1, factor: 2, count: 63),
42+
labels: DimensionHistogramLabels.self)
43+
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 8)
44+
let semaphore = DispatchSemaphore(value: 2)
45+
_ = elg.next().submit {
46+
for _ in 1...1_000 {
47+
let labels = DimensionHistogramLabels([("myValue", "1")])
48+
let labels2 = DimensionHistogramLabels([("myValue", "2")])
49+
50+
histogram.observe(1.0, labels)
51+
histogram.observe(1.0, labels2)
52+
}
53+
semaphore.signal()
54+
}
55+
_ = elg.next().submit {
56+
for _ in 1...1_000 {
57+
let labels = DimensionHistogramLabels([("myValue", "1")])
58+
let labels2 = DimensionHistogramLabels([("myValue", "2")])
59+
60+
histogram.observe(1.0, labels2)
61+
histogram.observe(1.0, labels)
62+
}
63+
semaphore.signal()
64+
}
65+
semaphore.wait()
66+
try elg.syncShutdownGracefully()
67+
XCTAssertTrue(histogram.collect().contains("my_histogram_count 4000.0"))
68+
XCTAssertTrue(histogram.collect().contains("my_histogram_sum 4000.0"))
69+
}
3670

3771
func testHistogramSwiftMetrics() {
3872
let recorder = Recorder(label: "my_histogram")

Tests/SwiftPrometheusTests/SummaryTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,39 @@ final class SummaryTests: XCTestCase {
7070
my_summary_sum{myValue="labels"} 123.0\n
7171
""")
7272
}
73+
74+
func testConcurrent() throws {
75+
let prom = PrometheusClient()
76+
let summary = prom.createSummary(forType: Double.self, named: "my_summary",
77+
helpText: "Summary for testing",
78+
labels: DimensionSummaryLabels.self)
79+
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 8)
80+
let semaphore = DispatchSemaphore(value: 2)
81+
_ = elg.next().submit {
82+
for _ in 1...1_000 {
83+
let labels = DimensionSummaryLabels([("myValue", "1")])
84+
let labels2 = DimensionSummaryLabels([("myValue", "2")])
85+
86+
summary.observe(1.0, labels)
87+
summary.observe(1.0, labels2)
88+
}
89+
semaphore.signal()
90+
}
91+
_ = elg.next().submit {
92+
for _ in 1...1_000 {
93+
let labels = DimensionSummaryLabels([("myValue", "1")])
94+
let labels2 = DimensionSummaryLabels([("myValue", "2")])
95+
96+
summary.observe(1.0, labels2)
97+
summary.observe(1.0, labels)
98+
}
99+
semaphore.signal()
100+
}
101+
semaphore.wait()
102+
try elg.syncShutdownGracefully()
103+
XCTAssertTrue(summary.collect().contains("my_summary_count 4000.0"))
104+
XCTAssertTrue(summary.collect().contains("my_summary_sum 4000.0"))
105+
}
73106

74107
func testSummaryWithPreferredDisplayUnit() {
75108
let summary = Timer(label: "my_summary", preferredDisplayUnit: .seconds)

0 commit comments

Comments
 (0)