Skip to content

Commit 5c91882

Browse files
committed
feat(profiling): implement automatic stage profiling and reset functionality
1 parent 26eefcf commit 5c91882

18 files changed

Lines changed: 386 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ test-embedded:
255255
cargo check --package aimdb-embassy-adapter --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime"
256256
@printf "$(YELLOW) → Checking aimdb-embassy-adapter with network support on thumbv7em-none-eabihf target$(NC)\n"
257257
cargo check --package aimdb-embassy-adapter --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,embassy-net-support"
258+
@printf "$(YELLOW) → Checking aimdb-embassy-adapter with profiling on thumbv7em-none-eabihf target$(NC)\n"
259+
cargo check --package aimdb-embassy-adapter --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,profiling"
258260
@printf "$(YELLOW) → Checking aimdb-mqtt-connector (Embassy) on thumbv7em-none-eabihf target$(NC)\n"
259261
cargo check --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime"
260262
@printf "$(YELLOW) → Checking aimdb-mqtt-connector (Embassy + defmt) on thumbv7em-none-eabihf target$(NC)\n"

aimdb-client/src/connection.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ impl AimxClient {
124124
Ok(records)
125125
}
126126

127+
/// Reset stage profiling counters for every record on the server.
128+
///
129+
/// Requires the server to be built with the `profiling` feature and the
130+
/// connection to have write permission.
131+
pub async fn reset_stage_profiling(&mut self) -> ClientResult<serde_json::Value> {
132+
self.send_request("profiling.reset", None).await
133+
}
134+
127135
/// Get current value of a record
128136
pub async fn get_record(&mut self, name: &str) -> ClientResult<serde_json::Value> {
129137
let params = json!({ "record": name });

aimdb-core/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ metrics = ["dep:metrics", "std"] # Requires std for aggregation
3838

3939
# Automatic stage profiling (.source()/.tap()/.link() timing).
4040
# Independent of `metrics`; works in no_std (only needs heap + a runtime clock).
41-
profiling = ["alloc"]
41+
# `portable-atomic/critical-section` provides the 64-bit-atomic fallback on targets
42+
# without native 64-bit atomics (e.g. thumbv7em); it's a no-op where native atomics
43+
# exist, and embedded binaries already supply a `critical-section` impl.
44+
profiling = ["alloc", "portable-atomic/critical-section"]
4245

4346
# Testing features
4447
test-utils = ["std"]

aimdb-core/src/builder.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,14 @@ impl<R: aimdb_executor::Spawn + 'static> AimDb<R> {
12651265
self.inner.list_records()
12661266
}
12671267

1268+
/// Resets stage profiling counters for every record (feature `profiling`).
1269+
#[cfg(feature = "profiling")]
1270+
pub fn reset_stage_profiling(&self) {
1271+
for record in &self.inner.storages {
1272+
record.reset_profiling();
1273+
}
1274+
}
1275+
12681276
/// Try to get record's latest value as JSON by name (std only)
12691277
///
12701278
/// Convenience wrapper around `AimDbInner::try_latest_as_json()`.

aimdb-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pub use typed_record::{AnyRecord, AnyRecordExt, TypedRecord};
8484

8585
// Stage profiling exports (feature-gated)
8686
#[cfg(feature = "profiling")]
87-
pub use profiling::{RecordProfilingMetrics, StageMetrics};
87+
pub use profiling::{RecordProfilingMetrics, StageMetrics, StageProfilingInfo};
8888

8989
// Connector Infrastructure exports
9090
pub use connector::TopicResolverFn;

aimdb-core/src/profiling/info.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! Serializable snapshot of stage profiling metrics, for remote introspection.
2+
3+
extern crate alloc;
4+
use alloc::{string::String, vec::Vec};
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::profiling::{RecordProfilingMetrics, StageEntry};
9+
10+
/// A point-in-time snapshot of one execution stage's timing metrics.
11+
///
12+
/// Carried in `RecordMetadata::stage_profiling` and exposed by the
13+
/// `get_stage_profiling` MCP tool. All times are wall-clock nanoseconds.
14+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15+
pub struct StageProfilingInfo {
16+
/// Stage kind: `"source"`, `"tap"`, `"link"`, or `"transform"`.
17+
pub stage_type: String,
18+
/// Registration index within the stage kind (0-based).
19+
pub index: usize,
20+
/// Name assigned via `.with_name("...")`, if any.
21+
#[serde(skip_serializing_if = "Option::is_none")]
22+
pub name: Option<String>,
23+
/// Number of recorded invocations.
24+
pub call_count: u64,
25+
/// Cumulative wall-clock time, nanoseconds.
26+
pub total_time_ns: u64,
27+
/// Mean wall-clock time per invocation, nanoseconds.
28+
pub avg_time_ns: u64,
29+
/// Fastest recorded invocation, nanoseconds.
30+
pub min_time_ns: u64,
31+
/// Slowest recorded invocation, nanoseconds.
32+
pub max_time_ns: u64,
33+
}
34+
35+
impl StageProfilingInfo {
36+
fn from_entry(stage_type: &str, index: usize, entry: &StageEntry) -> Self {
37+
let m = &entry.metrics;
38+
Self {
39+
stage_type: String::from(stage_type),
40+
index,
41+
name: entry.name.clone(),
42+
call_count: m.call_count(),
43+
total_time_ns: m.total_time_ns(),
44+
avg_time_ns: m.avg_time_ns(),
45+
min_time_ns: m.min_time_ns(),
46+
max_time_ns: m.max_time_ns(),
47+
}
48+
}
49+
}
50+
51+
impl RecordProfilingMetrics {
52+
/// Returns a serializable snapshot of every registered stage's metrics,
53+
/// ordered sources → taps → links → transforms.
54+
pub fn snapshot(&self) -> Vec<StageProfilingInfo> {
55+
let mut out = Vec::new();
56+
for (kind, stages) in [
57+
("source", self.sources()),
58+
("tap", self.taps()),
59+
("link", self.links()),
60+
] {
61+
for (i, entry) in stages.iter().enumerate() {
62+
out.push(StageProfilingInfo::from_entry(kind, i, entry));
63+
}
64+
}
65+
out
66+
}
67+
}

aimdb-core/src/profiling/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
//! answers *which* stage is slow, not *why*. Use `tracing` / `tokio-console` for
1616
//! CPU-vs-I/O analysis.
1717
18+
mod info;
1819
mod record_profiling;
1920
mod stage_metrics;
2021

22+
pub use info::StageProfilingInfo;
2123
pub use record_profiling::{RecordProfilingMetrics, StageEntry};
2224
pub use stage_metrics::StageMetrics;
2325

aimdb-core/src/remote/handler.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,8 @@ where
579579
"graph.nodes" => handle_graph_nodes(db, request.id).await,
580580
"graph.edges" => handle_graph_edges(db, request.id).await,
581581
"graph.topo_order" => handle_graph_topo_order(db, request.id).await,
582+
#[cfg(feature = "profiling")]
583+
"profiling.reset" => handle_profiling_reset(db, config, request.id).await,
582584
_ => {
583585
#[cfg(feature = "tracing")]
584586
tracing::warn!("Unknown method: {}", request.method);
@@ -625,6 +627,37 @@ where
625627
Response::success(request_id, json!(records))
626628
}
627629

630+
/// Handles profiling.reset method
631+
///
632+
/// Clears stage profiling counters for every record. Requires write permission.
633+
#[cfg(all(feature = "std", feature = "profiling"))]
634+
async fn handle_profiling_reset<R>(
635+
db: &Arc<AimDb<R>>,
636+
config: &AimxConfig,
637+
request_id: u64,
638+
) -> Response
639+
where
640+
R: crate::RuntimeAdapter + crate::Spawn + 'static,
641+
{
642+
if matches!(
643+
config.security_policy,
644+
crate::remote::SecurityPolicy::ReadOnly
645+
) {
646+
return Response::error(
647+
request_id,
648+
"permission_denied",
649+
"profiling.reset requires write permission (ReadOnly security policy)".to_string(),
650+
);
651+
}
652+
653+
db.reset_stage_profiling();
654+
655+
#[cfg(feature = "tracing")]
656+
tracing::info!("Stage profiling counters reset");
657+
658+
Response::success(request_id, json!({ "reset": true }))
659+
}
660+
628661
/// Handles record.get method
629662
///
630663
/// Returns the current value of a record as JSON.

aimdb-core/src/remote/metadata.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ pub struct RecordMetadata {
7777
#[cfg(feature = "metrics")]
7878
#[serde(skip_serializing_if = "Option::is_none")]
7979
pub occupancy: Option<(usize, usize)>,
80+
81+
// ===== Stage profiling (feature-gated) =====
82+
/// Per-stage timing metrics (`.source()`/`.tap()`/`.link()`), if the
83+
/// `profiling` feature is enabled and any stage has been registered.
84+
#[cfg(feature = "profiling")]
85+
#[serde(skip_serializing_if = "Option::is_none")]
86+
pub stage_profiling: Option<std::vec::Vec<crate::profiling::StageProfilingInfo>>,
8087
}
8188

8289
impl RecordMetadata {
@@ -132,6 +139,8 @@ impl RecordMetadata {
132139
dropped_count: None,
133140
#[cfg(feature = "metrics")]
134141
occupancy: None,
142+
#[cfg(feature = "profiling")]
143+
stage_profiling: None,
135144
}
136145
}
137146

@@ -162,6 +171,18 @@ impl RecordMetadata {
162171
}
163172
self
164173
}
174+
175+
/// Attaches a stage profiling snapshot (profiling feature only).
176+
#[cfg(feature = "profiling")]
177+
pub fn with_stage_profiling(
178+
mut self,
179+
stages: std::vec::Vec<crate::profiling::StageProfilingInfo>,
180+
) -> Self {
181+
if !stages.is_empty() {
182+
self.stage_profiling = Some(stages);
183+
}
184+
self
185+
}
165186
}
166187

167188
#[cfg(test)]

0 commit comments

Comments
 (0)