diff --git a/.gitignore b/.gitignore index ef9cdf0..aa7bc02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ .DS_Store -/.build +.build /Packages xcuserdata/ DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.swiftpm .netrc .repos /build +/Benchmark/.benchmarkBaselines diff --git a/Benchmark/Benchmarks/BenchmarkHelper/BenchmarkHelper.swift b/Benchmark/Benchmarks/BenchmarkHelper/BenchmarkHelper.swift new file mode 100644 index 0000000..b046556 --- /dev/null +++ b/Benchmark/Benchmarks/BenchmarkHelper/BenchmarkHelper.swift @@ -0,0 +1,15 @@ +import Benchmark + +package let defaultMetrics: [BenchmarkMetric] = [ + .cpuTotal, + .mallocCountTotal, + .instructions, + .peakMemoryResident, +] + +package let defaultConfiguration: Benchmark.Configuration = .init( + metrics: defaultMetrics, + scalingFactor: .kilo, + maxDuration: .seconds(20), + maxIterations: 10000 +) diff --git a/Benchmark/Benchmarks/BenchmarkHelper/Message.swift b/Benchmark/Benchmarks/BenchmarkHelper/Message.swift new file mode 100644 index 0000000..9b9b3df --- /dev/null +++ b/Benchmark/Benchmarks/BenchmarkHelper/Message.swift @@ -0,0 +1,34 @@ +import Foundation + +package enum Command: CaseIterable { + case unknown + case add + case sub + case mul + case div +} + +package struct Message { + package var command: Command + package var id: String + package var value1: Int64 + package var value2: Int64 + + package init(command: Command, id: String, value1: Int64, value2: Int64) { + self.command = command + self.id = id + self.value1 = value1 + self.value2 = value2 + } +} + +extension Message { + package static func random() -> Self { + .init( + command: Command.allCases.randomElement() ?? .unknown, + id: UUID().uuidString, + value1: Int64.random(in: 0...100), + value2: Int64.random(in: 0...100) + ) + } +} diff --git a/Benchmark/Benchmarks/ProtobufKitBenchmarks/Benchmarks.swift b/Benchmark/Benchmarks/ProtobufKitBenchmarks/Benchmarks.swift new file mode 100644 index 0000000..75e1b06 --- /dev/null +++ b/Benchmark/Benchmarks/ProtobufKitBenchmarks/Benchmarks.swift @@ -0,0 +1,102 @@ +import Benchmark +import ProtobufKit +import BenchmarkHelper + +extension Command: ProtobufEnum { + package init(protobufValue: UInt) { + switch protobufValue { + case 0: self = .unknown + case 1: self = .add + case 2: self = .sub + case 3: self = .mul + case 4: self = .div + default: self = .unknown + } + } + + package var protobufValue: UInt { + switch self { + case .unknown: return 0 + case .add: return 1 + case .sub: return 2 + case .mul: return 3 + case .div: return 4 + } + } +} + +extension Message: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.enumField(1, command) + try encoder.stringField(2, id) + encoder.int64Field(3, value1) + encoder.int64Field(4, value2) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var command: Command = .add + var id: String = "" + var value1: Int64 = 0 + var value2: Int64 = 0 + while let field = try decoder.nextField() { + switch field.tag { + case 1: + if let cmd: Command = try decoder.enumField(field) { + command = cmd + } + case 2: + id = try decoder.stringField(field) + case 3: + value1 = try Int64(decoder.intField(field)) + case 4: + value2 = try Int64(decoder.intField(field)) + default: + try decoder.skipField(field) + } + } + self.init(command: command, id: id, value1: value1, value2: value2) + } +} + +let benchmarks = { + Benchmark( + "Message.encode", + configuration: defaultConfiguration + ) { benchmark in + // Elide the cost of the 'Message.init'. + var messages: [Message] = [] + messages.reserveCapacity(benchmark.scaledIterations.count) + for _ in 0..(decoder: inout D) throws where D : SwiftProtobuf.Decoder { + 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.command) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.id) }() + case 3: try { try decoder.decodeSingularInt64Field(value: &self.value1) }() + case 4: try { try decoder.decodeSingularInt64Field(value: &self.value2) }() + default: break + } + } + } + + package func traverse(visitor: inout V) throws where V : SwiftProtobuf.Visitor { + if self.command != .unknown { + try visitor.visitSingularEnumField(value: self.command, fieldNumber: 1) + } + if !self.id.isEmpty { + try visitor.visitSingularStringField(value: self.id, fieldNumber: 2) + } + if self.value1 != 0 { + try visitor.visitSingularInt64Field(value: self.value1, fieldNumber: 3) + } + if self.value2 != 0 { + try visitor.visitSingularInt64Field(value: self.value2, fieldNumber: 4) + } + } + + package func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { + guard let other = message as? Message else { + return false + } + return self.id == other.id + } +} + +let benchmarks = { + Benchmark( + "Message.encode", + configuration: defaultConfiguration + ) { benchmark in + // Elide the cost of the 'Message.init'. + var messages: [Message] = [] + messages.reserveCapacity(benchmark.scaledIterations.count) + for _ in 0../dev/null 2>&1 && pwd -P)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +FILTER=$1 +BASELINE_PATH=$2 + +cd $PROJECT_ROOT + +swift package benchmark --grouping metric \ No newline at end of file diff --git a/Benchmark/Scripts/check_thresholds.sh b/Benchmark/Scripts/check_thresholds.sh new file mode 100755 index 0000000..cf5bf8f --- /dev/null +++ b/Benchmark/Scripts/check_thresholds.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd -P)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +cd $PROJECT_ROOT + +swift package benchmark thresholds check --target ProtobufKitBenchmarks --path ./Thresholds/ProtobufKit/1.0 +swift package benchmark thresholds check --target SwiftProtobufBenchmarks --path ./Thresholds/SwiftProtobuf/1.28 \ No newline at end of file diff --git a/Benchmark/Scripts/update_thresholds.sh b/Benchmark/Scripts/update_thresholds.sh new file mode 100755 index 0000000..017a6fa --- /dev/null +++ b/Benchmark/Scripts/update_thresholds.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd -P)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +cd $PROJECT_ROOT + +swift package --allow-writing-to-package-directory benchmark thresholds update --target ProtobufKitBenchmarks --path ./Thresholds/ProtobufKit/1.0 +swift package --allow-writing-to-package-directory benchmark thresholds update --target SwiftProtobufBenchmarks --path ./Thresholds/SwiftProtobuf/1.28 \ No newline at end of file diff --git a/Benchmark/Thresholds/ProtobufKit/1.0/ProtobufKitBenchmarks.Message.decode.p90.json b/Benchmark/Thresholds/ProtobufKit/1.0/ProtobufKitBenchmarks.Message.decode.p90.json new file mode 100644 index 0000000..225d435 --- /dev/null +++ b/Benchmark/Thresholds/ProtobufKit/1.0/ProtobufKitBenchmarks.Message.decode.p90.json @@ -0,0 +1,6 @@ +{ + "cpuTotal" : 436991, + "instructions" : 6475775, + "mallocCountTotal" : 4002, + "peakMemoryResident" : 808894464 +} \ No newline at end of file diff --git a/Benchmark/Thresholds/ProtobufKit/1.0/ProtobufKitBenchmarks.Message.encode.p90.json b/Benchmark/Thresholds/ProtobufKit/1.0/ProtobufKitBenchmarks.Message.encode.p90.json new file mode 100644 index 0000000..6e74fdc --- /dev/null +++ b/Benchmark/Thresholds/ProtobufKit/1.0/ProtobufKitBenchmarks.Message.encode.p90.json @@ -0,0 +1,6 @@ +{ + "cpuTotal" : 429311, + "instructions" : 6549503, + "mallocCountTotal" : 5000, + "peakMemoryResident" : 8683520 +} \ No newline at end of file diff --git a/Benchmark/Thresholds/SwiftProtobuf/1.28/SwiftProtobufBenchmarks.Message.decode.p90.json b/Benchmark/Thresholds/SwiftProtobuf/1.28/SwiftProtobufBenchmarks.Message.decode.p90.json new file mode 100644 index 0000000..2955886 --- /dev/null +++ b/Benchmark/Thresholds/SwiftProtobuf/1.28/SwiftProtobufBenchmarks.Message.decode.p90.json @@ -0,0 +1,6 @@ +{ + "cpuTotal" : 236927, + "instructions" : 3790847, + "mallocCountTotal" : 1000, + "peakMemoryResident" : 9355264 +} \ No newline at end of file diff --git a/Benchmark/Thresholds/SwiftProtobuf/1.28/SwiftProtobufBenchmarks.Message.encode.p90.json b/Benchmark/Thresholds/SwiftProtobuf/1.28/SwiftProtobufBenchmarks.Message.encode.p90.json new file mode 100644 index 0000000..f661375 --- /dev/null +++ b/Benchmark/Thresholds/SwiftProtobuf/1.28/SwiftProtobufBenchmarks.Message.encode.p90.json @@ -0,0 +1,6 @@ +{ + "cpuTotal" : 210687, + "instructions" : 2985983, + "mallocCountTotal" : 2000, + "peakMemoryResident" : 8929280 +} \ No newline at end of file