ZcashLightClientKit/Sources/ZcashLightClientKit/Metrics/SDKMetrics.swift

271 lines
11 KiB
Swift

//
// 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 popping 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)
}
}