Stopwatch: introduce tool for measuring time mainly for log messages

The helpers from `TimeUtils` are affected by the mock clock, which
is not desired for debug output.
This commit is contained in:
Andreas Schildbach 2023-04-01 17:11:50 +02:00
parent ff6499baab
commit 116194f39e
11 changed files with 203 additions and 26 deletions

View file

@ -0,0 +1,104 @@
package org.bitcoinj.base.internal;
/*
* Copyright by the original author or authors.
*
* 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.
*/
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.List;
import static org.bitcoinj.base.internal.Preconditions.checkArgument;
/**
* A tool for measuring time, mainly for log messages. Note this class isn't affected by the mock clock of
* {@link TimeUtils}.
*/
public class Stopwatch implements TemporalAmount {
private final Instant startTime;
private Instant stopTime = null;
/**
* Start a newly created stopwatch.
*
* @return the stopwatch that was just started
*/
public static Stopwatch start() {
return new Stopwatch();
}
private Stopwatch() {
this.startTime = Instant.now();
}
/**
* Stops the stopwatch, if it is running.
*
* @return the stopwatch that is stopped
*/
public Stopwatch stop() {
if (isRunning()) stopTime = Instant.now();
return this;
}
/**
* Returns true if the stopwatch is running.
*
* @return true if the stopwatch is running, false otherwise
*/
public boolean isRunning() {
return stopTime == null;
}
/**
* Gets the elapsed time on the watch. This doesn't stop the watch.
*
* @return elapsed time
*/
public Duration elapsed() {
return Duration.between(startTime, isRunning() ? Instant.now() : stopTime);
}
@Override
public long get(TemporalUnit temporalUnit) {
checkArgument(temporalUnit.equals(ChronoUnit.MILLIS), () -> "unsupported temporal unit: " + temporalUnit);
return elapsed().toMillis();
}
@Override
public List<TemporalUnit> getUnits() {
return Arrays.asList(ChronoUnit.MILLIS);
}
@Override
public Temporal addTo(Temporal temporal) {
return temporal.plus(elapsed());
}
@Override
public Temporal subtractFrom(Temporal temporal) {
return temporal.minus(elapsed());
}
@Override
public String toString() {
return elapsed().toMillis() + " ms";
}
}

View file

@ -72,6 +72,8 @@ public class TimeUtils {
/** /**
* Returns elapsed time between given start and current time as a Duration. * Returns elapsed time between given start and current time as a Duration.
* <p>
* Note that this method is affected by the mock clock. If you want to raise real debug data use {@link Stopwatch}.
*/ */
public static Duration elapsedTime(Instant start) { public static Duration elapsedTime(Instant start) {
return Duration.between(start, currentTime()); return Duration.between(start, currentTime());

View file

@ -27,6 +27,7 @@ import com.google.common.util.concurrent.Uninterruptibles;
import net.jcip.annotations.GuardedBy; import net.jcip.annotations.GuardedBy;
import org.bitcoinj.base.Network; import org.bitcoinj.base.Network;
import org.bitcoinj.base.internal.PlatformUtils; import org.bitcoinj.base.internal.PlatformUtils;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.core.listeners.AddressEventListener; import org.bitcoinj.core.listeners.AddressEventListener;
import org.bitcoinj.core.listeners.BlockchainDownloadEventListener; import org.bitcoinj.core.listeners.BlockchainDownloadEventListener;
@ -1074,7 +1075,7 @@ public class PeerGroup implements TransactionBroadcaster {
checkState(!lock.isHeldByCurrentThread()); checkState(!lock.isHeldByCurrentThread());
int maxPeersToDiscoverCount = this.vMaxPeersToDiscoverCount; int maxPeersToDiscoverCount = this.vMaxPeersToDiscoverCount;
Duration peerDiscoveryTimeout = this.vPeerDiscoveryTimeout; Duration peerDiscoveryTimeout = this.vPeerDiscoveryTimeout;
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
final List<PeerAddress> addressList = new LinkedList<>(); final List<PeerAddress> addressList = new LinkedList<>();
for (PeerDiscovery peerDiscovery : peerDiscoverers /* COW */) { for (PeerDiscovery peerDiscovery : peerDiscoverers /* COW */) {
List<InetSocketAddress> addresses; List<InetSocketAddress> addresses;
@ -1096,8 +1097,8 @@ public class PeerGroup implements TransactionBroadcaster {
registration.executor.execute(() -> registration.listener.onPeersDiscovered(peersDiscoveredSet)); registration.executor.execute(() -> registration.listener.onPeersDiscovered(peersDiscoveredSet));
} }
} }
log.info("Peer discovery took {} ms and returned {} items from {} discoverers", log.info("Peer discovery took {} and returned {} items from {} discoverers",
TimeUtils.elapsedTime(start).toMillis(), addressList.size(), peerDiscoverers.size()); watch, addressList.size(), peerDiscoverers.size());
return addressList.size(); return addressList.size();
} }
@ -1181,7 +1182,7 @@ public class PeerGroup implements TransactionBroadcaster {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try { try {
log.info("Stopping ..."); log.info("Stopping ...");
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
// The log output this creates can be useful. // The log output this creates can be useful.
setDownloadPeer(null); setDownloadPeer(null);
// Blocking close of all sockets. // Blocking close of all sockets.
@ -1191,7 +1192,7 @@ public class PeerGroup implements TransactionBroadcaster {
peerDiscovery.shutdown(); peerDiscovery.shutdown();
} }
vRunning = false; vRunning = false;
log.info("Stopped, took {} ms.", TimeUtils.elapsedTime(start).toMillis()); log.info("Stopped, took {}.", watch);
} catch (Throwable e) { } catch (Throwable e) {
log.error("Exception when shutting down", e); // The executor swallows exceptions :( log.error("Exception when shutting down", e); // The executor swallows exceptions :(
} }
@ -1203,11 +1204,11 @@ public class PeerGroup implements TransactionBroadcaster {
/** Does a blocking stop */ /** Does a blocking stop */
public void stop() { public void stop() {
try { try {
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
stopAsync(); stopAsync();
log.info("Awaiting PeerGroup shutdown ..."); log.info("Awaiting PeerGroup shutdown ...");
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
log.info("... took {} ms", TimeUtils.elapsedTime(start).toMillis()); log.info("... took {}", watch);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -18,6 +18,7 @@
package org.bitcoinj.crypto; package org.bitcoinj.crypto;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters; import org.bitcoinj.wallet.Protos.ScryptParameters;
@ -150,9 +151,9 @@ public class KeyCrypterScrypt implements KeyCrypter {
log.warn("You are using a ScryptParameters with no salt. Your encryption may be vulnerable to a dictionary attack."); log.warn("You are using a ScryptParameters with no salt. Your encryption may be vulnerable to a dictionary attack.");
} }
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
byte[] keyBytes = SCrypt.generate(passwordBytes, salt, (int) scryptParameters.getN(), scryptParameters.getR(), scryptParameters.getP(), KEY_LENGTH); byte[] keyBytes = SCrypt.generate(passwordBytes, salt, (int) scryptParameters.getN(), scryptParameters.getR(), scryptParameters.getP(), KEY_LENGTH);
log.info("Deriving key took {} ms for {}.", TimeUtils.elapsedTime(start).toMillis(), scryptParametersString()); log.info("Deriving key took {} for {}.", watch, scryptParametersString());
return new AesKey(keyBytes); return new AesKey(keyBytes);
} catch (Exception e) { } catch (Exception e) {
throw new KeyCrypterException("Could not generate key from password and salt.", e); throw new KeyCrypterException("Could not generate key from password and salt.", e);

View file

@ -19,6 +19,7 @@ package org.bitcoinj.crypto;
import org.bitcoinj.base.Sha256Hash; import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.base.internal.PlatformUtils; import org.bitcoinj.base.internal.PlatformUtils;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.base.internal.StreamUtils; import org.bitcoinj.base.internal.StreamUtils;
import org.bitcoinj.base.internal.InternalUtils; import org.bitcoinj.base.internal.InternalUtils;
@ -145,9 +146,9 @@ public class MnemonicCode {
String pass = InternalUtils.SPACE_JOINER.join(words); String pass = InternalUtils.SPACE_JOINER.join(words);
String salt = "mnemonic" + passphrase; String salt = "mnemonic" + passphrase;
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
byte[] seed = PBKDF2SHA512.derive(pass, salt, PBKDF2_ROUNDS, 64); byte[] seed = PBKDF2SHA512.derive(pass, salt, PBKDF2_ROUNDS, 64);
log.info("PBKDF2 took {} ms", TimeUtils.elapsedTime(start).toMillis()); log.info("PBKDF2 took {}", watch);
return seed; return seed;
} }

View file

@ -18,6 +18,7 @@
package org.bitcoinj.params; package org.bitcoinj.params;
import org.bitcoinj.base.BitcoinNetwork; import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.base.internal.ByteUtils; import org.bitcoinj.base.internal.ByteUtils;
import org.bitcoinj.core.BitcoinSerializer; import org.bitcoinj.core.BitcoinSerializer;
@ -178,7 +179,7 @@ public abstract class BitcoinNetworkParams extends NetworkParameters {
// We need to find a block far back in the chain. It's OK that this is expensive because it only occurs every // We need to find a block far back in the chain. It's OK that this is expensive because it only occurs every
// two weeks after the initial block chain download. // two weeks after the initial block chain download.
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
Sha256Hash hash = prev.getHash(); Sha256Hash hash = prev.getHash();
StoredBlock cursor = null; StoredBlock cursor = null;
final int interval = this.getInterval(); final int interval = this.getInterval();
@ -193,9 +194,9 @@ public abstract class BitcoinNetworkParams extends NetworkParameters {
} }
checkState(cursor != null && isDifficultyTransitionPoint(cursor.getHeight() - 1), () -> checkState(cursor != null && isDifficultyTransitionPoint(cursor.getHeight() - 1), () ->
"didn't arrive at a transition point"); "didn't arrive at a transition point");
Duration elapsed = TimeUtils.elapsedTime(start); watch.stop();
if (elapsed.toMillis() > 50) if (watch.elapsed().toMillis() > 50)
log.info("Difficulty transition traversal took {} ms", elapsed.toMillis()); log.info("Difficulty transition traversal took {}", watch);
Block blockIntervalAgo = cursor.getHeader(); Block blockIntervalAgo = cursor.getHeader();
int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds()); int timespan = (int) (prev.getTimeSeconds() - blockIntervalAgo.getTimeSeconds());

View file

@ -20,6 +20,7 @@ import com.google.common.base.MoreObjects;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.bitcoinj.base.Network; import org.bitcoinj.base.Network;
import org.bitcoinj.base.ScriptType; import org.bitcoinj.base.ScriptType;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.crypto.AesKey; import org.bitcoinj.crypto.AesKey;
import org.bitcoinj.base.internal.ByteUtils; import org.bitcoinj.base.internal.ByteUtils;
@ -1272,12 +1273,12 @@ public class DeterministicKeyChain implements EncryptableKeyChain {
log.info("{} keys needed for {} = {} issued + {} lookahead size + {} lookahead threshold - {} num children", log.info("{} keys needed for {} = {} issued + {} lookahead size + {} lookahead threshold - {} num children",
limit, parent.getPathAsString(), issued, lookaheadSize, lookaheadThreshold, numChildren); limit, parent.getPathAsString(), issued, lookaheadSize, lookaheadThreshold, numChildren);
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
List<DeterministicKey> result = HDKeyDerivation.generate(parent, numChildren) List<DeterministicKey> result = HDKeyDerivation.generate(parent, numChildren)
.limit(limit) .limit(limit)
.map(DeterministicKey::dropPrivateBytes) .map(DeterministicKey::dropPrivateBytes)
.collect(StreamUtils.toUnmodifiableList()); .collect(StreamUtils.toUnmodifiableList());
log.info("Took {} ms", TimeUtils.elapsedTime(start).toMillis()); log.info("Took {}", watch);
return result; return result;
} }

View file

@ -17,6 +17,7 @@
package org.bitcoinj.wallet; package org.bitcoinj.wallet;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.utils.ContextPropagatingThreadFactory; import org.bitcoinj.utils.ContextPropagatingThreadFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -137,7 +138,7 @@ public class WalletFiles {
} }
private void saveNowInternal() throws IOException { private void saveNowInternal() throws IOException {
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
File directory = file.getAbsoluteFile().getParentFile(); File directory = file.getAbsoluteFile().getParentFile();
if (!directory.exists()) { if (!directory.exists()) {
throw new FileNotFoundException(directory.getPath() + " (wallet directory not found)"); throw new FileNotFoundException(directory.getPath() + " (wallet directory not found)");
@ -149,7 +150,7 @@ public class WalletFiles {
wallet.saveToFile(temp, file); wallet.saveToFile(temp, file);
if (listener != null) if (listener != null)
listener.onAfterAutoSave(file); listener.onAfterAutoSave(file);
log.info("Save completed in {} ms", TimeUtils.elapsedTime(start).toMillis()); log.info("Save completed in {}", watch);
} }
/** Queues up a save in the background. Useful for not very important wallet changes. */ /** Queues up a save in the background. Useful for not very important wallet changes. */

View file

@ -0,0 +1,63 @@
/*
* Copyright by the original author or authors.
*
* 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 org.bitcoinj.base.internal;
import org.junit.Before;
import org.junit.Test;
import java.time.Duration;
import java.time.Instant;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class StopwatchTest {
private Stopwatch stopwatch;
@Before
public void setUp() {
stopwatch = Stopwatch.start();
}
@Test
public void toString_() {
stopwatch.toString();
}
@Test
public void stop() {
stopwatch.stop();
}
@Test
public void addSubstract() {
Instant i1 = Instant.now();
Instant i2 = i1.plus(stopwatch.stop());
Instant i3 = i2.minus(stopwatch);
assertEquals(i1, i3);
assertTrue(i2.compareTo(i1) >= 0);
assertTrue(i3.compareTo(i2) <= 0);
}
@Test
public void compareTo() {
Duration hour = Duration.ofHours(1);
assertTrue(stopwatch.elapsed().compareTo(hour) < 0);
assertTrue(hour.compareTo(stopwatch.elapsed()) > 0);
}
}

View file

@ -21,6 +21,7 @@ import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.base.ScriptType; import org.bitcoinj.base.ScriptType;
import org.bitcoinj.base.Address; import org.bitcoinj.base.Address;
import org.bitcoinj.base.internal.PlatformUtils; import org.bitcoinj.base.internal.PlatformUtils;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.core.Block; import org.bitcoinj.core.Block;
import org.bitcoinj.core.Context; import org.bitcoinj.core.Context;
@ -146,7 +147,7 @@ public class SPVBlockStoreTest {
final int ITERATIONS = 100000; final int ITERATIONS = 100000;
final Duration THRESHOLD = Duration.ofSeconds(5); final Duration THRESHOLD = Duration.ofSeconds(5);
SPVBlockStore store = new SPVBlockStore(TESTNET, blockStoreFile); SPVBlockStore store = new SPVBlockStore(TESTNET, blockStoreFile);
Instant start = TimeUtils.currentTime(); Stopwatch watch = Stopwatch.start();
for (int i = 0; i < ITERATIONS; i++) { for (int i = 0; i < ITERATIONS; i++) {
// Using i as the nonce so that the block hashes are different. // Using i as the nonce so that the block hashes are different.
Block block = new Block(TESTNET, 0, Sha256Hash.ZERO_HASH, Sha256Hash.ZERO_HASH, 0, 0, i, Block block = new Block(TESTNET, 0, Sha256Hash.ZERO_HASH, Sha256Hash.ZERO_HASH, 0, 0, i,
@ -155,9 +156,9 @@ public class SPVBlockStoreTest {
store.put(b); store.put(b);
store.setChainHead(b); store.setChainHead(b);
} }
Duration elapsed = TimeUtils.elapsedTime(start); watch.stop();
assertTrue("took " + elapsed.toMillis() + " ms for " + ITERATIONS + " iterations", assertTrue("took " + watch + " for " + ITERATIONS + " iterations",
elapsed.compareTo(THRESHOLD) < 0); watch.elapsed().compareTo(THRESHOLD) < 0);
store.close(); store.close();
} }

View file

@ -22,6 +22,7 @@ import org.bitcoinj.base.Address;
import org.bitcoinj.base.Coin; import org.bitcoinj.base.Coin;
import org.bitcoinj.base.ScriptType; import org.bitcoinj.base.ScriptType;
import org.bitcoinj.base.Sha256Hash; import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.base.internal.Stopwatch;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.core.listeners.DownloadProgressTracker; import org.bitcoinj.core.listeners.DownloadProgressTracker;
import org.bitcoinj.core.listeners.PeerConnectedEventListener; import org.bitcoinj.core.listeners.PeerConnectedEventListener;
@ -535,7 +536,7 @@ public class PeerGroupTest extends TestWithPeerGroup {
peerGroup.addConnectedEventListener(Threading.SAME_THREAD, (peer, peerCount) -> peerConnectedFuture.complete(null)); peerGroup.addConnectedEventListener(Threading.SAME_THREAD, (peer, peerCount) -> peerConnectedFuture.complete(null));
peerGroup.addDisconnectedEventListener(Threading.SAME_THREAD, (peer, peerCount) -> peerDisconnectedFuture.complete(null)); peerGroup.addDisconnectedEventListener(Threading.SAME_THREAD, (peer, peerCount) -> peerDisconnectedFuture.complete(null));
// connect to peer but don't do handshake // connect to peer but don't do handshake
Instant start = TimeUtils.currentTime(); // before connection so we don't get elapsed < timeout Stopwatch watch = Stopwatch.start(); // before connection so we don't get elapsed < timeout
connectPeerWithoutVersionExchange(0); connectPeerWithoutVersionExchange(0);
// wait for disconnect (plus a bit more, in case test server is overloaded) // wait for disconnect (plus a bit more, in case test server is overloaded)
try { try {
@ -545,8 +546,8 @@ public class PeerGroupTest extends TestWithPeerGroup {
} }
// check things after disconnect // check things after disconnect
assertFalse(peerConnectedFuture.isDone()); // should never have connected assertFalse(peerConnectedFuture.isDone()); // should never have connected
Duration elapsed = TimeUtils.elapsedTime(start); watch.stop();
assertTrue(elapsed.toMillis() + " ms",elapsed.compareTo(timeout) >= 0); // should not disconnect before timeout assertTrue(watch.toString(), watch.elapsed().compareTo(timeout) >= 0); // should not disconnect before timeout
assertTrue(peerDisconnectedFuture.isDone()); // but should disconnect eventually assertTrue(peerDisconnectedFuture.isDone()); // but should disconnect eventually
} }