diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/export/snapshot_csv.go | 103 | ||||
| -rw-r--r-- | internal/tui/tui.go | 97 |
2 files changed, 105 insertions, 95 deletions
diff --git a/internal/export/snapshot_csv.go b/internal/export/snapshot_csv.go new file mode 100644 index 0000000..3983b85 --- /dev/null +++ b/internal/export/snapshot_csv.go @@ -0,0 +1,103 @@ +package export + +import ( + "encoding/csv" + "fmt" + "os" + "time" + + "ior/internal/statsengine" +) + +// SnapshotCSV writes a dashboard snapshot to a timestamped CSV file. +func SnapshotCSV(snap *statsengine.Snapshot) (string, error) { + filename := fmt.Sprintf("ior-snapshot-%s.csv", time.Now().Format("20060102-150405")) + f, err := os.Create(filename) + if err != nil { + return "", err + } + defer f.Close() + + w := csv.NewWriter(f) + + rows := [][]string{ + {"section", "name", "value1", "value2", "value3"}, + {"summary", "totals", fmt.Sprint(snapValue(snap, func(s *statsengine.Snapshot) uint64 { return s.TotalSyscalls })), fmt.Sprint(snapValue(snap, func(s *statsengine.Snapshot) uint64 { return s.TotalErrors })), fmt.Sprint(snapValue(snap, func(s *statsengine.Snapshot) uint64 { return s.TotalBytes }))}, + {"summary", "rates_per_sec", fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.SyscallRatePerSec })), fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.ReadBytesPerSec })), fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.WriteBytesPerSec }))}, + {"summary", "latency_gap_mean_ns", fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.LatencyMeanNs })), fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.GapMeanNs })), ""}, + {"summary", "trend", trendSummary(snap, func(s *statsengine.Snapshot) statsengine.Trend { return s.LatencyTrend }), trendSummary(snap, func(s *statsengine.Snapshot) statsengine.Trend { return s.GapTrend }), trendSummary(snap, func(s *statsengine.Snapshot) statsengine.Trend { return s.ThroughputTrend })}, + } + for _, row := range rows { + if err := w.Write(row); err != nil { + return "", err + } + } + + if snap != nil { + for _, s := range snap.Syscalls() { + if err := w.Write([]string{"syscall", s.Name, fmt.Sprint(s.Count), fmt.Sprintf("%.2f", s.RatePerSec), fmt.Sprint(s.Bytes)}); err != nil { + return "", err + } + if err := w.Write([]string{"syscall_latency_ns", s.Name, fmt.Sprintf("%.2f", s.LatencyMeanNs), fmt.Sprint(s.LatencyMinNs), fmt.Sprint(s.LatencyMaxNs)}); err != nil { + return "", err + } + if err := w.Write([]string{"syscall_percentiles_ns", s.Name, fmt.Sprint(s.LatencyP50Ns), fmt.Sprint(s.LatencyP95Ns), fmt.Sprint(s.LatencyP99Ns)}); err != nil { + return "", err + } + } + for _, r := range snap.Files() { + if err := w.Write([]string{"file", r.Path, fmt.Sprint(r.Accesses), fmt.Sprint(r.BytesRead), fmt.Sprint(r.BytesWritten)}); err != nil { + return "", err + } + if err := w.Write([]string{"file_latency_ns", r.Path, fmt.Sprintf("%.2f", r.AvgLatencyNs), fmt.Sprint(r.MaxLatencyNs), ""}); err != nil { + return "", err + } + } + for _, p := range snap.Processes() { + if err := w.Write([]string{"process", fmt.Sprint(p.PID), fmt.Sprint(p.Syscalls), fmt.Sprintf("%.2f", p.RatePerSec), fmt.Sprint(p.Bytes)}); err != nil { + return "", err + } + if err := w.Write([]string{"process_latency_ns", fmt.Sprint(p.PID), fmt.Sprintf("%.2f", p.AvgLatencyNs), "", ""}); err != nil { + return "", err + } + } + for _, b := range snap.LatencyHistogram.Buckets() { + if err := w.Write([]string{"latency_hist", b.Label, fmt.Sprint(b.Count), fmt.Sprint(b.LowerNs), fmt.Sprint(b.UpperNs)}); err != nil { + return "", err + } + } + for _, b := range snap.GapHistogram.Buckets() { + if err := w.Write([]string{"gap_hist", b.Label, fmt.Sprint(b.Count), fmt.Sprint(b.LowerNs), fmt.Sprint(b.UpperNs)}); err != nil { + return "", err + } + } + } + + w.Flush() + if err := w.Error(); err != nil { + return "", err + } + return filename, nil +} + +func snapValue(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) uint64) uint64 { + if snap == nil { + return 0 + } + return get(snap) +} + +func snapValueF(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) float64) float64 { + if snap == nil { + return 0 + } + return get(snap) +} + +func trendSummary(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) statsengine.Trend) string { + if snap == nil { + return "stable:0.00" + } + trend := get(snap) + return fmt.Sprintf("%s:%.2f", trend.Direction, trend.DeltaPercent) +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index aaad69c..fb9229f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -2,15 +2,14 @@ package tui import ( "context" - "encoding/csv" "errors" "fmt" "log" - "os" "strings" "sync" "time" + coreexport "ior/internal/export" "ior/internal/flags" coreflamegraph "ior/internal/flamegraph" "ior/internal/probemanager" @@ -788,7 +787,7 @@ func runExportCmd(exportEnabled bool, option tuiexport.Option, snap *statsengine } switch option { case tuiexport.OptionCSV: - path, err := exportSnapshotCSV(snap) + path, err := coreexport.SnapshotCSV(snap) if err != nil { return tuiexport.FailedMsg{Err: err} } @@ -814,98 +813,6 @@ func (s lateBoundDashboardSource) Snapshot() *statsengine.Snapshot { return source.Snapshot() } -func exportSnapshotCSV(snap *statsengine.Snapshot) (string, error) { - filename := fmt.Sprintf("ior-snapshot-%s.csv", time.Now().Format("20060102-150405")) - f, err := os.Create(filename) - if err != nil { - return "", err - } - defer f.Close() - - w := csv.NewWriter(f) - - rows := [][]string{ - {"section", "name", "value1", "value2", "value3"}, - {"summary", "totals", fmt.Sprint(snapValue(snap, func(s *statsengine.Snapshot) uint64 { return s.TotalSyscalls })), fmt.Sprint(snapValue(snap, func(s *statsengine.Snapshot) uint64 { return s.TotalErrors })), fmt.Sprint(snapValue(snap, func(s *statsengine.Snapshot) uint64 { return s.TotalBytes }))}, - {"summary", "rates_per_sec", fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.SyscallRatePerSec })), fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.ReadBytesPerSec })), fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.WriteBytesPerSec }))}, - {"summary", "latency_gap_mean_ns", fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.LatencyMeanNs })), fmt.Sprintf("%.2f", snapValueF(snap, func(s *statsengine.Snapshot) float64 { return s.GapMeanNs })), ""}, - {"summary", "trend", trendSummary(snap, func(s *statsengine.Snapshot) statsengine.Trend { return s.LatencyTrend }), trendSummary(snap, func(s *statsengine.Snapshot) statsengine.Trend { return s.GapTrend }), trendSummary(snap, func(s *statsengine.Snapshot) statsengine.Trend { return s.ThroughputTrend })}, - } - for _, row := range rows { - if err := w.Write(row); err != nil { - return "", err - } - } - - if snap != nil { - for _, s := range snap.Syscalls() { - if err := w.Write([]string{"syscall", s.Name, fmt.Sprint(s.Count), fmt.Sprintf("%.2f", s.RatePerSec), fmt.Sprint(s.Bytes)}); err != nil { - return "", err - } - if err := w.Write([]string{"syscall_latency_ns", s.Name, fmt.Sprintf("%.2f", s.LatencyMeanNs), fmt.Sprint(s.LatencyMinNs), fmt.Sprint(s.LatencyMaxNs)}); err != nil { - return "", err - } - if err := w.Write([]string{"syscall_percentiles_ns", s.Name, fmt.Sprint(s.LatencyP50Ns), fmt.Sprint(s.LatencyP95Ns), fmt.Sprint(s.LatencyP99Ns)}); err != nil { - return "", err - } - } - for _, r := range snap.Files() { - if err := w.Write([]string{"file", r.Path, fmt.Sprint(r.Accesses), fmt.Sprint(r.BytesRead), fmt.Sprint(r.BytesWritten)}); err != nil { - return "", err - } - if err := w.Write([]string{"file_latency_ns", r.Path, fmt.Sprintf("%.2f", r.AvgLatencyNs), fmt.Sprint(r.MaxLatencyNs), ""}); err != nil { - return "", err - } - } - for _, p := range snap.Processes() { - if err := w.Write([]string{"process", fmt.Sprint(p.PID), fmt.Sprint(p.Syscalls), fmt.Sprintf("%.2f", p.RatePerSec), fmt.Sprint(p.Bytes)}); err != nil { - return "", err - } - if err := w.Write([]string{"process_latency_ns", fmt.Sprint(p.PID), fmt.Sprintf("%.2f", p.AvgLatencyNs), "", ""}); err != nil { - return "", err - } - } - for _, b := range snap.LatencyHistogram.Buckets() { - if err := w.Write([]string{"latency_hist", b.Label, fmt.Sprint(b.Count), fmt.Sprint(b.LowerNs), fmt.Sprint(b.UpperNs)}); err != nil { - return "", err - } - } - for _, b := range snap.GapHistogram.Buckets() { - if err := w.Write([]string{"gap_hist", b.Label, fmt.Sprint(b.Count), fmt.Sprint(b.LowerNs), fmt.Sprint(b.UpperNs)}); err != nil { - return "", err - } - } - } - - w.Flush() - if err := w.Error(); err != nil { - return "", err - } - return filename, nil -} - -func snapValue(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) uint64) uint64 { - if snap == nil { - return 0 - } - return get(snap) -} - -func snapValueF(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) float64) float64 { - if snap == nil { - return 0 - } - return get(snap) -} - -func trendSummary(snap *statsengine.Snapshot, get func(*statsengine.Snapshot) statsengine.Trend) string { - if snap == nil { - return "stable:0.00" - } - trend := get(snap) - return fmt.Sprintf("%s:%.2f", trend.Direction, trend.DeltaPercent) -} - func renderHelpOverlay(width, height int, groups [][]key.Binding) string { if width <= 0 { width = 80 |
