From 64072a4c0580d453ceda4bd7e566b25653c2092d Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Thu, 7 Nov 2024 16:48:50 +0100 Subject: [PATCH] perf: Proto encode telemetry data --- Package.resolved | 9 + Package.swift | 5 +- .../Apply/FlagApplierWithRetries.swift | 2 +- Sources/Confidence/Http/HttpClient.swift | 2 +- Sources/Confidence/Http/NetworkClient.swift | 28 +- .../Telemetry/TelemetryManager.swift | 40 ++- .../Telemetry/TelemetryPayload.pb.swift | 302 ++++++++++++++++++ .../Telemetry/TelemetryPayload.proto | 29 ++ .../Telemetry/TelemetryPayload.swift | 23 -- .../Helpers/HttpClientMock.swift | 2 +- 10 files changed, 379 insertions(+), 63 deletions(-) create mode 100644 Sources/Confidence/Telemetry/TelemetryPayload.pb.swift create mode 100644 Sources/Confidence/Telemetry/TelemetryPayload.proto delete mode 100644 Sources/Confidence/Telemetry/TelemetryPayload.swift diff --git a/Package.resolved b/Package.resolved index cbd9736a..5efaa616 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "SwiftProtobuf", + "repositoryURL": "https://github.com/apple/swift-protobuf.git", + "state": { + "branch": null, + "revision": "ebc7251dd5b37f627c93698e4374084d98409633", + "version": "1.28.2" + } + }, { "package": "OpenFeature", "repositoryURL": "git@github.com:open-feature/swift-sdk.git", diff --git a/Package.swift b/Package.swift index 6c98fbc8..2c01e886 100644 --- a/Package.swift +++ b/Package.swift @@ -19,11 +19,14 @@ let package = Package( ], dependencies: [ .package(url: "git@github.com:open-feature/swift-sdk.git", from: "0.1.0"), + .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.0.0"), ], targets: [ .target( name: "Confidence", - dependencies: [], + dependencies: [ + .product(name: "SwiftProtobuf", package: "swift-protobuf") + ], plugins: [] ), .target( diff --git a/Sources/Confidence/Apply/FlagApplierWithRetries.swift b/Sources/Confidence/Apply/FlagApplierWithRetries.swift index 5c5296c9..7459a0c0 100644 --- a/Sources/Confidence/Apply/FlagApplierWithRetries.swift +++ b/Sources/Confidence/Apply/FlagApplierWithRetries.swift @@ -140,7 +140,7 @@ final class FlagApplierWithRetries: FlagApplier { request: ApplyFlagsRequest ) async -> ApplyFlagResult { do { - let header = telemetry.getSnapshot() + let header: Data = telemetry.getSnapshot() return try await httpClient.post(path: ":apply", data: request, header: header) } catch { return .failure(handleError(error: error)) diff --git a/Sources/Confidence/Http/HttpClient.swift b/Sources/Confidence/Http/HttpClient.swift index cdb438c6..d967f4c6 100644 --- a/Sources/Confidence/Http/HttpClient.swift +++ b/Sources/Confidence/Http/HttpClient.swift @@ -4,7 +4,7 @@ typealias HttpClientResult = Result, Error> internal protocol HttpClient { func post(path: String, data: Encodable) async throws -> HttpClientResult - func post(path: String, data: Encodable, header: Encodable) async throws -> HttpClientResult + func post(path: String, data: Encodable, header: Data) async throws -> HttpClientResult } struct HttpClientResponse { diff --git a/Sources/Confidence/Http/NetworkClient.swift b/Sources/Confidence/Http/NetworkClient.swift index 5416297a..1d059475 100644 --- a/Sources/Confidence/Http/NetworkClient.swift +++ b/Sources/Confidence/Http/NetworkClient.swift @@ -29,7 +29,7 @@ final class NetworkClient: HttpClient { self.timeoutIntervalForRequests = timeoutIntervalForRequests } - func post(path: String, data: any Encodable, header: any Encodable) async throws -> HttpClientResult where T : Decodable { + func post(path: String, data: any Encodable, header: Data) async throws -> HttpClientResult where T : Decodable { let request = try buildRequest(path: path, data: data, header: header) return try await post(request: request) } @@ -107,7 +107,7 @@ extension NetworkClient { return URL(string: "\(normalisedBase)\(normalisedPath)") } - private func buildRequest(path: String, data: Encodable, header: Encodable? = nil) throws -> URLRequest { + private func buildRequest(path: String, data: Encodable, header: Data? = nil) throws -> URLRequest { guard let url = constructURL(base: baseUrl, path: path) else { throw ConfidenceError.internalError(message: "Could not create service url") } @@ -123,26 +123,10 @@ extension NetworkClient { encoder.dateEncodingStrategy = .iso8601 if let header = header { - let jsonHeaderData = try encoder.encode(header) - - if let headerJsonString = String(data: jsonHeaderData, encoding: .utf8) { - request.addValue(headerJsonString, forHTTPHeaderField: "Confidence-Metadata") - } - } - // TMP - TESTING - if let headers = request.allHTTPHeaderFields, let metadata = headers["Confidence-Metadata"] { - if let data = metadata.data(using: .utf8) { - do { - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - let prettyData = try JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) - - if let prettyPrintedString = String(data: prettyData, encoding: .utf8) { - print(prettyPrintedString) - } - } catch { - print("Failed to pretty print JSON: \(error)") - } - } + request.addValue(header.base64EncodedString(), forHTTPHeaderField: "Confidence-Metadata") + // TMP - TESTING + let telemetryData = try LibraryData(serializedBytes: header) + print(telemetryData) } let jsonData = try encoder.encode(data) diff --git a/Sources/Confidence/Telemetry/TelemetryManager.swift b/Sources/Confidence/Telemetry/TelemetryManager.swift index 72cb5e66..2ba00051 100644 --- a/Sources/Confidence/Telemetry/TelemetryManager.swift +++ b/Sources/Confidence/Telemetry/TelemetryManager.swift @@ -3,27 +3,39 @@ import Foundation protocol TelemetryManager { func incrementStaleAccess() func incrementFlagTypeMismatch() - func getSnapshot() -> TelemetryPayload + func getSnapshot() -> Data } class Telemetry: TelemetryManager { private let queue = DispatchQueue(label: "com.confidence.telemetry_manager") - private var staleAccessCounter = 0; - private var flagTypeMismatchCounter = 0; + private var staleAccessCounter: Int32 = 0; + private var flagTypeMismatchCounter: Int32 = 0; public init() {} static public let shared: TelemetryManager = Telemetry.init() - public func getSnapshot() -> TelemetryPayload { - TelemetryPayload( - libraryId: ConfidenceMetadata.defaultMetadata.id, - libraryVersion: ConfidenceMetadata.defaultMetadata.version, - countTraces: [ - CountTrace.init(traceId: TraceId.staleAccess, count: getStaleAccessAndReset()), - CountTrace.init(traceId: TraceId.typeMismatch, count: getFlagTypeMismatchAndReset()), - ], - durationsTraces: []) + public func getSnapshot() -> Data { + // Initialize your data using the generated types + var countTrace1 = CountTrace() + countTrace1.traceID = .traceStale + countTrace1.count = getStaleAccessAndReset() + + var countTrace2 = CountTrace() + countTrace2.traceID = .traceTypeMismatch + countTrace2.count = getFlagTypeMismatchAndReset() + + var libraryData = LibraryData() + libraryData.countTraces = [countTrace1, countTrace2] + libraryData.libraryID = .sdkSwiftCore + libraryData.libraryVersion = "1.0.1" + libraryData.durationsTraces = [] + do { + return try libraryData.serializedData() + } catch { + print("Failed to encode telemetry data: \(error)") + return Data() + } } public func incrementStaleAccess() { @@ -38,7 +50,7 @@ class Telemetry: TelemetryManager { } } - private func getStaleAccessAndReset() -> Int { + private func getStaleAccessAndReset() -> Int32 { return queue.sync { let currentCounter = staleAccessCounter staleAccessCounter = 0; @@ -46,7 +58,7 @@ class Telemetry: TelemetryManager { } } - private func getFlagTypeMismatchAndReset() -> Int { + private func getFlagTypeMismatchAndReset() -> Int32 { return queue.sync { let currentCounter = flagTypeMismatchCounter flagTypeMismatchCounter = 0; diff --git a/Sources/Confidence/Telemetry/TelemetryPayload.pb.swift b/Sources/Confidence/Telemetry/TelemetryPayload.pb.swift new file mode 100644 index 00000000..8a48301e --- /dev/null +++ b/Sources/Confidence/Telemetry/TelemetryPayload.pb.swift @@ -0,0 +1,302 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Sources/Confidence/Telemetry/TelemetryPayload.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +enum TraceId: SwiftProtobuf.Enum { + typealias RawValue = Int + case traceUnspecified // = 0 + case traceStale // = 1 + case traceTypeMismatch // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .traceUnspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .traceUnspecified + case 1: self = .traceStale + case 2: self = .traceTypeMismatch + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .traceUnspecified: return 0 + case .traceStale: return 1 + case .traceTypeMismatch: return 2 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension TraceId: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [TraceId] = [ + .traceUnspecified, + .traceStale, + .traceTypeMismatch, + ] +} + +#endif // swift(>=4.2) + +enum SdkId: SwiftProtobuf.Enum { + typealias RawValue = Int + case sdkUnknown // = 0 + case sdkSwiftCore // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .sdkUnknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .sdkUnknown + case 1: self = .sdkSwiftCore + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .sdkUnknown: return 0 + case .sdkSwiftCore: return 1 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension SdkId: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [SdkId] = [ + .sdkUnknown, + .sdkSwiftCore, + ] +} + +#endif // swift(>=4.2) + +struct LibraryData { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var libraryID: SdkId = .sdkUnknown + + var libraryVersion: String = String() + + var countTraces: [CountTrace] = [] + + var durationsTraces: [AverageDurationTrace] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct CountTrace { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var traceID: TraceId = .traceUnspecified + + var count: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct AverageDurationTrace { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var traceID: TraceId = .traceUnspecified + + var millisAverage: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension TraceId: @unchecked Sendable {} +extension SdkId: @unchecked Sendable {} +extension LibraryData: @unchecked Sendable {} +extension CountTrace: @unchecked Sendable {} +extension AverageDurationTrace: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension TraceId: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "TRACE_UNSPECIFIED"), + 1: .same(proto: "TRACE_STALE"), + 2: .same(proto: "TRACE_TYPE_MISMATCH"), + ] +} + +extension SdkId: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "SDK_UNKNOWN"), + 1: .same(proto: "SDK_SWIFT_CORE"), + ] +} + +extension LibraryData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "LibraryData" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "libraryId"), + 2: .same(proto: "libraryVersion"), + 3: .same(proto: "countTraces"), + 4: .same(proto: "durationsTraces"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.libraryID) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.libraryVersion) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.countTraces) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.durationsTraces) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.libraryID != .sdkUnknown { + try visitor.visitSingularEnumField(value: self.libraryID, fieldNumber: 1) + } + if !self.libraryVersion.isEmpty { + try visitor.visitSingularStringField(value: self.libraryVersion, fieldNumber: 2) + } + if !self.countTraces.isEmpty { + try visitor.visitRepeatedMessageField(value: self.countTraces, fieldNumber: 3) + } + if !self.durationsTraces.isEmpty { + try visitor.visitRepeatedMessageField(value: self.durationsTraces, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: LibraryData, rhs: LibraryData) -> Bool { + if lhs.libraryID != rhs.libraryID {return false} + if lhs.libraryVersion != rhs.libraryVersion {return false} + if lhs.countTraces != rhs.countTraces {return false} + if lhs.durationsTraces != rhs.durationsTraces {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension CountTrace: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "CountTrace" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "traceId"), + 2: .same(proto: "count"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.traceID) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.count) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.traceID != .traceUnspecified { + try visitor.visitSingularEnumField(value: self.traceID, fieldNumber: 1) + } + if self.count != 0 { + try visitor.visitSingularInt32Field(value: self.count, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: CountTrace, rhs: CountTrace) -> Bool { + if lhs.traceID != rhs.traceID {return false} + if lhs.count != rhs.count {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension AverageDurationTrace: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "AverageDurationTrace" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "traceId"), + 2: .same(proto: "millisAverage"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.traceID) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.millisAverage) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.traceID != .traceUnspecified { + try visitor.visitSingularEnumField(value: self.traceID, fieldNumber: 1) + } + if self.millisAverage != 0 { + try visitor.visitSingularFloatField(value: self.millisAverage, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: AverageDurationTrace, rhs: AverageDurationTrace) -> Bool { + if lhs.traceID != rhs.traceID {return false} + if lhs.millisAverage != rhs.millisAverage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Confidence/Telemetry/TelemetryPayload.proto b/Sources/Confidence/Telemetry/TelemetryPayload.proto new file mode 100644 index 00000000..f3331b59 --- /dev/null +++ b/Sources/Confidence/Telemetry/TelemetryPayload.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +message LibraryData { + SdkId libraryId = 1; + string libraryVersion = 2; + repeated CountTrace countTraces = 3; + repeated AverageDurationTrace durationsTraces = 4; +} + +message CountTrace { + TraceId traceId = 1; + int32 count = 2; +} + +message AverageDurationTrace { + TraceId traceId = 1; + float millisAverage = 2; +} + +enum TraceId { + TRACE_UNSPECIFIED = 0; + TRACE_STALE = 1; + TRACE_TYPE_MISMATCH = 2; +} + +enum SdkId { + SDK_UNKNOWN= 0; + SDK_SWIFT_CORE = 1; +} diff --git a/Sources/Confidence/Telemetry/TelemetryPayload.swift b/Sources/Confidence/Telemetry/TelemetryPayload.swift deleted file mode 100644 index 3a95f6b0..00000000 --- a/Sources/Confidence/Telemetry/TelemetryPayload.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -struct TelemetryPayload: Encodable { - var libraryId: Int - var libraryVersion: String - var countTraces: [CountTrace] - var durationsTraces: [DurationsTrace] -} - -struct CountTrace: Encodable { - var traceId: TraceId - var count: Int -} - -struct DurationsTrace: Encodable { - var traceId: TraceId - var millisDuration: [Int] -} - -enum TraceId: Int, Encodable { - case typeMismatch = 1 - case staleAccess = 2 -} diff --git a/Tests/ConfidenceTests/Helpers/HttpClientMock.swift b/Tests/ConfidenceTests/Helpers/HttpClientMock.swift index 81bd326c..3ab52934 100644 --- a/Tests/ConfidenceTests/Helpers/HttpClientMock.swift +++ b/Tests/ConfidenceTests/Helpers/HttpClientMock.swift @@ -23,7 +23,7 @@ final class HttpClientMock: HttpClient { try handlePost(path: path, data: data) } - func post(path: String, data: any Encodable, header: any Encodable) async throws -> HttpClientResult where T : Decodable { + func post(path: String, data: any Encodable, header: Data) async throws -> HttpClientResult where T : Decodable { try handlePost(path: path, data: data) }