Skip to content

Commit 411dfd4

Browse files
authored
Improve perf many state items (#372)
1 parent 2595ce8 commit 411dfd4

File tree

2 files changed

+32
-58
lines changed

2 files changed

+32
-58
lines changed

Blockchain/Sources/Blockchain/State/InMemoryBackend.swift

Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,16 @@ import Utils
66
private let logger = Logger(label: "InMemoryBackend")
77

88
public actor InMemoryBackend: StateBackendProtocol {
9-
public struct KVPair: Comparable, Sendable {
10-
var key: Data
11-
var value: Data
12-
13-
public static func < (lhs: KVPair, rhs: KVPair) -> Bool {
14-
lhs.key.lexicographicallyPrecedes(rhs.key)
15-
}
16-
}
17-
18-
// we really should be using Heap or some other Tree based structure here
19-
// but let's keep it simple for now
20-
public private(set) var store: SortedArray<KVPair> = .init([])
9+
// Use Dictionary for O(1) lookups
10+
private var store: [Data: Data] = [:]
2111
private var rawValues: [Data32: Data] = [:]
2212
public private(set) var refCounts: [Data: Int] = [:]
2313
private var rawValueRefCounts: [Data32: Int] = [:]
2414

2515
public init() {}
2616

2717
public func read(key: Data) async throws -> Data? {
28-
let idx = store.insertIndex(KVPair(key: key, value: Data()))
29-
let item = store.array[safe: idx]
30-
if item?.key == key {
31-
return item?.value
32-
}
33-
return nil
18+
store[key]
3419
}
3520

3621
public func readAll(prefix: Data, startKey: Data?, limit: UInt32?) async throws -> [(key: Data, value: Data)] {
@@ -41,33 +26,25 @@ public actor InMemoryBackend: StateBackendProtocol {
4126
}
4227

4328
let startKey = startKey ?? prefix
44-
let startIndex = store.insertIndex(KVPair(key: startKey, value: Data()))
45-
for i in startIndex ..< store.array.count {
46-
let item = store.array[i]
47-
if item.key.starts(with: prefix) {
48-
resp.append((item.key, item.value))
49-
} else {
50-
break
51-
}
52-
if let limit, resp.count == limit {
53-
break
54-
}
29+
30+
// Filter and sort entries
31+
let filtered = store
32+
.filter { $0.key.starts(with: prefix) && !$0.key.lexicographicallyPrecedes(startKey) }
33+
.sorted { $0.key.lexicographicallyPrecedes($1.key) }
34+
35+
// Apply limit if specified
36+
if let limit {
37+
return Array(filtered.prefix(Int(limit)))
5538
}
56-
return resp
39+
40+
return filtered
5741
}
5842

5943
public func batchUpdate(_ updates: [StateBackendOperation]) async throws {
6044
for update in updates {
6145
switch update {
6246
case let .write(key, value):
63-
let idx = store.insertIndex(KVPair(key: key, value: value))
64-
let item = store.array[safe: idx]
65-
if let item, item.key == key { // found
66-
// value is not used for ordering so this is safe
67-
store.unsafeArrayAccess[idx].value = value
68-
} else { // not found
69-
store.insert(KVPair(key: key, value: value))
70-
}
47+
store[key] = value
7148
case let .writeRawValue(key, value):
7249
rawValues[key] = value
7350
rawValueRefCounts[key, default: 0] += 1
@@ -86,11 +63,9 @@ public actor InMemoryBackend: StateBackendProtocol {
8663
public func gc(callback: @Sendable (Data) -> Data32?) async throws {
8764
// check ref counts and remove keys with 0 ref count
8865
for (key, count) in refCounts where count == 0 {
89-
let idx = store.insertIndex(KVPair(key: key, value: Data()))
90-
let item = store.array[safe: idx]
91-
if let item, item.key == key {
92-
store.remove(at: idx)
93-
if let rawValueKey = callback(item.value) {
66+
if let value = store[key] {
67+
store.removeValue(forKey: key)
68+
if let rawValueKey = callback(value) {
9469
rawValueRefCounts[rawValueKey, default: 0] -= 1
9570
if rawValueRefCounts[rawValueKey] == 0 {
9671
rawValues.removeValue(forKey: rawValueKey)
@@ -102,31 +77,30 @@ public actor InMemoryBackend: StateBackendProtocol {
10277
}
10378

10479
public func debugPrint() {
105-
for item in store.array {
106-
let refCount = refCounts[item.key, default: 0]
107-
logger.info("key: \(item.key.toHexString())")
108-
logger.info("value: \(item.value.toHexString())")
80+
for (key, value) in store {
81+
let refCount = refCounts[key, default: 0]
82+
logger.info("key: \(key.toHexString())")
83+
logger.info("value: \(value.toHexString())")
10984
logger.info("ref count: \(refCount)")
11085
}
11186
}
11287

11388
public func createIterator(prefix: Data, startKey: Data?) async throws -> StateBackendIterator {
114-
InMemoryStateIterator(store: store, prefix: prefix, startKey: startKey)
89+
// Create sorted array of matching items
90+
let searchKey = startKey ?? prefix
91+
let matchingItems = store
92+
.filter { $0.key.starts(with: prefix) && !$0.key.lexicographicallyPrecedes(searchKey) }
93+
.sorted { $0.key.lexicographicallyPrecedes($1.key) }
94+
95+
return InMemoryStateIterator(items: matchingItems)
11596
}
11697
}
11798

11899
public final class InMemoryStateIterator: StateBackendIterator, @unchecked Sendable {
119100
private var iterator: Array<(key: Data, value: Data)>.Iterator
120101

121-
init(store: SortedArray<InMemoryBackend.KVPair>, prefix: Data, startKey: Data?) {
122-
let searchKey = startKey ?? prefix
123-
let startIndex = store.insertIndex(InMemoryBackend.KVPair(key: searchKey, value: Data()))
124-
125-
let matchingItems = Array(store.array[startIndex...].prefix { item in
126-
item.key.starts(with: prefix)
127-
}.map { (key: $0.key, value: $0.value) })
128-
129-
iterator = matchingItems.makeIterator()
102+
init(items: [(key: Data, value: Data)]) {
103+
iterator = items.makeIterator()
130104
}
131105

132106
public func next() async throws -> (key: Data, value: Data)? {

JAMTests/Benchmarks/TestVectors/TestVectors.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ let benchmarks: @Sendable () -> Void = {
5252
}
5353

5454
// Traces
55-
let tracePaths = [("traces/fallback", 15), ("traces/safrole", 10), ("traces/storage", 5), ("traces/preimages", 5)]
55+
let tracePaths = [("traces/fallback", 15), ("traces/safrole", 10), ("traces/storage", 5), ("traces/preimages", 5), ("traces/fuzzy", 5)]
5656
for (path, iterations) in tracePaths {
5757
let traces = try! JamTestnet.loadTests(path: path, src: .w3f)
5858
Benchmark(

0 commit comments

Comments
 (0)