From 063eeb8d1253990e4ff9e53793fbafd07b4bae6e Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 12 May 2025 20:18:14 -0400 Subject: [PATCH] stats --- .../lib/dht_support/src/dht_record/stats.dart | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 packages/veilid_support/lib/dht_support/src/dht_record/stats.dart diff --git a/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart b/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart new file mode 100644 index 0000000..6388f5c --- /dev/null +++ b/packages/veilid_support/lib/dht_support/src/dht_record/stats.dart @@ -0,0 +1,175 @@ +import 'package:collection/collection.dart'; +import 'package:indent/indent.dart'; + +import '../../../veilid_support.dart'; + +const maxLatencySamples = 100; +const timeoutDuration = 10; + +extension LatencyStatsExt on LatencyStats { + String debugString() => 'fast($fastest)/avg($average)/slow($slowest)/' + 'tm90($tm90)/tm75($tm75)/p90($p90)/p75($p75)'; +} + +class LatencyStatsAccounting { + LatencyStatsAccounting({required this.maxSamples}); + + LatencyStats record(TimestampDuration dur) { + _samples.add(dur); + if (_samples.length > maxSamples) { + _samples.removeAt(0); + } + + final sortedList = _samples.sorted(); + + final fastest = sortedList.first; + final slowest = sortedList.last; + final average = TimestampDuration( + value: sortedList.fold(BigInt.zero, (acc, x) => acc + x.value) ~/ + BigInt.from(sortedList.length)); + + final tm90len = (sortedList.length * 90 + 99) ~/ 100; + final tm75len = (sortedList.length * 75 + 99) ~/ 100; + final tm90 = TimestampDuration( + value: sortedList + .sublist(0, tm90len) + .fold(BigInt.zero, (acc, x) => acc + x.value) ~/ + BigInt.from(tm90len)); + final tm75 = TimestampDuration( + value: sortedList + .sublist(0, tm75len) + .fold(BigInt.zero, (acc, x) => acc + x.value) ~/ + BigInt.from(tm90len)); + final p90 = sortedList[tm90len - 1]; + final p75 = sortedList[tm75len - 1]; + + final ls = LatencyStats( + fastest: fastest, + slowest: slowest, + average: average, + tm90: tm90, + tm75: tm75, + p90: p90, + p75: p75); + + return ls; + } + + ///////////////////////////// + final int maxSamples; + final _samples = []; +} + +class DHTCallStats { + void record(TimestampDuration dur, Exception? exc) { + final wasTimeout = + exc is VeilidAPIExceptionTimeout || dur.toSecs() >= timeoutDuration; + + calls++; + if (wasTimeout) { + timeouts++; + } else { + successLatency = successLatencyAcct.record(dur); + } + latency = latencyAcct.record(dur); + } + + String debugString() => + ' timeouts/calls: $timeouts/$calls (${(timeouts * 100 / calls).toStringAsFixed(3)}%)\n' + 'success latency: ${successLatency?.debugString()}\n' + ' all latency: ${latency?.debugString()}\n'; + + ///////////////////////////// + int calls = 0; + int timeouts = 0; + LatencyStats? latency; + LatencyStats? successLatency; + final latencyAcct = LatencyStatsAccounting(maxSamples: maxLatencySamples); + final successLatencyAcct = + LatencyStatsAccounting(maxSamples: maxLatencySamples); +} + +class DHTPerKeyStats { + DHTPerKeyStats(this.debugName); + + void record(String func, TimestampDuration dur, Exception? exc) { + final keyFuncStats = _perFuncStats.putIfAbsent(func, DHTCallStats.new); + + _stats.record(dur, exc); + keyFuncStats.record(dur, exc); + } + + String debugString() { + // + final out = StringBuffer() + ..write('Name: $debugName\n') + ..write(_stats.debugString().indent(4)) + ..writeln('Per-Function:'); + for (final entry in _perFuncStats.entries) { + final funcName = entry.key; + final funcStats = entry.value.debugString().indent(4); + out.write('$funcName:\n$funcStats'.indent(4)); + } + + return out.toString(); + } + + ////////////////////////////// + + final String debugName; + final _stats = DHTCallStats(); + final _perFuncStats = {}; +} + +class DHTStats { + DHTStats(); + + Future measure(TypedKey key, String debugName, String func, + Future Function() closure) async { + // + final start = Veilid.instance.now(); + final keyStats = + _statsPerKey.putIfAbsent(key, () => DHTPerKeyStats(debugName)); + final funcStats = _statsPerFunc.putIfAbsent(func, DHTCallStats.new); + + VeilidAPIException? exc; + + try { + final res = await closure(); + + return res; + } on VeilidAPIException catch (e) { + exc = e; + rethrow; + } finally { + final end = Veilid.instance.now(); + final dur = end.diff(start); + + keyStats.record(func, dur, exc); + funcStats.record(dur, exc); + } + } + + String debugString() { + // + final out = StringBuffer()..writeln('Per-Function:'); + for (final entry in _statsPerFunc.entries) { + final funcName = entry.key; + final funcStats = entry.value.debugString().indent(4); + out.write('$funcName:\n$funcStats\n'.indent(4)); + } + out.writeln('Per-Key:'); + for (final entry in _statsPerKey.entries) { + final keyName = entry.key; + final keyStats = entry.value.debugString().indent(4); + out.write('$keyName:\n$keyStats\n'.indent(4)); + } + + return out.toString(); + } + + ////////////////////////////// + + final _statsPerKey = {}; + final _statsPerFunc = {}; +}