Add block-update latency tracking, raw share counters, and shares bar UI

Log ZMQ→mining.notify latency in ckpool (patch 0005), expose a raw
reject counter (patch 0006), and surface both in the dashboard:

- Block latency card shows avg/last ms, wasted work, and block count
- SharesBar component shows session + all-time accepted/rejected with
  a Demon Slayer flame-slash animation on new shares
- Miners card info moved to MinersTable section header
- Latency stats and share counts persist across restarts via kv store
- Removed misleading pool-wide share stats from per-worker/user pages
  (ckpool doesn't expose per-user raw counts)
This commit is contained in:
satoshi
2026-05-11 02:12:16 +03:00
parent e622f1a81a
commit 6acff6280f
14 changed files with 555 additions and 18 deletions
+117
View File
@@ -88,6 +88,19 @@ type Snapshot struct {
LastZMQEventAge float64 `json:"last_zmq_event_age,omitempty"` // seconds; >=0
HasLastZMQEvent bool `json:"has_last_zmq_event"`
// Share counters: raw counts (1 submission = 1 share regardless of diff).
// Session = since ckpool started; AllTime = persisted across restarts.
SessionAccepted int64 `json:"session_accepted"`
SessionRejected int64 `json:"session_rejected"`
AllTimeAccepted int64 `json:"alltime_accepted"`
AllTimeRejected int64 `json:"alltime_rejected"`
// Block update latency diagnostics (ZMQ trigger → mining.notify).
LatencyCount int64 `json:"latency_count"`
LatencyAvgMs int64 `json:"latency_avg_ms"`
LatencyLastMs int64 `json:"latency_last_ms"`
StaleWorkHashes float64 `json:"stale_work_hashes"`
// Health
CKPoolOK bool `json:"ckpool_ok"`
BitcoinOK bool `json:"bitcoin_ok"`
@@ -107,6 +120,14 @@ const (
kvSubmitAttempts = "block_submit_attempts"
kvSubmitsConfirmed = "block_submits_confirmed"
kvLatencyCount = "latency_count"
kvLatencySumMs = "latency_sum_ms"
kvLatencyLastMs = "latency_last_ms"
kvStaleWorkHashes = "stale_work_hashes"
kvAllTimeAccepted = "alltime_accepted"
kvAllTimeRejected = "alltime_rejected"
// reconcileInterval is how often we sweep recent blocks looking
// for missing hash/reward enrichment and reorg-orphaned hashes.
// 60s is fast enough to recover from a transient bitcoind hiccup
@@ -192,6 +213,22 @@ type Aggregator struct {
// from zmqmon. Used by /healthz to flag stale subscriptions.
zmqEnabled bool
lastZMQEventTime time.Time
// All-time share counters (raw, not diff-weighted). Accumulated
// using the same delta-integration pattern as cumulative_shares.
allTimeAccepted int64
allTimeRejected int64
lastPoolAcceptedRaw int64
lastPoolRejectedRaw int64
hasShareCountBaseline bool
lastShareCountSave time.Time
// Block update latency tracking (from ckpool patch 0005).
// Persisted to kv store so stats accumulate across restarts.
latencyCount int64 // number of observations
latencySumMs int64 // sum of all latencies in ms
latencyLastMs int64 // most recent observation
staleWorkHashes float64 // cumulative wasted hashes = sum(latency_s * hashrate_at_event)
}
func New(ck *ckpool.Client, rpc *bitcoind.RPC, interval time.Duration, log *slog.Logger) *Aggregator {
@@ -396,6 +433,36 @@ func (a *Aggregator) refresh(ctx context.Context) {
a.lastCumulativeSave = now
}
// --- raw share count tracking (1 share = 1 submission) ---
// pool.Shares = raw accepted count, pool.RejectCount = raw rejected count.
// Delta-integrate the same way as cumulative work.
if next.Pool != nil {
curAcc := next.Pool.Shares
curRej := next.Pool.RejectCount
if a.hasShareCountBaseline {
if curAcc >= a.lastPoolAcceptedRaw {
a.allTimeAccepted += curAcc - a.lastPoolAcceptedRaw
}
if curRej >= a.lastPoolRejectedRaw {
a.allTimeRejected += curRej - a.lastPoolRejectedRaw
}
}
a.lastPoolAcceptedRaw = curAcc
a.lastPoolRejectedRaw = curRej
a.hasShareCountBaseline = true
next.SessionAccepted = curAcc
next.SessionRejected = curRej
}
next.AllTimeAccepted = a.allTimeAccepted
next.AllTimeRejected = a.allTimeRejected
// Persist share counts at most once per minute.
if a.Store != nil && now.Sub(a.lastShareCountSave) >= time.Minute {
_ = a.Store.SetKV(kvAllTimeAccepted, strconv.FormatInt(a.allTimeAccepted, 10))
_ = a.Store.SetKV(kvAllTimeRejected, strconv.FormatInt(a.allTimeRejected, 10))
a.lastShareCountSave = now
}
// --- hashrate history sample (once per minute) ---
if now.Sub(a.lastHRSampleAt) >= time.Minute {
p := HashratePoint{T: now.Unix(), V: next.HashrateHs}
@@ -455,6 +522,12 @@ func (a *Aggregator) refresh(ctx context.Context) {
next.LastZMQEventAge = time.Since(a.lastZMQEventTime).Seconds()
next.HasLastZMQEvent = true
}
next.LatencyCount = a.latencyCount
if a.latencyCount > 0 {
next.LatencyAvgMs = a.latencySumMs / a.latencyCount
}
next.LatencyLastMs = a.latencyLastMs
next.StaleWorkHashes = a.staleWorkHashes
a.snap = next
cb := a.OnRefresh
pushed := next
@@ -500,6 +573,50 @@ func (a *Aggregator) loadPersistedState() {
}
}
// Restore all-time share counts.
if v, err := a.Store.GetKV(kvAllTimeAccepted); err == nil && v != "" {
if n, perr := strconv.ParseInt(v, 10, 64); perr == nil {
a.mu.Lock()
a.allTimeAccepted = n
a.mu.Unlock()
}
}
if v, err := a.Store.GetKV(kvAllTimeRejected); err == nil && v != "" {
if n, perr := strconv.ParseInt(v, 10, 64); perr == nil {
a.mu.Lock()
a.allTimeRejected = n
a.mu.Unlock()
}
}
// Restore latency stats.
a.mu.Lock()
if v, err := a.Store.GetKV(kvLatencyCount); err == nil && v != "" {
if n, perr := strconv.ParseInt(v, 10, 64); perr == nil {
a.latencyCount = n
}
}
if v, err := a.Store.GetKV(kvLatencySumMs); err == nil && v != "" {
if n, perr := strconv.ParseInt(v, 10, 64); perr == nil {
a.latencySumMs = n
}
}
if v, err := a.Store.GetKV(kvLatencyLastMs); err == nil && v != "" {
if n, perr := strconv.ParseInt(v, 10, 64); perr == nil {
a.latencyLastMs = n
}
}
if v, err := a.Store.GetKV(kvStaleWorkHashes); err == nil && v != "" {
if f, perr := strconv.ParseFloat(v, 64); perr == nil {
a.staleWorkHashes = f
}
}
if a.latencyCount > 0 {
a.Log.Info("latency stats loaded", "count", a.latencyCount,
"avg_ms", a.latencySumMs/a.latencyCount, "last_ms", a.latencyLastMs)
}
a.mu.Unlock()
cutoff := time.Now().Add(-24 * time.Hour).Unix()
if samples, err := a.Store.HashrateSince(cutoff); err != nil {
a.Log.Warn("hashrate history load failed", "err", err)
+33
View File
@@ -80,6 +80,39 @@ func (a *Aggregator) IngestAttemptEvents(ctx context.Context, events <-chan logm
}
}
// IngestLatencyEvents reads block-update latency events from the tailer
// and accumulates stats for the dashboard. Stats are persisted to the
// kv store so they survive restarts.
func (a *Aggregator) IngestLatencyEvents(ctx context.Context, events <-chan logmon.LatencyEvent) {
for {
select {
case <-ctx.Done():
return
case ev, ok := <-events:
if !ok {
return
}
a.mu.Lock()
a.latencyCount++
a.latencySumMs += ev.LatencyMs
a.latencyLastMs = ev.LatencyMs
// Wasted hashes = latency in seconds × current 5m hashrate.
hs5m := a.snap.HashrateHs5m
a.staleWorkHashes += (float64(ev.LatencyMs) / 1000.0) * hs5m
count, sum, last, stale := a.latencyCount, a.latencySumMs, a.latencyLastMs, a.staleWorkHashes
a.mu.Unlock()
a.Log.Info("logmon: block update latency", "ms", ev.LatencyMs, "count", count)
// Blocks are infrequent (~10min); persist every event.
if a.Store != nil {
_ = a.Store.SetKV(kvLatencyCount, strconv.FormatInt(count, 10))
_ = a.Store.SetKV(kvLatencySumMs, strconv.FormatInt(sum, 10))
_ = a.Store.SetKV(kvLatencyLastMs, strconv.FormatInt(last, 10))
_ = a.Store.SetKV(kvStaleWorkHashes, strconv.FormatFloat(stale, 'f', -1, 64))
}
}
}
}
// IngestBlockEvents reads block events from the tailer and appends them
// to the snapshot's block history. Runs until ctx is cancelled.
func (a *Aggregator) IngestBlockEvents(ctx context.Context, events <-chan logmon.BlockEvent) {