|
|
@@ -5,6 +5,7 @@ import matplotlib.pyplot as plt
|
|
|
import argparse
|
|
|
from pathlib import Path
|
|
|
from itertools import cycle
|
|
|
+from model_properties import model_properties
|
|
|
|
|
|
# Standard Linux page size in bytes
|
|
|
PAGE_SIZE = 4096
|
|
|
@@ -16,170 +17,121 @@ SIMULATOR_MAPPING = {
|
|
|
"dynamic": "(d)",
|
|
|
"dynamic-classic": "(dc)",
|
|
|
"Memory": "(a)",
|
|
|
+ "Agents": "(a)",
|
|
|
}
|
|
|
|
|
|
-TIME_UNITS = {
|
|
|
- "ms": 0.001,
|
|
|
- "s": 1,
|
|
|
- "m": 60,
|
|
|
- "h": 3600,
|
|
|
-}
|
|
|
-
|
|
|
+TIME_UNITS = {"ms": 0.001, "s": 1, "m": 60, "h": 3600}
|
|
|
MEMORY_UNITS = {
|
|
|
- "Bytes": 1,
|
|
|
- "KB": 1000,
|
|
|
- "MB": 1000 ** 2,
|
|
|
- "GB": 1000 ** 3,
|
|
|
- "KiB": 1024,
|
|
|
- "MiB": 1024 ** 2,
|
|
|
- "GiB": 1024 ** 3
|
|
|
+ "Bytes": 1, "KB": 1000, "MB": 1000 ** 2, "GB": 1000 ** 3,
|
|
|
+ "KiB": 1024, "MiB": 1024 ** 2, "GiB": 1024 ** 3
|
|
|
}
|
|
|
|
|
|
|
|
|
-def get_memory_trace(db_path):
|
|
|
- """
|
|
|
- Connects to the SQLite database and retrieves the memory trace
|
|
|
- for processes named 'python3'.
|
|
|
- """
|
|
|
-
|
|
|
- if not db_path.exists():
|
|
|
- print(f"Error: File {db_path} does not exist.")
|
|
|
- return None
|
|
|
-
|
|
|
- if db_path.stat().st_size == 0:
|
|
|
- print(f"Error: File {db_path} is empty (0 bytes).")
|
|
|
- return None
|
|
|
-
|
|
|
+def normalize_database(db_path):
|
|
|
+ """Normalizes the database so that every process starts at 0 RSS (Net Growth)."""
|
|
|
try:
|
|
|
with sqlite3.connect(db_path) as conn:
|
|
|
cursor = conn.cursor()
|
|
|
-
|
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='record';")
|
|
|
- if not cursor.fetchone():
|
|
|
- print(f"Warning: Table 'record' not found in {db_path} Skipping.")
|
|
|
- return None
|
|
|
-
|
|
|
- # Query data: timestamp and RSS for python3 processes
|
|
|
- query = """
|
|
|
- SELECT ts, stat_rss
|
|
|
- FROM record
|
|
|
- WHERE stat_comm IN ('python3', 'julia')
|
|
|
- ORDER BY ts ASC \
|
|
|
- """
|
|
|
- df = pd.read_sql_query(query, conn)
|
|
|
+ if not cursor.fetchone(): return
|
|
|
+
|
|
|
+ cursor.execute("PRAGMA table_info(record)")
|
|
|
+ columns = [info[1] for info in cursor.fetchall()]
|
|
|
+
|
|
|
+ if 'raw_rss' not in columns:
|
|
|
+ cursor.execute("ALTER TABLE record ADD COLUMN raw_rss INTEGER")
|
|
|
+ cursor.execute("UPDATE record SET raw_rss = stat_rss")
|
|
|
+ cursor.execute("""
|
|
|
+ UPDATE record
|
|
|
+ SET stat_rss = raw_rss - (SELECT r2.raw_rss
|
|
|
+ FROM record r2
|
|
|
+ WHERE r2.stat_pid = record.stat_pid
|
|
|
+ ORDER BY r2.record_id ASC
|
|
|
+ LIMIT 1)
|
|
|
+ """)
|
|
|
+ conn.commit()
|
|
|
+ print(f"Normalized {db_path.name}: Processes now start at 0 (Net Growth).")
|
|
|
+ except sqlite3.Error as e:
|
|
|
+ print(f"Error normalizing {db_path}: {e}")
|
|
|
|
|
|
- if df.empty:
|
|
|
- print(f"Warning: No 'python3' or 'julia' records found in {db_path}.")
|
|
|
- return None
|
|
|
|
|
|
- return df
|
|
|
+def get_memory_trace(db_path):
|
|
|
+ if not db_path.exists() or db_path.stat().st_size == 0: return None
|
|
|
+ normalize_database(db_path)
|
|
|
+ try:
|
|
|
+ with sqlite3.connect(db_path) as conn:
|
|
|
+ query = "SELECT ts, stat_rss FROM record WHERE stat_comm IN ('python3', 'julia') ORDER BY ts ASC"
|
|
|
+ df = pd.read_sql_query(query, conn)
|
|
|
+ return df if not df.empty else None
|
|
|
except sqlite3.Error as e:
|
|
|
- print(f"Error reading {db_path}: {e}")
|
|
|
return None
|
|
|
|
|
|
|
|
|
def process_data(df, mem_unit, time_unit):
|
|
|
- """
|
|
|
- Converts timestamps to relative time and RSS to the specified units.
|
|
|
- """
|
|
|
- # Convert timestamps to relative time in seconds first
|
|
|
start_time = df["ts"].iloc[0]
|
|
|
- relative_seconds = df["ts"] - start_time
|
|
|
-
|
|
|
- # Scale time to the target unit
|
|
|
- df["relative_time"] = relative_seconds / TIME_UNITS[time_unit]
|
|
|
-
|
|
|
- # Convert RSS from Pages to the target memory unit
|
|
|
- # stat_rss is in pages (usually 4KB)
|
|
|
+ df["relative_time"] = (df["ts"] - start_time) / TIME_UNITS[time_unit]
|
|
|
df["memory"] = (df["stat_rss"] * PAGE_SIZE) / MEMORY_UNITS[mem_unit]
|
|
|
-
|
|
|
return df
|
|
|
|
|
|
|
|
|
-def plot_traces(files, mem_unit="MB", time_unit="s", path="", name="", title=None, bw=False):
|
|
|
+def plot_traces(files, width=None, depth=None, mem_unit="MB", time_unit="s", path="", name="", title=None, bw=False):
|
|
|
plt.figure(figsize=(10, 6))
|
|
|
+ style_cycle = cycle(zip(cycle(plt.rcParams['axes.prop_cycle'].by_key()['color'] if not bw else ['black']),
|
|
|
+ cycle(['-', '--', '-.', ':'])))
|
|
|
+ plotted_any, models, main_model_type = False, [], None
|
|
|
|
|
|
- # Styling
|
|
|
- colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] if not bw else ['black']
|
|
|
- line_styles = ['-', '--', '-.', ':']
|
|
|
-
|
|
|
- style_cycle = cycle(zip(cycle(colors), cycle(line_styles)))
|
|
|
-
|
|
|
- plotted_any = False
|
|
|
-
|
|
|
- models = []
|
|
|
for db_file in files:
|
|
|
file = Path(db_file).resolve()
|
|
|
df = get_memory_trace(file)
|
|
|
- if df is None:
|
|
|
- continue
|
|
|
- models.append(file.stem)
|
|
|
- df = process_data(df, mem_unit, time_unit)
|
|
|
+ if df is None: continue
|
|
|
|
|
|
+ model_type = file.stem
|
|
|
+ models.append(model_type)
|
|
|
+ if main_model_type is None: main_model_type = model_type
|
|
|
+ df = process_data(df, mem_unit, time_unit)
|
|
|
color, linestyle = next(style_cycle)
|
|
|
- label = file.stem + " " + SIMULATOR_MAPPING[file.parent.name]
|
|
|
-
|
|
|
- plt.plot(
|
|
|
- df["relative_time"],
|
|
|
- df["memory"],
|
|
|
- label=label,
|
|
|
- color=color,
|
|
|
- linestyle=linestyle,
|
|
|
- linewidth=1.5
|
|
|
- )
|
|
|
+ label = f"{model_type} {SIMULATOR_MAPPING.get(file.parent.name, f'({file.parent.name})')}"
|
|
|
+ plt.plot(df["relative_time"], df["memory"], label=label, color=color, linestyle=linestyle, linewidth=1.5)
|
|
|
plotted_any = True
|
|
|
|
|
|
- if not plotted_any:
|
|
|
- print("No valid data found to plot.")
|
|
|
- return
|
|
|
+ if not plotted_any: return
|
|
|
|
|
|
- # Labels and Title
|
|
|
- plt.xlabel(f"Time ({time_unit})")
|
|
|
- plt.ylabel(f"Resident Set Size ({mem_unit})")
|
|
|
+ plt.xlabel(f"Time ({time_unit})");
|
|
|
+ plt.ylabel(f"Resident Set Size Net Growth ({mem_unit})")
|
|
|
|
|
|
- unique_models = "|".join(set(models))
|
|
|
- if not name:
|
|
|
- final_name = f"{unique_models}"
|
|
|
- else:
|
|
|
- final_name = name
|
|
|
+ if main_model_type and width and depth:
|
|
|
+ props = model_properties(main_model_type, width, depth)
|
|
|
+ atomics = int(props.get("Atomic Models", 0))
|
|
|
+ events = int(props.get("Events", 0))
|
|
|
|
|
|
- if title:
|
|
|
- plt.title(title)
|
|
|
+ plt.suptitle(f"Memory Usage over Time (Net Growth): {main_model_type}", fontsize=12)
|
|
|
+ plt.title(f"Width: {width}, Depth: {depth} | #Atomic Models: {atomics:,} | #Events: {events:,}", fontsize=10)
|
|
|
else:
|
|
|
- plt.title("Memory Usage over Time")
|
|
|
+ plt.title("Memory Usage over Time (Net Growth)", fontsize=12)
|
|
|
|
|
|
plt.legend()
|
|
|
plt.grid(True, linestyle=':', alpha=0.6)
|
|
|
|
|
|
- # Save output
|
|
|
- script_dir = Path(__file__).resolve().parent
|
|
|
- output_dir = script_dir / "MemoryPlots"
|
|
|
- if path:
|
|
|
- output_dir = output_dir / path
|
|
|
- output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
- output_file = output_dir / f"{final_name}.png"
|
|
|
-
|
|
|
- plt.tight_layout()
|
|
|
- plt.savefig(output_file, dpi=300)
|
|
|
- print(f"Saved plot to {output_file}")
|
|
|
+ out_dir = Path(__file__).resolve().parent / "MemoryPlots" / path
|
|
|
+ out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
+ plt.tight_layout();
|
|
|
+ plt.savefig(out_dir / f"{name or '|'.join(set(models)) + '_memory'}.png", dpi=300)
|
|
|
|
|
|
|
|
|
def main():
|
|
|
- parser = argparse.ArgumentParser(description="Plot Resident Set Size (RSS) from SQLite memory traces.")
|
|
|
-
|
|
|
- parser.add_argument("files", nargs="+", type=str, help="SQLite database files to plot.")
|
|
|
- parser.add_argument("-mu", "--memory-unit", type=str, default="MB", choices=MEMORY_UNITS.keys(), help="Unit for memory usage (default: MB).")
|
|
|
- parser.add_argument("-tu", "--time-unit", type=str, default="s", choices=TIME_UNITS.keys(), help="Unit for the time scale (default: s).")
|
|
|
-
|
|
|
- parser.add_argument("-p", "--path", type=str, default="", help="Output image file path.")
|
|
|
- parser.add_argument("-n", "--name", type=str, default="", help="Output image file name.")
|
|
|
- parser.add_argument("-t", "--title", type=str, help="Custom title for the plot.")
|
|
|
- parser.add_argument("--bw", action="store_true", help="Black and white mode.")
|
|
|
-
|
|
|
+ parser = argparse.ArgumentParser()
|
|
|
+ parser.add_argument("files", nargs="+")
|
|
|
+ parser.add_argument("-w", "--width", type=int)
|
|
|
+ parser.add_argument("-d", "--depth", type=int)
|
|
|
+ parser.add_argument("-mu", "--memory-unit", default="MB")
|
|
|
+ parser.add_argument("-tu", "--time-unit", default="s")
|
|
|
+ parser.add_argument("-p", "--path", default="")
|
|
|
+ parser.add_argument("-n", "--name", default="")
|
|
|
+ parser.add_argument("-t", "--title")
|
|
|
+ parser.add_argument("--bw", action="store_true")
|
|
|
args = parser.parse_args()
|
|
|
-
|
|
|
- plot_traces(args.files, args.memory_unit, args.time_unit, args.path, args.name, args.title, args.bw)
|
|
|
+ plot_traces(args.files, args.width, args.depth, args.memory_unit, args.time_unit, args.path, args.name, args.title,
|
|
|
+ args.bw)
|
|
|
|
|
|
|
|
|
-if __name__ == "__main__":
|
|
|
- main()
|
|
|
+if __name__ == "__main__": main()
|