diff --git a/common/eta.go b/common/eta.go
new file mode 100644
index 000000000000..72c838f93ddb
--- /dev/null
+++ b/common/eta.go
@@ -0,0 +1,30 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package common
+
+import "time"
+
+// CalculateETA calculates the estimated remaining time based on the
+// number of finished task, remaining task, and the time cost for finished task.
+func CalculateETA(done, left uint64, elapsed time.Duration) time.Duration {
+ if done == 0 || elapsed.Milliseconds() == 0 {
+ return 0
+ }
+
+ speed := float64(done) / float64(elapsed.Milliseconds())
+ return time.Duration(float64(left)/speed) * time.Millisecond
+}
diff --git a/common/eta_test.go b/common/eta_test.go
new file mode 100644
index 000000000000..b1dbb09e6cb4
--- /dev/null
+++ b/common/eta_test.go
@@ -0,0 +1,60 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package common
+
+import (
+ "testing"
+ "time"
+)
+
+func TestCalculateETA(t *testing.T) {
+ type args struct {
+ done uint64
+ left uint64
+ elapsed time.Duration
+ }
+ tests := []struct {
+ name string
+ args args
+ want time.Duration
+ }{
+ {
+ name: "zero done",
+ args: args{done: 0, left: 100, elapsed: time.Second},
+ want: 0,
+ },
+ {
+ name: "zero elapsed",
+ args: args{done: 1, left: 100, elapsed: 0},
+ want: 0,
+ },
+ {
+ name: "@Jolly23 's case",
+ args: args{done: 16858580, left: 41802252, elapsed: 66179848 * time.Millisecond},
+ want: 164098440 * time.Millisecond,
+ // wrong msg: msg="Indexing state history" processed=16858580 left=41802252 elapsed=18h22m59.848s eta=11h36m42.252s
+ // should be around 45.58 hours
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := CalculateETA(tt.args.done, tt.args.left, tt.args.elapsed); got != tt.want {
+ t.Errorf("CalculateETA() = %v ms, want %v ms", got.Milliseconds(), tt.want)
+ }
+ })
+ }
+}
diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go
index 46558a6fceec..11f3963a3e46 100644
--- a/core/state/pruner/pruner.go
+++ b/core/state/pruner/pruner.go
@@ -160,11 +160,8 @@ func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, sta
var eta time.Duration // Realistically will never remain uninited
if done := binary.BigEndian.Uint64(key[:8]); done > 0 {
- var (
- left = math.MaxUint64 - binary.BigEndian.Uint64(key[:8])
- speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- )
- eta = time.Duration(left/speed) * time.Millisecond
+ left := math.MaxUint64 - binary.BigEndian.Uint64(key[:8])
+ eta = common.CalculateETA(done, left, time.Since(pstart))
}
if time.Since(logged) > 8*time.Second {
log.Info("Pruning state data", "nodes", count, "skipped", skipped, "size", size,
diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go
index 4b0774f2ae30..0d39687be4ad 100644
--- a/core/state/snapshot/conversion.go
+++ b/core/state/snapshot/conversion.go
@@ -171,20 +171,16 @@ func (stat *generateStats) report() {
// If there's progress on the account trie, estimate the time to finish crawling it
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
var (
- left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
- speed = done/uint64(time.Since(stat.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- eta = time.Duration(left/speed) * time.Millisecond
+ left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
+ eta = common.CalculateETA(done, left, time.Since(stat.start))
)
// If there are large contract crawls in progress, estimate their finish time
for acc, head := range stat.slotsHead {
start := stat.slotsStart[acc]
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
- var (
- left = math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
- speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- )
+ left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
// Override the ETA if larger than the largest until now
- if slotETA := time.Duration(left/speed) * time.Millisecond; eta < slotETA {
+ if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA {
eta = slotETA
}
}
diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go
index 127459b47cf5..14b9af536760 100644
--- a/triedb/pathdb/history_indexer.go
+++ b/triedb/pathdb/history_indexer.go
@@ -543,12 +543,10 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
logged = time.Now()
var (
- left = lastID - current + 1
- done = current - beginID
- speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
+ left = lastID - current + 1
+ done = current - beginID
)
- // Override the ETA if larger than the largest until now
- eta := time.Duration(left/speed) * time.Millisecond
+ eta := common.CalculateETA(done, left, time.Since(start))
log.Info("Indexing state history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta))
}
}
diff --git a/triedb/pathdb/verifier.go b/triedb/pathdb/verifier.go
index 2d6f72925b6e..a69b10f4f304 100644
--- a/triedb/pathdb/verifier.go
+++ b/triedb/pathdb/verifier.go
@@ -166,20 +166,17 @@ func (stat *generateStats) report() {
// If there's progress on the account trie, estimate the time to finish crawling it
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
var (
- left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
- speed = done/uint64(time.Since(stat.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- eta = time.Duration(left/speed) * time.Millisecond
+ left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
+ eta = common.CalculateETA(done, left, time.Since(stat.start))
)
// If there are large contract crawls in progress, estimate their finish time
for acc, head := range stat.slotsHead {
start := stat.slotsStart[acc]
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
- var (
- left = math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
- speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- )
+ left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
+
// Override the ETA if larger than the largest until now
- if slotETA := time.Duration(left/speed) * time.Millisecond; eta < slotETA {
+ if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA {
eta = slotETA
}
}