Skip to content

Commit 88c2742

Browse files
author
JkLondon
committed
commit
1 parent 32dfadb commit 88c2742

File tree

6 files changed

+419
-115
lines changed

6 files changed

+419
-115
lines changed

cmd/etui/app/app.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,23 @@ import (
1919

2020
// App is the top-level TUI application.
2121
type App struct {
22-
tview *tview.Application
23-
dp *datasource.DownloaderPinger
24-
sysColl *datasource.SystemCollector
25-
iopsTrack *datasource.DiskIOPSTracker
26-
datadir string
22+
tview *tview.Application
23+
dp *datasource.DownloaderPinger
24+
sysColl *datasource.SystemCollector
25+
iopsTrack *datasource.DiskIOPSTracker
26+
syncTracker *datasource.SyncTracker
27+
datadir string
2728
}
2829

2930
// New creates an App that reads from the given datadir.
3031
func New(datadir string) *App {
3132
return &App{
32-
datadir: datadir,
33-
tview: tview.NewApplication(),
34-
dp: datasource.NewDownloaderPinger(config.DefaultDownloaderURL),
35-
sysColl: datasource.NewSystemCollector(datadir),
36-
iopsTrack: datasource.NewDiskIOPSTracker(),
33+
datadir: datadir,
34+
tview: tview.NewApplication(),
35+
dp: datasource.NewDownloaderPinger(config.DefaultDownloaderURL),
36+
sysColl: datasource.NewSystemCollector(datadir),
37+
iopsTrack: datasource.NewDiskIOPSTracker(),
38+
syncTracker: datasource.NewSyncTracker(),
3739
}
3840
}
3941

@@ -130,9 +132,10 @@ func (a *App) fillStagesInfo(ctx context.Context, view *widgets.NodeInfoView, in
130132
if !ok {
131133
return
132134
}
135+
metrics := a.syncTracker.Update(info)
136+
currentStage := leadingStageName(info)
133137
a.tview.QueueUpdateDraw(func() {
134-
view.Overview.Clear()
135-
view.Overview.SetText(info.OverviewTUI())
138+
view.SyncStatus.UpdateSyncStatus(metrics, currentStage)
136139
view.Stages.Clear()
137140
view.Stages.SetText(info.Stages())
138141
view.DomainII.Clear()
@@ -142,6 +145,22 @@ func (a *App) fillStagesInfo(ctx context.Context, view *widgets.NodeInfoView, in
142145
}
143146
}
144147

148+
// leadingStageName returns the last stage in pipeline order that has non-zero
149+
// progress. This is the furthest stage the sync has reached, giving a reliable
150+
// indicator of current sync position regardless of early-sync zero values.
151+
func leadingStageName(info *commands.StagesInfo) string {
152+
if len(info.StagesProgress) == 0 {
153+
return "—"
154+
}
155+
last := "—"
156+
for _, sp := range info.StagesProgress {
157+
if sp.Progress > 0 {
158+
last = string(sp.Stage)
159+
}
160+
}
161+
return last
162+
}
163+
145164
// runClock updates the clock widget every second.
146165
func (a *App) runClock(ctx context.Context, clock *tview.TextView) {
147166
ticker := time.NewTicker(1 * time.Second)

cmd/etui/cmd/stages.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,15 @@ var infoCmd = &cobra.Command{
4545
for {
4646
err := commands.InfoAllStages(cmd.Context(), logger, datadirCli, infoCh)
4747
if err == nil {
48-
return
48+
return // context cancelled or clean exit
4949
}
50-
// All errors are transient for etui: DB locked, salt files
50+
// DB open errors are transient: DB locked, salt files
5151
// missing, chaindata not yet created, etc. Always retry.
52+
logger.Warn("InfoAllStages failed, retrying", "err", err)
53+
select {
54+
case errCh <- fmt.Errorf("InfoAllStages: %v (retrying in %v)", err, retryInterval):
55+
default:
56+
}
5257
select {
5358
case <-cmd.Context().Done():
5459
return

cmd/etui/datasource/stages.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package datasource
2+
3+
import (
4+
"sync"
5+
"time"
6+
7+
"github.com/erigontech/erigon/cmd/integration/commands"
8+
"github.com/erigontech/erigon/execution/stagedsync/stages"
9+
)
10+
11+
// SyncMetrics holds derived sync metrics computed from StagesInfo snapshots.
12+
type SyncMetrics struct {
13+
HeadBlock uint64 // highest block across all stages
14+
PipelineHead uint64 // Finish stage progress — last block that completed the full pipeline
15+
TipLagSeconds int64 // seconds since last stage movement (0 = unknown)
16+
ImportRate float64 // blocks per minute over 60-second window
17+
Status SyncStatus
18+
}
19+
20+
// SyncStatus represents the overall sync health.
21+
type SyncStatus int
22+
23+
const (
24+
StatusUnknown SyncStatus = iota
25+
StatusInSync // tip lag < 30s
26+
StatusSyncing // stages are moving but lagging
27+
StatusStalled // no stage movement for > 60s
28+
)
29+
30+
// String returns a human-readable label for the status.
31+
func (s SyncStatus) String() string {
32+
switch s {
33+
case StatusInSync:
34+
return "IN SYNC"
35+
case StatusSyncing:
36+
return "SYNCING"
37+
case StatusStalled:
38+
return "STALLED"
39+
default:
40+
return "UNKNOWN"
41+
}
42+
}
43+
44+
// SyncTracker computes derived sync metrics from successive StagesInfo snapshots.
45+
type SyncTracker struct {
46+
mu sync.Mutex
47+
48+
updateCount int // number of Update() calls received
49+
prevHead uint64
50+
prevTime time.Time
51+
lastMoveTime time.Time // last time any stage progressed
52+
53+
// Ring buffer for 60-second import rate calculation
54+
samples []rateSample
55+
sampleIdx int
56+
sampleFull bool
57+
}
58+
59+
type rateSample struct {
60+
block uint64
61+
ts time.Time
62+
}
63+
64+
const rateSamples = 12 // 12 samples × ~5s poll interval ≈ 60 seconds
65+
66+
// NewSyncTracker creates a new SyncTracker.
67+
func NewSyncTracker() *SyncTracker {
68+
return &SyncTracker{
69+
samples: make([]rateSample, rateSamples),
70+
}
71+
}
72+
73+
// Update computes sync metrics from the latest StagesInfo.
74+
func (t *SyncTracker) Update(info *commands.StagesInfo) SyncMetrics {
75+
t.mu.Lock()
76+
defer t.mu.Unlock()
77+
78+
now := time.Now()
79+
t.updateCount++
80+
m := SyncMetrics{}
81+
82+
// Head block: max progress across all stages
83+
for _, sp := range info.StagesProgress {
84+
if sp.Progress > m.HeadBlock {
85+
m.HeadBlock = sp.Progress
86+
}
87+
}
88+
89+
// Pipeline head: Finish stage progress (last block that completed the full pipeline)
90+
for _, sp := range info.StagesProgress {
91+
if sp.Stage == stages.Finish {
92+
m.PipelineHead = sp.Progress
93+
break
94+
}
95+
}
96+
97+
// Detect stage movement
98+
if t.prevTime.IsZero() {
99+
// First update — initialise but do NOT set lastMoveTime to now,
100+
// so the first poll shows StatusUnknown instead of a false green.
101+
t.prevHead = m.HeadBlock
102+
t.prevTime = now
103+
} else if m.HeadBlock != t.prevHead {
104+
t.lastMoveTime = now
105+
t.prevHead = m.HeadBlock
106+
t.prevTime = now
107+
}
108+
109+
// Tip lag: seconds since last stage movement
110+
if t.lastMoveTime.IsZero() {
111+
m.TipLagSeconds = -1 // no movement observed yet
112+
} else {
113+
m.TipLagSeconds = int64(now.Sub(t.lastMoveTime).Seconds())
114+
}
115+
116+
// Import rate: blocks/minute from 60-second ring buffer
117+
t.samples[t.sampleIdx] = rateSample{block: m.HeadBlock, ts: now}
118+
t.sampleIdx = (t.sampleIdx + 1) % rateSamples
119+
if t.sampleIdx == 0 {
120+
t.sampleFull = true
121+
}
122+
123+
m.ImportRate = t.calcRate(now)
124+
125+
// Status determination — need at least 2 samples to judge
126+
if t.updateCount < 2 || t.lastMoveTime.IsZero() {
127+
m.Status = StatusUnknown
128+
} else {
129+
switch {
130+
case m.TipLagSeconds < 30:
131+
m.Status = StatusInSync
132+
case m.TipLagSeconds >= 60:
133+
m.Status = StatusStalled
134+
default:
135+
m.Status = StatusSyncing
136+
}
137+
}
138+
139+
return m
140+
}
141+
142+
// calcRate computes blocks/minute from the ring buffer.
143+
func (t *SyncTracker) calcRate(now time.Time) float64 {
144+
var oldest rateSample
145+
146+
if t.sampleFull {
147+
oldest = t.samples[t.sampleIdx] // oldest sample in full ring
148+
} else {
149+
oldest = t.samples[0] // first sample
150+
}
151+
152+
if oldest.ts.IsZero() {
153+
return 0
154+
}
155+
156+
elapsed := now.Sub(oldest.ts).Seconds()
157+
if elapsed < 1 {
158+
return 0
159+
}
160+
161+
newest := t.samples[(t.sampleIdx-1+rateSamples)%rateSamples]
162+
if newest.block <= oldest.block {
163+
return 0
164+
}
165+
166+
blocksPerSec := float64(newest.block-oldest.block) / elapsed
167+
return blocksPerSec * 60 // blocks per minute
168+
}

cmd/etui/widgets/nodeinfo.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
// NodeInfoView holds the text views for the node-info page.
88
type NodeInfoView struct {
9-
Overview *tview.TextView
9+
SyncStatus *SyncStatusView
1010
Stages *tview.TextView
1111
DomainII *tview.TextView
1212
Clock *tview.TextView
@@ -17,7 +17,7 @@ type NodeInfoView struct {
1717
// NewNodeInfoPage builds the node-info page layout and returns it with its backing view.
1818
func NewNodeInfoPage() (*tview.Flex, *NodeInfoView) {
1919
view := &NodeInfoView{
20-
Overview: tview.NewTextView().SetText("waiting for fetch data from erigon...").SetDynamicColors(true),
20+
SyncStatus: NewSyncStatusView(),
2121
Stages: tview.NewTextView().SetDynamicColors(true),
2222
DomainII: tview.NewTextView().SetDynamicColors(true),
2323
Clock: tview.NewTextView().SetTextAlign(tview.AlignRight).SetDynamicColors(true),
@@ -29,7 +29,7 @@ func NewNodeInfoPage() (*tview.Flex, *NodeInfoView) {
2929

3030
view.Downloader.Box.SetBorder(true)
3131
topPanel := tview.NewFlex().
32-
AddItem(view.Overview, 0, 1, false).
32+
AddItem(view.SyncStatus.TextView, 0, 1, false).
3333
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
3434
AddItem(view.Clock, 1, 1, false).
3535
AddItem(view.Downloader, 0, 5, false), 0, 1, false)

cmd/etui/widgets/sync_status.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package widgets
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/rivo/tview"
7+
8+
"github.com/erigontech/erigon/cmd/etui/datasource"
9+
)
10+
11+
// SyncStatusView displays a human-readable sync dashboard:
12+
// Stage, Head, Finalized, Tip lag, Import rate, with color-coded status.
13+
type SyncStatusView struct {
14+
*tview.TextView
15+
}
16+
17+
// NewSyncStatusView creates a new sync-status widget.
18+
func NewSyncStatusView() *SyncStatusView {
19+
tv := tview.NewTextView().
20+
SetDynamicColors(true).
21+
SetText("[::d]waiting for sync data...[-]")
22+
return &SyncStatusView{TextView: tv}
23+
}
24+
25+
// UpdateSyncStatus refreshes the widget with new metrics.
26+
func (v *SyncStatusView) UpdateSyncStatus(m datasource.SyncMetrics, stageName string) {
27+
statusColor := statusToColor(m.Status)
28+
statusLabel := m.Status.String()
29+
30+
lagStr := formatLag(m.TipLagSeconds)
31+
rateStr := formatRate(m.ImportRate)
32+
33+
text := fmt.Sprintf(
34+
"[yellow]Status:[-] [%s::b]● %s[-:-:-]\n"+
35+
"[yellow]Stage:[-] [cyan]%s[-]\n"+
36+
"[yellow]Head:[-] [cyan]%d[-]\n"+
37+
"[yellow]Pipeline:[-] [cyan]%d[-]\n"+
38+
"[yellow]Tip lag:[-] %s\n"+
39+
"[yellow]Import:[-] %s",
40+
statusColor, statusLabel,
41+
stageName,
42+
m.HeadBlock,
43+
m.PipelineHead,
44+
lagStr,
45+
rateStr,
46+
)
47+
v.Clear()
48+
v.SetText(text)
49+
}
50+
51+
// statusToColor returns a tview color tag for the sync status.
52+
func statusToColor(s datasource.SyncStatus) string {
53+
switch s {
54+
case datasource.StatusInSync:
55+
return "green"
56+
case datasource.StatusSyncing:
57+
return "yellow"
58+
case datasource.StatusStalled:
59+
return "red"
60+
default:
61+
return "gray"
62+
}
63+
}
64+
65+
// formatLag formats tip lag with color coding.
66+
func formatLag(seconds int64) string {
67+
if seconds < 0 {
68+
return "[::d]—[-]"
69+
}
70+
if seconds < 30 {
71+
return fmt.Sprintf("[green]%ds[-]", seconds)
72+
}
73+
if seconds < 60 {
74+
return fmt.Sprintf("[yellow]%ds[-]", seconds)
75+
}
76+
if seconds < 3600 {
77+
return fmt.Sprintf("[red]%dm%ds[-]", seconds/60, seconds%60)
78+
}
79+
return fmt.Sprintf("[red]%dh%dm[-]", seconds/3600, (seconds%3600)/60)
80+
}
81+
82+
// formatRate formats import rate (blocks/minute).
83+
func formatRate(rate float64) string {
84+
if rate < 0.1 {
85+
return "[::d]—[-]"
86+
}
87+
if rate >= 1000 {
88+
return fmt.Sprintf("[green]%.0f blk/min[-]", rate)
89+
}
90+
return fmt.Sprintf("[green]%.1f blk/min[-]", rate)
91+
}

0 commit comments

Comments
 (0)