Skip to content

Commit daeaf04

Browse files
Merge pull request #7889 from onflow/janez/progress-logger-cleanup
Refactor and cleanup progress logger
2 parents f69494b + 9463b82 commit daeaf04

File tree

3 files changed

+434
-142
lines changed

3 files changed

+434
-142
lines changed

engine/verification/verifier/verifiers.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ func verifyConcurrently(
141141
defer cancel() // Ensure cancel is called to release resources
142142

143143
var lowestErr error
144-
var lowestErrHeight uint64 = ^uint64(0) // Initialize to max value of uint64
145-
var mu sync.Mutex // To protect access to lowestErr and lowestErrHeight
144+
var lowestErrHeight = ^uint64(0) // Initialize to max value of uint64
145+
var mu sync.Mutex // To protect access to lowestErr and lowestErrHeight
146146

147147
lg := util.LogProgress(
148148
log.Logger,
149149
util.DefaultLogProgressConfig(
150150
fmt.Sprintf("verifying heights progress for [%v:%v]", from, to),
151-
int(to+1-from),
151+
to+1-from,
152152
),
153153
)
154154

module/util/log.go

Lines changed: 144 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,113 @@ import (
88
"github.com/rs/zerolog"
99
)
1010

11-
// LogProgressFunc is a function that can be called to add to the progress
12-
type LogProgressFunc func(addProgress int)
13-
14-
type LogProgressConfig struct {
15-
Message string
16-
Total int
17-
Sampler zerolog.Sampler
11+
// LogProgressFunc is a function that can be called to add to the progress.
12+
// The function can be called concurrently. addProgress is the amount to add to the progress.
13+
// It is any integer number type, but all negative values are ignored.
14+
type LogProgressFunc[T int | uint | int32 | uint32 | uint64 | int64] func(addProgress T)
15+
16+
type LogProgressConfig[T int | uint | int32 | uint32 | uint64 | int64] struct {
17+
// message is part of the messages that will be logged.
18+
// The full template is: `%s progress %d/%d (%.1f%%) total time %s`.
19+
message string
20+
// total is the total value of progress expected.
21+
// When total is added to LogProgressFunc the progress is considered to be 100%.
22+
total T
23+
// noDataLogDuration. If the last log line was more than this duration ago and a new data point is added, a new log line is logged.
24+
// No line is logged if no data is received. The minimum resolution for noDataLogDuration is 1 millisecond.
25+
noDataLogDuration time.Duration
26+
// ticks is the number of increments to log at. If total is > 0 there will be at least 2 ticks. One at 0 and one at total.
27+
// If you want to log at every 10% set ticks to 11 (one is at 0%).
28+
// If the number of ticks is more than total, it will be set to total + 1.
29+
ticks uint64
1830
}
1931

20-
func DefaultLogProgressConfig(
32+
// DefaultLogProgressConfig returns a LogProgressConfig with default values.
33+
// The default values will log every 10% and will log an additional line if new data is received
34+
// after no data has been received for 1 minute.
35+
func DefaultLogProgressConfig[T int | uint | int32 | uint32 | uint64 | int64](
2136
message string,
22-
total int,
23-
) LogProgressConfig {
24-
nth := uint32(total / 10) // sample every 10% by default
25-
if nth == 0 {
26-
nth = 1
27-
}
28-
29-
sampler := newProgressLogsSampler(nth, 60*time.Second)
30-
return NewLogProgressConfig(
37+
total T,
38+
) LogProgressConfig[T] {
39+
return NewLogProgressConfig[T](
3140
message,
3241
total,
33-
sampler,
42+
60*time.Second,
43+
10,
3444
)
3545
}
3646

37-
func NewLogProgressConfig(
47+
// NewLogProgressConfig creates and returns a new LogProgressConfig with the specified message, total, duration, and ticks.
48+
// The duration is rounded to the nearest millisecond.
49+
// The number of ticks is the number of increments to log at. Logging at 0% is always done.
50+
// If you want to log at 10% increments, set ticks to 10.
51+
func NewLogProgressConfig[T int | uint | int32 | uint32 | uint64 | int64](
3852
message string,
39-
total int,
40-
sampler zerolog.Sampler) LogProgressConfig {
41-
return LogProgressConfig{
42-
Message: message,
43-
Total: total,
44-
Sampler: sampler,
53+
total T,
54+
noDataLogDuration time.Duration,
55+
ticks uint64,
56+
) LogProgressConfig[T] {
57+
// sanitize total
58+
if total < 0 {
59+
total = 0
4560
}
4661

47-
}
62+
// add the tick at 0%
63+
ticks = ticks + 1
4864

49-
type LogProgressOption func(config *LogProgressConfig)
65+
// sanitize ticks
66+
// number of ticks should be at most total + 1
67+
if uint64(total+1) < ticks {
68+
ticks = uint64(total + 1)
69+
}
70+
71+
// sanitize noDataLogDuration
72+
if noDataLogDuration < time.Millisecond {
73+
noDataLogDuration = time.Millisecond
74+
}
75+
76+
return LogProgressConfig[T]{
77+
message: message,
78+
total: total,
79+
noDataLogDuration: noDataLogDuration,
80+
ticks: ticks,
81+
}
5082

51-
// LogProgress takes a total and return function such that when called adds the given
52-
// number to the progress and logs the progress every 10% or every 60 seconds whichever
53-
// comes first.
83+
}
84+
85+
// LogProgress takes a LogProgressConfig and return function such that when called adds the given
86+
// number to the progress and logs the progress in defined increments or there is a time gap between progress
87+
// updates.
5488
// The returned function can be called concurrently.
5589
// An eta is also logged, but it assumes that the progress is linear.
56-
func LogProgress(
90+
func LogProgress[T int | uint | int32 | uint32 | uint64 | int64](
5791
log zerolog.Logger,
58-
config LogProgressConfig,
59-
) LogProgressFunc {
60-
sampler := log.Sample(config.Sampler)
61-
62-
start := time.Now()
63-
currentIndex := uint64(0)
64-
return func(add int) {
65-
current := atomic.AddUint64(&currentIndex, uint64(add))
92+
config LogProgressConfig[T],
93+
) LogProgressFunc[T] {
94+
95+
start := time.Now().UnixMilli()
96+
var lastDataTime atomic.Int64
97+
lastDataTime.Store(start)
98+
var currentIndex atomic.Uint64
99+
100+
// mutex to protect logProgress from concurrent calls
101+
// mutex is technically only needed for when the underlying io.Writer for the provider zerolog.Logger
102+
// is not thread safe. However we lock conservatively because we intend to call logProgress infrequently in normal
103+
// usage anyway.
104+
var mux sync.Mutex
105+
106+
total := uint64(config.total)
107+
logProgress := func(current uint64) {
108+
mux.Lock()
109+
defer mux.Unlock()
110+
111+
elapsed := time.Since(time.UnixMilli(start))
112+
elapsedString := elapsed.Round(1 * time.Second).String()
66113

67114
percentage := float64(100)
68-
if config.Total > 0 {
69-
percentage = (float64(current) / float64(config.Total)) * 100.
115+
if config.total > 0 {
116+
percentage = (float64(current) / float64(config.total)) * 100.
70117
}
71-
elapsed := time.Since(start)
72-
elapsedString := elapsed.Round(1 * time.Second).String()
73118

74119
etaString := "unknown"
75120
if percentage > 0 {
@@ -78,72 +123,76 @@ func LogProgress(
78123
eta = 0
79124
}
80125
etaString = eta.Round(1 * time.Second).String()
81-
82126
}
83127

84-
if current != uint64(config.Total) {
85-
sampler.Info().Msgf("%s progress %d/%d (%.1f%%) elapsed: %s, eta %s", config.Message, current, config.Total, percentage, elapsedString, etaString)
128+
if current < total {
129+
log.Info().Msgf("%s progress %d/%d (%.1f%%) elapsed: %s, eta %s", config.message, current, config.total, percentage, elapsedString, etaString)
86130
} else {
87-
log.Info().Msgf("%s progress %d/%d (%.1f%%) total time %s", config.Message, current, config.Total, percentage, elapsedString)
131+
log.Info().Msgf("%s progress %d/%d (%.1f%%) total time %s", config.message, current, config.total, percentage, elapsedString)
88132
}
89133
}
90-
}
91134

92-
type TimedSampler struct {
93-
start time.Time
94-
Duration time.Duration
95-
mu sync.Mutex
96-
}
135+
// log 0% progress
136+
logProgress(0)
97137

98-
var _ zerolog.Sampler = (*TimedSampler)(nil)
99-
100-
func NewTimedSampler(duration time.Duration) *TimedSampler {
101-
return &TimedSampler{
102-
start: time.Now(),
103-
Duration: duration,
104-
mu: sync.Mutex{},
138+
// sanitize inputs and calculate increment
139+
ticksIncludingZero := config.ticks
140+
if ticksIncludingZero < 2 {
141+
ticksIncludingZero = 2
105142
}
106-
}
143+
ticks := ticksIncludingZero - 1
107144

108-
func (s *TimedSampler) Sample(_ zerolog.Level) bool {
109-
s.mu.Lock()
110-
defer s.mu.Unlock()
111-
112-
if time.Since(s.start) > s.Duration {
113-
s.start = time.Now()
114-
return true
145+
increment := total / ticks
146+
if increment == 0 {
147+
increment = 1
115148
}
116-
return false
117-
}
118149

119-
func (s *TimedSampler) Reset() {
120-
s.mu.Lock()
121-
defer s.mu.Unlock()
150+
// increment doesn't necessarily divide config.total
151+
// Because we want 100% to mean 100% we need to deduct this overflow from the current value
152+
// before checking if it is a multiple of the increment.
153+
incrementsOverflow := total % increment
154+
noLogDurationMillis := config.noDataLogDuration.Milliseconds()
122155

123-
s.start = time.Now()
124-
}
125-
126-
type progressLogsSampler struct {
127-
basicSampler *zerolog.BasicSampler
128-
timedSampler *TimedSampler
129-
}
156+
return func(add T) {
157+
if total == 0 {
158+
return
159+
}
160+
if add < 0 {
161+
return
162+
}
163+
diff := uint64(add)
164+
now := time.Now().UnixMilli()
165+
166+
// it can technically happen that current > total. In this case we continue to log
167+
// the progress using the calculated increments
168+
current := currentIndex.Add(diff)
169+
lastTime := lastDataTime.Swap(now)
170+
171+
// if the diff went over one or more increments, log the progress for each increment
172+
fromTick := uint64(0)
173+
if current-diff >= incrementsOverflow {
174+
fromTick = (current - diff - incrementsOverflow) / increment
175+
}
176+
toTick := uint64(0)
177+
if current >= incrementsOverflow {
178+
toTick = (current - incrementsOverflow) / increment
179+
}
130180

131-
var _ zerolog.Sampler = (*progressLogsSampler)(nil)
181+
if fromTick == toTick && now-lastTime > noLogDurationMillis {
182+
// no data for a while, log whatever we are at now
183+
logProgress(current)
184+
return
185+
}
132186

133-
// newProgressLogsSampler returns a sampler that samples every nth log
134-
// and also samples a log if the last log was more than duration ago
135-
func newProgressLogsSampler(nth uint32, duration time.Duration) zerolog.Sampler {
136-
return &progressLogsSampler{
137-
basicSampler: &zerolog.BasicSampler{N: nth},
138-
timedSampler: NewTimedSampler(duration),
139-
}
140-
}
187+
if toTick <= fromTick {
188+
return
189+
}
141190

142-
func (s *progressLogsSampler) Sample(lvl zerolog.Level) bool {
143-
sample := s.basicSampler.Sample(lvl)
144-
if sample {
145-
s.timedSampler.Reset()
146-
return true
191+
for t := fromTick; t < toTick; t++ {
192+
// (t+1) because we want to log the progress for the increment reached
193+
// not the increment past
194+
current := increment*(t+1) + incrementsOverflow
195+
logProgress(current)
196+
}
147197
}
148-
return s.timedSampler.Sample(lvl)
149198
}

0 commit comments

Comments
 (0)