///| Human-oriented utilization band. The thresholds are intentionally
///| conservative: a resource at 85% has little room for disruptions even if
///|
/// it is not technically full.
pub enum UtilizationBand {
Idle
Light
Moderate
Busy
Saturated
} derive(Eq, Debug)
///|
pub fn UtilizationBand::render(self : UtilizationBand) -> String {
match self {
Idle => "idle"
Light => "light"
Moderate => "moderate"
Busy => "busy"
Saturated => "saturated"
}
}
///|
pub fn utilization_band(percent : Int) -> UtilizationBand {
if percent <= 0 {
Idle
} else if percent < 40 {
Light
} else if percent < 70 {
Moderate
} else if percent < 90 {
Busy
} else {
Saturated
}
}
///|
/// A resource-level report limited to one caller-supplied horizon.
pub struct ResourceReport {
resource_id : String
capacity : Int
available_minutes : Int
occupied_minutes : Int
free_minutes : Int
longest_free_minutes : Int
utilization_percent : Int
band : UtilizationBand
} derive(Debug)
///|
pub fn ResourceReport::resource_id(self : ResourceReport) -> String {
self.resource_id
}
///|
pub fn ResourceReport::capacity(self : ResourceReport) -> Int {
self.capacity
}
///|
pub fn ResourceReport::available_minutes(self : ResourceReport) -> Int {
self.available_minutes
}
///|
pub fn ResourceReport::occupied_minutes(self : ResourceReport) -> Int {
self.occupied_minutes
}
///|
pub fn ResourceReport::free_minutes(self : ResourceReport) -> Int {
self.free_minutes
}
///|
pub fn ResourceReport::longest_free_minutes(self : ResourceReport) -> Int {
self.longest_free_minutes
}
///|
pub fn ResourceReport::utilization_percent(self : ResourceReport) -> Int {
self.utilization_percent
}
///|
pub fn ResourceReport::free_percent(self : ResourceReport) -> Int {
if self.available_minutes == 0 {
0
} else {
self.free_minutes * 100 / self.available_minutes
}
}
///|
/// Total seat/equipment-minute capacity represented by this resource in the
///| report horizon. A room with capacity ten contributes ten times as much
///|
/// potential person-time as a single-seat resource.
pub fn ResourceReport::capacity_minutes(self : ResourceReport) -> Int {
self.capacity * self.available_minutes
}
///|
pub fn ResourceReport::band(self : ResourceReport) -> UtilizationBand {
self.band
}
///|
pub fn ResourceReport::render(self : ResourceReport) -> String {
self.resource_id +
" occupied=" +
self.occupied_minutes.to_string() +
"/" +
self.available_minutes.to_string() +
" utilization=" +
self.utilization_percent.to_string() +
"% " +
self.band.render()
}
///|
pub fn ResourceCalendar::report(
self : ResourceCalendar,
horizon : Interval,
) -> ResourceReport {
let available = self.availability().within(horizon)
let occupied = self.blocked().within(horizon).intersect(available)
let free = available.subtract(occupied)
let available_minutes = available.total_duration()
let occupied_minutes = occupied.total_duration()
let utilization_percent = if available_minutes == 0 {
0
} else {
occupied_minutes * 100 / available_minutes
}
{
resource_id: self.id(),
capacity: self.capacity(),
available_minutes,
occupied_minutes,
free_minutes: free.total_duration(),
longest_free_minutes: free.longest_duration(),
utilization_percent,
band: utilization_band(utilization_percent),
}
}
///|
/// Aggregated report over every resource in one planner snapshot.
pub struct PlannerReport {
horizon : Interval
resources : Array[ResourceReport]
available_minutes : Int
occupied_minutes : Int
free_minutes : Int
} derive(Debug)
///|
pub fn PlannerReport::horizon(self : PlannerReport) -> Interval {
self.horizon
}
///|
pub fn PlannerReport::resources(self : PlannerReport) -> Array[ResourceReport] {
self.resources.copy()
}
///|
pub fn PlannerReport::available_minutes(self : PlannerReport) -> Int {
self.available_minutes
}
///|
pub fn PlannerReport::occupied_minutes(self : PlannerReport) -> Int {
self.occupied_minutes
}
///|
pub fn PlannerReport::free_minutes(self : PlannerReport) -> Int {
self.free_minutes
}
///|
pub fn PlannerReport::resource_count(self : PlannerReport) -> Int {
self.resources.length()
}
///|
pub fn PlannerReport::utilization_percent(self : PlannerReport) -> Int {
if self.available_minutes == 0 {
0
} else {
self.occupied_minutes * 100 / self.available_minutes
}
}
///|
pub fn PlannerReport::most_utilized(self : PlannerReport) -> ResourceReport? {
let mut best : ResourceReport? = None
for report in self.resources {
match best {
None => best = Some(report)
Some(current) =>
if report.utilization_percent() > current.utilization_percent() ||
(
report.utilization_percent() == current.utilization_percent() &&
report.resource_id() < current.resource_id()
) {
best = Some(report)
}
}
}
best
}
///|
/// Return resources at or above an explicit utilization threshold. Keeping
///| the threshold caller-defined makes this report fit a lab, office or CI
///|
/// fleet without embedding a product-specific policy.
pub fn PlannerReport::bottlenecks(
self : PlannerReport,
minimum_percent : Int,
) -> Array[ResourceReport] {
let output : Array[ResourceReport] = []
for report in self.resources {
if report.utilization_percent() >= minimum_percent {
output.push(report)
}
}
output
}
///|
/// A compact, stable summary suitable for the CLI and a dashboard status
///|
/// card. Detailed per-resource values remain available through `resources`.
pub fn PlannerReport::render_summary(self : PlannerReport) -> String {
let busiest = match self.most_utilized() {
Some(report) =>
report.resource_id() +
"@" +
report.utilization_percent().to_string() +
"%"
None => "none"
}
"resources=" +
self.resource_count().to_string() +
" utilization=" +
self.utilization_percent().to_string() +
"% busiest=" +
busiest
}
///|
pub fn PlannerReport::render_bottlenecks(
self : PlannerReport,
minimum_percent : Int,
) -> String {
let mut output = ""
for report in self.bottlenecks(minimum_percent) {
if output.length() > 0 {
output = output + ", "
}
output = output + report.render()
}
if output.length() == 0 {
"none"
} else {
output
}
}
///|
pub fn Planner::report(self : Planner, horizon : Interval) -> PlannerReport {
let reports : Array[ResourceReport] = []
let mut available_minutes = 0
let mut occupied_minutes = 0
let mut free_minutes = 0
for resource in self.resources() {
let report = resource.report(horizon)
available_minutes = available_minutes + report.available_minutes()
occupied_minutes = occupied_minutes + report.occupied_minutes()
free_minutes = free_minutes + report.free_minutes()
reports.push(report)
}
{
horizon,
resources: reports,
available_minutes,
occupied_minutes,
free_minutes,
}
}