Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 12 additions & 19 deletions crates/analyzer/src/fsm/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ use crate::{
resource::{Usage, Using},
};

/// Trait for types that hold a collection of [`Fsm`]s.
pub trait FsmCollection<F, T>
where
F: Fsm<TransitionType = T>,
T: Transition,
{
fn fsms<'a>(&'a self) -> impl Iterator<Item = &'a F> + 'a
where
F: 'a;
/// Trait for types that hold a collection of [`Fsm`]s of a single type.
///
/// An application with several FSM kinds unifies them under one [`Self::Fsm`]
/// type (e.g. an enum) so a single collection spans all of them.
pub trait FsmCollection {
/// The FSM type held by this collection.
type Fsm: Fsm;

fn contains_fsm_type(&self, type_name: &str) -> bool;
fn fsms(&self) -> impl Iterator<Item = &Self::Fsm>;
}

/// An in-memory collection of [`Fsm`]s.
Expand All @@ -36,20 +34,15 @@ where
pub fsm_type_names: HashSet<String>,
}

impl<F, T> FsmCollection<F, T> for InMemoryFsms<F, T>
impl<F, T> FsmCollection for InMemoryFsms<F, T>
where
F: Fsm<TransitionType = T>,
T: Transition,
{
fn fsms<'a>(&'a self) -> impl Iterator<Item = &'a F> + 'a
where
T: 'a,
{
self.fsms.values()
}
type Fsm = F;

fn contains_fsm_type(&self, type_name: &str) -> bool {
self.fsm_type_names.contains(type_name)
fn fsms(&self) -> impl Iterator<Item = &F> {
self.fsms.values()
}
}

Expand Down
114 changes: 7 additions & 107 deletions crates/analyzer/src/timeline/binned/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

use std::hash::Hash;

use quent_time::{SpanNanoSec, TimeNanoSec, bin::BinnedSpan};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use uuid::Uuid;
use quent_time::{SpanNanoSec, bin::BinnedSpan};
use rustc_hash::FxHashMap as HashMap;

use crate::{
AnalyzerResult,
Expand All @@ -29,36 +28,29 @@ fn convert_capacity(
pub struct ResourceTimeline<'a> {
pub config: BinnedSpan,
pub data: HashMap<&'a str, Vec<f64>>,
pub long_entities: Vec<Uuid>,
}

#[derive(Clone, Debug)]
pub struct ResourceTimelineByKey<'a, K> {
pub config: BinnedSpan,
pub data: HashMap<(K, &'a str), Vec<f64>>,
pub long_entities: Vec<Uuid>,
}

pub struct ResourceTimelineBuilder<'a> {
resource_type: &'a ResourceTypeDecl,
aggregator: KeyedAggregator<&'a str>,
long_entities: HashSet<Uuid>,
long_entities_threshold: Option<TimeNanoSec>,
}

impl<'a> ResourceTimelineBuilder<'a> {
pub fn try_new(
resource_type: &'a ResourceTypeDecl,
config: BinnedSpan,
long_entities_threshold: Option<TimeNanoSec>,
) -> AnalyzerResult<Self> {
// Construct the aggregator.
let aggregator = KeyedAggregator::new(config);
Ok(Self {
resource_type,
aggregator,
long_entities: HashSet::default(),
long_entities_threshold,
})
}

Expand All @@ -71,13 +63,6 @@ impl<'a> ResourceTimelineBuilder<'a> {
.try_push(usage.span(), (capacity.name, value))?
}
}

if let Some(threshold) = self.long_entities_threshold
&& usage.span().duration() > threshold
&& usage.span().intersects(&self.aggregator.config.span)
{
self.long_entities.insert(usage.entity_id());
}
Ok(())
}

Expand All @@ -95,16 +80,13 @@ impl<'a> ResourceTimelineBuilder<'a> {
ResourceTimeline {
config: self.aggregator.config,
data: self.aggregator.finish(),
long_entities: self.long_entities.into_iter().collect(),
}
}
}

pub struct ResourceTimelineByKeyBuilder<'a, K> {
resource_type: &'a ResourceTypeDecl,
aggregator: KeyedAggregator<(K, &'a str)>,
long_entities: HashSet<Uuid>,
long_entities_threshold: Option<TimeNanoSec>,
}

impl<'a, K> ResourceTimelineByKeyBuilder<'a, K>
Expand All @@ -114,15 +96,12 @@ where
pub fn try_new(
resource_type: &'a ResourceTypeDecl,
config: BinnedSpan,
long_entities_threshold: Option<TimeNanoSec>,
) -> AnalyzerResult<Self> {
let aggregator = KeyedAggregator::new(config);

Ok(Self {
resource_type,
aggregator,
long_entities: HashSet::default(),
long_entities_threshold,
})
}

Expand All @@ -134,13 +113,6 @@ where
.try_push(usage.span(), ((key.clone(), capacity.name), value))?
}
}

if let Some(threshold) = self.long_entities_threshold
&& usage.span().duration() > threshold
&& usage.span().intersects(&self.aggregator.config.span)
{
self.long_entities.insert(usage.entity_id());
}
Ok(())
}

Expand All @@ -158,7 +130,6 @@ where
ResourceTimelineByKey {
config: self.aggregator.config,
data: self.aggregator.finish(),
long_entities: self.long_entities.into_iter().collect(),
}
}
}
Expand All @@ -184,6 +155,8 @@ mod tests {
use super::*;

use quent_time::{SpanNanoSec, bin::BinnedSpan};
use rustc_hash::FxHashSet as HashSet;
use uuid::Uuid;

const ROOT_RESOURCE_ID: Uuid = Uuid::from_u64_pair(0, 1);

Expand Down Expand Up @@ -258,7 +231,6 @@ mod tests {
.resource_type(resources.resource(resource_id).unwrap().type_name())
.unwrap(),
config,
None,
)
.unwrap();
builder
Expand Down Expand Up @@ -351,7 +323,6 @@ mod tests {
NonZero::try_from(10).unwrap(),
)
.unwrap(),
None,
)
.unwrap();
builder
Expand Down Expand Up @@ -440,7 +411,6 @@ mod tests {
let mut builder = ResourceTimelineBuilder::try_new(
resources.resource_type_of(resource_id).unwrap(),
config,
None,
)
.unwrap();
builder
Expand Down Expand Up @@ -538,7 +508,6 @@ mod tests {
.resource_type(resources.resource(resource_id).unwrap().type_name())
.unwrap(),
config,
None,
)
.unwrap();

Expand Down Expand Up @@ -672,12 +641,9 @@ mod tests {
})
.collect::<HashSet<_>>();

let mut builder = ResourceTimelineByKeyBuilder::try_new(
resources.resource_type("test").unwrap(),
config,
None,
)
.unwrap();
let mut builder =
ResourceTimelineByKeyBuilder::try_new(resources.resource_type("test").unwrap(), config)
.unwrap();
for fsm in fsms.fsms() {
for (state_name, usage) in fsm.usages_with_state_names() {
if group_resources.contains(&usage.resource_id()) {
Expand Down Expand Up @@ -723,70 +689,4 @@ mod tests {
],
);
}

/// Don't include long entities outside the window.
#[test]
fn test_long_entities_outside_window_excluded() {
let resource_id = Uuid::now_v7();

let mut resources = InMemoryResourcesBuilder::default();
build_root_and_memory(&mut resources, resource_id);
let resources = resources.try_build().unwrap();

// Config window: [1000, 2000], threshold: 100 ns (all spans below exceed it)
let config = BinnedSpan::try_new(
SpanNanoSec::try_new(1000, 2000).unwrap(),
NonZero::try_from(10).unwrap(),
)
.unwrap();
let threshold = 100u64;

let resource_type = resources
.resource_type(resources.resource(resource_id).unwrap().type_name())
.unwrap();

let make_fsm = |start, end| {
RtFsm::try_new(
Uuid::now_v7(),
"test",
"test",
[
RtFsmTransition {
name: "using".into(),
usages: vec![RtFsmStateUsage::new(
resource_id,
[CapacityValue::new("capacity_bytes", 1)],
)],
timestamp: start,
attributes: vec![],
},
RtFsmTransition {
name: "exit".into(),
usages: vec![],
timestamp: end,
attributes: vec![],
},
],
)
.unwrap()
};

let mut outside_fsms = InMemoryFsms::<RtFsm, RtFsmTransition>::new();
outside_fsms.insert(make_fsm(0, 500));
outside_fsms.insert(make_fsm(2500, 3000));

let mut outside_builder =
ResourceTimelineBuilder::try_new(resource_type, config, Some(threshold)).unwrap();
outside_builder.try_extend(outside_fsms.usages()).unwrap();
assert!(!outside_builder.build().long_entities.contains(&resource_id));

let mut inside_fsms = InMemoryFsms::<RtFsm, RtFsmTransition>::new();
inside_fsms.insert(make_fsm(500, 1500));
inside_fsms.insert(make_fsm(1100, 1900));

let mut inside_builder =
ResourceTimelineBuilder::try_new(resource_type, config, Some(threshold)).unwrap();
inside_builder.try_extend(inside_fsms.usages()).unwrap();
assert!(inside_builder.build().long_entities.contains(&resource_id));
}
}
33 changes: 33 additions & 0 deletions crates/ui/src/bulk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Umbrella request bundling several UI queries into one round-trip.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use ts_rs::TS;

use crate::{
entities::{request::EntityListEntry, response::BulkEntityListResponse},
timeline::{request::TimelineRequest, response::BulkTimelinesResponse},
};

/// A single UI refresh: any combination of timeline and entity-list queries,
/// sharing one set of application parameters.
#[derive(TS, Debug, Clone, Serialize, Deserialize)]
pub struct BulkRequest<GlobalParams, TimelineParams> {
/// Application parameters shared by every query in the bundle.
pub app_params: GlobalParams,
/// Timeline queries, keyed by a caller-chosen id.
pub timelines: Option<HashMap<String, TimelineRequest<TimelineParams>>>,
/// Entity-list queries, keyed by a caller-chosen id.
pub entities: Option<HashMap<String, EntityListEntry>>,
}

/// Response to a [`BulkRequest`]; each section is present iff requested.
#[derive(TS, Debug, Serialize)]
pub struct BulkResponse {
pub timelines: Option<BulkTimelinesResponse>,
pub entities: Option<BulkEntityListResponse>,
}
6 changes: 6 additions & 0 deletions crates/ui/src/entities/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Requests and responses for entity-list queries.
pub mod request;
pub mod response;
Loading
Loading