diff --git a/Blockchain/Sources/Blockchain/Types/AvailabilitySpecifications.swift b/Blockchain/Sources/Blockchain/Types/AvailabilitySpecifications.swift index a917a067..0013f9c8 100644 --- a/Blockchain/Sources/Blockchain/Types/AvailabilitySpecifications.swift +++ b/Blockchain/Sources/Blockchain/Types/AvailabilitySpecifications.swift @@ -1,3 +1,4 @@ +import Codec import Utils public struct AvailabilitySpecifications: Sendable, Equatable, Codable { @@ -37,3 +38,13 @@ extension AvailabilitySpecifications: Dummy { ) } } + +extension AvailabilitySpecifications: EncodedSize { + public var encodedSize: Int { + workPackageHash.encodedSize + length.encodedSize + erasureRoot.encodedSize + segmentRoot.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Blockchain/Sources/Blockchain/Types/RefinementContext.swift b/Blockchain/Sources/Blockchain/Types/RefinementContext.swift index a185e16f..bdfb4235 100644 --- a/Blockchain/Sources/Blockchain/Types/RefinementContext.swift +++ b/Blockchain/Sources/Blockchain/Types/RefinementContext.swift @@ -1,3 +1,4 @@ +import Codec import Utils // A refinement context, denoted by the set X, describes the context of the chain @@ -68,3 +69,33 @@ extension RefinementContext: Dummy { ) } } + +extension RefinementContext.Anchor: EncodedSize { + public var encodedSize: Int { + headerHash.encodedSize + stateRoot.encodedSize + beefyRoot.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension RefinementContext.LokupAnchor: EncodedSize { + public var encodedSize: Int { + headerHash.encodedSize + timeslot.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension RefinementContext: EncodedSize { + public var encodedSize: Int { + anchor.encodedSize + lokupAnchor.encodedSize + prerequistieWorkPackage.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Blockchain/Sources/Blockchain/Types/State+Merklization.swift b/Blockchain/Sources/Blockchain/Types/State+Merklization.swift index ca3e3c70..2265713a 100644 --- a/Blockchain/Sources/Blockchain/Types/State+Merklization.swift +++ b/Blockchain/Sources/Blockchain/Types/State+Merklization.swift @@ -22,8 +22,7 @@ extension State { } private static func constructKey(_ service: ServiceIndex, _ codeHash: Data32) -> Data32 { - var data = Data() - data.reserveCapacity(32) + var data = Data(capacity: 32) withUnsafeBytes(of: service) { ptr in data.append(ptr.load(as: UInt8.self)) data.append(codeHash.data[0]) @@ -81,10 +80,9 @@ extension State { } private func encode(_ account: ServiceAccount) throws -> Data { - var data = Data() - data.reserveCapacity(32 + 8 * 4 + 4) // codeHash, balance, accumlateGasLimit, onTransferGasLimit, totalByteLength, itemsCount + let capacity = 32 + 8 * 4 + 4 // codeHash, balance, accumlateGasLimit, onTransferGasLimit, totalByteLength, itemsCount - let encoder = JamEncoder(data) + let encoder = JamEncoder(capacity: capacity) try encoder.encode(account.codeHash) try encoder.encode(account.balance) diff --git a/Blockchain/Sources/Blockchain/Types/WorkOutput.swift b/Blockchain/Sources/Blockchain/Types/WorkOutput.swift index 3ddc9db7..ae036ddf 100644 --- a/Blockchain/Sources/Blockchain/Types/WorkOutput.swift +++ b/Blockchain/Sources/Blockchain/Types/WorkOutput.swift @@ -110,3 +110,18 @@ extension WorkOutput: Codable { } } } + +extension WorkOutput: EncodedSize { + public var encodedSize: Int { + switch result { + case let .success(success): + success.encodedSize + 1 + case .failure: + 1 + } + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Blockchain/Sources/Blockchain/Types/WorkReport.swift b/Blockchain/Sources/Blockchain/Types/WorkReport.swift index fd5da72b..dc8ea337 100644 --- a/Blockchain/Sources/Blockchain/Types/WorkReport.swift +++ b/Blockchain/Sources/Blockchain/Types/WorkReport.swift @@ -63,3 +63,14 @@ extension WorkReport { try! JamEncoder.encode(self).blake2b256hash() } } + +extension WorkReport: EncodedSize { + public var encodedSize: Int { + authorizerHash.encodedSize + coreIndex.encodedSize + output.encodedSize + refinementContext.encodedSize + packageSpecification + .encodedSize + results.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Blockchain/Sources/Blockchain/Types/WorkResult.swift b/Blockchain/Sources/Blockchain/Types/WorkResult.swift index 4b9f3cc3..c92773a8 100644 --- a/Blockchain/Sources/Blockchain/Types/WorkResult.swift +++ b/Blockchain/Sources/Blockchain/Types/WorkResult.swift @@ -48,3 +48,13 @@ extension WorkResult: Dummy { ) } } + +extension WorkResult: EncodedSize { + public var encodedSize: Int { + serviceIndex.encodedSize + codeHash.encodedSize + payloadHash.encodedSize + gas.encodedSize + output.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Codec/Sources/Codec/EncodedSize.swift b/Codec/Sources/Codec/EncodedSize.swift new file mode 100644 index 00000000..a5bd1f7c --- /dev/null +++ b/Codec/Sources/Codec/EncodedSize.swift @@ -0,0 +1,111 @@ +import Foundation + +public protocol EncodedSize { + var encodedSize: Int { get } + + static var encodeedSizeHint: Int? { get } +} + +extension FixedWidthInteger { + public var encodedSize: Int { + MemoryLayout.size + } + + public static var encodeedSizeHint: Int? { + MemoryLayout.size + } +} + +extension Bool: EncodedSize { + public var encodedSize: Int { + 1 + } + + public static var encodeedSizeHint: Int? { + 1 + } +} + +extension String: EncodedSize { + public var encodedSize: Int { + utf8.count + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension Data: EncodedSize { + public var encodedSize: Int { + UInt32(count).variableEncodingLength() + count + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension Array: EncodedSize where Element: EncodedSize { + public var encodedSize: Int { + let prefixSize = UInt32(count).variableEncodingLength() + if let hint = Element.encodeedSizeHint { + return prefixSize + hint * count + } + return reduce(into: prefixSize) { $0 += $1.encodedSize } + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension Optional: EncodedSize where Wrapped: EncodedSize { + public var encodedSize: Int { + switch self { + case let .some(wrapped): + wrapped.encodedSize + 1 + case .none: + 1 + } + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension Result: EncodedSize where Success: EncodedSize, Failure: EncodedSize { + public var encodedSize: Int { + switch self { + case let .success(success): + success.encodedSize + 1 + case let .failure(failure): + failure.encodedSize + 1 + } + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension Set: EncodedSize where Element: EncodedSize { + public var encodedSize: Int { + reduce(into: UInt32(count).variableEncodingLength()) { $0 += $1.encodedSize } + } + + public static var encodeedSizeHint: Int? { + nil + } +} + +extension Dictionary: EncodedSize where Key: EncodedSize, Value: EncodedSize { + public var encodedSize: Int { + reduce(into: UInt32(count).variableEncodingLength()) { $0 += $1.key.encodedSize + $1.value.encodedSize } + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Codec/Sources/Codec/JamEncoder.swift b/Codec/Sources/Codec/JamEncoder.swift index d7f21f0c..fe6d562f 100644 --- a/Codec/Sources/Codec/JamEncoder.swift +++ b/Codec/Sources/Codec/JamEncoder.swift @@ -7,12 +7,20 @@ public class JamEncoder { encoder = EncodeContext(data) } + public init(capacity: Int) { + encoder = EncodeContext(Data(capacity: capacity)) + } + public func encode(_ value: some Encodable) throws { try encoder.encode(value) } public static func encode(_ value: some Encodable) throws -> Data { - let encoder = JamEncoder() + let encoder = if let value = value as? EncodedSize { + JamEncoder(capacity: value.encodedSize) + } else { + JamEncoder() + } try encoder.encode(value) return encoder.data } @@ -55,9 +63,6 @@ private class EncodeContext: Encoder { } fileprivate func encodeData(_ value: Data, lengthPrefix: Bool) { - // reserve capacity for the length - // length is variable size but very unlikely to be larger than 4 bytes - data.reserveCapacity(data.count + value.count + (lengthPrefix ? 4 : 0)) if lengthPrefix { let length = UInt32(value.count) data.append(contentsOf: length.encode(method: .variableWidth)) @@ -66,9 +71,6 @@ private class EncodeContext: Encoder { } fileprivate func encodeData(_ value: [UInt8]) { - // reserve capacity for the length - // length is variable size but very unlikely to be larger than 4 bytes - data.reserveCapacity(data.count + value.count + 4) let length = UInt32(value.count) data.append(contentsOf: length.encode(method: .variableWidth)) data.append(contentsOf: value) diff --git a/Codec/Sources/Codec/UnsignedInteger+Codec.swift b/Codec/Sources/Codec/UnsignedInteger+Codec.swift index 7ae9e7fb..a611c235 100644 --- a/Codec/Sources/Codec/UnsignedInteger+Codec.swift +++ b/Codec/Sources/Codec/UnsignedInteger+Codec.swift @@ -77,12 +77,18 @@ extension UnsignedInteger { } public func encode() -> Data { - var data = Data() - data.reserveCapacity(MemoryLayout.size) + var data = Data(capacity: MemoryLayout.size) // use withUnsafeBytes to avoid the overhead of creating a copy of the data withUnsafeBytes(of: self) { bytes in data.append(contentsOf: bytes) } return data } + + public func variableEncodingLength() -> Int { + for l in 1 ..< 9 where self < (1 << (7 * l)) { + return l + } + return 9 + } } diff --git a/Utils/Sources/Utils/ConfigLimitedSizeArray.swift b/Utils/Sources/Utils/ConfigLimitedSizeArray.swift index a3785ab4..b32b89d7 100644 --- a/Utils/Sources/Utils/ConfigLimitedSizeArray.swift +++ b/Utils/Sources/Utils/ConfigLimitedSizeArray.swift @@ -1,3 +1,5 @@ +import Codec + // TODO: add tests // TODO: consider using a circular buffer instead of a regular array to reduce memory usage @@ -235,3 +237,18 @@ extension ConfigLimitedSizeArray: Encodable where T: Encodable { } } } + +extension ConfigLimitedSizeArray: EncodedSize where T: EncodedSize { + public var encodedSize: Int { + if TMinLength.self == TMaxLength.self { + if let hint = T.encodeedSizeHint { + return count * hint + } + } + return array.encodedSize + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Utils/Sources/Utils/ConfigSizeBitString.swift b/Utils/Sources/Utils/ConfigSizeBitString.swift index 6f110aef..f4605185 100644 --- a/Utils/Sources/Utils/ConfigSizeBitString.swift +++ b/Utils/Sources/Utils/ConfigSizeBitString.swift @@ -132,3 +132,13 @@ extension ConfigSizeBitString: FixedLengthData { try self.init(config: config, data: data) } } + +extension ConfigSizeBitString: EncodedSize { + public var encodedSize: Int { + UInt32(length).variableEncodingLength() + bytes.count + } + + public static var encodeedSizeHint: Int? { + nil + } +} diff --git a/Utils/Sources/Utils/Either.swift b/Utils/Sources/Utils/Either.swift index a89f1616..f892c843 100644 --- a/Utils/Sources/Utils/Either.swift +++ b/Utils/Sources/Utils/Either.swift @@ -1,3 +1,5 @@ +import Codec + public enum Either { case left(Left) case right(Right) @@ -84,3 +86,21 @@ extension Either: Codable where Left: Codable, Right: Codable { } } } + +extension Either: EncodedSize where Left: EncodedSize, Right: EncodedSize { + public var encodedSize: Int { + switch self { + case let .left(left): + left.encodedSize + 1 + case let .right(right): + right.encodedSize + 1 + } + } + + public static var encodeedSizeHint: Int? { + if let left = Left.encodeedSizeHint, let right = Right.encodeedSizeHint { + return left + right + 1 + } + return nil + } +} diff --git a/Utils/Sources/Utils/FixedSizeData.swift b/Utils/Sources/Utils/FixedSizeData.swift index f274b88a..ba3e9f05 100644 --- a/Utils/Sources/Utils/FixedSizeData.swift +++ b/Utils/Sources/Utils/FixedSizeData.swift @@ -58,6 +58,16 @@ extension FixedSizeData: FixedLengthData { } } +extension FixedSizeData: EncodedSize { + public var encodedSize: Int { + T.value + } + + public static var encodeedSizeHint: Int? { + T.value + } +} + public typealias Data32 = FixedSizeData public typealias Data48 = FixedSizeData public typealias Data64 = FixedSizeData diff --git a/Utils/Sources/Utils/LimitedSizeArray.swift b/Utils/Sources/Utils/LimitedSizeArray.swift index 675b688e..72816c0e 100644 --- a/Utils/Sources/Utils/LimitedSizeArray.swift +++ b/Utils/Sources/Utils/LimitedSizeArray.swift @@ -158,3 +158,23 @@ extension LimitedSizeArray: Decodable where T: Decodable { } } } + +extension LimitedSizeArray: EncodedSize where T: EncodedSize { + public var encodedSize: Int { + if TMinLength.self == TMaxLength.self { + if let hint = T.encodeedSizeHint { + return count * hint + } + } + return array.encodedSize + } + + public static var encodeedSizeHint: Int? { + if TMinLength.self == TMaxLength.self { + if let hint = T.encodeedSizeHint { + return hint * TMinLength.value + } + } + return nil + } +} diff --git a/Utils/Sources/Utils/Merklization/StateMerklization.swift b/Utils/Sources/Utils/Merklization/StateMerklization.swift index f4e3a155..6cf333e5 100644 --- a/Utils/Sources/Utils/Merklization/StateMerklization.swift +++ b/Utils/Sources/Utils/Merklization/StateMerklization.swift @@ -15,8 +15,7 @@ public func stateMerklize(kv: [Data32: Data], i: Int = 0) throws(MerklizeError) } func embeddedLeaf(key: Data32, value: Data, size: UInt8) -> Data64 { - var data = Data() - data.reserveCapacity(64) + var data = Data(capacity: 64) data.append(0b01 | (size << 2)) data += key.data[..<31] data += value @@ -25,8 +24,7 @@ public func stateMerklize(kv: [Data32: Data], i: Int = 0) throws(MerklizeError) } func regularLeaf(key: Data32, value: Data) -> Data64 { - var data = Data() - data.reserveCapacity(64) + var data = Data(capacity: 64) data.append(0b11) data += key.data[..<31] data += value.blake2b256hash().data