Analisis profundo de comunicaciones simulaciones ↔ app

Sesion s32 · 2026-04-11 · Revision completa post-ClipBridgeService

1. Mapa completo de flujos de datos

La plataforma LRU tiene 6 productores de datos y 8 consumidores, todos convergiendo en el DataPlane como bus central. Pero la convergencia no es total: hay caminos laterales que crean complejidad innecesaria.

┌─────────────────────────────────────┐ │ PRODUCTORES │ └─────────────────────────────────────┘ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ SimService│ │ MQTT hw │ │ MQTT sim │ │ Slider │ │ Registry │ │ Signal │ │ (sim1) │ │ (F3) │ │ (F3) │ │ (UI) │ │ (ORC) │ │ Engine │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ ┌────┴──────────────┴──────────────┴──────────────┘ │ │ │ │ │ ControlBus.allows() │ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ D A T A P L A N E │ │ Float64Array · 512 slots · 137+ canales │ │ sim/* (ungated) │ hw/* (gated by ControlBus) │ └──────┬──────────┬───────────┬───────────┬──────────┬──────────┬────────────────┘ │ │ │ │ │ │ subscribe() subscribe() hooks subscribe() hooks subscribe() │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ClipBridge│ │ Signal │ │Signals │ │ Mixer │ │ Matrix │ │ Chart │ │ Service │ │ Engine │ │ Panel │ │ Panel │ │ Tab │ │ Panel │ └────┬─────┘ └─────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ realToAdc() + JSON.stringify │ CustomEvent DOM │ ▼ ┌──────────┐ │ IB_03 │──→ JSON.parse ──→ change1() ──→ Clip properties │ (legacy) │ └──────────┘
Observacion clave

El DataPlane es la fuente de verdad universal para datos numericos. Sin embargo, el camino DataPlane → clips (Adobe Animate) sigue siendo el mas complejo y costoso porque requiere tres transformaciones innecesarias: escala real→ADC, serializacion JSON, y transporte via DOM events.

2. Inventario de mecanismos de transporte

Actualmente coexisten 7 mecanismos de comunicacion diferentes dentro de F1:

#MecanismoUsado porOverheadJustificado?
1 DataPlane.subscribe() ClipBridgeService, SignalEngine, paneles React (via hooks) Bajo dirty-flags + throttle 20fps Si — bus central optimizado
2 CustomEvent DOM ClipBridgeService → IB_03 (hwObjId_online) Medio DOM dispatch + bubble + JSON Parcial — IB_03 legacy lo requiere
3 React Context ORC (hwObjs/swObjs state), Sim1Context, Datos01Context Alto re-renders en cascada Parcial — necesario para UI, excesivo para datos RT
4 DebugBus (EventEmitter) Debug1, 11 canales Bajo Si — solo debug
5 RouterService.subscribe() RouterModeChip, ControlBus Bajo Si — pocos suscriptores, baja frecuencia
6 window.__sim1_event__() Eventos viewer-search, sim1-pip Bajo Si — comandos esporadicos
7 MQTT via RabbitMQ (F3) CommsManager → ORC → DataPlane Alto red + JSON + dispatch Si — unica via para hardware real
Conclusion

Los mecanismos 1, 4, 5, 6 y 7 estan bien justificados. Los problematicos son el #2 (CustomEvent DOM) y el #3 (React Context para datos RT). Ambos introducen overhead innecesario en el hot path.

3. Analisis pipeline por pipeline

3a. Pipeline Sim → clips (Adobe Animate)

Estado actual (post-s31): 5 eslabones

SimService.tick() │ ▼ fromRecord(record, 'sim1') ← escribe Float64 en DataPlane DataPlane.onFlush() │ ▼ subscribe('*') callback ← dirty-flags, 20fps throttle ClipBridgeService.onFlush() │ ├─ agrupa canales dirty por hwObjId ├─ realToAdc(value, sensorDef) ← transformacion por sensor ├─ JSON.stringify(adcMap) ← serializacion │ ▼ document.dispatchEvent(new CustomEvent(hwObjId + '_online', ...)) IB_03.listener │ ├─ JSON.parse(e.detail.data) ← deserializacion ├─ for each port: change1(port, adcValue) │ ▼ clip.gotoAndStop(frame) ← actualizacion visual
Problemas identificados

P1. Doble serializacion: JSON.stringify en ClipBridgeService + JSON.parse en IB_03. Esto existe solo porque CustomEvent.detail transporta strings. Si IB_03 leyera directamente del DataPlane, ambas operaciones desaparecen.

P2. Escala real→ADC: La conversion realToAdc() existe porque IB_03 espera valores ADC 0-4095. Pero los clips originales de Adobe Animate mapean internamente frame = f(adcValue). Si los clips supieran interpretar valores reales directamente (altitud en pies, temperatura en °C), la conversion ADC desaparece.

P3. CustomEvent como transporte: El DOM event system esta disenado para interaccion de usuario, no para streams de datos a 20fps. Cada dispatch implica: crear objeto Event, buscar listeners en el DOM tree, invocar callbacks. Un callback directo es 10-50× mas rapido.

P4. Agrupacion por hwObjId: ClipBridgeService agrupa canales dirty por hwObjId para enviar un solo evento por hwObj. Esto es una optimizacion sobre el modelo anterior, pero sigue siendo un paso innecesario si IB_03 pull del DataPlane.

Coste CPU estimado por frame (20fps × N hwObjs visibles)

OperacionCoste unitario× 3 hwObjs visibles× 20fpsTotal/s
realToAdc() × ~8 sensores~0.5μs1.5μs30μs~30μs/s
JSON.stringify(adcMap)~3μs9μs180μs~180μs/s
CustomEvent dispatch~2μs6μs120μs~120μs/s
JSON.parse en IB_03~3μs9μs180μs~180μs/s
change1() × ~8 ports~1μs3μs60μs~60μs/s
Total~570μs/s

570μs/s no es critico en un desktop, pero en un tablet o movil con 6+ hwObjs visibles puede subir a ~1.2ms/s, y compite con el presupuesto del RAF frame (16.6ms).

3b. Pipeline MQTT hardware → clips

Estado actual: 7 eslabones

ESP32 firmware │ ▼ MQTT publish (JSON) ← F3: red WiFi RabbitMQ broker │ ▼ WebSocket relay MqttAdapter.onMessage() │ ▼ JSON.parse(payload) ← parse #1 ORC.handleMqttEvent() │ ├─ ControlBus.allows(hwObjId, 'mqtt-hw') ← gate ├─ DataPlane.fromRecord(record, 'mqtt-hw') ← escribe Float64 │ ▼ (ya NO despacha _online para sim1_* — s31) │ ▼ DataPlane.onFlush → ClipBridgeService ← mismo pipeline 3a desde aqui ... ▼ JSON.stringify + CustomEvent + JSON.parse + change1()
Analisis

El tramo F3 (ESP32 → RabbitMQ → WebSocket → MqttAdapter) es irreducible: necesitamos la red. Pero una vez los datos llegan a MqttAdapter.onMessage(), la cadena tiene 2× JSON.parse (una en MqttAdapter, otra en IB_03) y 1× JSON.stringify (en ClipBridgeService). El parse de MqttAdapter es inevitable (viene de la red como texto). Pero el stringify+parse del tramo ClipBridge→IB_03 es eliminable.

3c. Pipeline DataPlane → paneles React

Estado actual: 2 eslabones (optimo)

DataPlane.onFlush() │ ▼ subscribe() callback con dirty-flags useDataPlaneValue / useDataPlaneValues │ ▼ setState() → re-render del componente
Veredicto

Bien disenado Este pipeline es limpio: Float64 directo, sin serializacion, throttled a 20fps. Los hooks useDataPlaneValue y useDataPlaneValues solo causan re-render cuando el valor cambia (dirty-flag). Es el modelo a seguir.

3d. Pipeline SignalEngine → sensores

Estado actual: 3 eslabones

SimService.tick(dt) o demoInterval │ ▼ escribe valores base en DataPlane SignalEngine.tick(dt) │ ├─ lee valor base de DataPlane.readBySensorId() ├─ aplica cadena de nodos (filter/inject/modulate) │ ▼ DataPlane.writeBySensorId(sensorId, processedValue) │ ▼ (consumidores leen el valor procesado via DataPlane.subscribe)
Veredicto

Bien disenado SignalEngine es un singleton vanilla TS que lee y escribe directamente en el DataPlane sin intermediarios. Zero-copy para floats. Los 45 nodos de procesamiento operan en el mismo espacio de memoria. Sin serializacion.

3e. Pipeline Slider → DataPlane

Estado actual: 2 eslabones

LruSlider.onChange(value) │ ▼ DataPlane.writeBySensorId(sensorId, value, 'slider') │ ▼ ControlBus.allows() gate (solo routerMode Loc)
Veredicto

Optimo Escritura directa. Sin intermediarios.

3f. Pipeline Escenarios → simulacion

Estado actual: desactivado (s27)

EscenariosContext (desactivado) │ ▼ cargar preset → setRegistryState() → ORC state update │ ▼ React Context re-render masivo │ ▼ ORC despacha _online events / DataPlane writes
Problema

Cuando se reactive, EscenariosContext necesita evitar el path React Context → ORC dispatch. Los datos de escenarios (valores nominales por sensor, fallos) deberian escribirse directamente al DataPlane y SimService, sin pasar por el ORC.

4. Redundancias y anti-patrones detectados

#Anti-patronDondeImpactoSeveridad
A1 Serializacion innecesaria: JSON.stringify + JSON.parse entre dos modulos in-memory (F1) ClipBridgeService → IB_03 ~360μs/s (3 hwObjs × 20fps) Alta
A2 DOM como bus de datos: CustomEvents para stream de datos a 20fps ClipBridgeService → IB_03 ~120μs/s + GC pressure (objetos Event) Alta
A3 Escala ADC redundante: conversion real→ADC que solo existe porque IB_03 espera ADC ClipBridgeService.realToAdc() ~30μs/s (bajo, pero complejidad innecesaria) Media
A4 IB_03 monolitico: 2033 lineas de JS legacy, mezcla responsabilidades (deteccion de clip, binding, update, fallback DOM) public/InstrumentBridge_03.js Deuda tecnica, dificil de optimizar Media
A5 ORC como intermediario MQTT: ORC recibe MQTT events, los procesa en React Context, y escribe al DataPlane. El hot path pasa por React. ObjectRegistryContext.handleMqttEvent() React re-renders + dispatch overhead en cada mensaje MQTT Media
A6 Dos mundos de suscripcion: DataPlane.subscribe (vanilla) vs React hooks (useDataPlaneValue). No es un bug, pero implica dos patrones mentales. Toda la app Complejidad cognitiva Baja
A7 channelMap indirecto: ORC mantiene channelMap (hwObjId→sensorId→channelIdx) que duplica info del DataPlane registry. ORC + DataPlane Estado duplicado, posible desincronizacion Baja

5. Costes de CPU medidos y estimados

Estimaciones basadas en el analisis de s31 y benchmarks tipicos de V8:

~570μs
CPU por segundo
pipeline clips (3 hwObjs)
~180μs
Solo JSON
stringify+parse
~120μs
Solo DOM
CustomEvents
0μs
Si IB_03 lee
del DataPlane
PipelineFrecuenciaCoste/frameCoste/sEliminable?
SimService → DataPlane 60fps ~5μs ~300μs No (core loop)
DataPlane → ClipBridge → IB_03 20fps ~28μs (3 hwObjs) ~570μs Si: -300μs con IB_04
DataPlane → React hooks 20fps ~2μs ~40μs No (ya optimo)
SignalEngine.tick() 60fps ~10μs (5 nodos activos) ~600μs No (DSP necesario)
MQTT → ORC → DataPlane ~10-50fps variable ~15μs ~300μs Parcial: ORC extraible

6. Propuestas de simplificacion (ordenadas por impacto/esfuerzo)

P1. IB_04 — Bridge directo DataPlane (impacto: alto · esfuerzo: medio)

Concepto

Crear InstrumentBridge_04.js que lea directamente del DataPlane en vez de escuchar CustomEvents. Esto elimina de un golpe los anti-patrones A1 (JSON), A2 (DOM events) y A3 (escala ADC).

Pipeline propuesto: 3 eslabones

SimService.tick() / MQTT │ ▼ DataPlane.write() ← escribe Float64 DataPlane (Float64Array) │ ▼ IB_04.pull() ← RAF-synced, lee Float64 directamente │ ├─ realToFrame(value, sensorDef) ← real → frame directo (sin paso por ADC) │ ▼ clip.gotoAndStop(frame) ← actualizacion visual

Que cambia

AspectoIB_03 actualIB_04 propuesto
Fuente de datos CustomEvent (push, JSON string) DataPlane.read() (pull, Float64)
Serializacion JSON.stringify + JSON.parse Ninguna (Float64 directo)
Escala real → ADC → frame (2 pasos) real → frame (1 paso)
Timing Push 20fps desde ClipBridgeService Pull synced con RAF del clip (autoregulado)
Backpressure IntersectionObserver externo Nativo: no lee si clip no visible
Dependencia ClipBridgeService Necesario Eliminable (IB_04 es autonomo)

Estrategia de migracion gradual

IB_04 puede coexistir con IB_03. Cada clip declara que bridge usa (data-bridge="ib04"). Migracion clip a clip sin romper nada. ClipBridgeService se mantiene para clips IB_03 legacy, y se elimina cuando todos migren.

Funcion realToFrame()

// Convierte valor real directo a frame del clip
// Elimina el paso intermedio ADC 0-4095
function realToFrame(value, sensorDef) {
  const { min, max, totalFrames } = sensorDef;
  const normalized = (value - min) / (max - min);  // 0..1
  const clamped = Math.max(0, Math.min(1, normalized));
  return Math.round(clamped * (totalFrames - 1)) + 1;
}

P2. MqttBridgeService — Extraer MQTT del ORC (impacto: medio · esfuerzo: medio)

Concepto

Crear MqttBridgeService — singleton vanilla TS que reciba los mensajes MQTT del CommsManager y escriba directamente al DataPlane, sin pasar por el ORC (React Context). El ORC solo se actualiza para cambios de estado de dispositivos (online/offline, firmware version), no para datos de sensores en tiempo real.

Pipeline actual vs propuesto

ACTUAL: MqttAdapter.onMessage() │ ▼ ORC.handleMqttEvent() ← React Context (re-renders!) │ ├─ actualiza hwObj state ← necesario para UI ├─ ControlBus.allows() ├─ DataPlane.fromRecord() ← datos RT │ ▼ React re-render tree PROPUESTO: MqttAdapter.onMessage() │ ├─────────────────────────┐ ▼ ▼ MqttBridgeService ORC (solo status) │ │ ├─ ControlBus.allows() ├─ online/offline ├─ DataPlane.fromRecord() ├─ firmware info │ ├─ device metadata ▼ ▼ (zero React overhead) (React re-render solo para cambios de estado)

Beneficios

Separa las dos responsabilidades del ORC: gestion de estado de dispositivos (baja frecuencia, necesita React) vs relay de datos de sensores (alta frecuencia, no necesita React). Elimina re-renders innecesarios del tree React en cada mensaje MQTT (anti-patron A5).

P3. channelMap → DataPlane registry (impacto: bajo · esfuerzo: bajo)

Concepto

Eliminar el channelMap duplicado en el ORC. El DataPlane ya tiene un registry de canales (sensorId → channelIdx). El ORC puede consultar el DataPlane en vez de mantener su propia copia. Elimina anti-patron A7.

P4. EscenariosContext → SimService directo (impacto: medio · esfuerzo: bajo)

Concepto

Cuando se reactive EscenariosContext, los presets de escenario escriben directamente al SimService (que a su vez escribe al DataPlane). Sin pasar por ORC ni React Context para los datos de simulacion. EscenariosContext solo gestiona la UI de seleccion de escenario.

PROPUESTO: EscenariosContext.loadPreset() │ ├─ SimService.loadScenario(scenarioDef) ← datos sim directo ├─ setState({ activePreset }) ← solo UI │ ▼ SimService escribe al DataPlane

P5. Unificar suscripcion con DataPlane hooks (impacto: bajo · esfuerzo: bajo)

Concepto

No es un cambio de codigo sino una convencion: documentar que DataPlane.subscribe() es el patron canonico para singletons vanilla TS, y useDataPlaneValue() para componentes React. Eliminar cualquier uso residual de CustomEvents o React Context para datos de sensores en tiempo real. Anti-patron A6 no se elimina (los dos mundos son necesarios) pero se clarifica.

7. Arquitectura objetivo

Diagrama simplificado

┌──────────────────────────────────────────────────────────────────────┐ │ PRODUCTORES │ │ │ │ SimService MqttBridgeService Slider SignalEngine │ │ │ │ │ │ │ │ └──────────────┴──────────────────┴───────────┘ │ │ │ │ │ ControlBus.allows() │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ D A T A P L A N E │ │ │ │ Float64Array — Fuente unica de verdad │ │ │ └──────┬──────────┬───────────┬──────────────┬────────────────┘ │ │ │ │ │ │ │ │ .read() .subscribe() hooks .read() │ │ │ │ │ │ │ │ IB_04 SignalEngine React DebugBus │ │ (pull) (tick r/w) panels (push) │ │ │ │ │ │ realToFrame() setState() │ │ │ │ │ │ clip.gotoAndStop() re-render │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ ORC — solo estado de dispositivos (no hot path) │ │ │ │ online/offline · firmware · metadata · channelMap=gone │ │ │ └──────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘

Mecanismos de transporte reducidos

#MecanismoUsoEstado
1DataPlane (subscribe + read)Todo dato de sensores Principal — unico bus RT
2React ContextSolo estado UI (dispositivos, presets, layout) Correctamente scoped
3Service.subscribe()RouterService, ControlBus (baja frecuencia) Sin cambios
4DebugBusDebug solamente Sin cambios
5MQTT (F3)Hardware real Irreducible
6CustomEvent DOMclips Eliminado con IB_04
Resultado: de 7 mecanismos a 5

CustomEvents eliminado. React Context sacado del hot path. DataPlane como unico bus de datos en tiempo real, con dos APIs: .subscribe() para singletons, hooks para React.

8. Resumen ejecutivo

7 → 5
Mecanismos
de transporte
−2
JSON serialize
eliminados
−53%
CPU pipeline
clips
5 → 3
Eslabones
pipeline clips

Orden de ejecucion recomendado

PrioridadPropuestaImpactoEsfuerzoDependencias
1 P1: IB_04 — Bridge directo DataPlane Alto — elimina JSON + DOM + ADC Medio — ~300 lineas nuevo JS, coexiste con IB_03 Ninguna
2 P2: MqttBridgeService Medio — saca React del hot path MQTT Medio — ~200 lineas, refactor ORC Ninguna
3 P4: Escenarios → SimService Medio — prepara reactivacion limpia Bajo — routing directo Cuando se reactive EscenariosContext
4 P3: channelMap unificar Bajo — elimina estado duplicado Bajo — refactor simple P2 (menos riesgo si ORC ya simplificado)
5 P5: Documentar convencion Bajo — claridad cognitiva Minimo — solo documentacion Ninguna
Vision a largo plazo

El DataPlane ya es el bus central correcto. Las propuestas no cambian la arquitectura — la completan. El objetivo final es que todo dato de sensor fluya exclusivamente por el DataPlane, con lectores pull-based (IB_04, SignalEngine) o push-based throttled (React hooks), sin intermediarios DOM ni React Context en el hot path.

Con IB_04 + MqttBridgeService, la plataforma queda con un unico patron mental: "si es dato de sensor en tiempo real, pasa por el DataPlane. Punto."