memory_accounting/
api.rs

1use std::{
2    collections::BTreeMap,
3    sync::{Arc, Mutex},
4};
5
6use saluki_api::{
7    extract::State,
8    response::IntoResponse,
9    routing::{get, Router},
10    APIHandler,
11};
12use serde::Serialize;
13
14use crate::{
15    allocator::{AllocationGroupRegistry, AllocationStatsSnapshot},
16    registry::ComponentMetadata,
17};
18
19#[derive(Serialize)]
20struct ComponentUsage {
21    minimum_required_bytes: usize,
22    firm_limit_bytes: usize,
23    actual_live_bytes: usize,
24}
25
26/// State used for the memory registry API handler.
27#[derive(Clone)]
28pub struct MemoryRegistryState {
29    inner: Arc<Mutex<ComponentMetadata>>,
30}
31
32impl MemoryRegistryState {
33    fn get_response(&self) -> String {
34        // The component registry is a nested structure, whereas the allocation registry is a flat structure. We can
35        // only iterate via closure with the allocation registry, so we do that, and for each entry, we look for it in
36        // the component registry. Luckily, the components in the allocation registry use the same naming structure as
37        // the component registry supports for doing nested lookups in a single call, so we can just use that.
38
39        // TODO: This is only going to show components which have taken a token, which means some components in the
40        // registry, the ones whose bounds _are_ display when we verify bounds at startup, won't have an entry here
41        // because they don't use a token... _and_ even if we made sure to include all components in the registry, even
42        // if they didn't have an allocation group associated, their usage might just be attributed to the root
43        // allocation group so then you end up with components that say they have a minimum usage, etc, but show a
44        // current usage of zero bytes, etc.
45        //
46        // One option we could try is:
47        // - get the total minimum/firm bounds by rolling up all components
48        // - as we iterate over each allocation group, we subtract the bounds of that component from the totals we
49        //   calculated before
50        // - at the end, we assign the remaining total bounds to the root component
51        //
52        // Essentially, because the memory usage for component with bounds that _don't_ use a token will inherently be
53        // attributed to the root allocation group anyways, this would allow at least capturing those bounds for
54        // comparison against the otherwise-unattributed usage.
55        //
56        // Realistically, we _should_ still have our code set up to track all allocations for components that declare
57        // bounds, but this could be a reasonable stopgap.
58        let empty_snapshot = AllocationStatsSnapshot::empty();
59
60        let mut component_usage = BTreeMap::new();
61
62        let mut inner = self.inner.lock().unwrap();
63        AllocationGroupRegistry::global().visit_allocation_groups(|component_name, component_stats| {
64            let component_meta = inner.get_or_create(component_name);
65            let component_meta = component_meta.lock().unwrap();
66            let bounds = component_meta.self_bounds();
67            let stats_snapshot = component_stats.snapshot_delta(&empty_snapshot);
68
69            component_usage.insert(
70                component_name.to_string(),
71                ComponentUsage {
72                    // TODO: figure out what's actually going on here, this might not be a valid change
73                    minimum_required_bytes: bounds.total_minimum_required_bytes(),
74                    firm_limit_bytes: bounds.total_firm_limit_bytes(),
75                    actual_live_bytes: stats_snapshot.live_bytes(),
76                },
77            );
78        });
79
80        serde_json::to_string(&component_usage).unwrap()
81    }
82}
83
84/// An API handler for reporting the memory bounds and usage of all components.
85///
86/// This handler exposes a single route -- `/memory/status` -- which returns the overall bounds and live usage of each
87/// registered component.
88pub struct MemoryAPIHandler {
89    state: MemoryRegistryState,
90}
91
92impl MemoryAPIHandler {
93    pub(crate) fn from_state(state: Arc<Mutex<ComponentMetadata>>) -> Self {
94        Self {
95            state: MemoryRegistryState { inner: state },
96        }
97    }
98
99    async fn status_handler(State(state): State<MemoryRegistryState>) -> impl IntoResponse {
100        state.get_response()
101    }
102}
103
104impl APIHandler for MemoryAPIHandler {
105    type State = MemoryRegistryState;
106
107    fn generate_initial_state(&self) -> Self::State {
108        self.state.clone()
109    }
110
111    fn generate_routes(&self) -> Router<Self::State> {
112        Router::new().route("/memory/status", get(Self::status_handler))
113    }
114}