From ba119c84857127ac9ff1e246cfd5237863a903b0 Mon Sep 17 00:00:00 2001 From: surajthomask Date: Sun, 14 Apr 2024 21:46:02 +0400 Subject: [PATCH 1/6] Add timestamp to date decoding strategy --- .../Decoding/BSONDecoderSettings.swift | 18 ++++++++--- .../Decoding/KeyedBSONDecodingContainer.swift | 31 ++++++++++++++----- .../SingleValueBSONDecodingContainer.swift | 16 +++++----- .../UnkeyedBSONDecodingContainer.swift | 16 +++++----- 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift index 626a384..5606033 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift @@ -147,7 +147,17 @@ public struct BSONDecoderSettings { /// This may be used for applying fallback values or other custom behaviour case custom(DecodingStrategy) } - + + public enum TimestampToDateDecodingStrategy { + + /// Do not convert, and throw an error + case never + /// Convert the timestamp relative to the unix epoch + case relativeToUnixEpoch + /// Convert the timestamp relative to the reference date, 1st of January 2000. + case relativeToReferenceDate + } + /// If `true`, BSON Null values will be regarded as `nil` public var decodeNullAsNil: Bool = true public var filterDollarPrefix = false @@ -158,9 +168,9 @@ public struct BSONDecoderSettings { /// If `true`, allows decoding ObjectIds from Strings if they're formatted as a 24-character hexString public var decodeObjectIdFromString: Bool = false - /// If `true`, allows decoding Date from a Double (TimeInterval) - public var decodeDateFromTimestamp: Bool = true - + /// A strategy to apply when converting time interval to date objects + public var timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy = .relativeToUnixEpoch + /// A strategy that is applied when encountering a request to decode a `Float` public var floatDecodingStrategy: FloatDecodingStrategy diff --git a/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift b/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift index 8507918..28896da 100644 --- a/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift +++ b/Sources/BSON/Codable/Decoding/KeyedBSONDecodingContainer.swift @@ -165,20 +165,22 @@ internal struct KeyedBSONDecodingContainer: KeyedDecodingContainer return date } catch { - if decoder.settings.decodeDateFromTimestamp { - switch self.document[key] { + let date: Date? + let strategy = decoder.settings.timestampToDateDecodingStrategy + switch self.document[key] { case let int as Int: - return Date(timeIntervalSince1970: Double(int)) as! T + date = strategy.convertTimeStampToDate(TimeInterval(int)) case let int as Int32: - return Date(timeIntervalSince1970: Double(int)) as! T + date = strategy.convertTimeStampToDate(TimeInterval(int)) case let double as Double: - return Date(timeIntervalSince1970: double) as! T + date = strategy.convertTimeStampToDate(double) default: throw error - } - } else { + } + guard let returnDate = date as? T else { throw error } + return returnDate } } else if let type = T.self as? BSONPrimitiveConvertible.Type { return try type.init(primitive: self.document[key]) as! T @@ -238,3 +240,18 @@ internal struct KeyedBSONDecodingContainer: KeyedDecodingContainer return decoder } } + +extension BSONDecoderSettings.TimestampToDateDecodingStrategy { + + func convertTimeStampToDate(_ timestamp: TimeInterval) -> Date? { + + switch self { + case .never: + return nil + case .relativeToUnixEpoch: + return Date(timeIntervalSince1970: timestamp) + case .relativeToReferenceDate: + return Date(timeIntervalSinceReferenceDate: timestamp) + } + } +} diff --git a/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift b/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift index 5eb15be..e195097 100644 --- a/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift +++ b/Sources/BSON/Codable/Decoding/SingleValueBSONDecodingContainer.swift @@ -167,20 +167,22 @@ internal struct SingleValueBSONDecodingContainer: SingleValueDecodingContainer, return date } catch { - if decoder.settings.decodeDateFromTimestamp { - switch self.decoder.primitive { + let date: Date? + let strategy = decoder.settings.timestampToDateDecodingStrategy + switch self.decoder.primitive { case let int as Int: - return Date(timeIntervalSince1970: Double(int)) as! T + date = strategy.convertTimeStampToDate(TimeInterval(int)) case let int as Int32: - return Date(timeIntervalSince1970: Double(int)) as! T + date = strategy.convertTimeStampToDate(TimeInterval(int)) case let double as Double: - return Date(timeIntervalSince1970: double) as! T + date = strategy.convertTimeStampToDate(double) default: throw error - } - } else { + } + guard let returnDate = date as? T else { throw error } + return returnDate } } else if let type = T.self as? BSONPrimitiveConvertible.Type { return try type.init(primitive: self.decoder.primitive) as! T diff --git a/Sources/BSON/Codable/Decoding/UnkeyedBSONDecodingContainer.swift b/Sources/BSON/Codable/Decoding/UnkeyedBSONDecodingContainer.swift index 1faae00..7ad2f79 100644 --- a/Sources/BSON/Codable/Decoding/UnkeyedBSONDecodingContainer.swift +++ b/Sources/BSON/Codable/Decoding/UnkeyedBSONDecodingContainer.swift @@ -123,20 +123,22 @@ internal struct UnkeyedBSONDecodingContainer: UnkeyedDecodingContainer { return date } catch { - if decoder.settings.decodeDateFromTimestamp { - switch element { + let date: Date? + let strategy = decoder.settings.timestampToDateDecodingStrategy + switch element { case let int as Int: - return Date(timeIntervalSince1970: Double(int)) as! T + date = strategy.convertTimeStampToDate(TimeInterval(int)) case let int as Int32: - return Date(timeIntervalSince1970: Double(int)) as! T + date = strategy.convertTimeStampToDate(TimeInterval(int)) case let double as Double: - return Date(timeIntervalSince1970: double) as! T + date = strategy.convertTimeStampToDate(double) default: throw error - } - } else { + } + guard let returnDate = date as? T else { throw error } + return returnDate } } else if let type = T.self as? BSONPrimitiveConvertible.Type { return try type.init(primitive: self.nextElement()) as! T From 79cf36bdd7a371e094f30306cbb05858925d4459 Mon Sep 17 00:00:00 2001 From: surajthomask Date: Sun, 14 Apr 2024 21:50:36 +0400 Subject: [PATCH 2/6] Add helper function to mutate decoder timestamp decoding strategy --- Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift index 5606033..c0ccdae 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift @@ -206,4 +206,11 @@ public struct BSONDecoderSettings { /// A strategy that is applied when encountering a request to decode a `UInt` public var uintDecodingStrategy: IntegerDecodingStrategy + + public func with(timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy) -> Self { + + var settings = self + settings.timestampToDateDecodingStrategy = timestampToDateDecodingStrategy + return settings + } } From 76355515d30208797e031b61742d4f766a3086da Mon Sep 17 00:00:00 2001 From: surajthomask Date: Mon, 15 Apr 2024 09:24:04 +0400 Subject: [PATCH 3/6] Set default date decoding strategy to relative to reference date --- Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift index c0ccdae..6a8ed85 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift @@ -12,6 +12,7 @@ public struct BSONDecoderSettings { filterDollarPrefix: false, stringDecodingStrategy: .string, decodeObjectIdFromString: false, + timestampToDateDecodingStrategy: .never, floatDecodingStrategy: .double, doubleDecodingStrategy: .double, int8DecodingStrategy: .anyInteger, @@ -34,6 +35,7 @@ public struct BSONDecoderSettings { filterDollarPrefix: false, stringDecodingStrategy: .adaptive, decodeObjectIdFromString: true, + timestampToDateDecodingStrategy: .relativeToReferenceDate, floatDecodingStrategy: .adaptive, doubleDecodingStrategy: .adaptive, int8DecodingStrategy: .adaptive, @@ -169,7 +171,7 @@ public struct BSONDecoderSettings { public var decodeObjectIdFromString: Bool = false /// A strategy to apply when converting time interval to date objects - public var timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy = .relativeToUnixEpoch + public var timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy = .relativeToReferenceDate /// A strategy that is applied when encountering a request to decode a `Float` public var floatDecodingStrategy: FloatDecodingStrategy From d16f2a1378309d3cff25915db53f331f8ae12e59 Mon Sep 17 00:00:00 2001 From: surajthomask Date: Mon, 15 Apr 2024 09:24:21 +0400 Subject: [PATCH 4/6] Add bson date decoding tests --- Tests/BSONTests/BSONCorpusTests.swift | 36 ++- Tests/BSONTests/BSONDateDecodingTests.swift | 269 ++++++++++++++++++++ 2 files changed, 286 insertions(+), 19 deletions(-) create mode 100644 Tests/BSONTests/BSONDateDecodingTests.swift diff --git a/Tests/BSONTests/BSONCorpusTests.swift b/Tests/BSONTests/BSONCorpusTests.swift index 1ccb944..6412e75 100644 --- a/Tests/BSONTests/BSONCorpusTests.swift +++ b/Tests/BSONTests/BSONCorpusTests.swift @@ -379,23 +379,23 @@ final class BSONCorpusTests: XCTestCase { // XCTAssertFalse(Document(bytes: [0x15,0x00,0x00,0x00,0x03,0x66,0x6F,0x6F,0x00,0x0A,0x00,0x00,0x00,0x08,0x62,0x61,0x72,0x00,0x01,0x00,0x00]).validate().valid) // XCTAssertFalse(Document(bytes: [0x1C,0x00,0x00,0x00,0x03,0x66,0x6F,0x6F,0x00,0x12,0x00,0x00,0x00,0x02,0x62,0x61,0x72,0x00,0x05,0x00,0x00,0x00,0x62,0x61,0x7A,0x00,0x00,0x00]).validate().valid) // } -// -// func testDateTime() { -// let doc0 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]) -// let doc1 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0xD8,0xD6,0xCC,0x3B,0x01,0x00,0x00,0x00]) -// let doc2 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0x3C,0xE7,0xB9,0xBD,0xFF,0xFF,0xFF,0x00]) -// -// XCTAssert(doc0.validate().valid) -// XCTAssert(doc1.validate().valid) -// XCTAssert(doc2.validate().valid) -// -// XCTAssertEqual(doc0["a"] as? Date, Date(timeIntervalSince1970: 0)) -// XCTAssertEqual(doc1["a"] as? Date, Date(timeIntervalSince1970: 1356351330.500)) -// XCTAssertEqual(doc2["a"] as? Date, Date(timeIntervalSince1970: -284643869.500)) -// -// XCTAssertFalse(Document(bytes: [0x0C,0x00,0x00,0x00,0x09,0x61,0x00,0x12,0x34,0x56,0x78,0x00]).validate().valid) -// } -// + + func testDateTime() { + let doc0 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]) + let doc1 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0xD8,0xD6,0xCC,0x3B,0x01,0x00,0x00,0x00]) + let doc2 = Document(bytes: [0x10,0x00,0x00,0x00,0x09,0x61,0x00,0xC4,0x3C,0xE7,0xB9,0xBD,0xFF,0xFF,0xFF,0x00]) + + XCTAssert(doc0.validate().isValid) + XCTAssert(doc1.validate().isValid) + XCTAssert(doc2.validate().isValid) + + XCTAssertEqual(doc0["a"] as? Date, Date(timeIntervalSince1970: 0)) + XCTAssertEqual(doc1["a"] as? Date, Date(timeIntervalSince1970: 1356351330.500)) + XCTAssertEqual(doc2["a"] as? Date, Date(timeIntervalSince1970: -284643869.500)) + + XCTAssertFalse(Document(bytes: [0x0C,0x00,0x00,0x00,0x09,0x61,0x00,0x12,0x34,0x56,0x78,0x00]).validate().isValid) + } + // func testBinary() { // let doc0 = Document(bytes: [0x0D,0x00,0x00,0x00,0x05,0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00]) // let doc1 = Document(bytes: [0x0F,0x00,0x00,0x00,0x05,0x78,0x00,0x02,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00]) @@ -525,5 +525,3 @@ final class BSONCorpusTests: XCTestCase { // // } } - - diff --git a/Tests/BSONTests/BSONDateDecodingTests.swift b/Tests/BSONTests/BSONDateDecodingTests.swift new file mode 100644 index 0000000..68f02d0 --- /dev/null +++ b/Tests/BSONTests/BSONDateDecodingTests.swift @@ -0,0 +1,269 @@ +// +// BSONTimeStampToDateDecodingTests.swift +// +// +// Created by Suraj Thomas Karathra on 15/04/2024. +// + +import Foundation +import XCTest +import BSON + +final class BSONTimeStampToDateDecodingTests: XCTestCase { + + private struct Target: Decodable { + + let date: Date + } + + func testPrimitiveDate() throws { + + struct Source: Encodable { + + let date: Date + } + + let source = Source(date: Date()) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let target = try BSONDecoder().decode(Target.self, from: sourceBSON) + + XCTAssertEqual( + source.date.timeIntervalSinceReferenceDate, + target.date.timeIntervalSinceReferenceDate, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of primitive date failed : \(error.localizedDescription)") + } + } + + func testIntegerDateRelativeToReferenceDate() throws { + + struct Source: Encodable { + + let date: Int + } + + let timeInterval: TimeInterval = 1356351330 + let source = Source(date: Int(timeInterval)) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let settings = BSONDecoderSettings.adaptive.with(timestampToDateDecodingStrategy: .relativeToReferenceDate) + let target = try BSONDecoder(settings: settings).decode(Target.self, from: sourceBSON) + + XCTAssertEqual( + target.date.timeIntervalSinceReferenceDate, + timeInterval, + accuracy: 0.001 + ) + XCTAssertNotEqual( + target.date.timeIntervalSince1970, + timeInterval, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of date as integer failed : \(error.localizedDescription)") + } + } + + func testIntegerDateRelativeToUnixEpoch() throws { + + struct Source: Encodable { + + let date: Int + } + + let timeInterval: TimeInterval = 1356351330 + let source = Source(date: Int(timeInterval)) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let settings = BSONDecoderSettings.adaptive.with(timestampToDateDecodingStrategy: .relativeToUnixEpoch) + let target = try BSONDecoder(settings: settings).decode(Target.self, from: sourceBSON) + XCTAssertNotEqual( + target.date.timeIntervalSinceReferenceDate, + timeInterval, + accuracy: 0.001 + ) + XCTAssertEqual( + target.date.timeIntervalSince1970, + timeInterval, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of date as integer failed : \(error.localizedDescription)") + } + } + + func testIntegerDateStrictMode() throws { + + struct Source: Encodable { + + let date: Int + } + + let timeInterval: TimeInterval = 1356351330 + let source = Source(date: Int(timeInterval)) + + let sourceBSON = try BSONEncoder().encode(source) + + XCTAssertThrowsError(try BSONDecoder(settings: BSONDecoderSettings.strict).decode(Target.self, from: sourceBSON)) + } + + func testInteger32DateRelativeToReferenceDate() throws { + + struct Source: Encodable { + + let date: Int32 + } + + let timeInterval: TimeInterval = 1356351330 + let source = Source(date: Int32(timeInterval)) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let settings = BSONDecoderSettings.adaptive.with(timestampToDateDecodingStrategy: .relativeToReferenceDate) + let target = try BSONDecoder(settings: settings).decode(Target.self, from: sourceBSON) + + XCTAssertEqual( + target.date.timeIntervalSinceReferenceDate, + timeInterval, + accuracy: 0.001 + ) + XCTAssertNotEqual( + target.date.timeIntervalSince1970, + timeInterval, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of date as integer32 failed : \(error.localizedDescription)") + } + } + + func testInteger32DateRelativeToUnixEpoch() throws { + + struct Source: Encodable { + + let date: Int32 + } + + let timeInterval: TimeInterval = 1356351330 + let source = Source(date: Int32(timeInterval)) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let settings = BSONDecoderSettings.adaptive.with(timestampToDateDecodingStrategy: .relativeToUnixEpoch) + let target = try BSONDecoder(settings: settings).decode(Target.self, from: sourceBSON) + XCTAssertNotEqual( + target.date.timeIntervalSinceReferenceDate, + timeInterval, + accuracy: 0.001 + ) + XCTAssertEqual( + target.date.timeIntervalSince1970, + timeInterval, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of date as integer32 failed : \(error.localizedDescription)") + } + } + + func testInteger32DateStrictMode() throws { + + struct Source: Encodable { + + let date: Int32 + } + + let timeInterval: TimeInterval = 1356351330 + let source = Source(date: Int32(timeInterval)) + + let sourceBSON = try BSONEncoder().encode(source) + + XCTAssertThrowsError(try BSONDecoder(settings: BSONDecoderSettings.strict).decode(Target.self, from: sourceBSON)) + } + + func testDoubleDateRelativeToReferenceDate() throws { + + struct Source: Encodable { + + let date: Double + } + + let timeInterval: TimeInterval = 1356351330.5 + let source = Source(date: timeInterval) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let settings = BSONDecoderSettings.adaptive.with(timestampToDateDecodingStrategy: .relativeToReferenceDate) + let target = try BSONDecoder(settings: settings).decode(Target.self, from: sourceBSON) + XCTAssertEqual( + target.date.timeIntervalSinceReferenceDate, + timeInterval, + accuracy: 0.001 + ) + XCTAssertNotEqual( + target.date.timeIntervalSince1970, + timeInterval, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of date as double failed : \(error.localizedDescription)") + } + } + + func testDoubleDateRelativeToUnixEpoch() throws { + + struct Source: Encodable { + + let date: Double + } + + let timeInterval: TimeInterval = 1356351330.5 + let source = Source(date: timeInterval) + + let sourceBSON = try BSONEncoder().encode(source) + + do { + let settings = BSONDecoderSettings.adaptive.with(timestampToDateDecodingStrategy: .relativeToUnixEpoch) + let target = try BSONDecoder(settings: settings).decode(Target.self, from: sourceBSON) + XCTAssertNotEqual( + target.date.timeIntervalSinceReferenceDate, + timeInterval, + accuracy: 0.001 + ) + XCTAssertEqual( + target.date.timeIntervalSince1970, + timeInterval, + accuracy: 0.001 + ) + } catch let error { + XCTFail("Decoding of date as double failed : \(error.localizedDescription)") + } + } + + func testDoubleDateStrictMode() throws { + + struct Source: Encodable { + + let date: Double + } + + let timeInterval: TimeInterval = 1356351330.5 + let source = Source(date: timeInterval) + + let sourceBSON = try BSONEncoder().encode(source) + + XCTAssertThrowsError(try BSONDecoder(settings: BSONDecoderSettings.strict).decode(Target.self, from: sourceBSON)) + } +} From cac4baa5e7e575baa3caf364ab5ea4c6a826379a Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sat, 20 Apr 2024 20:58:08 +0200 Subject: [PATCH 5/6] Add benchmarks, enable FastBSONDecoder as part of BSONDecoder. New overridable `default` BSONDecoderSettings --- .../BSON/BSONDecoderBenchmarks.swift | 77 ++++++++++++ Benchmarks/Benchmarks/BSON/Benchmarks.swift | 13 ++ Benchmarks/Package.swift | 27 +++++ .../BSON/Codable/Decoding/BSONDecoder.swift | 10 +- .../Decoding/BSONDecoderSettings.swift | 114 +++++++++++------- 5 files changed, 198 insertions(+), 43 deletions(-) create mode 100644 Benchmarks/Benchmarks/BSON/BSONDecoderBenchmarks.swift create mode 100644 Benchmarks/Benchmarks/BSON/Benchmarks.swift create mode 100644 Benchmarks/Package.swift diff --git a/Benchmarks/Benchmarks/BSON/BSONDecoderBenchmarks.swift b/Benchmarks/Benchmarks/BSON/BSONDecoderBenchmarks.swift new file mode 100644 index 0000000..522ee00 --- /dev/null +++ b/Benchmarks/Benchmarks/BSON/BSONDecoderBenchmarks.swift @@ -0,0 +1,77 @@ +import BSON +import Benchmark + +func bsonDecoderBenchmarks() { + let smallDocument: Document = [ + "string": "Hello, world!", + "int": 42, + "double": 3.14159, + "bool": true, + "array": [1, 2, 3, 4, 5], + "document": ["key": "value"], + ] + + struct SmallType: Codable { + let string: String + let int: Int + let double: Double + let bool: Bool + let array: [Int] + let document: [String: String] + } + + Benchmark("BSONDecoder:fastPath:small") { _ in + blackHole( + try BSONDecoder(settings: .fastPath) + .decode(SmallType.self, from: smallDocument) + ) + } + + Benchmark("BSONDecoder:adaptive:small") { _ in + blackHole( + try BSONDecoder(settings: .adaptive) + .decode(SmallType.self, from: smallDocument) + ) + } + + let largeDocument: Document = [ + "string": "Hello, world!", + "int": 42, + "double": 3.14159, + "bool": true, + "array": [1, 2, 3, 4, 5], + "document": ["key": "value"], + "nested": [ + "string": "Hello, world!", + "int": 42, + "double": 3.14159, + "bool": true, + "array": [1, 2, 3, 4, 5], + "document": ["key": "value"], + ] as Document, + ] + + struct LargeType: Codable { + let string: String + let int: Int + let double: Double + let bool: Bool + let array: [Int] + let document: [String: String] + let nested: SmallType + } + + Benchmark("BSONDecoder:fastPath:large") { _ in + blackHole( + try BSONDecoder(settings: .fastPath) + .decode(LargeType.self, from: largeDocument) + ) + } + + Benchmark("BSONDecoder:adaptive:large") { _ in + blackHole( + try BSONDecoder(settings: .adaptive) + .decode(LargeType.self, from: largeDocument) + ) + } +} diff --git a/Benchmarks/Benchmarks/BSON/Benchmarks.swift b/Benchmarks/Benchmarks/BSON/Benchmarks.swift new file mode 100644 index 0000000..150be71 --- /dev/null +++ b/Benchmarks/Benchmarks/BSON/Benchmarks.swift @@ -0,0 +1,13 @@ +import Benchmark + +let benchmarks = { + Benchmark.defaultConfiguration = .init( + metrics: [ + .cpuTotal, + .throughput, + .mallocCountTotal, + ], + warmupIterations: 10 + ) + bsonDecoderBenchmarks() +} diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 0000000..dbc25f4 --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Benchmarks", + platforms: [.macOS(.v14)], + dependencies: [ + .package(url: "https://github.com/ordo-one/package-benchmark.git", .upToNextMajor(from: "1.0.0")), + .package(path: "../"), + ], + targets: [ + // BSON benchmarks + .executableTarget( + name: "BSONBenchmarks", + dependencies: [ + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "BSON", package: "BSON"), + ], + path: "Benchmarks/BSON", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark"), + ] + ), + ] +) diff --git a/Sources/BSON/Codable/Decoding/BSONDecoder.swift b/Sources/BSON/Codable/Decoding/BSONDecoder.swift index fd90359..3304d05 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoder.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoder.swift @@ -9,7 +9,7 @@ public struct BSONDecoder { public var userInfo: [CodingUserInfoKey: Any] = [:] /// Creates a new decoder using fresh settings - public init(settings: BSONDecoderSettings = .adaptive) { + public init(settings: BSONDecoderSettings = .default) { self.settings = settings } } @@ -269,12 +269,20 @@ extension BSONDecoder { if let value = primitive as? D { return value } + + if self.settings.fastPath { + return try FastBSONDecoder().decode(D.self, from: primitive) + } let decoder = _BSONDecoder(wrapped: .primitive(primitive), settings: self.settings, codingPath: [], userInfo: self.userInfo) return try D(from: decoder) } public func decode(_ type: D.Type, from document: Document) throws -> D { + if self.settings.fastPath { + return try FastBSONDecoder().decode(D.self, from: document) + } + let decoder = _BSONDecoder(wrapped: .document(document), settings: self.settings, codingPath: [], userInfo: self.userInfo) return try D(from: decoder) } diff --git a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift index 6a8ed85..da38e93 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift @@ -6,50 +6,72 @@ public struct BSONDecoderSettings { /// /// - Float: Decode from Double /// - Non-native Integer types: .anyInteger - public static var strict: BSONDecoderSettings { - return .init( - decodeNullAsNil: false, - filterDollarPrefix: false, - stringDecodingStrategy: .string, - decodeObjectIdFromString: false, - timestampToDateDecodingStrategy: .never, - floatDecodingStrategy: .double, - doubleDecodingStrategy: .double, - int8DecodingStrategy: .anyInteger, - int16DecodingStrategy: .anyInteger, - int32DecodingStrategy: .int32, - int64DecodingStrategy: .int64, - intDecodingStrategy: .anyInteger, - uint8DecodingStrategy: .anyInteger, - uint16DecodingStrategy: .anyInteger, - uint32DecodingStrategy: .anyInteger, - uint64DecodingStrategy: .anyInteger, - uintDecodingStrategy: .anyInteger - ) - } + public static let strict: BSONDecoderSettings = BSONDecoderSettings( + fastPath: false, + decodeNullAsNil: false, + filterDollarPrefix: false, + stringDecodingStrategy: .string, + decodeObjectIdFromString: false, + timestampToDateDecodingStrategy: .never, + floatDecodingStrategy: .double, + doubleDecodingStrategy: .double, + int8DecodingStrategy: .anyInteger, + int16DecodingStrategy: .anyInteger, + int32DecodingStrategy: .int32, + int64DecodingStrategy: .int64, + intDecodingStrategy: .anyInteger, + uint8DecodingStrategy: .anyInteger, + uint16DecodingStrategy: .anyInteger, + uint32DecodingStrategy: .anyInteger, + uint64DecodingStrategy: .anyInteger, + uintDecodingStrategy: .anyInteger + ) + + /// Uses ``FastBSONDecoder`` + public static let fastPath: BSONDecoderSettings = BSONDecoderSettings( + fastPath: true, + decodeNullAsNil: false, + filterDollarPrefix: false, + stringDecodingStrategy: .string, + decodeObjectIdFromString: false, + timestampToDateDecodingStrategy: .never, + floatDecodingStrategy: .double, + doubleDecodingStrategy: .double, + int8DecodingStrategy: .anyInteger, + int16DecodingStrategy: .anyInteger, + int32DecodingStrategy: .int32, + int64DecodingStrategy: .int64, + intDecodingStrategy: .anyInteger, + uint8DecodingStrategy: .anyInteger, + uint16DecodingStrategy: .anyInteger, + uint32DecodingStrategy: .anyInteger, + uint64DecodingStrategy: .anyInteger, + uintDecodingStrategy: .anyInteger + ) + + public static var `default`: BSONDecoderSettings = .adaptive /// Tries to decode values, even if the types do not match. Some precision loss is possible. - public static var adaptive: BSONDecoderSettings { - return .init( - decodeNullAsNil: true, - filterDollarPrefix: false, - stringDecodingStrategy: .adaptive, - decodeObjectIdFromString: true, - timestampToDateDecodingStrategy: .relativeToReferenceDate, - floatDecodingStrategy: .adaptive, - doubleDecodingStrategy: .adaptive, - int8DecodingStrategy: .adaptive, - int16DecodingStrategy: .adaptive, - int32DecodingStrategy: .adaptive, - int64DecodingStrategy: .adaptive, - intDecodingStrategy: .adaptive, - uint8DecodingStrategy: .adaptive, - uint16DecodingStrategy: .adaptive, - uint32DecodingStrategy: .adaptive, - uint64DecodingStrategy: .adaptive, - uintDecodingStrategy: .adaptive - ) - } + public static let adaptive: BSONDecoderSettings = BSONDecoderSettings( + fastPath: false, + decodeNullAsNil: true, + filterDollarPrefix: false, + stringDecodingStrategy: .adaptive, + decodeObjectIdFromString: true, + timestampToDateDecodingStrategy: .relativeToUnixEpoch, + floatDecodingStrategy: .adaptive, + doubleDecodingStrategy: .adaptive, + int8DecodingStrategy: .adaptive, + int16DecodingStrategy: .adaptive, + int32DecodingStrategy: .adaptive, + int64DecodingStrategy: .adaptive, + intDecodingStrategy: .adaptive, + uint8DecodingStrategy: .adaptive, + uint16DecodingStrategy: .adaptive, + uint32DecodingStrategy: .adaptive, + uint64DecodingStrategy: .adaptive, + uintDecodingStrategy: .adaptive + ) /// A strategy used to decode `P` from a BSON `Primitive?` value /// @@ -160,6 +182,8 @@ public struct BSONDecoderSettings { case relativeToReferenceDate } + public var fastPath: Bool + /// If `true`, BSON Null values will be regarded as `nil` public var decodeNullAsNil: Bool = true public var filterDollarPrefix = false @@ -169,6 +193,12 @@ public struct BSONDecoderSettings { /// If `true`, allows decoding ObjectIds from Strings if they're formatted as a 24-character hexString public var decodeObjectIdFromString: Bool = false + + /// If `true`, allows decoding Date from a Double (TimeInterval) + public var decodeDateFromTimestamp: Bool { + get { timestampToDateDecodingStrategy != .never } + set { timestampToDateDecodingStrategy = newValue ? .relativeToUnixEpoch : .never } + } /// A strategy to apply when converting time interval to date objects public var timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy = .relativeToReferenceDate From 6dbde5e4151e8d6483e111a198ab46a945cd22c6 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 21 Apr 2024 09:27:31 +0200 Subject: [PATCH 6/6] Fix boolean to enum case conversion for `relativeToReferenceDate` --- .../BSON/Codable/Decoding/BSONDecoderSettings.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift index da38e93..2c4753a 100644 --- a/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift +++ b/Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift @@ -197,7 +197,18 @@ public struct BSONDecoderSettings { /// If `true`, allows decoding Date from a Double (TimeInterval) public var decodeDateFromTimestamp: Bool { get { timestampToDateDecodingStrategy != .never } - set { timestampToDateDecodingStrategy = newValue ? .relativeToUnixEpoch : .never } + set(decodeDateFromTimestamp) { + switch timestampToDateDecodingStrategy { + case .never: + if decodeDateFromTimestamp { + timestampToDateDecodingStrategy = .relativeToUnixEpoch + } + case .relativeToReferenceDate, .relativeToUnixEpoch: + if !decodeDateFromTimestamp { + timestampToDateDecodingStrategy = .never + } + } + } } /// A strategy to apply when converting time interval to date objects