package ui import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) func (a App) updateResults(m tea.KeyMsg) (App, tea.Cmd) { switch m.String() { case "esc": a.screen = screenMenu case "r": // 같은 조건으로 재실행 rs := startRun(a.config) a.run = &rs a.screen = screenRunning return a, tickCmd() case "n": // 새 실험 (설정 화면으로) a.screen = screenConfig case "q": return a, tea.Quit } return a, nil } func (a App) viewResults() string { if a.run == nil { return errorStyle.Render("결과가 없습니다.") } rows := []string{} // 실험 헤더 header := fmt.Sprintf("📊 결과 — %s", a.config.ScenarioLabel()) if a.run.Aborted { header += " " + errorStyle.Render("(중단됨)") } rows = append(rows, sectionHeader(header)) rows = append(rows, helpStyle.Render(fmt.Sprintf( "지연=%dms · 손실=%.1f%% · 대역폭=%dMbps · 디바이스=%d · 메시지=%dKB×%d/디바이스", a.config.LinkDelayMs, a.config.PacketLossPct, a.config.BandwidthMbps, a.config.DeviceCount, a.config.MessageSizeKB(), a.config.MessageCount(), ))) rows = append(rows, "") // 비교 테이블 rows = append(rows, sectionHeader("성능 비교 표")) rows = append(rows, helpStyle.Render(fmt.Sprintf( " %-32s %9s %9s %9s %10s %10s %12s", "시스템", "P50(ms)", "P95(ms)", "P99(ms)", "RPS", "성공률", "총 데이터", ))) rows = append(rows, helpStyle.Render(strings.Repeat("─", 100))) bestRPS, bestRPSID := 0.0, "" bestP99, bestP99ID := 1e9, "" for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } if r.ThroughputRPS > bestRPS { bestRPS = r.ThroughputRPS bestRPSID = sys.ID } if r.P99 > 0 && r.P99 < bestP99 { bestP99 = r.P99 bestP99ID = sys.ID } } for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } successRate := 0.0 if r.Total > 0 { successRate = float64(r.Success) / float64(r.Total) * 100 } clr := systemColor(sys.ID) labelStyled := lipgloss.NewStyle().Foreground(clr).Bold(true).Render(sys.Label) marker := "" if sys.ID == bestRPSID { marker += "★" } if sys.ID == bestP99ID { marker += "⚡" } // 라벨이 색상 escape 코드로 인해 visible width가 다르므로 자체 padding labelPad := padVisible(sys.Label, 32) rows = append(rows, fmt.Sprintf( " %s %9.1f %9.1f %9.1f %10.0f %9.1f%% %12s %s", lipgloss.NewStyle().Foreground(clr).Bold(true).Render(labelPad), r.P50, r.P95, r.P99, r.ThroughputRPS, successRate, formatBytes(r.BytesSent), marker, )) _ = labelStyled } rows = append(rows, "") rows = append(rows, helpStyle.Render(" ★ 최고 처리량 ⚡ 최저 P99 latency")) rows = append(rows, "") // Latency 비교 차트 rows = append(rows, sectionHeader("Latency 비교 (낮을수록 좋음)")) maxLat := 0.0 for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } if r.P99 > maxLat { maxLat = r.P99 } } if maxLat == 0 { maxLat = 1 } for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } clr := systemColor(sys.ID) rows = append(rows, lipgloss.NewStyle().Foreground(clr).Bold(true).Render(sys.Label)) rows = append(rows, " P50 "+horizontalBar(r.P50, maxLat, 50, clr)+fmt.Sprintf(" %.1f ms", r.P50)) rows = append(rows, " P95 "+horizontalBar(r.P95, maxLat, 50, clr)+fmt.Sprintf(" %.1f ms", r.P95)) rows = append(rows, " P99 "+horizontalBar(r.P99, maxLat, 50, clr)+fmt.Sprintf(" %.1f ms", r.P99)) } rows = append(rows, "") // 처리량 비교 rows = append(rows, sectionHeader("처리량 비교 (높을수록 좋음)")) maxRPS := 0.0 for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } if r.ThroughputRPS > maxRPS { maxRPS = r.ThroughputRPS } } if maxRPS == 0 { maxRPS = 1 } for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } clr := systemColor(sys.ID) rows = append(rows, fmt.Sprintf( " %-32s %s %.0f RPS", lipgloss.NewStyle().Foreground(clr).Bold(true).Render(padVisible(sys.Label, 32)), horizontalBar(r.ThroughputRPS, maxRPS, 50, clr), r.ThroughputRPS, )) } rows = append(rows, "") // 연결 수립 시간 rows = append(rows, sectionHeader("연결 수립 시간 (낮을수록 좋음 — QUIC 0/1-RTT 효과)")) maxConn := 0.0 for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } if r.ConnectionTime > maxConn { maxConn = r.ConnectionTime } } if maxConn == 0 { maxConn = 1 } for _, sys := range a.config.Systems { if !sys.Selected { continue } r := a.run.Results[sys.ID] if r == nil { continue } clr := systemColor(sys.ID) rows = append(rows, fmt.Sprintf( " %-32s %s %.1f ms", lipgloss.NewStyle().Foreground(clr).Bold(true).Render(padVisible(sys.Label, 32)), horizontalBar(r.ConnectionTime, maxConn, 50, clr), r.ConnectionTime, )) } rows = append(rows, "") rows = append(rows, helpStyle.Render(strings.Repeat("─", 70))) rows = append(rows, helpStyle.Render( "💡 본 결과는 데모 시뮬레이션이며 실제 측정값이 아닙니다.", )) rows = append(rows, helpStyle.Render( " 실제 벤치마크는 향후 cmd/server / cmd/benchmark-runner 와 tc 연동 후 수행됩니다.", )) box := boxStyle.Render(strings.Join(rows, "\n")) help := helpKeys( [2]string{"r", "같은 조건 재실행"}, [2]string{"n", "새 실험"}, [2]string{"Esc", "메뉴"}, [2]string{"q", "종료"}, ) return lipgloss.JoinVertical(lipgloss.Left, "", box, "", help) } // padVisible visible 길이 기준으로 우측 공백 패딩 (한글 등 wide character 무시 단순 구현) func padVisible(s string, width int) string { if len(s) >= width { return s } return s + strings.Repeat(" ", width-len(s)) }