Making zoom for timeline sticky, and adaptively coalesce intervals to limit payload sizes

This commit is contained in:
Kinesin Data Technologies Incorporated
2022-10-07 17:37:25 -03:00
parent 304e04cca9
commit 6042507ab7
6 changed files with 109 additions and 17 deletions
+25 -9
View File
@@ -146,16 +146,28 @@ struct TimelineGroup {
data: Vec<TimelineLabel>,
}
#[derive(Serialize, Deserialize)]
struct DetailedTimelineOptions {
#[serde(default)]
max_intervals: Option<usize>,
}
async fn get_detailed_timeline(
options: web::Query<DetailedTimelineOptions>,
span: web::Json<Interval>,
state: web::Data<AppState>,
) -> impl Responder {
let interval = span.into_inner();
let max_intervals = options.into_inner().max_intervals;
let (response, rx) = oneshot::channel();
state
.runner_tx
.send(RunnerMessage::GetResourceStateDetails { interval, response })
.send(RunnerMessage::GetResourceStateDetails {
interval,
response,
max_intervals,
})
.unwrap();
match rx.await {
@@ -201,32 +213,34 @@ async fn get_detailed_timeline(
/// What resources it relies on
/// Last attempt (if any)
async fn get_segment_details(
max_intervals: web::Query<Option<usize>>,
span: web::Json<Interval>,
state: web::Data<AppState>,
) -> impl Responder {
/*
let interval = span.into_inner();
let (response, rx) = oneshot::channel();
state
.runner_tx
.send(RunnerMessage::GetResourceStateDetails { interval, response })
.send(RunnerMessage::GetResourceStateDetails {
interval,
response,
max_intervals: max_intervals.into_inner(),
})
.unwrap();
match rx.await {
Ok(actions) => {
let mut timeline = Vec::new();
info!(
"Querying for actions over {}, got {} responses.",
interval,
actions.len()
);
for (resource, tasks) in actions {
let mut group = TimelineGroup {
group: resource.clone(),
data: Vec::new(),
};
for (task_name, intervals) in tasks.into_iter() {
for (task_name, mut intervals) in tasks.into_iter() {
// Collapse intervals
if intervals.len() > 50 {}
let data = intervals
.into_iter()
.map(|a| TimelineInterval {
@@ -249,6 +263,8 @@ async fn get_segment_details(
error: format!("{:?}", error),
}),
}
*/
HttpResponse::Ok()
}
/*
+1
View File
@@ -1,5 +1,6 @@
#![allow(unused_imports)]
#![allow(dead_code)]
#![feature(slice_group_by)]
use anyhow::{anyhow, Result};
use chrono::prelude::*;
+51 -4
View File
@@ -1,6 +1,7 @@
use super::*;
use futures::stream::futures_unordered::FuturesUnordered;
use futures::StreamExt;
use std::cmp::Ordering;
use std::collections::VecDeque;
/*
@@ -13,7 +14,7 @@ use std::collections::VecDeque;
- current = TaskSet::coverage (the theoretical)
*/
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, PartialOrd)]
pub enum ActionState {
Queued,
Running,
@@ -67,6 +68,7 @@ pub enum RunnerMessage {
GetResourceStateDetails {
interval: Interval,
response: oneshot::Sender<ResourceStateDetails>,
max_intervals: Option<usize>,
},
Stop,
}
@@ -237,6 +239,40 @@ fn delayed_event(delay: Duration, event: RunnerMessage) -> tokio::task::JoinHand
})
}
// Coalesces adjascent actions
fn coalesce_actions(mut actions: Vec<Action>) -> Vec<Action> {
if actions.is_empty() {
return actions;
}
actions.sort_unstable_by(|a, b| {
let ord = a.task.partial_cmp(&b.task).unwrap();
if ord == Ordering::Equal {
a.state.partial_cmp(&b.state).unwrap()
} else {
ord
}
});
let mut res: Vec<Action> = Vec::new();
for group in actions.group_by(|a, b| a.task == b.task && a.state == b.state) {
let intervals: Vec<Interval> = group.iter().map(|x| x.interval).collect();
let is = IntervalSet::from(intervals);
let task = group.first().unwrap().task;
let state = group.first().unwrap().state;
for interval in is.iter() {
res.push(Action {
task: task,
state: state,
interval: *interval,
})
}
}
res
}
impl Runner {
pub async fn new(
tasks: TaskSet,
@@ -375,6 +411,7 @@ impl Runner {
&self,
interval: Interval,
response: oneshot::Sender<ResourceStateDetails>,
max_intervals: Option<usize>,
) {
// HashMap<Resource, HashMap<String, Vec<(DateTime<Utc>, DateTime<Utc>, ActionState)>>>;
let mut res: ResourceStateDetails = HashMap::new();
@@ -396,13 +433,19 @@ impl Runner {
res.insert(resource.clone(), res_ints);
}
let actions: Vec<Action> = self
let mut actions: Vec<Action> = self
.actions
.iter()
.filter(|x| interval.is_contiguous(x.interval))
.cloned()
.collect();
if let Some(max_intv) = max_intervals {
if actions.len() > max_intv {
actions = coalesce_actions(actions);
}
}
info!(
"Filtered {} actions down to {}",
self.actions.len(),
@@ -444,8 +487,12 @@ impl Runner {
Some(Ok(RunnerMessage::Tick)) => {
self.tick();
}
Some(Ok(RunnerMessage::GetResourceStateDetails { interval, response })) => {
self.get_resource_state_details(interval, response);
Some(Ok(RunnerMessage::GetResourceStateDetails {
interval,
response,
max_intervals,
})) => {
self.get_resource_state_details(interval, response, max_intervals);
}
Some(Ok(RunnerMessage::ForceUp {
resources,
+8
View File
@@ -9,6 +9,7 @@ export default {
refreshSeconds: 15, // How often to refresh
waterfallURL: 'http://localhost:2503',
activeSegment: null,
maxDisplayIntervals: 500,
}
},
@@ -19,6 +20,10 @@ export default {
updateRefreshInterval(interval) {
this.refreshSeconds = interval;
},
updateMaxDisplayIntervals(cnt) {
this.maxDisplayIntervals = cnt;
},
setActiveSegment(segment) {
this.activeSegment = segment;
},
@@ -41,14 +46,17 @@ input { max-width: 25%; }
<GlobalSettings
:waterfallURL="waterfallURL"
:refreshSeconds="refreshSeconds"
:maxDisplayIntervals="maxDisplayIntervals"
@update-refresh-interval="(interval) => this.updateRefreshInterval(interval)"
@update-waterfall-url="(url) => this.updateURL(url)"
@update-max-display-intervals="(cnt) => this.updateMaxDisplayIntervals(cnt)"
/>
<br/>
<div>
<Timeline
:waterfallURL="waterfallURL"
:refreshSeconds="refreshSeconds"
:maxDisplayIntervals="maxDisplayIntervals"
@update-active-segment="(segment) => this.setActiveSegment(segment)"
/>
</div>
+18 -2
View File
@@ -1,17 +1,21 @@
<script>
export default {
props: ['refreshSeconds', 'waterfallURL'],
props: ['refreshSeconds', 'waterfallURL', 'maxDisplayIntervals'],
data() {
return {
interval: this.refreshSeconds,
url: this.waterfallURL,
max_display_intervals: this.maxDisplayIntervals,
};
},
emits: ['update-refresh-interval', 'update-waterfall-url'],
emits: ['update-refresh-interval', 'update-waterfall-url', 'update-max-intervals'],
computed: {
validRefreshIntervals() {
return [5, 10, 15, 30, 60, 300, 600];
},
validDisplayIntervals() {
return [0, 100, 250, 500, 1000, 1500];
},
isSelected(interval) {
return (interval === this.refreshSeconds ? 'selected' : 'unselected');
},
@@ -37,5 +41,17 @@ export default {
</option>
</select>
</label>
<label>
Max Display Intervals
<select @change="$emit('update-max-display-intervals', max_display_intervals)" v-model="max_display_intervals">
<option v-for="cnt in validDisplayIntervals"
:key="cnt"
:value="cnt"
>
{{ cnt }} Segments
</option>
</select>
</label>
</details>
</template>
+6 -2
View File
@@ -16,7 +16,7 @@ const MIN_TIME="1970-01-01T00:00:00Z";
const MAX_TIME="2099-01-01T00:00:00Z";
export default {
props: ['waterfallURL', 'refreshSeconds'],
props: ['waterfallURL', 'refreshSeconds', 'maxDisplayIntervals'],
data() {
return {
chart: null,
@@ -33,11 +33,15 @@ export default {
waterfallURL() {
this.fetchTimeline();
},
maxDisplayIntervals() {
this.fetchTimeline();
},
},
methods: {
async fetchTimeline() {
fetch(`${this.waterfallURL}/api/v1/details`, {
fetch(`${this.waterfallURL}/api/v1/details?max_intervals=${this.maxDisplayIntervals}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'