benchmark.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. #!/usr/bin/env python3
  2. import os
  3. import subprocess
  4. import csv
  5. import re
  6. from argparse import ArgumentParser, Action
  7. from pathlib import Path
  8. from model_properties import model_properties
  9. MODEL_TYPES = ("LI", "HI", "HO", "HOmod")
  10. DS_MODEL_TYPES = ("LI2HI", "HI2LI", "LI-HI", "dLI", "mdLI", "dHI", "mdHI", "dHO", "mdHO", "test")
  11. GEN_METHODS = ("start", "uniform", "end")
  12. INT_CYCLES = 0
  13. EXT_CYCLES = 0
  14. PyPDEVS = "./pythonpdevs/main.py"
  15. MinimalPyPDEVS = "./pythonpdevs-minimal/main.py"
  16. # Regex to capture times from script output
  17. TIME_PATTERN = re.compile(
  18. r"Model creation time: ([\d.eE+-]+).*"
  19. r"Engine setup time: ([\d.eE+-]+).*"
  20. r"Simulation time: ([\d.eE+-]+)",
  21. re.S
  22. )
  23. def create_graph(file, title=None):
  24. # Base command
  25. cmd = f"procpath plot -d {file} -f {file.with_suffix('.svg')} -q rss --relative-time"
  26. # Add title if provided
  27. if title:
  28. cmd += f" --title '{title}'"
  29. subprocess.run(cmd, shell=True)
  30. def run_devstone(model_type, depth, width, mode, out, memory, stats, DCP, *, classic=False, number=1, gen_mode="uniform"):
  31. """Runs the devstone_driver.py script and returns timing results as floats."""
  32. SCRIPT_PATH = PyPDEVS
  33. if mode == "minimal":
  34. SCRIPT_PATH = MinimalPyPDEVS
  35. cmd = [
  36. "uv", "run", SCRIPT_PATH,
  37. "-m", model_type,
  38. "-d", str(depth),
  39. "-w", str(width),
  40. "-i", str(INT_CYCLES),
  41. "-e", str(EXT_CYCLES),
  42. ]
  43. if DCP:
  44. cmd.extend(["--DCP", DCP])
  45. if stats:
  46. cmd.extend(["-s"])
  47. if mode == "dynamic":
  48. cmd.extend(["-D"])
  49. if classic:
  50. cmd.extend(["-C"])
  51. cmd.extend(["-n", str(number)])
  52. cmd.extend(["-g", gen_mode])
  53. if mode == "classic":
  54. cmd.extend(["-C"])
  55. full_cmd = " ".join(cmd)
  56. if memory:
  57. full_cmd = (
  58. f"{full_cmd} & "
  59. f"PID=$!; "
  60. f"python -m procpath record -i {memory['interval']} -f stat -d {memory['file']} "
  61. f"--stop-without-result \"$..children[?(@.stat.pid == $PID)]\""
  62. )
  63. env = os.environ.copy()
  64. project_root = os.path.abspath(os.path.join(os.getcwd(), ".."))
  65. env["PYTHONPATH"] = project_root + os.pathsep + env.get("PYTHONPATH", "")
  66. result = subprocess.run(full_cmd, env=env, shell=True, capture_output=True, text=True)
  67. cout = result.stdout.strip()
  68. if out:
  69. print(cout)
  70. if memory:
  71. props = model_properties(model_type, width, depth)
  72. n_atomic = props.get("Atomic Models", "0")
  73. graph_title = f"{model_type} ({n_atomic} atomic models)"
  74. create_graph(memory["file"], graph_title)
  75. cerr = result.stderr.strip()
  76. match = TIME_PATTERN.search(cout)
  77. if match:
  78. creation, setup, sim = map(float, match.groups())
  79. return creation, setup, sim
  80. print("Failed to run: ", " ".join(cmd))
  81. print(cout)
  82. print(cerr)
  83. return None, None, None
  84. def defaultArguments(parser):
  85. parser.add_argument("-p", "--path", type=Path, default=Path("./Results/PyPDEVS"), help="Path to the results directory")
  86. parser.add_argument("-f", '--filename', type=str, default="", help="Name of the output file")
  87. parser.add_argument("-a", "--append", action="store_true", help="Append to the output file instead of overwriting.")
  88. parser.add_argument("-D", "--max-depth", default=8, type=int, help="Max depth for the generated models")
  89. parser.add_argument("-ds", "--depth-stepsize", type=int, help="Determines for which depths a model is generated.")
  90. parser.add_argument("-W", "--max-width", default=8, type=int, help="Max width for the generated models")
  91. parser.add_argument("-ws", "--width-stepsize", type=int, help="Determines for which depths a model is generated.")
  92. parser.add_argument("-r", "--repetitions", default=1, type=int, help="The number of times a simulation is repeated")
  93. parser.add_argument("-O", "--output", action="store_true", help="Print Simulation Outputs")
  94. parser.add_argument("-M", "--memory", action="store_true", help="Measures the memory usage of a simulation.")
  95. parser.add_argument("-i", "--interval", default=1.0, type=float, help="The interval at which measurements are taken.")
  96. parser.add_argument("--stats", "-s", action="store_true", help="Enable Event Tracking")
  97. parser.add_argument("--DCP", choices=["old", "memoised"], default="", help="Select the DCP implementation, old: legacy, memoised: cached, and leave empty to use the default one.")
  98. def Minimal(subparsers):
  99. minimal = subparsers.add_parser("minimal", help="Minimal DEVS kernel")
  100. minimal.set_defaults(mode="minimal")
  101. defaultArguments(minimal)
  102. minimal.add_argument(
  103. "-ms", "--model-structure",
  104. nargs="+",
  105. choices=MODEL_TYPES,
  106. default=MODEL_TYPES
  107. )
  108. def Classic(subparsers):
  109. classic = subparsers.add_parser("classic", help="Classic DEVS Simulator")
  110. classic.set_defaults(mode="classic")
  111. defaultArguments(classic)
  112. classic.add_argument(
  113. "-ms", "--model-structure",
  114. nargs="+",
  115. choices=MODEL_TYPES,
  116. default=MODEL_TYPES
  117. )
  118. def Parallel(subparsers):
  119. parallel = subparsers.add_parser("parallel", help="Parallel DEVS")
  120. parallel.set_defaults(mode="parallel")
  121. defaultArguments(parallel)
  122. parallel.add_argument(
  123. "-ms", "--model-structure",
  124. nargs="+",
  125. choices=MODEL_TYPES,
  126. default=MODEL_TYPES
  127. )
  128. def Dynamic(subparsers):
  129. dynamic = subparsers.add_parser("dynamic", help="Dynamic Structure DEVS")
  130. dynamic.set_defaults(mode="dynamic")
  131. defaultArguments(dynamic)
  132. dynamic.add_argument(
  133. "-ms", "--model-structure",
  134. nargs="+",
  135. choices=MODEL_TYPES + DS_MODEL_TYPES,
  136. default=MODEL_TYPES + DS_MODEL_TYPES
  137. )
  138. dynamic.add_argument(
  139. "-C", "--classic",
  140. action="store_true",
  141. default=False,
  142. help="Selects the Classic DEVS Simulator"
  143. )
  144. dynamic.add_argument(
  145. "-n", "--number",
  146. default=1,
  147. type=int,
  148. help="Number of models to be generated in a cycle per coupled model")
  149. dynamic.add_argument(
  150. "-g", "--gen-mode",
  151. choices=GEN_METHODS,
  152. default="uniform"
  153. )
  154. def parse_args():
  155. parser = ArgumentParser()
  156. subparsers = parser.add_subparsers(dest="mode")
  157. Minimal(subparsers)
  158. Parallel(subparsers)
  159. Classic(subparsers)
  160. Dynamic(subparsers)
  161. args = parser.parse_args()
  162. if args.depth_stepsize is None:
  163. args.depth_stepsize = args.max_depth
  164. if args.width_stepsize is None:
  165. args.width_stepsize = args.max_width
  166. if args.memory or args.stats:
  167. args.output = True
  168. return args
  169. def run(writer, csvfile, model_type, d, w, args, mem_cmd, extra):
  170. """Orchestrates the repeated simulation runs."""
  171. for depth in range(d[1], d[0] + 1, d[1]):
  172. for width in range(w[1], w[0] + 1, w[1]):
  173. for run in range(args.repetitions):
  174. creation, setup, sim = run_devstone(
  175. model_type, depth, width,
  176. args.mode, args.output,
  177. mem_cmd, args.stats, args.DCP,
  178. **extra
  179. )
  180. # Only write to CSV if a writer was provided
  181. if writer and creation is not None:
  182. row = {
  183. "simulator": sim_id,
  184. "model_type": model_type, "depth": depth, "width": width, "run": run,
  185. "creation_time": creation, "setup_time": setup, "simulation_time": sim,
  186. }
  187. if args.mode == "dynamic":
  188. row["number"] = args.number
  189. row["generator_mode"] = args.gen_mode
  190. writer.writerow(row)
  191. csvfile.flush()
  192. if __name__ == "__main__":
  193. args = parse_args()
  194. print(args)
  195. d = (args.max_depth, args.depth_stepsize)
  196. w = (args.max_width, args.width_stepsize)
  197. if d[0] % d[1] != 0:
  198. raise RuntimeError("Invalid depth stepsize.")
  199. if w[0] % w[1] != 0:
  200. raise RuntimeError("Invalid width stepsize.")
  201. fieldnames = ["simulator", "model_type", "depth", "width", "run",
  202. "creation_time", "setup_time", "simulation_time"]
  203. mode = args.mode
  204. if mode == "dynamic":
  205. if args.classic:
  206. mode += "-classic"
  207. fieldnames += ["number", "generator_mode"]
  208. sim_id = f"pypdevs-{mode}"
  209. extra = {
  210. "classic": False if not hasattr(args, "classic") else args.classic,
  211. "gen_mode": "uniform" if not hasattr(args, "gen_mode") else args.gen_mode,
  212. "number": 1 if not hasattr(args, "number") else args.number,
  213. }
  214. time_dir = args.path / "Time"
  215. time_dir.mkdir(parents=True, exist_ok=True)
  216. mem_dir = args.path / "Memory"
  217. mem_dir.mkdir(parents=True, exist_ok=True)
  218. for model_type in args.model_structure:
  219. time_mode = time_dir / mode
  220. time_mode.mkdir(parents=True, exist_ok=True)
  221. mem_mode = mem_dir / mode
  222. mem_mode.mkdir(parents=True, exist_ok=True)
  223. time_file = (time_mode / model_type).with_suffix(".csv")
  224. memory_file = (mem_mode / model_type).with_suffix(".sqlite")
  225. if hasattr(args, "number") and args.number > 1:
  226. time_file = time_file.with_stem(time_file.stem + "-" + str(args.number))
  227. memory_file = memory_file.with_stem(memory_file.stem + "-" + str(args.number))
  228. if args.filename:
  229. suffix = f"_{args.filename}"
  230. time_file = time_file.with_stem(time_file.stem + suffix)
  231. memory_file = memory_file.with_stem(memory_file.stem + suffix)
  232. mem_cmd = {"interval": args.interval, "file": memory_file} if args.memory else {}
  233. if not args.memory:
  234. file_mode = "a" if args.append else "w"
  235. write_header = not args.append or not os.path.exists(time_file)
  236. with open(time_file, file_mode, newline="") as csvfile:
  237. writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
  238. if write_header:
  239. writer.writeheader()
  240. run(writer, csvfile, model_type, d, w, args, mem_cmd, extra)
  241. print(f"✅ Saved timing results to {time_file}")
  242. else:
  243. if os.path.exists(memory_file) and not args.append:
  244. os.remove(memory_file)
  245. print("📊 Measuring memory (Timing results will not be saved)...")
  246. run(None, None, model_type, d, w, args, mem_cmd, extra)
  247. print(f"✅ Saved memory database to {memory_file}")