Files
grpc_performance_comparison/src/internal/ui/screen_results.go
T
Godopu 3db48c3bae feat: 성능 비교 테스트베드 데모 UI 구현 (src/, Bubble Tea + Lipgloss)
Phase 0 산출물 — 실제 측정 없이 데모 모드로 동작하는 Terminal UI.
시연 시 최종 목표 화면을 가시화하고, 파라미터 조정에 반응하는
그럴듯한 추세를 시뮬레이션한다.

비교 대상 시스템:
- REST + HTTP/2 (TCP+TLS) + JSON
- gRPC + HTTP/2 (TCP+TLS) + Protobuf
- gRPC + HTTP/3 (QUIC+TLS1.3) + Protobuf  ★ 본 연구 제안

조절 가능한 파라미터:
- 워크로드 시나리오 (Small-Many 1KB×10000 / Large-Few 1MB×50)
- 링크 지연 (0~500ms), 패킷 손실 (0~5%)
- 대역폭 (1~1000Mbps), 디바이스 수 (1~100)

화면 구성:
- 메인 메뉴 / 설정 / 실시간 진행 / 결과 비교 / 정보 5개 화면
- 진행률 막대, latency sparkline, 비교 차트(P50/P95/P99/RPS/연결시간)

구현:
- src/cmd/benchcli/main.go        진입점
- src/internal/ui/app.go          Bubble Tea Model + 화면 dispatcher
- src/internal/ui/types.go        Config / Result / RunState 정의
- src/internal/ui/styles.go       Lipgloss 스타일·색상
- src/internal/ui/components.go   progressBar / sparkline / slider
- src/internal/ui/simulator.go    mock 시뮬레이터 (Phase 5에서 실측으로 교체)
- src/internal/ui/screen_*.go     각 화면 (menu/config/running/results/about)

의존성: bubbletea v1.2.4, lipgloss v1.0.0
빌드: `cd src && go run ./cmd/benchcli`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 01:32:15 +00:00

258 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}