syncova-backup/packages/benchmark/report.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

135 lines
4.5 KiB
Go

package benchmark
import (
"fmt"
"io"
"strings"
"time"
)
// Report fasst mehrere Messläufe zusammen.
type Report struct {
// Title benennt die Messreihe.
Title string `json:"title"`
// Machine ist die Maschine aller Läufe.
Machine MachineProfile `json:"machine"`
// Results sind die einzelnen Läufe.
Results []*Result `json:"results"`
// SkippedScenarios benennen nicht durchgeführte Szenarien samt Grund.
//
// Sie stehen gleichberechtigt neben den Ergebnissen: Ein Messbericht, der
// verschweigt, was er nicht gemessen hat, liest sich vollständig und ist es
// nicht. Dieselbe Regel wie bei den ungeprüften Bereichen im Security
// Center (Phase 15).
SkippedScenarios []SkippedScenario `json:"skipped_scenarios,omitempty"`
// GeneratedAt ist der Zeitpunkt des Berichts in UTC.
GeneratedAt time.Time `json:"generated_at"`
}
// SkippedScenario ist ein Szenario, das nicht gemessen werden konnte.
type SkippedScenario struct {
// Name benennt das Szenario.
Name string `json:"name"`
// Reason erklärt, warum es nicht gemessen wurde.
Reason string `json:"reason"`
}
// AddSkipped vermerkt ein nicht gemessenes Szenario.
func (report *Report) AddSkipped(scenarioName string, reason string) {
report.SkippedScenarios = append(report.SkippedScenarios,
SkippedScenario{Name: scenarioName, Reason: reason})
}
// WriteText schreibt den Bericht in lesbarer Form.
func (report *Report) WriteText(outputWriter io.Writer) {
fmt.Fprintf(outputWriter, "%s\n%s\n\n", report.Title, strings.Repeat("=", len(report.Title)))
fmt.Fprintf(outputWriter, "Maschine: %s/%s, %d Kerne (genutzt: %d), %s\n",
report.Machine.OperatingSystem, report.Machine.Architecture,
report.Machine.LogicalCPUs, report.Machine.GoMaxProcs, report.Machine.GoVersion)
if report.Machine.MemoryLimitBytes > 0 {
fmt.Fprintf(outputWriter, "Speichergrenze: %s\n",
FormatBytes(float64(report.Machine.MemoryLimitBytes)))
}
fmt.Fprintf(outputWriter, "Gemessen: %s\n\n", report.GeneratedAt.Format(time.RFC3339))
for _, result := range report.Results {
writeResultText(outputWriter, result)
}
if len(report.SkippedScenarios) > 0 {
fmt.Fprintf(outputWriter, "\nNicht gemessen\n--------------\n")
for _, skipped := range report.SkippedScenarios {
fmt.Fprintf(outputWriter, " %s\n %s\n", skipped.Name, skipped.Reason)
}
}
}
// writeResultText schreibt einen einzelnen Lauf.
func writeResultText(outputWriter io.Writer, result *Result) {
fmt.Fprintf(outputWriter, "%s\n", result.ScenarioName)
fmt.Fprintf(outputWriter, " %s\n", result.Description)
fmt.Fprintf(outputWriter, " Daten: %s (%s, %d Dateien)\n",
result.Data.Description, FormatBytes(float64(result.Data.TotalBytes)), result.Data.FileCount)
fmt.Fprintf(outputWriter, " Dauer: %.2f s\n", result.Usage.WallClockSeconds)
// Der Durchsatz erscheint nur bei inkompressiblen Daten. Bei
// komprimierbaren misst er die Kompression — und wer die Zahl aus dem
// Bericht zitiert, zitiert dann etwas anderes, als er meint.
if !result.Data.Compressible {
fmt.Fprintf(outputWriter, " Durchsatz: %s/s\n",
FormatBytes(result.ThroughputBytesPerSecond()))
} else {
fmt.Fprintf(outputWriter, " Durchsatz: nicht ausgewiesen (komprimierbare Daten — "+
"die Zahl beschriebe die Kompression)\n")
}
if result.FilesProcessed > 0 {
fmt.Fprintf(outputWriter, " Objekte: %d (%.0f je Sekunde)\n",
result.FilesProcessed, result.FilesPerSecond())
}
if result.BytesStored > 0 {
fmt.Fprintf(outputWriter, " Abgelegt: %s\n", FormatBytes(float64(result.BytesStored)))
}
fmt.Fprintf(outputWriter, " CPU: %.2f s Programm + %.2f s System "+
"(%.1f Kerne ausgelastet)\n",
result.Usage.UserCPUSeconds, result.Usage.SystemCPUSeconds, result.Usage.CPUEfficiency())
fmt.Fprintf(outputWriter, " Speicher: %s Höchststand, %s angefordert, %d Bereinigungen\n",
FormatBytes(float64(result.Usage.PeakMemoryBytes)),
FormatBytes(float64(result.Usage.AllocatedBytes)),
result.Usage.GarbageCollections)
for _, note := range result.Notes {
fmt.Fprintf(outputWriter, " Hinweis: %s\n", note)
}
fmt.Fprintln(outputWriter)
}
// FormatBytes stellt eine Datenmenge lesbar dar.
func FormatBytes(byteCount float64) string {
const unitStep = 1024.0
if byteCount < unitStep {
return fmt.Sprintf("%.0f B", byteCount)
}
unitNames := []string{"KiB", "MiB", "GiB", "TiB"}
scaledValue := byteCount / unitStep
for _, unitName := range unitNames {
if scaledValue < unitStep {
return fmt.Sprintf("%.1f %s", scaledValue, unitName)
}
scaledValue /= unitStep
}
return fmt.Sprintf("%.1f PiB", scaledValue)
}