safesight-edge/src/graph_manager.cpp

1600 lines
52 KiB
C++

#include "graph_manager.h"
#include <fstream>
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <map>
#include <set>
#include <deque>
#include <thread>
#include <chrono>
#include <unordered_map>
#include <sstream>
#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<int>(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<size_t>(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<size_t>(v);
return 64;
}
std::string AffinityKey(std::vector<int> 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<size_t>(queue_cfg->ValueOr<int>("size", static_cast<int>(default_queue_size)));
std::string policy = queue_cfg->ValueOr<std::string>("policy", "");
std::string strat = queue_cfg->ValueOr<std::string>("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<ParsedEdge>& 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<std::string>("from", "");
e.to = edge_val.ValueOr<std::string>("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<int>("batch_size", static_cast<int>(batch_size_));
const int budget = exec->ValueOr<int>("run_budget", static_cast<int>(run_budget_));
if (batch > 0 && batch <= 1024) batch_size_ = static_cast<size_t>(batch);
if (budget > 0 && budget <= 4096) run_budget_ = static_cast<size_t>(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<int> cpus, int threads) -> uint32_t {
auto p = std::make_unique<Pool>();
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<uint32_t>(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<std::mutex> 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<std::mutex> 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<std::mutex> lock(p.mu);
p.ready.push_back(n);
}
p.cv.notify_one();
}
private:
struct Pool {
std::string key;
std::vector<int> cpus;
int thread_count = 1;
std::mutex mu;
std::condition_variable cv;
std::deque<NodeEntry*> ready;
std::vector<std::thread> 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<std::mutex> 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<FramePtr> 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<std::chrono::nanoseconds>(t1 - t0).count();
if (entry.metrics) {
if (ns > 0) {
entry.metrics->process_time_ns_total.fetch_add(static_cast<uint64_t>(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<std::mutex> 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<bool> stop_{false};
bool started_ = false;
uint32_t default_pool_index_ = 0;
std::vector<std::unique_ptr<Pool>> pools_;
std::unordered_map<std::string, uint32_t> 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<std::mutex> 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<std::string>("id", "");
entry.type = node_val.ValueOr<std::string>("type", "");
entry.role = node_val.ValueOr<std::string>("role", "");
entry.enabled = node_val.ValueOr<bool>("enable", true);
entry.metrics = std::make_shared<NodeMetrics>();
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<std::string, NodeEntry*> id_to_node;
for (auto& n : nodes_) {
if (n.enabled) {
id_to_node[n.id] = &n;
}
}
std::vector<ParsedEdge> parsed_edges;
if (!ParseEdges(edges_it->second, parsed_edges, err)) {
return false;
}
// Track enabled-edge connectivity for validation.
std::vector<std::pair<std::string, std::string>> 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<SpscQueue<FramePtr>>(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<std::string, int> indeg;
std::map<std::string, std::vector<std::string>> 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<std::string> 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<ParsedEdge> pe_new;
std::vector<ParsedEdge> 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<ParsedEdge>& pes, std::map<EdgeKey, ParsedEdge>& out) {
out.clear();
for (const auto& e : pes) {
out[{e.from, e.to}] = e;
}
};
std::map<EdgeKey, ParsedEdge> m_new;
std::map<EdgeKey, ParsedEdge> 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<std::string, SimpleJson> new_node_cfg;
std::map<std::string, SimpleJson> old_node_cfg;
std::map<std::string, bool> new_enabled;
std::map<std::string, bool> old_enabled;
std::map<std::string, std::string> new_type;
std::map<std::string, std::string> old_type;
for (const auto& nv : new_nodes->AsArray()) {
if (!nv.IsObject()) continue;
std::string id = nv.ValueOr<std::string>("id", "");
if (id.empty()) continue;
new_node_cfg[id] = nv;
new_enabled[id] = nv.ValueOr<bool>("enable", true);
new_type[id] = nv.ValueOr<std::string>("type", "");
}
for (const auto& ov : old_nodes->AsArray()) {
if (!ov.IsObject()) continue;
std::string id = ov.ValueOr<std::string>("id", "");
if (id.empty()) continue;
old_node_cfg[id] = ov;
old_enabled[id] = ov.ValueOr<bool>("enable", true);
old_type[id] = ov.ValueOr<std::string>("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<std::string>("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<Executor>(*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<uint64_t>(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
}
QueueSnapshot ToQueueSnapshot(const SpscQueue<FramePtr>::Stats& st) {
QueueSnapshot q;
q.size = st.size;
q.capacity = st.capacity;
q.dropped_total = static_cast<uint64_t>(st.dropped);
q.pushed_total = static_cast<uint64_t>(st.pushed);
q.popped_total = static_cast<uint64_t>(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<std::mutex> lock(rate_mu_);
if (last_rate_tp_.time_since_epoch().count() != 0) {
dt_sec = std::chrono::duration_cast<std::chrono::duration<double>>(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<uint64_t>(oq->PushedCount());
}
{
std::lock_guard<std::mutex> 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<double>(in_popped - last_in) / dt_sec;
if (out_pushed >= last_out) ns.output_fps = static_cast<double>(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<double>(ns_total) / 1e6) / static_cast<double>(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<uint64_t>(a->AsNumber(0.0));
}
if (const SimpleJson* c = cm.Find("clients"); c && c->IsNumber()) {
snap.publish_clients += static_cast<uint64_t>(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<std::mutex> 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<double>(es.queue.pushed_total - last_pushed) / dt_sec;
}
if (es.queue.popped_total >= last_popped) {
es.queue.popped_fps = static_cast<double>(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<char>(ifs)), std::istreambuf_iterator<char>());
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<size_t>(queue_cfg->ValueOr<int>("size", 8));
std::string strategy = queue_cfg->ValueOr<std::string>("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<std::string>("plugin_path", "");
if (!plugin_path.empty()) {
loader_.SetPluginDir(plugin_path);
}
const std::string log_level = g->ValueOr<std::string>("log_level", "");
if (!log_level.empty()) {
LogLevel lvl;
if (ParseLogLevel(log_level, lvl)) {
Logger::Instance().SetLevel(lvl);
}
}
}
std::lock_guard<std::mutex> 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<std::string>("name", "noname");
auto graph = std::make_unique<Graph>(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<Graph*> 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<std::mutex> 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<size_t>(queue_cfg->ValueOr<int>("size", 8));
std::string strategy = queue_cfg->ValueOr<std::string>("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<std::string>("plugin_path", "");
}
return "";
}();
std::optional<LogLevel> new_log_level;
if (const SimpleJson* g = expanded.Find("global")) {
const std::string log_level = g->ValueOr<std::string>("log_level", "");
if (!log_level.empty()) {
LogLevel lvl;
if (ParseLogLevel(log_level, lvl)) {
new_log_level = lvl;
}
}
}
std::lock_guard<std::mutex> 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<std::unique_ptr<Graph>>& 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<std::string>("name", "noname");
auto graph = std::make_unique<Graph>(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<std::unique_ptr<Graph>>& 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<std::unique_ptr<Graph>> 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<std::unique_ptr<Graph>> 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<std::string> seen;
// Stage graphs that require rebuild or are newly added.
std::map<std::string, std::unique_ptr<Graph>> 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<std::string>("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<Graph>(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<Graph>(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<std::string>& graph,
const SimpleJson& new_node_cfg, std::string& err) {
std::lock_guard<std::mutex> 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=<name>";
return false;
}
return found->UpdateNodeConfig(node_id, new_node_cfg, err);
}
std::vector<GraphSnapshot> GraphManager::ListGraphSnapshots() {
std::vector<GraphSnapshot> out;
std::lock_guard<std::mutex> 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<std::mutex> 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<std::string>& graph,
NodeSnapshot& out, std::string& err) {
std::lock_guard<std::mutex> 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=<name>";
return false;
}
// New Result-based API implementations
Result<SimpleJson> 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<std::string>& graph) {
std::string err;
if (!UpdateNodeConfig(node_id, graph, new_node_cfg, err)) {
return Status::Fail(err);
}
return Status::Ok();
}
Result<GraphSnapshot> GraphManager::GetGraph(const std::string& name) {
GraphSnapshot out;
std::string err;
if (!GetGraphSnapshot(name, out, err)) {
return Error(err);
}
return out;
}
Result<NodeSnapshot> GraphManager::GetNode(const std::string& node_id,
const std::optional<std::string>& graph) {
NodeSnapshot out;
std::string err;
if (!GetNodeSnapshot(node_id, graph, out, err)) {
return Error(err);
}
return out;
}
} // namespace rk3588