Skip to content

Commit adf66da

Browse files
authored
Improve logging output during chain sync (#1478)
1 parent 2a6f3cb commit adf66da

File tree

5 files changed

+222
-48
lines changed

5 files changed

+222
-48
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
3+
from trinity.utils.humanize import humanize_elapsed
4+
5+
6+
SECOND = 1
7+
MINUTE = 60
8+
HOUR = 60 * 60
9+
DAY = 24 * HOUR
10+
YEAR = 365 * DAY
11+
MONTH = YEAR // 12
12+
WEEK = 7 * DAY
13+
14+
15+
@pytest.mark.parametrize(
16+
'seconds,expected',
17+
(
18+
(0, '0s'),
19+
(1, '1s'),
20+
(60, '1m'),
21+
(61, '1m1s'),
22+
(119, '1m59s'),
23+
(HOUR, '1h'),
24+
(HOUR + 1, '1h0m1s'),
25+
(HOUR + MINUTE + 1, '1h1m1s'),
26+
(DAY + HOUR, '1d1h'),
27+
(DAY + HOUR + MINUTE, '1d1h1m'),
28+
(DAY + MINUTE, '1d0h1m'),
29+
(DAY + MINUTE + 1, '1d0h1m'),
30+
(WEEK + DAY + HOUR, '1w1d1h'),
31+
(WEEK + DAY + HOUR + MINUTE, '1w1d1h'),
32+
(WEEK + DAY + HOUR + SECOND, '1w1d1h'),
33+
(MONTH + WEEK + DAY, '1m1w1d'),
34+
(MONTH + WEEK + DAY + HOUR, '1m1w1d'),
35+
(YEAR + MONTH + WEEK, '1y1m1w'),
36+
(YEAR + MONTH + WEEK + DAY, '1y1m1w'),
37+
),
38+
)
39+
def test_humanize_elapsed(seconds, expected):
40+
actual = humanize_elapsed(seconds)
41+
assert actual == expected

trinity/protocol/common/trackers.py

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
Generic,
55
Optional,
66
TypeVar,
7-
Union,
87
)
98

10-
from eth_utils import ValidationError
119

1210
from p2p.protocol import (
1311
BaseRequest,
1412
)
1513

14+
from trinity.utils.ema import EMA
1615
from trinity.utils.logging import HasTraceLogger
1716
from .constants import ROUND_TRIP_TIMEOUT
1817
from .types import (
@@ -23,39 +22,6 @@
2322
TRequest = TypeVar('TRequest', bound=BaseRequest[Any])
2423

2524

26-
class EMA:
27-
"""
28-
Represents an exponential moving average.
29-
https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
30-
31-
Smoothing factor, or "alpha" of the exponential moving average.
32-
33-
- Closer to 0 gives you smoother, slower-to-update, data
34-
- Closer to 1 gives you choppier, quicker-to-update, data
35-
36-
.. note::
37-
38-
A smoothing factor of 1 would completely ignore history whereas 0 would
39-
completely ignore new data
40-
41-
42-
The initial value is the starting value for the EMA
43-
"""
44-
def __init__(self, initial_value: float, smoothing_factor: float) -> None:
45-
self._value = initial_value
46-
if 0 < smoothing_factor < 1:
47-
self._alpha = smoothing_factor
48-
else:
49-
raise ValidationError("Smoothing factor of EMA must be between 0 and 1")
50-
51-
def update(self, scalar: Union[int, float]) -> None:
52-
self._value = (self._value * (1 - self._alpha)) + (scalar * self._alpha)
53-
54-
@property
55-
def value(self) -> float:
56-
return self._value
57-
58-
5925
class BasePerformanceTracker(ABC, HasTraceLogger, Generic[TRequest, TResult]):
6026
def __init__(self) -> None:
6127
self.total_msgs = 0

trinity/sync/full/chain.py

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
PriorityQueue,
44
)
55
from concurrent.futures import CancelledError
6+
import datetime
67
import enum
78
from functools import (
89
partial,
@@ -11,6 +12,7 @@
1112
from typing import (
1213
Dict,
1314
List,
15+
NamedTuple,
1416
Set,
1517
Tuple,
1618
Type,
@@ -62,6 +64,8 @@
6264
OrderedTaskPreparation,
6365
TaskQueue,
6466
)
67+
from trinity.utils.ema import EMA
68+
from trinity.utils.humanize import humanize_elapsed
6569
from trinity.utils.timer import Timer
6670

6771
# (ReceiptBundle, (Receipt, (root_hash, receipt_trie_data))
@@ -338,6 +342,71 @@ class BlockPersistPrereqs(enum.Enum):
338342
StoreReceipts = enum.auto()
339343

340344

345+
class ChainSyncStats(NamedTuple):
346+
prev_head: BlockHeader
347+
latest_head: BlockHeader
348+
349+
elapsed: float
350+
351+
num_blocks: int
352+
blocks_per_second: float
353+
354+
num_transactions: int
355+
transactions_per_second: float
356+
357+
358+
class ChainSyncPerformanceTracker:
359+
def __init__(self, head: BlockHeader) -> None:
360+
# The `head` from the previous time we reported stats
361+
self.prev_head = head
362+
# The latest `head` we have synced
363+
self.latest_head = head
364+
365+
# A `Timer` object to report elapsed time between reports
366+
self.timer = Timer()
367+
368+
# EMA of the blocks per second
369+
self.blocks_per_second_ema = EMA(initial_value=0, smoothing_factor=0.05)
370+
371+
# EMA of the transactions per second
372+
self.transactions_per_second_ema = EMA(initial_value=0, smoothing_factor=0.05)
373+
374+
# Number of transactions processed
375+
self.num_transactions = 0
376+
377+
def record_transactions(self, count: int) -> None:
378+
self.num_transactions += count
379+
380+
def set_latest_head(self, head: BlockHeader) -> None:
381+
self.latest_head = head
382+
383+
def report(self) -> ChainSyncStats:
384+
elapsed = self.timer.pop_elapsed()
385+
386+
num_blocks = self.latest_head.block_number - self.prev_head.block_number
387+
blocks_per_second = num_blocks / elapsed
388+
transactions_per_second = self.num_transactions / elapsed
389+
390+
self.blocks_per_second_ema.update(blocks_per_second)
391+
self.transactions_per_second_ema.update(transactions_per_second)
392+
393+
stats = ChainSyncStats(
394+
prev_head=self.prev_head,
395+
latest_head=self.latest_head,
396+
elapsed=elapsed,
397+
num_blocks=num_blocks,
398+
blocks_per_second=self.blocks_per_second_ema.value,
399+
num_transactions=self.num_transactions,
400+
transactions_per_second=self.transactions_per_second_ema.value,
401+
)
402+
403+
# reset the counters
404+
self.num_transactions = 0
405+
self.prev_head = self.latest_head
406+
407+
return stats
408+
409+
341410
class FastChainSyncer(BaseBodyChainSyncer):
342411
"""
343412
Sync with the Ethereum network by fetching block headers/bodies and storing them in our DB.
@@ -374,6 +443,8 @@ def __init__(self,
374443

375444
async def _run(self) -> None:
376445
head = await self.wait(self.db.coro_get_canonical_head())
446+
self.tracker = ChainSyncPerformanceTracker(head)
447+
377448
self._block_persist_tracker.set_finished_dependency(head)
378449
self.run_daemon_task(self._launch_prerequisite_tasks())
379450
self.run_daemon_task(self._assign_receipt_download_to_peers())
@@ -445,9 +516,6 @@ async def _launch_prerequisite_tasks(self) -> None:
445516
self.header_queue.complete(batch_id, headers)
446517

447518
async def _display_stats(self) -> None:
448-
last_head = await self.wait(self.db.coro_get_canonical_head())
449-
timer = Timer()
450-
451519
while self.is_operational:
452520
await self.sleep(5)
453521
self.logger.debug(
@@ -459,16 +527,29 @@ async def _display_stats(self) -> None:
459527
)],
460528
)
461529

462-
head = await self.wait(self.db.coro_get_canonical_head())
463-
if head == last_head:
464-
continue
465-
else:
466-
block_num_change = head.block_number - last_head.block_number
467-
last_head = head
468-
469-
self.logger.info(
470-
"Advanced by %d blocks in %0.1f seconds, new head: %s",
471-
block_num_change, timer.pop_elapsed(), head)
530+
stats = self.tracker.report()
531+
utcnow = int(datetime.datetime.utcnow().timestamp())
532+
head_age = utcnow - stats.latest_head.timestamp
533+
self.logger.info(
534+
(
535+
"blks=%-4d "
536+
"txs=%-5d "
537+
"bps=%-3d "
538+
"tps=%-4d "
539+
"elapsed=%0.1f "
540+
"head=#%d (%s\u2026%s) "
541+
"age=%s"
542+
),
543+
stats.num_blocks,
544+
stats.num_transactions,
545+
stats.blocks_per_second,
546+
stats.transactions_per_second,
547+
stats.elapsed,
548+
stats.latest_head.block_number,
549+
stats.latest_head.hex_hash[2:6],
550+
stats.latest_head.hex_hash[-4:],
551+
humanize_elapsed(head_age),
552+
)
472553

473554
async def _persist_ready_blocks(self) -> None:
474555
"""
@@ -514,8 +595,12 @@ async def _persist_blocks(self, headers: Tuple[BlockHeader, ...]) -> None:
514595
tx_class = block_class.get_transaction_class()
515596
transactions = [tx_class.from_base_transaction(tx) for tx in body.transactions]
516597

598+
# record progress in the tracker
599+
self.tracker.record_transactions(len(transactions))
600+
517601
block = block_class(header, transactions, uncles)
518602
await self.wait(self.db.coro_persist_block(block))
603+
self.tracker.set_latest_head(header)
519604

520605
async def _assign_receipt_download_to_peers(self) -> None:
521606
"""

trinity/utils/ema.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import Union
2+
3+
from eth_utils import ValidationError
4+
5+
6+
class EMA:
7+
"""
8+
Represents an exponential moving average.
9+
https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
10+
11+
Smoothing factor, or "alpha" of the exponential moving average.
12+
13+
- Closer to 0 gives you smoother, slower-to-update, data
14+
- Closer to 1 gives you choppier, quicker-to-update, data
15+
16+
.. note::
17+
18+
A smoothing factor of 1 would completely ignore history whereas 0 would
19+
completely ignore new data
20+
21+
22+
The initial value is the starting value for the EMA
23+
"""
24+
def __init__(self, initial_value: float, smoothing_factor: float) -> None:
25+
self._value = initial_value
26+
if 0 < smoothing_factor < 1:
27+
self._alpha = smoothing_factor
28+
else:
29+
raise ValidationError("Smoothing factor of EMA must be between 0 and 1")
30+
31+
def update(self, scalar: Union[int, float]) -> None:
32+
self._value = (self._value * (1 - self._alpha)) + (scalar * self._alpha)
33+
34+
@property
35+
def value(self) -> float:
36+
return self._value

trinity/utils/humanize.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Iterator
2+
3+
4+
def humanize_elapsed(seconds: int) -> str:
5+
return ''.join(_humanize_elapsed(seconds))
6+
7+
8+
SECOND = 1
9+
MINUTE = 60
10+
HOUR = 60 * 60
11+
DAY = 24 * HOUR
12+
YEAR = 365 * DAY
13+
MONTH = YEAR // 12
14+
WEEK = 7 * DAY
15+
16+
17+
UNITS = (
18+
(YEAR, 'y'),
19+
(MONTH, 'm'),
20+
(WEEK, 'w'),
21+
(DAY, 'd'),
22+
(HOUR, 'h'),
23+
(MINUTE, 'm'),
24+
(SECOND, 's'),
25+
)
26+
27+
28+
def _humanize_elapsed(seconds: int) -> Iterator[str]:
29+
if not seconds:
30+
yield '0s'
31+
32+
num_display_units = 0
33+
remainder = seconds
34+
35+
for duration, unit in UNITS:
36+
if not remainder:
37+
break
38+
if remainder >= duration or num_display_units:
39+
num = remainder // duration
40+
yield f"{num}{unit}"
41+
num_display_units += 1
42+
43+
if num_display_units >= 3:
44+
return
45+
46+
remainder %= duration

0 commit comments

Comments
 (0)