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