diff --git a/src/brs/Block.java b/src/brs/Block.java index 84bcf08be..f51de6cec 100644 --- a/src/brs/Block.java +++ b/src/brs/Block.java @@ -24,10 +24,9 @@ //TODO: Create JavaDocs and remove this @SuppressWarnings({ "checkstyle:MissingJavadocTypeCheck", "checkstyle:MissingJavadocMethodCheck" }) - public class Block { - private static final Logger logger = LoggerFactory.getLogger(Block.class); + private final int version; private final int timestamp; private final long previousBlockId; @@ -41,11 +40,14 @@ public class Block { private final byte[] generationSignature; private final byte[] payloadHash; private final AtomicReference> blockTransactions = new AtomicReference<>(); - + private final AtomicReference> allBlockTransactions = new AtomicReference<>(); + private final AtomicReference cachedBytes = new AtomicReference<>(); + private final AtomicReference cachedJsonObject = new AtomicReference<>(); + private List atTransactions = new ArrayList<>(); + private List subscriptionTransactions = new ArrayList<>(); + private List escrowTransactions = new ArrayList<>(); private byte[] blockSignature; - private BigInteger cumulativeDifficulty = BigInteger.ZERO; - private long baseTarget = Constants.INITIAL_BASE_TARGET; private final AtomicLong nextBlockId = new AtomicLong(); private int height = -1; @@ -53,12 +55,9 @@ public class Block { private final AtomicReference stringId = new AtomicReference<>(); private final AtomicLong generatorId = new AtomicLong(); private long nonce; - private BigInteger pocTime = null; private long commitment = 0L; - private final byte[] blockAts; - private Peer downloadedFrom = null; private int byteLength = 0; @@ -82,7 +81,6 @@ public class Block { int height, long baseTarget) throws SignumException.ValidationException { - if (payloadLength > Signum.getFluxCapacitor().getValue( FluxValues.MAX_PAYLOAD_LENGTH, height) || payloadLength < 0) { @@ -91,7 +89,6 @@ public class Block { + payloadLength + " height " + height + "previd " + previousBlockId); } - this.version = version; this.timestamp = timestamp; this.previousBlockId = previousBlockId; @@ -104,7 +101,6 @@ public class Block { this.generatorPublicKey = generatorPublicKey; this.generationSignature = generationSignature; this.blockSignature = blockSignature; - this.previousBlockHash = previousBlockHash; if (transactions != null) { this.blockTransactions.set(Collections.unmodifiableList(transactions)); @@ -151,7 +147,6 @@ public Block( long nonce, byte[] blockAts) throws SignumException.ValidationException { - this( version, timestamp, @@ -171,7 +166,6 @@ public Block( blockAts, height, baseTarget); - this.cumulativeDifficulty = cumulativeDifficulty == null ? BigInteger.ZERO : cumulativeDifficulty; @@ -261,17 +255,59 @@ public byte[] getBlockSignature() { } public List getTransactions() { - if (blockTransactions.get() == null) { - this.blockTransactions - .set(Collections.unmodifiableList( - transactionDb().findBlockTransactions(getId(), true))); - this.blockTransactions.get().forEach(transaction -> transaction.setBlock(this)); + List transactions = blockTransactions.get(); + if (transactions == null) { + synchronized (this) { + transactions = blockTransactions.get(); + if (transactions == null) { + List newTransactions = transactionDb().findBlockTransactions(getId(), true); + newTransactions.forEach(transaction -> transaction.setBlock(this)); + transactions = Collections.unmodifiableList(newTransactions); + blockTransactions.set(transactions); + } + } } - return blockTransactions.get(); + return transactions; } public List getAllTransactions() { - return Collections.unmodifiableList(transactionDb().findBlockTransactions(getId(), false)); + List transactions = allBlockTransactions.get(); + if (transactions == null) { + synchronized (this) { + transactions = allBlockTransactions.get(); + if (transactions == null) { + List newTransactions = transactionDb().findBlockTransactions(getId(), false); + newTransactions.forEach(transaction -> transaction.setBlock(this)); + transactions = Collections.unmodifiableList(newTransactions); + allBlockTransactions.set(transactions); + } + } + } + return transactions; + } + + public void setAtTransactions(List transactions) { + this.atTransactions = transactions; + } + + public List getAtTransactions() { + return Collections.unmodifiableList(this.atTransactions); + } + + public void setSubscriptionTransactions(List transactions) { + this.subscriptionTransactions = transactions; + } + + public List getSubscriptionTransactions() { + return Collections.unmodifiableList(this.subscriptionTransactions); + } + + public void setEscrowTransactions(List transactions) { + this.escrowTransactions = transactions; + } + + public List getEscrowTransactions() { + return Collections.unmodifiableList(this.escrowTransactions); } public long getBaseTarget() { @@ -367,29 +403,36 @@ public int hashCode() { } public JsonObject getJsonObject() { - JsonObject json = new JsonObject(); - json.addProperty("version", version); - json.addProperty("timestamp", timestamp); - json.addProperty("previousBlock", Convert.toUnsignedLong(previousBlockId)); - json.addProperty("totalAmountNQT", totalAmountNqt); - json.addProperty("totalFeeNQT", totalFeeNqt); - json.addProperty("totalFeeCashBackNQT", totalFeeCashBackNqt); - json.addProperty("totalFeeBurntNQT", totalFeeBurntNqt); - json.addProperty("payloadLength", payloadLength); - json.addProperty("payloadHash", Convert.toHexString(payloadHash)); - json.addProperty("generatorPublicKey", Convert.toHexString(generatorPublicKey)); - json.addProperty("generationSignature", Convert.toHexString(generationSignature)); - if (version > 1) { - json.addProperty("previousBlockHash", Convert.toHexString(previousBlockHash)); + if (cachedJsonObject.get() == null) { + synchronized (this) { + if (cachedJsonObject.get() == null) { + JsonObject json = new JsonObject(); + json.addProperty("version", version); + json.addProperty("timestamp", timestamp); + json.addProperty("previousBlock", Convert.toUnsignedLong(previousBlockId)); + json.addProperty("totalAmountNQT", totalAmountNqt); + json.addProperty("totalFeeNQT", totalFeeNqt); + json.addProperty("totalFeeCashBackNQT", totalFeeCashBackNqt); + json.addProperty("totalFeeBurntNQT", totalFeeBurntNqt); + json.addProperty("payloadLength", payloadLength); + json.addProperty("payloadHash", Convert.toHexString(payloadHash)); + json.addProperty("generatorPublicKey", Convert.toHexString(generatorPublicKey)); + json.addProperty("generationSignature", Convert.toHexString(generationSignature)); + if (version > 1) { + json.addProperty("previousBlockHash", Convert.toHexString(previousBlockHash)); + } + json.addProperty("blockSignature", Convert.toHexString(blockSignature)); + JsonArray transactionsData = new JsonArray(); + getTransactions().forEach(transaction -> transactionsData.add(transaction.getJsonObject())); + json.add("transactions", transactionsData); + json.addProperty("nonce", Convert.toUnsignedLong(nonce)); + json.addProperty("baseTarget", Convert.toUnsignedLong(baseTarget)); + json.addProperty("blockATs", Convert.toHexString(blockAts)); + cachedJsonObject.set(json); + } + } } - json.addProperty("blockSignature", Convert.toHexString(blockSignature)); - JsonArray transactionsData = new JsonArray(); - getTransactions().forEach(transaction -> transactionsData.add(transaction.getJsonObject())); - json.add("transactions", transactionsData); - json.addProperty("nonce", Convert.toUnsignedLong(nonce)); - json.addProperty("baseTarget", Convert.toUnsignedLong(baseTarget)); - json.addProperty("blockATs", Convert.toHexString(blockAts)); - return json; + return cachedJsonObject.get(); } // TODO: See about removing this check suppression: @@ -425,15 +468,12 @@ static Block parseBlock(JsonObject blockData, int height) long nonce = Convert.parseUnsignedLong(JSON.getAsString(blockData.get("nonce"))); long baseTarget = Convert.parseUnsignedLong( JSON.getAsString(blockData.get("baseTarget"))); - if (Signum.getFluxCapacitor().getValue( FluxValues.POC_PLUS, height) && baseTarget == 0L) { throw new SignumException.NotValidException("Block received without a baseTarget"); } - SortedMap blockTransactions = new TreeMap<>(); JsonArray transactionsData = JSON.getAsJsonArray(blockData.get("transactions")); - for (JsonElement transactionData : transactionsData) { Transaction transaction = Transaction.parseTransaction( JSON.getAsJsonObject(transactionData), height); @@ -443,7 +483,6 @@ static Block parseBlock(JsonObject blockData, int height) "Block contains duplicate transactions: " + transaction.getStringId()); } } - byte[] blockAts = Convert.parseHexString(JSON.getAsString(blockData.get("blockATs"))); return new Block( version, @@ -473,6 +512,28 @@ static Block parseBlock(JsonObject blockData, int height) } public byte[] getBytes() { + if (cachedBytes.get() == null) { + synchronized (this) { + if (cachedBytes.get() == null) { + byte[] unsignedBytes = getUnsignedBytes(); + ByteBuffer buffer = ByteBuffer.allocate(unsignedBytes.length + blockSignature.length); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put(unsignedBytes); + if (buffer.limit() - buffer.position() < blockSignature.length) { + logger.error("Something is too large here " + + "- buffer should have {} bytes left but only has {}", + blockSignature.length, + (buffer.limit() - buffer.position())); + } + buffer.put(blockSignature); + cachedBytes.set(buffer.array()); + } + } + } + return cachedBytes.get(); + } + + byte[] getUnsignedBytes() { ByteBuffer buffer = ByteBuffer.allocate( 4 + 4 @@ -484,8 +545,7 @@ public byte[] getBytes() { + 32 + (32 + 32) + 8 - + (blockAts != null ? blockAts.length : 0) - + 64); + + (blockAts != null ? blockAts.length : 0)); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(version); buffer.putInt(timestamp); @@ -509,25 +569,35 @@ public byte[] getBytes() { if (blockAts != null) { buffer.put(blockAts); } - if (buffer.limit() - buffer.position() < blockSignature.length) { - logger.error("Something is too large here " - + "- buffer should have {} bytes left but only has {}", - blockSignature.length, - (buffer.limit() - buffer.position())); - } - buffer.put(blockSignature); return buffer.array(); } void sign(String secretPhrase) { - if (blockSignature != null) { - throw new IllegalStateException("Block already signed"); + synchronized (this) { + if (blockSignature != null) { + throw new IllegalStateException("Block already signed"); + } + // 1. Calculate the unsigned bytes first. + byte[] unsignedBytes = getUnsignedBytes(); + + // 2. Sign the unsigned bytes to get the block signature. + blockSignature = Crypto.sign(unsignedBytes, secretPhrase); + + // 3. Now that blockSignature is available, construct the full signed bytes + // and cache them. This ensures cachedBytes always holds the final, signed + // state. + ByteBuffer buffer = ByteBuffer.allocate(unsignedBytes.length + blockSignature.length); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put(unsignedBytes); + if (buffer.limit() - buffer.position() < blockSignature.length) { + logger.error("Something is too large here " + + "- buffer should have {} bytes left but only has {}", + blockSignature.length, + (buffer.limit() - buffer.position())); + } + buffer.put(blockSignature); + cachedBytes.set(buffer.array()); // Cache the final, signed bytes } - blockSignature = new byte[64]; - byte[] data = getBytes(); - byte[] data2 = new byte[data.length - 64]; - System.arraycopy(data, 0, data2, 0, data2.length); - blockSignature = Crypto.sign(data2, secretPhrase); } public byte[] getBlockAts() { diff --git a/src/brs/BlockchainProcessor.java b/src/brs/BlockchainProcessor.java index defcea3e0..d4aaf1732 100644 --- a/src/brs/BlockchainProcessor.java +++ b/src/brs/BlockchainProcessor.java @@ -6,6 +6,8 @@ import brs.util.JSON; import brs.util.Observable; import com.google.gson.JsonObject; + +import java.util.Collection; import java.util.List; public interface BlockchainProcessor extends Observable { @@ -25,11 +27,14 @@ class QueueStatus { public final int unverifiedSize; public final int verifiedSize; public final int totalSize; + public final int cacheFullness; - public QueueStatus(int unverifiedSize, int verifiedSize, int totalSize) { + public QueueStatus(int unverifiedSize, int verifiedSize, int totalSize, int cacheFullness) { this.unverifiedSize = unverifiedSize; this.verifiedSize = verifiedSize; this.totalSize = totalSize; + this.cacheFullness = cacheFullness; + } } @@ -55,12 +60,19 @@ class PerformanceStats { public final long blockApplyTimeMs; public final long commitTimeMs; public final long miscTimeMs; - public final Block block; + public final int height; + public final int allTransactionCount; + public final int systemTransactionCount; + public final int atCount; + public final int payloadSize; + public final int maxPayloadSize; public PerformanceStats(long totalTimeMs, long validationTimeMs, long txLoopTimeMs, long housekeepingTimeMs, long txApplyTimeMs, long atTimeMs, long subscriptionTimeMs, long blockApplyTimeMs, long commitTimeMs, - long miscTimeMs, Block block) { + long miscTimeMs, int height, + int allTransactionCount, int systemTransactionCount, int atCount, + int payloadSize, int maxPayloadSize) { this.totalTimeMs = totalTimeMs; this.validationTimeMs = validationTimeMs; this.txLoopTimeMs = txLoopTimeMs; @@ -71,7 +83,12 @@ public PerformanceStats(long totalTimeMs, long validationTimeMs, long txLoopTime this.blockApplyTimeMs = blockApplyTimeMs; this.commitTimeMs = commitTimeMs; this.miscTimeMs = miscTimeMs; - this.block = block; + this.height = height; + this.allTransactionCount = allTransactionCount; + this.systemTransactionCount = systemTransactionCount; + this.atCount = atCount; + this.payloadSize = payloadSize; + this.maxPayloadSize = maxPayloadSize; } } @@ -98,6 +115,8 @@ enum Event { QueueStatus getQueueStatus(); + Collection getAllPeers(); + PerformanceStats getPerformanceStats(); long getAccumulatedSyncTimeMs(); @@ -112,6 +131,10 @@ enum Event { void fullReset(); + void setGetMoreBlocksPause(boolean getMoreBlocksPause); + + void setBlockImporterPause(boolean blockImporterPause); + void generateBlock(String secretPhrase, byte[] publicKey, Long nonce) throws BlockNotAcceptedException; diff --git a/src/brs/BlockchainProcessorImpl.java b/src/brs/BlockchainProcessorImpl.java index 59ca2045a..f4db0064f 100644 --- a/src/brs/BlockchainProcessorImpl.java +++ b/src/brs/BlockchainProcessorImpl.java @@ -59,6 +59,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -146,6 +147,8 @@ public final class BlockchainProcessorImpl implements BlockchainProcessor { private final AtomicReference lastBlockchainFeeder = new AtomicReference<>(); private final AtomicInteger lastBlockchainFeederHeight = new AtomicInteger(); private final AtomicBoolean getMoreBlocks = new AtomicBoolean(true); + private final AtomicBoolean getMoreBlocksPause = new AtomicBoolean(false); + private final AtomicBoolean blockImporterPause = new AtomicBoolean(false); private final AtomicBoolean isScanning = new AtomicBoolean(false); @@ -246,6 +249,11 @@ public QueueStatus getQueueStatus() { return queueStatus.get(); } + @Override + public Collection getAllPeers() { + return Peers.getAllPeers(); + } + @Override public PerformanceStats getPerformanceStats() { return performanceStats.get(); @@ -412,7 +420,12 @@ public BlockchainProcessorImpl(ThreadPool threadPool, if (stats != null) { int userTransactionCount = block.getTransactions().size(); - int allTransactionCount = block.getAllTransactions().size(); + int atTransactionCount = block.getAtTransactions().size(); + int subscriptionTransactionCount = block.getSubscriptionTransactions().size(); + int escrowTransactionCount = block.getEscrowTransactions().size(); + int systemTransactionCount = atTransactionCount + subscriptionTransactionCount + + escrowTransactionCount; + int allTransactionCount = userTransactionCount + systemTransactionCount; int atCount = 0; if (block.getBlockAts() != null) { @@ -505,6 +518,11 @@ public void run() { while (!Thread.currentThread().isInterrupted() && ThreadPool.running.get()) { try { try { + + if (getMoreBlocksPause.get()) { + return; + } + if (!getMoreBlocks.get()) { return; } @@ -659,6 +677,7 @@ public void run() { // we stop the loop since cahce has been locked return; } + updateAndFireQueueStatus(); // Fire event immediately after adding if (logger.isDebugEnabled()) { logger.debug("Added from download: Id: {} Height: {}", block.getId(), block.getHeight()); @@ -943,6 +962,11 @@ private void processFork(Peer peer, final List forkBlocks, long forkBlock Runnable blockImporterThread = () -> { while (!Thread.interrupted() && ThreadPool.running.get() && downloadCache.size() > 0) { try { + + if (blockImporterPause.get()) { + return; + } + Block lastBlock = blockchain.getLastBlock(); Long lastId = lastBlock.getId(); Block currentBlock = downloadCache.getNextBlock(lastId); /* @@ -1087,7 +1111,10 @@ private void updateAndFireQueueStatus() { if (totalSize != lastTotalSize || unverifiedSize != lastUnverifiedQueueSize) { lastUnverifiedQueueSize = unverifiedSize; lastTotalSize = totalSize; - queueStatus.set(new BlockchainProcessor.QueueStatus(unverifiedSize, verifiedSize, totalSize)); + int downloadCacheFullness = (int) downloadCache.getBlockCacheSize(); + queueStatus + .set(new BlockchainProcessor.QueueStatus(unverifiedSize, verifiedSize, totalSize, + downloadCacheFullness)); blockListeners.notify(null, Event.QUEUE_STATUS_CHANGED); } } @@ -1434,6 +1461,16 @@ public void fullReset() { downloadCache.resetCache(); } + @Override + public void setGetMoreBlocksPause(boolean getMoreBlocksPause) { + this.getMoreBlocksPause.set(getMoreBlocksPause); + } + + @Override + public void setBlockImporterPause(boolean blockImporterPause) { + this.blockImporterPause.set(blockImporterPause); + } + void setGetMoreBlocks(boolean getMoreBlocks) { this.getMoreBlocks.set(getMoreBlocks); } @@ -1742,13 +1779,34 @@ private void pushBlock(final Block block) throws BlockNotAcceptedException { atTimeMs = TimeUnit.NANOSECONDS.toMillis(atTimeNanos); long subscriptionTimeMs = TimeUnit.NANOSECONDS.toMillis(subscriptionTimeNanos); long blockApplyTimeMs = TimeUnit.NANOSECONDS.toMillis(blockApplyTimeNanos); - long miscTimeMs = totalTimeMs - (validationTimeMs + txLoopTimeMs + housekeepingTimeMs - + txApplyTimeMs + atTimeMs + subscriptionTimeMs + blockApplyTimeMs + commitTimeMs); + long sumTimeMs = validationTimeMs + txLoopTimeMs + housekeepingTimeMs + txApplyTimeMs + atTimeMs + + subscriptionTimeMs + blockApplyTimeMs + commitTimeMs; + long miscTimeMs = totalTimeMs - sumTimeMs; + + int userTransactionCount = block.getTransactions().size(); + int atTransactionCount = block.getAtTransactions().size(); + int subscriptionTransactionCount = block.getSubscriptionTransactions().size(); + int escrowTransactionCount = block.getEscrowTransactions().size(); + int systemTransactionCount = atTransactionCount + subscriptionTransactionCount + + escrowTransactionCount; + int allTransactionCount = userTransactionCount + systemTransactionCount; + int atCount = 0; + + if (block.getBlockAts() != null) { + try { + atCount = AtController.getATsFromBlock(block.getBlockAts()).size(); + } catch (Exception e) { + // ignore, as this is for measurement only + } + } + + int maxPayloadSize = Signum.getFluxCapacitor().getValue(FluxValues.MAX_PAYLOAD_LENGTH, block.getHeight()); - performanceStats.set(new BlockchainProcessor.PerformanceStats( - totalTimeMs, validationTimeMs, txLoopTimeMs, housekeepingTimeMs, txApplyTimeMs, atTimeMs, - subscriptionTimeMs, blockApplyTimeMs, commitTimeMs, miscTimeMs, block)); - blockListeners.notify(block, Event.PERFORMANCE_STATS_UPDATED); + performanceStats.set(new BlockchainProcessor.PerformanceStats(totalTimeMs, validationTimeMs, txLoopTimeMs, + housekeepingTimeMs, txApplyTimeMs, atTimeMs, subscriptionTimeMs, blockApplyTimeMs, commitTimeMs, + miscTimeMs, block.getHeight(), allTransactionCount, systemTransactionCount, atCount, + block.getPayloadLength(), maxPayloadSize)); + blockListeners.notify(null, Event.PERFORMANCE_STATS_UPDATED); logger.debug("Successfully pushed {} (height {})", block.getId(), block.getHeight()); statisticsManager.blockAdded(); @@ -1906,7 +1964,6 @@ private List popOffTo(Block commonBlock, List forkBlocks) { stores.commitTransaction(); downloadCache.resetCache(); atProcessorCache.reset(); - ; } catch (RuntimeException e) { stores.rollbackTransaction(); logger.debug("Error popping off to {}", commonBlock.getHeight(), e); diff --git a/src/brs/at/AT.java b/src/brs/at/AT.java index 7b89c6c8d..45635c236 100644 --- a/src/brs/at/AT.java +++ b/src/brs/at/AT.java @@ -6,7 +6,6 @@ package brs.at; - import brs.*; import brs.db.SignumKey; import brs.db.TransactionDb; @@ -47,10 +46,10 @@ private AT(byte[] atId, byte[] creator, String name, String description, byte[] } public AT(byte[] atId, byte[] creator, String name, String description, short version, - int height, - byte[] stateBytes, int csize, int dsize, int cUserStackBytes, int cCallStackBytes, - int creationBlockHeight, int sleepBetween, int nextHeight, - boolean freezeWhenSameBalance, long minActivationAmount, byte[] apCode, long apCodeHashId) { + int height, + byte[] stateBytes, int csize, int dsize, int cUserStackBytes, int cCallStackBytes, + int creationBlockHeight, int sleepBetween, int nextHeight, + boolean freezeWhenSameBalance, long minActivationAmount, byte[] apCode, long apCodeHashId) { super(atId, creator, version, height, stateBytes, csize, dsize, cUserStackBytes, cCallStackBytes, @@ -72,9 +71,9 @@ public static void clearPending(int blockHeight, long generatorId) { public static void addPendingFee(long id, long fee, int blockHeight, long generatorId) { long hash = blockHeight + generatorId; LinkedHashMap pendingFees = pendingFeesMap.get(hash); - if(pendingFees == null) { - pendingFees = new LinkedHashMap<>(); - pendingFeesMap.put(hash, pendingFees); + if (pendingFees == null) { + pendingFees = new LinkedHashMap<>(); + pendingFeesMap.put(hash, pendingFees); } pendingFees.put(id, fee); } @@ -82,30 +81,30 @@ public static void addPendingFee(long id, long fee, int blockHeight, long genera public static void addPendingTransaction(AtTransaction atTransaction, int blockHeight, long generatorId) { long hash = blockHeight + generatorId; List pendingTransactions = pendingTransactionsMap.get(hash); - if(pendingTransactions == null) { - pendingTransactions = new ArrayList<>(); - pendingTransactionsMap.put(hash, pendingTransactions); + if (pendingTransactions == null) { + pendingTransactions = new ArrayList<>(); + pendingTransactionsMap.put(hash, pendingTransactions); } pendingTransactions.add(atTransaction); } public static void addMapUpdates(Collection entries, int blockHeight, long generatorId) { - if(entries == null) - return; - - long hash = blockHeight + generatorId; - List pendingUpdates = pendingEntryUpdatesMap.get(hash); - if(pendingUpdates == null) { - pendingUpdates = new ArrayList<>(); - pendingEntryUpdatesMap.put(hash, pendingUpdates); - } - pendingUpdates.addAll(entries); + if (entries == null) + return; + + long hash = blockHeight + generatorId; + List pendingUpdates = pendingEntryUpdatesMap.get(hash); + if (pendingUpdates == null) { + pendingUpdates = new ArrayList<>(); + pendingEntryUpdatesMap.put(hash, pendingUpdates); + } + pendingUpdates.addAll(entries); } public static boolean findPendingTransaction(byte[] recipientId, int blockHeight, long generatorId) { long hash = blockHeight + generatorId; - if(pendingTransactionsMap.get(hash) == null) { - return false; + if (pendingTransactionsMap.get(hash) == null) { + return false; } for (AtTransaction tx : pendingTransactionsMap.get(hash)) { if (Arrays.equals(recipientId, tx.getRecipientId())) { @@ -139,7 +138,8 @@ public static AT getAT(Long id) { return Signum.getStores().getAtStore().getAT(id, -1); } - public static void addAT(Long atId, Long senderAccountId, String name, String description, byte[] creationBytes, int height, long atCodeHashId) { + public static void addAT(Long atId, Long senderAccountId, String name, String description, byte[] creationBytes, + int height, long atCodeHashId) { ByteBuffer bf = ByteBuffer.allocate(8 + 8); bf.order(ByteOrder.LITTLE_ENDIAN); @@ -156,8 +156,8 @@ public static void addAT(Long atId, Long senderAccountId, String name, String de AT at = new AT(id, creator, name, description, creationBytes, height); - if(at.getApCodeHashId() == 0L) - at.setApCodeHashId(atCodeHashId); + if (at.getApCodeHashId() == 0L) + at.setApCodeHashId(atCodeHashId); AtController.resetMachine(at); @@ -195,8 +195,8 @@ public static byte[] decompressState(byte[] stateBytes) { } try (ByteArrayInputStream bis = new ByteArrayInputStream(stateBytes); - GZIPInputStream gzip = new GZIPInputStream(bis); - ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + GZIPInputStream gzip = new GZIPInputStream(bis); + ByteArrayOutputStream bos = new ByteArrayOutputStream()) { byte[] buffer = new byte[256]; int read; while ((read = gzip.read(buffer, 0, buffer.length)) > 0) { @@ -213,28 +213,29 @@ public void saveState() { int prevHeight = Signum.getBlockchain().getHeight(); int newNextHeight = prevHeight + getWaitForNumberOfBlocks(); ATState state = new ATState(AtApiHelper.getLong(this.getId()), - getState(), newNextHeight, getSleepBetween(), - getpBalance(), freezeOnSameBalance(), minActivationAmount()); + getState(), newNextHeight, getSleepBetween(), + getpBalance(), freezeOnSameBalance(), minActivationAmount()); state.setPrevHeight(prevHeight); atStateTable().insert(state); } public static void saveMapUpdates(int blockHeight, long generatorId) { - long hash = blockHeight+generatorId; - List updates = pendingEntryUpdatesMap.get(hash); - if(updates != null) { - VersionedEntityTable table = Signum.getStores().getAtStore().getAtMapTable(); - for(AtMapEntry e : updates) { - AtMapEntry cacheEntry = Signum.getStores().getAtStore().getMapValueEntry(e.getAtId(), e.getKey1(), e.getKey2()); - if(cacheEntry != null) { - cacheEntry.setValue(e.getValue()); - e = cacheEntry; - } - table.insert(e); - } - updates.clear(); - } + long hash = blockHeight + generatorId; + List updates = pendingEntryUpdatesMap.get(hash); + if (updates != null) { + VersionedEntityTable table = Signum.getStores().getAtStore().getAtMapTable(); + for (AtMapEntry e : updates) { + AtMapEntry cacheEntry = Signum.getStores().getAtStore().getMapValueEntry(e.getAtId(), e.getKey1(), + e.getKey2()); + if (cacheEntry != null) { + cacheEntry.setValue(e.getValue()); + e = cacheEntry; + } + table.insert(e); + } + updates.clear(); + } } public String getName() { @@ -262,35 +263,36 @@ public HandleATBlockTransactionsListener(AccountService accountService, Transact public void notify(Block block) { long hash = block.getHeight() + block.getGeneratorId(); LinkedHashMap pendingFees = pendingFeesMap.get(hash); - if(pendingFees != null) { - pendingFees.forEach((key, value) -> { - Account atAccount = accountService.getAccount(key); - accountService.addToBalanceAndUnconfirmedBalanceNQT(atAccount, -value); - }); + if (pendingFees != null) { + pendingFees.forEach((key, value) -> { + Account atAccount = accountService.getAccount(key); + accountService.addToBalanceAndUnconfirmedBalanceNQT(atAccount, -value); + }); } pendingFeesMap.remove(hash); List transactions = new ArrayList<>(); List pendingTransactions = pendingTransactionsMap.get(hash); - if(pendingTransactions != null) { - for (AtTransaction atTransaction : pendingTransactions) { - try { - Transaction transaction = atTransaction.build(block); - - if (!transactionDb.hasTransaction(transaction.getIdCheckSignature(false))) { - atTransaction.apply(accountService, transaction); - transactions.add(transaction); + if (pendingTransactions != null) { + for (AtTransaction atTransaction : pendingTransactions) { + try { + Transaction transaction = atTransaction.build(block); + + if (!transactionDb.hasTransaction(transaction.getIdCheckSignature(false))) { + atTransaction.apply(accountService, transaction); + transactions.add(transaction); + } + } catch (SignumException.NotValidException e) { + throw new RuntimeException("Failed to construct AT payment transaction", e); } - } catch (SignumException.NotValidException e) { - throw new RuntimeException("Failed to construct AT payment transaction", e); } - } - pendingTransactionsMap.remove(hash); + pendingTransactionsMap.remove(hash); } if (!transactions.isEmpty()) { // WATCH: Replace after transactions are converted! transactionDb.saveTransactions(transactions); + block.setAtTransactions(transactions); } } } @@ -308,7 +310,8 @@ public static class ATState { private long minActivationAmount; protected ATState(long atId, byte[] state, - int nextHeight, int sleepBetween, long prevBalance, boolean freezeWhenSameBalance, long minActivationAmount) { + int nextHeight, int sleepBetween, long prevBalance, boolean freezeWhenSameBalance, + long minActivationAmount) { this.atId = atId; this.dbKey = atStateDbKeyFactory().newKey(this.atId); this.state = state; @@ -319,7 +322,6 @@ protected ATState(long atId, byte[] state, this.minActivationAmount = minActivationAmount; } - public long getATId() { return atId; } @@ -382,33 +384,37 @@ void setMinActivationAmount(long newMinActivationAmount) { } public static class AtMapEntry { - private long atId; - private long key1; - private long key2; - private long value; - - public AtMapEntry(long atId, long key1, long key2, long value) { - this.atId = atId; - this.key1 = key1; - this.key2 = key2; - this.value = value; - } - - public long getValue() { - return value; - } - public void setValue(long value) { - this.value = value; - } - public long getAtId() { - return atId; - } - public long getKey1() { - return key1; - } - public long getKey2() { - return key2; - } + private long atId; + private long key1; + private long key2; + private long value; + + public AtMapEntry(long atId, long key1, long key2, long value) { + this.atId = atId; + this.key1 = key1; + this.key2 = key2; + this.value = value; + } + + public long getValue() { + return value; + } + + public void setValue(long value) { + this.value = value; + } + + public long getAtId() { + return atId; + } + + public long getKey1() { + return key1; + } + + public long getKey2() { + return key2; + } } } diff --git a/src/brs/db/sql/VersionedEntitySqlTable.java b/src/brs/db/sql/VersionedEntitySqlTable.java index eed1d3882..41ffd6266 100644 --- a/src/brs/db/sql/VersionedEntitySqlTable.java +++ b/src/brs/db/sql/VersionedEntitySqlTable.java @@ -15,159 +15,208 @@ import java.util.List; public abstract class VersionedEntitySqlTable extends EntitySqlTable implements VersionedEntityTable { - - private static final Logger logger = LoggerFactory.getLogger(VersionedEntitySqlTable.class); - VersionedEntitySqlTable(String table, TableImpl tableClass, SignumKey.Factory dbKeyFactory, DerivedTableManager derivedTableManager) { - super(table, tableClass, dbKeyFactory, true, derivedTableManager); - } + private static final Logger logger = LoggerFactory.getLogger(VersionedEntitySqlTable.class); - @Override - public void rollback(int height) { - rollback(table, tableClass, heightField, latestField, height, dbKeyFactory); - } + VersionedEntitySqlTable(String table, TableImpl tableClass, SignumKey.Factory dbKeyFactory, + DerivedTableManager derivedTableManager) { + super(table, tableClass, dbKeyFactory, true, derivedTableManager); + } - static void rollback(final String table, final TableImpl tableClass, Field heightField, Field latestField, final int height, final DbKey.Factory dbKeyFactory) { - if (!Db.isInTransaction()) { - throw new IllegalStateException("Not in transaction"); + @Override + public void rollback(int height) { + rollback(table, tableClass, heightField, latestField, height, dbKeyFactory); } - Db.useDSLContext(ctx -> { - // get dbKey's for entries whose stuff newer than height would be deleted, to allow fixing - // their latest flag of the "potential" remaining newest entry - SelectQuery selectForDeleteQuery = ctx.selectQuery(); - selectForDeleteQuery.addFrom(tableClass); - selectForDeleteQuery.addConditions(heightField.gt(height)); - for (String column : dbKeyFactory.getPKColumns()) { - selectForDeleteQuery.addSelect(tableClass.field(column, Long.class)); - } - selectForDeleteQuery.setDistinct(true); - List dbKeys = selectForDeleteQuery.fetch(r -> (DbKey) dbKeyFactory.newKey(r)); - - // delete all entries > height - DeleteQuery deleteQuery = ctx.deleteQuery(tableClass); - deleteQuery.addConditions(heightField.gt(height)); - deleteQuery.execute(); - - // update latest flags for remaining entries, if there any remaining (per deleted dbKey) - for (DbKey dbKey : dbKeys) { - SelectQuery selectMaxHeightQuery = ctx.selectQuery(); - selectMaxHeightQuery.addFrom(tableClass); - selectMaxHeightQuery.addConditions(dbKey.getPKConditions(tableClass)); - selectMaxHeightQuery.addSelect(DSL.max(heightField)); - Integer maxHeight = selectMaxHeightQuery.fetchOne().get(DSL.max(heightField)); - - if (maxHeight != null) { - UpdateQuery setLatestQuery = ctx.updateQuery(tableClass); - setLatestQuery.addConditions(dbKey.getPKConditions(tableClass)); - setLatestQuery.addConditions(heightField.eq(maxHeight)); - setLatestQuery.addValue(latestField, true); - setLatestQuery.execute(); + static void rollback(final String table, final TableImpl tableClass, Field heightField, + Field latestField, final int height, final DbKey.Factory dbKeyFactory) { + if (!Db.isInTransaction()) { + throw new IllegalStateException("Not in transaction"); } - } - }); - Db.getCache(table).clear(); - } - - @Override - public final void trim(int height) { - trim(tableClass, heightField, height, dbKeyFactory); - } - - static void trim(final TableImpl tableClass, Field heightField, final int height, final DbKey.Factory dbKeyFactory) { - if (!Db.isInTransaction()) { - throw new IllegalStateException("Not in transaction"); + + Db.useDSLContext(ctx -> { + // get dbKey's for entries whose stuff newer than height would be deleted, to + // allow fixing + // their latest flag of the "potential" remaining newest entry + SelectQuery selectForDeleteQuery = ctx.selectQuery(); + selectForDeleteQuery.addFrom(tableClass); + selectForDeleteQuery.addConditions(heightField.gt(height)); + for (String column : dbKeyFactory.getPKColumns()) { + selectForDeleteQuery.addSelect(tableClass.field(column, Long.class)); + } + selectForDeleteQuery.setDistinct(true); + List dbKeys = selectForDeleteQuery.fetch(r -> (DbKey) dbKeyFactory.newKey(r)); + + // delete all entries > height + DeleteQuery deleteQuery = ctx.deleteQuery(tableClass); + deleteQuery.addConditions(heightField.gt(height)); + deleteQuery.execute(); + + // update latest flags for remaining entries, if there any remaining (per + // deleted dbKey) + for (DbKey dbKey : dbKeys) { + SelectQuery selectMaxHeightQuery = ctx.selectQuery(); + selectMaxHeightQuery.addFrom(tableClass); + selectMaxHeightQuery.addConditions(dbKey.getPKConditions(tableClass)); + selectMaxHeightQuery.addSelect(DSL.max(heightField)); + Integer maxHeight = selectMaxHeightQuery.fetchOne().get(DSL.max(heightField)); + + if (maxHeight != null) { + UpdateQuery setLatestQuery = ctx.updateQuery(tableClass); + setLatestQuery.addConditions(dbKey.getPKConditions(tableClass)); + setLatestQuery.addConditions(heightField.eq(maxHeight)); + setLatestQuery.addValue(latestField, true); + setLatestQuery.execute(); + } + } + }); + Db.getCache(table).clear(); } - // "accounts" is just an example to make it easier to understand what the code does - // select all accounts with multiple entries where height < trimToHeight[current height - 1440] - Db.useDSLContext(ctx -> { - Field latestField = tableClass.field("latest", Boolean.class); - SelectQuery selectMaxHeightQuery = ctx.selectQuery(); - selectMaxHeightQuery.addFrom(tableClass); - selectMaxHeightQuery.addSelect(DSL.max(heightField).as("max_height")); - for (String column : dbKeyFactory.getPKColumns()) { - Field pkField = tableClass.field(column, Long.class); - selectMaxHeightQuery.addSelect(pkField); - selectMaxHeightQuery.addGroupBy(pkField); - } - selectMaxHeightQuery.addConditions(heightField.lt(height)); - selectMaxHeightQuery.addHaving(DSL.countDistinct(heightField).gt(1)); - // to avoid problems if trimming is enable after sync - selectMaxHeightQuery.addLimit(50000); - - // delete all fetched accounts, except if it's height is the max height we figured out - DeleteQuery deleteLowerHeightQuery = ctx.deleteQuery(tableClass); - deleteLowerHeightQuery.addConditions(heightField.lt((Integer) null)); - for (String column : dbKeyFactory.getPKColumns()) { - Field pkField = tableClass.field(column, Long.class); - deleteLowerHeightQuery.addConditions(pkField.eq((Long) null)); - } - BatchBindStep deleteBatch = ctx.batch(deleteLowerHeightQuery); - - for (Record record : selectMaxHeightQuery.fetch()) { - DbKey dbKey = (DbKey) dbKeyFactory.newKey(record); - int maxHeight = record.get("max_height", Integer.class); - List bindValues = new ArrayList<>(); - bindValues.add((long) maxHeight); - for (Long pkValue : dbKey.getPKValues()) { - bindValues.add(pkValue); - } - deleteBatch.bind(bindValues.toArray()); - } - logger.debug("Trimming {} to height {} by {} elements", tableClass, height, deleteBatch.size()); - if (deleteBatch.size() > 0) { - deleteBatch.execute(); - } - - int deletedNotLatest = ctx.deleteFrom(tableClass) - .where(heightField.lt(height).and(latestField.isFalse())) - .execute(); - if (deletedNotLatest > 0) { - logger.debug("Trimming {} removed {} obsolete non-latest elements below height {}", - tableClass, deletedNotLatest, height); - } - }); - } - - @Override - public boolean delete(T t) { - if (t == null) { - return false; + @Override + public final void trim(int height) { + trim(tableClass, heightField, height, dbKeyFactory); } - if (!Db.isInTransaction()) { - throw new IllegalStateException("Not in transaction"); + + static void trim(final TableImpl tableClass, Field heightField, final int height, + final DbKey.Factory dbKeyFactory) { + if (!Db.isInTransaction()) { + throw new IllegalStateException("Not in transaction"); + } + + // "accounts" is just an example to make it easier to understand what the code + // does + // select all accounts with multiple entries where height < trimToHeight[current + // height - 1440] + + final int selectBatchSize = 10000; + final int deleteBatchSize = 1000; + + Db.useDSLContext(ctx -> { + Field latestField = tableClass.field("latest", Boolean.class); + + int offset = 0; + int totalDeleted = 0; + + while (true) { + SelectQuery selectMaxHeightQuery = ctx.selectQuery(); + selectMaxHeightQuery.addFrom(tableClass); + selectMaxHeightQuery.addSelect(DSL.max(heightField).as("max_height")); + + for (String column : dbKeyFactory.getPKColumns()) { + Field pkField = tableClass.field(column, Long.class); + selectMaxHeightQuery.addSelect(pkField); + selectMaxHeightQuery.addGroupBy(pkField); + } + + selectMaxHeightQuery.addConditions(heightField.lt(height)); + selectMaxHeightQuery.addHaving(DSL.countDistinct(heightField).gt(1)); + selectMaxHeightQuery.addLimit(selectBatchSize); + selectMaxHeightQuery.addOffset(offset); + + List records = selectMaxHeightQuery.fetch(); + if (records.isEmpty()) { + break; + } + + DeleteQuery deleteLowerHeightQuery = ctx.deleteQuery(tableClass); + deleteLowerHeightQuery.addConditions(heightField.lt((Integer) null)); + for (String column : dbKeyFactory.getPKColumns()) { + Field pkField = tableClass.field(column, Long.class); + deleteLowerHeightQuery.addConditions(pkField.eq((Long) null)); + } + + BatchBindStep deleteBatch = ctx.batch(deleteLowerHeightQuery); + int deleteCounter = 0; + + for (Record record : records) { + DbKey dbKey = (DbKey) dbKeyFactory.newKey(record); + int maxHeight = record.get("max_height", Integer.class); + + List bindValues = new ArrayList<>(); + bindValues.add(maxHeight); + + for (long pkValue : dbKey.getPKValues()) { + bindValues.add(pkValue); + } + + deleteBatch.bind(bindValues.toArray()); + deleteCounter++; + + if (deleteCounter % deleteBatchSize == 0) { + deleteBatch.execute(); + totalDeleted += deleteCounter; + deleteBatch = ctx.batch(deleteLowerHeightQuery); + } + } + + if (deleteCounter % deleteBatchSize != 0) { + deleteBatch.execute(); + totalDeleted += deleteCounter; + } + + logger.debug("Trimmed {} batch: {} rows processed (offset {}).", + tableClass.getName(), deleteCounter, offset); + + offset += selectBatchSize; + } + + logger.debug("Total trimmed {} rows from {} below height {}", + totalDeleted, tableClass.getName(), height); + + int totalDeletedNotLatest = 0; + while (true) { + int deletedInBatch = ctx.deleteFrom(tableClass) + .where(heightField.lt(height).and(latestField.isFalse())) + .limit(deleteBatchSize) + .execute(); + totalDeletedNotLatest += deletedInBatch; + if (deletedInBatch < deleteBatchSize) { + break; + } + } + logger.debug("Trimming {} removed {} obsolete elements (latest = 0) below height {}", + tableClass.getName(), totalDeletedNotLatest, height); + }); } - DbKey dbKey = (DbKey) dbKeyFactory.newKey(t); - return Db.useDSLContext(ctx -> { - try { - SelectQuery countQuery = ctx.selectQuery(); - countQuery.addFrom(tableClass); - countQuery.addConditions(dbKey.getPKConditions(tableClass)); - countQuery.addConditions(heightField.lt(Signum.getBlockchain().getHeight())); - if (ctx.fetchCount(countQuery) > 0) { - UpdateQuery updateQuery = ctx.updateQuery(tableClass); - updateQuery.addValue( - latestField, - false - ); - updateQuery.addConditions(dbKey.getPKConditions(tableClass)); - updateQuery.addConditions(latestField.isTrue()); - - updateQuery.execute(); - save(ctx, t); - // delete after the save - updateQuery.execute(); - - return true; - } else { - DeleteQuery deleteQuery = ctx.deleteQuery(tableClass); - deleteQuery.addConditions(dbKey.getPKConditions(tableClass)); - return deleteQuery.execute() > 0; + + @Override + public boolean delete(T t) { + if (t == null) { + return false; } - } finally { - Db.getCache(table).remove(dbKey); - } - }); - } + if (!Db.isInTransaction()) { + throw new IllegalStateException("Not in transaction"); + } + DbKey dbKey = (DbKey) dbKeyFactory.newKey(t); + return Db.useDSLContext(ctx -> { + try { + SelectQuery countQuery = ctx.selectQuery(); + countQuery.addFrom(tableClass); + countQuery.addConditions(dbKey.getPKConditions(tableClass)); + countQuery.addConditions(heightField.lt(Signum.getBlockchain().getHeight())); + if (ctx.fetchCount(countQuery) > 0) { + UpdateQuery updateQuery = ctx.updateQuery(tableClass); + updateQuery.addValue( + latestField, + false); + updateQuery.addConditions(dbKey.getPKConditions(tableClass)); + updateQuery.addConditions(latestField.isTrue()); + + updateQuery.execute(); + save(ctx, t); + // delete after the save + updateQuery.execute(); + + return true; + } else { + DeleteQuery deleteQuery = ctx.deleteQuery(tableClass); + deleteQuery.addConditions(dbKey.getPKConditions(tableClass)); + return deleteQuery.execute() > 0; + } + } finally { + Db.getCache(table).remove(dbKey); + } + }); + } } diff --git a/src/brs/MetricsPanel.java b/src/brs/gui/MetricsPanel.java similarity index 54% rename from src/brs/MetricsPanel.java rename to src/brs/gui/MetricsPanel.java index 423528844..c5a1bad6f 100644 --- a/src/brs/MetricsPanel.java +++ b/src/brs/gui/MetricsPanel.java @@ -1,4 +1,4 @@ -package brs; +package brs.gui; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; @@ -13,23 +13,26 @@ import org.jfree.data.xy.XYSeriesCollection; import javax.swing.*; -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.font.TextAttribute; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.LinkedList; +import java.awt.event.*; import java.util.Map; import java.util.concurrent.ExecutorService; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.function.Consumer; -import brs.at.AtController; +import brs.Signum; +import brs.gui.util.MovingAverage; +import brs.BlockchainProcessor; +import brs.Block; import brs.props.Props; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.ForkJoinPool; +import java.awt.*; +import java.awt.font.TextAttribute; +import java.util.HashMap; + @SuppressWarnings("serial") public class MetricsPanel extends JPanel { @@ -38,29 +41,33 @@ public class MetricsPanel extends JPanel { private static final int SPEED_HISTORY_SIZE = 1000; private static final int MAX_SPEED_BPS = 10 * 1024 * 1024; // 10 MB/s - private final LinkedList blockTimestamps = new LinkedList<>(); - private final LinkedList transactionCounts = new LinkedList<>(); - private final LinkedList atTransactionCounts = new LinkedList<>(); - private final LinkedList pushTimes = new LinkedList<>(); - private final LinkedList validationTimes = new LinkedList<>(); - private final LinkedList txLoopTimes = new LinkedList<>(); - private final LinkedList housekeepingTimes = new LinkedList<>(); - private final LinkedList txApplyTimes = new LinkedList<>(); - private final LinkedList commitTimes = new LinkedList<>(); - private final LinkedList atTimes = new LinkedList<>(); - private final LinkedList subscriptionTimes = new LinkedList<>(); - private final LinkedList blockApplyTimes = new LinkedList<>(); - private final LinkedList miscTimes = new LinkedList<>(); - private final LinkedList atCounts = new LinkedList<>(); - private final LinkedList blocksPerSecondHistory = new LinkedList<>(); - private final LinkedList transactionsPerSecondHistory = new LinkedList<>(); - private final LinkedList atTransactionsPerSecondHistory = new LinkedList<>(); - private int movingAverageWindow = 100; // Default value + private int movingAverageWindow = 100; + private final MovingAverage blockTimestamps = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage blocksPerSec = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage allTransactionsPerBlock = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage systemTransactionsPerBlock = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage allTransactionsPerSec = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage systemTransactionsPerSec = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage pushTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage validationTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage txLoopTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage housekeepingTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage txApplyTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage commitTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage atTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage subscriptionTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage blockApplyTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage miscTimes = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage atCountsPerBlock = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage payloadSize = new MovingAverage(CHART_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage uploadSpeeds = new MovingAverage(SPEED_HISTORY_SIZE, movingAverageWindow); + private final MovingAverage downloadSpeeds = new MovingAverage(SPEED_HISTORY_SIZE, movingAverageWindow); + private XYSeries blocksPerSecondSeries; - private XYSeries transactionsPerSecondSeries; - private XYSeries transactionsPerBlockSeries; - private XYSeries atTransactionsPerBlockSeries; - private XYSeries atTransactionsPerSecondSeries; + private XYSeries allTransactionsPerSecondSeries; + private XYSeries allTransactionsPerBlockSeries; + private XYSeries systemTransactionsPerBlockSeries; + private XYSeries systemTransactionsPerSecondSeries; private XYSeries pushTimePerBlockSeries; private XYSeries uploadSpeedSeries; private XYSeries downloadSpeedSeries; @@ -74,11 +81,16 @@ public class MetricsPanel extends JPanel { private XYSeries blockApplyTimePerBlockSeries; private XYSeries miscTimePerBlockSeries; private XYSeries atCountPerBlockSeries; + private XYSeries payloadFullnessSeries; private JProgressBar blocksPerSecondProgressBar; - private JProgressBar transactionsPerSecondProgressBar; - private JProgressBar transactionsPerBlockProgressBar; + private JProgressBar allTransactionsPerSecondProgressBar; + private JProgressBar allTransactionsPerBlockProgressBar; private int oclUnverifiedQueueThreshold; + private int maxUnverifiedQueueSize; + private int maxUnconfirmedTxs; + private int maxPayloadSize; + private int downloadCacheSize; private JLabel uploadSpeedLabel; private JLabel downloadSpeedLabel; private JLabel metricsUploadVolumeLabel; @@ -109,21 +121,25 @@ public class MetricsPanel extends JPanel { private JProgressBar blockApplyTimeProgressBar; private JLabel miscTimeLabel; private JProgressBar miscTimeProgressBar; + private JLabel payloadFullnessLabel; + private JProgressBar payloadFullnessProgressBar; private JLabel atCountLabel; - private JProgressBar atCountProgressBar; + private JProgressBar atCountsPerBlockProgressBar; private JLabel systemTxPerBlockLabel; - private JProgressBar atTransactionsPerBlockProgressBar; + private JProgressBar systemTransactionsPerBlockProgressBar; private JLabel systemTxPerSecondLabel; private JProgressBar systemTransactionsPerSecondProgressBar; private JProgressBar uploadSpeedProgressBar; private JProgressBar downloadSpeedProgressBar; + private JProgressBar unconfirmedTxsProgressBar; + private JLabel cacheFullnessLabel; + private JProgressBar cacheFullnessProgressBar; + private ChartPanel performanceChartPanel; private ChartPanel timingChartPanel; private ChartPanel netSpeedChartPanel; - - private final LinkedList uploadSpeedHistory = new LinkedList<>(); - private final LinkedList downloadSpeedHistory = new LinkedList<>(); + private Timer netSpeedChartUpdater; private XYSeries uploadVolumeSeries; private XYSeries downloadVolumeSeries; @@ -131,8 +147,8 @@ public class MetricsPanel extends JPanel { private long uploadedVolume = 0; private long downloadedVolume = 0; - private final Dimension chartDimension1 = new Dimension(280, 210); - private final Dimension chartDimension2 = new Dimension(280, 105); + private final Dimension chartDimension1 = new Dimension(320, 240); + private final Dimension chartDimension2 = new Dimension(320, 120); private final Dimension progressBarSize1 = new Dimension(200, 20); private final Dimension progressBarSize2 = new Dimension(150, 20); @@ -143,24 +159,316 @@ public class MetricsPanel extends JPanel { private final ExecutorService chartUpdateExecutor = Executors.newSingleThreadExecutor(); private JProgressBar syncProgressBarDownloadedBlocks; + + // Data Transfer Objects for UI updates + private static class TimingUpdateData { + Map progressBarUpdates = new HashMap<>(); + Map seriesUpdates = new HashMap<>(); + } + + private ChartPanel createPerformanceChartPanel() { + blocksPerSecondSeries = new XYSeries("Blocks/Second (MA)"); + allTransactionsPerSecondSeries = new XYSeries("All Txs/Sec (MA)"); + systemTransactionsPerSecondSeries = new XYSeries("System Txs/Sec (MA)"); + atCountPerBlockSeries = new XYSeries("ATs/Block (MA)"); + + XYSeriesCollection lineDataset = new XYSeriesCollection(); + lineDataset.addSeries(blocksPerSecondSeries); + lineDataset.addSeries(allTransactionsPerSecondSeries); + lineDataset.addSeries(systemTransactionsPerSecondSeries); + lineDataset.addSeries(atCountPerBlockSeries); + + // Create chart with no title or axis labels to save space + JFreeChart chart = ChartFactory.createXYLineChart( + null, // No title + null, // No X-axis label + null, // No Y-axis label + lineDataset); + + // Remove the legend to maximize plot area + chart.removeLegend(); + chart.setBorderVisible(false); + + XYPlot plot = chart.getXYPlot(); + plot.getDomainAxis().setLowerMargin(0.0); + plot.getDomainAxis().setUpperMargin(0.0); + plot.setBackgroundPaint(Color.DARK_GRAY); + plot.setDomainGridlinesVisible(false); + plot.setRangeGridlinesVisible(false); + + plot.getRenderer().setSeriesPaint(0, Color.CYAN); + plot.getRenderer().setSeriesPaint(1, Color.GREEN); + plot.getRenderer().setSeriesPaint(2, new Color(135, 206, 250)); + plot.getRenderer().setSeriesPaint(3, new Color(153, 0, 76)); + + // Set line thickness + plot.getRenderer().setSeriesStroke(0, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(1, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(2, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(3, new java.awt.BasicStroke(1.2f)); + + // Hide axis tick labels (the numbers on the axes) + plot.getDomainAxis().setTickLabelsVisible(false); + plot.getRangeAxis().setTickLabelsVisible(false); + + // Second Y-axis for transaction count + NumberAxis transactionAxis = new NumberAxis(null); + transactionAxis.setTickLabelsVisible(false); + XYSeriesCollection barDataset = new XYSeriesCollection(); + barDataset.addSeries(systemTransactionsPerBlockSeries); + barDataset.addSeries(allTransactionsPerBlockSeries); + plot.setRangeAxis(1, transactionAxis); + plot.setDataset(1, barDataset); + plot.mapDatasetToRangeAxis(1, 1); + + // Renderer for transaction bars + XYBarRenderer transactionRenderer = new XYBarRenderer(0); + transactionRenderer.setBarPainter(new StandardXYBarPainter()); + transactionRenderer.setShadowVisible(false); + transactionRenderer.setSeriesPaint(0, new Color(0, 0, 255, 128)); // System Txs/Block + transactionRenderer.setSeriesPaint(1, new Color(255, 165, 0, 128)); // All Txs/Block + plot.setRenderer(1, transactionRenderer); + + // Remove all padding around the plot area + plot.setInsets(new RectangleInsets(0, 0, 0, 0)); + plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); + + ChartPanel chartPanel = new ChartPanel(chart); + chartPanel.setPreferredSize(chartDimension1); + chartPanel.setMinimumSize(chartDimension1); + chartPanel.setMaximumSize(chartDimension1); + return chartPanel; + } + + private ChartPanel createTimingChartPanel() { + pushTimePerBlockSeries = new XYSeries("Push Time (MA)"); + validationTimePerBlockSeries = new XYSeries("Validation Time (MA)"); + txLoopTimePerBlockSeries = new XYSeries("TX Loop Time (MA)"); + housekeepingTimePerBlockSeries = new XYSeries("Housekeeping Time (MA)"); + + txApplyTimePerBlockSeries = new XYSeries("TX Apply Time (MA)"); + atTimePerBlockSeries = new XYSeries("AT Time (MA)"); + subscriptionTimePerBlockSeries = new XYSeries("Subscription Time (MA)"); + blockApplyTimePerBlockSeries = new XYSeries("Block Apply Time (MA)"); + commitTimePerBlockSeries = new XYSeries("Commit Time (MA)"); + miscTimePerBlockSeries = new XYSeries("Misc. Time (MA)"); + + XYSeriesCollection lineDataset = new XYSeriesCollection(); + lineDataset.addSeries(pushTimePerBlockSeries); + lineDataset.addSeries(validationTimePerBlockSeries); + lineDataset.addSeries(txLoopTimePerBlockSeries); + lineDataset.addSeries(housekeepingTimePerBlockSeries); + lineDataset.addSeries(txApplyTimePerBlockSeries); + lineDataset.addSeries(atTimePerBlockSeries); + lineDataset.addSeries(subscriptionTimePerBlockSeries); + lineDataset.addSeries(blockApplyTimePerBlockSeries); + lineDataset.addSeries(commitTimePerBlockSeries); + lineDataset.addSeries(miscTimePerBlockSeries); + lineDataset.addSeries(payloadFullnessSeries); + + // Create chart with no title or axis labels to save space + JFreeChart chart = ChartFactory.createXYLineChart( + null, // No title + null, // No X-axis label + null, // No Y-axis label + lineDataset); + + // Remove the legend to maximize plot area + chart.removeLegend(); + chart.setBorderVisible(false); + + XYPlot plot = chart.getXYPlot(); + plot.getDomainAxis().setLowerMargin(0.0); + plot.getDomainAxis().setUpperMargin(0.0); + plot.setBackgroundPaint(Color.DARK_GRAY); + plot.setDomainGridlinesVisible(false); + plot.setRangeGridlinesVisible(false); + + plot.getRenderer().setSeriesPaint(0, Color.BLUE); + plot.getRenderer().setSeriesPaint(1, Color.YELLOW); + plot.getRenderer().setSeriesPaint(2, new Color(128, 0, 128)); // Purple for TX Loop + plot.getRenderer().setSeriesPaint(3, new Color(42, 223, 223)); // Cyan for Housekeeping + plot.getRenderer().setSeriesPaint(4, new Color(255, 165, 0)); // Orange for TX Apply + plot.getRenderer().setSeriesPaint(5, new Color(153, 0, 76)); // Dark Red for AT + plot.getRenderer().setSeriesPaint(6, new Color(255, 105, 100)); // Hot Pink for Subscription + plot.getRenderer().setSeriesPaint(7, new Color(0, 100, 100)); // Teal for Block Apply + plot.getRenderer().setSeriesPaint(8, new Color(150, 0, 200)); // Magenta for Commit + plot.getRenderer().setSeriesPaint(9, Color.LIGHT_GRAY); // Light Gray for Misc + plot.getRenderer().setSeriesPaint(10, Color.WHITE); // Payload Fullness + + // Set line thickness + plot.getRenderer().setSeriesStroke(0, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(1, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(2, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(3, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(4, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(5, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(6, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(7, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(8, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(9, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(10, new java.awt.BasicStroke(1.2f)); // Payload Fullness + + // Hide axis tick labels (the numbers on the axes) + plot.getDomainAxis().setTickLabelsVisible(false); + plot.getRangeAxis().setTickLabelsVisible(false); + + // Second Y-axis for transaction count + NumberAxis transactionAxis = new NumberAxis(null); // No label for the second axis + transactionAxis.setTickLabelsVisible(false); + XYSeriesCollection barDataset = new XYSeriesCollection(); // All Txs + barDataset.addSeries(systemTransactionsPerBlockSeries); // System Txs + barDataset.addSeries(allTransactionsPerBlockSeries); // Series 1: All Txs + plot.setRangeAxis(1, transactionAxis); + plot.setDataset(1, barDataset); + plot.mapDatasetToRangeAxis(1, 1); + + // Renderer for transaction bars + XYBarRenderer transactionRenderer = new XYBarRenderer(0); // Set margin to 0 to remove gaps + transactionRenderer.setBarPainter(new StandardXYBarPainter()); + transactionRenderer.setShadowVisible(false); + transactionRenderer.setSeriesPaint(0, new Color(0, 0, 255, 128)); // Blue, semi-transparent + transactionRenderer.setSeriesPaint(1, new Color(255, 165, 0, 128)); // Orange, semi-transparent + plot.setRenderer(1, transactionRenderer); + + // Remove all padding around the plot area + plot.setInsets(new RectangleInsets(0, 0, 0, 0)); + plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); + + ChartPanel chartPanel = new ChartPanel(chart); + chartPanel.setPreferredSize(chartDimension1); + chartPanel.setMinimumSize(chartDimension1); + chartPanel.setMaximumSize(chartDimension1); + return chartPanel; + } + + private ChartPanel createNetSpeedChartPanel() { + uploadSpeedSeries = new XYSeries("Upload Speed"); + downloadSpeedSeries = new XYSeries("Download Speed"); + uploadVolumeSeries = new XYSeries("Upload Volume"); + downloadVolumeSeries = new XYSeries("Download Volume"); + + XYSeriesCollection lineDataset = new XYSeriesCollection(); + lineDataset.addSeries(uploadSpeedSeries); + lineDataset.addSeries(downloadSpeedSeries); + + JFreeChart chart = ChartFactory.createXYLineChart( + null, // No title + null, // No X-axis label + null, // No Y-axis label + lineDataset); + + // Remove the legend to maximize plot area + chart.removeLegend(); + chart.setBorderVisible(false); + + XYPlot plot = chart.getXYPlot(); + plot.getDomainAxis().setLowerMargin(0.0); + plot.getDomainAxis().setUpperMargin(0.0); + plot.setBackgroundPaint(Color.DARK_GRAY); + plot.setDomainGridlinesVisible(false); + plot.setRangeGridlinesVisible(false); + + plot.getRenderer().setSeriesPaint(0, new Color(128, 0, 0)); // Upload - Red, semi-transparent + plot.getRenderer().setSeriesPaint(1, new Color(0, 100, 0)); // Download - Green, semi-transparent + + // Set line thickness + plot.getRenderer().setSeriesStroke(0, new java.awt.BasicStroke(1.2f)); + plot.getRenderer().setSeriesStroke(1, new java.awt.BasicStroke(1.2f)); + + // Hide axis tick labels (the numbers on the axes) + plot.getDomainAxis().setTickLabelsVisible(false); + plot.getRangeAxis().setTickLabelsVisible(false); + + // Second Y-axis for volume + NumberAxis volumeAxis = new NumberAxis(null); // No label for the second axis + volumeAxis.setTickLabelsVisible(false); + plot.setRangeAxis(1, volumeAxis); // Use axis index 1 for volume + + // A single dataset and renderer for both volume series. + XYSeriesCollection volumeDataset = new XYSeriesCollection(); + volumeDataset.addSeries(downloadVolumeSeries); // Series 0: Download (top layer) + volumeDataset.addSeries(uploadVolumeSeries); // Series 1: Upload (bottom layer) + + XYStepAreaRenderer volumeRenderer = new XYStepAreaRenderer(); + volumeRenderer.setShapesVisible(false); + volumeRenderer.setSeriesPaint(0, new Color(50, 205, 50, 128)); // Download - Green + volumeRenderer.setSeriesPaint(1, new Color(233, 150, 122, 128)); // Upload - Red + plot.setDataset(1, volumeDataset); + plot.setRenderer(1, volumeRenderer); + plot.mapDatasetToRangeAxis(1, 1); + + // Remove all padding around the plot area + plot.setInsets(new RectangleInsets(0, 0, 0, 0)); + plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); + + ChartPanel chartPanel = new ChartPanel(chart); + chartPanel.setPreferredSize(chartDimension2); + chartPanel.setMinimumSize(chartDimension2); + chartPanel.setMaximumSize(chartDimension2); + return chartPanel; + } + + private static class PerformanceUpdateData { + Map progressBarUpdates = new HashMap<>(); + Map seriesUpdates = new HashMap<>(); + } + + // DTO for shared bar chart updates + private static class SharedBarChartUpdateData { + Map progressBarUpdates = new HashMap<>(); + Map seriesUpdates = new HashMap<>(); + } + private JProgressBar syncProgressBarUnverifiedBlocks; private final JFrame parentFrame; public MetricsPanel(JFrame parentFrame) { super(new GridBagLayout()); - this.parentFrame = parentFrame; - atCountPerBlockSeries = new XYSeries("ATs/Block (MA)"); - transactionsPerBlockSeries = new XYSeries("All Txs/Block (MA)"); // Orange - atTransactionsPerBlockSeries = new XYSeries("System Txs/Block (MA)"); // Blue - performanceChartPanel = createPerformanceChartPanel(); - timingChartPanel = createTimingChartPanel(); - netSpeedChartPanel = createNetSpeedChartPanel(); - layoutComponents(); + try { + this.parentFrame = parentFrame; + atCountPerBlockSeries = new XYSeries("ATs/Block (MA)"); + allTransactionsPerBlockSeries = new XYSeries("All Txs/Block (MA)"); // Orange + systemTransactionsPerBlockSeries = new XYSeries("System Txs/Block (MA)"); // Blue + payloadFullnessSeries = new XYSeries("Payload Fullness (MA)"); + performanceChartPanel = createPerformanceChartPanel(); + timingChartPanel = createTimingChartPanel(); + netSpeedChartPanel = createNetSpeedChartPanel(); + layoutComponents(); + } catch (Exception e) { + LOGGER.error("Failed to initialize MetricsPanel", e); + throw new RuntimeException("Could not initialize MetricsPanel", e); + } } public void init() { - oclUnverifiedQueueThreshold = Signum.getPropertyService().getInt(Props.GPU_UNVERIFIED_QUEUE); - initListeners(); + try { + oclUnverifiedQueueThreshold = Signum.getPropertyService().getInt(Props.GPU_UNVERIFIED_QUEUE); + maxUnverifiedQueueSize = Signum.getPropertyService().getInt(Props.P2P_MAX_BLOCKS); + maxUnconfirmedTxs = Signum.getPropertyService().getInt(Props.P2P_MAX_UNCONFIRMED_TRANSACTIONS); + maxPayloadSize = (Signum.getFluxCapacitor().getValue(brs.fluxcapacitor.FluxValues.MAX_PAYLOAD_LENGTH, + Signum.getBlockchain().getHeight()) / 1024); + String payloadTooltip = "Shows the percentage of the block's data section (payload) that is filled with transactions. This is a measure of block space utilization and network activity.\n\n" + + "The maximum payload size is currently " + + maxPayloadSize + + " KB.\n\nLegend:\n- Moving Average\n- C: Current block fullness\n- min: Minimum value in the window\n- max: Maximum value in the window"; + addInfoTooltip(payloadFullnessLabel, payloadTooltip); + + downloadCacheSize = Signum.getPropertyService().getInt(Props.BRS_BLOCK_CACHE_MB); + String cacheTooltip = "The percentage of the allocated download cache memory that is currently in use.\n\nThis indicates how much space is available for downloading new blocks before they are processed and added to the blockchain.\n\n" + + "Configured cache size: " + downloadCacheSize + + " MB."; + addInfoTooltip(cacheFullnessLabel, cacheTooltip); + + syncProgressBarUnverifiedBlocks.setMaximum(maxUnverifiedQueueSize); + unconfirmedTxsProgressBar.setMaximum(maxUnconfirmedTxs); + unconfirmedTxsProgressBar.setString(0 + " / " + maxUnconfirmedTxs); + initListeners(); + LOGGER.info("MetricsPanel initialized successfully"); + } catch (Exception e) { + LOGGER.error("Failed to initialize MetricsPanel components", e); + throw new RuntimeException("Could not initialize MetricsPanel components", e); + } } private void layoutComponents() { @@ -172,81 +480,98 @@ private void layoutComponents() { // SyncPanel (Progress Bars) JPanel SyncPanel = new JPanel(new GridBagLayout()); - // Verified/Total Blocks - tooltip = "Shows the number of blocks in the download queue that have passed PoC verification against the total number of blocks in the queue.\n\n- Verified: PoC signature has been checked (CPU/GPU intensive).\n- Total: All blocks currently in the download queue.\n\nA high number of unverified blocks may indicate a slow verification process."; - JLabel verifLabel = createLabel("Verified/Total Blocks", null, tooltip); - syncProgressBarDownloadedBlocks = createProgressBar(0, 100, Color.GREEN, "0 / 0 - 0%", progressBarSize1); - addComponent(SyncPanel, verifLabel, 0, 0, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, + // Cache Fullness + cacheFullnessLabel = createLabel("Download Cache", null, null); // Tooltip is set in init() + cacheFullnessProgressBar = createProgressBar(0, 100, Color.ORANGE, "0 / 0 MB | 0%", progressBarSize1); + addComponent(SyncPanel, cacheFullnessLabel, 0, 0, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, + labelInsets); + addComponent(SyncPanel, cacheFullnessProgressBar, 1, 0, 1, 1, 0, GridBagConstraints.LINE_START, + GridBagConstraints.HORIZONTAL, barInsets); + + // Verified / Total Blocks + tooltip = "Shows the number of blocks in the download queue that have passed PoC verification against the total number of blocks in the queue.\n\n- Verified: PoC signature has been checked (CPU/GPU intensive).\n- Total: All blocks currently in the download queue.\n\nA high number of unverified blocks may indicate a slow verification process.\n\nThe progress bar displays: Verified Blocks / Total Blocks - Percentage of Verified Blocks."; + JLabel verifLabel = createLabel("Verified / Total Blocks", null, tooltip); + syncProgressBarDownloadedBlocks = createProgressBar(0, 100, Color.GREEN, "0 / 0 0%", progressBarSize1); + addComponent(SyncPanel, verifLabel, 0, 1, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, syncProgressBarDownloadedBlocks, 1, 0, 1, 1, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, syncProgressBarDownloadedBlocks, 1, 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // Unverified Blocks - tooltip = "The number of blocks in the download queue that are waiting for Proof-of-Capacity (PoC) verification.\n\nA persistently high number might indicate that the CPU or GPU is a bottleneck and cannot keep up with the network's block generation rate."; + tooltip = "The number of blocks in the download queue that are waiting for Proof-of-Capacity (PoC) verification.\n\nA persistently high number might indicate that the CPU or GPU is a bottleneck and cannot keep up with the network's block generation rate.\n\nThe progress bar displays the current count of unverified blocks."; JLabel unVerifLabel = createLabel("Unverified Blocks", null, tooltip); syncProgressBarUnverifiedBlocks = createProgressBar(0, 2000, Color.GREEN, "0", progressBarSize1); - addComponent(SyncPanel, unVerifLabel, 0, 1, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, + addComponent(SyncPanel, unVerifLabel, 0, 2, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, syncProgressBarUnverifiedBlocks, 1, 1, 1, 1, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, syncProgressBarUnverifiedBlocks, 1, 2, 1, 1, 0, GridBagConstraints.LINE_START, + GridBagConstraints.HORIZONTAL, barInsets); + + // Unconfirmed Transactions + tooltip = "The number of transactions waiting in the memory pool to be included in the next block.\n\nA high number indicates significant network activity. If this number grows continuously without being cleared, it might suggest that transaction fees are too low or the network is under heavy load.\n\nThe progress bar displays the current count of unconfirmed transactions."; + JLabel unconfirmedTxsLabel = createLabel("Unconfirmed Txs", null, tooltip); + unconfirmedTxsProgressBar = createProgressBar(0, 1000, Color.GREEN, "0 / 0", progressBarSize1); + addComponent(SyncPanel, unconfirmedTxsLabel, 0, 3, 1, 0, 0, GridBagConstraints.LINE_END, + GridBagConstraints.NONE, labelInsets); + addComponent(SyncPanel, unconfirmedTxsProgressBar, 1, 3, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // Separator JSeparator separator1 = new JSeparator(SwingConstants.HORIZONTAL); - addComponent(SyncPanel, separator1, 0, 2, 2, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, + addComponent(SyncPanel, separator1, 0, 4, 2, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, barInsets); // Blocks/Second (Moving Average) - tooltip = "The moving average of blocks processed per second. This is a key indicator of the node's synchronization speed.\n\nA higher value means the node is rapidly catching up with the current state of the blockchain. This metric is particularly useful during the initial sync or after a period of being offline."; + tooltip = "The moving average of blocks processed per second. This is a key indicator of the node's synchronization speed.\n\nA higher value means the node is rapidly catching up with the current state of the blockchain. This metric is particularly useful during the initial sync or after a period of being offline.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; JLabel blocksPerSecondLabel = createLabel("Blocks/Sec (MA)", Color.CYAN, tooltip); - blocksPerSecondProgressBar = createProgressBar(0, 200, null, "0", progressBarSize1); - addComponent(SyncPanel, blocksPerSecondLabel, 0, 3, 1, 0, 0, GridBagConstraints.LINE_END, + blocksPerSecondProgressBar = createProgressBar(0, 200, null, "0.00 - max: 0.00", progressBarSize1); + addComponent(SyncPanel, blocksPerSecondLabel, 0, 5, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, blocksPerSecondProgressBar, 1, 3, 1, 0, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, blocksPerSecondProgressBar, 1, 5, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // All Transactions/Second (Moving Average) - tooltip = "The moving average of the total number of transactions (user-submitted and AT-generated) processed per second. This metric reflects the total transactional throughput of the network as seen by your node.\n\nIncludes:\n- Payments (Ordinary, Multi-Out, Multi-Same-Out)\n- Messages (Arbitrary, Alias, Account Info, TLD)\n- Assets (Issuance, Transfer, Orders, Minting, Distribution)\n- Digital Goods (Listing, Delisting, Price Change, Quantity Change, Purchase, Delivery, Feedback, Refund)\n- Account Control (Leasing)\n- Mining (Reward Recipient, Commitment)\n- Advanced Payments (Escrow, Subscriptions)\n- Automated Transactions (ATs)"; + tooltip = "The moving average of the total number of transactions (user-submitted and AT-generated) processed per second. This metric reflects the total transactional throughput of the network as seen by your node.\n\nIncludes:\n- Payments (Ordinary, Multi-Out, Multi-Same-Out)\n- Messages (Arbitrary, Alias, Account Info, TLD)\n- Assets (Issuance, Transfer, Orders, Minting, Distribution)\n- Digital Goods (Listing, Delisting, Price Change, Quantity Change, Purchase, Delivery, Feedback, Refund)\n- Account Control (Leasing)\n- Mining (Reward Recipient, Commitment)\n- Advanced Payments (Escrow, Subscriptions)\n- Automated Transactions (ATs)\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; JLabel txPerSecondLabel = createLabel("All Txs/Sec (MA)", Color.GREEN, tooltip); - transactionsPerSecondProgressBar = createProgressBar(0, 2000, null, "0", progressBarSize1); - addComponent(SyncPanel, txPerSecondLabel, 0, 4, 1, 0, 0, GridBagConstraints.LINE_END, + allTransactionsPerSecondProgressBar = createProgressBar(0, 2000, null, "0.00 - max: 0.00", progressBarSize1); + addComponent(SyncPanel, txPerSecondLabel, 0, 6, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, transactionsPerSecondProgressBar, 1, 4, 1, 0, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, allTransactionsPerSecondProgressBar, 1, 6, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // All Transactions/Block (Moving Average) - tooltip = "The moving average of the total number of transactions (user-submitted and AT-generated) included in each block. This metric provides insight into the network's activity and block space utilization.\n\nIncludes:\n- Payments (Ordinary, Multi-Out, Multi-Same-Out)\n- Messages (Arbitrary, Alias, Account Info, TLD)\n- Assets (Issuance, Transfer, Orders, Minting, Distribution)\n- Digital Goods (Listing, Delisting, Price Change, Quantity Change, Purchase, Delivery, Feedback, Refund)\n- Account Control (Leasing)\n- Mining (Reward Recipient, Commitment)\n- Advanced Payments (Escrow, Subscriptions)\n- Automated Transactions (ATs)"; + tooltip = "The moving average of the total number of transactions (user-submitted and AT-generated) included in each block. This metric provides insight into the network's activity and block space utilization.\n\nIncludes:\n- Payments (Ordinary, Multi-Out, Multi-Same-Out)\n- Messages (Arbitrary, Alias, Account Info, TLD)\n- Assets (Issuance, Transfer, Orders, Minting, Distribution)\n- Digital Goods (Listing, Delisting, Price Change, Quantity Change, Purchase, Delivery, Feedback, Refund)\n- Account Control (Leasing)\n- Mining (Reward Rec. Assignment, Commitment)\n- Advanced Payments (Escrow, Subscriptions)\n- Automated Transactions (ATs)\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; JLabel txPerBlockLabel = createLabel("All Txs/Block (MA)", new Color(255, 165, 0), tooltip); // Orange - transactionsPerBlockProgressBar = createProgressBar(0, 255, null, "0", progressBarSize1); - addComponent(SyncPanel, txPerBlockLabel, 0, 5, 1, 0, 0, GridBagConstraints.LINE_END, + allTransactionsPerBlockProgressBar = createProgressBar(0, 255, null, "0.00 - max: 0.00", progressBarSize1); + addComponent(SyncPanel, txPerBlockLabel, 0, 7, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, transactionsPerBlockProgressBar, 1, 5, 1, 0, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, allTransactionsPerBlockProgressBar, 1, 7, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // System Transactions/Second (Moving Average) - tooltip = "The moving average of system-generated transactions processed per second. This includes payments from Automated Transactions (ATs), Escrow results, and Subscription payments."; + tooltip = "The moving average of system-generated transactions processed per second. This includes payments from Automated Transactions (ATs), Escrow results, and Subscription payments.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; systemTxPerSecondLabel = createLabel("System Txs/Sec (MA)", new Color(135, 206, 250), tooltip); // LightSkyBlue - systemTransactionsPerSecondProgressBar = createProgressBar(0, 2000, null, "0", progressBarSize1); - addComponent(SyncPanel, systemTxPerSecondLabel, 0, 6, 1, 0, 0, GridBagConstraints.LINE_END, + systemTransactionsPerSecondProgressBar = createProgressBar(0, 2000, null, "0.00 - max: 0.00", progressBarSize1); + addComponent(SyncPanel, systemTxPerSecondLabel, 0, 8, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, systemTransactionsPerSecondProgressBar, 1, 6, 1, 0, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, systemTransactionsPerSecondProgressBar, 1, 8, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // System Transactions/Block (Moving Average) - tooltip = "The moving average of system-generated transactions included in each block. This includes payments from Automated Transactions (ATs), Escrow results, and Subscription payments."; - systemTxPerBlockLabel = createLabel("System Txs/Block (MA)", Color.BLUE, tooltip); - atTransactionsPerBlockProgressBar = createProgressBar(0, 255, null, "0", progressBarSize1); - addComponent(SyncPanel, systemTxPerBlockLabel, 0, 7, 1, 0, 0, GridBagConstraints.LINE_END, + tooltip = "The moving average of system-generated transactions included in each block. This includes payments from Automated Transactions (ATs), Escrow results, and Subscription payments.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; + systemTxPerBlockLabel = createLabel("System Txs/Block (MA)", new Color(0, 0, 255, 128), tooltip); + systemTransactionsPerBlockProgressBar = createProgressBar(0, 255, null, "0.00 - max: 0.00", progressBarSize1); + addComponent(SyncPanel, systemTxPerBlockLabel, 0, 9, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, atTransactionsPerBlockProgressBar, 1, 7, 1, 0, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, systemTransactionsPerBlockProgressBar, 1, 9, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // ATs/Block (Moving Average) - tooltip = "The moving average of the number of Automated Transactions (ATs) executed per block. This metric shows the activity level of smart contracts on the network."; + tooltip = "The moving average of the number of Automated Transactions (ATs) executed per block. This metric shows the activity level of smart contracts on the network.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; atCountLabel = createLabel("ATs/Block (MA)", new Color(153, 0, 76), tooltip); // Deep Pink - atCountProgressBar = createProgressBar(0, 100, null, "0", progressBarSize1); - addComponent(SyncPanel, atCountLabel, 0, 8, 1, 0, 0, GridBagConstraints.LINE_END, + atCountsPerBlockProgressBar = createProgressBar(0, 100, null, "0.00 - max: 0.00", progressBarSize1); + addComponent(SyncPanel, atCountLabel, 0, 10, 1, 0, 0, GridBagConstraints.LINE_END, GridBagConstraints.NONE, labelInsets); - addComponent(SyncPanel, atCountProgressBar, 1, 8, 1, 0, 0, GridBagConstraints.LINE_START, + addComponent(SyncPanel, atCountsPerBlockProgressBar, 1, 10, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, barInsets); // Add SyncPanel to performanceMetricsPanel @@ -263,7 +588,7 @@ private void layoutComponents() { addToggleListener(blocksPerSecondLabel, performanceChartPanel, 0, 0); addToggleListener(txPerSecondLabel, performanceChartPanel, 0, 1); addToggleListener(systemTxPerSecondLabel, performanceChartPanel, 0, 2); - addToggleListener(atCountLabel, performanceChartPanel, 0, 3); + addToggleListener(atCountLabel, performanceChartPanel, 0, 3); // atCountPerBlockSeries addDualChartToggleListener(txPerBlockLabel, performanceChartPanel, 1, 1, timingChartPanel, 1, 1); // End Performance Metrics Panel @@ -279,18 +604,18 @@ private void layoutComponents() { // --- Row 1: Push Time / Validation Time --- // Push Time (Left) - tooltip = "The moving average of the total time taken to process and push a new block. This value is the sum of all individual timing components measured during block processing.\n\nIt includes:\n- Validation Time\n- TX Loop Time\n- Housekeeping Time\n- TX Apply Time\n- AT Time\n- Subscription Time\n- Block Apply Time\n- Commit Time\n- Complementer (miscellaneous) Time"; + tooltip = "The moving average of the total time taken to process and push a new block. This value is the sum of all individual timing components measured during block processing.\n\nIt includes:\n- Validation Time\n- TX Loop Time\n- Housekeeping Time\n- TX Apply Time\n- AT Time\n- Subscription Time\n- Block Apply Time\n- Commit Time\n- Complementer (miscellaneous) Time\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; pushTimeLabel = createLabel("Push Time (MA)", Color.BLUE, tooltip); - pushTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + pushTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, pushTimeLabel, 0, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, pushTimeProgressBar, 0, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, timerBarInsets); // Validation Time (Right) - tooltip = "The moving average of the time spent on block-level validation, excluding the per-transaction validation loop. This is a CPU-intensive task.\n\nMeasured steps include:\n- Verifying block version and timestamp\n- Checking previous block reference\n- Verifying block and generation signatures\n- Validating payload hash and total amounts/fees after transaction processing"; + tooltip = "The moving average of the time spent on block-level validation, excluding the per-transaction validation loop. This is a CPU-intensive task.\n\nMeasured steps include:\n- Verifying block version and timestamp\n- Checking previous block reference\n- Verifying block and generation signatures\n- Validating payload hash and total amounts/fees after transaction processing\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; validationTimeLabel = createLabel("Validation Time (MA)", Color.YELLOW, tooltip); - validationTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + validationTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, validationTimeLabel, 2, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, validationTimeProgressBar, 2, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, @@ -299,18 +624,18 @@ private void layoutComponents() { // --- Row 2: TX Loop / Housekeeping --- // TX Loop Time (Left) - tooltip = "The moving average of the time spent iterating through and validating all transactions within a block. This involves both CPU and database read operations.\n\nFor each transaction, this includes:\n- Checking timestamps and deadlines\n- Verifying signatures and public keys\n- Validating referenced transactions\n- Checking for duplicates\n- Executing transaction-specific business logic"; + tooltip = "The moving average of the time spent iterating through and validating all transactions within a block. This involves both CPU and database read operations.\n\nFor each transaction, this includes:\n- Checking timestamps and deadlines\n- Verifying signatures and public keys\n- Validating referenced transactions\n- Checking for duplicates\n- Executing transaction-specific business logic\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; txLoopTimeLabel = createLabel("TX Loop Time (MA)", new Color(128, 0, 128), tooltip); - txLoopTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + txLoopTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, txLoopTimeLabel, 0, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, txLoopTimeProgressBar, 0, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, timerBarInsets); // Housekeeping Time (Right) - tooltip = "The moving average of the time spent on various 'housekeeping' tasks during block processing.\n\nThis includes:\n- Re-queuing unconfirmed transactions that were not included in the new block\n- Updating peer states and other miscellaneous tasks"; + tooltip = "The moving average of the time spent on various 'housekeeping' tasks during block processing.\n\nThis includes:\n- Re-queuing unconfirmed transactions that were not included in the new block\n- Updating peer states and other miscellaneous tasks\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; housekeepingTimeLabel = createLabel("Housekeeping Time (MA)", new Color(42, 223, 223), tooltip); - housekeepingTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + housekeepingTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, housekeepingTimeLabel, 2, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, housekeepingTimeProgressBar, 2, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, @@ -319,18 +644,18 @@ private void layoutComponents() { // --- Row 3: TX Apply / AT Time --- // TX Apply Time (Left) - tooltip = "The moving average of the time spent applying the effects of each transaction within the block to the in-memory state. This step handles changes to account balances, aliases, assets, etc., based on the transaction type. It is the first major operation within the 'apply' phase."; + tooltip = "The moving average of the time spent applying the effects of each transaction within the block to the in-memory state. This step handles changes to account balances, aliases, assets, etc., based on the transaction type. It is the first major operation within the 'apply' phase.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; txApplyTimeLabel = createLabel("TX Apply Time (MA)", new Color(255, 165, 0), tooltip); - txApplyTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + txApplyTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, txApplyTimeLabel, 0, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, txApplyTimeProgressBar, 0, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, timerBarInsets); // AT Time (Right) - tooltip = "The moving average of the time spent validating and processing all Automated Transactions (ATs) within the block. This is a separate computational step that occurs after 'TX Apply Time'."; + tooltip = "The moving average of the time spent validating and processing all Automated Transactions (ATs) within the block. This is a separate computational step that occurs after 'TX Apply Time'.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; atTimeLabel = createLabel("AT Time (MA)", new Color(153, 0, 76), tooltip); - atTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + atTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, atTimeLabel, 2, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, atTimeProgressBar, 2, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, @@ -339,18 +664,18 @@ private void layoutComponents() { // --- Row 4: Subscription / Block Apply --- // Subscription Time (Left) - tooltip = "The moving average of the time spent processing recurring subscription payments for the block. This is a separate step that occurs after AT processing."; + tooltip = "The moving average of the time spent processing recurring subscription payments for the block. This is a separate step that occurs after AT processing.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; subscriptionTimeLabel = createLabel("Subscription Time (MA)", new Color(255, 105, 100), tooltip); // Hot pink - subscriptionTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + subscriptionTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, subscriptionTimeLabel, 0, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, subscriptionTimeProgressBar, 0, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, timerBarInsets); // Block Apply Time (Right) - tooltip = "The moving average of the time spent applying block-level changes. This includes distributing the block reward to the generator, updating escrow services, and notifying listeners about the applied block. This is the final step before the 'Commit' phase."; + tooltip = "The moving average of the time spent applying block-level changes. This includes distributing the block reward to the generator, updating escrow services, and notifying listeners about the applied block. This is the final step before the 'Commit' phase.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; blockApplyTimeLabel = createLabel("Block Apply Time (MA)", new Color(0, 100, 100), tooltip); // Teal - blockApplyTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + blockApplyTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, blockApplyTimeLabel, 2, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, blockApplyTimeProgressBar, 2, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, @@ -359,25 +684,35 @@ private void layoutComponents() { // --- Row 5: Commit / Misc. Time --- // Commit Time (Left) - tooltip = "The moving average of the time spent committing all in-memory state changes to the database on disk. This is a disk I/O-intensive operation."; + tooltip = "The moving average of the time spent committing all in-memory state changes to the database on disk. This is a disk I/O-intensive operation.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; commitTimeLabel = createLabel("Commit Time (MA)", new Color(150, 0, 200), tooltip); - commitTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + commitTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, commitTimeLabel, 0, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, commitTimeProgressBar, 0, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, timerBarInsets); // Misc. Time (Right) - tooltip = "The moving average of the time spent on miscellaneous operations not explicitly measured in other timing categories. This value is the difference between the 'Total Push Time' and the sum of all other measured components (Validation, TX Loop, Housekeeping, TX Apply, AT, Subscription, Block Apply, and Commit). A consistently high value may indicate performance overhead in parts of the code that are not individually timed, such as memory management or other background tasks."; + tooltip = "The moving average of the time spent on miscellaneous operations not explicitly measured in other timing categories. This value is the difference between the 'Total Push Time' and the sum of all other measured components (Validation, TX Loop, Housekeeping, TX Apply, AT, Subscription, Block Apply, and Commit). A consistently high value may indicate performance overhead in parts of the code that are not individually timed, such as memory management or other background tasks.\n\nThe progress bar displays: Current MA Value - Max MA Value seen in this session."; miscTimeLabel = createLabel("Misc. Time (MA)", Color.LIGHT_GRAY, tooltip); - miscTimeProgressBar = createProgressBar(0, 100, null, "0 ms", progressBarSize1); + miscTimeProgressBar = createProgressBar(0, 100, null, "0 ms - max: 0 ms", progressBarSize1); addComponent(timingInfoPanel, miscTimeLabel, 2, y, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, timerLabelInsets); addComponent(timingInfoPanel, miscTimeProgressBar, 2, y + 1, 1, 1, 0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, timerBarInsets); y += 2; - // Add timingInfoPanel to timingMetricsPanel + // --- Row 6: Payload Fullness --- + payloadFullnessLabel = createLabel("Payload Fullness (MA)", Color.WHITE, null); // Tooltip is set in init() + Dimension wideProgressBarSize = new Dimension(progressBarSize1.width * 2 + 5, progressBarSize1.height); + payloadFullnessProgressBar = createProgressBar(0, 100, null, + "0% - C: 0% (0 / 0 bytes) - min: 0% - max: 0%", wideProgressBarSize); + addComponent(timingInfoPanel, payloadFullnessLabel, 0, y + 2, 3, 1, 0, GridBagConstraints.CENTER, + GridBagConstraints.NONE, timerLabelInsets); + addComponent(timingInfoPanel, payloadFullnessProgressBar, 0, y + 3, 3, 1, 0, GridBagConstraints.CENTER, + GridBagConstraints.HORIZONTAL, timerBarInsets); + + // Add timingInfoPanel to timingMetricsPanel addComponent(timingMetricsPanel, timingInfoPanel, 0, 0, 1, 0, 0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0)); @@ -405,22 +740,44 @@ private void layoutComponents() { barInsets); // --- Upload Speed --- - tooltip = "The moving average of data upload speed to other peers in the network.\n\nThis reflects how much data your node is sharing, which includes:\n- Blocks\n- Transactions\n- Peer information"; + JPanel uploadSpeedPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0)); + uploadSpeedPanel.setOpaque(false); + tooltip = "The moving average of data upload speed to other peers in the network.\n\nThis reflects how much data your node is sharing, which includes:\n- Blocks\n- Transactions\n- Peer information\n\nThe progress bar displays the current moving average speed."; uploadSpeedLabel = createLabel("▲ Speed (MA)", new Color(128, 0, 0), tooltip); - uploadSpeedProgressBar = createProgressBar(0, MAX_SPEED_BPS, null, "0 B/s", progressBarSize2); - addComponent(netSpeedInfoPanel, uploadSpeedLabel, 0, 1, 1, 0, 0, GridBagConstraints.LINE_END, + uploadSpeedProgressBar = createProgressBar(0, MAX_SPEED_BPS, null, "0.00 B/s", progressBarSize2); + uploadSpeedPanel.add(uploadSpeedLabel); + uploadSpeedPanel.add(uploadSpeedProgressBar); + addComponent(netSpeedInfoPanel, uploadSpeedPanel, 0, 1, 2, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, labelInsets); - addComponent(netSpeedInfoPanel, uploadSpeedProgressBar, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START, - GridBagConstraints.HORIZONTAL, barInsets); + + /* + * addComponent(netSpeedInfoPanel, uploadSpeedLabel, 0, 1, 1, 0, 0, + * GridBagConstraints.LINE_END, + * GridBagConstraints.NONE, labelInsets); + * addComponent(netSpeedInfoPanel, uploadSpeedProgressBar, 1, 1, 1, 0, 0, + * GridBagConstraints.LINE_START, + * GridBagConstraints.HORIZONTAL, barInsets); + */ // --- Download Speed --- - tooltip = "The moving average of data download speed from other peers in the network.\n\nThis indicates how quickly your node is receiving data, which includes:\n- Blocks\n- Transactions\n- Peer information"; + JPanel downloadSpeePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0)); + downloadSpeePanel.setOpaque(false); + tooltip = "The moving average of data download speed from other peers in the network.\n\nThis indicates how quickly your node is receiving data, which includes:\n- Blocks\n- Transactions\n- Peer information\n\nThe progress bar displays the current moving average speed."; downloadSpeedLabel = createLabel("▼ Speed (MA)", new Color(0, 100, 0), tooltip); - downloadSpeedProgressBar = createProgressBar(0, MAX_SPEED_BPS, null, "0 B/s", progressBarSize2); - addComponent(netSpeedInfoPanel, downloadSpeedLabel, 0, 2, 1, 0, 0, GridBagConstraints.LINE_END, + downloadSpeedProgressBar = createProgressBar(0, MAX_SPEED_BPS, null, "0.00 B/s", progressBarSize2); + downloadSpeePanel.add(downloadSpeedLabel); + downloadSpeePanel.add(downloadSpeedProgressBar); + addComponent(netSpeedInfoPanel, downloadSpeePanel, 0, 2, 2, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, labelInsets); - addComponent(netSpeedInfoPanel, downloadSpeedProgressBar, 1, 2, 1, 0, 0, GridBagConstraints.LINE_START, - GridBagConstraints.HORIZONTAL, barInsets); + + /* + * addComponent(netSpeedInfoPanel, downloadSpeedLabel, 0, 2, 1, 0, 0, + * GridBagConstraints.LINE_END, + * GridBagConstraints.NONE, labelInsets); + * addComponent(netSpeedInfoPanel, downloadSpeedProgressBar, 1, 2, 1, 0, 0, + * GridBagConstraints.LINE_START, + * GridBagConstraints.HORIZONTAL, barInsets); + */ // --- Combined Volume --- JPanel combinedVolumePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0)); @@ -431,6 +788,7 @@ private void layoutComponents() { metricsUploadVolumeLabel = createLabel("", new Color(233, 150, 122), tooltip); tooltip = "The total amount of data downloaded from the network during this session."; metricsDownloadVolumeLabel = createLabel("", new Color(50, 205, 50), tooltip); + combinedVolumePanel.add(volumeTitleLabel); combinedVolumePanel.add(metricsUploadVolumeLabel); combinedVolumePanel.add(new JLabel(" / ")); @@ -439,27 +797,64 @@ private void layoutComponents() { GridBagConstraints.NONE, barInsets); // --- Moving Average Window --- + JPanel maWindowPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0)); + maWindowPanel.setOpaque(false); tooltip = "The number of recent blocks used to calculate the moving average for performance metrics. A larger window provides a smoother but less responsive trend, while a smaller window is more reactive to recent changes."; JLabel maWindowLabel = createLabel("MA Window (Blocks)", null, tooltip); - - // Define the discrete values for the dropdown - final Integer[] maWindowValues = { 10, 100, 200, 300, 400, 500 }; - JComboBox movingAverageComboBox = new JComboBox<>(maWindowValues); - movingAverageComboBox.setSelectedItem(movingAverageWindow); - movingAverageComboBox.setPreferredSize(new Dimension(150, 25)); - - movingAverageComboBox.addActionListener(e -> { - JComboBox source = (JComboBox) e.getSource(); - Object selectedItem = source.getSelectedItem(); - if (selectedItem instanceof Integer) { - movingAverageWindow = (Integer) selectedItem; + maWindowPanel.add(maWindowLabel); + + final int[] maWindowValues = { 10, 100, 200, 300, 400, 500 }; + int initialSliderValue = 1; // Default to 100 + for (int i = 0; i < maWindowValues.length; i++) { + if (movingAverageWindow == maWindowValues[i]) { + initialSliderValue = i; + break; } + } + + JSlider movingAverageSlider = new JSlider(JSlider.HORIZONTAL, 0, maWindowValues.length - 1, initialSliderValue); + movingAverageSlider.setMajorTickSpacing(1); + movingAverageSlider.setPaintTicks(true); + movingAverageSlider.setSnapToTicks(true); + movingAverageSlider.setPreferredSize(new Dimension(150, 40)); + + java.util.Hashtable labelTable = new java.util.Hashtable<>(); + for (int i = 0; i < maWindowValues.length; i++) { + labelTable.put(i, new JLabel(String.valueOf(maWindowValues[i]))); + } + movingAverageSlider.setLabelTable(labelTable); + movingAverageSlider.setPaintLabels(true); + + movingAverageSlider.addChangeListener(e -> { + JSlider source = (JSlider) e.getSource(); + movingAverageWindow = maWindowValues[source.getValue()]; + blockTimestamps.setWindowSize(movingAverageWindow); + allTransactionsPerBlock.setWindowSize(movingAverageWindow); + systemTransactionsPerBlock.setWindowSize(movingAverageWindow); + pushTimes.setWindowSize(movingAverageWindow); + validationTimes.setWindowSize(movingAverageWindow); + txLoopTimes.setWindowSize(movingAverageWindow); + housekeepingTimes.setWindowSize(movingAverageWindow); + txApplyTimes.setWindowSize(movingAverageWindow); + commitTimes.setWindowSize(movingAverageWindow); + atTimes.setWindowSize(movingAverageWindow); + subscriptionTimes.setWindowSize(movingAverageWindow); + blockApplyTimes.setWindowSize(movingAverageWindow); + miscTimes.setWindowSize(movingAverageWindow); + atCountsPerBlock.setWindowSize(movingAverageWindow); }); - addComponent(netSpeedInfoPanel, maWindowLabel, 0, 4, 1, 0, 0, GridBagConstraints.LINE_END, - GridBagConstraints.NONE, labelInsets); - addComponent(netSpeedInfoPanel, movingAverageComboBox, 1, 4, 1, 0, 0, GridBagConstraints.LINE_START, - GridBagConstraints.HORIZONTAL, barInsets); + maWindowPanel.add(movingAverageSlider); + addComponent(netSpeedInfoPanel, maWindowPanel, 0, 4, 2, 0, 0, GridBagConstraints.CENTER, + GridBagConstraints.NONE, new Insets(2, 0, 2, 0)); + + // addComponent(netSpeedInfoPanel, maWindowLabel, 0, 4, 1, 0, 0, + // GridBagConstraints.LINE_END, + // GridBagConstraints.NONE, labelInsets); + + // addComponent(netSpeedInfoPanel, movingAverageComboBox, 1, 4, 1, 0, 0, + // GridBagConstraints.LINE_START, + // GridBagConstraints.HORIZONTAL, barInsets); netSpeedChartContainer.add(netSpeedInfoPanel); addComponent(this, netSpeedChartContainer, 3, 0, 1, 0, 0, GridBagConstraints.NORTH, @@ -489,6 +884,7 @@ private void layoutComponents() { addToggleListener(blockApplyTimeLabel, timingChartPanel, 0, 7); addToggleListener(commitTimeLabel, timingChartPanel, 0, 8); addToggleListener(miscTimeLabel, timingChartPanel, 0, 9); + addToggleListener(payloadFullnessLabel, timingChartPanel, 0, 10); addToggleListener(uploadSpeedLabel, netSpeedChartPanel, 0, 0); addToggleListener(downloadSpeedLabel, netSpeedChartPanel, 0, 1); @@ -499,7 +895,7 @@ private void layoutComponents() { // Timer to periodically update the network speed chart so it flows even with no // traffic - Timer netSpeedChartUpdater = new Timer(100, e -> { + netSpeedChartUpdater = new Timer(100, e -> { updateNetVolumeAndSpeedChart(uploadedVolume, downloadedVolume); }); netSpeedChartUpdater.start(); @@ -513,22 +909,34 @@ private void initListeners() { BlockchainProcessor.Event.NET_VOLUME_CHANGED); blockchainProcessor.addListener(this::onPerformanceStatsUpdated, BlockchainProcessor.Event.PERFORMANCE_STATS_UPDATED); - blockchainProcessor.addListener(this::onBlockPushed, BlockchainProcessor.Event.BLOCK_PUSHED); + brs.TransactionProcessor transactionProcessor = Signum.getTransactionProcessor(); + transactionProcessor.addListener(transactions -> onUnconfirmedTransactionCountChanged(), + brs.TransactionProcessor.Event.ADDED_UNCONFIRMED_TRANSACTIONS); + transactionProcessor.addListener(transactions -> onUnconfirmedTransactionCountChanged(), + brs.TransactionProcessor.Event.REMOVED_UNCONFIRMED_TRANSACTIONS); } } public void shutdown() { chartUpdateExecutor.shutdown(); + if (netSpeedChartUpdater != null) { + netSpeedChartUpdater.stop(); + } } public void onQueueStatus() { BlockchainProcessor.QueueStatus status = Signum.getBlockchainProcessor().getQueueStatus(); if (status != null) { - SwingUtilities.invokeLater( - () -> updateQueueStatus(status.unverifiedSize, status.verifiedSize, status.totalSize)); + SwingUtilities.invokeLater(() -> updateQueueStatus(status.unverifiedSize, + status.verifiedSize, status.totalSize, status.cacheFullness)); } } + public void onUnconfirmedTransactionCountChanged() { + SwingUtilities.invokeLater(() -> updateUnconfirmedTxCount( + Signum.getTransactionProcessor().getAmountUnconfirmedTransactions())); + } + public void onNetVolumeChanged() { BlockchainProcessor blockchainProcessor = Signum.getBlockchainProcessor(); SwingUtilities.invokeLater(() -> updateNetVolume(blockchainProcessor.getUploadedVolume(), @@ -537,12 +945,8 @@ public void onNetVolumeChanged() { public void onPerformanceStatsUpdated(Block block) { BlockchainProcessor.PerformanceStats stats = Signum.getBlockchainProcessor().getPerformanceStats(); - if (stats != null && block != null) { - chartUpdateExecutor.submit(() -> updateTimingChart(stats.totalTimeMs, stats.validationTimeMs, - stats.txLoopTimeMs, - stats.housekeepingTimeMs, stats.txApplyTimeMs, stats.atTimeMs, - stats.subscriptionTimeMs, stats.blockApplyTimeMs, stats.commitTimeMs, - stats.miscTimeMs, block)); + if (stats != null) { // block is not used, we use the DTO + chartUpdateExecutor.submit(() -> updateAllCharts(stats)); } } @@ -576,13 +980,6 @@ public void mouseClicked(MouseEvent e) { }); } - private void onBlockPushed(Block block) { - if (block == null) { - return; - } - chartUpdateExecutor.submit(() -> updatePerformanceChart(block)); - } - private JProgressBar createProgressBar(int min, int max, Color color, String initialString, Dimension size) { JProgressBar bar = new JProgressBar(min, max); bar.setBackground(color); @@ -660,20 +1057,39 @@ private void addDualChartToggleListener(JLabel label, }); } + private void updateUnconfirmedTxCount(int count) { + unconfirmedTxsProgressBar.setValue(count); + unconfirmedTxsProgressBar.setString(count + " / " + maxUnconfirmedTxs); + } + private void updateQueueStatus(int downloadCacheUnverifiedSize, int downloadCacheVerifiedSize, - int downloadCacheTotalSize) { + int downloadCacheTotalSize, int cacheFullness) { + + long cacheSizeBytes = (long) cacheFullness; + long cacheCapacityBytes = (long) Signum.getPropertyService().getInt(Props.BRS_BLOCK_CACHE_MB) * 1024L * 1024L; + + double cacheSizeMB = cacheSizeBytes / (1024.0 * 1024.0); + double cacheCapacityMB = cacheCapacityBytes / (1024.0 * 1024.0); + int cacheFullnessPercentage = cacheCapacityBytes == 0 ? 0 : (int) (100.0 * cacheSizeBytes / cacheCapacityBytes); syncProgressBarDownloadedBlocks.setStringPainted(true); + cacheFullnessProgressBar.setStringPainted(true); + cacheFullnessProgressBar + .setString( + String.format("%.2f / %.2f MB | %d%%", cacheSizeMB, cacheCapacityMB, cacheFullnessPercentage)); + cacheFullnessProgressBar.setMaximum(100); + cacheFullnessProgressBar.setValue(cacheFullnessPercentage); syncProgressBarUnverifiedBlocks.setStringPainted(true); if (downloadCacheTotalSize != 0) { - syncProgressBarDownloadedBlocks.setString(downloadCacheVerifiedSize + " / " + downloadCacheTotalSize + " - " - + 100 * downloadCacheVerifiedSize / downloadCacheTotalSize + "%"); - syncProgressBarDownloadedBlocks.setValue(100 * downloadCacheVerifiedSize / downloadCacheTotalSize); + int percentage = (int) (100.0 * downloadCacheVerifiedSize / downloadCacheTotalSize); + syncProgressBarDownloadedBlocks.setString(String.format("%d / %d | %d%%", downloadCacheVerifiedSize, + downloadCacheTotalSize, percentage)); + syncProgressBarDownloadedBlocks.setValue(percentage); } else { - syncProgressBarDownloadedBlocks.setString("0 / 0 - 0%"); + syncProgressBarDownloadedBlocks.setString("0 / 0 | 0%"); syncProgressBarDownloadedBlocks.setValue(0); } @@ -710,70 +1126,48 @@ private void updateNetVolumeAndSpeedChart(long uploadedVolume, long downloadedVo double currentDownloadSpeed = (double) deltaDownloaded * 1000 / deltaTime; // bytes per second // Add current speed to history and maintain size - uploadSpeedHistory.add(currentUploadSpeed); - if (uploadSpeedHistory.size() > SPEED_HISTORY_SIZE) { - uploadSpeedHistory.removeFirst(); - } - - downloadSpeedHistory.add(currentDownloadSpeed); - if (downloadSpeedHistory.size() > SPEED_HISTORY_SIZE) { - downloadSpeedHistory.removeFirst(); - } + uploadSpeeds.add(currentUploadSpeed); + downloadSpeeds.add(currentDownloadSpeed); - int currentWindowSize = Math.min(uploadSpeedHistory.size(), movingAverageWindow); + int currentWindowSize = Math.min(uploadSpeeds.size(), movingAverageWindow); if (currentWindowSize < 1) { return; } - double avgUploadSpeed = uploadSpeedHistory.stream() - .skip(Math.max(0, uploadSpeedHistory.size() - currentWindowSize)) - .mapToDouble(d -> d) - .average().orElse(0.0); - - double avgDownloadSpeed = downloadSpeedHistory.stream() - .skip(Math.max(0, downloadSpeedHistory.size() - currentWindowSize)) - .mapToDouble(d -> d) - .average().orElse(0.0); + double avgUploadSpeed = uploadSpeeds.getAverage(); + double avgUploadSpeedMax = uploadSpeeds.getMax(); + double avgDownloadSpeed = downloadSpeeds.getAverage(); + double avgDownloadSpeedMax = downloadSpeeds.getMax(); lastNetVolumeUpdateTime = currentTime; lastUploadedVolume = uploadedVolume; lastDownloadedVolume = downloadedVolume; // --- UI Updates on EDT --- - SwingUtilities.invokeLater(() -> { - if (metricsUploadVolumeLabel != null) { - metricsUploadVolumeLabel.setText("▲ " + formatDataSize(uploadedVolume)); - } - if (metricsDownloadVolumeLabel != null) { - metricsDownloadVolumeLabel.setText("▼ " + formatDataSize(downloadedVolume)); - } + SwingUtilities.invokeLater(() -> updateNetSpeedUI(currentTime, uploadedVolume, downloadedVolume, + avgUploadSpeed, avgDownloadSpeed, avgUploadSpeedMax, avgDownloadSpeedMax)); + }); + } - uploadSpeedProgressBar.setValue((int) avgUploadSpeed); - uploadSpeedProgressBar.setString(formatDataRate(avgUploadSpeed)); - downloadSpeedProgressBar.setValue((int) avgDownloadSpeed); - downloadSpeedProgressBar.setString(formatDataRate(avgDownloadSpeed)); + private void updateNetSpeedUI(long currentTime, long uploadedVolume, long downloadedVolume, double avgUploadSpeed, + double avgDownloadSpeed, double avgUploadSpeedMax, double avgDownloadSpeedMax) { + if (metricsUploadVolumeLabel != null) { + metricsUploadVolumeLabel.setText("▲ " + formatDataSize(uploadedVolume)); + } + if (metricsDownloadVolumeLabel != null) { + metricsDownloadVolumeLabel.setText("▼ " + formatDataSize(downloadedVolume)); + } - if (uploadedVolume > 0 || downloadedVolume > 0) { - uploadSpeedSeries.add(currentTime, avgUploadSpeed); - downloadSpeedSeries.add(currentTime, avgDownloadSpeed); - uploadVolumeSeries.add(currentTime, uploadedVolume); - downloadVolumeSeries.add(currentTime, downloadedVolume); + updateProgressBar(uploadSpeedProgressBar, avgUploadSpeed, avgUploadSpeedMax, this::formatDataRate); - while (uploadSpeedSeries.getItemCount() > SPEED_HISTORY_SIZE) { - uploadSpeedSeries.remove(0); - } - while (downloadSpeedSeries.getItemCount() > SPEED_HISTORY_SIZE) { - downloadSpeedSeries.remove(0); - } - while (uploadVolumeSeries.getItemCount() > SPEED_HISTORY_SIZE) { - uploadVolumeSeries.remove(0); - } - while (downloadVolumeSeries.getItemCount() > SPEED_HISTORY_SIZE) { - downloadVolumeSeries.remove(0); - } - } - }); - }); + updateProgressBar(downloadSpeedProgressBar, avgDownloadSpeed, avgDownloadSpeedMax, this::formatDataRate); + + if (uploadedVolume > 0 || downloadedVolume > 0) { + updateChartSeries(uploadSpeedSeries, currentTime, avgUploadSpeed, SPEED_HISTORY_SIZE); + updateChartSeries(downloadSpeedSeries, currentTime, avgDownloadSpeed, SPEED_HISTORY_SIZE); + updateChartSeries(uploadVolumeSeries, currentTime, uploadedVolume, SPEED_HISTORY_SIZE); + updateChartSeries(downloadVolumeSeries, currentTime, downloadedVolume, SPEED_HISTORY_SIZE); + } } private void updateNetVolume(long uploadedVolume, long downloadedVolume) { @@ -807,574 +1201,284 @@ private String formatDataRate(double bytesPerSecond) { return String.format("%.2f %s/s", bytesPerSecond, units[unitIndex]); } - private void updateTimingChart(long totalTimeMs, long validationTimeMs, long txLoopTimeMs, long housekeepingTimeMs, - long txApplyTimeMs, long atTimeMs, long subscriptionTimeMs, long blockApplyTimeMs, long commitTimeMs, - long miscTimeMs, Block block) { - - if (block == null) { - return; - } + private void updateAllCharts(BlockchainProcessor.PerformanceStats stats) { + // Run independent calculations in parallel using CompletableFuture + CompletableFuture timingFuture = CompletableFuture + .supplyAsync(() -> calculateTimingUpdate(stats), ForkJoinPool.commonPool()); + CompletableFuture performanceFuture = CompletableFuture + .supplyAsync(() -> calculatePerformanceUpdate(stats), ForkJoinPool.commonPool()); + // Add a future for the shared bar chart data + CompletableFuture sharedBarChartFuture = CompletableFuture + .supplyAsync(() -> calculateSharedBarChartUpdate(stats), ForkJoinPool.commonPool()); + + // Wait for both to complete and then update the UI in a single batch on the EDT + CompletableFuture.allOf(timingFuture, performanceFuture, sharedBarChartFuture).thenRun(() -> { + try { + TimingUpdateData timingData = timingFuture.get(); + PerformanceUpdateData performanceData = performanceFuture.get(); + SharedBarChartUpdateData sharedBarChartData = sharedBarChartFuture.get(); + + // Schedule a single UI update on the EDT + SwingUtilities.invokeLater(() -> { + try { + // Disable chart notifications to batch updates and prevent GUI freezes + setChartNotification(false); + + // Apply all updates + applyUpdates(timingData.progressBarUpdates, timingData.seriesUpdates); + applyUpdates(performanceData.progressBarUpdates, performanceData.seriesUpdates); + applyUpdates(sharedBarChartData.progressBarUpdates, sharedBarChartData.seriesUpdates); + + } finally { + // Re-enable chart notifications and trigger a repaint, even if an error + // occurred + setChartNotification(true); + } + }); + } catch (Exception e) { + LOGGER.error("Error updating charts in parallel", e); + } + }); + } - int blockHeight = block.getHeight(); - - pushTimes.add(totalTimeMs); - validationTimes.add(validationTimeMs); - txLoopTimes.add(txLoopTimeMs); - housekeepingTimes.add(housekeepingTimeMs); - txApplyTimes.add(txApplyTimeMs); - commitTimes.add(commitTimeMs); - atTimes.add(atTimeMs); - subscriptionTimes.add(subscriptionTimeMs); - blockApplyTimes.add(blockApplyTimeMs); - miscTimes.add(miscTimeMs); - - while (pushTimes.size() > CHART_HISTORY_SIZE) { - pushTimes.removeFirst(); - } - while (validationTimes.size() > CHART_HISTORY_SIZE) { - validationTimes.removeFirst(); - } - while (txLoopTimes.size() > CHART_HISTORY_SIZE) { - txLoopTimes.removeFirst(); - } - while (housekeepingTimes.size() > CHART_HISTORY_SIZE) { - housekeepingTimes.removeFirst(); - } - while (commitTimes.size() > CHART_HISTORY_SIZE) { - commitTimes.removeFirst(); - } - while (atTimes.size() > CHART_HISTORY_SIZE) { - atTimes.removeFirst(); - } - while (txApplyTimes.size() > CHART_HISTORY_SIZE) { - txApplyTimes.removeFirst(); + private void setChartNotification(boolean enabled) { + if (performanceChartPanel != null) { + performanceChartPanel.getChart().getXYPlot().setNotify(enabled); } - while (subscriptionTimes.size() > CHART_HISTORY_SIZE) { - subscriptionTimes.removeFirst(); + if (timingChartPanel != null) { + timingChartPanel.getChart().getXYPlot().setNotify(enabled); } - while (blockApplyTimes.size() > CHART_HISTORY_SIZE) { - blockApplyTimes.removeFirst(); + if (netSpeedChartPanel != null) { + netSpeedChartPanel.getChart().getXYPlot().setNotify(enabled); } - while (miscTimes.size() > CHART_HISTORY_SIZE) { - miscTimes.removeFirst(); - } - - int currentWindowSize = Math.min(pushTimes.size(), movingAverageWindow); - if (currentWindowSize < 1) { - return; - } - - long maxPushTime = pushTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxValidationTime = validationTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxTxLoopTime = txLoopTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxHousekeepingTime = housekeepingTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxCommitTime = commitTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxAtTime = atTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxTxApplyTime = txApplyTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxSubscriptionTime = subscriptionTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxBlockApplyTime = blockApplyTimes.stream().mapToLong(Long::longValue).max().orElse(0); - long maxMiscTime = miscTimes.stream().mapToLong(Long::longValue).max().orElse(0); - - long displayPushTime = (long) pushTimes.stream() - .skip(Math.max(0, pushTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayValidationTime = (long) validationTimes.stream() - .skip(Math.max(0, validationTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayTxLoopTime = (long) txLoopTimes.stream() - .skip(Math.max(0, txLoopTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayHousekeepingTime = (long) housekeepingTimes.stream() - .skip(Math.max(0, housekeepingTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayCommitTime = (long) commitTimes.stream() - .skip(Math.max(0, commitTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayAtTime = (long) atTimes.stream() - .skip(Math.max(0, atTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayTxApplyTime = (long) txApplyTimes.stream() - .skip(Math.max(0, txApplyTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displaySubscriptionTime = (long) subscriptionTimes.stream() - .skip(Math.max(0, subscriptionTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayBlockApplyTime = (long) blockApplyTimes.stream() - .skip(Math.max(0, blockApplyTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - - long displayMiscTime = (long) miscTimes.stream() - .skip(Math.max(0, miscTimes.size() - currentWindowSize)) - .mapToLong(Long::longValue) - .average().orElse(0.0); - SwingUtilities.invokeLater(() -> { - pushTimeProgressBar.setMaximum((int) Math.ceil(maxPushTime)); - pushTimeProgressBar.setValue((int) displayPushTime); - pushTimeProgressBar.setString(String.format("%d ms - max: %d ms", displayPushTime, maxPushTime)); - validationTimeProgressBar.setMaximum((int) Math.ceil(maxValidationTime)); - validationTimeProgressBar.setValue((int) displayValidationTime); - validationTimeProgressBar - .setString(String.format("%d ms - max: %d ms", displayValidationTime, maxValidationTime)); - txLoopTimeProgressBar.setMaximum((int) Math.ceil(maxTxLoopTime)); - txLoopTimeProgressBar.setValue((int) displayTxLoopTime); - txLoopTimeProgressBar.setString(String.format("%d ms - max: %d ms", displayTxLoopTime, maxTxLoopTime)); - housekeepingTimeProgressBar.setMaximum((int) Math.ceil(maxHousekeepingTime)); - housekeepingTimeProgressBar.setValue((int) displayHousekeepingTime); - housekeepingTimeProgressBar - .setString(String.format("%d ms - max: %d ms", displayHousekeepingTime, maxHousekeepingTime)); - commitTimeProgressBar.setMaximum((int) Math.ceil(maxCommitTime)); - commitTimeProgressBar.setValue((int) displayCommitTime); - commitTimeProgressBar.setString(String.format("%d ms - max: %d ms", displayCommitTime, maxCommitTime)); - atTimeProgressBar.setMaximum((int) Math.ceil(maxAtTime)); - atTimeProgressBar.setValue((int) displayAtTime); - atTimeProgressBar.setString(String.format("%d ms - max: %d ms", displayAtTime, maxAtTime)); - txApplyTimeProgressBar.setMaximum((int) Math.ceil(maxTxApplyTime)); - txApplyTimeProgressBar.setValue((int) displayTxApplyTime); - txApplyTimeProgressBar.setString(String.format("%d ms - max: %d ms", displayTxApplyTime, maxTxApplyTime)); - subscriptionTimeProgressBar.setMaximum((int) Math.ceil(maxSubscriptionTime)); - subscriptionTimeProgressBar.setValue((int) displaySubscriptionTime); - subscriptionTimeProgressBar - .setString(String.format("%d ms - max: %d ms", displaySubscriptionTime, maxSubscriptionTime)); - blockApplyTimeProgressBar.setMaximum((int) Math.ceil(maxBlockApplyTime)); - blockApplyTimeProgressBar.setValue((int) displayBlockApplyTime); - blockApplyTimeProgressBar - .setString(String.format("%d ms - max: %d ms", displayBlockApplyTime, maxBlockApplyTime)); - miscTimeProgressBar.setMaximum((int) Math.ceil(maxMiscTime)); - miscTimeProgressBar.setValue((int) displayMiscTime); - miscTimeProgressBar.setString(String.format("%d ms - max: %d ms", displayMiscTime, maxMiscTime)); - - // Update timing chart series - pushTimePerBlockSeries.add(blockHeight, displayPushTime); - validationTimePerBlockSeries.add(blockHeight, displayValidationTime); - txLoopTimePerBlockSeries.add(blockHeight, displayTxLoopTime); - housekeepingTimePerBlockSeries.add(blockHeight, displayHousekeepingTime); - commitTimePerBlockSeries.add(blockHeight, displayCommitTime); - atTimePerBlockSeries.add(blockHeight, displayAtTime); - txApplyTimePerBlockSeries.add(blockHeight, displayTxApplyTime); - subscriptionTimePerBlockSeries.add(blockHeight, displaySubscriptionTime); - miscTimePerBlockSeries.add(blockHeight, displayMiscTime); - blockApplyTimePerBlockSeries.add(blockHeight, displayBlockApplyTime); - - // Keep history size for timing chart - while (pushTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - pushTimePerBlockSeries.remove(0); - } - while (validationTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - validationTimePerBlockSeries.remove(0); - } - while (txLoopTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - txLoopTimePerBlockSeries.remove(0); - } - while (housekeepingTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - housekeepingTimePerBlockSeries.remove(0); - } - while (commitTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - commitTimePerBlockSeries.remove(0); - } - while (atTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - atTimePerBlockSeries.remove(0); - } - while (txApplyTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - txApplyTimePerBlockSeries.remove(0); - } - while (subscriptionTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - subscriptionTimePerBlockSeries.remove(0); - } - while (blockApplyTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - blockApplyTimePerBlockSeries.remove(0); - } - while (miscTimePerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - miscTimePerBlockSeries.remove(0); - } - }); } - private ChartPanel createPerformanceChartPanel() { - blocksPerSecondSeries = new XYSeries("Blocks/Second (MA)"); - transactionsPerSecondSeries = new XYSeries("All Txs/Sec (MA)"); - atTransactionsPerSecondSeries = new XYSeries("User Txs/Sec (MA)"); - - XYSeriesCollection lineDataset = new XYSeriesCollection(); - lineDataset.addSeries(blocksPerSecondSeries); - lineDataset.addSeries(transactionsPerSecondSeries); - lineDataset.addSeries(atTransactionsPerSecondSeries); - - // Create chart with no title or axis labels to save space - JFreeChart chart = ChartFactory.createXYLineChart( - null, // No title - null, // No X-axis label - null, // No Y-axis label - lineDataset); - - // Remove the legend to maximize plot area - chart.removeLegend(); - chart.setBorderVisible(false); - - XYPlot plot = chart.getXYPlot(); - plot.getDomainAxis().setLowerMargin(0.0); - plot.getDomainAxis().setUpperMargin(0.0); - plot.setBackgroundPaint(Color.DARK_GRAY); - plot.setDomainGridlinesVisible(false); - plot.setRangeGridlinesVisible(false); - - plot.getRenderer().setSeriesPaint(0, Color.CYAN); - plot.getRenderer().setSeriesPaint(1, Color.GREEN); - plot.getRenderer().setSeriesPaint(2, new Color(135, 206, 250)); - - // Set line thickness - plot.getRenderer().setSeriesStroke(0, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(1, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(2, new java.awt.BasicStroke(1.2f)); - - // Hide axis tick labels (the numbers on the axes) - plot.getDomainAxis().setTickLabelsVisible(false); - plot.getRangeAxis().setTickLabelsVisible(false); - - // Second Y-axis for transaction count - NumberAxis transactionAxis = new NumberAxis(null); - transactionAxis.setTickLabelsVisible(false); - XYSeriesCollection barDataset = new XYSeriesCollection(); - barDataset.addSeries(atTransactionsPerBlockSeries); - barDataset.addSeries(transactionsPerBlockSeries); - plot.setRangeAxis(1, transactionAxis); - plot.setDataset(1, barDataset); - plot.mapDatasetToRangeAxis(1, 1); - - // Renderer for transaction bars - XYBarRenderer transactionRenderer = new XYBarRenderer(0); - transactionRenderer.setBarPainter(new StandardXYBarPainter()); - transactionRenderer.setShadowVisible(false); - transactionRenderer.setSeriesPaint(0, new Color(0, 0, 255, 128)); // System Txs/Block - transactionRenderer.setSeriesPaint(1, new Color(255, 165, 0, 128)); // All Txs/Block - plot.setRenderer(1, transactionRenderer); - - lineDataset.addSeries(atCountPerBlockSeries); - plot.getRenderer(0).setSeriesPaint(3, new Color(153, 0, 76)); - - // Remove all padding around the plot area - plot.setInsets(new RectangleInsets(0, 0, 0, 0)); - plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); - - ChartPanel chartPanel = new ChartPanel(chart); - chartPanel.setPreferredSize(chartDimension1); - chartPanel.setMinimumSize(chartDimension1); - chartPanel.setMaximumSize(chartDimension1); - return chartPanel; + private void applyUpdates(Map progressBarUpdates, + Map seriesUpdates) { + progressBarUpdates.values().forEach(Runnable::run); + seriesUpdates.forEach( + (series, point) -> updateChartSeries(series, point.x, point.y, CHART_HISTORY_SIZE)); } - private ChartPanel createTimingChartPanel() { - pushTimePerBlockSeries = new XYSeries("Push Time (MA)"); - validationTimePerBlockSeries = new XYSeries("Validation Time (MA)"); - txLoopTimePerBlockSeries = new XYSeries("TX Loop Time (MA)"); - housekeepingTimePerBlockSeries = new XYSeries("Housekeeping Time (MA)"); - txApplyTimePerBlockSeries = new XYSeries("TX Apply Time (MA)"); - atTimePerBlockSeries = new XYSeries("AT Time (MA)"); - subscriptionTimePerBlockSeries = new XYSeries("Subscription Time (MA)"); - blockApplyTimePerBlockSeries = new XYSeries("Block Apply Time (MA)"); - commitTimePerBlockSeries = new XYSeries("Commit Time (MA)"); - miscTimePerBlockSeries = new XYSeries("Misc. Time (MA)"); - - XYSeriesCollection lineDataset = new XYSeriesCollection(); - lineDataset.addSeries(pushTimePerBlockSeries); - lineDataset.addSeries(validationTimePerBlockSeries); - lineDataset.addSeries(txLoopTimePerBlockSeries); - lineDataset.addSeries(housekeepingTimePerBlockSeries); - lineDataset.addSeries(txApplyTimePerBlockSeries); - lineDataset.addSeries(atTimePerBlockSeries); - lineDataset.addSeries(subscriptionTimePerBlockSeries); - lineDataset.addSeries(blockApplyTimePerBlockSeries); - lineDataset.addSeries(commitTimePerBlockSeries); - lineDataset.addSeries(miscTimePerBlockSeries); - - // Create chart with no title or axis labels to save space - JFreeChart chart = ChartFactory.createXYLineChart( - null, // No title - null, // No X-axis label - null, // No Y-axis label - lineDataset); - - // Remove the legend to maximize plot area - chart.removeLegend(); - chart.setBorderVisible(false); - - XYPlot plot = chart.getXYPlot(); - plot.getDomainAxis().setLowerMargin(0.0); - plot.getDomainAxis().setUpperMargin(0.0); - plot.setBackgroundPaint(Color.DARK_GRAY); - plot.setDomainGridlinesVisible(false); - plot.setRangeGridlinesVisible(false); - - plot.getRenderer().setSeriesPaint(0, Color.BLUE); - plot.getRenderer().setSeriesPaint(1, Color.YELLOW); - plot.getRenderer().setSeriesPaint(2, new Color(128, 0, 128)); // Purple for TX Loop - plot.getRenderer().setSeriesPaint(3, new Color(42, 223, 223)); // Cyan for Housekeeping - plot.getRenderer().setSeriesPaint(4, new Color(255, 165, 0)); // Orange for TX Apply - plot.getRenderer().setSeriesPaint(5, new Color(153, 0, 76)); // Dark Red for AT - plot.getRenderer().setSeriesPaint(6, new Color(255, 105, 100)); // Hot Pink for Subscription - plot.getRenderer().setSeriesPaint(7, new Color(0, 100, 100)); // Teal for Block Apply - plot.getRenderer().setSeriesPaint(8, new Color(150, 0, 200)); // Magenta for Commit - plot.getRenderer().setSeriesPaint(9, Color.LIGHT_GRAY); // Light Gray for Misc - - // Set line thickness - plot.getRenderer().setSeriesStroke(0, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(1, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(2, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(3, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(4, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(5, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(6, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(7, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(8, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(9, new java.awt.BasicStroke(1.2f)); - - // Hide axis tick labels (the numbers on the axes) - plot.getDomainAxis().setTickLabelsVisible(false); - plot.getRangeAxis().setTickLabelsVisible(false); - - // Second Y-axis for transaction count - NumberAxis transactionAxis = new NumberAxis(null); // No label for the second axis - transactionAxis.setTickLabelsVisible(false); - plot.setRangeAxis(1, transactionAxis); - XYSeriesCollection barDataset = new XYSeriesCollection(); - barDataset.addSeries(atTransactionsPerBlockSeries); - barDataset.addSeries(transactionsPerBlockSeries); - plot.setDataset(1, barDataset); - plot.mapDatasetToRangeAxis(1, 1); - - // Renderer for transaction bars - XYBarRenderer transactionRenderer = new XYBarRenderer(0); - transactionRenderer.setBarPainter(new StandardXYBarPainter()); - transactionRenderer.setShadowVisible(false); - transactionRenderer.setSeriesPaint(0, new Color(0, 0, 255, 128)); // Blue, semi-transparent - transactionRenderer.setSeriesPaint(1, new Color(255, 165, 0, 128)); // Orange, semi-transparent - plot.setRenderer(1, transactionRenderer); - - // Remove all padding around the plot area - plot.setInsets(new RectangleInsets(0, 0, 0, 0)); - plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); + private TimingUpdateData calculateTimingUpdate(BlockchainProcessor.PerformanceStats stats) { + TimingUpdateData data = new TimingUpdateData(); + if (stats == null) { + return data; + } - ChartPanel chartPanel = new ChartPanel(chart); - chartPanel.setPreferredSize(chartDimension1); - chartPanel.setMinimumSize(chartDimension1); - chartPanel.setMaximumSize(chartDimension1); - return chartPanel; + int blockHeight = stats.height; + pushTimes.add(stats.totalTimeMs); + validationTimes.add(stats.validationTimeMs); + txLoopTimes.add(stats.txLoopTimeMs); + housekeepingTimes.add(stats.housekeepingTimeMs); + txApplyTimes.add(stats.txApplyTimeMs); + commitTimes.add(stats.commitTimeMs); + atTimes.add(stats.atTimeMs); + subscriptionTimes.add(stats.subscriptionTimeMs); + blockApplyTimes.add(stats.blockApplyTimeMs); + miscTimes.add(stats.miscTimeMs); + payloadSize.add(stats.payloadSize); + + long avgPushTime = Math.round(pushTimes.getAverage()); + long avgValidationTime = Math.round(validationTimes.getAverage()); + long avgTxLoopTime = Math.round(txLoopTimes.getAverage()); + long avgHousekeepingTime = Math.round(housekeepingTimes.getAverage()); + long avgCommitTime = Math.round(commitTimes.getAverage()); + long avgAtTime = Math.round(atTimes.getAverage()); + long avgTxApplyTime = Math.round(txApplyTimes.getAverage()); + long avgSubscriptionTime = Math.round(subscriptionTimes.getAverage()); + long avgBlockApplyTime = Math.round(blockApplyTimes.getAverage()); + long avgMiscTime = Math.round(miscTimes.getAverage()); + + long payloadFullnessPercentage = Math.round(100.0 * stats.payloadSize / stats.maxPayloadSize); + + double avgPayloadFullness = payloadSize.getAverage(); + double avgPayloadFullnessPercentage = 100.0 * avgPayloadFullness / stats.maxPayloadSize; + + double avgPayloadFullnessMax = payloadSize.getMax(); + long avgPayloadFullnessMaxPercentage = Math.round(100.0 * avgPayloadFullnessMax / stats.maxPayloadSize); + + double avgPayloadFullnessMin = payloadSize.getMin(); + long avgPayloadFullnessMinPercentage = Math.round(100.0 * avgPayloadFullnessMin / stats.maxPayloadSize); + + long avgPushTimeMax = Math.round(pushTimes.getMax()); + long avgValidationTimeMax = Math.round(validationTimes.getMax()); + long avgTxLoopTimeMax = Math.round(txLoopTimes.getMax()); + long avgHousekeepingTimeMax = Math.round(housekeepingTimes.getMax()); + long avgCommitTimeMax = Math.round(commitTimes.getMax()); + long avgAtTimeMax = Math.round(atTimes.getMax()); + long avgTxApplyTimeMax = Math.round(txApplyTimes.getMax()); + long avgSubscriptionTimeMax = Math.round(subscriptionTimes.getMax()); + long avgBlockApplyTimeMax = Math.round(blockApplyTimes.getMax()); + long avgMiscTimeMax = Math.round(miscTimes.getMax()); + + // Prepare progress bar updates + data.progressBarUpdates.put(pushTimeProgressBar, () -> updateProgressBar(pushTimeProgressBar, avgPushTime, + avgPushTimeMax, val -> String.format("%.0f ms - max: %d ms", val, avgPushTimeMax))); + data.progressBarUpdates.put(validationTimeProgressBar, + () -> updateProgressBar(validationTimeProgressBar, avgValidationTime, avgValidationTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgValidationTimeMax))); + data.progressBarUpdates.put(txLoopTimeProgressBar, + () -> updateProgressBar(txLoopTimeProgressBar, avgTxLoopTime, avgTxLoopTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgTxLoopTimeMax))); + data.progressBarUpdates.put(housekeepingTimeProgressBar, + () -> updateProgressBar(housekeepingTimeProgressBar, avgHousekeepingTime, + avgHousekeepingTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgHousekeepingTimeMax))); + data.progressBarUpdates.put(commitTimeProgressBar, + () -> updateProgressBar(commitTimeProgressBar, avgCommitTime, avgCommitTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgCommitTimeMax))); + data.progressBarUpdates.put(atTimeProgressBar, () -> updateProgressBar(atTimeProgressBar, avgAtTime, + avgAtTimeMax, val -> String.format("%.0f ms - max: %d ms", val, avgAtTimeMax))); + data.progressBarUpdates.put(txApplyTimeProgressBar, + () -> updateProgressBar(txApplyTimeProgressBar, avgTxApplyTime, avgTxApplyTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgTxApplyTimeMax))); + data.progressBarUpdates.put(subscriptionTimeProgressBar, + () -> updateProgressBar(subscriptionTimeProgressBar, avgSubscriptionTime, avgSubscriptionTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgSubscriptionTimeMax))); + data.progressBarUpdates.put(blockApplyTimeProgressBar, + () -> updateProgressBar(blockApplyTimeProgressBar, avgBlockApplyTime, avgBlockApplyTimeMax, + val -> String.format("%.0f ms - max: %d ms", val, avgBlockApplyTimeMax))); + data.progressBarUpdates.put(miscTimeProgressBar, () -> updateProgressBar(miscTimeProgressBar, avgMiscTime, + avgMiscTimeMax, val -> String.format("%.0f ms - max: %d ms", val, avgMiscTimeMax))); + data.progressBarUpdates.put(payloadFullnessProgressBar, + () -> updateProgressBar(payloadFullnessProgressBar, avgPayloadFullnessPercentage, 100, val -> String + .format("%06.2f%% - C: %03d%% (%06d / %d bytes) - min: %03d%% - max: %03d%%", + avgPayloadFullnessPercentage, payloadFullnessPercentage, stats.payloadSize, + stats.maxPayloadSize, avgPayloadFullnessMinPercentage, + avgPayloadFullnessMaxPercentage, 100))); + + // Prepare chart series updates + data.seriesUpdates.put(pushTimePerBlockSeries, new Point.Double(blockHeight, avgPushTime)); + data.seriesUpdates.put(validationTimePerBlockSeries, new Point.Double(blockHeight, avgValidationTime)); + data.seriesUpdates.put(txLoopTimePerBlockSeries, new Point.Double(blockHeight, avgTxLoopTime)); + data.seriesUpdates.put(housekeepingTimePerBlockSeries, new Point.Double(blockHeight, avgHousekeepingTime)); + data.seriesUpdates.put(commitTimePerBlockSeries, new Point.Double(blockHeight, avgCommitTime)); + data.seriesUpdates.put(atTimePerBlockSeries, new Point.Double(blockHeight, avgAtTime)); + data.seriesUpdates.put(txApplyTimePerBlockSeries, new Point.Double(blockHeight, avgTxApplyTime)); + data.seriesUpdates.put(subscriptionTimePerBlockSeries, new Point.Double(blockHeight, avgSubscriptionTime)); + data.seriesUpdates.put(miscTimePerBlockSeries, new Point.Double(blockHeight, avgMiscTime)); + data.seriesUpdates.put(blockApplyTimePerBlockSeries, new Point.Double(blockHeight, avgBlockApplyTime)); + data.seriesUpdates.put(payloadFullnessSeries, new Point.Double(stats.height, avgPayloadFullnessPercentage)); + + return data; } - private ChartPanel createNetSpeedChartPanel() { - uploadSpeedSeries = new XYSeries("Upload Speed"); - downloadSpeedSeries = new XYSeries("Download Speed"); - uploadVolumeSeries = new XYSeries("Upload Volume"); - downloadVolumeSeries = new XYSeries("Download Volume"); - - XYSeriesCollection lineDataset = new XYSeriesCollection(); - lineDataset.addSeries(uploadSpeedSeries); - lineDataset.addSeries(downloadSpeedSeries); - - JFreeChart chart = ChartFactory.createXYLineChart( - null, // No title - null, // No X-axis label - null, // No Y-axis label - lineDataset); + private PerformanceUpdateData calculatePerformanceUpdate(BlockchainProcessor.PerformanceStats stats) { + PerformanceUpdateData data = new PerformanceUpdateData(); + if (stats == null) { + return data; + } - // Remove the legend to maximize plot area - chart.removeLegend(); - chart.setBorderVisible(false); + blockTimestamps.add(System.currentTimeMillis()); - XYPlot plot = chart.getXYPlot(); - plot.getDomainAxis().setLowerMargin(0.0); - plot.getDomainAxis().setUpperMargin(0.0); - plot.setBackgroundPaint(Color.DARK_GRAY); - plot.setDomainGridlinesVisible(false); - plot.setRangeGridlinesVisible(false); + double timeSpanMs = blockTimestamps.getLast() + - blockTimestamps.get(blockTimestamps.size() - Math.min(blockTimestamps.size(), movingAverageWindow)); - plot.getRenderer().setSeriesPaint(0, new Color(128, 0, 0)); // Upload - Red, semi-transparent - plot.getRenderer().setSeriesPaint(1, new Color(0, 100, 0)); // Download - Green, semi-transparent + double blocksPerSecond = 0.0; + if (timeSpanMs > 0) { + blocksPerSecond = (double) Math.min(blockTimestamps.size(), movingAverageWindow) * 1000.0 / timeSpanMs; + } + blocksPerSec.add(blocksPerSecond); + double avgBlocksPerSecond = blocksPerSec.getAverage(); + double avgBlocksPerSecondMax = blocksPerSec.getMax(); + + double allTransactionsPerSecond = 0.0; + double systemTransactionsPerSecond = 0.0; + if (timeSpanMs > 0) { + allTransactionsPerSecond = allTransactionsPerBlock.getSum() * 1000.0 / timeSpanMs; + systemTransactionsPerSecond = systemTransactionsPerBlock.getSum() * 1000.0 / timeSpanMs; + } + allTransactionsPerSec.add(allTransactionsPerSecond); + double avgAllTransactionsPerSecond = allTransactionsPerSec.getAverage(); + double avgAllTransactionsPerSecondMax = allTransactionsPerSec.getMax(); + + systemTransactionsPerSec.add(systemTransactionsPerSecond); + double avgSystemTransactionsPerSecond = systemTransactionsPerSec.getAverage(); + double avgSystemTransactionsPerSecondMax = systemTransactionsPerSec.getMax(); + + atCountsPerBlock.add((double) stats.atCount); + double avgAtCount = atCountsPerBlock.getAverage(); + double avgAtCountMax = atCountsPerBlock.getMax(); + + // Prepare chart series updates + data.seriesUpdates.put(blocksPerSecondSeries, new Point.Double(stats.height, avgBlocksPerSecond)); + data.seriesUpdates.put(allTransactionsPerSecondSeries, + new Point.Double(stats.height, avgAllTransactionsPerSecond)); + data.seriesUpdates.put(systemTransactionsPerSecondSeries, + new Point.Double(stats.height, avgSystemTransactionsPerSecond)); + data.seriesUpdates.put(atCountPerBlockSeries, new Point.Double(stats.height, avgAtCount)); + + // Prepare progress bar updates + data.progressBarUpdates.put(blocksPerSecondProgressBar, + () -> updateProgressBar(blocksPerSecondProgressBar, avgBlocksPerSecond, avgBlocksPerSecondMax, + val -> String.format("%.2f - max: %.2f", val, avgBlocksPerSecondMax), 100)); + data.progressBarUpdates.put(allTransactionsPerSecondProgressBar, + () -> updateProgressBar(allTransactionsPerSecondProgressBar, avgAllTransactionsPerSecond, + avgAllTransactionsPerSecondMax, + val -> String.format("%.2f - max: %.2f", val, avgAllTransactionsPerSecondMax), 100)); + data.progressBarUpdates.put(systemTransactionsPerSecondProgressBar, + () -> updateProgressBar(systemTransactionsPerSecondProgressBar, avgSystemTransactionsPerSecond, + avgSystemTransactionsPerSecondMax, + val -> String.format("%.2f - max: %.2f", val, avgSystemTransactionsPerSecondMax), + 100)); + data.progressBarUpdates.put(atCountsPerBlockProgressBar, + () -> updateProgressBar(atCountsPerBlockProgressBar, avgAtCount, avgAtCountMax, + val -> String.format("%.2f - max: %.2f", val, avgAtCountMax), 100)); + + return data; + } - // Set line thickness - plot.getRenderer().setSeriesStroke(0, new java.awt.BasicStroke(1.2f)); - plot.getRenderer().setSeriesStroke(1, new java.awt.BasicStroke(1.2f)); + private SharedBarChartUpdateData calculateSharedBarChartUpdate(BlockchainProcessor.PerformanceStats stats) { + SharedBarChartUpdateData data = new SharedBarChartUpdateData(); + if (stats == null) { + return data; + } - // Hide axis tick labels (the numbers on the axes) - plot.getDomainAxis().setTickLabelsVisible(false); - plot.getRangeAxis().setTickLabelsVisible(false); + allTransactionsPerBlock.add((double) stats.allTransactionCount); + systemTransactionsPerBlock.add((double) stats.systemTransactionCount); - // Second Y-axis for volume - NumberAxis volumeAxis = new NumberAxis(null); // No label for the second axis - volumeAxis.setTickLabelsVisible(false); - plot.setRangeAxis(1, volumeAxis); // Use axis index 1 for volume + double avgAllTransactions = allTransactionsPerBlock.getAverage(); + double avgAllTransactionsMax = allTransactionsPerBlock.getMax(); - // A single dataset and renderer for both volume series. - XYSeriesCollection volumeDataset = new XYSeriesCollection(); - volumeDataset.addSeries(downloadVolumeSeries); // Series 0: Download (top layer) - volumeDataset.addSeries(uploadVolumeSeries); // Series 1: Upload (bottom layer) + double avgSystemTransactions = systemTransactionsPerBlock.getAverage(); + double avgSystemTransactionsMax = systemTransactionsPerBlock.getMax(); - XYStepAreaRenderer volumeRenderer = new XYStepAreaRenderer(); - volumeRenderer.setShapesVisible(false); - volumeRenderer.setSeriesPaint(0, new Color(50, 205, 50, 128)); // Download - Green - volumeRenderer.setSeriesPaint(1, new Color(233, 150, 122, 128)); // Upload - Red - plot.setDataset(1, volumeDataset); - plot.setRenderer(1, volumeRenderer); - plot.mapDatasetToRangeAxis(1, 1); + // Prepare chart series updates + data.seriesUpdates.put(allTransactionsPerBlockSeries, new Point.Double(stats.height, avgAllTransactions)); + data.seriesUpdates.put(systemTransactionsPerBlockSeries, new Point.Double(stats.height, avgSystemTransactions)); - // Remove all padding around the plot area - plot.setInsets(new RectangleInsets(0, 0, 0, 0)); - plot.setAxisOffset(new RectangleInsets(0, 0, 0, 0)); + // Prepare progress bar updates + data.progressBarUpdates.put(allTransactionsPerBlockProgressBar, + () -> updateProgressBar(allTransactionsPerBlockProgressBar, avgAllTransactions, + avgAllTransactionsMax, + val -> String.format("%.2f - max: %.2f", val, avgAllTransactionsMax), + 100)); + data.progressBarUpdates.put(systemTransactionsPerBlockProgressBar, + () -> updateProgressBar(systemTransactionsPerBlockProgressBar, avgSystemTransactions, + avgSystemTransactionsMax, + val -> String.format("%.2f - max: %.2f", val, avgSystemTransactionsMax), + 100)); - ChartPanel chartPanel = new ChartPanel(chart); - chartPanel.setPreferredSize(chartDimension2); - chartPanel.setMinimumSize(chartDimension2); - chartPanel.setMaximumSize(chartDimension2); - return chartPanel; + return data; } - private void updatePerformanceChart(Block block) { + private void updateProgressBar(JProgressBar bar, double value, double max, + java.util.function.Function stringFormatter) { + updateProgressBar(bar, value, max, stringFormatter, 1); + } - blockTimestamps.add(System.currentTimeMillis()); + private void updateProgressBar(JProgressBar bar, double value, double max, + java.util.function.Function stringFormatter, int multiplier) { + bar.setMaximum((int) (max * multiplier)); + bar.setValue((int) (value * multiplier)); + bar.setString(stringFormatter.apply(value)); + } - int allTxCount = block.getAllTransactions().size(); - int systemTxCount = block.getAllTransactions().size() - block.getTransactions().size(); - int atCount = 0; - if (block.getBlockAts() != null) { - try { - atCount = AtController.getATsFromBlock(block.getBlockAts()).size(); - } catch (Exception e) { - LOGGER.warn("Could not parse ATs from block", e); - } + private void updateChartSeries(XYSeries series, double x, double y, int maxItems) { + while (series.getItemCount() >= maxItems) { + series.remove(0); } - transactionCounts.add(allTxCount); - atTransactionCounts.add(systemTxCount); - atCounts.add(atCount); - - while (blockTimestamps.size() > CHART_HISTORY_SIZE) { - blockTimestamps.removeFirst(); - transactionCounts.removeFirst(); - atTransactionCounts.removeFirst(); - atCounts.removeFirst(); - if (!blocksPerSecondHistory.isEmpty()) { - blocksPerSecondHistory.removeFirst(); - } - if (!transactionsPerSecondHistory.isEmpty()) { - transactionsPerSecondHistory.removeFirst(); - } - if (!atTransactionsPerSecondHistory.isEmpty()) { - atTransactionsPerSecondHistory.removeFirst(); - } - } - - long timeSpanMs = blockTimestamps.getLast() - - blockTimestamps.get(blockTimestamps.size() - Math.min(blockTimestamps.size(), movingAverageWindow)); - double blocksPerSecond = (timeSpanMs > 0) - ? (double) Math.min(blockTimestamps.size(), movingAverageWindow) * 1000.0 / timeSpanMs - : 0; - blocksPerSecondHistory.add(blocksPerSecond); - - double avgTransactions = transactionCounts.stream() - .skip(Math.max(0, transactionCounts.size() - Math.min(transactionCounts.size(), movingAverageWindow))) - .mapToInt(Integer::intValue) - .average().orElse(0.0); - - double avgAtTransactions = atTransactionCounts.stream() - .skip(Math.max(0, - atTransactionCounts.size() - Math.min(atTransactionCounts.size(), movingAverageWindow))) - .mapToInt(Integer::intValue) - .average().orElse(0.0); - - double avgAtCount = atCounts.stream() - .skip(Math.max(0, atCounts.size() - Math.min(atCounts.size(), movingAverageWindow))) - .mapToInt(Integer::intValue) - .average().orElse(0.0); - - double transactionsPerSecond = avgTransactions * blocksPerSecond; - transactionsPerSecondHistory.add(transactionsPerSecond); - - double atTransactionsPerSecond = avgAtTransactions * blocksPerSecond; - atTransactionsPerSecondHistory.add(atTransactionsPerSecond); - - double maxBlocksPerSecond = blocksPerSecondHistory.stream().mapToDouble(Double::doubleValue).max().orElse(0.0); - int maxTransactionsPerBlock = transactionCounts.stream().mapToInt(Integer::intValue).max().orElse(0); - int maxAtTransactionsPerBlock = atTransactionCounts.stream().mapToInt(Integer::intValue).max().orElse(0); - int maxAtCount = atCounts.stream().mapToInt(Integer::intValue).max().orElse(0); - double maxTransactionsPerSecond = transactionsPerSecondHistory.stream().mapToDouble(Double::doubleValue).max() - .orElse(0.0); - double maxAtTransactionsPerSecond = atTransactionsPerSecondHistory.stream().mapToDouble(Double::doubleValue) - .max() - .orElse(0.0); - - // Now, schedule only the UI updates on the EDT - SwingUtilities.invokeLater(() -> { - // Prune series on EDT before adding new data - while (blocksPerSecondSeries.getItemCount() >= CHART_HISTORY_SIZE) { - blocksPerSecondSeries.remove(0); - } - while (transactionsPerSecondSeries.getItemCount() >= CHART_HISTORY_SIZE) { - transactionsPerSecondSeries.remove(0); - } - while (transactionsPerBlockSeries.getItemCount() >= CHART_HISTORY_SIZE) { - transactionsPerBlockSeries.remove(0); - } - while (atTransactionsPerBlockSeries.getItemCount() >= CHART_HISTORY_SIZE) { - atTransactionsPerBlockSeries.remove(0); - } - while (atTransactionsPerSecondSeries.getItemCount() >= CHART_HISTORY_SIZE) { - atTransactionsPerSecondSeries.remove(0); - } - while (atCountPerBlockSeries.getItemCount() > CHART_HISTORY_SIZE) { - atCountPerBlockSeries.remove(0); - } - - blocksPerSecondSeries.add(block.getHeight(), blocksPerSecond); - transactionsPerBlockSeries.add(block.getHeight(), avgTransactions); - blocksPerSecondProgressBar.setMaximum((int) Math.ceil(maxBlocksPerSecond)); - blocksPerSecondProgressBar.setValue((int) (blocksPerSecond)); - blocksPerSecondProgressBar - .setString(String.format("%.2f - max: %.2f", blocksPerSecond, maxBlocksPerSecond)); - - transactionsPerSecondSeries.add(block.getHeight(), transactionsPerSecond); - transactionsPerSecondProgressBar.setMaximum((int) Math.ceil(maxTransactionsPerSecond)); - transactionsPerSecondProgressBar.setValue((int) transactionsPerSecond); - transactionsPerSecondProgressBar - .setString(String.format("%.2f - max: %.2f", transactionsPerSecond, maxTransactionsPerSecond)); - - transactionsPerBlockProgressBar.setMaximum(maxTransactionsPerBlock); - transactionsPerBlockProgressBar.setValue((int) avgTransactions); - transactionsPerBlockProgressBar - .setString(String.format("%.2f - max: %d", avgTransactions, maxTransactionsPerBlock)); - - atTransactionsPerBlockSeries.add(block.getHeight(), avgAtTransactions); - atTransactionsPerBlockProgressBar.setMaximum(maxAtTransactionsPerBlock); - atTransactionsPerBlockProgressBar.setValue((int) avgAtTransactions); - atTransactionsPerBlockProgressBar - .setString(String.format("%.2f - max: %d", avgAtTransactions, maxAtTransactionsPerBlock)); - - atCountPerBlockSeries.add(block.getHeight(), avgAtCount); - atCountProgressBar.setMaximum(maxAtCount); - atCountProgressBar.setValue((int) avgAtCount); - atCountProgressBar - .setString(String.format("%.2f - max: %d", avgAtCount, maxAtCount)); - - systemTransactionsPerSecondProgressBar.setMaximum((int) Math.ceil(maxAtTransactionsPerSecond)); - atTransactionsPerSecondSeries.add(block.getHeight(), atTransactionsPerSecond); - systemTransactionsPerSecondProgressBar.setValue((int) atTransactionsPerSecond); - systemTransactionsPerSecondProgressBar - .setString( - String.format("%.2f - max: %.2f", atTransactionsPerSecond, maxAtTransactionsPerSecond)); - }); + series.addOrUpdate(x, y); } } \ No newline at end of file diff --git a/src/brs/gui/PeersDialog.java b/src/brs/gui/PeersDialog.java new file mode 100644 index 000000000..69bf6f502 --- /dev/null +++ b/src/brs/gui/PeersDialog.java @@ -0,0 +1,132 @@ +package brs.gui; + +import brs.Block; +import brs.Signum; +import brs.BlockchainProcessor; +import brs.peer.Peer; +import brs.util.Listener; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@SuppressWarnings("serial") +public class PeersDialog extends JFrame { + + private final Listener peerListener; + private final JTabbedPane tabbedPane; + + private enum PeerCategory { + ACTIVE("Active", p -> p.getState() != Peer.State.NON_CONNECTED), + CONNECTED("Connected", p -> p.getState() == Peer.State.CONNECTED), + BLACKLISTED("Blacklisted", Peer::isBlacklisted), + ALL("All Known", p -> true); + + private final String title; + private final Predicate filter; + + PeerCategory(String title, Predicate filter) { + this.title = title; + this.filter = filter; + } + } + + public PeersDialog(JFrame owner) { + super("Peer Information"); + + JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + JTextArea legendArea = new JTextArea(); + legendArea.setEditable(false); + legendArea.setLineWrap(true); + legendArea.setWrapStyleWord(true); + legendArea.setBackground(UIManager.getColor("Panel.background")); + legendArea.setText( + "Peers: Active / All Known (BL: Blacklisted)\n\n" + + "• Active: Peers your node is currently communicating with.\n" + + "• Connected: A subset of active peers with a stable connection.\n" + + "• Blacklisted: Peers temporarily banned for sending invalid data.\n" + + "• All Known: All peers your node has ever discovered."); + mainPanel.add(legendArea, BorderLayout.NORTH); + + tabbedPane = new JTabbedPane(); + + for (PeerCategory category : PeerCategory.values()) { + tabbedPane.addTab(category.title, createPeerListScrollPane()); + } + + updateTabs(); // Initial population + + peerListener = block -> SwingUtilities.invokeLater(this::updateTabs); + Signum.getBlockchainProcessor().addListener(peerListener, BlockchainProcessor.Event.PEER_COUNT_CHANGED); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + Signum.getBlockchainProcessor().removeListener(peerListener, BlockchainProcessor.Event.PEER_COUNT_CHANGED); + dispose(); + } + }); + + mainPanel.add(tabbedPane, BorderLayout.CENTER); + add(mainPanel); + pack(); + setLocationRelativeTo(owner); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + } + + private void updateTabs() { + Collection allPeers = Signum.getBlockchainProcessor().getAllPeers(); + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + PeerCategory category = PeerCategory.values()[i]; + List filteredPeers = allPeers.stream().filter(category.filter).collect(Collectors.toList()); + tabbedPane.setTitleAt(i, category.title + " (" + filteredPeers.size() + ")"); + updatePeerListScrollPane((JScrollPane) tabbedPane.getComponentAt(i), filteredPeers, category); + } + } + + private void updatePeerListScrollPane(JScrollPane scrollPane, List peers, PeerCategory category) { + JEditorPane editorPane = (JEditorPane) scrollPane.getViewport().getView(); + peers.sort(Comparator.comparing(Peer::getPeerAddress)); + StringBuilder sb = new StringBuilder(2048); + sb.append(""); + + if (peers.isEmpty()) { + sb.append("No peers in this category."); + } else { + for (Peer p : peers) { + String color; + if (p.isBlacklisted()) { + color = "red"; + } else if (p.getState() != Peer.State.NON_CONNECTED) { + color = "green"; + } else { + color = category == PeerCategory.ALL ? "yellow" : "green"; + } + sb.append(""); + sb.append(p.getPeerAddress()).append(" (").append(p.getVersion().toStringIfNotEmpty()).append(")"); + sb.append("
"); + } + } + sb.append(""); + editorPane.setText(sb.toString()); + editorPane.setCaretPosition(0); + } + + private JScrollPane createPeerListScrollPane() { + JEditorPane editorPane = new JEditorPane(); + editorPane.setContentType("text/html"); + editorPane.setEditable(false); + editorPane.setBackground(UIManager.getColor("Panel.background")); + JScrollPane scrollPane = new JScrollPane(editorPane); + scrollPane.setPreferredSize(new Dimension(400, 250)); + return scrollPane; + } +} \ No newline at end of file diff --git a/src/brs/SignumGUI.java b/src/brs/gui/SignumGUI.java similarity index 87% rename from src/brs/SignumGUI.java rename to src/brs/gui/SignumGUI.java index 51086bb45..30e97d64f 100644 --- a/src/brs/SignumGUI.java +++ b/src/brs/gui/SignumGUI.java @@ -1,4 +1,4 @@ -package brs; +package brs.gui; import java.awt.*; import java.awt.TrayIcon.MessageType; @@ -12,8 +12,13 @@ import java.io.PrintStream; import java.net.URI; import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Comparator; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.List; import javax.imageio.ImageIO; import javax.swing.BorderFactory; @@ -21,6 +26,7 @@ import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; +import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; @@ -39,10 +45,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import brs.Signum; +import brs.BlockchainProcessor; +import brs.Block; +import brs.peer.Peer; import brs.fluxcapacitor.FluxValues; import brs.props.PropertyService; import brs.props.Props; import brs.util.Convert; +import brs.util.Listener; import jiconfont.icons.font_awesome.FontAwesome; import jiconfont.swing.IconFontSwing; @@ -72,12 +83,14 @@ public class SignumGUI extends JFrame { private JLabel connectedPeersLabel; private JLabel peersCountLabel; + private JLabel blacklistedPeersLabel; private JLabel uploadVolumeLabel; private JLabel downloadVolumeLabel; private JCheckBox showPopOffCheckbox; private JCheckBox showMetricsCheckbox; private boolean showMetrics = false; private boolean showPopOff = false; + private boolean isSyncStopped = false; private JButton openPhoenixButton; private JButton openClassicButton; @@ -85,6 +98,7 @@ public class SignumGUI extends JFrame { private JButton editConfButton; private JButton popOff10Button; private JButton popOff100Button; + private JButton syncButton; private JButton shutdownButton; // private JButton restartButton; @@ -367,17 +381,29 @@ public void mouseClicked(MouseEvent e) { // --- Peers --- JPanel peersPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); - tooltip = "The number of peers (other nodes) your node is currently actively connected to. These are the nodes with which your node is exchanging blockchain data, such as new blocks and transactions.\n\nA healthy number of connected peers is crucial for staying synchronized with the network."; - connectedPeersLabel = createLabel("0", null, tooltip); - tooltip = "The total number of known peers in the network that your node has discovered. This includes both connected and disconnected peers.\n\nYour node periodically attempts to connect to peers from this list to maintain a stable set of connections."; - peersCountLabel = createLabel("0", null, tooltip); + tooltip = "Active Peers: The number of peers your node is currently communicating with."; + connectedPeersLabel = createLabel("0", null, tooltip); // Represents 'Active' peers now + tooltip = "Total Discovered Peers: The total number of peers your node has ever discovered, including active, disconnected, and blacklisted ones."; + peersCountLabel = createLabel("0", null, tooltip); // Represents 'All Known' peers + tooltip = "Blacklisted Peers: The number of peers that have been temporarily banned for sending invalid data or other network violations."; + blacklistedPeersLabel = createLabel("0", null, tooltip); peersPanel.add(new JLabel("Peers: ")); peersPanel.add(connectedPeersLabel); peersPanel.add(new JLabel(" / ")); peersPanel.add(peersCountLabel); + peersPanel.add(new JLabel(" (BL: ")); + peersPanel.add(blacklistedPeersLabel); + peersPanel.add(new JLabel(")")); // Add peersPanel + peersPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + new PeersDialog(SignumGUI.this).setVisible(true); + } + }); + peersPanel.setToolTipText("Click to see detailed peer information."); gbc.gridx = 1; infoPanel.add(peersPanel, gbc); // No left inset needed, timePanel provides right spacing @@ -541,6 +567,8 @@ private TrayIcon createTrayIcon() { IconFontSwing.buildIcon(FontAwesome.STEP_BACKWARD, 18, iconColor)); popOff100Button = new JButton("Pop off 100 blocks", IconFontSwing.buildIcon(FontAwesome.BACKWARD, 18, iconColor)); + syncButton = new JButton("Stop Sync", + IconFontSwing.buildIcon(FontAwesome.PAUSE, 18, iconColor)); /* * restartButton = new JButton("Restart", * IconFontSwing.buildIcon(FontAwesome.REFRESH, 18, iconColor)); @@ -552,6 +580,19 @@ private TrayIcon createTrayIcon() { // JButton popOffMaxButton = new JButton("Pop off max", // IconFontSwing.buildIcon(FontAwesome.FAST_BACKWARD, 18, iconColor)); + addInfoTooltip(openPhoenixButton, "Opens the modern Phoenix Wallet in your default web browser."); + addInfoTooltip(openClassicButton, "Opens the Classic Wallet in your default web browser."); + addInfoTooltip(openApiButton, "Opens the interactive API documentation in your default web browser."); + addInfoTooltip(editConfButton, + "Opens the node's configuration file (node.properties or node-default.properties) in your default text editor for easy modification."); + + addInfoTooltip(popOff10Button, + "Removes the last 10 blocks from your local blockchain. This can help resolve a local fork if your node is stuck."); + addInfoTooltip(popOff100Button, + "Removes the last 100 blocks from your local blockchain. Use this if a smaller pop-off does not resolve a fork."); + addInfoTooltip(syncButton, + "Toggles the synchronization process. 'Pause Sync' pauses the downloading and processing of new blocks. 'Resume Sync' continues the process."); + openPhoenixButton.addActionListener(e -> openWebUi("/phoenix")); openClassicButton.addActionListener(e -> openWebUi("/classic")); openApiButton.addActionListener(e -> openWebUi("/api-doc")); @@ -563,6 +604,28 @@ private TrayIcon createTrayIcon() { File phoenixIndex = new File("html/ui/phoenix/index.html"); File classicIndex = new File("html/ui/classic/index.html"); + syncButton.addActionListener(e -> { + isSyncStopped = !isSyncStopped; + if (isSyncStopped) { + Signum.getBlockchainProcessor().setGetMoreBlocksPause(true); + Signum.getBlockchainProcessor().setBlockImporterPause(true); + syncButton.setText("Resume Sync"); + syncButton.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY, 18, iconColor)); + if (guiTimer != null) { + guiTimer.stop(); + } + } else { + Signum.getBlockchainProcessor().setGetMoreBlocksPause(false); + Signum.getBlockchainProcessor().setBlockImporterPause(false); + syncButton.setText("Pause Sync"); + syncButton.setIcon(IconFontSwing.buildIcon(FontAwesome.PAUSE, 18, iconColor)); + if (guiTimer != null) { + guiTimer.start(); + } + } + updateTitle(); + }); + shutdownButton.addActionListener(e -> { if (JOptionPane.showConfirmDialog(SignumGUI.this, "This will stop the node. Are you sure?", "Shutdown Node", @@ -598,6 +661,8 @@ private TrayIcon createTrayIcon() { popOff100Button.setVisible(showPopOff); // leftButtons.add(popOffMaxButton); + leftButtons.add(syncButton); + // leftButtons.add(restartButton); leftButtons.add(shutdownButton); @@ -710,8 +775,8 @@ private void initListeners() { public void onPeerCountChanged() { BlockchainProcessor blockchainProcessor = Signum.getBlockchainProcessor(); - SwingUtilities.invokeLater(() -> updatePeerCount(blockchainProcessor.getLastKnownConnectedPeerCount(), - blockchainProcessor.getLastKnownPeerCount())); + Collection allPeers = blockchainProcessor.getAllPeers(); + SwingUtilities.invokeLater(() -> updatePeerCount(allPeers)); } public void onNetVolumeChanged() { @@ -801,6 +866,10 @@ public void startSignumWithGUI() { if (experimentalActive) { experimentalPanel.setVisible(true); timePanel.setVisible(true); + + updateLatestBlock(Signum.getBlockchain().getLastBlock()); + updatePeerCount(Signum.getBlockchainProcessor().getLastKnownConnectedPeerCount(), + Signum.getBlockchainProcessor().getLastKnownPeerCount()); } }); @@ -863,10 +932,15 @@ private void updateTimeLabelVisibility() { private void updateTitle() { String networkName = Signum.getPropertyService().getString(Props.NETWORK_NAME); - SwingUtilities.invokeLater(() -> setTitle( - this.programName + " [" + networkName + "] " + this.version)); - if (trayIcon != null) - trayIcon.setToolTip(trayIcon.getToolTip() + " " + networkName); + String title = this.programName + " [" + networkName + "] " + this.version; + if (isSyncStopped) { + title += " (Sync paused)"; + } + final String finalTitle = title; + SwingUtilities.invokeLater(() -> setTitle(finalTitle)); + if (trayIcon != null) { + trayIcon.setToolTip(finalTitle); + } } private void updateLatestBlock(Block block) { @@ -916,6 +990,41 @@ private void updatePeerCount(int newConnectedCount, int count) { peersCountLabel.setText(count + ""); } + private void addInfoTooltip(JComponent component, String text) { + component.setToolTipText("Right-click for more info"); + component.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + String title = ""; + if (component instanceof JLabel) { + title = ((JLabel) component).getText(); + } else if (component instanceof JButton) { + title = ((JButton) component).getText(); + } + showInfoDialog(title, text, 300); + } + } + }); + } + + private void showInfoDialog(String title, String text, int width) { + if (title.endsWith(":")) { + title = title.substring(0, title.length() - 1); + } + } + + private void updatePeerCount(Collection peers) { + long activeCount = peers.stream().filter(p -> p.getState() != Peer.State.NON_CONNECTED).count(); + long allKnownCount = peers.size(); + long blacklistedCount = peers.stream().filter(Peer::isBlacklisted).count(); + + // The label previously for 'connected' now shows 'active' peers. + connectedPeersLabel.setText(String.valueOf(activeCount)); + peersCountLabel.setText(String.valueOf(allKnownCount)); + blacklistedPeersLabel.setText(blacklistedCount + ""); + } + private String formatDataSize(double bytes) { if (bytes <= 0) { return "0 B"; diff --git a/src/brs/gui/util/MovingAverage.java b/src/brs/gui/util/MovingAverage.java new file mode 100644 index 000000000..5be6c896b --- /dev/null +++ b/src/brs/gui/util/MovingAverage.java @@ -0,0 +1,212 @@ +package brs.gui.util; + +public class MovingAverage { + private final double[] rawDataList; + private int rawDataHead = 0; + private int rawDataCount = 0; + private final double[] avgDataList; + private int avgDataHead = 0; + private int avgDataCount = 0; + private double sum = 0.0; + private double compensation = 0.0; // Kahan summation compensation + private double max = 0.0; + private double min = Double.MAX_VALUE; + private double avg = 0.0; + private int capacity; + private int windowSize; + private short index = 0; + + private final Object lock = new Object(); + + public MovingAverage(int capacity, int windowSize) { + this.capacity = capacity; + this.windowSize = windowSize; + this.rawDataList = new double[capacity]; + this.avgDataList = new double[capacity]; + } + + public void setWindowSize(int newWindowSize) { + synchronized (lock) { + if (newWindowSize <= 0 || newWindowSize > capacity) { + throw new IllegalArgumentException("Window size must be between 1 and capacity."); + } + + this.windowSize = newWindowSize; + recalculateSum(); + } + } + + private void recalculateSum() { + sum = 0.0; + compensation = 0.0; + avg = 0.0; + for (int i = 0; i < Math.min(rawDataCount, windowSize); i++) { + int index = (rawDataHead - 1 - i + capacity) % capacity; + kahanAdd(rawDataList[index]); + } + } + + private void kahanAdd(double input) { + double y = input - compensation; + double t = sum + y; + compensation = (t - sum) - y; + sum = t; + } + + public void add(double value) { + synchronized (lock) { + rawDataList[rawDataHead] = value; + rawDataHead = (rawDataHead + 1) % capacity; + if (rawDataCount < capacity) { + rawDataCount++; + } + + kahanAdd(value); + if (rawDataCount > windowSize) { + int outOfWindowIndex = (rawDataHead - windowSize - 1 + capacity) % capacity; + kahanAdd(-rawDataList[outOfWindowIndex]); + } + + if (sum < 0.0) { + sum = 0.0; + compensation = 0.0; + } + + if (index >= 10000) { + // Recalculate sum with Kahan to prevent precision drift + recalculateSum(); + index = 0; + } + + avg = sum / Math.min(rawDataCount, windowSize); + addToAvg(avg); + index++; + } + } + + private void addToAvg(double value) { + double removed = 0.0; + boolean wasFull = avgDataCount == capacity; + if (wasFull) { + removed = avgDataList[avgDataHead]; + } + + avgDataList[avgDataHead] = value; + avgDataHead = (avgDataHead + 1) % capacity; + if (avgDataCount < capacity) { + avgDataCount++; + } + + max = Math.max(max, value); + min = Math.min(min, value); + if (wasFull) { + if (removed >= max) { + recalculateMax(); + } + if (removed <= min) { + recalculateMin(); + } + } + } + + private void recalculateMax() { + // This method should only be called from within a synchronized(lock) block. + if (avgDataCount == 0) { + max = 0.0; + } else { + // This is O(N) but only called when max is removed or window shrinks. + double currentMax = 0.0; + for (int i = 0; i < avgDataCount; i++) { + double value = avgDataList[i]; + if (value > currentMax) + currentMax = value; + } + max = currentMax; + } + } + + private void recalculateMin() { + // This method should only be called from within a synchronized(lock) block. + if (avgDataCount == 0) { + min = 0.0; + } else { + // This is O(N) but only called when min is removed or window shrinks. + double currentMin = Double.MAX_VALUE; + for (int i = 0; i < avgDataCount; i++) { + double value = avgDataList[i]; + if (value < currentMin) + currentMin = value; + } + min = currentMin; + } + } + + public double getAverage() { + synchronized (lock) { + return avgDataCount == 0 ? 0.0 : Math.max(avg, 0.0); + } + } + + public int size() { + synchronized (lock) { + return rawDataCount; + } + } + + public boolean isEmpty() { + synchronized (lock) { + return rawDataCount == 0; + } + } + + public double getLast() { + synchronized (lock) { + if (rawDataCount == 0) { + return 0.0; + } + return rawDataList[(rawDataHead - 1 + capacity) % capacity]; + } + } + + public double get(int index) { + synchronized (lock) { + if (index < 0 || index >= rawDataCount) { + return 0.0; + } + int internalIndex = (rawDataHead - rawDataCount + index + capacity) % capacity; + return rawDataList[internalIndex]; + } + } + + public double getMax() { + synchronized (lock) { + return Math.max(max, 0.0); + } + } + + public double getMin() { + synchronized (lock) { + return avgDataCount == 0 ? 0.0 : Math.max(min, 0.0); + } + } + + public double getSum() { + synchronized (lock) { + return Math.max(sum, 0.0); + } + } + + public void clear() { + synchronized (lock) { + rawDataHead = 0; + rawDataCount = 0; + avgDataHead = 0; + avgDataCount = 0; + sum = 0.0; + compensation = 0.0; + max = 0.0; + min = Double.MAX_VALUE; + avg = 0.0; + } + } +} \ No newline at end of file diff --git a/src/brs/services/impl/EscrowServiceImpl.java b/src/brs/services/impl/EscrowServiceImpl.java index 7f5d243a9..aacfbede7 100644 --- a/src/brs/services/impl/EscrowServiceImpl.java +++ b/src/brs/services/impl/EscrowServiceImpl.java @@ -21,235 +21,248 @@ public class EscrowServiceImpl implements EscrowService { - private final VersionedEntityTable escrowTable; - private final LongKeyFactory escrowDbKeyFactory; - private final VersionedEntityTable decisionTable; - private final LinkKeyFactory decisionDbKeyFactory; - private final EscrowStore escrowStore; - private final Blockchain blockchain; - private final AliasService aliasService; - private final AccountService accountService; - private final List resultTransactions; - - public EscrowServiceImpl(EscrowStore escrowStore, Blockchain blockchain, AliasService aliasService, AccountService accountService) { - this.escrowStore = escrowStore; - this.escrowTable = escrowStore.getEscrowTable(); - this.escrowDbKeyFactory = escrowStore.getEscrowDbKeyFactory(); - this.decisionTable = escrowStore.getDecisionTable(); - this.decisionDbKeyFactory = escrowStore.getDecisionDbKeyFactory(); - this.resultTransactions = escrowStore.getResultTransactions(); - this.blockchain = blockchain; - this.aliasService = aliasService; - this.accountService = accountService; - } - - @Override - public Collection getAllEscrowTransactions() { - return escrowTable.getAll(0, -1); - } - - @Override - public Escrow getEscrowTransaction(Long id) { - return escrowTable.get(escrowDbKeyFactory.newKey(id)); - } - - @Override - public Collection getEscrowTransactionsByParticipant(Long accountId) { - return escrowStore.getEscrowTransactionsByParticipant(accountId); - } - - @Override - public boolean isEnabled() { - if(blockchain.getLastBlock().getHeight() >= Constants.SIGNUM_ESCROW_START_BLOCK) { - return true; + private final VersionedEntityTable escrowTable; + private final LongKeyFactory escrowDbKeyFactory; + private final VersionedEntityTable decisionTable; + private final LinkKeyFactory decisionDbKeyFactory; + private final EscrowStore escrowStore; + private final Blockchain blockchain; + private final AliasService aliasService; + private final AccountService accountService; + private final List resultTransactions; + + public EscrowServiceImpl(EscrowStore escrowStore, Blockchain blockchain, AliasService aliasService, + AccountService accountService) { + this.escrowStore = escrowStore; + this.escrowTable = escrowStore.getEscrowTable(); + this.escrowDbKeyFactory = escrowStore.getEscrowDbKeyFactory(); + this.decisionTable = escrowStore.getDecisionTable(); + this.decisionDbKeyFactory = escrowStore.getDecisionDbKeyFactory(); + this.resultTransactions = escrowStore.getResultTransactions(); + this.blockchain = blockchain; + this.aliasService = aliasService; + this.accountService = accountService; } - Alias escrowEnabled = aliasService.getAlias("featureescrow", 0L); - return escrowEnabled != null && escrowEnabled.getAliasUri().equals("enabled"); - } - - @Override - public void removeEscrowTransaction(Long id) { - Escrow escrow = escrowTable.get(escrowDbKeyFactory.newKey(id)); - if(escrow == null) { - return; - } - escrow.getDecisions().forEach(decisionTable::delete); - escrowTable.delete(escrow); - } - - - @Override - public void addEscrowTransaction(Account sender, Account recipient, Long id, Long amountNQT, int requiredSigners, Collection signers, int deadline, DecisionType deadlineAction) { - final SignumKey dbKey = escrowDbKeyFactory.newKey(id); - Escrow newEscrowTransaction = new Escrow(dbKey, sender, recipient, id, amountNQT, requiredSigners, deadline, deadlineAction); - escrowTable.insert(newEscrowTransaction); - SignumKey senderDbKey = decisionDbKeyFactory.newKey(id, sender.getId()); - Decision senderDecision = new Decision(senderDbKey, id, sender.getId(), DecisionType.UNDECIDED); - decisionTable.insert(senderDecision); - SignumKey recipientDbKey = decisionDbKeyFactory.newKey(id, recipient.getId()); - Decision recipientDecision = new Decision(recipientDbKey, id, recipient.getId(), DecisionType.UNDECIDED); - decisionTable.insert(recipientDecision); - for(Long signer : signers) { - SignumKey signerDbKey = decisionDbKeyFactory.newKey(id, signer); - Decision decision = new Decision(signerDbKey, id, signer, DecisionType.UNDECIDED); - decisionTable.insert(decision); - } - } - - @Override - public synchronized void sign(Long id, DecisionType decision, Escrow escrow) { - if(id.equals(escrow.getSenderId()) && decision != DecisionType.RELEASE) { - return; + @Override + public Collection getAllEscrowTransactions() { + return escrowTable.getAll(0, -1); } - if(id.equals(escrow.getRecipientId()) && decision != DecisionType.REFUND) { - return; + @Override + public Escrow getEscrowTransaction(Long id) { + return escrowTable.get(escrowDbKeyFactory.newKey(id)); } - Decision decisionChange = decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), id)); - if(decisionChange == null) { - return; + @Override + public Collection getEscrowTransactionsByParticipant(Long accountId) { + return escrowStore.getEscrowTransactionsByParticipant(accountId); } - decisionChange.setDecision(decision); - decisionTable.insert(decisionChange); - - updatedEscrowIds.add(escrow.getId()); - } + @Override + public boolean isEnabled() { + if (blockchain.getLastBlock().getHeight() >= Constants.SIGNUM_ESCROW_START_BLOCK) { + return true; + } - @Override - public DecisionType checkComplete(Escrow escrow) { - Decision senderDecision = decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), escrow.getSenderId())); - if(senderDecision.getDecision() == DecisionType.RELEASE) { - return DecisionType.RELEASE; - } - Decision recipientDecision = decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), escrow.getRecipientId())); - if(recipientDecision.getDecision() == DecisionType.REFUND) { - return DecisionType.REFUND; + Alias escrowEnabled = aliasService.getAlias("featureescrow", 0L); + return escrowEnabled != null && escrowEnabled.getAliasUri().equals("enabled"); } - int countRelease = 0; - int countRefund = 0; - int countSplit = 0; - - for (Decision decision : Signum.getStores().getEscrowStore().getDecisions(escrow.getId())) { - if(decision.getAccountId().equals(escrow.getSenderId()) || - decision.getAccountId().equals(escrow.getRecipientId())) { - continue; - } - switch(decision.getDecision()) { - case RELEASE: - countRelease++; - break; - case REFUND: - countRefund++; - break; - case SPLIT: - countSplit++; - break; - default: - break; - } + @Override + public void removeEscrowTransaction(Long id) { + Escrow escrow = escrowTable.get(escrowDbKeyFactory.newKey(id)); + if (escrow == null) { + return; + } + escrow.getDecisions().forEach(decisionTable::delete); + escrowTable.delete(escrow); } - if(countRelease >= escrow.getRequiredSigners()) { - return DecisionType.RELEASE; - } - if(countRefund >= escrow.getRequiredSigners()) { - return DecisionType.REFUND; - } - if(countSplit >= escrow.getRequiredSigners()) { - return DecisionType.SPLIT; + @Override + public void addEscrowTransaction(Account sender, Account recipient, Long id, Long amountNQT, int requiredSigners, + Collection signers, int deadline, DecisionType deadlineAction) { + final SignumKey dbKey = escrowDbKeyFactory.newKey(id); + Escrow newEscrowTransaction = new Escrow(dbKey, sender, recipient, id, amountNQT, requiredSigners, deadline, + deadlineAction); + escrowTable.insert(newEscrowTransaction); + SignumKey senderDbKey = decisionDbKeyFactory.newKey(id, sender.getId()); + Decision senderDecision = new Decision(senderDbKey, id, sender.getId(), DecisionType.UNDECIDED); + decisionTable.insert(senderDecision); + SignumKey recipientDbKey = decisionDbKeyFactory.newKey(id, recipient.getId()); + Decision recipientDecision = new Decision(recipientDbKey, id, recipient.getId(), DecisionType.UNDECIDED); + decisionTable.insert(recipientDecision); + for (Long signer : signers) { + SignumKey signerDbKey = decisionDbKeyFactory.newKey(id, signer); + Decision decision = new Decision(signerDbKey, id, signer, DecisionType.UNDECIDED); + decisionTable.insert(decision); + } } - return DecisionType.UNDECIDED; - } + @Override + public synchronized void sign(Long id, DecisionType decision, Escrow escrow) { + if (id.equals(escrow.getSenderId()) && decision != DecisionType.RELEASE) { + return; + } - private static Condition getUpdateOnBlockClause(final int timestamp) { - return ESCROW.DEADLINE.lt(timestamp); - } + if (id.equals(escrow.getRecipientId()) && decision != DecisionType.REFUND) { + return; + } + Decision decisionChange = decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), id)); + if (decisionChange == null) { + return; + } + decisionChange.setDecision(decision); - private final ConcurrentSkipListSet updatedEscrowIds = new ConcurrentSkipListSet<>(); + decisionTable.insert(decisionChange); - @Override - public void updateOnBlock(Block block, int blockchainHeight) { - resultTransactions.clear(); + updatedEscrowIds.add(escrow.getId()); + } - escrowTable.getManyBy(getUpdateOnBlockClause(block.getTimestamp()), 0, -1).forEach(escrow -> updatedEscrowIds.add(escrow.getId())); + @Override + public DecisionType checkComplete(Escrow escrow) { + Decision senderDecision = decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), escrow.getSenderId())); + if (senderDecision.getDecision() == DecisionType.RELEASE) { + return DecisionType.RELEASE; + } + Decision recipientDecision = decisionTable + .get(decisionDbKeyFactory.newKey(escrow.getId(), escrow.getRecipientId())); + if (recipientDecision.getDecision() == DecisionType.REFUND) { + return DecisionType.REFUND; + } - if (!updatedEscrowIds.isEmpty()) { - for (Long escrowId : updatedEscrowIds) { - Escrow escrow = escrowTable.get(escrowDbKeyFactory.newKey(escrowId)); - Escrow.DecisionType result = checkComplete(escrow); - if (result != Escrow.DecisionType.UNDECIDED || escrow.getDeadline() < block.getTimestamp()) { - if (result == Escrow.DecisionType.UNDECIDED) { - result = escrow.getDeadlineAction(); - } - doPayout(result, block, blockchainHeight, escrow); + int countRelease = 0; + int countRefund = 0; + int countSplit = 0; + + for (Decision decision : Signum.getStores().getEscrowStore().getDecisions(escrow.getId())) { + if (decision.getAccountId().equals(escrow.getSenderId()) || + decision.getAccountId().equals(escrow.getRecipientId())) { + continue; + } + switch (decision.getDecision()) { + case RELEASE: + countRelease++; + break; + case REFUND: + countRefund++; + break; + case SPLIT: + countSplit++; + break; + default: + break; + } + } - removeEscrowTransaction(escrowId); + if (countRelease >= escrow.getRequiredSigners()) { + return DecisionType.RELEASE; + } + if (countRefund >= escrow.getRequiredSigners()) { + return DecisionType.REFUND; + } + if (countSplit >= escrow.getRequiredSigners()) { + return DecisionType.SPLIT; } - } - if (!resultTransactions.isEmpty()) { - Signum.getDbs().getTransactionDb().saveTransactions( resultTransactions); - } - updatedEscrowIds.clear(); + + return DecisionType.UNDECIDED; } - } - - @Override - public synchronized void doPayout(DecisionType result, Block block, int blockchainHeight, Escrow escrow) { - switch(result) { - case RELEASE: - accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getRecipientId()), escrow.getAmountNQT()); - saveResultTransaction(block, escrow.getId(), escrow.getRecipientId(), escrow.getAmountNQT(), DecisionType.RELEASE, blockchainHeight); - break; - case REFUND: - accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getSenderId()), escrow.getAmountNQT()); - saveResultTransaction(block, escrow.getId(), escrow.getSenderId(), escrow.getAmountNQT(), DecisionType.REFUND, blockchainHeight); - break; - case SPLIT: - Long halfAmountNQT = escrow.getAmountNQT() / 2; - accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getRecipientId()), halfAmountNQT); - accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getSenderId()), escrow.getAmountNQT() - halfAmountNQT); - saveResultTransaction(block, escrow.getId(), escrow.getRecipientId(), halfAmountNQT, DecisionType.SPLIT, blockchainHeight); - saveResultTransaction(block, escrow.getId(), escrow.getSenderId(), escrow.getAmountNQT() - halfAmountNQT, DecisionType.SPLIT, blockchainHeight); - break; - default: // should never get here - break; + + private static Condition getUpdateOnBlockClause(final int timestamp) { + return ESCROW.DEADLINE.lt(timestamp); } - } - - @Override - public boolean isIdSigner(Long id, Escrow escrow) { - return decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), id)) != null; - } - - @Override - public void saveResultTransaction(Block block, Long escrowId, Long recipientId, Long amountNQT, DecisionType decision, int blockchainHeight) { - Attachment.AbstractAttachment attachment = new Attachment.AdvancedPaymentEscrowResult(escrowId, decision, blockchainHeight); - Transaction.Builder builder = new Transaction.Builder((byte)1, Genesis.getCreatorPublicKey(), - amountNQT, 0L, block.getTimestamp(), (short)1440, attachment); - builder.senderId(0L) - .recipientId(recipientId) - .blockId(block.getId()) - .height(block.getHeight()) - .blockTimestamp(block.getTimestamp()) - .ecBlockHeight(0) - .ecBlockId(0L); - - Transaction transaction; - try { - transaction = builder.build(); + + private final ConcurrentSkipListSet updatedEscrowIds = new ConcurrentSkipListSet<>(); + + @Override + public void updateOnBlock(Block block, int blockchainHeight) { + resultTransactions.clear(); + + escrowTable.getManyBy(getUpdateOnBlockClause(block.getTimestamp()), 0, -1) + .forEach(escrow -> updatedEscrowIds.add(escrow.getId())); + + if (!updatedEscrowIds.isEmpty()) { + for (Long escrowId : updatedEscrowIds) { + Escrow escrow = escrowTable.get(escrowDbKeyFactory.newKey(escrowId)); + Escrow.DecisionType result = checkComplete(escrow); + if (result != Escrow.DecisionType.UNDECIDED || escrow.getDeadline() < block.getTimestamp()) { + if (result == Escrow.DecisionType.UNDECIDED) { + result = escrow.getDeadlineAction(); + } + doPayout(result, block, blockchainHeight, escrow); + + removeEscrowTransaction(escrowId); + } + } + if (!resultTransactions.isEmpty()) { + Signum.getDbs().getTransactionDb().saveTransactions(resultTransactions); + block.setEscrowTransactions(resultTransactions); + } + updatedEscrowIds.clear(); + } } - catch(SignumException.NotValidException e) { - throw new RuntimeException(e.toString(), e); + + @Override + public synchronized void doPayout(DecisionType result, Block block, int blockchainHeight, Escrow escrow) { + switch (result) { + case RELEASE: + accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getRecipientId()), + escrow.getAmountNQT()); + saveResultTransaction(block, escrow.getId(), escrow.getRecipientId(), escrow.getAmountNQT(), + DecisionType.RELEASE, blockchainHeight); + break; + case REFUND: + accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getSenderId()), + escrow.getAmountNQT()); + saveResultTransaction(block, escrow.getId(), escrow.getSenderId(), escrow.getAmountNQT(), + DecisionType.REFUND, blockchainHeight); + break; + case SPLIT: + Long halfAmountNQT = escrow.getAmountNQT() / 2; + accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getRecipientId()), + halfAmountNQT); + accountService.addToBalanceAndUnconfirmedBalanceNQT(accountService.getAccount(escrow.getSenderId()), + escrow.getAmountNQT() - halfAmountNQT); + saveResultTransaction(block, escrow.getId(), escrow.getRecipientId(), halfAmountNQT, DecisionType.SPLIT, + blockchainHeight); + saveResultTransaction(block, escrow.getId(), escrow.getSenderId(), + escrow.getAmountNQT() - halfAmountNQT, DecisionType.SPLIT, blockchainHeight); + break; + default: // should never get here + break; + } + } + + @Override + public boolean isIdSigner(Long id, Escrow escrow) { + return decisionTable.get(decisionDbKeyFactory.newKey(escrow.getId(), id)) != null; } - if(!Signum.getDbs().getTransactionDb().hasTransaction(transaction.getId())) { - resultTransactions.add(transaction); + @Override + public void saveResultTransaction(Block block, Long escrowId, Long recipientId, Long amountNQT, + DecisionType decision, int blockchainHeight) { + Attachment.AbstractAttachment attachment = new Attachment.AdvancedPaymentEscrowResult(escrowId, decision, + blockchainHeight); + Transaction.Builder builder = new Transaction.Builder((byte) 1, Genesis.getCreatorPublicKey(), + amountNQT, 0L, block.getTimestamp(), (short) 1440, attachment); + builder.senderId(0L) + .recipientId(recipientId) + .blockId(block.getId()) + .height(block.getHeight()) + .blockTimestamp(block.getTimestamp()) + .ecBlockHeight(0) + .ecBlockId(0L); + + Transaction transaction; + try { + transaction = builder.build(); + } catch (SignumException.NotValidException e) { + throw new RuntimeException(e.toString(), e); + } + + if (!Signum.getDbs().getTransactionDb().hasTransaction(transaction.getId())) { + resultTransactions.add(transaction); + } } - } } diff --git a/src/brs/services/impl/SubscriptionServiceImpl.java b/src/brs/services/impl/SubscriptionServiceImpl.java index e8694800c..f2e32b098 100644 --- a/src/brs/services/impl/SubscriptionServiceImpl.java +++ b/src/brs/services/impl/SubscriptionServiceImpl.java @@ -21,226 +21,232 @@ public class SubscriptionServiceImpl implements SubscriptionService { - private final Logger logger = LoggerFactory.getLogger(SubscriptionServiceImpl.class); - private final SubscriptionStore subscriptionStore; - private final VersionedEntityTable subscriptionTable; - private final LongKeyFactory subscriptionDbKeyFactory; - - private final Blockchain blockchain; - private final AliasService aliasService; - private final AccountService accountService; - - private final TransactionDb transactionDb; - - private static final List paymentTransactions = new ArrayList<>(); - private static final List appliedSubscriptions = new ArrayList<>(); - private static final Set removeSubscriptions = new HashSet<>(); - - public SubscriptionServiceImpl(SubscriptionStore subscriptionStore, TransactionDb transactionDb, Blockchain blockchain, AliasService aliasService, AccountService accountService) { - this.subscriptionStore = subscriptionStore; - this.subscriptionTable = subscriptionStore.getSubscriptionTable(); - this.subscriptionDbKeyFactory = subscriptionStore.getSubscriptionDbKeyFactory(); - this.transactionDb = transactionDb; - this.blockchain = blockchain; - this.aliasService = aliasService; - this.accountService = accountService; - } - - @Override - public Subscription getSubscription(Long id) { - return subscriptionTable.get(subscriptionDbKeyFactory.newKey(id)); - } - - @Override - public Collection getSubscriptionsByParticipant(Long accountId) { - return subscriptionStore.getSubscriptionsByParticipant(accountId); - } - - @Override - public Collection getSubscriptionsToId(Long accountId) { - return subscriptionStore.getSubscriptionsToId(accountId); - } - - @Override - public void addSubscription(Account sender, long recipientId, Long id, Long amountNQT, int startTimestamp, int frequency) { - final SignumKey dbKey = subscriptionDbKeyFactory.newKey(id); - final Subscription subscription = new Subscription(sender.getId(), recipientId, id, amountNQT, frequency, startTimestamp + frequency, dbKey); - - subscriptionTable.insert(subscription); - } - - @Override - public boolean isEnabled() { - if (blockchain.getLastBlock().getHeight() >= Constants.SIGNUM_SUBSCRIPTION_START_BLOCK) { - return true; - } - - final Alias subscriptionEnabled = aliasService.getAlias("featuresubscription", 0L); - return subscriptionEnabled != null && subscriptionEnabled.getAliasUri().equals("enabled"); - } - - @Override - public void applyConfirmed(Block block, int blockchainHeight) { - paymentTransactions.clear(); - for (Subscription subscription : appliedSubscriptions) { - apply(block, blockchainHeight, subscription); - } - subscriptionStore.saveSubscriptions(appliedSubscriptions); - - if (! paymentTransactions.isEmpty()) { - transactionDb.saveTransactions(paymentTransactions); - } - removeSubscriptions.forEach(this::removeSubscription); - if(logger.isDebugEnabled()) { - if(appliedSubscriptions.size() > 0 || removeSubscriptions.size() > 0) { - logger.debug("Subscriptions: applied {}, removed {}", appliedSubscriptions.size(), removeSubscriptions.size()); - } - } - } - - private long getFee(int height) { - if (Signum.getFluxCapacitor().getValue(FluxValues.SODIUM, height)) - return Signum.getFluxCapacitor().getValue(FluxValues.FEE_QUANT, height); - return Constants.ONE_SIGNA; - } - - @Override - public void removeSubscription(Long id) { - Subscription subscription = subscriptionTable.get(subscriptionDbKeyFactory.newKey(id)); - if (subscription != null) { - if(subscription.getRecipientId()!=0L) { - Alias alias = aliasService.getAlias(subscription.getRecipientId()); - if(alias != null && alias.getId() == subscription.getId()) { - Offer offer = aliasService.getOffer(alias); - if(offer != null) { - Signum.getStores().getAliasStore().getOfferTable().delete(offer); - } - Signum.getStores().getAliasStore().getAliasTable().delete(alias); + private final Logger logger = LoggerFactory.getLogger(SubscriptionServiceImpl.class); + private final SubscriptionStore subscriptionStore; + private final VersionedEntityTable subscriptionTable; + private final LongKeyFactory subscriptionDbKeyFactory; + + private final Blockchain blockchain; + private final AliasService aliasService; + private final AccountService accountService; + + private final TransactionDb transactionDb; + + private static final List paymentTransactions = new ArrayList<>(); + private static final List appliedSubscriptions = new ArrayList<>(); + private static final Set removeSubscriptions = new HashSet<>(); + + public SubscriptionServiceImpl(SubscriptionStore subscriptionStore, TransactionDb transactionDb, + Blockchain blockchain, AliasService aliasService, AccountService accountService) { + this.subscriptionStore = subscriptionStore; + this.subscriptionTable = subscriptionStore.getSubscriptionTable(); + this.subscriptionDbKeyFactory = subscriptionStore.getSubscriptionDbKeyFactory(); + this.transactionDb = transactionDb; + this.blockchain = blockchain; + this.aliasService = aliasService; + this.accountService = accountService; + } + + @Override + public Subscription getSubscription(Long id) { + return subscriptionTable.get(subscriptionDbKeyFactory.newKey(id)); + } + + @Override + public Collection getSubscriptionsByParticipant(Long accountId) { + return subscriptionStore.getSubscriptionsByParticipant(accountId); + } + + @Override + public Collection getSubscriptionsToId(Long accountId) { + return subscriptionStore.getSubscriptionsToId(accountId); + } + + @Override + public void addSubscription(Account sender, long recipientId, Long id, Long amountNQT, int startTimestamp, + int frequency) { + final SignumKey dbKey = subscriptionDbKeyFactory.newKey(id); + final Subscription subscription = new Subscription(sender.getId(), recipientId, id, amountNQT, frequency, + startTimestamp + frequency, dbKey); + + subscriptionTable.insert(subscription); + } + + @Override + public boolean isEnabled() { + if (blockchain.getLastBlock().getHeight() >= Constants.SIGNUM_SUBSCRIPTION_START_BLOCK) { + return true; + } + + final Alias subscriptionEnabled = aliasService.getAlias("featuresubscription", 0L); + return subscriptionEnabled != null && subscriptionEnabled.getAliasUri().equals("enabled"); + } + + @Override + public void applyConfirmed(Block block, int blockchainHeight) { + paymentTransactions.clear(); + for (Subscription subscription : appliedSubscriptions) { + apply(block, blockchainHeight, subscription); + } + subscriptionStore.saveSubscriptions(appliedSubscriptions); + + if (!paymentTransactions.isEmpty()) { + transactionDb.saveTransactions(paymentTransactions); + block.setSubscriptionTransactions(paymentTransactions); + } + removeSubscriptions.forEach(this::removeSubscription); + if (logger.isDebugEnabled()) { + if (appliedSubscriptions.size() > 0 || removeSubscriptions.size() > 0) { + logger.debug("Subscriptions: applied {}, removed {}", appliedSubscriptions.size(), + removeSubscriptions.size()); + } + } + } + + private long getFee(int height) { + if (Signum.getFluxCapacitor().getValue(FluxValues.SODIUM, height)) + return Signum.getFluxCapacitor().getValue(FluxValues.FEE_QUANT, height); + return Constants.ONE_SIGNA; + } + + @Override + public void removeSubscription(Long id) { + Subscription subscription = subscriptionTable.get(subscriptionDbKeyFactory.newKey(id)); + if (subscription != null) { + if (subscription.getRecipientId() != 0L) { + Alias alias = aliasService.getAlias(subscription.getRecipientId()); + if (alias != null && alias.getId() == subscription.getId()) { + Offer offer = aliasService.getOffer(alias); + if (offer != null) { + Signum.getStores().getAliasStore().getOfferTable().delete(offer); + } + Signum.getStores().getAliasStore().getAliasTable().delete(alias); + } + } + subscriptionTable.delete(subscription); + } + } + + @Override + public long calculateFees(int timestamp, int height) { + long totalFeeNQT = 0; + List appliedUnconfirmedSubscriptions = new ArrayList<>(); + for (Subscription subscription : subscriptionStore.getUpdateSubscriptions(timestamp)) { + if (removeSubscriptions.contains(subscription.getId())) { + continue; + } + if (applyUnconfirmed(subscription, height)) { + appliedUnconfirmedSubscriptions.add(subscription); + } + } + if (!appliedUnconfirmedSubscriptions.isEmpty()) { + for (Subscription subscription : appliedUnconfirmedSubscriptions) { + totalFeeNQT = Convert.safeAdd(totalFeeNQT, getFee(height)); + undoUnconfirmed(subscription, height); + } } - } - subscriptionTable.delete(subscription); - } - } - - @Override - public long calculateFees(int timestamp, int height) { - long totalFeeNQT = 0; - List appliedUnconfirmedSubscriptions = new ArrayList<>(); - for (Subscription subscription : subscriptionStore.getUpdateSubscriptions(timestamp)){ - if (removeSubscriptions.contains(subscription.getId())) { - continue; - } - if (applyUnconfirmed(subscription, height)) { - appliedUnconfirmedSubscriptions.add(subscription); - } - } - if (! appliedUnconfirmedSubscriptions.isEmpty()) { - for (Subscription subscription : appliedUnconfirmedSubscriptions) { - totalFeeNQT = Convert.safeAdd(totalFeeNQT, getFee(height)); - undoUnconfirmed(subscription, height); - } - } - return totalFeeNQT; - } - - @Override - public void clearRemovals() { - removeSubscriptions.clear(); - } - - @Override - public void addRemoval(Long id) { - removeSubscriptions.add(id); - } - - @Override - public long applyUnconfirmed(int timestamp, int height) { - appliedSubscriptions.clear(); - long totalFees = 0; - for (Subscription subscription : subscriptionStore.getUpdateSubscriptions(timestamp)) { - if (removeSubscriptions.contains(subscription.getId())) { - continue; - } - if (applyUnconfirmed(subscription, height)) { - appliedSubscriptions.add(subscription); - totalFees += getFee(height); - } else { - removeSubscriptions.add(subscription.getId()); - } - } - return totalFees; - } - - private Account getSender(Subscription subscription) { - return accountService.getAccount(subscription.getSenderId()); - } - - private Account getRecipient(Subscription subscription) { - if(Signum.getFluxCapacitor().getValue(FluxValues.SMART_ALIASES)) { - Alias alias = aliasService.getAlias(subscription.getRecipientId()); - if(alias != null) { - Alias tld = aliasService.getTLD(alias.getTld()); - return accountService.getOrAddAccount(tld.getAccountId()); - } - } - return accountService.getOrAddAccount(subscription.getRecipientId()); - } - - private boolean applyUnconfirmed(Subscription subscription, int height) { - Account sender = getSender(subscription); - long totalAmountNQT = Convert.safeAdd(subscription.getAmountNQT(), getFee(height)); - - Account.Balance senderBalance = sender == null ? null : Account.getAccountBalance(sender.getId()); - if (sender == null || senderBalance.getUnconfirmedBalanceNqt() < totalAmountNQT) { - return false; - } - - accountService.addToUnconfirmedBalanceNQT(sender, -totalAmountNQT); - - return true; - } - - private void undoUnconfirmed(Subscription subscription, int height) { - Account sender = getSender(subscription); - long totalAmountNQT = Convert.safeAdd(subscription.getAmountNQT(), getFee(height)); - - if (sender != null) { - accountService.addToUnconfirmedBalanceNQT(sender, totalAmountNQT); - } - } - - private void apply(Block block, int blockchainHeight, Subscription subscription) { - Account sender = getSender(subscription); - Account recipient = getRecipient(subscription); - - long totalAmountNQT = Convert.safeAdd(subscription.getAmountNQT(), getFee(block.getHeight())); - - accountService.addToBalanceNQT(sender, -totalAmountNQT); - accountService.addToBalanceAndUnconfirmedBalanceNQT(recipient, subscription.getAmountNQT()); - - Attachment.AbstractAttachment attachment = new Attachment.AdvancedPaymentSubscriptionPayment(subscription.getId(), blockchainHeight); - Transaction.Builder builder = new Transaction.Builder((byte) 1, - sender.getPublicKey(), subscription.getAmountNQT(), - getFee(block.getHeight()), - subscription.getTimeNext(), (short) 1440, attachment); - - try { - builder.senderId(sender.getId()) - .recipientId(recipient.getId()) - .blockId(block.getId()) - .height(block.getHeight()) - .blockTimestamp(block.getTimestamp()) - .ecBlockHeight(0) - .ecBlockId(0L); - Transaction transaction = builder.build(); - if (!transactionDb.hasTransaction(transaction.getId())) { - paymentTransactions.add(transaction); - } - } catch (NotValidException e) { - throw new RuntimeException("Failed to build subscription payment transaction", e); - } - - subscription.timeNextGetAndAdd(subscription.getFrequency()); - } + return totalFeeNQT; + } + + @Override + public void clearRemovals() { + removeSubscriptions.clear(); + } + + @Override + public void addRemoval(Long id) { + removeSubscriptions.add(id); + } + + @Override + public long applyUnconfirmed(int timestamp, int height) { + appliedSubscriptions.clear(); + long totalFees = 0; + for (Subscription subscription : subscriptionStore.getUpdateSubscriptions(timestamp)) { + if (removeSubscriptions.contains(subscription.getId())) { + continue; + } + if (applyUnconfirmed(subscription, height)) { + appliedSubscriptions.add(subscription); + totalFees += getFee(height); + } else { + removeSubscriptions.add(subscription.getId()); + } + } + return totalFees; + } + + private Account getSender(Subscription subscription) { + return accountService.getAccount(subscription.getSenderId()); + } + + private Account getRecipient(Subscription subscription) { + if (Signum.getFluxCapacitor().getValue(FluxValues.SMART_ALIASES)) { + Alias alias = aliasService.getAlias(subscription.getRecipientId()); + if (alias != null) { + Alias tld = aliasService.getTLD(alias.getTld()); + return accountService.getOrAddAccount(tld.getAccountId()); + } + } + return accountService.getOrAddAccount(subscription.getRecipientId()); + } + + private boolean applyUnconfirmed(Subscription subscription, int height) { + Account sender = getSender(subscription); + long totalAmountNQT = Convert.safeAdd(subscription.getAmountNQT(), getFee(height)); + + Account.Balance senderBalance = sender == null ? null : Account.getAccountBalance(sender.getId()); + if (sender == null || senderBalance.getUnconfirmedBalanceNqt() < totalAmountNQT) { + return false; + } + + accountService.addToUnconfirmedBalanceNQT(sender, -totalAmountNQT); + + return true; + } + + private void undoUnconfirmed(Subscription subscription, int height) { + Account sender = getSender(subscription); + long totalAmountNQT = Convert.safeAdd(subscription.getAmountNQT(), getFee(height)); + + if (sender != null) { + accountService.addToUnconfirmedBalanceNQT(sender, totalAmountNQT); + } + } + + private void apply(Block block, int blockchainHeight, Subscription subscription) { + Account sender = getSender(subscription); + Account recipient = getRecipient(subscription); + + long totalAmountNQT = Convert.safeAdd(subscription.getAmountNQT(), getFee(block.getHeight())); + + accountService.addToBalanceNQT(sender, -totalAmountNQT); + accountService.addToBalanceAndUnconfirmedBalanceNQT(recipient, subscription.getAmountNQT()); + + Attachment.AbstractAttachment attachment = new Attachment.AdvancedPaymentSubscriptionPayment( + subscription.getId(), blockchainHeight); + Transaction.Builder builder = new Transaction.Builder((byte) 1, + sender.getPublicKey(), subscription.getAmountNQT(), + getFee(block.getHeight()), + subscription.getTimeNext(), (short) 1440, attachment); + + try { + builder.senderId(sender.getId()) + .recipientId(recipient.getId()) + .blockId(block.getId()) + .height(block.getHeight()) + .blockTimestamp(block.getTimestamp()) + .ecBlockHeight(0) + .ecBlockId(0L); + Transaction transaction = builder.build(); + if (!transactionDb.hasTransaction(transaction.getId())) { + paymentTransactions.add(transaction); + } + } catch (NotValidException e) { + throw new RuntimeException("Failed to build subscription payment transaction", e); + } + + subscription.timeNextGetAndAdd(subscription.getFrequency()); + } } diff --git a/src/signum/Launcher.java b/src/signum/Launcher.java index 623000ea2..b66f8efe0 100644 --- a/src/signum/Launcher.java +++ b/src/signum/Launcher.java @@ -13,43 +13,45 @@ import java.lang.reflect.InvocationTargetException; public class Launcher { - public static void main(String[] args) { - Logger logger = LoggerFactory.getLogger(Launcher.class); - boolean canRunGui = true; + public static void main(String[] args) { + Logger logger = LoggerFactory.getLogger(Launcher.class); + boolean canRunGui = true; - try { - CommandLine cmd = new DefaultParser().parse(Signum.CLI_OPTIONS, args); - if (cmd.hasOption("h")) { - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("java -jar signum-node.jar", "Signum Node version " + Signum.VERSION, - Signum.CLI_OPTIONS, - "Check for updates at https://github.com/signum-network/signum-node", true); - return; - } - if (cmd.hasOption("l")) { - logger.info("Running in headless mode as specified by argument"); - canRunGui = false; - } - } catch (ParseException e) { - logger.error("Error parsing arguments", e); - } + try { + CommandLine cmd = new DefaultParser().parse(Signum.CLI_OPTIONS, args); + if (cmd.hasOption("h")) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("java -jar signum-node.jar", "Signum Node version " + Signum.VERSION, + Signum.CLI_OPTIONS, + "Check for updates at https://github.com/signum-network/signum-node", true); + return; + } + if (cmd.hasOption("l")) { + logger.info("Running in headless mode as specified by argument"); + canRunGui = false; + } + } catch (ParseException e) { + logger.error("Error parsing arguments", e); + } - if (canRunGui && GraphicsEnvironment.isHeadless()) { - logger.error("Cannot start GUI as running in headless environment"); - canRunGui = false; - } + if (canRunGui && GraphicsEnvironment.isHeadless()) { + logger.error("Cannot start GUI as running in headless environment"); + canRunGui = false; + } - if (canRunGui) { - try { - Class.forName("brs.SignumGUI") - .getDeclaredMethod("main", String[].class) - .invoke(null, (Object) args); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - logger.warn("Your build does not seem to include the SignumGUI extension or it cannot be run. Running as headless..."); - Signum.main(args); - } - } else { - Signum.main(args); + if (canRunGui) { + try { + Class.forName("brs.gui.SignumGUI") + .getDeclaredMethod("main", String[].class) + .invoke(null, (Object) args); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException e) { + logger.warn( + "Your build does not seem to include the SignumGUI extension or it cannot be run. Running as headless..."); + Signum.main(args); + } + } else { + Signum.main(args); + } } - } }