Skip to content

Commit 2f89787

Browse files
committed
feat(metrics): adding emitting metrics for component CPU and memory usage
Signed-off-by: Andrew Steurer <94206073+asteurer@users.noreply.github.com>
1 parent be8a5cd commit 2f89787

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

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.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ watchexec-filterer-globset = "8.0"
5656
spin-app = { path = "crates/app" }
5757
spin-build = { path = "crates/build" }
5858
spin-common = { path = "crates/common" }
59+
spin-factors-executor = { path = "crates/factors-executor" }
5960
spin-doctor = { path = "crates/doctor" }
6061
spin-environments = { path = "crates/environments" }
6162
spin-factor-outbound-networking = { path = "crates/factor-outbound-networking" }
@@ -106,12 +107,14 @@ vergen = { version = "^8.2.1", default-features = false, features = [
106107
] }
107108

108109
[features]
109-
default = ["llm"]
110+
default = ["llm", "cpu-time-metrics"]
110111
all-tests = ["extern-dependencies-tests"]
111112
extern-dependencies-tests = []
112113
llm = ["spin-runtime-factors/llm"]
113114
llm-metal = ["llm", "spin-runtime-factors/llm-metal"]
114115
llm-cublas = ["llm", "spin-runtime-factors/llm-cublas"]
116+
# This enables the collection and emission CPU time elapsed per component execution.
117+
cpu-time-metrics = ["spin-factors-executor/cpu-time-metrics"]
115118

116119
[workspace]
117120
members = [

crates/factors-executor/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ anyhow = { workspace = true }
1313
spin-app = { path = "../app" }
1414
spin-core = { path = "../core" }
1515
spin-factors = { path = "../factors" }
16+
spin-telemetry = { path = "../telemetry" }
17+
tracing = { workspace = true }
1618

1719
[dev-dependencies]
1820
spin-factor-wasi = { path = "../factor-wasi" }
@@ -21,3 +23,6 @@ tokio = { workspace = true, features = ["macros", "rt"] }
2123

2224
[lints]
2325
workspace = true
26+
27+
[features]
28+
cpu-time-metrics = ["spin-core/call-hook"]

crates/factors-executor/src/lib.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
use std::time::{Duration, Instant};
12
use std::{collections::HashMap, sync::Arc};
23

34
use anyhow::Context;
45
use spin_app::{App, AppComponent};
5-
use spin_core::{async_trait, Component};
6+
use spin_core::{async_trait, wasmtime::CallHook, Component};
67
use spin_factors::{
78
AsInstanceState, ConfiguredApp, Factor, HasInstanceBuilder, RuntimeFactors,
89
RuntimeFactorsInstanceState,
@@ -255,9 +256,24 @@ impl<T: RuntimeFactors, U: Send> FactorsInstanceBuilder<'_, T, U> {
255256
core: Default::default(),
256257
factors: self.factors.build_instance_state(self.factor_builders)?,
257258
executor: executor_instance_state,
259+
cpu_time_elapsed: Duration::from_millis(0),
260+
cpu_time_last_entry: None,
261+
memory_used_on_init: 0,
262+
component_id: self.app_component.id().into(),
258263
};
259264
let mut store = self.store_builder.build(instance_state)?;
265+
266+
#[cfg(feature = "cpu-time-metrics")]
267+
store.as_mut().call_hook(|mut store, hook| {
268+
CpuTimeCallHook.handle_call_event::<T, U>(store.data_mut(), hook)
269+
});
270+
260271
let instance = self.instance_pre.instantiate_async(&mut store).await?;
272+
273+
// Track memory usage after instantiation in the instance state.
274+
// Note: This only applies if the component has initial memory reservations.
275+
store.data_mut().memory_used_on_init = store.data().core_state().memory_consumed();
276+
261277
Ok((instance, store))
262278
}
263279

@@ -269,11 +285,41 @@ impl<T: RuntimeFactors, U: Send> FactorsInstanceBuilder<'_, T, U> {
269285
core: Default::default(),
270286
factors: self.factors.build_instance_state(self.factor_builders)?,
271287
executor: executor_instance_state,
288+
cpu_time_elapsed: Duration::from_millis(0),
289+
cpu_time_last_entry: None,
290+
memory_used_on_init: 0,
291+
component_id: self.app_component.id().into(),
272292
};
273293
self.store_builder.build(instance_state)
274294
}
275295
}
276296

297+
// Tracks CPU time used by a Wasm guest.
298+
#[allow(unused)]
299+
struct CpuTimeCallHook;
300+
301+
#[allow(unused)]
302+
impl CpuTimeCallHook {
303+
fn handle_call_event<T: RuntimeFactors, U>(
304+
&self,
305+
state: &mut InstanceState<T::InstanceState, U>,
306+
ch: CallHook,
307+
) -> anyhow::Result<()> {
308+
match ch {
309+
CallHook::CallingWasm | CallHook::ReturningFromHost => {
310+
debug_assert!(state.cpu_time_last_entry.is_none());
311+
state.cpu_time_last_entry = Some(Instant::now());
312+
}
313+
CallHook::ReturningFromWasm | CallHook::CallingHost => {
314+
let elapsed = state.cpu_time_last_entry.take().unwrap().elapsed();
315+
state.cpu_time_elapsed += elapsed;
316+
}
317+
}
318+
319+
Ok(())
320+
}
321+
}
322+
277323
/// InstanceState is the [`spin_core::Store`] `data` for an instance.
278324
///
279325
/// It is generic over the [`RuntimeFactors::InstanceState`] and any ad-hoc
@@ -282,6 +328,43 @@ pub struct InstanceState<T, U> {
282328
core: spin_core::State,
283329
factors: T,
284330
executor: U,
331+
/// The component ID.
332+
component_id: String,
333+
334+
/// The last time guest code started running in this instance.
335+
cpu_time_last_entry: Option<Instant>,
336+
/// The total CPU time elapsed actively running guest code in this instance.
337+
cpu_time_elapsed: Duration,
338+
/// The memory (in bytes) consumed on initialization.
339+
memory_used_on_init: u64,
340+
}
341+
342+
impl<T, U> Drop for InstanceState<T, U> {
343+
fn drop(&mut self) {
344+
// Record the component execution time.
345+
#[cfg(feature = "cpu-time-metrics")]
346+
spin_telemetry::metrics::histogram!(
347+
spin.component_cpu_time = self.cpu_time_elapsed.as_secs_f64(),
348+
component_id = self.component_id,
349+
// According to the OpenTelemetry spec, instruments measuring durations should use "s" as the unit.
350+
// See https://opentelemetry.io/docs/specs/semconv/general/metrics/#units
351+
unit = "s"
352+
);
353+
354+
// Record the component memory consumed on initialization.
355+
spin_telemetry::metrics::histogram!(
356+
spin.component_memory_used_on_init = self.memory_used_on_init,
357+
component_id = self.component_id,
358+
unit = "By"
359+
);
360+
361+
// Record the component memory consumed during execution.
362+
spin_telemetry::metrics::histogram!(
363+
spin.component_memory_used = self.core.memory_consumed(),
364+
component_id = self.component_id,
365+
unit = "By"
366+
);
367+
}
285368
}
286369

287370
impl<T, U> InstanceState<T, U> {

0 commit comments

Comments
 (0)