This commit is contained in:
2026-08-27 16:36:29 +08:00
parent 63bb406a27
commit 8ffb642c36
36 changed files with 633 additions and 868 deletions
+17 -17
View File
@@ -42,7 +42,7 @@ public:
nlohmann::json schema() const override {
nlohmann::json components = nlohmann::json::array();
for (const auto& descriptor : descriptors) components.push_back(descriptor->schema());
return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}};
return {{"protocol", "aethera.plot.inspector"}, {"version", 3}, {"components", std::move(components)}};
}
nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) {
@@ -53,6 +53,20 @@ public:
result["component"] = component;
return result;
}
nlohmann::json component_state(std::string_view component) const override {
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) {
return item->id() == component;
});
if (found == descriptors.end())
return {{"success", false}, {"error", "unknown component"}};
return (*found)->state();
}
nlohmann::json component_snapshots() const override {
nlohmann::json result = nlohmann::json::object();
for (const auto& descriptor : descriptors)
result[std::string(descriptor->id())] = descriptor->snapshot();
return result;
}
nlohmann::json data_generator_schema() const override {
if (!data_generator) return nullptr;
return data_generator.schema;
@@ -85,15 +99,8 @@ template <typename Object, typename... Fields>
std::unique_ptr<detail::Renderable_Descriptor> make_renderable_component(
std::string id, std::string label, std::string kind, Object& object) {
using Definition = typename Object::Attached_Object;
using Tag = typename Definition::Base_Tag;
using State = typename Definition::State;
using Adapter = detail::Renderable_Adapter<Object, Fields...,
detail::Prop_Field < &Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">,
detail::State_Field<Tag, &State::render_dirty, "render_dirty", "Whether the render graph was dirty when evaluated.">,
detail::State_Field<Tag, &State::render_executed, "render_executed", "Whether the render graph executed during the latest scene cycle.">,
detail::State_Field<Tag, &State::graph_rebuilt, "graph_rebuilt", "Whether the render graph was rebuilt during the latest cycle.">,
detail::State_Field<Tag, &State::task_count, "task_count", "Number of tasks in the current render graph.">,
detail::State_Field<Tag, &State::execution_time_ns, "execution_time_ns", "Measured render graph execution time in nanoseconds."> >;
detail::Prop_Field < &Renderable_2D::Prop::cache_enabled, "cache_enabled", "Whether this renderable reuses its complete color result across unchanged frames.">>;
return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object});
}
template <typename Scene_Object>
@@ -103,14 +110,7 @@ std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object
using Adapter = detail::Renderable_Adapter<Scene_Object,
detail::Prop_Field < &Prop::viewport, "viewport", "Final scene viewport in physical pixels.">,
detail::Prop_Field < &Prop::background, "background", "Scene clear color." >,
detail::Prop_Field < &Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_dependency_count, "taskflow_dependency_count", "Number of graph dependencies.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds."> >;
detail::Prop_Field < &Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
}
template <typename Axis_Object>
+16 -5
View File
@@ -225,10 +225,7 @@ public:
using State = typename Definition::State;
using Scene_Adapter = detail::Renderable_Adapter<Scene_3D,
detail::Prop_Field < &Render_Scene_3D::Prop::clear_color, "clear_color", "Linear scene clear color.">,
detail::Prop_Field < &Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >,
detail::State_Field<Scene::Base_Tag, &Scene::State::renderable_count, "renderable_count", "Number of renderables attached to the scene.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_task_count, "taskflow_task_count", "Number of tasks in the scene graph.">,
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds."> >;
detail::Prop_Field < &Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames." >>;
using Visual_Adapter = detail::Renderable_Adapter<Visual_Object,
detail::Prop_Field < &Prop::transform, "transform", "World transform applied to the complete visual.">,
detail::Prop_Field < &Prop::visible, "visible", "Whether the visual participates in rendering." >,
@@ -287,7 +284,7 @@ public:
[[nodiscard]] nlohmann::json schema() const override {
nlohmann::json components = nlohmann::json::array();
for (const auto& descriptor : descriptors_) components.push_back(descriptor->schema());
return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}};
return {{"protocol", "aethera.plot.inspector"}, {"version", 3}, {"components", std::move(components)}};
}
[[nodiscard]] nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
@@ -298,6 +295,20 @@ public:
result["component"] = component;
return result;
}
[[nodiscard]] nlohmann::json component_state(std::string_view component) const override {
const auto found = std::ranges::find_if(descriptors_, [&](const auto& descriptor) {
return descriptor->id() == component;
});
if (found == descriptors_.end())
return {{"success", false}, {"error", "unknown component"}};
return (*found)->state();
}
[[nodiscard]] nlohmann::json component_snapshots() const override {
nlohmann::json result = nlohmann::json::object();
for (const auto& descriptor : descriptors_)
result[std::string(descriptor->id())] = descriptor->snapshot();
return result;
}
[[nodiscard]] nlohmann::json data_generator_schema() const override {
if constexpr (std::same_as<Data_Generator, Random_Data_Generator>) {
using Definition = typename Visual_Object::Attached_Object;
+38 -20
View File
@@ -198,7 +198,9 @@ void append_event_statistics_json(nlohmann::json& output,
}
nlohmann::json taskflow_trace_json(const Taskflow_Frame_Trace& trace) {
nlohmann::json taskflow_trace_json(
const Taskflow_Frame_Trace& trace,
const nlohmann::json& component_snapshots) {
nlohmann::json markers = nlohmann::json::object();
for (const auto& marker : trace.markers)
markers[magic_enum::enum_name(marker.marker)] =
@@ -218,12 +220,24 @@ nlohmann::json taskflow_trace_json(const Taskflow_Frame_Trace& trace) {
nlohmann::json attributes = nlohmann::json::object();
for (const auto& [key, value] : node.attributes)
attributes[key] = value;
nodes.push_back({
nlohmann::json encoded{
{"native_id", std::to_string(node.native_id)}, {"id", node.node_id},
{"parent_id", node.parent_node_id}, {"name", node.name},
{"type", node.type}, {"predecessors", std::move(predecessors)},
{"successors", std::move(successors)},
{"attributes", std::move(attributes)}});
{"attributes", std::move(attributes)}};
const auto owner = encoded["attributes"].value(
"owner_component", std::string{});
if (!owner.empty() && component_snapshots.contains(owner)) {
const auto& snapshot = component_snapshots.at(owner);
encoded["owner"] = {
{"component", owner},
{"label", snapshot.value("label", owner)},
{"kind", snapshot.value("kind", std::string{})}};
encoded["prop"] = snapshot.value("prop", nlohmann::json::object());
encoded["state"] = snapshot.value("state", nlohmann::json::object());
}
nodes.push_back(std::move(encoded));
}
graphs.push_back({
{"stage", graph.stage}, {"name", graph.taskflow_name},
@@ -374,11 +388,11 @@ struct Plot::Private {
/* 高 32 位 requested,低 32 位 captured。每槽只发布一次不可变 Trace,
* GET */
std::atomic_uint64_t taskflow_trace_control{};
std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames> taskflow_trace_slots{};
std::atomic_size_t post_publish_trace_remaining{}; /* 仅捕获 publish 后外接 DAG 的剩余样本。 */
std::atomic_uint64_t post_publish_trace_control{}; /* 高 32 位 requested,低 32 位 captured。 */
std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames> post_publish_trace_slots{};
Task_Node completion_tail{}; /* Scene 图内固定停在 plot.frame.publish。 */
Task_Graph post_publish_graph{"plot.post_publish"}; /* publish 后外接 DAG;不再占用 Scene render admission。 */
@@ -428,13 +442,14 @@ struct Plot::Private {
[[nodiscard]] bool mark_taskflow_trace(Render_Frame& frame);
[[nodiscard]] bool mark_post_publish_taskflow_trace();
void store_trace(std::atomic_uint64_t& control,
std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots,
const Taskflow_Frame_Trace& trace);
const Taskflow_Frame_Trace& trace,
const nlohmann::json& component_snapshots = {});
[[nodiscard]] nlohmann::json trace_response(
const std::atomic_uint64_t& control,
const std::atomic_size_t& remaining,
const std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots) const;
void fail(std::exception_ptr failure) noexcept;
};
@@ -610,10 +625,12 @@ bool Plot::Private::mark_post_publish_taskflow_trace() {
void Plot::Private::store_trace(
std::atomic_uint64_t& control,
std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots,
const Taskflow_Frame_Trace& value) {
auto trace = std::make_shared<const Taskflow_Frame_Trace>(value);
const Taskflow_Frame_Trace& value,
const nlohmann::json& component_snapshots) {
auto trace = std::make_shared<const nlohmann::json>(
taskflow_trace_json(value, component_snapshots));
auto state = control.load(std::memory_order_acquire);
for (;;) {
const auto requested = static_cast<std::uint32_t>(state >> 32U);
@@ -632,7 +649,7 @@ void Plot::Private::store_trace(
nlohmann::json Plot::Private::trace_response(
const std::atomic_uint64_t& control,
const std::atomic_size_t& remaining,
const std::array<std::atomic<std::shared_ptr<const Taskflow_Frame_Trace>>,
const std::array<std::atomic<std::shared_ptr<const nlohmann::json>>,
maximum_taskflow_trace_frames>& slots) const {
nlohmann::json frames = nlohmann::json::array();
const auto state = control.load(std::memory_order_acquire);
@@ -640,7 +657,7 @@ nlohmann::json Plot::Private::trace_response(
const auto captured = static_cast<std::uint32_t>(state);
for (std::uint32_t index = 0; index < captured; ++index)
if (const auto trace = slots[index].load(std::memory_order_acquire))
frames.push_back(taskflow_trace_json(*trace));
frames.push_back(*trace);
const auto left = remaining.load(std::memory_order_acquire);
return {
{"protocol", "aethera.taskflow.frames"}, {"version", 1},
@@ -961,15 +978,12 @@ void Plot::Private::retire_completed_frame(Render_Frame* frame) {
{
std::lock_guard lock(completed_frame_statistics_mutex);
completed_frame_statistics_state = completed_frame_statistics.submit(
*frame, std::holds_alternative<std::unique_ptr<Frame_2D>>(managed->frame)
? Frame_Dimension::two_dimensional
: Frame_Dimension::three_dimensional);
completed_frame_statistics_state = completed_frame_statistics.submit(*frame);
}
if (frame->taskflow_trace_requested())
store_trace(taskflow_trace_control, taskflow_trace_slots,
frame->take_taskflow_trace());
frame->take_taskflow_trace(), view->component_snapshots());
auto expected = Frame_State::consuming;
if (!managed->state.compare_exchange_strong(
@@ -1174,6 +1188,10 @@ nlohmann::json Plot::write_prop(std::string_view component,
return result;
}
nlohmann::json Plot::component_state(std::string_view component) const {
return d->view->component_state(component);
}
nlohmann::json Plot::generate_data(const nlohmann::json& input) {
ensure_started();
return d->view->generate_data(input);
@@ -1234,7 +1252,7 @@ nlohmann::json Plot::diagnostics() const {
const std::size_t byte_length = pacing.video_enabled
? static_cast<std::size_t>(stream.width) * stream.height * 4U : 0U;
nlohmann::json output{
{"protocol", "aethera.plot.diagnostics"}, {"version", 2},
{"protocol", "aethera.plot.diagnostics"}, {"version", 3},
{"dimension", is_3d ? "3D" : "2D"},
{"sequence", identity.sequence},
{"correlation_id", identity.correlation_id},
@@ -1356,7 +1374,7 @@ nlohmann::json Plot::post_publish_taskflow_trace() const {
}
void Plot::reset_diagnostics() {
std::visit([](auto& scene) { scene->reset_frame_statistics(); }, d->scene);
std::visit([](auto& scene) { scene->reset_diagnostics(); }, d->scene);
std::lock_guard lock(d->completed_frame_statistics_mutex);
d->completed_frame_statistics.reset();
d->completed_frame_statistics_state = {};
+4
View File
@@ -65,6 +65,9 @@ public:
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view component,
std::string_view key,
const nlohmann::json& value) = 0;
[[nodiscard]] virtual nlohmann::json component_state(
std::string_view component) const = 0;
[[nodiscard]] virtual nlohmann::json component_snapshots() const = 0;
[[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0;
[[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0;
virtual void update(const Plot_Render_Tick& tick) = 0;
@@ -86,6 +89,7 @@ public:
[[nodiscard]] nlohmann::json write_prop(std::string_view component,
std::string_view key,
const nlohmann::json& value);
[[nodiscard]] nlohmann::json component_state(std::string_view component) const;
[[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input);
[[nodiscard]] nlohmann::json diagnostics() const;
/* 清空旧捕获并请求接下来实际完成的 frame_count 帧 Task DAG。 */
+4
View File
@@ -23,6 +23,8 @@ public:
virtual ~Renderable_Descriptor() = default;
[[nodiscard]] virtual std::string_view id() const noexcept = 0;
[[nodiscard]] virtual nlohmann::json schema() const = 0;
[[nodiscard]] virtual nlohmann::json state() const = 0;
[[nodiscard]] virtual nlohmann::json snapshot() const = 0;
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) = 0;
};
template <typename Adapter>
@@ -31,6 +33,8 @@ public:
Renderable_Descriptor_Model(std::string id, std::string label, std::string kind, Adapter adapter);
[[nodiscard]] std::string_view id() const noexcept override;
[[nodiscard]] nlohmann::json schema() const override;
[[nodiscard]] nlohmann::json state() const override;
[[nodiscard]] nlohmann::json snapshot() const override;
[[nodiscard]] nlohmann::json write_prop(std::string_view key, const nlohmann::json& value) override;
private:
std::string component_id;
+34 -18
View File
@@ -44,6 +44,11 @@ template <typename Object, typename... Fields>
struct Renderable_Adapter final : public structive::Property_Object<Renderable_Adapter<Object, Fields...>, structive::No_Lock_Policy> {
public:
explicit Renderable_Adapter(Object& value) : object(&value) {}
void identify(std::string_view id) {
if constexpr (std::derived_from<Object, aethera::Renderable>)
double_buffer::detail::Internal_Access::layer<aethera::Renderable>(object)
.diagnostic_component_id = id;
}
private:
template <typename Adapter, typename Field>
friend struct Renderable_Field_Accessor;
@@ -287,15 +292,7 @@ inline std::string_view protocol_field_label(std::string_view key) {
{"selected_regions", "已选区域"}, {"cache_enabled", "启用绘制缓存"},
{"transform", "空间变换"}, {"visible", "是否可见"},
{"depth_test", "深度测试"}, {"items", "项目数据"},
{"render_dirty", "渲染图待更新"},
{"render_executed", "渲染图已执行"},
{"graph_rebuilt", "渲染图已重建"},
{"task_count", "任务数量"},
{"execution_time_ns", "渲染耗时"},
{"taskflow_rebuilt", "场景任务图已重建"}, {"renderable_count", "可渲染对象数量"},
{"taskflow_task_count", "场景任务数量"}, {"taskflow_dependency_count", "任务依赖数量"},
{"taskflow_max_predecessors", "最大前驱数量"}, {"taskflow_max_successors", "最大后继数量"},
{"taskflow_execution_time_ns", "场景执行耗时"}, {"next_tick", "下一时间刻度"},
{"next_tick", "下一时间刻度"},
{"sample_count", "样本数量"}, {"rendered_point_count", "已绘制点数量"},
{"selectable_marker_count", "可选标记数量"}, {"stored_block_count", "已保存数据块数量"},
{"stored_point_count", "已保存数据点数量"}, {"history_count", "历史帧数量"},
@@ -312,11 +309,9 @@ inline std::string_view protocol_field_label(std::string_view key) {
template <typename Adapter>
nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::string_view label, std::string_view kind) {
nlohmann::json fields = nlohmann::json::array();
nlohmann::json state = nlohmann::json::object();
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
using Field = std::remove_cvref_t<decltype(field)>;
using Value = typename Field::value_type;
const auto value = field.accessor.read(adapter);
const bool editable = field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop;
const auto field_label = protocol_field_label(field.key());
std::string_view editor = protocol_editor<Value>();
@@ -327,9 +322,9 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
{"key", field.key()}, {"label", field_label}, {"editor", editor},
{"editable", editable},
{"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"},
{"technical_description", std::string(Field::accessor_type::description.view())},
{"value", encode_protocol_value(value)}
{"technical_description", std::string(Field::accessor_type::description.view())}
};
if (editable) item["value"] = encode_protocol_value(field.accessor.read(adapter));
if constexpr (std::same_as<Value, render_3d::Linear_Color>) item["color_channel_scale"] = "normalized";
else if constexpr (std::same_as<Value, Color>) item["color_channel_scale"] = "byte";
if (editable) {
@@ -341,17 +336,23 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
}
}
}
else {
state[field.key()] = encode_protocol_value(value);
}
fields.push_back(std::move(item));
});
return {
{"id", id}, {"label", label}, {"kind", kind},
{"fields", std::move(fields)}, {"state", std::move(state)}
{"fields", std::move(fields)}
};
}
template <typename Adapter>
nlohmann::json adapter_values(const Adapter& adapter, Renderable_Field_Role role) {
nlohmann::json result = nlohmann::json::object();
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
if (field.template attribute<Renderable_Field_Role_Category>().value == role)
result[field.key()] = encode_protocol_value(field.accessor.read(adapter));
});
return result;
}
template <typename Adapter>
nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const nlohmann::json& input) {
nlohmann::json result{{"success", false}, {"key", key}};
const bool found = structive::visit_schema_property(structive::type_descriptor<Adapter>(), key, [&](auto, const auto& field) {
@@ -382,7 +383,9 @@ nlohmann::json write_adapter_prop(Adapter& adapter, std::string_view key, const
}
template <typename Adapter>
Renderable_Descriptor_Model<Adapter>::Renderable_Descriptor_Model(
std::string id, std::string label, std::string kind, Adapter value) : component_id(std::move(id)), component_label(std::move(label)), component_kind(std::move(kind)), adapter(std::move(value)) {}
std::string id, std::string label, std::string kind, Adapter value) : component_id(std::move(id)), component_label(std::move(label)), component_kind(std::move(kind)), adapter(std::move(value)) {
adapter.identify(component_id);
}
template <typename Adapter>
std::string_view Renderable_Descriptor_Model<Adapter>::id() const noexcept {
return component_id;
@@ -392,6 +395,19 @@ nlohmann::json Renderable_Descriptor_Model<Adapter>::schema() const {
return adapter_schema(adapter, component_id, component_label, component_kind);
}
template <typename Adapter>
nlohmann::json Renderable_Descriptor_Model<Adapter>::state() const {
return {{"protocol", "aethera.component.state"}, {"version", 1},
{"component", component_id},
{"state", adapter_values(adapter, Renderable_Field_Role::state)}};
}
template <typename Adapter>
nlohmann::json Renderable_Descriptor_Model<Adapter>::snapshot() const {
return {{"component", component_id}, {"label", component_label},
{"kind", component_kind},
{"prop", adapter_values(adapter, Renderable_Field_Role::prop)},
{"state", adapter_values(adapter, Renderable_Field_Role::state)}};
}
template <typename Adapter>
nlohmann::json Renderable_Descriptor_Model<Adapter>::write_prop(std::string_view key, const nlohmann::json& value) {
return write_adapter_prop(adapter, key, value);
}
+2 -1
View File
@@ -4,5 +4,6 @@
namespace aethera::web {
[[nodiscard]] nlohmann::json taskflow_trace_json(
const Taskflow_Frame_Trace& trace);
const Taskflow_Frame_Trace& trace,
const nlohmann::json& component_snapshots = {});
}
+22
View File
@@ -442,6 +442,28 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
}
}, {drogon::Put});
app.registerHandler("/plot/{1}/component/{2}/state", [plots](
const drogon::HttpRequestPtr&,
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
std::string plot_id,
std::string component) {
auto plot = find_plot(*plots, plot_id);
if (!plot) {
callback(error_response(drogon::k404NotFound, "unknown plot"));
return;
}
try {
auto result = plot->component_state(component);
if (result.value("success", true)) callback(json_response(std::move(result)));
else callback(error_response(drogon::k404NotFound,
result.value("error", "unknown component")));
}
catch (const std::exception& failure) {
callback(error_response(drogon::k500InternalServerError,
failure.what()));
}
}, {drogon::Get});
app.registerHandler("/plot/{1}/data/generate", [plots](
const drogon::HttpRequestPtr& request,
std::function<void(const drogon::HttpResponsePtr&)>&& callback,