sunnypilot v2026.002.000

version: sunnypilot v2026.002.000 (staging)
date: 2026-07-21T07:44:02
master commit: 2c334ede443d7391d27575af8c854a095ba702a8
This commit is contained in:
github-actions[bot] committed 2026-07-21 07:44:02 +00:00
commit de77c89cf6
3995 files changed
+1260688

No files matched your search

Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,164 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import matplotlib.pyplot as plt
import os
import sys
import argparse
import numpy as np
import base64
import io
from openpilot.tools.lib.logreader import LogReader, ReadMode
def extract_mem_cpu_data(lr):
times, mems, cpus = [], [], []
start_time = None
for msg in lr:
if msg.which() == 'procLog':
if start_time is None:
start_time = msg.logMonoTime
mem = msg.procLog.mem
mem_usage = (mem.total - mem.available) / mem.total * 100
cpu_usages = [(total - cpu.idle) / total * 100 for cpu in msg.procLog.cpuTimes
if (total := cpu.idle + cpu.user + cpu.system + cpu.nice + cpu.iowait + cpu.irq + cpu.softirq) > 0]
avg_cpu = sum(cpu_usages) / len(cpu_usages) if cpu_usages else 0
times.append((msg.logMonoTime - start_time) / 1e9)
mems.append(mem_usage)
cpus.append(avg_cpu)
return times, mems, cpus
def process_segment(lr):
return [extract_mem_cpu_data(lr)]
def calculate_r_squared(y_true, y_pred):
ss_res = np.sum((y_true - y_pred) ** 2)
ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
return 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
def plot_results(segments, segment_data, route_name):
valid_data = [d for d in segment_data if d and d[0]]
if not valid_data:
print("No valid data to plot")
return
avg_mems = [np.mean(d[1]) for d in valid_data]
avg_cpus = [np.mean(d[2]) for d in valid_data]
valid_segments = [segments[i] for i, d in enumerate(segment_data) if d and d[0]]
height = max(10, 5 + len(valid_segments) * 0.4)
fig1, ax1 = plt.subplots(1, 1, figsize=(12, height), dpi=150)
y_pos = range(len(valid_segments))
ax1.barh([y - 0.2 for y in y_pos], avg_mems, height=0.4, color="dodgerblue", alpha=0.8, label="Avg Mem %")
ax1.barh([y + 0.2 for y in y_pos], avg_cpus, height=0.4, color="green", alpha=0.8, label="Avg CPU %")
for i, (mem, cpu) in enumerate(zip(avg_mems, avg_cpus, strict=True)):
ax1.text(mem, i - 0.2, f"{mem:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
ax1.text(cpu, i + 0.2, f"{cpu:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
ax1.set_yticks(y_pos)
ax1.set_yticklabels([f"Seg {s}" for s in valid_segments])
ax1.set_xlabel("Usage (%)")
ax1.set_title("Average Memory and CPU Usage by Segment")
ax1.legend()
ax1.grid(axis="x", linestyle="--", alpha=0.5)
ax1.invert_yaxis()
fig2, ax2 = plt.subplots(1, 1, figsize=(12, 8), dpi=150)
combined_times, combined_mems, combined_cpus = [], [], []
time_offset = 0.0
for times, mems, cpus in valid_data:
if times:
combined_times.extend([t + time_offset for t in times])
combined_mems.extend(mems)
combined_cpus.extend(cpus)
time_offset += max(times)
ax2.plot(combined_times, combined_mems, color="red", label="Memory Usage", alpha=0.6)
ax2.plot(combined_times, combined_cpus, color="blue", label="CPU Usage", alpha=0.6)
warmup_sec = 60
if len(combined_times) > 1 and combined_times[-1] > warmup_sec:
mask = np.array(combined_times) > warmup_sec
x_reg = np.array(combined_times)[mask]
y_mem_reg = np.array(combined_mems)[mask]
slope_mem, intercept_mem = np.polyfit(x_reg, y_mem_reg, 1)
trend_mem = slope_mem * x_reg + intercept_mem
r2_mem = calculate_r_squared(y_mem_reg, trend_mem)
ax2.plot(x_reg, trend_mem, color="darkred", linestyle="--", linewidth=2.5,
label=f"Mem Trend (Slope: {slope_mem:.4f} %/s, R²: {r2_mem:.2f})")
y_cpu_reg = np.array(combined_cpus)[mask]
slope_cpu, intercept_cpu = np.polyfit(x_reg, y_cpu_reg, 1)
trend_cpu = slope_cpu * x_reg + intercept_cpu
r2_cpu = calculate_r_squared(y_cpu_reg, trend_cpu)
ax2.plot(x_reg, trend_cpu, color="navy", linestyle="--", linewidth=2.5,
label=f"CPU Trend (Slope: {slope_cpu:.4f} %/s, R²: {r2_cpu:.2f})")
ax2.set_xlabel("Time (s)")
ax2.set_ylabel("Usage (%)")
ax2.set_title("Memory and CPU Usage Over Time")
ax2.legend(loc='lower left', fontsize='small', framealpha=0.9)
ax2.grid(True, linestyle="--", alpha=0.5)
buffer1 = io.BytesIO()
fig1.savefig(buffer1, format='webp', bbox_inches='tight', pad_inches=1.0)
buffer1.seek(0)
img1 = base64.b64encode(buffer1.getvalue()).decode()
buffer2 = io.BytesIO()
fig2.savefig(buffer2, format='webp', bbox_inches='tight', pad_inches=1.0)
buffer2.seek(0)
img2 = base64.b64encode(buffer2.getvalue()).decode()
filename = f"memory_usage_{route_name}.html"
save_path = os.path.join(os.path.dirname(__file__), "plots", filename)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
html_template = (
"<style>body{font-family:Arial,sans-serif;margin:20px}" +
"h1,h2,h3{text-align:center;margin:5px 0}h2{margin-bottom:10px}" +
"img{width:100%;max-width:800px;height:auto;display:block;margin:0 auto}</style>" +
f"<h1>Memory Profile Report</h1><h3>Route: {route_name.replace('_', '/')}</h3>" +
f"<img src='data:image/webp;base64,{img1}'>" +
f"<img src='data:image/webp;base64,{img2}'>"
)
plt.close(fig1)
plt.close(fig2)
with open(save_path, "w") as f:
f.write(html_template)
print(f"Report saved to {save_path}")
def main():
parser = argparse.ArgumentParser(description='Extract memory usage from route logs.')
parser.add_argument('route_or_segment_name', help='Route or segment name from comma connect')
args = parser.parse_args()
try:
print(f"Fetching logs for {args.route_or_segment_name}")
lr = LogReader(args.route_or_segment_name, default_mode=ReadMode.QLOG)
segment_data = lr.run_across_segments(24, process_segment)
segments = list(range(len(segment_data)))
route_name = args.route_or_segment_name.replace('/', '_')
plot_results(segments, segment_data, route_name)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import argparse
import os
import shutil
import subprocess
import sys
import requests
from openpilot.tools.lib.route import Route
def get_segments(source, route_id, camera, seg_range):
if "@" in source or "comma-" in source or "sunny-" in source: # SSH
if not route_id:
raise ValueError("route_id required for SSH")
cmd = ["ssh", source, f"ls -d /data/media/0/realdata/{route_id.split('--')[0]}--*"]
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode("utf-8").strip()
return [{
"type": "ssh",
"host": source,
"src": os.path.join(path, camera),
"num": int(path.split("--")[-1])
} for path in sorted(output.split("\n"), key=lambda x: int(x.split("--")[-1])) if path]
else: # URL
route = Route(route_id)
cameras = [camera]
if camera == "fcamera.hevc":
cameras.extend([c for c in ["ecamera.hevc", "qcamera.ts"] if c != camera])
for cam in cameras:
attr_name = "camera_paths" if cam == "fcamera.hevc" else f"{cam.split('.')[0]}_paths"
paths = getattr(route, attr_name)()
if any(paths):
return [{"type": "url", "src": url, "num": idx, "cam": cam} for idx, url in enumerate(paths) if url]
raise ValueError(f"No footage found for {route_id}")
def download(job, out_dir):
destination = os.path.join(out_dir, f"{job['num']}_{os.path.basename(job.get('cam', job.get('src')))}")
if os.path.exists(destination) and os.path.getsize(destination) > 0:
return destination
print(f"Downloading segment {job['num']}")
if job["type"] == "ssh":
subprocess.check_call(["scp", f"{job['host']}:{job['src']}", destination])
else:
with requests.get(job["src"], stream=True) as r:
r.raise_for_status()
with open(destination, 'wb') as f:
shutil.copyfileobj(r.raw, f)
return destination
def mux(files, output_file, codec):
list_filename = f"{output_file}.list.txt"
with open(list_filename, 'w') as f:
f.write('\n'.join([f"file '{os.path.abspath(name)}'" for name in files]))
try:
cmd = [
"ffmpeg", "-y", "-probesize", "100M", "-analyzeduration", "100M", "-f", "concat",
"-safe", "0", "-r", "20", "-i", list_filename, "-c", "copy", "-tag:v", codec, output_file
]
subprocess.check_call(cmd)
print(f"Saved: {output_file} ({os.path.getsize(output_file) / 1048576:.2f} MB)")
if sys.platform == "darwin":
subprocess.run(["open", "-R", output_file])
finally:
if os.path.exists(list_filename):
os.remove(list_filename)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("route_id", nargs='?')
parser.add_argument("--output", "-o", default="output.mp4")
parser.add_argument("--camera", "-c", default="fcamera.hevc")
parser.add_argument("--keep-segments", action="store_true")
args = parser.parse_args()
try:
route_id_str = args.route_id or args.source
segment_range = None
if "/" in route_id_str:
route_id_str, range_str = route_id_str.rsplit("/", 1)
if ":" in range_str or range_str.isdigit():
segment_range = range_str
is_ssh = "@" in args.source or "comma-" in args.source or "sunny-" in args.source
if not is_ssh and len(route_id_str.split("--")) > 2:
route_id_str = "--".join(route_id_str.split("--")[:2])
segments = get_segments(args.source, route_id_str, args.camera, segment_range)
if segment_range:
if ":" in segment_range:
parts = segment_range.split(":")
start_idx = int(parts[0]) if parts[0] else None
end_idx = int(parts[1]) if parts[1] else None
else:
start_idx = int(segment_range)
end_idx = start_idx + 1
segments = [
segment for segment in segments
if (start_idx is None or segment['num'] >= start_idx) and (end_idx is None or segment['num'] < end_idx)
]
download_dir = f"{route_id_str}_segments"
os.makedirs(download_dir, exist_ok=True)
downloaded_files = sorted(
[download(segment, download_dir) for segment in segments],
key=lambda x: int(os.path.basename(x).split("_")[0])
)
camera_name = segments[0].get('cam', args.camera)
codec = "hvc1" if camera_name.endswith("hevc") else "avc1"
mux(downloaded_files, f"{route_id_str}--{args.output}", codec)
if not args.keep_segments:
shutil.rmtree(download_dir)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()