Plan de optimización del banco (simulación Nav2)¶
Plan para hacer el banco más eficiente (CPU/RAM en la Pi4), más ordenado (arranque reproducible) y más mantenible, sin romper lo que ya funciona. Basado en la auditoría de nodos y el perfilado de CPU medidos en la Pi el 20-ago-2026.
Idea rectora: el lenguaje no es la palanca. El trabajo pesado y siempre activo (Nav2) ya está en C++; nuestros nodos Python son pegamento (event-driven, ligeros) o simulación (desechables cuando llegue el robot real). Las ganancias están en qué se streamea, arreglar bugs de CPU, vectorizar la simulación y componer/ordenar el arranque — no en reescribir.
1. Diagnóstico (lo medido)¶
CPU por nodo (100 % = 1 core; la Pi4 tiene 4 cores = 400 %). Panel web conectado.
| Nodo | Lenguaje | reposo % | nav % | Lectura |
|---|---|---|---|---|
| rosbridge | Python (stock) | 82.7 | 87.5 | #1 — coste de serializar sensores a la web |
| sim_sensors | Python (propio, sim) | 46.7 | 28.2 | Ray-cast del láser: pesado |
| goal_relay | Python (propio) | 0.0 | 36.8 | Pico anómalo en nav → sospecha de bug |
| sim_motion | Python (propio, sim) | 23.0 | 29.5 | Integrador a alta frecuencia |
| sim_map_grid | Python (propio, sim) | 18.0 | 17.8 | Alto para republicar a 1 Hz |
| Nav2 (bt+controller+planner+…) | C++ | ~60 | ~80 | Trabajo real de navegación (OK) |
| TOTAL | 237 | 282 | La Pi va al 60–70 %; poco margen |
Hallazgos estructurales:
- Arranque incoherente. 5 de 6 nodos propios corren como scripts sueltos (
python3 …_node.py), no por colcon;sim_sensorsestá en colcon pero se lanza suelto (elinstall/no refleja el código parcheado). No hay launch file único: todo esbringupNAV2.shconnohup. - Sobrecoste de wrappers. Cada
ros2 rundeja vivo un proceso wrapper de ~23 MB además del nodo real. ≈ 7 wrappers ≈ 160 MB de RAM en procesos que no hacen nada. trajectory_navcorre siempre, también en modo Nav2 (ocioso pero suscrito a odometría y ultrasonidos).- Posible remap suelto:
controller_serverescucha/odom, perosim_motionpublica/odometry/filtered(funciona por TF, pero conviene fijarodom_topic). - Pi4 justa (ya se congeló una vez con todo el stack): rosbridge ~77 MB, rosapi ~66 MB, cada C++ Nav2 30–65 MB, cada Python nuestro 44–65 MB.
2. Principios del plan¶
- Medir antes y después con el mismo perfilador (
/proc,nav_cpu_profile.py) y la RAM (ps rss). - No reescribir a C++ los nodos de simulación/pegamento (ROI negativo; se pierde la iteración rápida sin rebuild). Único "a C++" con sentido: cambiar el componente rosbridge por foxglove.
- Trabajar en
main, un cambio cada vez, verificando que el banco sigue navegando de punta a punta.
3. Acciones priorizadas por ROI¶
Tier 1 — Quick wins (alto valor, bajo esfuerzo)¶
O1 · Arreglar el pico de CPU de goal_relay (37 % en nav). ✅ HECHO (20-ago)
- Problema (diagnosticado, no era lo que parecía): el estado se publicaba a solo ~4 Hz, así que
no era sobre-publicación. El pico venía de un busy-spin del executor de rclpy: mientras hay
una acción NavigateToPose activa, una guard condition del goal handle queda siempre "lista"
y rclpy.spin() gira a máxima velocidad sin publicar nada (medido: 35.6 % con 0 mensajes/s).
- Solución: bucle con frecuencia acotada (spin_once + sleep 50 ms) en vez de rclpy.spin()
— atiende feedback/resultado/cancel con <20 ms de retardo pero elimina el giro en vacío. Además
dedupe del estado + redondeo de distancia a 0.1 m (alivia también a rosbridge).
- Resultado medido: 35.6 % → 6.6 % en nav (−81 %), idle 1.8 %. Navegación de punta a punta
verificada (DESTINO ALCANZADO). Commit 2e545f46.
- Hallazgo lateral: restart_nav2 reescribe el yaml desde self.vals, así que un cambio de
marcha_atras/radio_giro_min persiste en disco y cambia el comportamiento por defecto hasta
revertirlo (nos dejó REEDS_SHEPP+reversa, que provocaba abortos "collision ahead"). → ojo en O6/O10.
O2 · Recortar lo que la web streamea (ataca el 79 % de rosbridge). ✅ HECHO (20-ago)
- Problema (medido): rosbridge es un único proceso Python (GIL → tope ~1 core) y estaba al
79 % serializando a JSON todo lo que la web suscribe sin throttle: /odometry/filtered (49 Hz),
/scan (~12 Hz, grande), /global_costmap/costmap (200×200 = 40 000 celdas), /map, /plan.
- Solución: throttle_rate + queue_length:1 en las suscripciones pesadas del cliente web:
odometry 49→10 Hz, scan →5 Hz, costmap →1 Hz, map →0.5 Hz, plan →3 Hz. Los de texto (status,
nav_config) sin throttle. Verificado con un cliente WS de prueba (odometry 49.8→9.7, scan 11.8→5.0).
- Resultado medido: rosbridge 79 % → 50 % (−37 %) con un panel. Commit 3f003f69.
- Gotcha: solo aplica al recargar la página (F5); reconectar no basta (el JS viejo sigue en
memoria). Y cada pestaña/cliente cuenta: 2 pestañas viejas sin throttle mantenían rosbridge alto.
- Palanca extra (si se quiere <40 %): subir el throttle de los dos OccupancyGrid grandes
(/global_costmap/costmap a 2.5–3 s, /map a 5 s) — cambian despacio; o servir la vista costmap
bajo demanda. No hay un culpable único (grids y scan/odom cuestan parecido), hay que apretar en varios.
O3 · sim_map_grid que actúe solo al cambiar el mapa. ✅ HECHO (20-ago)
- Problema (medido): 17.8 % de CPU. No era recalcular (el rasterizado ya era por evento), sino
mantener una suscripción a /odometry/filtered a 49 Hz solo para cachear la pose. Confirmado:
cambiarla por un TransformListener la dejó igual (17→17 %) — el coste es consumir cualquier
topic a 49 Hz, no la deserialización concreta.
- Solución: suscripción efímera a odometría — se crea al llegar un /sim_map, captura una sola
pose para anclar los segmentos, y se destruye (fuera de su callback, desde el timer). En reposo:
cero topics de alta frecuencia consumidos.
- Resultado medido: 17.8 % → ~0.2 % (idle). Mapa anclado y publicado OK (/map=180 celdas) y
navegación de punta a punta verificada (DESTINO ALCANZADO). Commit pendiente.
- Bonus: limpiar el mapa (/sim_map vacío) ya no exige tener odometría.
Tier 2 — Estructura (RAM + mantenibilidad)¶
O4 · Quitar los wrappers ros2 run (−RAM). ✅ HECHO (20-ago)
- Problema (medido): cada ros2 run <pkg> <exe> deja vivo un proceso wrapper Python de ~23 MB
como padre del nodo real. 6 wrappers Nav2 = 138 MB de RAM sin hacer nada.
- Solución: en bringupNAV2.sh y restart_nav2.sh, lanzar los binarios directos
(/opt/ros/humble/lib/nav2_*/…) en vez de ros2 run. Mismos params/remaps/orden; el entorno
(ROS_LOCALHOST_ONLY=1, RMW) se hereda del shell. Los nodos propios ya iban por python3 directo.
- Resultado medido: 0 wrappers, RAM disponible +94 MB (2812→2909 MB), Nav2 active y navegación
de punta a punta OK (DESTINO ALCANZADO). Commit pendiente.
- Paso extra futuro (más RAM, más riesgo): composición (nodos Nav2 en un ComposableNodeContainer
único) — ahorra memoria y coste de descubrimiento, pero es un cambio mayor. No hecho.
O5 · Apagar trajectory_nav en modo Nav2. ✅ HECHO (20-ago)
- Problema (medido): ocioso en modo Nav2 consumía 26 % de CPU por escuchar /odometry/filtered
a 49 Hz + /ultrasound_data (más un wrapper ros2 run de ~23 MB).
- Solución (patrón efímero, como O3, sin romper la web): odometría y ultrasonidos se arman al
recibir waypoints y se sueltan al terminar la ruta; en modo Nav2 (sin ruta) queda ocioso sin
consumir. Además pasado de ros2 run a python3 directo (usa src, quita el wrapper, −RAM).
- Resultado medido: 26 % → 3 % ocioso. Modo RUTA verificado: ruta de 2 m seguida a completar
(state=DONE wp=2/2, robot movió 1.81 m) y sensores soltados después. Commit pendiente.
O6 · Política colcon (coherencia). ✅ HECHO (20-ago) — decidida y documentada
- Decisión: los nodos propios (robocar_pkg) se lanzan por python3 directo desde src/, no
por ros2 run. Motivo: se editan sin recompilar (colcon build en la Pi4 es lento y arriesga
OOM), y así el src/ es la única fuente de verdad. Los nodos stock de Nav2 se lanzan por su
binario directo (O4). Nadie usa ros2 run en el banco.
- Coherencia aplicada: trajectory_nav (que iba por ros2 run) pasó a python3 directo en O5, así
que todos los nodos propios siguen la misma regla. setup.py/entry_points se mantiene por si
algún día se quiere ros2 run, pero el arranque canónico es bringupNAV2.sh (python3 directo).
- Documentado en la cabecera de bringupNAV2.sh.
Tier 3 — CPU pesada (si necesitamos más margen)¶
O7 · Vectorizar el ray-cast de sim_sensors con numpy. ✅ HECHO (20-ago) — con matiz importante
- Acción hecha: el bucle rayo-a-rayo (120 rayos × N segmentos = ~45k llamadas Python/s) sustituido
por una intersección vectorizada (todos los rayos contra todos los segmentos en una pasada numpy).
- Correctitud verificada: /scan idéntico físicamente (obstáculo a 1.0 m → rayo frontal = 1.00,
pared trasera 2.50, ultrasonidos y IR correctos). Bug encontrado y corregido: cast() devolvía
np.float64 → emergency_stop daba np.bool_ y el campo bool del mensaje lo rechazaba (crash);
se fuerza float() nativo.
- Resultado real: escala plano (4 paredes 30.5 % → 19 paredes 31.5 %, +1 %) — con el bucle viejo
se habría disparado. PERO el 31 % de base NO baja: está dominado por la suscripción a
/odometry/filtered a 49 Hz + el publicado (7 msgs × 12 Hz), no por el ray-cast.
- Conclusión: la vectorización es correcta y evita que los planos con muebles disparen el CPU,
pero el número de cabecera de sim_sensors solo baja con O8 (menos Hz de odometría). → O8 es
ahora la palanca clara (toca a sim_sensors, sim_motion consumidores, etc. a la vez).
O8 · Bajar la frecuencia de sim_motion (odom + TF). ✅ HECHO (20-ago) — la palanca de fondo
- Problema (medido): sim_motion publicaba /odometry/filtered + TF a 60 Hz; cada consumidor
(sim_sensors, bt_navigator, controller, sim_motion mismo…) paga la deserializacion a 60 Hz. La
suma de consumidores = 88 % de CPU.
- Solución: sim_motion a 20 Hz (-p rate_hz:=20.0 en el bringup; por encima del control a
20 Hz, con margen). Sin tocar codigo, reversible.
- Resultado medido (estado limpio): suma 88 % → 59.7 % (−28 pts): sim_motion 25→11,
sim_sensors 31.6→19, bt_navigator 14.8→11. Navegacion DESTINO ALCANZADO. Commit pendiente.
- Lección operativa (→ O10/robustez): reiniciar en caliente un nodo que publica TF
(sim_motion = odom→base_link, sim_map_grid = map→odom) corrompe la cache TF/costmap y provoca
"message filter dropping" + "collision ahead". Tras tocar un nodo de TF, hacer bringup completo,
no reinicio suelto.
O9 · (Opcional, mayor) rosbridge → foxglove_bridge (C++).
- El único cambio "a C++" con ROI real. Es un swap de componente, no reescribir código nuestro.
- Riesgo: alto — cambia el protocolo; habría que adaptar el cliente web (hoy usa la API de rosbridge).
Dejar para el final y solo si O2 no basta.
Tier 4 — Correcciones de fondo¶
O10 · Fijar el odom_topic del controller_server a /odometry/filtered (o remapear /odom).
Correctness, no CPU.
O11 · Gestión de carga en la Pi4: nice/cpuset para acotar la simulación, o mover la web
(rosbridge + http.server) a otra máquina para liberar la Pi.
4. Orden recomendado y objetivo¶
Secuencia: ~~O1~~ ✅ → ~~O3~~ ✅ → ~~O2~~ ✅ → ~~O5~~ ✅ → ~~O4~~ ✅ → ~~O7~~ ✅ → ~~O8~~ ✅ → ~~O10~~ ✅ → ~~O6~~ ✅. Backlog: O9 (foxglove), O11 (carga Pi).
Progreso CPU (medido): goal_relay 35.6→6.6 (O1) · sim_map_grid 17.8→0.2 (O3) · rosbridge 79→50 (O2) · trajectory_nav 26→3 (O5) · O8 bajó la suma de consumidores de odometría 88→60 (sim_motion 25→11, sim_sensors 31.6→19, bt_navigator 15→11). En total >120 puntos de CPU. Progreso RAM: O4 quitó 6 wrappers
ros2 run= 138 MB (+94 MB disponibles medidos) + O5 otro. La Pi ha pasado de ir al ~60-70 % (y congelarse) a holgura amplia. O7 además evita que los planos con muebles disparen el CPU (ray-cast vectorizado, escala plano). Insight recurrente: consumir topics de alta frecuencia (49 Hz odom / 20 Hz control) en Python es caro; O8 (bajar la frecuencia desim_motion) aliviaría a varios nodos a la vez → sube prioridad.
Objetivo cuantitativo: bajar de 282 % (nav) a < 200 % de CPU y −150 MB de RAM, manteniendo la navegación de punta a punta. Cada paso se valida con el perfilador antes de seguir.
5. Qué NO vamos a hacer¶
- No portar
sim_motion,sim_sensors,sim_map_grid,goal_relay,nav_confignitrajectory_nava C++: son desechables (simulación) o ya ligeros (pegamento), y perderíamos la edición sin rebuild. El reparto Python/C++ actual es el correcto.
Medido y redactado el 20-ago-2026. Perfilador: nav_cpu_profile.py (lee /proc/<pid>/stat).
Ver también: Mapa de nodos y Control por el LLM.