Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4ad8c92
SMT-0001 proposal added
kukushechkin Feb 16, 2026
47c668f
SMT-0001 proposal implementation
kukushechkin Feb 16, 2026
4b9cb32
access current task-local factory
kukushechkin Feb 18, 2026
363fd76
testing is a secondary use-case
kukushechkin Feb 18, 2026
2edabd4
SMT-0001 In Review
kukushechkin Mar 3, 2026
556c558
missing currentFactory
kukushechkin Mar 3, 2026
32bb07e
remove @discardableResult
kukushechkin Mar 3, 2026
3601837
make new API nonisolated(nonsending)
kukushechkin Mar 3, 2026
b68779d
update the .with() API to be less confusing
kukushechkin Mar 4, 2026
69311b7
update the .with() API to be less confusing — impl
kukushechkin Mar 4, 2026
de24600
typed throws in .withCurrent() API
kukushechkin Mar 4, 2026
4a795ae
implementation workaround for typed throws in .withCurrent() API
kukushechkin Mar 4, 2026
1d35b8e
reformat
kukushechkin Mar 4, 2026
2899fdf
workaround nonisolated(nonsending) <6.2
kukushechkin Mar 4, 2026
fb2d32b
metrics initialization benchmark
kukushechkin Mar 5, 2026
50f83f1
not all inits used tasklocal object
kukushechkin Mar 5, 2026
b3667ae
fixup! not all inits used tasklocal object
kukushechkin Mar 5, 2026
a8e8a27
fixup! metrics initialization benchmark
kukushechkin Mar 5, 2026
76a453d
fixup! metrics initialization benchmark
kukushechkin Mar 5, 2026
88bafb1
rename withCurrent to withMetricsFactory
kukushechkin Mar 5, 2026
5299276
fixup! rename withCurrent to withMetricsFactory
kukushechkin Mar 5, 2026
caefce3
move withMetricsFactory to the top level
kukushechkin Mar 5, 2026
b6be490
fixup! move withMetricsFactory to the top level
kukushechkin Mar 5, 2026
d13b513
fixup! move withMetricsFactory to the top level
kukushechkin Mar 5, 2026
bdb8f0c
fixup! metrics initialization benchmark
kukushechkin Mar 5, 2026
b860183
add word on correct metrics factory usage pattern to README
kukushechkin Mar 11, 2026
6d03872
encourage correct metrics usage pattern
kukushechkin Mar 11, 2026
afda72b
better explain why runtime metrics creation is bad
kukushechkin Mar 13, 2026
4b3200d
more scoped metrics factory examples
kukushechkin Mar 13, 2026
a4ac32b
better explain why runtime metrics creation is bad in README
kukushechkin Mar 13, 2026
8df7973
more scoped metrics factory examples in README
kukushechkin Mar 13, 2026
01ac345
Ready for Implementation
kukushechkin Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Benchmarks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
79 changes: 79 additions & 0 deletions Benchmarks/Benchmarks/MetricsBenchmarks/MetricsBenchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Metrics API open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift Metrics API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Metrics API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Benchmark
import CoreMetrics
import Metrics
import MetricsTestKit

public func makeBenchmark(
_ suffix: String = "",
_ body: @escaping (Benchmark) -> Void
) {
let iterations = 1_000_000
let metrics: [BenchmarkMetric] = [.instructions, .objectAllocCount]

Benchmark(
"metrics_benchmark_\(suffix)",
configuration: .init(
metrics: metrics,
maxIterations: iterations,
thresholds: [
.instructions: BenchmarkThresholds(
relative: [
.p90: 1.0 // we only record p90
]
),
.objectAllocCount: BenchmarkThresholds(
absolute: [
.p90: 0 // we only record p90
]
),
]
)
) { benchmark in
body(benchmark)
}
}

public let benchmarks: @Sendable () -> Void = {
let metricsFactory = TestMetrics()
MetricsSystem.bootstrap(metricsFactory)

makeBenchmark("task-local-init") { benchmark in
withMetricsFactory(changingFactory: metricsFactory) {
benchmark.startMeasurement()
let _ = Timer(label: "test-timer")
let _ = Counter(label: "test-counter")
let _ = Gauge(label: "test-gauge")
benchmark.stopMeasurement()
}
}
makeBenchmark("explicit-init") { benchmark in
benchmark.startMeasurement()
let _ = Timer(label: "test-timer", factory: metricsFactory)
let _ = Counter(label: "test-counter", factory: metricsFactory)
let _ = Gauge(label: "test-gauge", factory: metricsFactory)
benchmark.stopMeasurement()
}
makeBenchmark("explicit-init-with-current") { benchmark in
withMetricsFactory(changingFactory: metricsFactory) {
benchmark.startMeasurement()
let _ = Timer(label: "test-timer", factory: MetricsSystem.currentFactory)
let _ = Counter(label: "test-counter", factory: MetricsSystem.currentFactory)
let _ = Gauge(label: "test-gauge", factory: MetricsSystem.currentFactory)
benchmark.stopMeasurement()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"instructions" : 10855,
"objectAllocCount" : 3
}
33 changes: 33 additions & 0 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "MetricsBenchmarks",
platforms: [
.macOS(.v13)
],
products: [
.executable(name: "MetricsBenchmarks", targets: ["MetricsBenchmarks"])
],
dependencies: [
// swift-metrics
.package(name: "swift-metrics", path: "../"),
.package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.29.6"),
],
targets: [
.executableTarget(
name: "MetricsBenchmarks",
dependencies: [
.product(name: "Benchmark", package: "package-benchmark"),
.product(name: "CoreMetrics", package: "swift-metrics"),
.product(name: "MetricsTestKit", package: "swift-metrics"),
],
path: "Benchmarks/MetricsBenchmarks",
plugins: [
.plugin(name: "BenchmarkPlugin", package: "package-benchmark")
]
)
]
)
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,74 @@ let counter = Counter(label: "com.example.BestExampleApp.numberOfRequests")
counter.increment()
```

### Correct metrics usage pattern

Metrics objects should be created **once**, with pre-defined labels and dimensions known at initialization time, and
reused for the lifetime of the component. Creating new metric objects on every request or operation is an antipattern:

- It can lead to **unbounded memory allocation** if the labels or dimensions are unbounded (e.g. per-request IDs),
causing unbounded cardinality in the metrics backend.
- It is **slow** and can become a bottleneck for fast parallel execution, since metric creation typically involves
factory synchronization and backend registration.

```swift
// ❌ Creating metrics on demand — unbounded cardinality when dimensions vary per-request
func handleRequest(requestID: String) {
let counter = Counter(label: "requests", dimensions: [("request_id", requestID)])
counter.increment()
}

// ✅ Create metrics once during setup with fixed dimensions and reuse them
struct RequestHandler {
let requestCounter = Counter(label: "requests")

func handleRequest(requestID: String) {
requestCounter.increment()
}
}
```

When using a scoped factory override — such as `withMetricsFactory(_:)` for testing — the factory is only active for
the duration of the closure. Any metrics created outside that scope will not see the overridden factory and will fall
back to the global one. If no global factory has been bootstrapped, such metrics will fail to initialize, providing a
safeguard against creating metrics outside of the designated setup scope.

```swift
struct UserService {
let counter: Counter

init() {
// ✅ Created during init — picks up the task-local factory
self.counter = Counter(label: "users.created")
}

func createUser(name: String) async throws -> User {
// ❌ Created on demand — task-local factory is no longer in scope,
// falls back to global; fails if global is not bootstrapped
let onDemandCounter = Counter(label: "users.created.on_demand")
let user = User()
self.counter.increment()
return user
}
}

@Test
func testUserCreation() async throws {
let testMetrics = TestMetrics()

// The task-local factory is only active inside this block
let service = withMetricsFactory(testMetrics) {
UserService() // counter is created here — uses testMetrics
}

// service.createUser() runs outside the withMetricsFactory scope,
// so onDemandCounter inside it will NOT use testMetrics
_ = try await service.createUser(name: "Alice")

#expect(try testMetrics.expectCounter("users.created").values == [1])
}
```

### Selecting a metrics backend implementation (applications only)

Note: If you are building a library, you don't need to concern yourself with this section. It is the end users of your library (the applications) who will decide which metrics backend to use. Libraries should never change the metrics implementation as that is something owned by the application.
Expand Down
1 change: 1 addition & 0 deletions Sources/CoreMetrics/Docs.docc/Proposals/Proposals.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ If you have any questions, ask in an issue on GitHub.

## Topics

- <doc:SMT-0001-task-local-metrics-factory>
- <doc:SMT-NNNN>
Loading
Loading