From 9cf005f04b38106640c0fae591ba6a2695e12c8a Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Thu, 11 Apr 2024 06:08:01 -0700 Subject: [PATCH] Optimized scanning to skip decoding index entries when iterating over an extent in either direction --- Package.swift | 2 +- .../Indexes/IndexRangeExpression.swift | 7 + .../Disk Persistence/SortOrder.swift | 33 ++-- .../Transaction/Transaction.swift | 147 +++++++++++------- 4 files changed, 110 insertions(+), 79 deletions(-) diff --git a/Package.swift b/Package.swift index d08920d..d771236 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift index f83de54..6ecf213 100644 --- a/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift +++ b/Sources/CodableDatastore/Indexes/IndexRangeExpression.swift @@ -180,6 +180,13 @@ public struct IndexRange: IndexRangeExpression { self.upperBoundExpression = upperBoundExpression self.order = order } + + /// An index range spanning a single value. + public init(only value: Bound) { + self.lowerBoundExpression = .including(value) + self.upperBoundExpression = .including(value) + self.order = .ascending + } } infix operator ..> diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift index 36e965d..1e9a866 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/SortOrder.swift @@ -10,6 +10,13 @@ enum SortOrder { case ascending case equal case descending + + init(_ order: RangeOrder) { + switch order { + case .ascending: self = .ascending + case .descending: self = .descending + } + } } extension Comparable { @@ -22,25 +29,13 @@ extension Comparable { extension RangeBoundExpression { func sortOrder(comparedTo rhs: Bound, order: RangeOrder) -> SortOrder { - switch order { - case .ascending: - switch self { - case .extent: - return .ascending - case .excluding(let bound): - return bound < rhs ? .ascending : .descending - case .including(let bound): - return bound <= rhs ? .ascending : .descending - } - case .descending: - switch self { - case .extent: - return .descending - case .excluding(let bound): - return bound <= rhs ? .ascending : .descending - case .including(let bound): - return bound < rhs ? .ascending : .descending - } + switch (order, self) { + case (.ascending, .extent): .ascending + case (.ascending, .excluding(let bound)): bound < rhs ? .ascending : .descending + case (.ascending, .including(let bound)): bound <= rhs ? .ascending : .descending + case (.descending, .extent): .descending + case (.descending, .excluding(let bound)): rhs < bound ? .descending : .ascending + case (.descending, .including(let bound)): rhs <= bound ? .descending : .ascending } } } diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift index cd203e4..7f1a441 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift @@ -626,8 +626,9 @@ private func primaryIndexBoundComparator(lhs: (bound: guard rhs.headers.count == 2 else { throw DiskPersistenceError.invalidEntryFormat } - let identifierBytes = rhs.headers[1] + if case .extent = lhs.bound { return SortOrder(lhs.order) } + let identifierBytes = rhs.headers[1] let entryIdentifier = try JSONDecoder.shared.decode(IdentifierType.self, from: Data(identifierBytes)) return lhs.bound.sortOrder(comparedTo: entryIdentifier, order: lhs.order) @@ -637,8 +638,9 @@ private func directIndexBoundComparator(lhs: (bound: Range guard rhs.headers.count == 3 else { throw DiskPersistenceError.invalidEntryFormat } - let indexBytes = rhs.headers[1] + if case .extent = lhs.bound { return SortOrder(lhs.order) } + let indexBytes = rhs.headers[1] let indexedValue = try JSONDecoder.shared.decode(IndexType.self, from: Data(indexBytes)) return lhs.bound.sortOrder(comparedTo: indexedValue, order: lhs.order) @@ -648,8 +650,9 @@ private func secondaryIndexBoundComparator(lhs: (bound: Ra guard rhs.headers.count == 1 else { throw DiskPersistenceError.invalidEntryFormat } - let indexBytes = rhs.headers[0] + if case .extent = lhs.bound { return SortOrder(lhs.order) } + let indexBytes = rhs.headers[0] let indexedValue = try JSONDecoder.shared.decode(IndexType.self, from: Data(indexBytes)) return lhs.bound.sortOrder(comparedTo: indexedValue, order: lhs.order) @@ -670,22 +673,23 @@ extension DiskPersistence.Transaction { switch range.order { case .ascending: - let startCursor: DiskPersistence.InsertionCursor - if range.lowerBoundExpression == .extent { - startCursor = await index.firstInsertionCursor + let startCursor = if range.lowerBoundExpression == .extent { + await index.firstInsertionCursor } else { - startCursor = try await index.insertionCursor( - for: (range.lowerBoundExpression, range.order), + try await index.insertionCursor( + for: (range.lowerBoundExpression, .ascending), comparator: primaryIndexBoundComparator ) } try await index.forwardScanEntries(after: startCursor) { entry in - guard case .descending = try primaryIndexBoundComparator( - lhs: (bound: range.upperBoundExpression, order: .descending), - rhs: entry - ) - else { return false } + if range.upperBoundExpression != .extent { + guard case .descending = try primaryIndexBoundComparator( + lhs: (bound: range.upperBoundExpression, order: .descending), + rhs: entry + ) + else { return false } + } let versionData = Data(entry.headers[0]) let instanceData = Data(entry.content) @@ -694,22 +698,23 @@ extension DiskPersistence.Transaction { return true } case .descending: - let startCursor: DiskPersistence.InsertionCursor - if range.upperBoundExpression == .extent { - startCursor = try await index.lastInsertionCursor + let startCursor = if range.upperBoundExpression == .extent { + try await index.lastInsertionCursor } else { - startCursor = try await index.insertionCursor( - for: (range.upperBoundExpression, range.order), + try await index.insertionCursor( + for: (range.upperBoundExpression, .descending), comparator: primaryIndexBoundComparator ) } try await index.backwardScanEntries(before: startCursor) { entry in - guard case .ascending = try primaryIndexBoundComparator( - lhs: (bound: range.lowerBoundExpression, order: .ascending), - rhs: entry - ) - else { return false } + if range.lowerBoundExpression != .extent { + guard case .ascending = try primaryIndexBoundComparator( + lhs: (bound: range.lowerBoundExpression, order: .ascending), + rhs: entry + ) + else { return false } + } let versionData = Data(entry.headers[0]) let instanceData = Data(entry.content) @@ -736,17 +741,23 @@ extension DiskPersistence.Transaction { switch range.order { case .ascending: - let startCursor = try await index.insertionCursor( - for: (range.lowerBoundExpression, range.order), - comparator: directIndexBoundComparator - ) + let startCursor = if range.lowerBoundExpression == .extent { + await index.firstInsertionCursor + } else { + try await index.insertionCursor( + for: (range.lowerBoundExpression, .ascending), + comparator: directIndexBoundComparator + ) + } try await index.forwardScanEntries(after: startCursor) { entry in - guard case .descending = try directIndexBoundComparator( - lhs: (bound: range.upperBoundExpression, order: .descending), - rhs: entry - ) - else { return false } + if range.upperBoundExpression != .extent { + guard case .descending = try directIndexBoundComparator( + lhs: (bound: range.upperBoundExpression, order: .descending), + rhs: entry + ) + else { return false } + } let versionData = Data(entry.headers[0]) let instanceData = Data(entry.content) @@ -755,17 +766,23 @@ extension DiskPersistence.Transaction { return true } case .descending: - let startCursor = try await index.insertionCursor( - for: (range.upperBoundExpression, range.order), - comparator: directIndexBoundComparator - ) + let startCursor = if range.upperBoundExpression == .extent { + try await index.lastInsertionCursor + } else { + try await index.insertionCursor( + for: (range.upperBoundExpression, .descending), + comparator: directIndexBoundComparator + ) + } try await index.backwardScanEntries(before: startCursor) { entry in - guard case .ascending = try directIndexBoundComparator( - lhs: (bound: range.lowerBoundExpression, order: .ascending), - rhs: entry - ) - else { return false } + if range.lowerBoundExpression != .extent { + guard case .ascending = try directIndexBoundComparator( + lhs: (bound: range.lowerBoundExpression, order: .ascending), + rhs: entry + ) + else { return false } + } let versionData = Data(entry.headers[0]) let instanceData = Data(entry.content) @@ -792,34 +809,46 @@ extension DiskPersistence.Transaction { switch range.order { case .ascending: - let startCursor = try await index.insertionCursor( - for: (range.lowerBoundExpression, range.order), - comparator: secondaryIndexBoundComparator - ) + let startCursor = if range.lowerBoundExpression == .extent { + await index.firstInsertionCursor + } else { + try await index.insertionCursor( + for: (range.lowerBoundExpression, .ascending), + comparator: secondaryIndexBoundComparator + ) + } try await index.forwardScanEntries(after: startCursor) { entry in - guard case .descending = try secondaryIndexBoundComparator( - lhs: (bound: range.upperBoundExpression, order: .descending), - rhs: entry - ) - else { return false } + if range.upperBoundExpression != .extent { + guard case .descending = try secondaryIndexBoundComparator( + lhs: (bound: range.upperBoundExpression, order: .descending), + rhs: entry + ) + else { return false } + } let entryIdentifier = try JSONDecoder.shared.decode(IdentifierType.self, from: Data(entry.content)) try await identifierConsumer(entryIdentifier) return true } case .descending: - let startCursor = try await index.insertionCursor( - for: (range.upperBoundExpression, range.order), - comparator: secondaryIndexBoundComparator - ) + let startCursor = if range.upperBoundExpression == .extent { + try await index.lastInsertionCursor + } else { + try await index.insertionCursor( + for: (range.upperBoundExpression, .descending), + comparator: secondaryIndexBoundComparator + ) + } try await index.backwardScanEntries(before: startCursor) { entry in - guard case .ascending = try secondaryIndexBoundComparator( - lhs: (bound: range.lowerBoundExpression, order: .ascending), - rhs: entry - ) - else { return false } + if range.lowerBoundExpression != .extent { + guard case .ascending = try secondaryIndexBoundComparator( + lhs: (bound: range.lowerBoundExpression, order: .ascending), + rhs: entry + ) + else { return false } + } let entryIdentifier = try JSONDecoder.shared.decode(IdentifierType.self, from: Data(entry.content)) try await identifierConsumer(entryIdentifier)