summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/export/snapshot_csv.go103
-rw-r--r--internal/tui/tui.go97
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