serverpod-offline-sync/benchmark/utils/results.dart

290 lines
9 KiB
Dart
Raw Permalink Normal View History

// Pretty-printing benchmark comparison output.
import 'benchmark.dart';
import 'conversion.dart';
import 'space.dart';
void printPerformanceImpact(
BenchmarkResults results, {
required int rowCount,
bool runningInCI = false,
}) {
print(
'${runningInCI ? '```' : ''}'
'\n📊 ${results.operation.label.toUpperCase()} performance impact:',
);
final baselineTime = results.baseline;
final crdtTime = results.offlineSync;
final runDelayUs = crdtTime - baselineTime;
final slowdown = runDelayUs / baselineTime * 100;
final baselineDelay = baselineTime.toFormattedDuration();
final crdtDelay = crdtTime.toFormattedDuration();
final runDelay = runDelayUs.toFormattedDuration();
final runDelayPerOperation = (runDelayUs / rowCount).toFormattedDuration();
print(' Time: $baselineDelay --> $crdtDelay ($runDelay)');
print(' CRDT overhead: ${formatter2.format(slowdown)}% slower');
if (results.operation != Operation.select) {
print(' Delay per ${results.operation.label}: $runDelayPerOperation');
}
if (runningInCI) print('```');
}
void printStorageImpact(
StorageBenchmarkResults benchmarkResults, {
required int rowCount,
bool runningInCI = false,
}) {
final stages = [
_StorageStageComparison(
title: 'INSERT only',
baselineSize: benchmarkResults.baseline.sizeFor(StorageStage.insert),
crdtSize: benchmarkResults.offlineSync.sizeFor(StorageStage.insert),
baselineIncrement: benchmarkResults.baseline.sizeFor(StorageStage.insert),
crdtIncrement: benchmarkResults.offlineSync.sizeFor(StorageStage.insert),
unitCount: rowCount,
unitLabel: 'inserted row',
),
_StorageStageComparison(
title: 'INSERT + UPDATE',
baselineSize: benchmarkResults.baseline.sizeFor(StorageStage.update),
crdtSize: benchmarkResults.offlineSync.sizeFor(StorageStage.update),
baselineIncrement:
benchmarkResults.baseline.sizeFor(StorageStage.update) -
benchmarkResults.baseline.previousSizeFor(StorageStage.update),
crdtIncrement:
benchmarkResults.offlineSync.sizeFor(StorageStage.update) -
benchmarkResults.offlineSync.previousSizeFor(StorageStage.update),
unitCount: rowCount * TypesTableBenchmark.updatedColumnsPerRow,
unitLabel: 'updated column',
),
_StorageStageComparison(
title: 'INSERT + UPDATE + DELETE',
baselineSize: benchmarkResults.baseline.sizeFor(StorageStage.delete),
crdtSize: benchmarkResults.offlineSync.sizeFor(StorageStage.delete),
baselineIncrement:
benchmarkResults.baseline.sizeFor(StorageStage.delete) -
benchmarkResults.baseline.previousSizeFor(StorageStage.delete),
crdtIncrement:
benchmarkResults.offlineSync.sizeFor(StorageStage.delete) -
benchmarkResults.offlineSync.previousSizeFor(StorageStage.delete),
unitCount: rowCount,
unitLabel: 'deleted row',
),
];
print('${runningInCI ? '```' : ''}\n💽 Storage impact (base footprint removed):');
print(
' Base database size: '
'${benchmarkResults.baseline.baseDatabaseSize.toFormattedStorageSize()} '
'--> ${benchmarkResults.offlineSync.baseDatabaseSize.toFormattedStorageSize()}',
);
for (final stage in stages) {
print(' ${stage.title}:');
print(
' Net storage: ${stage.baselineSize.toFormattedStorageSize()} '
'--> ${stage.crdtSize.toFormattedStorageSize()} '
'(${_formatSignedStorage(stage.extraBytes)})',
);
print(
' CRDT overhead: ${_formatPercentageIncrease(stage.overheadPercent)}',
);
print(
' Extra storage per ${stage.unitLabel}: '
'${_formatSignedStorage(stage.incrementalExtraBytesPerUnit)}',
);
}
print(
' Storage overhead range: '
'${_formatPercentageValue(stages.first.overheadPercent)}% '
'--> ${_formatPercentageValue(stages.last.overheadPercent)}%',
);
if (runningInCI) print('```');
}
void printMergeImpact(
MergeBenchmarkResults results, {
bool runningInCI = false,
}) {
final timePerChangeUs = results.average / results.changeCount;
final changesPerSecond = Duration.microsecondsPerSecond / timePerChangeUs;
final queriesPerChange = results.averageQueries / results.changeCount;
print(
'${runningInCI ? '```' : ''}'
'\n🔀 MERGE ${results.title} performance:',
);
final batchDescription = results.batchDescription;
if (batchDescription != null) {
print(' Batch: $batchDescription');
}
print(' Merge time: ${results.average.toFormattedDuration()}');
print(' Time per change: ${timePerChangeUs.toFormattedDuration()}');
print(' Throughput: ${formatter0.format(changesPerSecond)} changes/s');
print(
' Queries: ${formatter0.format(results.averageQueries)} per batch '
'(${formatter2.format(queriesPerChange)} per change)',
);
2026-09-14 10:26:56 -03:00
print(' Rows returned: ${formatter2.format(results.averageRowsRead)} per batch');
final types = results.averageRowsReadByType.keys.toList()..sort();
print(
' Rows by type: ${[
for (final type in types) '$type: ${formatter2.format(results.averageRowsReadByType[type])}',
].join(', ')}',
);
if (runningInCI) print('```');
}
void printSpaceImpact(
SpaceBenchmarkResults results, {
required int rowCount,
bool runningInCI = false,
}) {
String compare(
double baseline,
double spaceScoped,
double unscoped,
) {
String overhead(double value) =>
'${formatter2.format((value - baseline) / baseline * 100)}%';
return '${baseline.toFormattedDuration()} --> '
'${spaceScoped.toFormattedDuration()} space-scoped (+${overhead(spaceScoped)}) / '
'${unscoped.toFormattedDuration()} unscoped (+${overhead(unscoped)})';
}
print(
'${runningInCI ? '```' : ''}'
'\n🔭 SELECT space impact '
'(${formatter0.format(results.noiseUsers)} extra users, '
'${formatter0.format(results.noiseCrdtRows)} CRDT noise rows):',
);
print(
' find all (${formatter0.format(rowCount)} rows): '
'${compare(results.baseline.findAllMicros, results.spaceScoped.findAllMicros, results.unscoped.findAllMicros)}',
);
print(
' findById (per lookup): '
'${compare(results.baseline.findByIdMicros, results.spaceScoped.findByIdMicros, results.unscoped.findByIdMicros)}',
);
if (runningInCI) print('```');
}
class BenchmarkResults {
const BenchmarkResults({
required this.operation,
required this.baseline,
required this.offlineSync,
});
final Operation operation;
final double baseline;
final double offlineSync;
}
class SpaceBenchmarkResults {
const SpaceBenchmarkResults({
required this.baseline,
required this.spaceScoped,
required this.unscoped,
required this.noiseUsers,
required this.noiseCrdtRows,
});
final SpaceMeasurement baseline;
final SpaceMeasurement spaceScoped;
final SpaceMeasurement unscoped;
final int noiseUsers;
final int noiseCrdtRows;
}
class MergeBenchmarkResults {
const MergeBenchmarkResults({
required this.title,
required this.batchDescription,
required this.average,
required this.averageQueries,
2026-09-14 10:26:56 -03:00
required this.averageRowsRead,
required this.averageRowsReadByType,
required this.changeCount,
});
final String title;
final String? batchDescription;
/// Average microseconds to merge one batch of [changeCount] changes.
final double average;
/// Average number of queries issued while merging one batch.
final double averageQueries;
2026-09-14 10:26:56 -03:00
/// Top-level rows returned by reads during one merge batch, including repeats.
/// These are result counts, not physical database page reads or scanned rows.
final double averageRowsRead;
final Map<String, double> averageRowsReadByType;
final int changeCount;
}
class StorageBenchmarkResults {
const StorageBenchmarkResults({
required this.baseline,
required this.offlineSync,
});
final StorageBenchmarkRun baseline;
final StorageBenchmarkRun offlineSync;
}
class _StorageStageComparison {
const _StorageStageComparison({
required this.title,
required this.baselineSize,
required this.crdtSize,
required this.baselineIncrement,
required this.crdtIncrement,
required this.unitCount,
required this.unitLabel,
});
final String title;
final int baselineSize;
final int crdtSize;
final int baselineIncrement;
final int crdtIncrement;
final int unitCount;
final String unitLabel;
int get extraBytes => crdtSize - baselineSize;
double? get overheadPercent =>
baselineSize == 0 ? null : extraBytes / baselineSize * 100;
double get incrementalExtraBytesPerUnit =>
(crdtIncrement - baselineIncrement) / unitCount;
}
String _formatSignedStorage(num size) {
if (size == 0) {
return '0 B';
}
final prefix = size > 0 ? '+' : '-';
return '$prefix${size.abs().toFormattedStorageSize()}';
}
String _formatPercentageIncrease(double? percent) {
if (percent == null) {
return 'n/a (baseline storage is zero)';
}
return '${formatter2.format(percent)}% increase';
}
String _formatPercentageValue(double? percent) {
if (percent == null) {
return 'n/a';
}
return formatter2.format(percent);
}