From e793ed17df45a75b49e2b6879f3c1b6b938d705c Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Tue, 21 Mar 2023 17:43:15 -0500 Subject: [PATCH 01/13] commit in progress rework and log reduction to allow merge from main --- tests/performance_tests/log_reader.py | 60 ++++--------------- .../performance_test_basic.py | 44 +++++++++++--- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index 348b1a2af2..0b27e57988 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -156,7 +156,6 @@ class blockData(): cpu: int = 0 elapsed: int = 0 time: int = 0 - latency: int = 0 class chainData(): def __init__(self): @@ -168,7 +167,6 @@ def __init__(self): self.totalCpu = 0 self.totalElapsed = 0 self.totalTime = 0 - self.totalLatency = 0 self.droppedBlocks = {} self.forkedBlocks = [] def __eq__(self, other): @@ -178,19 +176,17 @@ def __eq__(self, other): self.totalNet == other.totalNet and\ self.totalCpu == other.totalCpu and\ self.totalElapsed == other.totalElapsed and\ - self.totalTime == other.totalTime and\ - self.totalLatency == other.totalLatency - def updateTotal(self, transactions, net, cpu, elapsed, time, latency): + self.totalTime == other.totalTime + def updateTotal(self, transactions, net, cpu, elapsed, time): self.totalTransactions += transactions self.totalNet += net self.totalCpu += cpu self.totalElapsed += elapsed self.totalTime += time - self.totalLatency += latency def __str__(self): return (f"Starting block: {self.startBlock}\nEnding block:{self.ceaseBlock}\nChain transactions: {self.totalTransactions}\n" f"Chain cpu: {self.totalCpu}\nChain net: {(self.totalNet / (self.ceaseBlock - self.startBlock + 1))}\nChain elapsed: {self.totalElapsed}\n" - f"Chain time: {self.totalTime}\nChain latency: {self.totalLatency}") + f"Chain time: {self.totalTime}\n") def printBlockData(self): for block in self.blockLog: print(block) @@ -201,29 +197,19 @@ def selectedOpen(path): return gzip.open if path.suffix == '.gz' else open def scrapeLog(data: chainData, path): - #node_00/stderr.txt + # node_XX/stderr.txt where XX is the first nonproducing node selectedopen = selectedOpen(path) with selectedopen(path, 'rt') as f: line = f.read() blockResult = re.findall(r'Received block ([0-9a-fA-F]*).* #(\d+) .*trxs: (\d+)(.*)', line) - if data.startBlock is None: - data.startBlock = 2 - if data.ceaseBlock is None: - data.ceaseBlock = len(blockResult) + 1 for value in blockResult: - v3Logging = re.findall(r'net: (\d+), cpu: (\d+), elapsed: (\d+), time: (\d+), latency: (-?\d+) ms', value[3]) - if v3Logging: - data.blockLog.append(blockData(value[0], int(value[1]), int(value[2]), int(v3Logging[0][0]), int(v3Logging[0][1]), int(v3Logging[0][2]), int(v3Logging[0][3]), int(v3Logging[0][4]))) - if int(value[1]) in range(data.startBlock, data.ceaseBlock + 1): - data.updateTotal(int(value[2]), int(v3Logging[0][0]), int(v3Logging[0][1]), int(v3Logging[0][2]), int(v3Logging[0][3]), int(v3Logging[0][4])) - else: - v2Logging = re.findall(r'latency: (-?\d+) ms', value[3]) - if v2Logging: - data.blockLog.append(blockData(value[0], int(value[1]), int(value[2]), 0, 0, 0, 0, int(v2Logging[0]))) - if int(value[1]) in range(data.startBlock, data.ceaseBlock + 1): - data.updateTotal(int(value[2]), 0, 0, 0, 0, int(v2Logging[0])) - else: - print("Error: Unknown log format") + if int(value[1]) in range(data.startBlock, data.ceaseBlock + 1): + v3Logging = re.findall(r'elapsed: (\d+), time: (\d+)', value[3]) + if v3Logging: + index = int(value[1]) - data.startBlock + data.blockLog[index].elapsed = int(v3Logging[0][0]) + data.blockLog[index].time = int(v3Logging[0][1]) + data.updateTotal(0, 0, 0, int(v3Logging[0][0]), int(v3Logging[0][1])) droppedBlocks = re.findall(r'dropped incoming block #(\d+) id: ([0-9a-fA-F]+)', line) for block in droppedBlocks: data.droppedBlocks[block[0]] = block[1] @@ -237,21 +223,6 @@ def scrapeTrxGenLog(trxSent, path): with selectedopen(path, 'rt') as f: trxSent.update(dict([(x[0], x[1]) for x in (line.rstrip('\n').split(',') for line in f)])) -def scrapeBlockTrxDataLog(trxDict, path, nodeosVers): - #blockTrxData.txt - selectedopen = selectedOpen(path) - with selectedopen(path, 'rt') as f: - if nodeosVers == "v2": - trxDict.update(dict([(x[0], trxData(blockNum=x[1], cpuUsageUs=x[2], netUsageUs=x[3])) for x in (line.rstrip('\n').split(',') for line in f)])) - else: - trxDict.update(dict([(x[0], trxData(blockNum=x[1], blockTime=x[2], cpuUsageUs=x[3], netUsageUs=x[4])) for x in (line.rstrip('\n').split(',') for line in f)])) - -def scrapeBlockDataLog(blockDict, path): - #blockData.txt - selectedopen = selectedOpen(path) - with selectedopen(path, 'rt') as f: - blockDict.update(dict([(x[0], blkData(x[1], x[2], x[3], x[4])) for x in (line.rstrip('\n').split(',') for line in f)])) - def scrapeTrxGenTrxSentDataLogs(trxSent, trxGenLogDirPath, quiet): filesScraped = [] for fileName in trxGenLogDirPath.glob("trx_data_output_*.txt"): @@ -490,18 +461,13 @@ def default(self, obj): def reportAsJSON(report: dict) -> json: return json.dumps(report, indent=2, cls=LogReaderEncoder) -def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: ArtifactPaths, argsDict: dict, testStart: datetime=None, completedRun: bool=True, nodeosVers: str="") -> dict: +def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: ArtifactPaths, argsDict: dict, testStart: datetime=None, completedRun: bool=True, nodeosVers: str="", + blockDict: dict={}, trxDict: dict={}) -> dict: scrapeLog(data, artifacts.nodeosLogPath) trxSent = {} scrapeTrxGenTrxSentDataLogs(trxSent, artifacts.trxGenLogDirPath, tpsTestConfig.quiet) - trxDict = {} - scrapeBlockTrxDataLog(trxDict, artifacts.blockTrxDataPath, nodeosVers) - - blockDict = {} - scrapeBlockDataLog(blockDict, artifacts.blockDataPath) - notFound = [] populateTrxSentTimestamp(trxSent, trxDict, notFound) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 6cb2569b7b..435cf42e3a 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -113,12 +113,15 @@ def __post_init__(self): self.writeBlock = lambda blockDataFile, block: blockDataFile.write(f"{block['payload']['block_num']},{block['payload']['id']},{block['payload']['producer']},{block['payload']['confirmed']},{block['payload']['timestamp']}\n") self.fetchHeadBlock = lambda node, headBlock: node.processUrllibRequest("chain", "get_block", {"block_num_or_id":headBlock}, silentErrors=False, exitOnError=True) self.specificExtraNodeosArgs.update({f"{node}" : '--plugin eosio::history_api_plugin --filter-on "*"' for node in range(self.pnodes, self._totalNodes)}) + self.updateBlockDict = lambda blockNum, block, blockDict: blockDict.update(dict([(blockNum, log_reader.blkData(blockId=block["payload"]["id"], producer=block["payload"]["producer"], status=block["payload"]["confirmed"], _timestamp=block["payload"]["timestamp"]))])) + self.updateTrxDict = lambda blockNum, transaction, trxDict: trxDict.update(dict([(transaction['trx']['id'], log_reader.trxData(blockNum, transaction['cpu_usage_us'],transaction['net_usage_words']))])) else: self.fetchBlock = lambda node, blockNum: node.processUrllibRequest("trace_api", "get_block", {"block_num":blockNum}, silentErrors=False, exitOnError=True) self.writeTrx = lambda trxDataFile, block, blockNum:[ self.log_transactions(trxDataFile, block) ] self.writeBlock = lambda blockDataFile, block: blockDataFile.write(f"{block['payload']['number']},{block['payload']['id']},{block['payload']['producer']},{block['payload']['status']},{block['payload']['timestamp']}\n") self.fetchHeadBlock = lambda node, headBlock: node.processUrllibRequest("chain", "get_block_info", {"block_num":headBlock}, silentErrors=False, exitOnError=True) - + self.updateBlockDict = lambda blockNum, block, blockDict: blockDict.update(dict([(blockNum, log_reader.blkData(blockId=block["payload"]["id"], producer=block["payload"]["producer"], status=block["payload"]["status"], _timestamp=block["payload"]["timestamp"]))])) + self.updateTrxDict = lambda blockNum, transaction, trxDict: trxDict.update(dict([(transaction["id"], log_reader.trxData(blockNum=transaction["block_num"], cpuUsageUs=transaction["cpu_usage_us"], netUsageUs=transaction["net_usage_words"], blockTime=transaction["block_time"]))])) @dataclass class PtbConfig: targetTps: int=8000 @@ -239,9 +242,29 @@ def fileOpenMode(self, filePath) -> str: append_write = 'w' return append_write - def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum, endBlockNum): - for blockNum in range(startBlockNum, endBlockNum): + def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum, endBlockNum, blockDict: dict, trxDict: dict): + for blockNum in range(startBlockNum, endBlockNum + 1): + blockCpuTotal = 0 + blockNetTotal = 0 + blockTransactionTotal = 0 block = self.clusterConfig.fetchBlock(node, blockNum) + for transaction in block['payload']['transactions']: + if self.clusterConfig.nodeosVers == "v2": + self.clusterConfig.updateTrxDict(blockNum, transaction, trxDict) + blockCpuTotal += transaction["cpu_usage_us"] + blockNetTotal += transaction["net_usage_words"] + blockTransactionTotal += 1 + else: + for actions in transaction['actions']: + if actions['account'] != 'eosio' or actions['action'] != 'onblock': + self.clusterConfig.updateTrxDict(blockNum, transaction, trxDict) + blockCpuTotal += transaction["cpu_usage_us"] + blockNetTotal += transaction["net_usage_words"] + blockTransactionTotal += 1 + self.clusterConfig.updateBlockDict(blockNum, block, blockDict) + # elapsed and time are only available in logs and are therefore updated in scrapeLog + self.data.blockLog.append(log_reader.blockData(block["payload"]["id"], blockNum, blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0)) + self.data.updateTotal(blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0) btdf_append_write = self.fileOpenMode(blockTrxDataPath) with open(blockTrxDataPath, btdf_append_write) as trxDataFile: self.clusterConfig.writeTrx(trxDataFile, block, blockNum) @@ -435,7 +458,7 @@ def captureLowLevelArtifacts(self): print(f"Failed to move '{etcEosioDir}/{path}' to '{self.etcEosioLogsDirPath}/{path}': {type(e)}: {e}") - def analyzeResultsAndReport(self, testResult: PtbTpsTestResult): + def analyzeResultsAndReport(self, testResult: PtbTpsTestResult, blockDict: dict, trxDict: dict): args = self.prepArgs() artifactsLocate = log_reader.ArtifactPaths(nodeosLogPath=self.nodeosLogPath, trxGenLogDirPath=self.trxGenLogDirPath, blockTrxDataPath=self.blockTrxDataPath, blockDataPath=self.blockDataPath, transactionMetricsDataPath=self.transactionMetricsDataPath) @@ -443,7 +466,7 @@ def analyzeResultsAndReport(self, testResult: PtbTpsTestResult): numBlocksToPrune=self.ptbConfig.numAddlBlocksToPrune, numTrxGensUsed=testResult.numGeneratorsUsed, targetTpsPerGenList=testResult.targetTpsPerGenList, quiet=self.ptbConfig.quiet) self.report = log_reader.calcAndReport(data=self.data, tpsTestConfig=tpsTestConfig, artifacts=artifactsLocate, argsDict=args, testStart=self.testStart, - completedRun=testResult.completedRun,nodeosVers=self.clusterConfig.nodeosVers) + completedRun=testResult.completedRun,nodeosVers=self.clusterConfig.nodeosVers, blockDict=blockDict, trxDict=trxDict) jsonReport = None if not self.ptbConfig.quiet or not self.ptbConfig.delReport: @@ -465,8 +488,9 @@ def preTestSpinup(self): if self.launchCluster() == False: self.errorExit('Failed to stand up cluster.') - def postTpsTestSteps(self): - self.queryBlockTrxData(self.validationNode, self.blockDataPath, self.blockTrxDataPath, self.data.startBlock, self.data.ceaseBlock) + def postTpsTestSteps(self, blockDict: dict, trxDict: dict): + return self.queryBlockTrxData(self.validationNode, self.blockDataPath, self.blockTrxDataPath, self.data.startBlock, self.data.ceaseBlock, + blockDict=blockDict, trxDict=trxDict) def runTest(self) -> bool: testSuccessful = False @@ -478,9 +502,11 @@ def runTest(self) -> bool: self.ptbTestResult = self.runTpsTest() - self.postTpsTestSteps() + blockDict = {} + trxDict = {} + self.postTpsTestSteps(blockDict, trxDict) - self.analyzeResultsAndReport(self.ptbTestResult) + self.analyzeResultsAndReport(self.ptbTestResult, blockDict, trxDict) testSuccessful = self.ptbTestResult.completedRun From 1bd16e8a6d93727fe7314d94add66fdb7c08d0c8 Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Wed, 22 Mar 2023 12:21:30 -0500 Subject: [PATCH 02/13] some additional improvements to reduce performance test code and wasted effort --- .../performance_test_basic.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 303a63eadc..72506631ef 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -92,12 +92,6 @@ class SpecifiedContract: _totalNodes: int = 2 nonProdsEosVmOcEnable: bool = False - def log_transactions(self, trxDataFile, block): - for trx in block['payload']['transactions']: - for actions in trx['actions']: - if actions['account'] != 'eosio' or actions['action'] != 'onblock': - trxDataFile.write(f"{trx['id']},{trx['block_num']},{trx['block_time']},{trx['cpu_usage_us']},{trx['net_usage_words']},{trx['actions']}\n") - def __post_init__(self): self._totalNodes = self.pnodes + 1 if self.totalNodes <= self.pnodes else self.totalNodes nonProdsSpecificNodeosStr = "" @@ -108,13 +102,13 @@ def __post_init__(self): self.specificExtraNodeosArgs.update({f"{node}" : nonProdsSpecificNodeosStr for node in range(self.pnodes, self._totalNodes)}) assert self.nodeosVers != "v1" and self.nodeosVers != "v0", f"nodeos version {Utils.getNodeosVersion().split('.')[0]} is unsupported by performance test" if self.nodeosVers == "v2": - self.writeTrx = lambda trxDataFile, block, blockNum: [trxDataFile.write(f"{trx['trx']['id']},{blockNum},{trx['cpu_usage_us']},{trx['net_usage_words']}\n") for trx in block['payload']['transactions'] if block['payload']['transactions']] + self.writeTrx = lambda trxDataFile, blockNum, trx: [trxDataFile.write(f"{trx['trx']['id']},{blockNum},{trx['cpu_usage_us']},{trx['net_usage_words']}\n")] self.writeBlock = lambda blockDataFile, block: blockDataFile.write(f"{block['payload']['block_num']},{block['payload']['id']},{block['payload']['producer']},{block['payload']['confirmed']},{block['payload']['timestamp']}\n") self.specificExtraNodeosArgs.update({f"{node}" : '--plugin eosio::history_api_plugin --filter-on "*"' for node in range(self.pnodes, self._totalNodes)}) self.updateBlockDict = lambda blockNum, block, blockDict: blockDict.update(dict([(blockNum, log_reader.blkData(blockId=block["payload"]["id"], producer=block["payload"]["producer"], status=block["payload"]["confirmed"], _timestamp=block["payload"]["timestamp"]))])) self.updateTrxDict = lambda blockNum, transaction, trxDict: trxDict.update(dict([(transaction['trx']['id'], log_reader.trxData(blockNum, transaction['cpu_usage_us'],transaction['net_usage_words']))])) else: - self.writeTrx = lambda trxDataFile, block, blockNum:[ self.log_transactions(trxDataFile, block) ] + self.writeTrx = lambda trxDataFile, blockNum, trx:[ trxDataFile.write(f"{trx['id']},{trx['block_num']},{trx['block_time']},{trx['cpu_usage_us']},{trx['net_usage_words']},{trx['actions']}\n") ] self.writeBlock = lambda blockDataFile, block: blockDataFile.write(f"{block['payload']['number']},{block['payload']['id']},{block['payload']['producer']},{block['payload']['status']},{block['payload']['timestamp']}\n") self.updateBlockDict = lambda blockNum, block, blockDict: blockDict.update(dict([(blockNum, log_reader.blkData(blockId=block["payload"]["id"], producer=block["payload"]["producer"], status=block["payload"]["status"], _timestamp=block["payload"]["timestamp"]))])) self.updateTrxDict = lambda blockNum, transaction, trxDict: trxDict.update(dict([(transaction["id"], log_reader.trxData(blockNum=transaction["block_num"], cpuUsageUs=transaction["cpu_usage_us"], netUsageUs=transaction["net_usage_words"], blockTime=transaction["block_time"]))])) @@ -239,36 +233,34 @@ def fileOpenMode(self, filePath) -> str: append_write = 'w' return append_write + def isImportantTransaction(self, transaction): + if self.clusterConfig.nodeosVers == "v2": + return True + else: + if transaction['actions'][0]['account'] != 'eosio' or transaction['actions'][0]['action'] != 'onblock': + return True + return False + def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum, endBlockNum, blockDict: dict, trxDict: dict): for blockNum in range(startBlockNum, endBlockNum + 1): - blockCpuTotal = 0 - blockNetTotal = 0 - blockTransactionTotal = 0 + blockCpuTotal, blockNetTotal, blockTransactionTotal = 0, 0, 0 block = node.fetchBlock(blockNum) - for transaction in block['payload']['transactions']: - if self.clusterConfig.nodeosVers == "v2": - self.clusterConfig.updateTrxDict(blockNum, transaction, trxDict) - blockCpuTotal += transaction["cpu_usage_us"] - blockNetTotal += transaction["net_usage_words"] - blockTransactionTotal += 1 - else: - for actions in transaction['actions']: - if actions['account'] != 'eosio' or actions['action'] != 'onblock': - self.clusterConfig.updateTrxDict(blockNum, transaction, trxDict) - blockCpuTotal += transaction["cpu_usage_us"] - blockNetTotal += transaction["net_usage_words"] - blockTransactionTotal += 1 - self.clusterConfig.updateBlockDict(blockNum, block, blockDict) - # elapsed and time are only available in logs and are therefore updated in scrapeLog - self.data.blockLog.append(log_reader.blockData(block["payload"]["id"], blockNum, blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0)) - self.data.updateTotal(blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0) btdf_append_write = self.fileOpenMode(blockTrxDataPath) with open(blockTrxDataPath, btdf_append_write) as trxDataFile: - self.clusterConfig.writeTrx(trxDataFile, block, blockNum) - + for transaction in block['payload']['transactions']: + if self.isImportantTransaction(transaction): + self.clusterConfig.updateTrxDict(blockNum, transaction, trxDict) + self.clusterConfig.writeTrx(trxDataFile, blockNum, transaction) + blockCpuTotal += transaction["cpu_usage_us"] + blockNetTotal += transaction["net_usage_words"] + blockTransactionTotal += 1 + self.clusterConfig.updateBlockDict(blockNum, block, blockDict) bdf_append_write = self.fileOpenMode(blockDataPath) with open(blockDataPath, bdf_append_write) as blockDataFile: self.clusterConfig.writeBlock(blockDataFile, block) + # elapsed and time are only available in logs and are therefore updated in scrapeLog + self.data.blockLog.append(log_reader.blockData(block["payload"]["id"], blockNum, blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0)) + self.data.updateTotal(blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0) def waitForEmptyBlocks(self, node, numEmptyToWaitOn): emptyBlocks = 0 From c6908a45a693232b0411a5d24a37a35274a35504 Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Tue, 28 Mar 2023 17:18:51 -0500 Subject: [PATCH 03/13] Remove blkData by merging it with blockData and changing some use cases --- tests/performance_tests/log_reader.py | 69 +++++++++---------- .../performance_test_basic.py | 14 ++-- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index 544695d3df..c7e8bf7793 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -88,38 +88,6 @@ def sentTimestamp(self): self._sentTimestamp = "" self._calcdTimeEpoch = 0 -@dataclass -class blkData(): - blockId: int = 0 - producer: str = "" - status: str = "" - _timestamp: str = field(init=True, repr=True, default='') - _calcdTimeEpoch: float = 0 - - def __post_init__(self): - self.timestamp = self._timestamp - - @property - def timestamp(self): - return self._timestamp - - @property - def calcdTimeEpoch(self): - return self._calcdTimeEpoch - - @timestamp.setter - def timestamp(self, time: str): - self._timestamp = time[:-1] - # When we no longer support Python 3.6, would be great to update to use this - # self._calcdTimeEpoch = datetime.fromisoformat(time[:-1]).timestamp() - #Note block timestamp formatted like: '2022-09-30T16:48:13.500Z', but 'Z' is not part of python's recognized iso format, so strip it off the end - self._calcdTimeEpoch = datetime.strptime(time[:-1], "%Y-%m-%dT%H:%M:%S.%f").timestamp() - - @timestamp.deleter - def timestamp(self): - self._timestamp = "" - self._calcdTimeEpoch = 0 - @dataclass class productionWindow(): producer: str = "" @@ -149,13 +117,41 @@ class chainBlocksGuide(): @dataclass class blockData(): - partialBlockId: str = "" + blockId: int = 0 blockNum: int = 0 transactions: int = 0 net: int = 0 cpu: int = 0 elapsed: int = 0 time: int = 0 + producer: str = "" + status: str = "" + _timestamp: str = field(init=True, repr=True, default='') + _calcdTimeEpoch: float = 0 + + def __post_init__(self): + self.timestamp = self._timestamp + + @property + def timestamp(self): + return self._timestamp + + @property + def calcdTimeEpoch(self): + return self._calcdTimeEpoch + + @timestamp.setter + def timestamp(self, time: str): + self._timestamp = time[:-1] + # When we no longer support Python 3.6, would be great to update to use this + # self._calcdTimeEpoch = datetime.fromisoformat(time[:-1]).timestamp() + #Note block timestamp formatted like: '2022-09-30T16:48:13.500Z', but 'Z' is not part of python's recognized iso format, so strip it off the end + self._calcdTimeEpoch = datetime.strptime(time[:-1], "%Y-%m-%dT%H:%M:%S.%f").timestamp() + + @timestamp.deleter + def timestamp(self): + self._timestamp = "" + self._calcdTimeEpoch = 0 class chainData(): def __init__(self): @@ -240,7 +236,7 @@ def populateTrxSentTimestamp(trxSent: dict, trxDict: dict, notFound): def populateTrxLatencies(blockDict: dict, trxDict: dict): for trxId, data in trxDict.items(): if data.calcdTimeEpoch != 0: - trxDict[trxId].latency = blockDict[data.blockNum].calcdTimeEpoch - data.calcdTimeEpoch + trxDict[trxId].latency = blockDict[str(data.blockNum)].calcdTimeEpoch - data.calcdTimeEpoch def writeTransactionMetrics(trxDict: dict, path): with open(path, 'wt') as transactionMetricsFile: @@ -388,12 +384,11 @@ def calcBlockSizeStats(data: chainData, guide : chainBlocksGuide) -> stats: # Note: numpy array slicing in use -> [:,0] -> from all elements return index 0 return stats(int(np.min(npBlkSizeList[:,0])), int(np.max(npBlkSizeList[:,0])), float(np.average(npBlkSizeList[:,0])), float(np.std(npBlkSizeList[:,0])), int(np.sum(npBlkSizeList[:,1])), len(prunedBlockDataLog)) -def calcTrxLatencyCpuNetStats(trxDict : dict, blockDict: dict): +def calcTrxLatencyCpuNetStats(trxDict : dict): """Analyzes a test scenario's steady state block data for transaction latency statistics during the test window Keyword arguments: trxDict -- the dictionary mapping trx id to trxData, wherein the trx sent timestamp has been populated from the trx generator at moment of send - blockDict -- the dictionary of block number to blockData, wherein the block production timestamp is recorded Returns: transaction latency stats as a basicStats object @@ -480,7 +475,7 @@ def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: Arti populateTrxLatencies(blockDict, trxDict) writeTransactionMetrics(trxDict, artifacts.transactionMetricsDataPath) guide = calcChainGuide(data, tpsTestConfig.numBlocksToPrune) - trxLatencyStats, trxCpuStats, trxNetStats = calcTrxLatencyCpuNetStats(trxDict, blockDict) + trxLatencyStats, trxCpuStats, trxNetStats = calcTrxLatencyCpuNetStats(trxDict) tpsStats = scoreTransfersPerSecond(data, guide) blkSizeStats = calcBlockSizeStats(data, guide) prodWindows = calcProductionWindows(prodDict) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 72506631ef..3c4c78420b 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -103,14 +103,12 @@ def __post_init__(self): assert self.nodeosVers != "v1" and self.nodeosVers != "v0", f"nodeos version {Utils.getNodeosVersion().split('.')[0]} is unsupported by performance test" if self.nodeosVers == "v2": self.writeTrx = lambda trxDataFile, blockNum, trx: [trxDataFile.write(f"{trx['trx']['id']},{blockNum},{trx['cpu_usage_us']},{trx['net_usage_words']}\n")] - self.writeBlock = lambda blockDataFile, block: blockDataFile.write(f"{block['payload']['block_num']},{block['payload']['id']},{block['payload']['producer']},{block['payload']['confirmed']},{block['payload']['timestamp']}\n") self.specificExtraNodeosArgs.update({f"{node}" : '--plugin eosio::history_api_plugin --filter-on "*"' for node in range(self.pnodes, self._totalNodes)}) - self.updateBlockDict = lambda blockNum, block, blockDict: blockDict.update(dict([(blockNum, log_reader.blkData(blockId=block["payload"]["id"], producer=block["payload"]["producer"], status=block["payload"]["confirmed"], _timestamp=block["payload"]["timestamp"]))])) + self.createBlockData = lambda block, blockTransactionTotal, blockNetTotal, blockCpuTotal: log_reader.blockData(blockId=block["payload"]["id"], blockNum=block['payload']['block_num'], transactions=blockTransactionTotal, net=blockNetTotal, cpu=blockCpuTotal, producer=block["payload"]["producer"], status=block["payload"]["confirmed"], _timestamp=block["payload"]["timestamp"]) self.updateTrxDict = lambda blockNum, transaction, trxDict: trxDict.update(dict([(transaction['trx']['id'], log_reader.trxData(blockNum, transaction['cpu_usage_us'],transaction['net_usage_words']))])) else: self.writeTrx = lambda trxDataFile, blockNum, trx:[ trxDataFile.write(f"{trx['id']},{trx['block_num']},{trx['block_time']},{trx['cpu_usage_us']},{trx['net_usage_words']},{trx['actions']}\n") ] - self.writeBlock = lambda blockDataFile, block: blockDataFile.write(f"{block['payload']['number']},{block['payload']['id']},{block['payload']['producer']},{block['payload']['status']},{block['payload']['timestamp']}\n") - self.updateBlockDict = lambda blockNum, block, blockDict: blockDict.update(dict([(blockNum, log_reader.blkData(blockId=block["payload"]["id"], producer=block["payload"]["producer"], status=block["payload"]["status"], _timestamp=block["payload"]["timestamp"]))])) + self.createBlockData = lambda block, blockTransactionTotal, blockNetTotal, blockCpuTotal: log_reader.blockData(blockId=block["payload"]["id"], blockNum=block['payload']['number'], transactions=blockTransactionTotal, net=blockNetTotal, cpu=blockCpuTotal, producer=block["payload"]["producer"], status=block["payload"]["status"], _timestamp=block["payload"]["timestamp"]) self.updateTrxDict = lambda blockNum, transaction, trxDict: trxDict.update(dict([(transaction["id"], log_reader.trxData(blockNum=transaction["block_num"], cpuUsageUs=transaction["cpu_usage_us"], netUsageUs=transaction["net_usage_words"], blockTime=transaction["block_time"]))])) @dataclass class PtbConfig: @@ -254,12 +252,14 @@ def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum blockCpuTotal += transaction["cpu_usage_us"] blockNetTotal += transaction["net_usage_words"] blockTransactionTotal += 1 - self.clusterConfig.updateBlockDict(blockNum, block, blockDict) + blockData = self.clusterConfig.createBlockData(block=block, blockTransactionTotal=blockTransactionTotal, + blockNetTotal=blockNetTotal, blockCpuTotal=blockCpuTotal) + self.data.blockLog.append(blockData) + blockDict[str(blockNum)] = blockData bdf_append_write = self.fileOpenMode(blockDataPath) with open(blockDataPath, bdf_append_write) as blockDataFile: - self.clusterConfig.writeBlock(blockDataFile, block) + blockDataFile.write(f"{blockData.blockNum},{blockData.blockId},{blockData.producer},{blockData.status},{blockData._timestamp}\n") # elapsed and time are only available in logs and are therefore updated in scrapeLog - self.data.blockLog.append(log_reader.blockData(block["payload"]["id"], blockNum, blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0)) self.data.updateTotal(blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0) def waitForEmptyBlocks(self, node, numEmptyToWaitOn): From 82e8ba8b6a3a0d49c2072b624edc74c12d7b2bf5 Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Wed, 29 Mar 2023 17:17:07 -0500 Subject: [PATCH 04/13] change folder for performance_test runs to performance_test from p --- tests/performance_tests/performance_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/performance_tests/performance_test.py b/tests/performance_tests/performance_test.py index 83d97e1191..af0abee485 100755 --- a/tests/performance_tests/performance_test.py +++ b/tests/performance_tests/performance_test.py @@ -80,7 +80,7 @@ class PerfTestSearchResults: @dataclass class LoggingConfig: - logDirBase: Path = Path(".")/PurePath(PurePath(__file__).name).stem[0] + logDirBase: Path = Path(".")/PurePath(PurePath(__file__).name).stem logDirTimestamp: str = f"{datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S')}" logDirPath: Path = field(default_factory=Path, init=False) ptbLogsDirPath: Path = field(default_factory=Path, init=False) @@ -99,7 +99,7 @@ def __init__(self, testHelperConfig: PerformanceTestBasic.TestHelperConfig=Perfo self.testsStart = datetime.utcnow() - self.loggingConfig = PerformanceTest.LoggingConfig(logDirBase=Path(self.ptConfig.logDirRoot)/PurePath(PurePath(__file__).name).stem[0], + self.loggingConfig = PerformanceTest.LoggingConfig(logDirBase=Path(self.ptConfig.logDirRoot)/PurePath(PurePath(__file__).name).stem, logDirTimestamp=f"{self.testsStart.strftime('%Y-%m-%d_%H-%M-%S')}") def performPtbBinarySearch(self, clusterConfig: PerformanceTestBasic.ClusterConfig, logDirRoot: Path, delReport: bool, quiet: bool, delPerfLogs: bool) -> TpsTestResult.PerfTestSearchResults: From 1fd2e6789807a0229911f78eddbb9f5b92e74ab8 Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Fri, 31 Mar 2023 15:55:57 -0500 Subject: [PATCH 05/13] check forked and dropped blocks for each node in a performance test run. Change where updateTotal happens to not double count if blocks fork. --- tests/performance_tests/log_reader.py | 64 +++++++++++++------ .../performance_test_basic.py | 8 +-- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index c7e8bf7793..e34f98a71d 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -25,6 +25,7 @@ @dataclass class ArtifactPaths: + nodeosLogDir: Path = Path("") nodeosLogPath: Path = Path("") trxGenLogDirPath: Path = Path("") blockTrxDataPath: Path = Path("") @@ -164,7 +165,8 @@ def __init__(self): self.totalElapsed = 0 self.totalTime = 0 self.droppedBlocks = {} - self.forkedBlocks = [] + self.forkedBlocks = {} + self.nodes = 0 def __eq__(self, other): return self.startBlock == other.startBlock and\ self.ceaseBlock == other.ceaseBlock and\ @@ -172,7 +174,8 @@ def __eq__(self, other): self.totalNet == other.totalNet and\ self.totalCpu == other.totalCpu and\ self.totalElapsed == other.totalElapsed and\ - self.totalTime == other.totalTime + self.totalTime == other.totalTime and\ + self.nodes == other.nodes def updateTotal(self, transactions, net, cpu, elapsed, time): self.totalTransactions += transactions self.totalNet += net @@ -190,7 +193,7 @@ def assertEquality(self, other): def selectedOpen(path): return gzip.open if path.suffix == '.gz' else open -def scrapeLog(data: chainData, path): +def scrapeLogBlockElapsedTime(blockDict: dict, data: chainData, path): # node_XX/stderr.txt where XX is the first nonproducing node selectedopen = selectedOpen(path) with selectedopen(path, 'rt') as f: @@ -200,16 +203,25 @@ def scrapeLog(data: chainData, path): if int(value[1]) in range(data.startBlock, data.ceaseBlock + 1): v3Logging = re.findall(r'elapsed: (\d+), time: (\d+)', value[3]) if v3Logging: - index = int(value[1]) - data.startBlock - data.blockLog[index].elapsed = int(v3Logging[0][0]) - data.blockLog[index].time = int(v3Logging[0][1]) - data.updateTotal(0, 0, 0, int(v3Logging[0][0]), int(v3Logging[0][1])) - droppedBlocks = re.findall(r'dropped incoming block #(\d+) id: ([0-9a-fA-F]+)', line) - for block in droppedBlocks: - data.droppedBlocks[block[0]] = block[1] - forks = re.findall(r'switching forks from ([0-9a-fA-F]+) \(block number (\d+)\) to ([0-9a-fA-F]+) \(block number (\d+)\)', line) - for fork in forks: - data.forkedBlocks.append(int(fork[1]) - int(fork[3]) + 1) + blockDict[str(value[1])].elapsed = int(v3Logging[0][0]) + blockDict[str(value[1])].time = int(v3Logging[0][1]) + +def scrapeLogDroppedForkedBlocks(data: chainData, path): + for nodeNum in range(0, data.nodes): + nodePath = path/f"node_{str(nodeNum).zfill(2)}"/"stderr.txt" + selectedopen = selectedOpen(path) + with selectedopen(nodePath, 'rt') as f: + line = f.read() + droppedBlocksByCurrentNode = {} + forkedBlocksByCurrentNode = [] + droppedBlocks = re.findall(r'dropped incoming block #(\d+) id: ([0-9a-fA-F]+)', line) + for block in droppedBlocks: + droppedBlocksByCurrentNode[block[0]] = block[1] + forks = re.findall(r'switching forks from ([0-9a-fA-F]+) \(block number (\d+)\) to ([0-9a-fA-F]+) \(block number (\d+)\)', line) + for fork in forks: + forkedBlocksByCurrentNode.append(int(fork[1]) - int(fork[3]) + 1) + data.droppedBlocks[str(nodeNum).zfill(2)] = droppedBlocksByCurrentNode + data.forkedBlocks[str(nodeNum).zfill(2)] = forkedBlocksByCurrentNode def scrapeTrxGenLog(trxSent, path): #trxGenLogs/trx_data_output_*.txt @@ -238,6 +250,10 @@ def populateTrxLatencies(blockDict: dict, trxDict: dict): if data.calcdTimeEpoch != 0: trxDict[trxId].latency = blockDict[str(data.blockNum)].calcdTimeEpoch - data.calcdTimeEpoch +def updateBlockTotals(blockDict: dict, data: chainData): + for _, block in blockDict.items(): + data.updateTotal(transactions=block.transactions, net=block.net, cpu=block.cpu, elapsed=block.elapsed, time=block.time) + def writeTransactionMetrics(trxDict: dict, path): with open(path, 'wt') as transactionMetricsFile: transactionMetricsFile.write("TransactionId,BlockNumber,BlockTime,CpuUsageUs,NetUsageUs,Latency,SentTimestamp,CalcdTimeEpoch\n") @@ -403,7 +419,7 @@ def calcTrxLatencyCpuNetStats(trxDict : dict): def createReport(guide: chainBlocksGuide, tpsTestConfig: TpsTestConfig, tpsStats: stats, blockSizeStats: stats, trxLatencyStats: basicStats, trxCpuStats: basicStats, trxNetStats: basicStats, forkedBlocks, droppedBlocks, prodWindows: productionWindows, notFound: dict, testStart: datetime, testFinish: datetime, - argsDict: dict, completedRun: bool, nodeosVers: str) -> dict: + argsDict: dict, completedRun: bool, nodeosVers: str, numNodes: int) -> dict: report = {} report['completedRun'] = completedRun report['testStart'] = testStart @@ -419,14 +435,20 @@ def createReport(guide: chainBlocksGuide, tpsTestConfig: TpsTestConfig, tpsStats report['Analysis']['TrxCPU'] = asdict(trxCpuStats) report['Analysis']['TrxLatency'] = asdict(trxLatencyStats) report['Analysis']['TrxNet'] = asdict(trxNetStats) - report['Analysis']['DroppedBlocks'] = droppedBlocks - report['Analysis']['DroppedBlocksCount'] = len(droppedBlocks) report['Analysis']['DroppedTransactions'] = len(notFound) report['Analysis']['ProductionWindowsTotal'] = prodWindows.totalWindows report['Analysis']['ProductionWindowsAverageSize'] = prodWindows.averageWindowSize report['Analysis']['ProductionWindowsMissed'] = prodWindows.missedWindows - report['Analysis']['ForkedBlocks'] = forkedBlocks - report['Analysis']['ForksCount'] = len(forkedBlocks) + report['Analysis']['ForkedBlocks'] = {} + report['Analysis']['ForksCount'] = {} + report['Analysis']['DroppedBlocks'] = {} + report['Analysis']['DroppedBlocksCount'] = {} + for nodeNum in range(0, numNodes): + formattedNodeNum = str(nodeNum).zfill(2) + report['Analysis']['ForkedBlocks'][formattedNodeNum] = forkedBlocks[formattedNodeNum] + report['Analysis']['ForksCount'][formattedNodeNum] = len(forkedBlocks[formattedNodeNum]) + report['Analysis']['DroppedBlocks'][formattedNodeNum] = droppedBlocks[formattedNodeNum] + report['Analysis']['DroppedBlocksCount'][formattedNodeNum] = len(droppedBlocks[formattedNodeNum]) report['args'] = argsDict report['env'] = {'system': system(), 'os': os.name, 'release': release(), 'logical_cpu_count': os.cpu_count()} report['nodeosVersion'] = nodeosVers @@ -456,7 +478,8 @@ def reportAsJSON(report: dict) -> json: def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: ArtifactPaths, argsDict: dict, testStart: datetime=None, completedRun: bool=True, nodeosVers: str="", blockDict: dict={}, trxDict: dict={}) -> dict: - scrapeLog(data, artifacts.nodeosLogPath) + scrapeLogBlockElapsedTime(blockDict, data, artifacts.nodeosLogPath) + scrapeLogDroppedForkedBlocks(data, artifacts.nodeosLogDir) trxSent = {} scrapeTrxGenTrxSentDataLogs(trxSent, artifacts.trxGenLogDirPath, tpsTestConfig.quiet) @@ -472,6 +495,7 @@ def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: Arti if argsDict.get("printMissingTransactions"): print(notFound) + updateBlockTotals(blockDict, data) populateTrxLatencies(blockDict, trxDict) writeTransactionMetrics(trxDict, artifacts.transactionMetricsDataPath) guide = calcChainGuide(data, tpsTestConfig.numBlocksToPrune) @@ -492,7 +516,7 @@ def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: Arti report = createReport(guide=guide, tpsTestConfig=tpsTestConfig, tpsStats=tpsStats, blockSizeStats=blkSizeStats, trxLatencyStats=trxLatencyStats, trxCpuStats=trxCpuStats, trxNetStats=trxNetStats, forkedBlocks=data.forkedBlocks, droppedBlocks=data.droppedBlocks, prodWindows=prodWindows, notFound=notFound, testStart=start, testFinish=finish, argsDict=argsDict, completedRun=completedRun, - nodeosVers=nodeosVers) + nodeosVers=nodeosVers, numNodes=data.nodes) return report def exportReportAsJSON(report: json, exportPath): diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 3c4c78420b..0eb358ab05 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -170,7 +170,8 @@ def __init__(self, testHelperConfig: TestHelperConfig=TestHelperConfig(), cluste self.producerNodeId = 0 self.validationNodeId = self.clusterConfig.pnodes pid = os.getpid() - self.nodeosLogPath = Path(self.loggingConfig.logDirPath)/"var"/f"{self.testNamePath}{pid}"/f"node_{str(self.validationNodeId).zfill(2)}"/"stderr.txt" + self.nodeosLogDir = Path(self.loggingConfig.logDirPath)/"var"/f"{self.testNamePath}{pid}" + self.nodeosLogPath = self.nodeosLogDir/f"node_{str(self.validationNodeId).zfill(2)}"/"stderr.txt" # Setup cluster and its wallet manager self.walletMgr=WalletMgr(True) @@ -259,8 +260,6 @@ def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum bdf_append_write = self.fileOpenMode(blockDataPath) with open(blockDataPath, bdf_append_write) as blockDataFile: blockDataFile.write(f"{blockData.blockNum},{blockData.blockId},{blockData.producer},{blockData.status},{blockData._timestamp}\n") - # elapsed and time are only available in logs and are therefore updated in scrapeLog - self.data.updateTotal(blockTransactionTotal, blockNetTotal, blockCpuTotal, elapsed=0, time=0) def waitForEmptyBlocks(self, node, numEmptyToWaitOn): emptyBlocks = 0 @@ -352,6 +351,7 @@ def runTpsTest(self) -> PtbTpsTestResult: chainId = info['chain_id'] lib_id = info['last_irreversible_block_id'] self.data = log_reader.chainData() + self.data.nodes = self.clusterConfig._totalNodes abiFile=None actionsDataJson=None @@ -451,7 +451,7 @@ def captureLowLevelArtifacts(self): def analyzeResultsAndReport(self, testResult: PtbTpsTestResult, blockDict: dict, trxDict: dict): args = self.prepArgs() - artifactsLocate = log_reader.ArtifactPaths(nodeosLogPath=self.nodeosLogPath, trxGenLogDirPath=self.trxGenLogDirPath, blockTrxDataPath=self.blockTrxDataPath, + artifactsLocate = log_reader.ArtifactPaths(nodeosLogDir=self.nodeosLogDir, nodeosLogPath=self.nodeosLogPath, trxGenLogDirPath=self.trxGenLogDirPath, blockTrxDataPath=self.blockTrxDataPath, blockDataPath=self.blockDataPath, transactionMetricsDataPath=self.transactionMetricsDataPath) tpsTestConfig = log_reader.TpsTestConfig(targetTps=self.ptbConfig.targetTps, testDurationSec=self.ptbConfig.testTrxGenDurationSec, tpsLimitPerGenerator=self.ptbConfig.tpsLimitPerGenerator, numBlocksToPrune=self.ptbConfig.numAddlBlocksToPrune, numTrxGensUsed=testResult.numGeneratorsUsed, From 72c36e0876e0b6a107d29408285fb558389e0ebe Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Fri, 31 Mar 2023 16:25:05 -0500 Subject: [PATCH 06/13] put blockDict and trxDict into chainData class --- tests/performance_tests/log_reader.py | 41 ++++++++++--------- .../performance_test_basic.py | 21 ++++------ 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index e34f98a71d..be0e971839 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -157,6 +157,8 @@ def timestamp(self): class chainData(): def __init__(self): self.blockLog = [] + self.blockDict = {} + self.trxDict = {} self.startBlock = None self.ceaseBlock = None self.totalTransactions = 0 @@ -193,7 +195,7 @@ def assertEquality(self, other): def selectedOpen(path): return gzip.open if path.suffix == '.gz' else open -def scrapeLogBlockElapsedTime(blockDict: dict, data: chainData, path): +def scrapeLogBlockElapsedTime(data: chainData, path): # node_XX/stderr.txt where XX is the first nonproducing node selectedopen = selectedOpen(path) with selectedopen(path, 'rt') as f: @@ -203,8 +205,8 @@ def scrapeLogBlockElapsedTime(blockDict: dict, data: chainData, path): if int(value[1]) in range(data.startBlock, data.ceaseBlock + 1): v3Logging = re.findall(r'elapsed: (\d+), time: (\d+)', value[3]) if v3Logging: - blockDict[str(value[1])].elapsed = int(v3Logging[0][0]) - blockDict[str(value[1])].time = int(v3Logging[0][1]) + data.blockDict[str(value[1])].elapsed = int(v3Logging[0][0]) + data.blockDict[str(value[1])].time = int(v3Logging[0][1]) def scrapeLogDroppedForkedBlocks(data: chainData, path): for nodeNum in range(0, data.nodes): @@ -245,13 +247,13 @@ def populateTrxSentTimestamp(trxSent: dict, trxDict: dict, notFound): else: notFound.append(sentTrxId) -def populateTrxLatencies(blockDict: dict, trxDict: dict): - for trxId, data in trxDict.items(): - if data.calcdTimeEpoch != 0: - trxDict[trxId].latency = blockDict[str(data.blockNum)].calcdTimeEpoch - data.calcdTimeEpoch +def populateTrxLatencies(data: chainData): + for trxId, trxData in data.trxDict.items(): + if trxData.calcdTimeEpoch != 0: + data.trxDict[trxId].latency = data.blockDict[str(trxData.blockNum)].calcdTimeEpoch - trxData.calcdTimeEpoch -def updateBlockTotals(blockDict: dict, data: chainData): - for _, block in blockDict.items(): +def updateBlockTotals(data: chainData): + for _, block in data.blockDict.items(): data.updateTotal(transactions=block.transactions, net=block.net, cpu=block.cpu, elapsed=block.elapsed, time=block.time) def writeTransactionMetrics(trxDict: dict, path): @@ -260,12 +262,12 @@ def writeTransactionMetrics(trxDict: dict, path): for trxId, data in trxDict.items(): transactionMetricsFile.write(f"{trxId},{data.blockNum},{data.blockTime},{data.cpuUsageUs},{data.netUsageUs},{data.latency},{data._sentTimestamp},{data._calcdTimeEpoch}\n") -def getProductionWindows(prodDict: dict, blockDict: dict, data: chainData): +def getProductionWindows(prodDict: dict, data: chainData): prod = "" count = 0 blocksFromCurProd = 0 numProdWindows = 0 - for k, v in blockDict.items(): + for k, v in data.blockDict.items(): count += 1 if prod == "": prod = v.producer @@ -476,30 +478,29 @@ def default(self, obj): def reportAsJSON(report: dict) -> json: return json.dumps(report, indent=2, cls=LogReaderEncoder) -def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: ArtifactPaths, argsDict: dict, testStart: datetime=None, completedRun: bool=True, nodeosVers: str="", - blockDict: dict={}, trxDict: dict={}) -> dict: - scrapeLogBlockElapsedTime(blockDict, data, artifacts.nodeosLogPath) +def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: ArtifactPaths, argsDict: dict, testStart: datetime=None, completedRun: bool=True, nodeosVers: str="") -> dict: + scrapeLogBlockElapsedTime(data, artifacts.nodeosLogPath) scrapeLogDroppedForkedBlocks(data, artifacts.nodeosLogDir) trxSent = {} scrapeTrxGenTrxSentDataLogs(trxSent, artifacts.trxGenLogDirPath, tpsTestConfig.quiet) notFound = [] - populateTrxSentTimestamp(trxSent, trxDict, notFound) + populateTrxSentTimestamp(trxSent, data.trxDict, notFound) prodDict = {} - getProductionWindows(prodDict, blockDict, data) + getProductionWindows(prodDict, data) if len(notFound) > 0: print(f"Transactions logged as sent but NOT FOUND in block!! lost {len(notFound)} out of {len(trxSent)}") if argsDict.get("printMissingTransactions"): print(notFound) - updateBlockTotals(blockDict, data) - populateTrxLatencies(blockDict, trxDict) - writeTransactionMetrics(trxDict, artifacts.transactionMetricsDataPath) + updateBlockTotals(data) + populateTrxLatencies(data) + writeTransactionMetrics(data.trxDict, artifacts.transactionMetricsDataPath) guide = calcChainGuide(data, tpsTestConfig.numBlocksToPrune) - trxLatencyStats, trxCpuStats, trxNetStats = calcTrxLatencyCpuNetStats(trxDict) + trxLatencyStats, trxCpuStats, trxNetStats = calcTrxLatencyCpuNetStats(data.trxDict) tpsStats = scoreTransfersPerSecond(data, guide) blkSizeStats = calcBlockSizeStats(data, guide) prodWindows = calcProductionWindows(prodDict) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 0eb358ab05..e3dc52b14a 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -240,7 +240,7 @@ def isImportantTransaction(self, transaction): return True return False - def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum, endBlockNum, blockDict: dict, trxDict: dict): + def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum, endBlockNum): for blockNum in range(startBlockNum, endBlockNum + 1): blockCpuTotal, blockNetTotal, blockTransactionTotal = 0, 0, 0 block = node.fetchBlock(blockNum) @@ -248,7 +248,7 @@ def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum with open(blockTrxDataPath, btdf_append_write) as trxDataFile: for transaction in block['payload']['transactions']: if self.isImportantTransaction(transaction): - self.clusterConfig.updateTrxDict(blockNum, transaction, trxDict) + self.clusterConfig.updateTrxDict(blockNum, transaction, self.data.trxDict) self.clusterConfig.writeTrx(trxDataFile, blockNum, transaction) blockCpuTotal += transaction["cpu_usage_us"] blockNetTotal += transaction["net_usage_words"] @@ -256,7 +256,7 @@ def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum blockData = self.clusterConfig.createBlockData(block=block, blockTransactionTotal=blockTransactionTotal, blockNetTotal=blockNetTotal, blockCpuTotal=blockCpuTotal) self.data.blockLog.append(blockData) - blockDict[str(blockNum)] = blockData + self.data.blockDict[str(blockNum)] = blockData bdf_append_write = self.fileOpenMode(blockDataPath) with open(blockDataPath, bdf_append_write) as blockDataFile: blockDataFile.write(f"{blockData.blockNum},{blockData.blockId},{blockData.producer},{blockData.status},{blockData._timestamp}\n") @@ -449,7 +449,7 @@ def captureLowLevelArtifacts(self): print(f"Failed to move '{etcEosioDir}/{path}' to '{self.etcEosioLogsDirPath}/{path}': {type(e)}: {e}") - def analyzeResultsAndReport(self, testResult: PtbTpsTestResult, blockDict: dict, trxDict: dict): + def analyzeResultsAndReport(self, testResult: PtbTpsTestResult): args = self.prepArgs() artifactsLocate = log_reader.ArtifactPaths(nodeosLogDir=self.nodeosLogDir, nodeosLogPath=self.nodeosLogPath, trxGenLogDirPath=self.trxGenLogDirPath, blockTrxDataPath=self.blockTrxDataPath, blockDataPath=self.blockDataPath, transactionMetricsDataPath=self.transactionMetricsDataPath) @@ -457,7 +457,7 @@ def analyzeResultsAndReport(self, testResult: PtbTpsTestResult, blockDict: dict, numBlocksToPrune=self.ptbConfig.numAddlBlocksToPrune, numTrxGensUsed=testResult.numGeneratorsUsed, targetTpsPerGenList=testResult.targetTpsPerGenList, quiet=self.ptbConfig.quiet) self.report = log_reader.calcAndReport(data=self.data, tpsTestConfig=tpsTestConfig, artifacts=artifactsLocate, argsDict=args, testStart=self.testStart, - completedRun=testResult.completedRun,nodeosVers=self.clusterConfig.nodeosVers, blockDict=blockDict, trxDict=trxDict) + completedRun=testResult.completedRun,nodeosVers=self.clusterConfig.nodeosVers) jsonReport = None if not self.ptbConfig.quiet or not self.ptbConfig.delReport: @@ -479,9 +479,8 @@ def preTestSpinup(self): if self.launchCluster() == False: self.errorExit('Failed to stand up cluster.') - def postTpsTestSteps(self, blockDict: dict, trxDict: dict): - return self.queryBlockTrxData(self.validationNode, self.blockDataPath, self.blockTrxDataPath, self.data.startBlock, self.data.ceaseBlock, - blockDict=blockDict, trxDict=trxDict) + def postTpsTestSteps(self): + return self.queryBlockTrxData(self.validationNode, self.blockDataPath, self.blockTrxDataPath, self.data.startBlock, self.data.ceaseBlock) def runTest(self) -> bool: testSuccessful = False @@ -493,12 +492,10 @@ def runTest(self) -> bool: self.ptbTestResult = self.runTpsTest() - blockDict = {} - trxDict = {} - self.postTpsTestSteps(blockDict, trxDict) + self.postTpsTestSteps() self.captureLowLevelArtifacts() - self.analyzeResultsAndReport(self.ptbTestResult, blockDict, trxDict) + self.analyzeResultsAndReport(self.ptbTestResult) testSuccessful = self.ptbTestResult.completedRun From b1dfcb99f8ff8e92386cfd21a20f6c77197e555a Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Fri, 31 Mar 2023 16:27:22 -0500 Subject: [PATCH 07/13] rename blockLog in chainData to blockList to better represent what the member actually is --- tests/performance_tests/log_reader.py | 18 +++++++++--------- .../performance_test_basic.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index be0e971839..778017a7cd 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -156,7 +156,7 @@ def timestamp(self): class chainData(): def __init__(self): - self.blockLog = [] + self.blockList = [] self.blockDict = {} self.trxDict = {} self.startBlock = None @@ -307,15 +307,15 @@ def calcChainGuide(data: chainData, numAddlBlocksToDrop=0) -> chainBlocksGuide: 3) Additional blocks - potentially partially full blocks while test scenario ramps up to steady state Keyword arguments: - data -- the chainData for the test run. Includes blockLog, startBlock, and ceaseBlock + data -- the chainData for the test run. Includes blockList, startBlock, and ceaseBlock numAddlBlocksToDrop -- num potentially non-empty blocks to ignore at beginning and end of test for steady state purposes Returns: chain guide describing key blocks and counts of blocks to describe test scenario """ - firstBN = data.blockLog[0].blockNum - lastBN = data.blockLog[-1].blockNum - total = len(data.blockLog) + firstBN = data.blockList[0].blockNum + lastBN = data.blockList[-1].blockNum + total = len(data.blockList) testStartBN = data.startBlock testEndBN = data.ceaseBlock @@ -329,14 +329,14 @@ def calcChainGuide(data: chainData, numAddlBlocksToDrop=0) -> chainBlocksGuide: leadingEmpty = 0 for le in range(setupCnt, total - tearDownCnt - 1): - if data.blockLog[le].transactions == 0: + if data.blockList[le].transactions == 0: leadingEmpty += 1 else: break trailingEmpty = 0 for te in range(total - tearDownCnt - 1, setupCnt + leadingEmpty, -1): - if data.blockLog[te].transactions == 0: + if data.blockList[te].transactions == 0: trailingEmpty += 1 else: break @@ -355,14 +355,14 @@ def pruneToSteadyState(data: chainData, guide: chainBlocksGuide): 3) Additional blocks - potentially partially full blocks while test scenario ramps up to steady state Keyword arguments: - data -- the chainData for the test run. Includes blockLog, startBlock, and ceaseBlock + data -- the chainData for the test run. Includes blockList, startBlock, and ceaseBlock guide -- chain guiderails calculated over chain data to guide interpretation of whole run's block data Returns: pruned list of blockData representing steady state operation """ - return data.blockLog[guide.setupBlocksCnt + guide.leadingEmptyBlocksCnt + guide.configAddlDropCnt:-(guide.tearDownBlocksCnt + guide.trailingEmptyBlocksCnt + guide.configAddlDropCnt)] + return data.blockList[guide.setupBlocksCnt + guide.leadingEmptyBlocksCnt + guide.configAddlDropCnt:-(guide.tearDownBlocksCnt + guide.trailingEmptyBlocksCnt + guide.configAddlDropCnt)] def scoreTransfersPerSecond(data: chainData, guide: chainBlocksGuide) -> stats: """Analyzes a test scenario's steady state block data for statistics around transfers per second over every two-consecutive-block window""" diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index e3dc52b14a..3566ad1103 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -255,7 +255,7 @@ def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum blockTransactionTotal += 1 blockData = self.clusterConfig.createBlockData(block=block, blockTransactionTotal=blockTransactionTotal, blockNetTotal=blockNetTotal, blockCpuTotal=blockCpuTotal) - self.data.blockLog.append(blockData) + self.data.blockList.append(blockData) self.data.blockDict[str(blockNum)] = blockData bdf_append_write = self.fileOpenMode(blockDataPath) with open(blockDataPath, bdf_append_write) as blockDataFile: From 466093fad92859f2ee1ffee3fa7db03695879667 Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Fri, 31 Mar 2023 16:32:03 -0500 Subject: [PATCH 08/13] remove pointless return --- tests/performance_tests/performance_test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 3566ad1103..083df0dab5 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -480,7 +480,7 @@ def preTestSpinup(self): self.errorExit('Failed to stand up cluster.') def postTpsTestSteps(self): - return self.queryBlockTrxData(self.validationNode, self.blockDataPath, self.blockTrxDataPath, self.data.startBlock, self.data.ceaseBlock) + self.queryBlockTrxData(self.validationNode, self.blockDataPath, self.blockTrxDataPath, self.data.startBlock, self.data.ceaseBlock) def runTest(self) -> bool: testSuccessful = False From 6486facd38f9e33d214f055dfa0a91dd3c97e2a3 Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Fri, 31 Mar 2023 17:00:04 -0500 Subject: [PATCH 09/13] rename chainData::nodes to chainData::numNodes to clarify what it means --- tests/performance_tests/log_reader.py | 8 ++++---- tests/performance_tests/performance_test_basic.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index 778017a7cd..68405849eb 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -168,7 +168,7 @@ def __init__(self): self.totalTime = 0 self.droppedBlocks = {} self.forkedBlocks = {} - self.nodes = 0 + self.numNodes = 0 def __eq__(self, other): return self.startBlock == other.startBlock and\ self.ceaseBlock == other.ceaseBlock and\ @@ -177,7 +177,7 @@ def __eq__(self, other): self.totalCpu == other.totalCpu and\ self.totalElapsed == other.totalElapsed and\ self.totalTime == other.totalTime and\ - self.nodes == other.nodes + self.numNodes == other.numNodes def updateTotal(self, transactions, net, cpu, elapsed, time): self.totalTransactions += transactions self.totalNet += net @@ -209,7 +209,7 @@ def scrapeLogBlockElapsedTime(data: chainData, path): data.blockDict[str(value[1])].time = int(v3Logging[0][1]) def scrapeLogDroppedForkedBlocks(data: chainData, path): - for nodeNum in range(0, data.nodes): + for nodeNum in range(0, data.numNodes): nodePath = path/f"node_{str(nodeNum).zfill(2)}"/"stderr.txt" selectedopen = selectedOpen(path) with selectedopen(nodePath, 'rt') as f: @@ -517,7 +517,7 @@ def calcAndReport(data: chainData, tpsTestConfig: TpsTestConfig, artifacts: Arti report = createReport(guide=guide, tpsTestConfig=tpsTestConfig, tpsStats=tpsStats, blockSizeStats=blkSizeStats, trxLatencyStats=trxLatencyStats, trxCpuStats=trxCpuStats, trxNetStats=trxNetStats, forkedBlocks=data.forkedBlocks, droppedBlocks=data.droppedBlocks, prodWindows=prodWindows, notFound=notFound, testStart=start, testFinish=finish, argsDict=argsDict, completedRun=completedRun, - nodeosVers=nodeosVers, numNodes=data.nodes) + nodeosVers=nodeosVers, numNodes=data.numNodes) return report def exportReportAsJSON(report: json, exportPath): diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 083df0dab5..764c547486 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -351,7 +351,7 @@ def runTpsTest(self) -> PtbTpsTestResult: chainId = info['chain_id'] lib_id = info['last_irreversible_block_id'] self.data = log_reader.chainData() - self.data.nodes = self.clusterConfig._totalNodes + self.data.numNodes = self.clusterConfig._totalNodes abiFile=None actionsDataJson=None From a78965bc9de87a1de63efcebc223eb47503b03bb Mon Sep 17 00:00:00 2001 From: Clayton Calabrese Date: Fri, 31 Mar 2023 17:03:08 -0500 Subject: [PATCH 10/13] remove accidental newline --- tests/performance_tests/log_reader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/performance_tests/log_reader.py b/tests/performance_tests/log_reader.py index 68405849eb..e9d63fdad8 100644 --- a/tests/performance_tests/log_reader.py +++ b/tests/performance_tests/log_reader.py @@ -188,7 +188,6 @@ def __str__(self): return (f"Starting block: {self.startBlock}\nEnding block:{self.ceaseBlock}\nChain transactions: {self.totalTransactions}\n" f"Chain cpu: {self.totalCpu}\nChain net: {(self.totalNet / (self.ceaseBlock - self.startBlock + 1))}\nChain elapsed: {self.totalElapsed}\n" f"Chain time: {self.totalTime}\n") - def assertEquality(self, other): assert self == other, f"Error: Actual log:\n{self}\ndid not match expected log:\n{other}" From 1164893367532a424dd60b66e886e2a89d6138b2 Mon Sep 17 00:00:00 2001 From: Peter Oschwald Date: Mon, 3 Apr 2023 11:27:54 -0500 Subject: [PATCH 11/13] Remove log_reader_tests.py and associated test log files. With rework to no longer rely on parsing a majority of the block and transaction data out of the nodeos log in favor of querying it directly from an active node, the log_reader_tests have lost their usefulness. Can remove nodeos logs as well which were supporting the testing. --- tests/performance_tests/CMakeLists.txt | 5 - .../block_trx_data_log_2_0_14.txt.gz | Bin 152 -> 0 bytes tests/performance_tests/log_reader_tests.py | 206 ------------------ .../nodeos_log_2_0_14.txt.gz | Bin 38098 -> 0 bytes tests/performance_tests/nodeos_log_3_2.txt.gz | Bin 14544 -> 0 bytes 5 files changed, 211 deletions(-) delete mode 100644 tests/performance_tests/block_trx_data_log_2_0_14.txt.gz delete mode 100755 tests/performance_tests/log_reader_tests.py delete mode 100644 tests/performance_tests/nodeos_log_2_0_14.txt.gz delete mode 100644 tests/performance_tests/nodeos_log_3_2.txt.gz diff --git a/tests/performance_tests/CMakeLists.txt b/tests/performance_tests/CMakeLists.txt index dd6215252a..bae18fcd82 100644 --- a/tests/performance_tests/CMakeLists.txt +++ b/tests/performance_tests/CMakeLists.txt @@ -1,10 +1,6 @@ configure_file(performance_test_basic.py . COPYONLY) configure_file(performance_test.py . COPYONLY) configure_file(log_reader.py . COPYONLY) -configure_file(log_reader_tests.py . COPYONLY) -configure_file(nodeos_log_2_0_14.txt.gz . COPYONLY) -configure_file(nodeos_log_3_2.txt.gz . COPYONLY) -configure_file(block_trx_data_log_2_0_14.txt.gz . COPYONLY) configure_file(genesis.json . COPYONLY) configure_file(cpuTrxData.json . COPYONLY) configure_file(ramTrxData.json . COPYONLY) @@ -24,7 +20,6 @@ add_test(NAME performance_test_basic_ex_transfer_trx_spec COMMAND tests/performa add_test(NAME performance_test_basic_ex_new_acct_trx_spec COMMAND tests/performance_tests/performance_test_basic.py -v -p 1 -n 1 --target-tps 20 --tps-limit-per-generator 10 --test-duration-sec 5 --clean-run --chain-state-db-size-mb 200 --user-trx-data-file tests/performance_tests/userTrxDataNewAccount.json ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) add_test(NAME performance_test_basic_ex_cpu_trx_spec COMMAND tests/performance_tests/performance_test_basic.py -v -p 1 -n 1 --target-tps 20 --tps-limit-per-generator 10 --test-duration-sec 5 --clean-run --chain-state-db-size-mb 200 --account-name "c" --abi-file eosmechanics.abi --wasm-file eosmechanics.wasm --contract-dir unittests/contracts/eosio.mechanics --user-trx-data-file tests/performance_tests/cpuTrxData.json ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) add_test(NAME performance_test_basic_ex_ram_trx_spec COMMAND tests/performance_tests/performance_test_basic.py -v -p 1 -n 1 --target-tps 20 --tps-limit-per-generator 10 --test-duration-sec 5 --clean-run --chain-state-db-size-mb 200 --account-name "r" --abi-file eosmechanics.abi --wasm-file eosmechanics.wasm --contract-dir unittests/contracts/eosio.mechanics --user-trx-data-file tests/performance_tests/ramTrxData.json ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) -add_test(NAME log_reader_tests COMMAND tests/performance_tests/log_reader_tests.py WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST performance_test PROPERTY LABELS long_running_tests) set_property(TEST performance_test_ex_cpu_trx_spec PROPERTY LABELS long_running_tests) set_property(TEST performance_test_basic PROPERTY LABELS nonparallelizable_tests) diff --git a/tests/performance_tests/block_trx_data_log_2_0_14.txt.gz b/tests/performance_tests/block_trx_data_log_2_0_14.txt.gz deleted file mode 100644 index 006f693786508fed8b42e84af99ef84d5b058e49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 152 zcmV;J0B8RniwFpnQvzfF17d7%V{2b@a(G{4VRT_%Y;R{@GG8!XF*Gi8cys_Q%(o2# zK@0%FT(x{ab9Cj<9Q!XpFF?UY3(UdR=_G-TvNN%%>1dYG&H-IGfJI0=y6!Az#c%FOWTANB>@4w7V< G0001vWj$E{ diff --git a/tests/performance_tests/log_reader_tests.py b/tests/performance_tests/log_reader_tests.py deleted file mode 100755 index be4c09557f..0000000000 --- a/tests/performance_tests/log_reader_tests.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -# Unit tests to ensure that nodeos log scraping and evaluation behavior from log_reader.py does not change -# Also ensures that all versions of nodeos logs can be handled -import log_reader - -from pathlib import Path - -testSuccessful = False - -# Test log scraping for 3.2 log format -dataCurrent = log_reader.chainData() -dataCurrent.startBlock = None -dataCurrent.ceaseBlock = None -log_reader.scrapeLog(dataCurrent, Path("tests")/"performance_tests"/"nodeos_log_3_2.txt.gz") - -expectedCurrent = log_reader.chainData() -expectedCurrent.startBlock = 2 -expectedCurrent.ceaseBlock = 265 -expectedCurrent.totalTransactions = 133 -expectedCurrent.totalNet = 105888 -expectedCurrent.totalCpu = 27275 -expectedCurrent.totalElapsed = 7704 -expectedCurrent.totalTime = 5743400 -expectedCurrent.totalLatency = -9398 - -dataCurrent.assertEquality(expectedCurrent) - -# First test full block data stats with no pruning -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataCurrent, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataCurrent, guide) -blkSizeStats = log_reader.calcBlockSizeStats(dataCurrent, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 264, expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 0, 0, 15, 30, 0, 264-15-30) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(0, 21, 1.2110091743119267, 3.2256807673357684, 147, 219) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" -expectedBlkSizeStats = log_reader.stats(0, 66920, 483.5068493150685, 4582.238297120407, 147, 219) -assert expectedBlkSizeStats == blkSizeStats , f"Error: Stats calculated: {blkSizeStats} did not match expected stats: {expectedBlkSizeStats}" - -# Next test block data stats with empty block pruning -dataCurrent.startBlock = 105 -dataCurrent.ceaseBlock = 257 -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataCurrent, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataCurrent, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 264, 105, 257, 103, 8, 12, 22, 0, 264-103-8-12-22) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(1, 1, 1.0, 0.0, 59, 119) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -# Next test block data stats with additional block pruning -dataCurrent.startBlock = 105 -dataCurrent.ceaseBlock = 257 -numAddlBlocksToPrune = 2 -guide = log_reader.calcChainGuide(dataCurrent, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataCurrent, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 264, 105, 257, 103, 8, 12, 22, 2, 264-103-8-12-22-4) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(1, 1, 1.0, 0.0, 57, 115) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -# Next test block data stats with 0 blocks left -dataCurrent.startBlock = 117 -dataCurrent.ceaseBlock = 118 -numAddlBlocksToPrune = 2 -guide = log_reader.calcChainGuide(dataCurrent, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataCurrent, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 264, 117, 118, 115, 147, 0, 1, 2, 0) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(0, 0, 0, 0.0, 0, 0) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -# Next test block data stats with 1 block left -dataCurrent.startBlock = 117 -dataCurrent.ceaseBlock = 117 -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataCurrent, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataCurrent, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 264, 117, 117, 115, 148, 0, 0, 0, 264-115-148) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(1, 1, 1.0, 0.0, 0, 1) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -# Next test block data stats with 2 blocks left -dataCurrent.startBlock = 80 -dataCurrent.ceaseBlock = 81 -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataCurrent, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataCurrent, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedCurrent.startBlock, expectedCurrent.ceaseBlock, 264, 80, 81, 78, 184, 0, 0, 0, 264-78-184) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(3, 3, 3, 0.0, 0, 2) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - - -# Test log scraping from a 2.0.14 log format -dataOld = log_reader.chainData() -dataOld.startBlock = None -dataOld.ceaseBlock = None -log_reader.scrapeLog(dataOld, Path("tests")/"performance_tests"/"nodeos_log_2_0_14.txt.gz") -expectedOld = log_reader.chainData() -expectedOld.startBlock = 2 -expectedOld.ceaseBlock = 93 -expectedOld.totalTransactions = 129 -# Net, Cpu, Elapsed, and Time are not logged in the old logging and will thus be 0 -expectedOld.totalNet = 0 -expectedOld.totalCpu = 0 -expectedOld.totalElapsed = 0 -expectedOld.totalTime = 0 -expectedOld.totalLatency = -5802 - -dataOld.assertEquality(expectedOld) - -# First test full block data stats with no pruning -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataOld, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataOld, guide) -blkSizeStats = log_reader.calcBlockSizeStats(dataOld, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedOld.startBlock, expectedOld.ceaseBlock, 92, 2, 93, 0, 0, 17, 9, 0, 92-17-9) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(0, 61, 3.753846153846154, 11.38153804562563, 51, 66) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" -expectedBlkSizeStats = log_reader.stats(0, 0, 0, 0, 66, 66) -assert expectedBlkSizeStats == blkSizeStats , f"Error: Stats calculated: {blkSizeStats} did not match expected stats: {expectedBlkSizeStats}" - -# Next test block data stats with empty block pruning -dataOld.startBlock = 15 -dataOld.ceaseBlock = 33 -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataOld, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataOld, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedOld.startBlock, expectedOld.ceaseBlock, 92, 15, 33, 13, 60, 4, 6, 0, 92-13-60-4-6) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(0, 61, 24.5, 22.666053913286273, 3, 9) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - - -# Next test block data stats with additional block pruning -dataOld.startBlock = 15 -dataOld.ceaseBlock = 33 -numAddlBlocksToPrune = 2 -guide = log_reader.calcChainGuide(dataOld, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataOld, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedOld.startBlock, expectedOld.ceaseBlock, 92, 15, 33, 13, 60, 4, 6, 2, 92-13-60-4-6-4) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(0, 52, 17.75, 21.241174637952582, 2, 5) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - - -# Next test block data stats with 0 blocks left -dataOld.startBlock = 19 -dataOld.ceaseBlock = 20 -numAddlBlocksToPrune = 2 -guide = log_reader.calcChainGuide(dataOld, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataOld, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedOld.startBlock, expectedOld.ceaseBlock, 92, 19, 20, 17, 73, 0, 0, 2, 0) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(0, 0, 0, 0.0, 0, 0) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -# Next test block data stats with 1 block left -dataOld.startBlock = 19 -dataOld.ceaseBlock = 19 -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataOld, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataOld, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedOld.startBlock, expectedOld.ceaseBlock, 92, 19, 19, 17, 74, 0, 0, 0, 92-17-74) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(13, 13, 13.0, 0.0, 0, 1) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -# Next test block data stats with 2 blocks left -dataOld.startBlock = 19 -dataOld.ceaseBlock = 20 -numAddlBlocksToPrune = 0 -guide = log_reader.calcChainGuide(dataOld, numAddlBlocksToPrune) -stats = log_reader.scoreTransfersPerSecond(dataOld, guide) - -expectedGuide = log_reader.chainBlocksGuide(expectedOld.startBlock, expectedOld.ceaseBlock, 92, 19, 20, 17, 73, 0, 0, 0, 92-17-73) -assert expectedGuide == guide, f"Error: Guide calculated: {guide} did not match expected stats: {expectedGuide}" -expectedTpsStats = log_reader.stats(41, 41, 41, 0.0, 0, 2) -assert expectedTpsStats == stats , f"Error: Stats calculated: {stats} did not match expected stats: {expectedTpsStats}" - -#ensure that scraping of trxDataLog is compatible with 2.0 -trxDict = {} -log_reader.scrapeBlockTrxDataLog(trxDict=trxDict, path=Path("tests")/"performance_tests"/"block_trx_data_log_2_0_14.txt.gz", nodeosVers="v2") -expectedDict = {} -expectedDict["41c6dca250f9b74d9fa6a8177a9c8390cb1d01b2123d6f88354f571f0053df72"] = log_reader.trxData(blockNum='112',cpuUsageUs='1253',netUsageUs='19') -expectedDict["fa17f9033589bb8757be009af46d465f0d903e26b7d198ea0fb6a3cbed93c2e6"] = log_reader.trxData(blockNum='112',cpuUsageUs='1263',netUsageUs='19') -assert trxDict == expectedDict, f"Scraped transaction dictionary: {trxDict} did not match expected dictionary : {expectedDict}" - -testSuccessful = True - -exitCode = 0 if testSuccessful else 1 -exit(exitCode) diff --git a/tests/performance_tests/nodeos_log_2_0_14.txt.gz b/tests/performance_tests/nodeos_log_2_0_14.txt.gz deleted file mode 100644 index bde508dc9645deb93fbffcf90d1bd6892feeed1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38098 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 OPG8^9naOxaN%DXFbv%*) diff --git a/tests/performance_tests/nodeos_log_3_2.txt.gz b/tests/performance_tests/nodeos_log_3_2.txt.gz deleted file mode 100644 index 43a277b94cf64c6b1f3f3628b663ea9a11f22f38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14544 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 p!5_%0-wl+18)53j>`9H4uUZem3 From 31273296fee664f68d6978efa136db27a0df5ac4 Mon Sep 17 00:00:00 2001 From: Peter Oschwald Date: Tue, 4 Apr 2023 16:18:39 -0500 Subject: [PATCH 12/13] Address peer review comments. --- tests/performance_tests/performance_test_basic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 4e64b3f84b..9a90edf369 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -232,7 +232,8 @@ def fileOpenMode(self, filePath) -> str: append_write = 'w' return append_write - def isImportantTransaction(self, transaction): + def isOnBlockTransaction(self, transaction): + # v2 history does not include onblock if self.clusterConfig.nodeosVers == "v2": return True else: @@ -247,7 +248,7 @@ def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum btdf_append_write = self.fileOpenMode(blockTrxDataPath) with open(blockTrxDataPath, btdf_append_write) as trxDataFile: for transaction in block['payload']['transactions']: - if self.isImportantTransaction(transaction): + if not self.isOnBlockTransaction(transaction): self.clusterConfig.updateTrxDict(blockNum, transaction, self.data.trxDict) self.clusterConfig.writeTrx(trxDataFile, blockNum, transaction) blockCpuTotal += transaction["cpu_usage_us"] From cd87f42a9b5f672a98afc978403c960ddad6f332 Mon Sep 17 00:00:00 2001 From: Peter Oschwald Date: Tue, 4 Apr 2023 16:44:53 -0500 Subject: [PATCH 13/13] Fix logic in isOnBlockTransaction as it was inverted. --- tests/performance_tests/performance_test_basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/performance_tests/performance_test_basic.py b/tests/performance_tests/performance_test_basic.py index 9a90edf369..c078168730 100755 --- a/tests/performance_tests/performance_test_basic.py +++ b/tests/performance_tests/performance_test_basic.py @@ -235,11 +235,11 @@ def fileOpenMode(self, filePath) -> str: def isOnBlockTransaction(self, transaction): # v2 history does not include onblock if self.clusterConfig.nodeosVers == "v2": - return True + return False else: if transaction['actions'][0]['account'] != 'eosio' or transaction['actions'][0]['action'] != 'onblock': - return True - return False + return False + return True def queryBlockTrxData(self, node, blockDataPath, blockTrxDataPath, startBlockNum, endBlockNum): for blockNum in range(startBlockNum, endBlockNum + 1):