[#663] Create a benchmarking section on the Demo App that can be automated (#681)

- SDKMetrics singleton for the measurement
- download, validate and scan blocks supported
- Sample app refactored to use SDKMetrics, connected to appropriate notifications and labels
- enhancement metric added
- total sync time metric added
- sample app extended to show summary time and operations summaries
- unit tests for the SDKMetrics
- doc for the SDKMetrics and our approach for the performance testing in general
- changelog update
This commit is contained in:
Lukas Korba 2022-12-21 13:30:05 +01:00 committed by GitHub
parent be3ff4299d
commit 3c3b97ca42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 897 additions and 160 deletions

View File

@ -1,3 +1,8 @@
# Unreleased
- [#663] Foundations for the benchmarking/performance testing in the SDK.
This change presents 2 building blocks for the future automated tests, consisting
of a new SDKMetrics interface to control flow of the data in the SDK and
new performance (unit) test measuring synchronization of 100 mainnet blocks.
# 0.17.4-beta
- [#665] Fix testShieldFunds() `get_transparent_balance` error
updates `libzcashlc` to `0.1.1` to fix an error where getting a

View File

@ -140,8 +140,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "fad9802b907822d5a1877584c91f3786824521b7",
"version" : "0.1.0"
"revision" : "b6013b8b313523b2c72ce62dbdc17f6ffad3a500",
"version" : "0.1.1"
}
}
],

View File

@ -959,16 +959,16 @@
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="246" verticalCompressionResistancePriority="250" ambiguous="YES" alignment="center" spacing="13" translatesAutoresizingMaskIntoConstraints="NO" id="IIl-kO-2M8">
<rect key="frame" x="0.0" y="79" width="374" height="102"/>
<rect key="frame" x="0.0" y="79" width="374" height="114"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" text="Status:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3t3-Tr-UlI">
<rect key="frame" x="0.0" y="32" width="95.666666666666671" height="38.333333333333343"/>
<rect key="frame" x="0.0" y="38" width="95.666666666666671" height="38.333333333333343"/>
<fontDescription key="fontDescription" type="system" pointSize="32"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jj9-7r-s2Y">
<rect key="frame" x="108.66666666666666" y="37.333333333333343" width="265.33333333333337" height="27.666666666666671"/>
<rect key="frame" x="108.66666666666666" y="43.333333333333343" width="265.33333333333337" height="27.666666666666671"/>
<fontDescription key="fontDescription" type="italicSystem" pointSize="23"/>
<color key="textColor" systemColor="scrollViewTexturedBackgroundColor"/>
<nil key="highlightedColor"/>
@ -979,22 +979,22 @@
</constraints>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Progress" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JPx-ol-2nc">
<rect key="frame" x="0.0" y="205" width="366" height="43"/>
<rect key="frame" x="0.0" y="217" width="366" height="43"/>
<fontDescription key="fontDescription" type="system" pointSize="36"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" ambiguous="YES" progressViewStyle="bar" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oKg-9s-8Ym">
<rect key="frame" x="0.0" y="272" width="366" height="2.6666666666666856"/>
<rect key="frame" x="0.0" y="284" width="366" height="2.6666666666666856"/>
</progressView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="0%" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kXE-5z-HN2">
<rect key="frame" x="0.0" y="297.66666666666669" width="374" height="39.666666666666686"/>
<rect key="frame" x="0.0" y="309.66666666666669" width="374" height="39.666666666666686"/>
<fontDescription key="fontDescription" type="system" pointSize="33"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G5M-gm-1ux">
<rect key="frame" x="0.0" y="361.33333333333331" width="374" height="45"/>
<rect key="frame" x="0.0" y="373.33333333333331" width="374" height="45"/>
<fontDescription key="fontDescription" type="system" pointSize="27"/>
<state key="normal" title="Start"/>
<connections>
@ -1002,14 +1002,14 @@
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XK2-GA-Ufv">
<rect key="frame" x="0.0" y="430.33333333333337" width="374" height="20.333333333333314"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<rect key="frame" x="0.0" y="442.33333333333337" width="374" height="14.333333333333314"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ROk-70-c41">
<rect key="frame" x="0.0" y="474.66666666666663" width="374" height="20.333333333333314"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<rect key="frame" x="0.0" y="480.66666666666663" width="374" height="14.333333333333314"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>

View File

@ -19,10 +19,20 @@ class SyncBlocksViewController: UIViewController {
@IBOutlet weak var startPause: UIButton!
@IBOutlet weak var metricLabel: UILabel!
@IBOutlet weak var summaryLabel: UILabel!
var lastMetric = PassthroughSubject<SDKMetrics.BlockMetricReport, Never>()
private var queue = DispatchQueue(label: "metrics.queue", qos: .default)
private var accumulatedMetrics: ProcessorMetrics = .initial
private var currentMetric: SDKMetrics.Operation?
private var currentMetricName: String {
guard let currentMetric else { return "" }
switch currentMetric {
case .downloadBlocks: return "download: "
case .validateBlocks: return "validate: "
case .scanBlocks: return "scan: "
case .enhancement: return "enhancement: "
case .fetchUTXOs: return "fetchUTXOs: "
}
}
let synchronizer = AppDelegate.shared.sharedSynchronizer
@ -65,27 +75,47 @@ class SyncBlocksViewController: UIViewController {
.store(in: &notificationCancellables)
}
self.lastMetric
.throttle(for: 5, scheduler: DispatchQueue.main, latest: true)
.receive(on: DispatchQueue.main)
.sink { report in
// handle report
self.metricLabel.text = report.debugDescription
NotificationCenter.default.publisher(for: .blockProcessorStartedDownloading, object: nil)
.sink { [weak self] _ in
self?.currentMetric = .downloadBlocks
}
.store(in: &notificationCancellables)
NotificationCenter.default.publisher(for: SDKMetrics.notificationName)
.receive(on: queue)
.compactMap { SDKMetrics.blockReportFromNotification($0) }
.map { [weak self] report in
guard let self = self else { return report }
NotificationCenter.default.publisher(for: .blockProcessorStartedValidating, object: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.accumulateMetrics()
self?.summaryLabel.text = "download: \((self?.accumulatedMetrics.debugDescription ?? "No summary"))"
self?.accumulatedMetrics = .initial
self?.currentMetric = .validateBlocks
}
.store(in: &notificationCancellables)
self.accumulatedMetrics = .accummulate(self.accumulatedMetrics, current: report)
NotificationCenter.default.publisher(for: .blockProcessorStartedScanning, object: nil)
.sink { [weak self] _ in
self?.currentMetric = .scanBlocks
}
.store(in: &notificationCancellables)
return report
NotificationCenter.default.publisher(for: .blockProcessorStartedEnhancing, object: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.accumulateMetrics()
self?.summaryLabel.text = "scan: \((self?.accumulatedMetrics.debugDescription ?? "No summary"))"
self?.accumulatedMetrics = .initial
self?.currentMetric = .enhancement
}
.store(in: &notificationCancellables)
NotificationCenter.default.publisher(for: .blockProcessorUpdated, object: nil)
.throttle(for: 5, scheduler: DispatchQueue.main, latest: true)
.receive(on: DispatchQueue.main)
.map { [weak self] _ -> SDKMetrics.BlockMetricReport? in
guard let currentMetric = self?.currentMetric else { return nil }
return SDKMetrics.shared.popBlock(operation: currentMetric)?.last
}
.sink { [weak self] report in
self?.lastMetric.send(report)
self?.metricLabel.text = (self?.currentMetricName ?? "") + report.debugDescription
}
.store(in: &notificationCancellables)
@ -93,7 +123,9 @@ class SyncBlocksViewController: UIViewController {
.receive(on: DispatchQueue.main)
.delay(for: 0.5, scheduler: DispatchQueue.main)
.sink { [weak self] _ in
self?.summaryLabel.text = self?.accumulatedMetrics.debugDescription ?? "No summary"
self?.accumulateMetrics()
self?.summaryLabel.text = "enhancement: \((self?.accumulatedMetrics.debugDescription ?? "No summary"))"
self?.overallSummary()
}
.store(in: &notificationCancellables)
}
@ -117,6 +149,37 @@ class SyncBlocksViewController: UIViewController {
return
}
}
func accumulateMetrics() {
guard let currentMetric else { return }
if let reports = SDKMetrics.shared.popBlock(operation: currentMetric) {
for report in reports {
accumulatedMetrics = .accumulate(accumulatedMetrics, current: report)
}
}
}
func overallSummary() {
let cumulativeSummary = SDKMetrics.shared.cumulativeSummary()
let downloadedBlocksReport = cumulativeSummary.downloadedBlocksReport ?? SDKMetrics.ReportSummary.zero
let validatedBlocksReport = cumulativeSummary.validatedBlocksReport ?? SDKMetrics.ReportSummary.zero
let scannedBlocksReport = cumulativeSummary.scannedBlocksReport ?? SDKMetrics.ReportSummary.zero
let enhancementReport = cumulativeSummary.enhancementReport ?? SDKMetrics.ReportSummary.zero
let fetchUTXOsReport = cumulativeSummary.fetchUTXOsReport ?? SDKMetrics.ReportSummary.zero
let totalSyncReport = cumulativeSummary.totalSyncReport ?? SDKMetrics.ReportSummary.zero
metricLabel.text =
"""
Summary:
downloadedBlocks: min: \(downloadedBlocksReport.minTime) max: \(downloadedBlocksReport.maxTime) avg: \(downloadedBlocksReport.avgTime)
validatedBlocks: min: \(validatedBlocksReport.minTime) max: \(validatedBlocksReport.maxTime) avg: \(validatedBlocksReport.avgTime)
scannedBlocks: min: \(scannedBlocksReport.minTime) max: \(scannedBlocksReport.maxTime) avg: \(scannedBlocksReport.avgTime)
enhancement: min: \(enhancementReport.minTime) max: \(enhancementReport.maxTime) avg: \(enhancementReport.avgTime)
fetchUTXOs: min: \(fetchUTXOsReport.minTime) max: \(fetchUTXOsReport.maxTime) avg: \(fetchUTXOsReport.avgTime)
totalSync: min: \(totalSyncReport.minTime) max: \(totalSyncReport.maxTime) avg: \(totalSyncReport.avgTime)
"""
}
@IBAction func startStop() {
Task { @MainActor in
@ -132,6 +195,7 @@ class SyncBlocksViewController: UIViewController {
_ = try synchronizer.prepare(with: DemoAppConfig.seed)
}
SDKMetrics.shared.enableMetrics()
try synchronizer.start()
updateUI()
} catch {
@ -140,6 +204,7 @@ class SyncBlocksViewController: UIViewController {
}
default:
synchronizer.stop()
SDKMetrics.shared.disableMetrics()
updateUI()
}
@ -223,11 +288,11 @@ struct ProcessorMetrics {
var maxHeight: BlockHeight
var maxDuration: (TimeInterval, CompactBlockRange)
var minDuration: (TimeInterval, CompactBlockRange)
var cummulativeDuration: TimeInterval
var cumulativeDuration: TimeInterval
var measuredCount: Int
var averageDuration: TimeInterval {
measuredCount > 0 ? cummulativeDuration / Double(measuredCount) : 0
measuredCount > 0 ? cumulativeDuration / Double(measuredCount) : 0
}
static let initial = Self.init(
@ -235,11 +300,11 @@ struct ProcessorMetrics {
maxHeight: .min,
maxDuration: (TimeInterval.leastNonzeroMagnitude, 0 ... 1),
minDuration: (TimeInterval.greatestFiniteMagnitude, 0 ... 1),
cummulativeDuration: 0,
cumulativeDuration: 0,
measuredCount: 0
)
static func accummulate(_ prev: ProcessorMetrics, current: SDKMetrics.BlockMetricReport) -> Self {
static func accumulate(_ prev: ProcessorMetrics, current: SDKMetrics.BlockMetricReport) -> Self {
.init(
minHeight: min(prev.minHeight, current.startHeight),
maxHeight: max(prev.maxHeight, current.progressHeight),
@ -253,7 +318,7 @@ struct ProcessorMetrics {
(current.duration, current.progressHeight - current.batchSize ... current.progressHeight),
min
),
cummulativeDuration: prev.cummulativeDuration + current.duration,
cumulativeDuration: prev.cumulativeDuration + current.duration,
measuredCount: prev.measuredCount + 1
)
}
@ -274,12 +339,13 @@ extension ProcessorMetrics: CustomDebugStringConvertible {
minHeight: \(self.minHeight)
maxHeight: \(self.maxHeight)
avg scan time: \(self.averageDuration)
slowest scanned range:
avg time: \(self.averageDuration)
overall time: \(self.cumulativeDuration)
slowest range:
range: \(self.maxDuration.1.description)
count: \(self.maxDuration.1.count)
seconds: \(self.maxDuration.0)
Fastest scanned range:
Fastest range:
range: \(self.minDuration.1.description)
count: \(self.minDuration.1.count)
seconds: \(self.minDuration.0)
@ -296,7 +362,7 @@ extension CompactBlockRange {
extension SDKMetrics.BlockMetricReport: CustomDebugStringConvertible {
public var debugDescription: String {
"""
BlockMetric: Scan
BlockMetric:
startHeight: \(self.progressHeight - self.batchSize)
endHeight: \(self.progressHeight)
batchSize: \(self.batchSize)

View File

@ -16,7 +16,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.8.0"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"),
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", from: "0.1.1")
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", from: "0.1.1"),
],
targets: [
.target(
@ -60,6 +60,10 @@ let package = Package(
.testTarget(
name: "DarksideTests",
dependencies: ["ZcashLightClientKit", "TestUtils"]
),
.testTarget(
name: "PerformanceTests",
dependencies: ["ZcashLightClientKit", "TestUtils"]
)
]
)

View File

@ -22,6 +22,7 @@ extension CompactBlockProcessor {
var targetHeightInternal: BlockHeight?
do {
var startTime = Date()
targetHeightInternal = targetHeight
if targetHeight == nil {
targetHeightInternal = try await service.latestBlockHeightAsync()
@ -38,22 +39,53 @@ extension CompactBlockProcessor {
endHeight: latestHeight
)
var counter = 0
var lastDownloadedBlockHeight = -1
for try await zcashCompactBlock in stream {
try Task.checkCancellation()
buffer.append(zcashCompactBlock)
if buffer.count >= blockBufferSize {
try await storage.write(blocks: buffer)
await blocksBufferWritten(buffer)
buffer.removeAll(keepingCapacity: true)
}
counter += 1
lastDownloadedBlockHeight = zcashCompactBlock.height
let progress = BlockProgress(
startHeight: startHeight,
targetHeight: latestHeight,
progressHeight: zcashCompactBlock.height
progressHeight: lastDownloadedBlockHeight
)
notifyProgress(.download(progress))
if buffer.count >= blockBufferSize {
let finishTime = Date()
try await storage.write(blocks: buffer)
await blocksBufferWritten(buffer)
buffer.removeAll(keepingCapacity: true)
SDKMetrics.shared.pushProgressReport(
progress: progress,
start: startTime,
end: finishTime,
batchSize: Int(blockBufferSize),
operation: .downloadBlocks
)
counter = 0
startTime = finishTime
}
}
if counter > 0 {
SDKMetrics.shared.pushProgressReport(
progress: BlockProgress(
startHeight: startHeight,
targetHeight: latestHeight,
progressHeight: lastDownloadedBlockHeight
),
start: startTime,
end: Date(),
batchSize: Int(blockBufferSize),
operation: .downloadBlocks
)
}
try await storage.write(blocks: buffer)
await blocksBufferWritten(buffer)
buffer.removeAll(keepingCapacity: true)

View File

@ -64,6 +64,7 @@ extension CompactBlockProcessor {
// fetch transactions
do {
let startTime = Date()
guard let transactions = try transactionRepository.findTransactions(in: blockRange, limit: Int.max), !transactions.isEmpty else {
await internalSyncProgress.set(range.upperBound, .latestEnhancedHeight)
LoggerProxy.debug("no transactions detected on range: \(blockRange.printRange)")
@ -100,6 +101,18 @@ extension CompactBlockProcessor {
}
}
}
SDKMetrics.shared.pushProgressReport(
progress: BlockProgress(
startHeight: range.lowerBound,
targetHeight: range.upperBound,
progressHeight: range.upperBound
),
start: startTime,
end: Date(),
batchSize: range.count,
operation: .enhancement
)
} catch {
LoggerProxy.error("error enhancing transactions! \(error)")
throw error

View File

@ -651,7 +651,7 @@ public actor CompactBlockProcessor {
try await handleSaplingParametersIfNeeded()
try await removeCacheDB()
if !Task.isCancelled {
await processBatchFinished(height: anyActionExecuted ? ranges.latestBlockHeight : nil)
}

View File

@ -26,19 +26,19 @@ extension CompactBlockProcessor {
throw error
}
let scanFinishTime = Date()
NotificationSender.default.post(notification:
SDKMetrics.progressReportNotification(
progress: BlockProgress(
startHeight: range.lowerBound,
targetHeight: range.upperBound,
progressHeight: range.upperBound
),
start: scanStartTime,
end: scanFinishTime,
batchSize: Int(batchSize),
task: .scanBlocks
)
SDKMetrics.shared.pushProgressReport(
progress: BlockProgress(
startHeight: range.lowerBound,
targetHeight: range.upperBound,
progressHeight: range.upperBound
),
start: scanStartTime,
end: scanFinishTime,
batchSize: Int(batchSize),
operation: .scanBlocks
)
let seconds = scanFinishTime.timeIntervalSinceReferenceDate - scanStartTime.timeIntervalSinceReferenceDate
LoggerProxy.debug("Scanned \(range.count) blocks in \(seconds) seconds")
} else {
@ -71,14 +71,13 @@ extension CompactBlockProcessor {
if scannedNewBlocks {
let progress = BlockProgress(startHeight: scanStartHeight, targetHeight: targetScanHeight, progressHeight: lastScannedHeight)
notifyProgress(.scan(progress))
NotificationSender.default.post(notification:
SDKMetrics.progressReportNotification(
progress: progress,
start: scanStartTime,
end: scanFinishTime,
batchSize: Int(batchSize),
task: .scanBlocks
)
SDKMetrics.shared.pushProgressReport(
progress: progress,
start: scanStartTime,
end: scanFinishTime,
batchSize: Int(batchSize),
operation: .scanBlocks
)
let heightCount = lastScannedHeight - previousScannedHeight
@ -128,84 +127,3 @@ extension CompactBlockProcessor {
}
}
}
public enum SDKMetrics {
public struct BlockMetricReport {
public var startHeight: BlockHeight
public var progressHeight: BlockHeight
public var targetHeight: BlockHeight
public var batchSize: Int
public var duration: TimeInterval
public var task: TaskReported
}
public enum TaskReported: String {
case scanBlocks
}
public static let startBlockHeightKey = "SDKMetrics.startBlockHeightKey"
public static let targetBlockHeightKey = "SDKMetrics.targetBlockHeightKey"
public static let progressHeightKey = "SDKMetrics.progressHeight"
public static let batchSizeKey = "SDKMetrics.batchSize"
public static let startDateKey = "SDKMetrics.startDateKey"
public static let endDateKey = "SDKMetrics.endDateKey"
public static let taskReportedKey = "SDKMetrics.taskReported"
public static let notificationName = Notification.Name("SDKMetrics.Notification")
public static func blockReportFromNotification(_ notification: Notification) -> BlockMetricReport? {
guard
notification.name == notificationName,
let info = notification.userInfo,
let startHeight = info[startBlockHeightKey] as? BlockHeight,
let progressHeight = info[progressHeightKey] as? BlockHeight,
let targetHeight = info[targetBlockHeightKey] as? BlockHeight,
let batchSize = info[batchSizeKey] as? Int,
let task = info[taskReportedKey] as? TaskReported,
let startDate = info[startDateKey] as? Date,
let endDate = info[endDateKey] as? Date
else {
return nil
}
return BlockMetricReport(
startHeight: startHeight,
progressHeight: progressHeight,
targetHeight: targetHeight,
batchSize: batchSize,
duration: abs(
startDate.timeIntervalSinceReferenceDate - endDate.timeIntervalSinceReferenceDate
),
task: task
)
}
static func progressReportNotification(
progress: BlockProgress,
start: Date,
end: Date,
batchSize: Int,
task: SDKMetrics.TaskReported
) -> Notification {
var notification = Notification(name: notificationName)
notification.userInfo = [
startBlockHeightKey: progress.startHeight,
targetBlockHeightKey: progress.targetHeight,
progressHeightKey: progress.progressHeight,
startDateKey: start,
endDateKey: end,
batchSizeKey: batchSize,
taskReportedKey: task
]
return notification
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: SDKMetrics.BlockMetricReport) {
let literal = "\(value.task) - \(abs(value.startHeight - value.targetHeight)) processed on \(value.duration) seconds"
appendLiteral(literal)
}
}

View File

@ -19,8 +19,18 @@ extension CompactBlockProcessor {
state = .validating
let startTime = Date()
let result = rustBackend.validateCombinedChain(dbCache: config.cacheDb, dbData: config.dataDb, networkType: config.network.networkType)
let finishTime = Date()
SDKMetrics.shared.pushProgressReport(
progress: BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0),
start: startTime,
end: finishTime,
batchSize: 0,
operation: .validateBlocks
)
do {
switch result {
case 0:

View File

@ -43,6 +43,7 @@ extension CompactBlockProcessor {
var refreshed: [UnspentTransactionOutputEntity] = []
var skipped: [UnspentTransactionOutputEntity] = []
let startTime = Date()
for utxo in utxos {
do {
try rustBackend.putUnspentTransparentOutput(
@ -62,6 +63,18 @@ extension CompactBlockProcessor {
}
}
SDKMetrics.shared.pushProgressReport(
progress: BlockProgress(
startHeight: range.lowerBound,
targetHeight: range.upperBound,
progressHeight: range.upperBound
),
start: startTime,
end: Date(),
batchSize: range.count,
operation: .fetchUTXOs
)
let result = (inserted: refreshed, skipped: skipped)
NotificationSender.default.post(

View File

@ -0,0 +1,270 @@
//
// SDKMetrics.swift
//
//
// Created by Lukáš Korba on 13.12.2022.
//
import Foundation
/// SDK's tool for the measurement of metrics.
/// The barebone API of the `SDKMetrics` is all about turning it on/off, pushing new reports in and poping RAW data out.
/// The processing of data is either left to the user of `SDKMetrics` or anybody can take an advantage of extension APIs
/// providing useful structs and reports.
///
/// Usage:
/// The `SDKMetrics` API has been designed so it has the lowest impact possible on the SDK itself.
/// Reporting of the metrics is already in place but ignored until `enableMetrics()` is called. Once turned on, the data is collected
/// and cumulated to the in memory structural storage until `disableMetrics()` is called.
/// `disableMetrics()` also clears out the in memory storage.
///
/// To collect data and process it there are 2 ways:
///
/// 1.
/// Get RAW data by calling either `popBlock` or `popAllBlockReports`. The post-processing of data is then delegated to the caller.
///
/// 2.
/// Get cumulated data by using an extension APIs. For the summarized collection, call `cumulativeSummary()`.
/// Sometimes, typically when you want to run several iterations, the `cumulateReportsAndStartNewSet()` automatically computes
/// cumulativeSummary, stores it and starts to collect a new set. All summaries can be either processed by a caller,
/// accessing the collection `cumulativeSummaries` directly or values can be merged into one final summary by calling `summarizedCumulativeReports()`.
///
/// We encourage you to check`SDKMetricsTests` and other tests in the Test/PerformanceTests/ folder.
public class SDKMetrics {
public static let shared = SDKMetrics()
public struct BlockMetricReport: Equatable {
public let startHeight: BlockHeight
public let progressHeight: BlockHeight
public let targetHeight: BlockHeight
public let batchSize: Int
public let startTime: TimeInterval
public let endTime: TimeInterval
public var duration: TimeInterval { endTime - startTime }
}
public enum Operation {
case downloadBlocks
case validateBlocks
case scanBlocks
case enhancement
case fetchUTXOs
}
public struct SyncReport: Equatable {
public let startTime: TimeInterval
public let endTime: TimeInterval
public var duration: TimeInterval { endTime - startTime }
}
public var cumulativeSummaries: [CumulativeSummary] = []
public var syncReport: SyncReport?
var isEnabled = false
var reports: [Operation : [BlockMetricReport]] = [:]
/// `SDKMetrics` is disabled by default. Any pushed data are simply ignored until `enableMetrics()` is called.
public func enableMetrics() {
isEnabled = true
}
public func disableMetrics() {
isEnabled = false
clearAll()
}
/// `SDKMetrics` focuses deeply on sync process and metrics related to it. By default there are reports around
/// block operations like download, validate, etc. This method pushes data on a stack for the specific operation.
func pushProgressReport(
progress: BlockProgress,
start: Date,
end: Date,
batchSize: Int,
operation: Operation
) {
guard isEnabled else { return }
let blockMetricReport = BlockMetricReport(
startHeight: progress.startHeight,
progressHeight: progress.progressHeight,
targetHeight: progress.targetHeight,
batchSize: batchSize,
startTime: start.timeIntervalSinceReferenceDate,
endTime: end.timeIntervalSinceReferenceDate
)
guard reports[operation] != nil else {
reports[operation] = [blockMetricReport]
return
}
reports[operation]?.append(blockMetricReport)
}
/// Block synchronisation consists of operations but the whole process is measured also, represented by
/// different struct `SyncReport`, missing specifics for the operations like batch size, etc.
/// Used for the total syncing time report in the first place.
func pushSyncReport(
start: Date,
end: Date
) {
guard isEnabled else { return }
let syncReport = SyncReport(
startTime: start.timeIntervalSinceReferenceDate,
endTime: end.timeIntervalSinceReferenceDate
)
self.syncReport = syncReport
}
/// A method allowing users of the `SDKMetrics` to pop the RAW data out of the system. For the specific `operation`
/// with option to either leave data in the storage or flushing it out and start the next batch of collecting new ones.
public func popBlock(operation: Operation, flush: Bool = false) -> [BlockMetricReport]? {
defer {
if flush { clearReport(operation) }
}
return reports[operation]
}
/// A method allowing users of the `SDKMetrics` to pop the RAW data out of the system. This time for all measured operations
/// with option to either leave data in the storage or flushing it out and start the next batch of collecting new ones.
public func popAllBlockReports(flush: Bool = false) -> [Operation : [BlockMetricReport]] {
defer {
if flush { clearAllBlockReports() }
}
return reports
}
func clearReport(_ operation: Operation) {
reports.removeValue(forKey: operation)
}
func clearAllBlockReports() {
reports.removeAll()
cumulativeSummaries.removeAll()
}
func clearAll() {
clearAllBlockReports()
syncReport = nil
}
}
/// This extension provides an API that provides the summary and accumulated reports.
/// The RAW data can pulled out and be processed without this extension but we
/// wanted to provide a way how to get essential summaries right from the SDK.
extension SDKMetrics {
public struct CumulativeSummary: Equatable {
public let downloadedBlocksReport: ReportSummary?
public let validatedBlocksReport: ReportSummary?
public let scannedBlocksReport: ReportSummary?
public let enhancementReport: ReportSummary?
public let fetchUTXOsReport: ReportSummary?
public let totalSyncReport: ReportSummary?
}
public struct ReportSummary: Equatable {
public let minTime: TimeInterval
public let maxTime: TimeInterval
public let avgTime: TimeInterval
public static let zero = Self(minTime: 0, maxTime: 0, avgTime: 0)
}
/// This method takes all the RAW data and computes a `CumulativeSummary` for every `operation`
/// independently. A `ReportSummary` is the result per `operation`, providing min, max and avg times.
public func cumulativeSummary() -> CumulativeSummary {
let downloadReport = summaryFor(reports: reports[.downloadBlocks])
let validateReport = summaryFor(reports: reports[.validateBlocks])
let scanReport = summaryFor(reports: reports[.scanBlocks])
let enhancementReport = summaryFor(reports: reports[.enhancement])
let fetchUTXOsReport = summaryFor(reports: reports[.fetchUTXOs])
var totalSyncReport: ReportSummary?
if let duration = SDKMetrics.shared.syncReport?.duration {
totalSyncReport = ReportSummary(minTime: duration, maxTime: duration, avgTime: duration)
}
return CumulativeSummary(
downloadedBlocksReport: downloadReport,
validatedBlocksReport: validateReport,
scannedBlocksReport: scanReport,
enhancementReport: enhancementReport,
fetchUTXOsReport: fetchUTXOsReport,
totalSyncReport: totalSyncReport
)
}
/// This method computes the `CumulativeSummary` for the RAW data already in the system, stores it
/// and leave room for collecting new RAW data. Typical use case is when some code is expected to run several times
/// and every run is expected to be a new data collection.
/// Usage of this API is then typically followed by calling `summarizedCumulativeReports()` which merges all stored
/// cumulative reports into one final report.
public func cumulateReportsAndStartNewSet() {
cumulativeSummaries.append(cumulativeSummary())
reports.removeAll()
syncReport = nil
}
/// This method takes all `CumulativeSummary` reports and merge them all together, providing
/// final `CumulativeSummary` per `operation`, ensuring right min and max values are in the place
/// as well as computes final avg time per `operation`.
public func summarizedCumulativeReports() -> CumulativeSummary? {
var finalSummary: CumulativeSummary?
cumulativeSummaries.forEach { summary in
finalSummary = CumulativeSummary(
downloadedBlocksReport: accumulate(left: finalSummary?.downloadedBlocksReport, right: summary.downloadedBlocksReport),
validatedBlocksReport: accumulate(left: finalSummary?.validatedBlocksReport, right: summary.validatedBlocksReport),
scannedBlocksReport: accumulate(left: finalSummary?.scannedBlocksReport, right: summary.scannedBlocksReport),
enhancementReport: accumulate(left: finalSummary?.enhancementReport, right: summary.enhancementReport),
fetchUTXOsReport: accumulate(left: finalSummary?.fetchUTXOsReport, right: summary.fetchUTXOsReport),
totalSyncReport: accumulate(left: finalSummary?.totalSyncReport, right: summary.totalSyncReport)
)
}
return finalSummary
}
/// Internal helper method that accumulates `ReportSummary` times.
func accumulate(left: ReportSummary?, right: ReportSummary?) -> ReportSummary? {
guard let left, let right else {
if let right {
return ReportSummary(
minTime: right.minTime,
maxTime: right.maxTime,
avgTime: right.avgTime
)
}
return nil
}
return ReportSummary(
minTime: min(left.minTime, right.minTime),
maxTime: max(left.maxTime, right.maxTime),
avgTime: (left.avgTime + right.avgTime) * 0.5
)
}
/// Internal helper method that computes min, max and avg times for the `BlockMetricReport` collection.
func summaryFor(reports: [BlockMetricReport]?) -> ReportSummary? {
guard let reports, !reports.isEmpty else { return nil }
var min: TimeInterval = 99999999.0
var max: TimeInterval = 0.0
var avg: TimeInterval = 0.0
reports.forEach { report in
let duration = report.duration
avg += duration
if duration > max { max = duration }
if duration < min { min = duration }
}
// reports.count is guarded to never be a zero
avg /= TimeInterval(reports.count)
return ReportSummary(minTime: min, maxTime: max, avgTime: avg)
}
}

View File

@ -88,6 +88,19 @@ public extension BlockProgress {
static let nullProgress = BlockProgress(startHeight: 0, targetHeight: 0, progressHeight: 0)
}
protocol LatestBlockHeightProvider {
func latestBlockHeight(streamer: CompactTxStreamerNIOClient?) throws -> BlockHeight
}
class LiveLatestBlockHeightProvider: LatestBlockHeightProvider {
func latestBlockHeight(streamer: CompactTxStreamerNIOClient?) throws -> BlockHeight {
guard let height = try? streamer?.getLatestBlock(ChainSpec()).response.wait().compactBlockHeight() else {
throw LightWalletServiceError.timeOut
}
return height
}
}
public class LightWalletGRPCService {
let channel: Channel
let connectionManager: ConnectionStatusManager
@ -95,6 +108,7 @@ public class LightWalletGRPCService {
let compactTxStreamerAsync: CompactTxStreamerAsyncClient
let singleCallTimeout: TimeLimit
let streamingCallTimeout: TimeLimit
var latestBlockHeightProvider: LatestBlockHeightProvider = LiveLatestBlockHeightProvider()
var queue: DispatchQueue
@ -197,10 +211,7 @@ extension LightWalletGRPCService: LightWalletService {
}
public func latestBlockHeight() throws -> BlockHeight {
guard let height = try? compactTxStreamer.getLatestBlock(ChainSpec()).response.wait().compactBlockHeight() else {
throw LightWalletServiceError.timeOut
}
return height
try latestBlockHeightProvider.latestBlockHeight(streamer: compactTxStreamer)
}
public func latestBlockHeightAsync() async throws -> BlockHeight {

View File

@ -119,6 +119,8 @@ public class SDKSynchronizer: Synchronizer {
private let statusUpdateLock = NSRecursiveLock()
private var syncStartDate: Date?
/// Creates an SDKSynchronizer instance
/// - Parameter initializer: a wallet Initializer object
public convenience init(initializer: Initializer) throws {
@ -147,15 +149,15 @@ public class SDKSynchronizer: Synchronizer {
self.transactionRepository = transactionRepository
self.utxoRepository = utxoRepository
self.blockProcessor = blockProcessor
let lastscannedHeight = (try? transactionRepository.lastScannedHeight()) ?? initializer.walletBirthday
self.latestScannedHeight = lastscannedHeight
let lastScannedHeight = (try? transactionRepository.lastScannedHeight()) ?? initializer.walletBirthday
self.latestScannedHeight = lastScannedHeight
self.network = initializer.network
self.lastStateSubject = CurrentValueSubject(
SynchronizerState(
shieldedBalance: .zero,
transparentBalance: .zero,
syncStatus: .unprepared,
latestScannedHeight: lastscannedHeight
latestScannedHeight: lastScannedHeight
)
)
@ -203,6 +205,7 @@ public class SDKSynchronizer: Synchronizer {
]
)
syncStartDate = Date()
await blockProcessor.start(retry: retry)
}
}
@ -460,6 +463,13 @@ public class SDKSynchronizer: Synchronizer {
}
self.refreshPendingTransactions()
self.status = .synced
if let syncStartDate {
SDKMetrics.shared.pushSyncReport(
start: syncStartDate,
end: Date()
)
}
}
@objc func processorTransitionUnknown(_ notification: Notification) {

View File

@ -107,11 +107,11 @@ class BlockScanTests: XCTestCase {
}
@objc func observeBenchmark(_ notification: Notification) {
guard let report = SDKMetrics.blockReportFromNotification(notification) else {
return
}
let reports = SDKMetrics.shared.popAllBlockReports(flush: true)
print("observed benchmark: \(report)")
reports.forEach {
print("observed benchmark: \($0)")
}
}
func testScanValidateDownload() async throws {
@ -119,10 +119,12 @@ class BlockScanTests: XCTestCase {
logger = SampleLogger(logLevel: .debug)
SDKMetrics.shared.enableMetrics()
NotificationCenter.default.addObserver(
self,
selector: #selector(observeBenchmark(_:)),
name: SDKMetrics.notificationName,
name: .blockProcessorUpdated,
object: nil
)
@ -207,5 +209,7 @@ class BlockScanTests: XCTestCase {
XCTFail("Error should have been a timeLimit reached Error - \(error)")
}
}
SDKMetrics.shared.disableMetrics()
}
}

View File

@ -0,0 +1,161 @@
//
// SDKMetricsTests.swift
//
//
// Created by Lukáš Korba on 19.12.2022.
//
import XCTest
@testable import ZcashLightClientKit
final class SDKMetricsTests: XCTestCase {
func testPushDownloadBlocksReport() throws {
SDKMetrics.shared.enableMetrics()
SDKMetrics.shared.pushProgressReport(
progress: BlockProgress(
startHeight: 1_730_000,
targetHeight: 1_730_099,
progressHeight: 1_730_050
),
start: Date(timeIntervalSinceReferenceDate: 0.0),
end: Date(timeIntervalSinceReferenceDate: 1.0),
batchSize: 10,
operation: .downloadBlocks
)
XCTAssertTrue(SDKMetrics.shared.popBlock(operation: .downloadBlocks)?.count == 1)
if let reports = SDKMetrics.shared.reports[.downloadBlocks], let report = reports.first {
XCTAssertEqual(report, SDKMetrics.BlockMetricReport.placeholderA)
} else {
XCTFail()
}
SDKMetrics.shared.disableMetrics()
}
func testPopDownloadBlocksReport() throws {
SDKMetrics.shared.enableMetrics()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA]
if let reports = SDKMetrics.shared.popBlock(operation: .downloadBlocks), let report = reports.first {
XCTAssertEqual(report, SDKMetrics.BlockMetricReport.placeholderA)
} else {
XCTFail()
}
SDKMetrics.shared.disableMetrics()
}
func testCumulativeSummary() throws {
SDKMetrics.shared.enableMetrics()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA]
let summary = SDKMetrics.CumulativeSummary(
downloadedBlocksReport: SDKMetrics.ReportSummary(minTime: 1.0, maxTime: 1.0, avgTime: 1.0),
validatedBlocksReport: nil,
scannedBlocksReport: nil,
enhancementReport: nil,
fetchUTXOsReport: nil,
totalSyncReport: nil
)
XCTAssertEqual(summary, SDKMetrics.shared.cumulativeSummary())
SDKMetrics.shared.disableMetrics()
}
func testCumulateAndStartNewSet() throws {
SDKMetrics.shared.enableMetrics()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA]
SDKMetrics.shared.cumulateReportsAndStartNewSet()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA]
SDKMetrics.shared.cumulateReportsAndStartNewSet()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA]
SDKMetrics.shared.cumulateReportsAndStartNewSet()
XCTAssertTrue(SDKMetrics.shared.cumulativeSummaries.count == 3)
let summary = SDKMetrics.CumulativeSummary(
downloadedBlocksReport: SDKMetrics.ReportSummary(minTime: 1.0, maxTime: 1.0, avgTime: 1.0),
validatedBlocksReport: nil,
scannedBlocksReport: nil,
enhancementReport: nil,
fetchUTXOsReport: nil,
totalSyncReport: nil
)
let summaries = [summary, summary, summary]
XCTAssertEqual(summaries, SDKMetrics.shared.cumulativeSummaries)
SDKMetrics.shared.disableMetrics()
}
func testCumulativeSummaryMinMaxAvg() throws {
SDKMetrics.shared.enableMetrics()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA, SDKMetrics.BlockMetricReport.placeholderB]
let summary = SDKMetrics.CumulativeSummary(
downloadedBlocksReport: SDKMetrics.ReportSummary(minTime: 1.0, maxTime: 6.0, avgTime: 3.5),
validatedBlocksReport: nil,
scannedBlocksReport: nil,
enhancementReport: nil,
fetchUTXOsReport: nil,
totalSyncReport: nil
)
XCTAssertEqual(summary, SDKMetrics.shared.cumulativeSummary())
SDKMetrics.shared.disableMetrics()
}
func testSummarizedCumulativeReports() throws {
SDKMetrics.shared.enableMetrics()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderA]
SDKMetrics.shared.cumulateReportsAndStartNewSet()
SDKMetrics.shared.reports[.downloadBlocks] = [SDKMetrics.BlockMetricReport.placeholderB]
SDKMetrics.shared.cumulateReportsAndStartNewSet()
let summary = SDKMetrics.CumulativeSummary(
downloadedBlocksReport: SDKMetrics.ReportSummary(minTime: 1.0, maxTime: 6.0, avgTime: 3.5),
validatedBlocksReport: nil,
scannedBlocksReport: nil,
enhancementReport: nil,
fetchUTXOsReport: nil,
totalSyncReport: nil
)
XCTAssertEqual(SDKMetrics.shared.summarizedCumulativeReports(), summary)
SDKMetrics.shared.disableMetrics()
}
}
extension SDKMetrics.BlockMetricReport {
static let placeholderA = Self(
startHeight: 1_730_000,
progressHeight: 1_730_050,
targetHeight: 1_730_099,
batchSize: 10,
startTime: Date(timeIntervalSinceReferenceDate: 0.0).timeIntervalSinceReferenceDate,
endTime: Date(timeIntervalSinceReferenceDate: 1.0).timeIntervalSinceReferenceDate
)
static let placeholderB = Self(
startHeight: 1_730_000,
progressHeight: 1_730_080,
targetHeight: 1_730_099,
batchSize: 10,
startTime: Date(timeIntervalSinceReferenceDate: 0.0).timeIntervalSinceReferenceDate,
endTime: Date(timeIntervalSinceReferenceDate: 6.0).timeIntervalSinceReferenceDate
)
}

View File

@ -0,0 +1,104 @@
//
// SynchronizerTests.swift
//
//
// Created by Lukáš Korba on 13.12.2022.
//
import XCTest
@testable import ZcashLightClientKit
@testable import TestUtils
class SynchronizerTests: XCTestCase {
class MockLatestBlockHeightProvider: LatestBlockHeightProvider {
let birthday: BlockHeight
init(birthday: BlockHeight) {
self.birthday = birthday
}
func latestBlockHeight(streamer: ZcashLightClientKit.CompactTxStreamerNIOClient?) throws -> ZcashLightClientKit.BlockHeight {
self.birthday
}
}
var coordinator: TestCoordinator!
// swiftlint:disable:next line_length
let seedPhrase = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame"
var birthday: BlockHeight = 1_730_000
@MainActor
func testHundredBlocksSync() async throws {
let derivationTool = DerivationTool(networkType: .mainnet)
let seedData: Data = Data(base64Encoded: "9VDVOZZZOWWHpZtq1Ebridp3Qeux5C+HwiRR0g7Oi7HgnMs8Gfln83+/Q1NnvClcaSwM4ADFL1uZHxypEWlWXg==")!
let seedBytes = [UInt8](seedData)
let spendingKey = try derivationTool.deriveUnifiedSpendingKey(
seed: seedBytes,
accountIndex: 0
)
let ufvk = try derivationTool.deriveUnifiedFullViewingKey(from: spendingKey)
let network = ZcashNetworkBuilder.network(for: .mainnet)
let endpoint = LightWalletEndpoint(address: "lightwalletd.electriccoin.co", port: 9067, secure: true)
SDKMetrics.shared.enableMetrics()
for _ in 1...5 {
let databases = TemporaryDbBuilder.build()
let initializer = Initializer(
cacheDbURL: databases.cacheDB,
dataDbURL: databases.dataDB,
pendingDbURL: databases.pendingDB,
endpoint: endpoint,
network: network,
spendParamsURL: try __spendParamsURL(),
outputParamsURL: try __outputParamsURL(),
viewingKeys: [ufvk],
walletBirthday: birthday,
alias: "",
loggerProxy: SampleLogger(logLevel: .debug)
)
try? FileManager.default.removeItem(at: databases.cacheDB)
try? FileManager.default.removeItem(at: databases.dataDB)
try? FileManager.default.removeItem(at: databases.pendingDB)
let synchronizer = try SDKSynchronizer(initializer: initializer)
_ = try synchronizer.prepare(with: seedBytes)
let syncSyncedExpectation = XCTestExpectation(description: "synchronizerSynced Expectation")
syncSyncedExpectation.subscribe(to: .synchronizerSynced, object: nil)
let internalSyncProgress = InternalSyncProgress(storage: UserDefaults.standard)
await internalSyncProgress.rewind(to: birthday)
await (synchronizer.blockProcessor.service as? LightWalletGRPCService)?.latestBlockHeightProvider = MockLatestBlockHeightProvider(birthday: self.birthday + 99)
try synchronizer.start()
wait(for: [syncSyncedExpectation], timeout: 100)
SDKMetrics.shared.cumulateReportsAndStartNewSet()
}
if let cumulativeSummary = SDKMetrics.shared.summarizedCumulativeReports() {
let downloadedBlocksReport = cumulativeSummary.downloadedBlocksReport ?? .zero
let validatedBlocksReport = cumulativeSummary.validatedBlocksReport ?? .zero
let scannedBlocksReport = cumulativeSummary.scannedBlocksReport ?? .zero
let enhancementReport = cumulativeSummary.enhancementReport ?? .zero
let fetchUTXOsReport = cumulativeSummary.fetchUTXOsReport ?? .zero
let totalSyncReport = cumulativeSummary.totalSyncReport ?? .zero
LoggerProxy.debug("""
testHundredBlocksSync() SUMMARY min max avg REPORT:
downloadedBlocksTimes: min: \(downloadedBlocksReport.minTime) max: \(downloadedBlocksReport.maxTime) avg: \(downloadedBlocksReport.avgTime)
validatedBlocksTimes: min: \(validatedBlocksReport.minTime) max: \(validatedBlocksReport.maxTime) avg: \(validatedBlocksReport.avgTime)
scannedBlocksTimes: min: \(scannedBlocksReport.minTime) max: \(scannedBlocksReport.maxTime) avg: \(scannedBlocksReport.avgTime)
enhancementTimes: min: \(enhancementReport.minTime) max: \(enhancementReport.maxTime) avg: \(enhancementReport.avgTime)
fetchUTXOsTimes: min: \(fetchUTXOsReport.minTime) max: \(fetchUTXOsReport.maxTime) avg: \(fetchUTXOsReport.avgTime)
totalSyncTimes: min: \(totalSyncReport.minTime) max: \(totalSyncReport.maxTime) avg: \(totalSyncReport.avgTime)
""")
}
SDKMetrics.shared.disableMetrics()
}
}

116
docs/testing/Performance.md Normal file
View File

@ -0,0 +1,116 @@
# Performance Testing
### Intro
Benchmarking the SDK is one of the key focuses of `SDKMetrics` API. The most important operation of the SDK is the ability to synchronize blocks in order to see transactions, right balances and to send & receive funds. In short, be in sync with the mainnet blockchain. Synchronization consists of several sub-operations like downloading the blocks, validating & scanning, etc.
### Benchmarking the Synchronization
The initial work on benchmarking/performance testing allows us to properly measure times spent in sub-operations of a synchronization. There will be iterations and enhancements and ideally fully automated solution one day but for now we rely on manual approach.
### SDKMetrics
`SDKMetrics` is the public interface on the SDK side allowing users to get the synchronization related metrics. It's a shared singleton and you can access it via
```swift
SDKMetrics.shared
```
By default the gathering of the data is turned of so anything reported inside the SDK is simply ignored. You need to call
```swift
SDKMetrics.shared.enableMetrics()
```
in order to allow `SDKMetrics` to actually collect the RAW data. There's a counterpart API for the enablement, called
```swift
SDKMetrics.shared.disableMetrics()
```
This method turns the `SDKMetrics` off from collecting and also flushes all the so far collected RAW data.
Like we said, the SDK is automatically reporting sub-operation metrics. The reports are collected and held in memory, split by the operation:
```swift
enum Operation {
case downloadBlocks // download of the blocks
case validateBlocks // validation of the downloaded blocks
case scanBlocks // scanning of the downloaded blocks
case enhancement // enhancement of the transactions
case fetchUTXOs // fetching the UTXOs
}
```
Every report is represented by a struct:
```swift
struct BlockMetricReport: Equatable {
startHeight: BlockHeight // start of the range to be processed
progressHeight: BlockHeight // latest processed height
targetHeight: BlockHeight // end of the range to be processed
batchSize: Int // size of the batch to be processed
startTime: TimeInterval // when the operation started
endTime: TimeInterval // when the operation finished
duration: TimeInterval // computed property to provide duration
}
```
`SDKMetrics` holds the reports in a dictionary where keys are the operations. You can receive the data via either of the following methods.
```swift
// Get all reports for the specific Operation
SDKMetrics.shared.popBlock(operation: Operation, flush: Bool = false) -> [BlockMetricReport]?
// Get the whole dictionary of collected data
SDKMetrics.shared.popAllBlockReports(flush: Bool = false) -> [Operation : [BlockMetricReport]]
```
Notice `flush` to be set to false by default. Collection the data leaves it in memory but you can clear it out of the memory by setting `flush` to `true`. Such option is handy when you plan to start to collect a new set of data.
These two `pop` methods simply returns RAW data from the `SDKMetrics` so post-processing of the data is up to the caller. There are extension methods that help with the accumulation of reports and provide decent post-processing for typical use cases though.
### SDKMetrics extension
Post-processing the array of reports per `Operation` or more general a dictionary of all arrays of reports may be time consuming, especially when you need to know just the times `Operation`s have taken. For this specific needs we introduce `CumulativeSummary` struct:
```swift
struct CumulativeSummary: Equatable {
downloadedBlocksReport: ReportSummary?
validatedBlocksReport: ReportSummary?
scannedBlocksReport: ReportSummary?
enhancementReport: ReportSummary?
fetchUTXOsReport: ReportSummary?
totalSyncReport: ReportSummary?
```
where `ReportSummary` represents:
```swift
struct ReportSummary: Equatable {
minTime: TimeInterval
maxTime: TimeInterval
avgTime: TimeInterval
```
As you can see `CumulativeSummary` basically holds `min`, `max` and `avg` times per `Operation`. To generate such summary you call:
```swift
// Get the cumulative summary of the collected data
SDKMetrics.shared.cumulativeSummary() -> CumulativeSummary
```
There is a specific use case we want to mention, imagine you want to run a performance test that calls synchronization several times and you want to measure all runs separately and process runs afterwards. You either call `cumulativeSummary()` and re-enable the metrics to flush the data out and post-process collected summaries on your own or you can take advantage of 2 more methods available:
```swift
// Generates `CumulativeSummary` and stores it in memory & clears out data
SDKMetrics.shared.cumulateReportsAndStartNewSet()
// Merges all cumulativeSummaries into one
SDKMetrics.shared.summarizedCumulativeReports() -> CumulativeSummary?
```
So typical use case of these methods is sketched in the following pseudo code
```swift
SDKMetrics.shared.enableMetrics()
for run in 1...X {
// ensure fresh start of synchronization
// synchronize
// collect data for this run and start new set
SDKMetrics.shared.cumulateReportsAndStartNewSet()
}
// collect final data as a merge of all runs
let finalSummary = SDKMetrics.shared.summarizedCumulativeReports()
SDKMetrics.shared.disableMetrics()
```
Printed out example of the `finalSummary`:
```
downloadedBlocksTimes: min: 0.002303004264831543 max: 0.9062199592590332 avg: 0.14520481750369074
validatedBlocksTimes: min: 0.01760399341583252 max: 0.019036054611206055 avg: 0.0178409144282341
scannedBlocksTimes: min: 0.045277953147888184 max: 0.5136369466781616 avg: 0.2530662305653095
enhancementTimes: min: 0.0 max: 0.0 avg: 0.0
fetchUTXOsTimes: min: 1.9073486328125e-06 max: 2.09808349609375e-05 avg: 3.166496753692627e-06
totalSyncTimes: min: 7.222689986228943 max: 10.718868017196655 avg: 8.997062936425209
```
### Performance tests
We encourage you to visit `Tests/PerformanceTests/` folder and check `SynchronizerTests` where we do exactly what is mentioned in this doc. We run synchronization for specific range of 100 blocks 5 times, measure every run separately and merge results together in the end. The `SDKMetrics` and `SynchronizerTests` lay down foundations for the future automatization of performance testing.