node-metering.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env python
  2. # This file is part of DarkFi (https://dark.fi)
  3. #
  4. # Copyright (C) 2020-2025 Dyne.org foundation
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as
  8. # published by the Free Software Foundation, either version 3 of the
  9. # License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. import asyncio, json, random, sys, time, argparse, csv, signal, statistics
  19. from collections import defaultdict
  20. class JsonRpc:
  21. async def start(self, server, port):
  22. reader, writer = await asyncio.open_connection(server, port)
  23. self.reader = reader
  24. self.writer = writer
  25. async def stop(self):
  26. self.writer.close()
  27. await self.writer.wait_closed()
  28. async def _make_request(self, method, params):
  29. ident = random.randint(0, 2**16)
  30. request = {
  31. "jsonrpc": "2.0",
  32. "method": method,
  33. "params": params,
  34. "id": ident,
  35. }
  36. message = json.dumps(request) + "\n"
  37. self.writer.write(message.encode())
  38. await self.writer.drain()
  39. data = await self.reader.readline()
  40. message = data.decode().strip()
  41. response = json.loads(message)
  42. return response
  43. async def _subscribe(self, method, params):
  44. ident = random.randint(0, 2**16)
  45. request = {
  46. "jsonrpc": "2.0",
  47. "method": method,
  48. "params": params,
  49. "id": ident,
  50. }
  51. message = json.dumps(request) + "\n"
  52. self.writer.write(message.encode())
  53. await self.writer.drain()
  54. async def ping(self):
  55. return await self._make_request("ping", [])
  56. async def dnet_switch(self, state):
  57. return await self._make_request("dnet.switch", [state])
  58. async def dnet_subscribe_events(self):
  59. return await self._subscribe("dnet.subscribe_events", [])
  60. async def collect_messages(server, port, output_file):
  61. rpc = JsonRpc()
  62. while True:
  63. try:
  64. await rpc.start(server, port)
  65. break
  66. except OSError:
  67. pass
  68. file = open(output_file, "a", encoding="utf-8")
  69. print(f"Started collecting measurements, saving to {output_file} ...")
  70. try:
  71. await rpc.dnet_switch(True)
  72. await rpc.dnet_subscribe_events()
  73. file_writer = csv.writer(file, delimiter="\t")
  74. count = 0
  75. while True:
  76. data = await rpc.reader.readline()
  77. data = json.loads(data)
  78. params = data["params"][0]
  79. ev = params["event"]
  80. if ev != "recv":
  81. continue
  82. row = [params["info"]["cmd"], int(params["info"]["time"]), params["info"]["chan"]["addr"]]
  83. file_writer.writerow(row)
  84. count += 1
  85. print(f"Messages collected: {count}", end="\r", flush=True)
  86. except asyncio.CancelledError:
  87. print("Stopping message collection.")
  88. finally:
  89. await rpc.dnet_switch(False)
  90. await rpc.stop()
  91. file.close()
  92. def analyze_messages(output_file):
  93. data = []
  94. with open(output_file, mode="r", encoding="utf-8") as file:
  95. tsv_reader = csv.reader(file, delimiter="\t")
  96. # 0 - message_type
  97. # 1 - time in nano seconds
  98. # 2 - peer_addr
  99. for row in tsv_reader:
  100. row[1] = int(row[1])
  101. data.append(row)
  102. print(f"Analyzing the collected measurement data from {output_file} ...")
  103. # Group by message_type and peer_addr since we want to get the number of
  104. # messages we received from a particular peer in some time window
  105. grouped_data = defaultdict(list)
  106. for item in data:
  107. grouped_data[(item[0], item[2])].append(item)
  108. # Use a 10 second window size in nano seconds
  109. window_size = 10 * 1_000_000_000
  110. results = defaultdict(list)
  111. for key, messages in grouped_data.items():
  112. messages.sort(key=lambda x: x[1]) # Sort by time
  113. if len(messages) < 2:
  114. continue
  115. # We will start with the first item and count the number of messages
  116. # of some particular message_type in a 10 second window for each peer
  117. start_time = messages[0][1]
  118. window_counts = []
  119. count = 0
  120. for message in messages:
  121. message_time = message[1]
  122. if message_time < start_time + window_size:
  123. count += 1
  124. else:
  125. window_counts.append(count)
  126. start_time = message_time
  127. count = 1
  128. if count:
  129. window_counts.append(count)
  130. message_type = key[0]
  131. # Store counts of the same message_type across different peers
  132. results[message_type].extend(window_counts)
  133. for message_type, counts in results.items():
  134. print(f"Message Type: {message_type}")
  135. print(f" Count: {len(counts)}")
  136. print(f" Mean : {statistics.mean(counts)}")
  137. print(f" Median: {statistics.median(counts)}")
  138. print(f" Variance: {statistics.variance(counts)}")
  139. print(f" Max: {max(counts)}")
  140. print(f" Min: {min(counts)}\n")
  141. async def main(argv):
  142. parser = argparse.ArgumentParser(description='Tool to collect and analyze measurement of received messages')
  143. parser.add_argument('--server', default='127.0.0.1', help='RPC server')
  144. parser.add_argument('--port', default=26660, help='Port of the RPC server')
  145. parser.add_argument('--output-file', default='/tmp/node-metering-data.tsv', help='Location of the file containing the collected data')
  146. parser.add_argument('--analyze', action='store_true', help='Analyzes existing message from the output file without collecting new ones')
  147. args = parser.parse_args()
  148. if args.analyze:
  149. analyze_messages(args.output_file)
  150. else:
  151. collect_task = asyncio.create_task(collect_messages(args.server, args.port, args.output_file))
  152. loop = asyncio.get_event_loop()
  153. loop.add_signal_handler(signal.SIGINT, lambda: collect_task.cancel())
  154. await collect_task
  155. asyncio.run(main(sys.argv))