Repository: coinbase/rosetta-cli
Pull request #142 (2020-09-29 06:22:18)

[check:construction] Wait to start check:construction until reaching tip
Closes: https://github.com/coinbase/rosetta-cli/issues/100

This PR delays the start of `check:construction` until an implementation is at tip. This prevents the `rosetta-cli` from syncing a ton of blocks unnecessarily.

### Logging Changes
#### Progress
![image](https://user-images.githubusercontent.com/13023275/94596132-953e9080-0240-11eb-86b1-da75ded4929d.png)
#### Waiting to Start
![image](https://user-images.githubusercontent.com/13023275/94598897-8528b000-0244-11eb-9518-861638f0d43b.png)

### Changes
- [x] Update to new v0.4.8 release of rosetta-sdk-go
- [x] Log memory usage periodically
- [x] Add `waitForTip` method to wait to begin syncing until we are at tip
- [x] use float64 for blocks per second (we could easily be less than 1)
- [x] check wait for tip status before sleeping the first time (right now we always wait 10 seconds)
- [x] Update version


Patch:
diff --git a/cmd/check_construction.go b/cmd/check_construction.go
index 09c15af..6f4ded1 100644
--- a/cmd/check_construction.go
+++ b/cmd/check_construction.go
@@ -131,6 +131,10 @@ func runCheckConstructionCmd(cmd *cobra.Command, args []string) {
 		return constructionTester.WatchEndConditions(ctx)
 	})
 
+	g.Go(func() error {
+		return tester.LogMemoryLoop(ctx)
+	})
+
 	sigListeners := []context.CancelFunc{cancel}
 	go handleSignals(&sigListeners)
 
diff --git a/cmd/check_data.go b/cmd/check_data.go
index 774ee65..9f7b374 100644
--- a/cmd/check_data.go
+++ b/cmd/check_data.go
@@ -141,6 +141,14 @@ func runCheckDataCmd(cmd *cobra.Command, args []string) {
 		return dataTester.WatchEndConditions(ctx)
 	})
 
+	g.Go(func() error {
+		return tester.LogMemoryLoop(ctx)
+	})
+
+	g.Go(func() error {
+		return dataTester.StartProgressLogger(ctx)
+	})
+
 	sigListeners := []context.CancelFunc{cancel}
 	go handleSignals(&sigListeners)
 
diff --git a/cmd/root.go b/cmd/root.go
index c03681e..33c1fd1 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -132,6 +132,6 @@ var versionCmd = &cobra.Command{
 	Use:   "version",
 	Short: "Print rosetta-cli version",
 	Run: func(cmd *cobra.Command, args []string) {
-		fmt.Println("v0.5.4")
+		fmt.Println("v0.5.5")
 	},
 }
diff --git a/go.mod b/go.mod
index 796d8f2..823653e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/coinbase/rosetta-cli
 go 1.13
 
 require (
-	github.com/coinbase/rosetta-sdk-go v0.4.6
+	github.com/coinbase/rosetta-sdk-go v0.4.8
 	github.com/fatih/color v1.9.0
 	github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
 	github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c
diff --git a/go.sum b/go.sum
index ff156c1..a2849f9 100644
--- a/go.sum
+++ b/go.sum
@@ -13,6 +13,7 @@ github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN
 github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
 github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
@@ -155,6 +156,10 @@ github.com/coinbase/rosetta-sdk-go v0.4.5 h1:5Z+25mm/J1SStzSdTp5e8dlEMtCZRBZdOaT
 github.com/coinbase/rosetta-sdk-go v0.4.5/go.mod h1:Luv0AhzZH81eul2hYZ3w0hBGwmFPiexwbntYxihEZck=
 github.com/coinbase/rosetta-sdk-go v0.4.6 h1:zX2SLmBF1oFbK0c4QCMwkwcHN9VjenFfIt0Dr8k8JGY=
 github.com/coinbase/rosetta-sdk-go v0.4.6/go.mod h1:Luv0AhzZH81eul2hYZ3w0hBGwmFPiexwbntYxihEZck=
+github.com/coinbase/rosetta-sdk-go v0.4.7 h1:5KFc0CgLMkKamX++hYUFvE58a5/tCn0wSqpcTnDhRhY=
+github.com/coinbase/rosetta-sdk-go v0.4.7/go.mod h1:8d4iN4VSGvLUzl+jRQlvYSLyS9TeY0QZebneWouizqU=
+github.com/coinbase/rosetta-sdk-go v0.4.8 h1:+E1TM4q1c5/x/jE9FPI1IZIbNvUWy4tRRgDPLnKzUV4=
+github.com/coinbase/rosetta-sdk-go v0.4.8/go.mod h1:8d4iN4VSGvLUzl+jRQlvYSLyS9TeY0QZebneWouizqU=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -198,6 +203,7 @@ github.com/dop251/goja v0.0.0-20200219165308-d1232e640a87/go.mod h1:Mw6PkjjMXWbT
 github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
 github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -207,6 +213,8 @@ github.com/ethereum/go-ethereum v1.9.20 h1:kk/J5OIoaoz3DRrCXznz3RGi212mHHXwzXlY/
 github.com/ethereum/go-ethereum v1.9.20/go.mod h1:JSSTypSMTkGZtAdAChH2wP5dZEvPGh3nUTuDpH+hNrg=
 github.com/ethereum/go-ethereum v1.9.21 h1:8qRlhzrItnmUGdVlBzZLI2Tb46S0RdSNjFwICo781ws=
 github.com/ethereum/go-ethereum v1.9.21/go.mod h1:RXAVzbGrSGmDkDnHymruTAIEjUR3E4TX0EOpaj702sI=
+github.com/ethereum/go-ethereum v1.9.22 h1:/Fea9n2EWJuNJ9oahMq9luqjRBcbW7QWdThbcJl13ek=
+github.com/ethereum/go-ethereum v1.9.22/go.mod h1:FQjK3ZwD8C5DYn7ukTmFee36rq1dOMESiUfXr5RUc1w=
 github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
@@ -249,6 +257,7 @@ github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26 h1:lMm2hD9Fy0ynom5
 github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/addlicense v0.0.0-20200817051935-6f4cd4aacc89 h1:oTppmscIAQ2Y1tcsMDcTLR3z4MN/96/pvIsBSLGl7o8=
 github.com/google/addlicense v0.0.0-20200817051935-6f4cd4aacc89/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
+github.com/google/addlicense v0.0.0-20200827091314-d1655b921368 h1:Ds6gDZHoviaQM7r7oMx/cG2qwZc3l5u7cg6gTkxOZNE=
 github.com/google/addlicense v0.0.0-20200827091314-d1655b921368/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -260,6 +269,7 @@ github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
@@ -480,11 +490,19 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -560,11 +578,13 @@ golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03 h1:4gtJXHJ9ud0q8MNSDxJsRU/WH+afypbe4Vk4zq+8qow=
 golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200729181040-64cdafbe085c/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index 2d65bf8..343b7b6 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -110,17 +110,6 @@ func (l *Logger) LogDataStats(ctx context.Context) error {
 		return nil
 	}
 
-	elapsedTime, err := l.CounterStorage.Get(ctx, TimeElapsedCounter)
-	if err != nil {
-		return fmt.Errorf("%w cannot get elapsed time", err)
-	}
-
-	if elapsedTime.Sign() == 0 { // wait for at least some elapsed time
-		return nil
-	}
-
-	blocksPerSecond := new(big.Int).Div(blocks, elapsedTime)
-
 	orphans, err := l.CounterStorage.Get(ctx, storage.OrphanCounter)
 	if err != nil {
 		return fmt.Errorf("%w cannot get orphan counter", err)
@@ -147,10 +136,9 @@ func (l *Logger) LogDataStats(ctx context.Context) error {
 	}
 
 	statsMessage := fmt.Sprintf(
-		"[STATS] Blocks: %s (Orphaned: %s, Rate: %s/sec) Transactions: %s Operations: %s",
+		"[STATS] Blocks: %s (Orphaned: %s) Transactions: %s Operations: %s",
 		blocks.String(),
 		orphans.String(),
-		blocksPerSecond.String(),
 		txs.String(),
 		ops.String(),
 	)
@@ -181,6 +169,54 @@ func (l *Logger) LogDataStats(ctx context.Context) error {
 	return nil
 }
 
+// LogTipEstimate logs information about the remaining blocks to sync.
+func (l *Logger) LogTipEstimate(ctx context.Context, tipIndex int64) error {
+	blocks, err := l.CounterStorage.Get(ctx, storage.BlockCounter)
+	if err != nil {
+		return fmt.Errorf("%w cannot get block counter", err)
+	}
+
+	if blocks.Sign() == 0 { // wait for at least 1 block to be processed
+		return nil
+	}
+
+	orphans, err := l.CounterStorage.Get(ctx, storage.OrphanCounter)
+	if err != nil {
+		return fmt.Errorf("%w cannot get orphan counter", err)
+	}
+
+	adjustedBlocks := blocks.Int64() - orphans.Int64()
+	if tipIndex-adjustedBlocks <= 0 { // return if no blocks to sync
+		return nil
+	}
+
+	elapsedTime, err := l.CounterStorage.Get(ctx, TimeElapsedCounter)
+	if err != nil {
+		return fmt.Errorf("%w cannot get elapsed time", err)
+	}
+
+	if elapsedTime.Sign() == 0 { // wait for at least some elapsed time
+		return nil
+	}
+
+	blocksPerSecond := new(big.Float).Quo(new(big.Float).SetInt64(adjustedBlocks), new(big.Float).SetInt(elapsedTime))
+	blocksPerSecondFloat, _ := blocksPerSecond.Float64()
+	blocksSynced := new(big.Float).Quo(new(big.Float).SetInt64(adjustedBlocks), new(big.Float).SetInt64(tipIndex))
+	blocksSyncedFloat, _ := blocksSynced.Float64()
+
+	statsMessage := fmt.Sprintf(
+		"[PROGRESS] Blocks Synced: %d/%d (Completed: %f%%, Rate: %f/second) Time Remaining: %s",
+		adjustedBlocks,
+		tipIndex,
+		blocksSyncedFloat*utils.OneHundred,
+		blocksPerSecondFloat,
+		utils.TimeToTip(blocksPerSecondFloat, adjustedBlocks, tipIndex),
+	)
+
+	color.Cyan(statsMessage)
+	return nil
+}
+
 // LogConstructionStats logs all construction values in CounterStorage.
 func (l *Logger) LogConstructionStats(ctx context.Context, inflightTransactions int) error {
 	transactionsCreated, err := l.CounterStorage.Get(ctx, storage.TransactionsCreatedCounter)
@@ -227,6 +263,20 @@ func (l *Logger) LogConstructionStats(ctx context.Context, inflightTransactions
 	return nil
 }
 
+// LogMemoryStats logs memory usage information.
+func LogMemoryStats(ctx context.Context) {
+	memUsage := utils.MonitorMemoryUsage(ctx, -1)
+	statsMessage := fmt.Sprintf(
+		"[MEMORY] Heap: %fMB Stack: %fMB System: %fMB GCs: %d",
+		memUsage.Heap,
+		memUsage.Stack,
+		memUsage.System,
+		memUsage.GarbageCollections,
+	)
+
+	color.Cyan(statsMessage)
+}
+
 // AddBlockStream writes the next processed block to the end of the
 // blockStreamFile output file.
 func (l *Logger) AddBlockStream(


Test patch:
diff --git a/pkg/tester/construction.go b/pkg/tester/construction.go
index 20289fa..75ac867 100644
--- a/pkg/tester/construction.go
+++ b/pkg/tester/construction.go
@@ -44,6 +44,7 @@ const (
 	constructionCmdName = "check-construction"
 
 	endConditionsCheckInterval = 10 * time.Second
+	tipWaitInterval            = 10 * time.Second
 )
 
 // ConstructionTester coordinates the `check:construction` test.
@@ -283,6 +284,47 @@ func (t *ConstructionTester) StartPeriodicLogger(
 	}
 }
 
+func (t *ConstructionTester) checkTip(ctx context.Context) (int64, error) {
+	status, fetchErr := t.onlineFetcher.NetworkStatusRetry(ctx, t.network, nil)
+	if fetchErr != nil {
+		return -1, fmt.Errorf("%w: unable to fetch network status", fetchErr.Err)
+	}
+
+	// If a block has yet to be synced, start syncing from tip.
+	if utils.AtTip(t.config.TipDelay, status.CurrentBlockTimestamp) {
+		return status.CurrentBlockIdentifier.Index, nil
+	}
+
+	return -1, nil
+}
+
+// waitForTip loops until the Rosetta implementation is at tip.
+func (t *ConstructionTester) waitForTip(ctx context.Context) (int64, error) {
+	tc := time.NewTicker(tipWaitInterval)
+	defer tc.Stop()
+
+	for {
+		// Don't wait any time before first tick if at tip.
+		tipIndex, err := t.checkTip(ctx)
+		if err != nil {
+			return -1, err
+		}
+
+		if tipIndex != -1 {
+			return tipIndex, nil
+		}
+
+		log.Println("waiting for implementation to reach tip before testing...")
+
+		select {
+		case <-ctx.Done():
+			return -1, ctx.Err()
+		case <-tc.C:
+			continue
+		}
+	}
+}
+
 // StartSyncer uses the tester's stateful syncer
 // to compute balance changes and track transactions
 // for confirmation on-chain.
@@ -293,14 +335,12 @@ func (t *ConstructionTester) StartSyncer(
 	startIndex := int64(-1)
 	_, err := t.blockStorage.GetHeadBlockIdentifier(ctx)
 	if errors.Is(err, storage.ErrHeadBlockNotFound) {
-		// If a block has yet to be synced, start syncing from tip.
-		// TODO: make configurable
-		status, fetchErr := t.onlineFetcher.NetworkStatusRetry(ctx, t.network, nil)
-		if fetchErr != nil {
-			return fmt.Errorf("%w: unable to fetch network status", fetchErr.Err)
+		// If no head block exists, ensure we are at tip before starting. Otherwise,
+		// we will unnecessarily sync tons of blocks before reaching any that matter.
+		startIndex, err = t.waitForTip(ctx)
+		if err != nil {
+			return fmt.Errorf("%w: unable to wait for tip", err)
 		}
-
-		startIndex = status.CurrentBlockIdentifier.Index
 	} else if err != nil {
 		return fmt.Errorf("%w: unable to get last block synced", err)
 	}
diff --git a/pkg/tester/construction_results.go b/pkg/tester/construction_results.go
index 55ff54b..00b26c7 100644
--- a/pkg/tester/construction_results.go
+++ b/pkg/tester/construction_results.go
@@ -240,8 +240,10 @@ func ExitConstruction(
 		counterStorage,
 		jobStorage,
 	)
-	results.Print()
-	results.Output(config.Construction.ResultsOutputFile)
+	if results != nil {
+		results.Print()
+		results.Output(config.Construction.ResultsOutputFile)
+	}
 
 	os.Exit(status)
 }
diff --git a/pkg/tester/data.go b/pkg/tester/data.go
index 8387f72..1eab0aa 100644
--- a/pkg/tester/data.go
+++ b/pkg/tester/data.go
@@ -355,6 +355,24 @@ func (t *DataTester) StartPeriodicLogger(
 			// Print stats one last time before exiting
 			_ = t.logger.LogDataStats(ctx)
 
+			return ctx.Err()
+		case <-tc.C:
+			_ = t.logger.LogDataStats(ctx)
+		}
+	}
+}
+
+// StartProgressLogger priunts out periodic
+// estimates of sync duration if we are behind tip.
+func (t *DataTester) StartProgressLogger(
+	ctx context.Context,
+) error {
+	tc := time.NewTicker(PeriodicLoggingFrequency)
+	defer tc.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
 			return ctx.Err()
 		case <-tc.C:
 			// Update the elapsed time in counter storage so that
@@ -364,7 +382,14 @@ func (t *DataTester) StartPeriodicLogger(
 				logger.TimeElapsedCounter,
 				big.NewInt(periodicLoggingSeconds),
 			)
-			_ = t.logger.LogDataStats(ctx)
+
+			status, fetchErr := t.fetcher.NetworkStatusRetry(ctx, t.network, nil)
+			if fetchErr != nil {
+				log.Printf("%v: unable to get network status\n", fetchErr.Err)
+				continue
+			}
+
+			_ = t.logger.LogTipEstimate(ctx, status.CurrentBlockIdentifier.Index)
 		}
 	}
 }
diff --git a/pkg/tester/data_results.go b/pkg/tester/data_results.go
index 06252da..09b8d4c 100644
--- a/pkg/tester/data_results.go
+++ b/pkg/tester/data_results.go
@@ -471,8 +471,10 @@ func ExitData(
 		endCondition,
 		endConditionDetail,
 	)
-	results.Print()
-	results.Output(config.Data.ResultsOutputFile)
+	if results != nil {
+		results.Print()
+		results.Output(config.Data.ResultsOutputFile)
+	}
 
 	os.Exit(status)
 }
diff --git a/pkg/tester/general.go b/pkg/tester/general.go
new file mode 100644
index 0000000..ae636ca
--- /dev/null
+++ b/pkg/tester/general.go
@@ -0,0 +1,46 @@
+// Copyright 2020 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tester
+
+import (
+	"context"
+	"time"
+
+	"github.com/coinbase/rosetta-cli/pkg/logger"
+)
+
+const (
+	// MemoryLoggingFrequency is the frequency that memory
+	// usage stats are logged to the terminal.
+	MemoryLoggingFrequency = 10 * time.Second
+)
+
+// LogMemoryLoop runs a loop that logs memory usage.
+func LogMemoryLoop(
+	ctx context.Context,
+) error {
+	ticker := time.NewTicker(MemoryLoggingFrequency)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			logger.LogMemoryStats(ctx)
+			return ctx.Err()
+		case <-ticker.C:
+			logger.LogMemoryStats(ctx)
+		}
+	}
+}