Ver código fonte

Added memory usage measuring and reconfigured the cli, updated the readme with a 'guide' on how to run the damn thing

Snej69420 9 meses atrás
pai
commit
d6ee4ea44c
7 arquivos alterados com 207 adições e 69 exclusões
  1. 2 1
      .gitignore
  2. 46 0
      README.md
  3. 0 3
      devstone/pythonpdevs-minimal/main.py
  4. 98 63
      devstone/testPyPDEVS.py
  5. 3 1
      pyproject.toml
  6. 1 1
      setup.sh
  7. 57 0
      uv.lock

+ 2 - 1
.gitignore

@@ -11,4 +11,5 @@ __pycache__
 .python-version
 
 *.csv
-*.log
+*.log
+*.sqlite

+ 46 - 0
README.md

@@ -1,2 +1,48 @@
 # devstone-comparative
 Here you can find several DEVStone implementations for comparing the main DEVS engines.
+
+Adapted to benchmark Dynamic Structure DEVS
+
+## Running the pypdevs benchmarks
+First select a simulator:
+- classic
+- parallel
+- dynamic
+  - classic
+  - number of models to generate per cycle
+- minimal
+
+Shared parameters:
+- path: where the results will be stored
+- filename: 
+- append: will append the data to the output file instead of overwriting it
+- max width/depth: max model width/depth
+- width/depth step size: 
+  - determines how fast the width/depth increases to the max specified value
+  - the simulation will be run for every width and depth combination
+  - the step size must be a divider of the max width/depth
+  - if not provided the model will run only the max width/depth
+- repetitions: number of times the exact same simulation will be repeated
+- output
+- memory: Measures the memory usage of a simulation.
+- interval: The interval at which measurements are taken.
+
+- model structure: each simulator has its own set of models
+  - LI, HI, HO, HOmod (classic, parallel, dynamic, minimal)
+  - HI2LI, LI2HI, dLI, dHI (dynamic)
+
+For each simulation the parameters and the creation, setup and simulation time are saved to a csv file.
+
+## plotting
+
+## Measuring Memory
+Run the following command, the width and depth step size parameters should be left out, 
+to measure the memory usage of a single simulation, for the same reason the repetitions should be set to one.
+
+```
+uv run testPyPDEVS.py -md 100 -mw 100 -r 1 -O dynamic -ms dLI & PID=$!; python -m procpath record -i 0.1 -f stat -d output.sqlite --stop-without-result "$..children[?(@.stat.pid == $PID)]"
+```
+```
+procpath plot -d output.sqlite -f out.svg -q rss
+```
+

+ 0 - 3
devstone/pythonpdevs-minimal/main.py

@@ -37,7 +37,6 @@ def parse_args():
     parser.add_argument('-e', '--ext-cycles', type=int, default=0, help='Dhrystone cycles executed in external transitions')
     parser.add_argument('-f', '--flatten', action="store_true", help='Activate flattening on model')
     parser.add_argument('-c', '--chained', action="store_true", help='Use chained coordinator')
-    parser.add_argument('-D', '--dynamic', action="store_true", default=False, help='Enables Dynamic Structure DEVS')
     args = parser.parse_args()
 
     if args.model_type not in MODEL_TYPES:
@@ -68,8 +67,6 @@ if __name__ == '__main__':
     engine_setup_time = time.time()
 
     sim.simulate()
-    if args.dynamic:
-        sim.setDSDEVS(True)
     sim_time = time.time()
 
     print("Model creation time: {}".format(model_created_time - start_time))

+ 98 - 63
devstone/testPyPDEVS.py

@@ -4,6 +4,7 @@ import subprocess
 import csv
 import re
 from argparse import ArgumentParser, Action
+from pathlib import Path
 from itertools import product
 from datetime import datetime
 
@@ -27,7 +28,7 @@ TIME_PATTERN = re.compile(
     re.S
 )
 
-def run_devstone(model_type, depth, width, mode="parallel", out=False, *, classic=False, number=1, gen_mode="uniform"):
+def run_devstone(model_type, depth, width, mode, out, memory, *, classic=False, number=1, gen_mode="uniform"):
     """Runs the devstone_driver.py script and returns timing results as floats."""
     SCRIPT_PATH = PyPDEVS
     if mode == "minimal":
@@ -49,27 +50,54 @@ def run_devstone(model_type, depth, width, mode="parallel", out=False, *, classi
         cmd.extend(["-g", gen_mode])
     if mode == "classic":
         cmd.extend(["-C"])
-
-    result = subprocess.run(cmd, capture_output=True, text=True)
-    output = result.stdout.strip()
+    full_cmd = " ".join(cmd)
+
+    if memory:
+        Path("/".join(memory["file"].split("/")[0:-1])).mkdir(parents=True, exist_ok=True)
+        full_cmd = (
+            f"{full_cmd} & "
+            f"PID=$!; "
+            f"python -m procpath record -i {memory['interval']} -f stat -d {memory['file']} "
+            f"--stop-without-result \"$..children[?(@.stat.pid == $PID)]\""
+        )
+
+    result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
+    cout = result.stdout.strip()
     if out:
-        print(output)
+        print(cout)
 
-    errors = result.stderr.strip()
-    match = TIME_PATTERN.search(output)
+    cerr = result.stderr.strip()
+    match = TIME_PATTERN.search(cout)
 
     if match:
         creation, setup, sim = map(float, match.groups())
-        return creation, setup, sim, output
+        return creation, setup, sim
 
     print("Failed to run: ", cmd)
-    print(output)
-    print(errors)
-    return None, None, None, output
+    print(cout)
+    print(cerr)
+    return None, None, None
+
+def defaultArguments(parser):
+    parser.add_argument("-p", "--path", type=str, default="./Results-PyPDEVS", help="Path to the results directory")
+    parser.add_argument("-f", '--filename', type=str, default="", help="Name of the output file")
+    parser.add_argument("-a", "--append", action="store_true", help="Append to the output file instead of overwriting.")
+
+    parser.add_argument("-md", "--max-depth", default=8, type=int, help="Max depth for the generated models")
+    parser.add_argument("-ds", "--depth-stepsize", type=int, help="Determines for which depths a model is generated.")
+
+    parser.add_argument("-mw", "--max-width", default=8, type=int, help="Max width for the generated models")
+    parser.add_argument("-ws", "--width-stepsize", type=int, help="Determines for which depths a model is generated.")
+
+    parser.add_argument("-r", "--repetitions", default=1, type=int, help="The number of times a simulation is repeated")
+    parser.add_argument("-O", "--output", action="store_true", help="Print Simulation Outputs")
+    parser.add_argument("-M", "--memory", action="store_true", help="Measures the memory usage of a simulation.")
+    parser.add_argument("-i", "--interval", default=1.0, type=float, help="The interval at which measurements are taken.")
 
 def Minimal(subparsers):
     minimal = subparsers.add_parser("minimal", help="Minimal DEVS kernel")
     minimal.set_defaults(mode="minimal")
+    defaultArguments(minimal)
     minimal.add_argument(
         "-ms", "--model-structure",
         nargs="+",
@@ -80,6 +108,7 @@ def Minimal(subparsers):
 def Classic(subparsers):
     classic = subparsers.add_parser("classic", help="Classic DEVS Simulator")
     classic.set_defaults(mode="classic")
+    defaultArguments(classic)
     classic.add_argument(
         "-ms", "--model-structure",
         nargs="+",
@@ -90,6 +119,7 @@ def Classic(subparsers):
 def Parallel(subparsers):
     parallel = subparsers.add_parser("parallel", help="Parallel DEVS")
     parallel.set_defaults(mode="parallel")
+    defaultArguments(parallel)
     parallel.add_argument(
         "-ms", "--model-structure",
         nargs="+",
@@ -100,6 +130,7 @@ def Parallel(subparsers):
 def Dynamic(subparsers):
     dynamic = subparsers.add_parser("dynamic", help="Dynamic Structure DEVS")
     dynamic.set_defaults(mode="dynamic")
+    defaultArguments(dynamic)
     dynamic.add_argument(
         "-ms", "--model-structure",
         nargs="+",
@@ -125,21 +156,6 @@ def Dynamic(subparsers):
 
 def parse_args():
     parser = ArgumentParser()
-    parser.add_argument("-p", "--path", type=str, default="./Results-PyPDEVS/", help="Path to the results directory")
-    parser.add_argument("-f", '--filename', type=str, default="", help="Name of the output file")
-    parser.add_argument("-a", "--append", action="store_true", help="Append to the output file instead of overwriting.")
-
-    parser.add_argument("-md", "--max-depth", default=8, type=int, help="Max depth for the generated models")
-    parser.add_argument("-ds", "--depth-stepsize", type=int,
-                        help="Determines for which depths a model is generated.")
-
-    parser.add_argument("-mw", "--max-width", default=8, type=int, help="Max width for the generated models")
-    parser.add_argument("-ws", "--width-stepsize", type=int,
-                        help="Determines for which depths a model is generated.")
-
-    parser.add_argument("-r", "--repetitions", default=1, type=int, help="The number of times a simulation is repeated")
-    parser.add_argument("-O", "--output", action="store_true", help="Print Simulation Outputs")
-
     subparsers = parser.add_subparsers(dest="mode")
     Minimal(subparsers)
     Parallel(subparsers)
@@ -154,6 +170,31 @@ def parse_args():
 
     return args
 
+def run(writer, csvfile, model_type, d, w, args, mem_cmd, extra):
+    """Orchestrates the repeated simulation runs."""
+    for depth in range(d[1], d[0] + 1, d[1]):
+        for width in range(w[1], w[0] + 1, w[1]):
+            for run in range(args.repetitions):
+                creation, setup, sim = run_devstone(
+                    model_type, depth, width,
+                    args.mode, args.output,
+                    mem_cmd,
+                    **extra
+                )
+
+                # Only write to CSV if a writer was provided
+                if writer and creation is not None:
+                    row = {
+                        "model_type": model_type, "depth": depth, "width": width, "run": run,
+                        "creation_time": creation, "setup_time": setup, "simulation_time": sim,
+                    }
+                    if args.mode == "dynamic":
+                        row["number"] = args.number
+                        row["generator_mode"] = args.gen_mode
+                    writer.writerow(row)
+                    csvfile.flush()
+
+
 if __name__ == "__main__":
     args = parse_args()
     print(args)
@@ -167,7 +208,10 @@ if __name__ == "__main__":
 
     fieldnames = ["model_type", "depth", "width", "run",
                   "creation_time", "setup_time", "simulation_time"]
-    if args.mode == "dynamic":
+    mode = args.mode
+    if mode == "dynamic":
+        if args.classic:
+            mode += "-classic"
         fieldnames += ["number", "generator_mode"]
 
     extra = {
@@ -177,41 +221,32 @@ if __name__ == "__main__":
     }
 
     for model_type in args.model_structure:
-        model_filename = f"{args.path}{model_type}"
+        model_time_file = f"{args.path}/Time/{mode}/{model_type}"
+        model_memory_file = f"{args.path}/Memory/{mode}/{model_type}"
+
         if args.filename:
-            model_filename += f"_{args.filename}"
-        model_filename += ".csv"
-
-        file_mode = "a" if args.append else "w"
-        write_header = not args.append or not os.path.exists(model_filename)
-        with open(model_filename, file_mode, newline="") as csvfile:
-            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
-            if write_header:
-                writer.writeheader()
-
-            for depth in range(d[1], d[0] + 1, d[1]):
-                for width in range(w[1], w[0] + 1, w[1]):
-                    for run in range(args.repetitions):
-                        creation, setup, sim, output = run_devstone(
-                            model_type, depth, width,
-                            args.mode, args.output,
-                            **extra
-                        )
-
-                        if creation is not None:
-                            row = {
-                                "model_type": model_type,
-                                "depth": depth,
-                                "width": width,
-                                "run": run,
-                                "creation_time": creation,
-                                "setup_time": setup,
-                                "simulation_time": sim,
-                            }
-                            if args.mode == "dynamic":
-                                row["number"] = args.number
-                                row["generator_mode"] = args.gen_mode
-                            writer.writerow(row)
-                            csvfile.flush()
-        print(f"✅ Saved {model_type} results to {model_filename}")
+            suffix = f"_{args.filename}"
+            model_time_file += suffix
+            model_memory_file += suffix
+
+        model_time_file += ".csv"
+        model_memory_file += ".sqlite"
+
+        mem_cmd = {"interval": args.interval, "file": model_memory_file} if args.memory else {}
+
+        if not args.memory:
+            file_mode = "a" if args.append else "w"
+            write_header = not args.append or not os.path.exists(model_time_file)
+
+            with open(model_time_file, file_mode, newline="") as csvfile:
+                writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
+                if write_header:
+                    writer.writeheader()
+
+                run(writer, csvfile, model_type, d, w, args, mem_cmd, extra)
+            print(f"✅ Saved timing results to {model_time_file}")
+        else:
+            print("📊 Measuring memory (Timing results will not be saved)...")
+            run(None, None, model_type, d, w, args, mem_cmd, extra)
+            print(f"✅ Saved memory database to {model_memory_file}")
 

+ 3 - 1
pyproject.toml

@@ -3,9 +3,11 @@ name = "devstone-comparative"
 version = "0.1.0"
 requires-python = ">=3.11"
 dependencies = [
+    "graphviz>=0.21",
     "matplotlib>=3.10.7",
     "pandas>=2.3.3",
+    "procpath>=1.14.0",
     "seaborn>=0.13.2",
 ]
 
-[tool.uv]
+[tool.uv]

+ 1 - 1
setup.sh

@@ -40,7 +40,7 @@ cd ../..
 
 # COMPILE THE DEVSTONE PROJECT FOR ALL THE PYTHON ENGINES
 # Install PythonPDEVS
-uv pip install ~/Desktop/pythonpdevs/src
+uv pip install pythonpdevs2.4.2/src
 
 # Install xDEVS Python
 cd ./simulators/xdevs.py || exit

+ 57 - 0
uv.lock

@@ -105,6 +105,7 @@ dependencies = [
     { name = "graphviz" },
     { name = "matplotlib" },
     { name = "pandas" },
+    { name = "procpath" },
     { name = "seaborn" },
 ]
 
@@ -113,6 +114,7 @@ requires-dist = [
     { name = "graphviz", specifier = ">=0.21" },
     { name = "matplotlib", specifier = ">=3.10.7" },
     { name = "pandas", specifier = ">=2.3.3" },
+    { name = "procpath", specifier = ">=1.14.0" },
     { name = "seaborn", specifier = ">=0.13.2" },
 ]
 
@@ -174,6 +176,27 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" },
 ]
 
+[[package]]
+name = "importlib-metadata"
+version = "8.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
+]
+
+[[package]]
+name = "jsonpyth"
+version = "0.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pyparsing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6f/7d/f0c9f28064522bca8cf1231237e6b7ab4895e4a3cbcbb0222666af2c14bd/JSONPyth-0.1.3.tar.gz", hash = "sha256:ab9d8e9e72558e7e1e59d598b408d60372e0d95f56d4c4167df2946b9b99da68", size = 6861, upload-time = "2018-11-28T15:30:58.33Z" }
+
 [[package]]
 name = "kiwisolver"
 version = "1.4.9"
@@ -559,6 +582,31 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
 ]
 
+[[package]]
+name = "procpath"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jsonpyth" },
+    { name = "pygal" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/53/0d/45fbc2613205cb790a933109f44451f0ebc2e90a6ac19fdd1732cce471d4/procpath-1.14.0.tar.gz", hash = "sha256:f7ddb1d97e7070f78386302d128cd3386249016ef765ec46e753ddc81f13e197", size = 76107, upload-time = "2025-09-03T12:31:36.798Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/10/99/82065e853aa1319a4f58257a7e553e00715b649d5d4fa0696a4d0d2313fc/Procpath-1.14.0-py3-none-any.whl", hash = "sha256:f3f93ecea4323b6be415297f05cef3eb40056dcc48d28d8aae068770615979fb", size = 83356, upload-time = "2025-09-03T12:31:35.302Z" },
+]
+
+[[package]]
+name = "pygal"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "importlib-metadata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/b6/04176faeb312c84d7f9bc1a810f96ee38d15597e226bb9bda59f3a5cb122/pygal-3.1.0.tar.gz", hash = "sha256:fbdee7351a7423e7907fb8a9c3b77305f6b5678cb2e6fd0db36a8825e42955ec", size = 81006, upload-time = "2025-12-09T10:29:19.587Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0b/4c/2862dd25352fe4b22ec7760d4fa12cb692587cd7ec3e378cbf644fc0d2a8/pygal-3.1.0-py3-none-any.whl", hash = "sha256:4e923490f3490c90c481f4535fa3adcda20ff374257ab9d8ae897f91b632c0bb", size = 130171, upload-time = "2025-12-09T10:29:16.721Z" },
+]
+
 [[package]]
 name = "pyparsing"
 version = "3.2.5"
@@ -620,3 +668,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be76
 wheels = [
     { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
 ]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]