Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date decoding strategy, benchmarks and allow changing default BSON settings #83

Merged
merged 6 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
77 changes: 77 additions & 0 deletions Benchmarks/Benchmarks/BSON/BSONDecoderBenchmarks.swift
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
13 changes: 13 additions & 0 deletions Benchmarks/Benchmarks/BSON/Benchmarks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Benchmark

let benchmarks = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally forgot about Benchmark!

Benchmark.defaultConfiguration = .init(
metrics: [
.cpuTotal,
.throughput,
.mallocCountTotal,
],
warmupIterations: 10
)
bsonDecoderBenchmarks()
}
27 changes: 27 additions & 0 deletions Benchmarks/Package.swift
Original file line number Diff line number Diff line change
@@ -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"),
]
),
]
)
10 changes: 9 additions & 1 deletion Sources/BSON/Codable/Decoding/BSONDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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<D: Decodable>(_ 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)
}
Expand Down
114 changes: 72 additions & 42 deletions Sources/BSON/Codable/Decoding/BSONDecoderSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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
Expand All @@ -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 }
Joannis marked this conversation as resolved.
Show resolved Hide resolved
}

/// A strategy to apply when converting time interval to date objects
public var timestampToDateDecodingStrategy: TimestampToDateDecodingStrategy = .relativeToReferenceDate
Expand Down