#include "graph_manager.h" #include #include #include #include #include #include #include #include #include #include #include #include "utils/config_expand.h" #include "utils/config_schema.h" #include "utils/logger.h" #include "utils/simple_json_writer.h" #include "utils/thread_affinity.h" #include "hw/hw_factory.h" namespace rk3588 { namespace { int GetEnvInt(const char* name, int def) { if (!name) return def; const char* v = std::getenv(name); if (!v || !*v) return def; try { return std::stoi(v); } catch (...) { return def; } } int DefaultGraphPoolThreads() { // Override via env: RK3588_GRAPH_POOL_THREADS const int v = GetEnvInt("RK3588_GRAPH_POOL_THREADS", 0); if (v > 0 && v <= 64) return v; const unsigned hc = std::thread::hardware_concurrency(); const int d = (hc == 0) ? 2 : static_cast(hc); return std::max(1, std::min(4, d)); } size_t DefaultGraphBatchSize() { // Override via env: RK3588_GRAPH_BATCH_SIZE const int v = GetEnvInt("RK3588_GRAPH_BATCH_SIZE", 0); if (v > 0 && v <= 1024) return static_cast(v); return 8; } size_t DefaultGraphRunBudget() { // Max frames processed per scheduled run before yielding. const int v = GetEnvInt("RK3588_GRAPH_RUN_BUDGET", 0); if (v > 0 && v <= 4096) return static_cast(v); return 64; } std::string AffinityKey(std::vector cpus) { if (cpus.empty()) return ""; std::sort(cpus.begin(), cpus.end()); cpus.erase(std::unique(cpus.begin(), cpus.end()), cpus.end()); std::ostringstream oss; for (size_t i = 0; i < cpus.size(); ++i) { if (i) oss << ','; oss << cpus[i]; } return oss.str(); } QueueDropStrategy ParseDropStrategy(const std::string& s, QueueDropStrategy def) { if (s == "drop_oldest") return QueueDropStrategy::DropOldest; if (s == "drop_newest") return QueueDropStrategy::DropNewest; if (s == "block") return QueueDropStrategy::Block; return def; } void ApplyQueueConfig(const SimpleJson* queue_cfg, size_t default_queue_size, QueueDropStrategy default_strategy, size_t& out_size, QueueDropStrategy& out_strategy) { out_size = default_queue_size; out_strategy = default_strategy; if (!queue_cfg || !queue_cfg->IsObject()) return; out_size = static_cast(queue_cfg->ValueOr("size", static_cast(default_queue_size))); std::string policy = queue_cfg->ValueOr("policy", ""); std::string strat = queue_cfg->ValueOr("strategy", ""); std::string final_policy = policy.empty() ? strat : policy; if (!final_policy.empty()) { out_strategy = ParseDropStrategy(final_policy, default_strategy); } } struct ParsedEdge { std::string from; std::string to; const SimpleJson* queue_cfg = nullptr; // can be null SimpleJson queue_value; // normalized copy (null if none) }; bool ParseEdges(const SimpleJson& edges_json, std::vector& out, std::string& err) { out.clear(); if (!edges_json.IsArray()) { err = "edges must be array"; return false; } for (const auto& edge_val : edges_json.AsArray()) { ParsedEdge e; if (edge_val.IsArray()) { const auto& edge_arr = edge_val.AsArray(); if (edge_arr.size() < 2) { err = "Edge array must be [from, to] or [from, to, {...}]"; return false; } e.from = edge_arr[0].AsString(""); e.to = edge_arr[1].AsString(""); if (edge_arr.size() >= 3 && edge_arr[2].IsObject()) { const SimpleJson* q = edge_arr[2].Find("queue"); e.queue_cfg = q ? q : &edge_arr[2]; } } else if (edge_val.IsObject()) { e.from = edge_val.ValueOr("from", ""); e.to = edge_val.ValueOr("to", ""); e.queue_cfg = edge_val.Find("queue"); } else { err = "Edge must be an array or object"; return false; } if (e.from.empty() || e.to.empty()) { err = "Edge has empty endpoint"; return false; } if (e.queue_cfg) e.queue_value = *e.queue_cfg; out.push_back(std::move(e)); } return true; } struct EdgeKey { std::string from; std::string to; bool operator<(const EdgeKey& o) const { if (from != o.from) return from < o.from; return to < o.to; } }; bool EffectiveQueueForEdge(const ParsedEdge& e, const SimpleJson* from_node_queue_cfg, size_t default_queue_size, QueueDropStrategy default_strategy, size_t& out_size, QueueDropStrategy& out_strategy) { const SimpleJson* chosen = e.queue_cfg ? e.queue_cfg : from_node_queue_cfg; ApplyQueueConfig(chosen, default_queue_size, default_strategy, out_size, out_strategy); return true; } } // namespace class Graph::Executor { public: explicit Executor(Graph& g) : g_(g), batch_size_(DefaultGraphBatchSize()), run_budget_(DefaultGraphRunBudget()) { if (const SimpleJson* exec = g_.graph_cfg_.Find("executor"); exec && exec->IsObject()) { const int batch = exec->ValueOr("batch_size", static_cast(batch_size_)); const int budget = exec->ValueOr("run_budget", static_cast(run_budget_)); if (batch > 0 && batch <= 1024) batch_size_ = static_cast(batch); if (budget > 0 && budget <= 4096) run_budget_ = static_cast(budget); } } bool Start() { Stop(); stop_.store(false); // Build pools: default pool (no affinity) + pools for explicit cpu_affinity. pools_.clear(); key_to_pool_.clear(); auto add_pool = [&](const std::string& key, std::vector cpus, int threads) -> uint32_t { auto p = std::make_unique(); p->key = key; p->cpus = std::move(cpus); p->thread_count = std::max(1, threads); pools_.push_back(std::move(p)); const uint32_t idx = static_cast(pools_.size() - 1); key_to_pool_[key] = idx; return idx; }; default_pool_index_ = add_pool("", {}, DefaultGraphPoolThreads()); for (auto& n : g_.nodes_) { if (!n.enabled || !n.node) continue; if (n.role == "source" || !n.context.input_queue) continue; n.cpu_affinity = ParseCpuAffinity(n.config); const std::string key = AffinityKey(n.cpu_affinity); if (!key.empty() && key_to_pool_.find(key) == key_to_pool_.end()) { add_pool(key, n.cpu_affinity, 1); } } // Assign nodes to pools. for (auto& n : g_.nodes_) { if (!n.enabled || !n.node) continue; if (n.role == "source" || !n.context.input_queue) continue; const std::string key = AffinityKey(n.cpu_affinity); auto it = key_to_pool_.find(key); n.pool_index = (it != key_to_pool_.end()) ? it->second : default_pool_index_; n.sched_state.store(0); } // Start pool threads. for (auto& up : pools_) { Pool* p = up.get(); if (!p) continue; for (int i = 0; i < p->thread_count; ++i) { p->threads.emplace_back([this, p]() { WorkerLoop(*p); }); } } started_ = true; return true; } void Stop() { if (!started_) return; stop_.store(true); for (auto& up : pools_) { if (!up) continue; Pool& p = *up; { std::lock_guard lock(p.mu); p.ready.clear(); } p.cv.notify_all(); } for (auto& up : pools_) { if (!up) continue; Pool& p = *up; for (auto& t : p.threads) { if (t.joinable()) t.join(); } p.threads.clear(); { std::lock_guard lock(p.mu); p.ready.clear(); } } pools_.clear(); key_to_pool_.clear(); started_ = false; } void AttachQueueCallbacks() { for (auto& n : g_.nodes_) { if (!n.enabled || !n.node) continue; if (n.role == "source" || !n.context.input_queue) continue; NodeEntry* ptr = &n; n.context.input_queue->SetOnDataAvailable([this, ptr]() { Schedule(ptr); }); } } void DetachQueueCallbacks() { for (auto& n : g_.nodes_) { if (!n.enabled || !n.node) continue; if (n.role == "source" || !n.context.input_queue) continue; n.context.input_queue->SetOnDataAvailable(nullptr); } } void Schedule(NodeEntry* n) { if (!n) return; if (!n->enabled || !n->node || !n->context.input_queue) return; constexpr uint32_t kQueued = 1u; constexpr uint32_t kRunning = 2u; const uint32_t prev = n->sched_state.fetch_or(kQueued, std::memory_order_acq_rel); if (prev & kQueued) return; if (prev & kRunning) return; const uint32_t idx = (n->pool_index < pools_.size()) ? n->pool_index : default_pool_index_; if (idx >= pools_.size() || !pools_[idx]) return; Pool& p = *pools_[idx]; { std::lock_guard lock(p.mu); p.ready.push_back(n); } p.cv.notify_one(); } private: struct Pool { std::string key; std::vector cpus; int thread_count = 1; std::mutex mu; std::condition_variable cv; std::deque ready; std::vector threads; }; void WorkerLoop(Pool& p) { if (!p.cpus.empty()) { std::string aerr; if (!SetCurrentThreadAffinity(p.cpus, aerr)) { LogWarn("[Graph] SetCurrentThreadAffinity failed for pool: " + aerr); } } while (true) { NodeEntry* n = nullptr; { std::unique_lock lock(p.mu); p.cv.wait(lock, [&]() { return stop_.load() || !p.ready.empty(); }); if (stop_.load() && p.ready.empty()) { return; } if (!p.ready.empty()) { n = p.ready.front(); p.ready.pop_front(); } } if (n) { RunNode(*n, p); } } } void RunNode(NodeEntry& entry, Pool& p) { if (!entry.enabled || !entry.node || !entry.context.input_queue) return; constexpr uint32_t kQueued = 1u; constexpr uint32_t kRunning = 2u; // Mark running. entry.sched_state.fetch_or(kRunning, std::memory_order_acq_rel); std::vector batch; batch.reserve(batch_size_); while (true) { size_t processed = 0; // Drain up to run_budget_ frames to avoid starving other nodes. while (processed < run_budget_ && entry.context.input_queue->TryPopBatch(batch, batch_size_)) { for (auto& frame : batch) { if (!frame) continue; const auto t0 = std::chrono::steady_clock::now(); NodeStatus st = entry.node->Process(frame); const auto t1 = std::chrono::steady_clock::now(); const auto ns = std::chrono::duration_cast(t1 - t0).count(); if (entry.metrics) { if (ns > 0) { entry.metrics->process_time_ns_total.fetch_add(static_cast(ns), std::memory_order_relaxed); } if (st == NodeStatus::OK) { entry.metrics->ok_total.fetch_add(1, std::memory_order_relaxed); } else if (st == NodeStatus::DROP) { entry.metrics->drop_total.fetch_add(1, std::memory_order_relaxed); } else { entry.metrics->error_total.fetch_add(1, std::memory_order_relaxed); } } ++processed; if (processed >= run_budget_) break; } } // If we hit budget and there's still work, yield by re-enqueueing. if (processed >= run_budget_ && entry.context.input_queue->Size() > 0) { entry.sched_state.fetch_or(kQueued, std::memory_order_acq_rel); entry.sched_state.fetch_and(~kRunning, std::memory_order_acq_rel); { std::lock_guard lock(p.mu); p.ready.push_back(&entry); } p.cv.notify_one(); return; } // No immediate items; if someone scheduled while we were running, continue. uint32_t st = entry.sched_state.load(std::memory_order_acquire); if (st & kQueued) { entry.sched_state.fetch_and(~kQueued, std::memory_order_acq_rel); continue; } // Try to transition RUNNING -> 0. If it fails, a concurrent schedule happened; loop again. uint32_t expected = kRunning; if (entry.sched_state.compare_exchange_strong(expected, 0u, std::memory_order_acq_rel)) { return; } } } Graph& g_; size_t batch_size_ = 8; size_t run_budget_ = 64; std::atomic stop_{false}; bool started_ = false; uint32_t default_pool_index_ = 0; std::vector> pools_; std::unordered_map key_to_pool_; }; Graph::Graph(std::string name) : name_(std::move(name)) {} Graph::~Graph() { Stop(); } bool Graph::Build(const SimpleJson& graph_cfg, PluginLoader& loader, size_t default_queue_size, QueueDropStrategy default_strategy, std::string& err) { graph_cfg_ = graph_cfg; built_default_queue_size_ = default_queue_size; built_default_strategy_ = default_strategy; nodes_.clear(); edges_.clear(); executor_.reset(); { std::lock_guard lock(rate_mu_); last_rate_tp_ = {}; last_node_in_popped_.clear(); last_node_out_pushed_.clear(); last_edge_pushed_.clear(); last_edge_popped_.clear(); } const auto& obj = graph_cfg.AsObject(); auto name_it = obj.find("name"); if (name_it != obj.end() && name_it->second.IsString()) { name_ = name_it->second.AsString(name_); } infer_backend_ = HwFactory::CreateInferBackend(graph_cfg_); // Parse nodes auto nodes_it = obj.find("nodes"); if (nodes_it == obj.end() || !nodes_it->second.IsArray()) { err = "Graph missing 'nodes' array"; return false; } for (const auto& node_val : nodes_it->second.AsArray()) { if (!node_val.IsObject()) { err = "Node entry is not object"; return false; } NodeEntry entry; entry.config = node_val; entry.id = node_val.ValueOr("id", ""); entry.type = node_val.ValueOr("type", ""); entry.role = node_val.ValueOr("role", ""); entry.enabled = node_val.ValueOr("enable", true); entry.metrics = std::make_shared(); entry.context.graph_name = name_; entry.context.infer_backend = infer_backend_; if (entry.type == "preprocess" || entry.type == "ai_shoe_det" || entry.type == "ai_yolo") { entry.context.image_processor = HwFactory::CreateImageProcessor(entry.config); } if (entry.id.empty() || entry.type.empty()) { err = "Node missing id or type"; return false; } nodes_.push_back(std::move(entry)); } // Parse edges auto edges_it = obj.find("edges"); if (edges_it == obj.end() || !edges_it->second.IsArray()) { err = "Graph missing 'edges' array"; return false; } std::map id_to_node; for (auto& n : nodes_) { if (n.enabled) { id_to_node[n.id] = &n; } } std::vector parsed_edges; if (!ParseEdges(edges_it->second, parsed_edges, err)) { return false; } // Track enabled-edge connectivity for validation. std::vector> enabled_edges; for (const auto& e : parsed_edges) { auto from_it = id_to_node.find(e.from); auto to_it = id_to_node.find(e.to); if (from_it == id_to_node.end() || to_it == id_to_node.end()) { // Check if nodes exist but are disabled bool from_exists = false; bool to_exists = false; for(const auto& n : nodes_) { if(n.id == e.from) from_exists = true; if(n.id == e.to) to_exists = true; } if (!from_exists || !to_exists) { err = "Edge references unknown node: " + e.from + " -> " + e.to; return false; } // At least one is disabled, skip edge continue; } const SimpleJson* from_node_queue = from_it->second->config.Find("queue"); size_t qsize = default_queue_size; QueueDropStrategy strategy = default_strategy; EffectiveQueueForEdge(e, from_node_queue, default_queue_size, default_strategy, qsize, strategy); auto queue = std::make_shared>(qsize, strategy); from_it->second->context.output_queues.push_back(queue); if (!to_it->second->context.input_queue) { to_it->second->context.input_queue = queue; } else { err = "Node " + e.to + " has multiple inputs; only 1 is supported"; return false; } enabled_edges.emplace_back(e.from, e.to); edges_.push_back(EdgeEntry{e.from, e.to, queue}); } // Role validation for (auto& entry : nodes_) { if (!entry.enabled) continue; if (entry.role == "source" && entry.context.input_queue) { err = "Source node " + entry.id + " cannot have input"; return false; } if (entry.role == "sink" && !entry.context.output_queues.empty()) { err = "Sink node " + entry.id + " cannot have output"; return false; } if ((entry.role == "filter" || entry.role == "sink") && !entry.context.input_queue) { err = "Node " + entry.id + " role=" + entry.role + " must have input"; return false; } } // Cycle detection on enabled nodes. { std::map indeg; std::map> adj; for (const auto& n : nodes_) { if (!n.enabled) continue; indeg[n.id] = 0; } for (const auto& pr : enabled_edges) { adj[pr.first].push_back(pr.second); indeg[pr.second] += 1; } std::deque q; for (const auto& kv : indeg) { if (kv.second == 0) q.push_back(kv.first); } size_t visited = 0; while (!q.empty()) { auto cur = q.front(); q.pop_front(); ++visited; for (const auto& nxt : adj[cur]) { auto it = indeg.find(nxt); if (it == indeg.end()) continue; if (--it->second == 0) q.push_back(nxt); } } if (visited != indeg.size()) { err = "Graph contains a cycle (DAG required)"; return false; } } // Instantiation for (auto& entry : nodes_) { if (!entry.enabled) continue; std::string load_err; entry.node = loader.Create(entry.type, load_err); if (!entry.node) { err = load_err; return false; } if (!entry.node->Init(entry.config, entry.context)) { err = "Init failed for node " + entry.id; return false; } } return true; } bool Graph::TryUpdateInPlace(const SimpleJson& new_graph_cfg, size_t default_queue_size, QueueDropStrategy default_strategy, std::string& err) { err.clear(); if (!new_graph_cfg.IsObject()) { err = "new graph config is not object"; return false; } const SimpleJson* new_nodes = new_graph_cfg.Find("nodes"); const SimpleJson* new_edges = new_graph_cfg.Find("edges"); const SimpleJson* old_nodes = graph_cfg_.Find("nodes"); const SimpleJson* old_edges = graph_cfg_.Find("edges"); if (!new_nodes || !new_edges || !old_nodes || !old_edges) { err = "graph missing nodes/edges"; return false; } // If topology changed, require rebuild. std::vector pe_new; std::vector pe_old; std::string parse_err; if (!ParseEdges(*new_edges, pe_new, parse_err) || !ParseEdges(*old_edges, pe_old, parse_err)) { err = parse_err; return false; } auto build_edge_map = [](const std::vector& pes, std::map& out) { out.clear(); for (const auto& e : pes) { out[{e.from, e.to}] = e; } }; std::map m_new; std::map m_old; build_edge_map(pe_new, m_new); build_edge_map(pe_old, m_old); if (m_new.size() != m_old.size()) { return false; // rebuild } // Build node config maps by id. std::map new_node_cfg; std::map old_node_cfg; std::map new_enabled; std::map old_enabled; std::map new_type; std::map old_type; for (const auto& nv : new_nodes->AsArray()) { if (!nv.IsObject()) continue; std::string id = nv.ValueOr("id", ""); if (id.empty()) continue; new_node_cfg[id] = nv; new_enabled[id] = nv.ValueOr("enable", true); new_type[id] = nv.ValueOr("type", ""); } for (const auto& ov : old_nodes->AsArray()) { if (!ov.IsObject()) continue; std::string id = ov.ValueOr("id", ""); if (id.empty()) continue; old_node_cfg[id] = ov; old_enabled[id] = ov.ValueOr("enable", true); old_type[id] = ov.ValueOr("type", ""); } if (new_node_cfg.size() != old_node_cfg.size()) { return false; // rebuild } for (const auto& kv : old_node_cfg) { auto it = new_node_cfg.find(kv.first); if (it == new_node_cfg.end()) return false; // rebuild if (new_type[kv.first] != old_type[kv.first]) return false; // rebuild if (new_enabled[kv.first] != old_enabled[kv.first]) return false; // rebuild } // Compare effective queue specs. for (const auto& kv : m_old) { auto itn = m_new.find(kv.first); if (itn == m_new.end()) return false; const auto& eo = kv.second; const auto& en = itn->second; const SimpleJson* from_old_queue = nullptr; const SimpleJson* from_new_queue = nullptr; if (auto fn = old_node_cfg.find(eo.from); fn != old_node_cfg.end()) { from_old_queue = fn->second.Find("queue"); } if (auto fn = new_node_cfg.find(en.from); fn != new_node_cfg.end()) { from_new_queue = fn->second.Find("queue"); } size_t osz = 0, nsz = 0; QueueDropStrategy ostrat = QueueDropStrategy::DropOldest; QueueDropStrategy nstrat = QueueDropStrategy::DropOldest; EffectiveQueueForEdge(eo, from_old_queue, built_default_queue_size_, built_default_strategy_, osz, ostrat); EffectiveQueueForEdge(en, from_new_queue, default_queue_size, default_strategy, nsz, nstrat); if (osz != nsz || ostrat != nstrat) { return false; // rebuild } } // In-place update: only apply for nodes whose full config changed. for (auto& entry : nodes_) { if (!entry.enabled || !entry.node) continue; auto it_new = new_node_cfg.find(entry.id); auto it_old = old_node_cfg.find(entry.id); if (it_new == new_node_cfg.end() || it_old == old_node_cfg.end()) return false; if (!JsonDeepEqual(it_new->second, it_old->second)) { if (!entry.node->UpdateConfig(it_new->second)) { // If any node cannot update in place, request rebuild. return false; } entry.config = it_new->second; } } graph_cfg_ = new_graph_cfg; built_default_queue_size_ = default_queue_size; built_default_strategy_ = default_strategy; return true; } namespace { SimpleJson ReplaceNodeInGraphCfg(const SimpleJson& graph_cfg, const std::string& node_id, const SimpleJson& new_node_cfg) { if (!graph_cfg.IsObject()) return graph_cfg; SimpleJson::Object obj = graph_cfg.AsObject(); const SimpleJson* nodes = graph_cfg.Find("nodes"); if (!nodes || !nodes->IsArray()) return graph_cfg; SimpleJson::Array out_nodes; out_nodes.reserve(nodes->AsArray().size()); for (const auto& n : nodes->AsArray()) { if (n.IsObject() && n.ValueOr("id", "") == node_id) { out_nodes.push_back(new_node_cfg); } else { out_nodes.push_back(n); } } obj["nodes"] = SimpleJson(std::move(out_nodes)); return SimpleJson(std::move(obj)); } } // namespace bool Graph::UpdateNodeConfig(const std::string& node_id, const SimpleJson& new_node_cfg, std::string& err) { err.clear(); for (auto& entry : nodes_) { if (entry.id != node_id) continue; if (!entry.enabled || !entry.node) { err = "node not running or disabled: " + node_id; return false; } if (!entry.node->UpdateConfig(new_node_cfg)) { err = "UpdateConfig returned false"; return false; } entry.config = new_node_cfg; graph_cfg_ = ReplaceNodeInGraphCfg(graph_cfg_, node_id, new_node_cfg); return true; } err = "node not found: " + node_id; return false; } bool Graph::Start() { bool expected = false; if (!running_.compare_exchange_strong(expected, true)) { return true; // Already running } stop_requested_.store(false); auto is_source_like = [](const NodeEntry& n) { return n.role == "source" || !n.context.input_queue; }; // 1) Start non-source nodes first (they should only allocate resources). for (auto& entry : nodes_) { if (!entry.enabled || !entry.node) continue; if (is_source_like(entry)) continue; if (!entry.node->Start()) { LogError("[Graph] failed to start node: " + entry.id); Stop(); return false; } } // 2) Start executor + attach callbacks before any source can push frames. executor_ = std::make_unique(*this); if (!executor_->Start()) { LogError("[Graph] failed to start executor"); Stop(); return false; } executor_->AttachQueueCallbacks(); // 3) Start sources last. for (auto& entry : nodes_) { if (!entry.enabled || !entry.node) continue; if (!is_source_like(entry)) continue; if (!entry.node->Start()) { LogError("[Graph] failed to start node: " + entry.id); Stop(); return false; } } LogInfo("[Graph] started graph " + name_ + " with " + std::to_string(nodes_.size()) + " nodes"); return true; } void Graph::Stop() { if (!running_.load()) { return; } stop_requested_.store(true); auto is_source_like = [](const NodeEntry& n) { return n.role == "source" || !n.context.input_queue; }; // 1) Stop sources first to ensure no new data is produced. for (auto& n : nodes_) { if (!n.enabled || !n.node) continue; if (!is_source_like(n)) continue; n.node->Drain(); n.node->Stop(); for (auto& q : n.context.output_queues) q->Stop(); } // 2) Stop all edge queues to unblock downstream nodes. for (auto& e : edges_) { if (e.queue) e.queue->Stop(); } // 3) Stop executor (no concurrent Process() beyond this point). if (executor_) { executor_->DetachQueueCallbacks(); executor_->Stop(); executor_.reset(); } // 4) Drain non-source nodes. for (auto& n : nodes_) { if (!n.enabled || !n.node) continue; if (is_source_like(n)) continue; n.node->Drain(); } // 5) Stop remaining nodes (reverse order). for (auto it = nodes_.rbegin(); it != nodes_.rend(); ++it) { if (!it->enabled || !it->node) continue; if (is_source_like(*it)) continue; it->node->Stop(); } running_.store(false); } namespace { uint64_t NowEpochMs() { using namespace std::chrono; return static_cast(duration_cast(system_clock::now().time_since_epoch()).count()); } QueueSnapshot ToQueueSnapshot(const SpscQueue::Stats& st) { QueueSnapshot q; q.size = st.size; q.capacity = st.capacity; q.dropped_total = static_cast(st.dropped); q.pushed_total = static_cast(st.pushed); q.popped_total = static_cast(st.popped); q.stopped = st.stopped; return q; } } // namespace GraphSnapshot Graph::Snapshot() const { GraphSnapshot snap; snap.name = name_; snap.running = running_.load(); snap.timestamp_ms = NowEpochMs(); const auto now_tp = std::chrono::steady_clock::now(); double dt_sec = 0.0; { std::lock_guard lock(rate_mu_); if (last_rate_tp_.time_since_epoch().count() != 0) { dt_sec = std::chrono::duration_cast>(now_tp - last_rate_tp_).count(); } last_rate_tp_ = now_tp; } double total_fps = 0.0; for (const auto& n : nodes_) { if (!n.enabled || !n.node) continue; NodeSnapshot ns; ns.graph = name_; ns.id = n.id; ns.type = n.type; ns.role = n.role; ns.enabled = n.enabled; uint64_t in_popped = 0; if (n.context.input_queue) { auto st = n.context.input_queue->GetStats(); ns.input_queue = ToQueueSnapshot(st); in_popped = ns.input_queue.popped_total; } uint64_t out_pushed = 0; for (const auto& oq : n.context.output_queues) { if (!oq) continue; out_pushed += static_cast(oq->PushedCount()); } { std::lock_guard lock(rate_mu_); const uint64_t last_in = last_node_in_popped_[n.id]; const uint64_t last_out = last_node_out_pushed_[n.id]; if (dt_sec > 0.0) { if (in_popped >= last_in) ns.input_fps = static_cast(in_popped - last_in) / dt_sec; if (out_pushed >= last_out) ns.output_fps = static_cast(out_pushed - last_out) / dt_sec; } last_node_in_popped_[n.id] = in_popped; last_node_out_pushed_[n.id] = out_pushed; } if (n.metrics) { ns.ok_total = n.metrics->ok_total.load(std::memory_order_relaxed); ns.drop_total = n.metrics->drop_total.load(std::memory_order_relaxed); ns.error_total = n.metrics->error_total.load(std::memory_order_relaxed); } const uint64_t proc_cnt = ns.ok_total + ns.drop_total + ns.error_total; const uint64_t ns_total = n.metrics ? n.metrics->process_time_ns_total.load(std::memory_order_relaxed) : 0; if (proc_cnt > 0) { ns.avg_process_time_ms = (static_cast(ns_total) / 1e6) / static_cast(proc_cnt); } { SimpleJson cm; if (n.node && n.node->GetCustomMetrics(cm)) { ns.custom_metrics = cm; if (cm.IsObject()) { if (const SimpleJson* a = cm.Find("alarm_total"); a && a->IsNumber()) { snap.alarm_total += static_cast(a->AsNumber(0.0)); } if (const SimpleJson* c = cm.Find("clients"); c && c->IsNumber()) { snap.publish_clients += static_cast(c->AsNumber(0.0)); } } } } const bool source_like = (n.role == "source") || !n.context.input_queue; if (source_like) { total_fps += ns.output_fps; } snap.nodes.push_back(std::move(ns)); } for (const auto& e : edges_) { if (!e.queue) continue; EdgeSnapshot es; es.from = e.from; es.to = e.to; const auto st = e.queue->GetStats(); es.queue = ToQueueSnapshot(st); const std::string key = e.from + "->" + e.to; { std::lock_guard lock(rate_mu_); const uint64_t last_pushed = last_edge_pushed_[key]; const uint64_t last_popped = last_edge_popped_[key]; if (dt_sec > 0.0) { if (es.queue.pushed_total >= last_pushed) { es.queue.pushed_fps = static_cast(es.queue.pushed_total - last_pushed) / dt_sec; } if (es.queue.popped_total >= last_popped) { es.queue.popped_fps = static_cast(es.queue.popped_total - last_popped) / dt_sec; } } last_edge_pushed_[key] = es.queue.pushed_total; last_edge_popped_[key] = es.queue.popped_total; } snap.edges.push_back(std::move(es)); } snap.total_fps = total_fps; return snap; } bool Graph::FindNodeSnapshotById(const std::string& node_id, NodeSnapshot& out) const { auto snap = Snapshot(); for (auto& n : snap.nodes) { if (n.id == node_id) { out = std::move(n); return true; } } return false; } GraphManager::GraphManager(std::string plugin_dir) : loader_(std::move(plugin_dir)) {} GraphManager::~GraphManager() { StopAll(); } bool GraphManager::LoadConfigFile(const std::string& path, SimpleJson& out, std::string& err) { std::ifstream ifs(path); if (!ifs.is_open()) { err = "Failed to open config: " + path; return false; } std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); return ParseSimpleJson(content, out, err); } bool GraphManager::Build(const SimpleJson& root_cfg, std::string& err) { SimpleJson expanded; if (!ExpandRootConfig(root_cfg, expanded, err)) { return false; } if (!ValidateExpandedRootConfig(expanded, err)) { return false; } auto graphs_it = expanded.AsObject().find("graphs"); if (graphs_it == expanded.AsObject().end() || !graphs_it->second.IsArray()) { err = "Root config missing 'graphs' array"; return false; } size_t default_queue_size = 8; QueueDropStrategy default_strategy = QueueDropStrategy::DropOldest; if (const auto* queue_cfg = expanded.Find("queue")) { if (queue_cfg->IsObject()) { default_queue_size = static_cast(queue_cfg->ValueOr("size", 8)); std::string strategy = queue_cfg->ValueOr("strategy", "drop_oldest"); default_strategy = ParseDropStrategy(strategy, default_strategy); } } // Apply plugin_path only on initial Build (safe: no nodes exist yet). if (const SimpleJson* g = expanded.Find("global")) { const std::string plugin_path = g->ValueOr("plugin_path", ""); if (!plugin_path.empty()) { loader_.SetPluginDir(plugin_path); } const std::string log_level = g->ValueOr("log_level", ""); if (!log_level.empty()) { LogLevel lvl; if (ParseLogLevel(log_level, lvl)) { Logger::Instance().SetLevel(lvl); } } } std::lock_guard lock(graphs_mu_); graphs_.clear(); for (const auto& graph_val : graphs_it->second.AsArray()) { if (!graph_val.IsObject()) { err = "Graph entry is not object"; return false; } std::string name = graph_val.ValueOr("name", "noname"); auto graph = std::make_unique(name); if (!graph->Build(graph_val, loader_, default_queue_size, default_strategy, err)) { return false; } graphs_.push_back(std::move(graph)); } last_good_source_root_ = root_cfg; last_good_expanded_root_ = expanded; default_queue_size_ = default_queue_size; default_strategy_ = default_strategy; return true; } bool GraphManager::BuildFromFile(const std::string& path, std::string& err) { config_path_ = path; SimpleJson root_cfg; if (!LoadConfigFile(path, root_cfg, err)) { return false; } return Build(root_cfg, err); } bool GraphManager::StartAll() { std::scoped_lock lock(mu_, graphs_mu_); if (running_) return true; // Start all graphs; on failure, stop any already started graphs so we never leave a partially-running state. std::vector started; started.reserve(graphs_.size()); for (auto& g : graphs_) { if (!g) continue; if (!g->Start()) { for (auto* sg : started) { if (sg) sg->Stop(); } return false; } started.push_back(g.get()); } running_ = true; return true; } void GraphManager::StopAll() { { std::scoped_lock lock(mu_, graphs_mu_); if (!running_) return; running_ = false; for (auto& g : graphs_) { if (g) g->Stop(); } } cv_.notify_all(); } void GraphManager::RequestStop() { StopAll(); } void GraphManager::BlockUntilStop() { std::unique_lock lock(mu_); cv_.wait(lock, [&] { return !running_; }); } bool GraphManager::ReloadFromFile(const std::string& path, std::string& err) { if (config_path_.empty()) { config_path_ = path; } SimpleJson root_cfg; if (!LoadConfigFile(path, root_cfg, err)) { return false; } // Keep the original source config (templates/instances preserved). const SimpleJson source_root = root_cfg; SimpleJson expanded; if (!ExpandRootConfig(root_cfg, expanded, err)) { return false; } if (!ValidateExpandedRootConfig(expanded, err)) { return false; } auto graphs_it = expanded.AsObject().find("graphs"); if (graphs_it == expanded.AsObject().end() || !graphs_it->second.IsArray()) { err = "Root config missing 'graphs' array"; return false; } size_t new_default_queue_size = 8; QueueDropStrategy new_default_strategy = QueueDropStrategy::DropOldest; if (const auto* queue_cfg = expanded.Find("queue")) { if (queue_cfg->IsObject()) { new_default_queue_size = static_cast(queue_cfg->ValueOr("size", 8)); std::string strategy = queue_cfg->ValueOr("strategy", "drop_oldest"); new_default_strategy = ParseDropStrategy(strategy, new_default_strategy); } } const std::string new_plugin_path = [&]() -> std::string { if (const SimpleJson* g = expanded.Find("global")) { return g->ValueOr("plugin_path", ""); } return ""; }(); std::optional new_log_level; if (const SimpleJson* g = expanded.Find("global")) { const std::string log_level = g->ValueOr("log_level", ""); if (!log_level.empty()) { LogLevel lvl; if (ParseLogLevel(log_level, lvl)) { new_log_level = lvl; } } } std::lock_guard lock(graphs_mu_); const SimpleJson prev_last_good = last_good_expanded_root_; const size_t prev_default_queue_size = default_queue_size_; const QueueDropStrategy prev_default_strategy = default_strategy_; const std::string prev_plugin_dir = loader_.PluginDir(); auto build_graphs_locked = [&](const SimpleJson& expanded_root, PluginLoader& loader, size_t def_q, QueueDropStrategy def_s, std::vector>& out_graphs, std::string& build_err) -> bool { out_graphs.clear(); const SimpleJson* graphs = expanded_root.Find("graphs"); if (!graphs || !graphs->IsArray()) { build_err = "Root config missing 'graphs' array"; return false; } out_graphs.reserve(graphs->AsArray().size()); for (const auto& gv : graphs->AsArray()) { if (!gv.IsObject()) { build_err = "Graph entry is not object"; return false; } std::string name = gv.ValueOr("name", "noname"); auto graph = std::make_unique(name); if (!graph->Build(gv, loader, def_q, def_s, build_err)) { return false; } out_graphs.push_back(std::move(graph)); } return true; }; auto start_graphs_locked = [&](std::vector>& gs, std::string& start_err) -> bool { for (auto& g : gs) { if (!g) continue; if (!g->Start()) { start_err = "Failed to start graph: " + g->Name(); return false; } } return true; }; auto stop_all_locked = [&]() { for (auto& g : graphs_) { if (g) g->Stop(); } }; auto recover_locked = [&](std::string& recover_err) -> bool { stop_all_locked(); graphs_.clear(); if (!prev_plugin_dir.empty() && prev_plugin_dir != loader_.PluginDir()) { loader_.SetPluginDir(prev_plugin_dir); } std::vector> recovered; std::string berr; if (!build_graphs_locked(prev_last_good, loader_, prev_default_queue_size, prev_default_strategy, recovered, berr)) { recover_err = "Recovery build failed: " + berr; return false; } std::string serr; if (!start_graphs_locked(recovered, serr)) { recover_err = "Recovery start failed: " + serr; for (auto& gg : recovered) { if (gg) gg->Stop(); } return false; } graphs_ = std::move(recovered); default_queue_size_ = prev_default_queue_size; default_strategy_ = prev_default_strategy; // last_good_source_root_/last_good_expanded_root_ remain unchanged. return true; }; const bool plugin_dir_change = (!new_plugin_path.empty() && new_plugin_path != loader_.PluginDir()); if (plugin_dir_change) { PluginLoader staged_loader(new_plugin_path); std::vector> staged_graphs; std::string berr; if (!build_graphs_locked(expanded, staged_loader, new_default_queue_size, new_default_strategy, staged_graphs, berr)) { err = berr; return false; } // Switch window: allow short downtime, but must be recoverable. stop_all_locked(); graphs_.clear(); PluginLoader old_loader = std::move(loader_); loader_ = std::move(staged_loader); graphs_ = std::move(staged_graphs); std::string serr; if (!start_graphs_locked(graphs_, serr)) { err = "Failed to start after plugin_path switch: " + serr; // Stop partially started graphs before recovery. stop_all_locked(); graphs_.clear(); loader_ = std::move(old_loader); std::string rerr; if (!recover_locked(rerr)) { err += "; recovery failed: " + rerr; return false; } return false; } last_good_source_root_ = source_root; last_good_expanded_root_ = expanded; default_queue_size_ = new_default_queue_size; default_strategy_ = new_default_strategy; if (new_log_level) { Logger::Instance().SetLevel(*new_log_level); } return true; } auto find_graph_index_locked = [&](const std::string& name, size_t& out_idx) -> bool { for (size_t i = 0; i < graphs_.size(); ++i) { if (graphs_[i] && graphs_[i]->Name() == name) { out_idx = i; return true; } } return false; }; // Track graphs referenced by new config. std::set seen; // Stage graphs that require rebuild or are newly added. std::map> staged; for (const auto& graph_val : graphs_it->second.AsArray()) { if (!graph_val.IsObject()) { err = "Graph entry is not object"; return false; } std::string name = graph_val.ValueOr("name", "noname"); seen.insert(name); size_t idx = 0; if (!find_graph_index_locked(name, idx)) { // New graph: stage build (do not start until we have stopped removed graphs). auto graph = std::make_unique(name); if (!graph->Build(graph_val, loader_, new_default_queue_size, new_default_strategy, err)) { return false; } staged[name] = std::move(graph); continue; } auto& g = graphs_[idx]; std::string upd_err; if (g->TryUpdateInPlace(graph_val, new_default_queue_size, new_default_strategy, upd_err)) { continue; } if (!upd_err.empty()) { err = "UpdateConfig failed for graph " + name + ": " + upd_err; return false; } auto graph = std::make_unique(name); if (!graph->Build(graph_val, loader_, new_default_queue_size, new_default_strategy, err)) { return false; } staged[name] = std::move(graph); } // Stop and remove graphs not present anymore (may free resources needed by staged graphs). for (auto itg = graphs_.begin(); itg != graphs_.end();) { if (*itg && !seen.count((*itg)->Name())) { (*itg)->Stop(); itg = graphs_.erase(itg); } else { ++itg; } } // Apply staged graphs. for (auto& kv : staged) { const std::string& name = kv.first; auto& new_g = kv.second; if (!new_g) continue; size_t idx = 0; if (find_graph_index_locked(name, idx)) { graphs_[idx]->Stop(); if (!new_g->Start()) { err = "Failed to start rebuilt graph: " + name; std::string rerr; if (!recover_locked(rerr)) { err += "; recovery failed: " + rerr; return false; } return false; } graphs_[idx] = std::move(new_g); } else { if (!new_g->Start()) { err = "Failed to start new graph: " + name; std::string rerr; if (!recover_locked(rerr)) { err += "; recovery failed: " + rerr; return false; } return false; } graphs_.push_back(std::move(new_g)); } } last_good_source_root_ = source_root; last_good_expanded_root_ = expanded; default_queue_size_ = new_default_queue_size; default_strategy_ = new_default_strategy; if (new_log_level) { Logger::Instance().SetLevel(*new_log_level); } return true; } bool GraphManager::UpdateNodeConfig(const std::string& node_id, const std::optional& graph, const SimpleJson& new_node_cfg, std::string& err) { std::lock_guard lock(graphs_mu_); if (graph && !graph->empty()) { for (const auto& g : graphs_) { if (!g || g->Name() != *graph) continue; return g->UpdateNodeConfig(node_id, new_node_cfg, err); } err = "graph not found: " + *graph; return false; } size_t hits = 0; Graph* found = nullptr; for (const auto& g : graphs_) { if (!g) continue; NodeSnapshot tmp; if (g->FindNodeSnapshotById(node_id, tmp)) { found = g.get(); ++hits; if (hits > 1) break; } } if (hits == 0 || !found) { err = "node not found: " + node_id; return false; } if (hits > 1) { err = "node id not unique, specify ?graph="; return false; } return found->UpdateNodeConfig(node_id, new_node_cfg, err); } std::vector GraphManager::ListGraphSnapshots() { std::vector out; std::lock_guard lock(graphs_mu_); out.reserve(graphs_.size()); for (const auto& g : graphs_) { if (!g) continue; out.push_back(g->Snapshot()); } return out; } bool GraphManager::GetGraphSnapshot(const std::string& name, GraphSnapshot& out, std::string& err) { std::lock_guard lock(graphs_mu_); for (const auto& g : graphs_) { if (g && g->Name() == name) { out = g->Snapshot(); return true; } } err = "graph not found: " + name; return false; } bool GraphManager::GetNodeSnapshot(const std::string& node_id, const std::optional& graph, NodeSnapshot& out, std::string& err) { std::lock_guard lock(graphs_mu_); if (graph && !graph->empty()) { for (const auto& g : graphs_) { if (!g || g->Name() != *graph) continue; if (g->FindNodeSnapshotById(node_id, out)) return true; err = "node not found: " + node_id + " in graph " + *graph; return false; } err = "graph not found: " + *graph; return false; } // Auto-match when unique. size_t hits = 0; for (const auto& g : graphs_) { if (!g) continue; NodeSnapshot tmp; if (g->FindNodeSnapshotById(node_id, tmp)) { out = std::move(tmp); ++hits; if (hits > 1) break; } } if (hits == 1) return true; if (hits == 0) { err = "node not found: " + node_id; return false; } err = "node id is not unique; specify ?graph="; return false; } // New Result-based API implementations Result GraphManager::LoadConfig(const std::string& path) { SimpleJson out; std::string err; if (!LoadConfigFile(path, out, err)) { return Error(err); } return out; } Status GraphManager::BuildFromConfig(const SimpleJson& root_cfg) { std::string err; if (!Build(root_cfg, err)) { return Status::Fail(err); } return Status::Ok(); } Status GraphManager::BuildFromPath(const std::string& path) { std::string err; if (!BuildFromFile(path, err)) { return Status::Fail(err); } return Status::Ok(); } Status GraphManager::Reload(const std::string& path) { std::string err; if (!ReloadFromFile(path, err)) { return Status::Fail(err); } return Status::Ok(); } Status GraphManager::SetNodeConfig(const std::string& node_id, const SimpleJson& new_node_cfg, const std::optional& graph) { std::string err; if (!UpdateNodeConfig(node_id, graph, new_node_cfg, err)) { return Status::Fail(err); } return Status::Ok(); } Result GraphManager::GetGraph(const std::string& name) { GraphSnapshot out; std::string err; if (!GetGraphSnapshot(name, out, err)) { return Error(err); } return out; } Result GraphManager::GetNode(const std::string& node_id, const std::optional& graph) { NodeSnapshot out; std::string err; if (!GetNodeSnapshot(node_id, graph, out, err)) { return Error(err); } return out; } } // namespace rk3588