WIP: GUI for seeing the metrics
This commit is contained in:
parent
f8e6eea2ff
commit
a78d384252
327
bare-metrics-gui/src/graph.rs
Normal file
327
bare-metrics-gui/src/graph.rs
Normal file
@ -0,0 +1,327 @@
|
||||
use bare_metrics_core::structures::{Frame, MetricDescriptor, MetricId, UnixTimestampMilliseconds};
|
||||
use bare_metrics_reader::{MetricsLogReader, SeekToken};
|
||||
use eframe::egui::{
|
||||
Color32, Frame as EguiFrame, PointerButton, Pos2, Rect, Sense, Stroke, Ui, Vec2,
|
||||
};
|
||||
use log::{debug, error, info};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::io::{Read, Seek};
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Make a checkpoint every 10 minutes?
|
||||
/// This should probably be tunable since it will likely vary per-source ...
|
||||
pub const CHECKPOINT_EVERY_MILLISECONDS: u64 = 600_000_000;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MetricsWindow {
|
||||
time_range: RangeInclusive<f64>,
|
||||
wanted_time_points: u32,
|
||||
metrics: MetricDescriptorTable,
|
||||
histograms: HashMap<MetricId, HistogramWindow>,
|
||||
counters: HashMap<MetricId, ScalarWindow>,
|
||||
gauges: HashMap<MetricId, ScalarWindow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HistogramWindow {
|
||||
map: Vec<(
|
||||
UnixTimestampMilliseconds,
|
||||
UnixTimestampMilliseconds,
|
||||
HistogramWindow,
|
||||
)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScalarWindow {
|
||||
points: Vec<(UnixTimestampMilliseconds, f64)>,
|
||||
}
|
||||
|
||||
pub struct MetricsLogReadingRequester {
|
||||
shared: Arc<MetricsLogReadingShared>,
|
||||
// todo sender:
|
||||
}
|
||||
|
||||
pub struct MetricsLogReadingShared {
|
||||
/// The current window of data.
|
||||
pub current_window: RwLock<MetricsWindow>,
|
||||
/// True if a new window of data is being loaded.
|
||||
pub loading_new_window: AtomicBool,
|
||||
}
|
||||
|
||||
pub enum MetricsLogReaderMessage {
|
||||
LoadNewWindow {
|
||||
/// Callback that gets called when there's a state change.
|
||||
/// The bool represents whether the loading is finished or not.
|
||||
on_state_change: Box<dyn Fn(bool) -> () + 'static + Send>,
|
||||
|
||||
new_time_range: RangeInclusive<f64>,
|
||||
new_wanted_time_points: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl MetricsLogReadingRequester {
|
||||
pub fn new_manager<R: Read + Seek + Send + 'static>(
|
||||
reader: MetricsLogReader<R>,
|
||||
) -> MetricsLogReadingRequester {
|
||||
let shared = Arc::new(MetricsLogReadingShared {
|
||||
current_window: RwLock::new(MetricsWindow {
|
||||
time_range: 0.0..=0.0,
|
||||
wanted_time_points: 0,
|
||||
metrics: Default::default(),
|
||||
histograms: Default::default(),
|
||||
counters: Default::default(),
|
||||
gauges: Default::default(),
|
||||
}),
|
||||
loading_new_window: AtomicBool::new(false),
|
||||
});
|
||||
let manager_shared_ref = shared.clone();
|
||||
let requester = MetricsLogReadingRequester { shared };
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("metricslogreader".to_string())
|
||||
.spawn(move || {
|
||||
if let Err(err) =
|
||||
MetricsLogReaderManager::new_and_run(manager_shared_ref, reader, rx)
|
||||
{
|
||||
error!("Error in background log reader: {:?}", err);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
requester
|
||||
}
|
||||
}
|
||||
|
||||
/// We don't track histograms because they don't 'accumulate'; the histograms are emitted every
|
||||
/// frame when there are any samples.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MetricsState {
|
||||
at: UnixTimestampMilliseconds,
|
||||
gauges: HashMap<MetricId, f64>,
|
||||
counters: HashMap<MetricId, u64>,
|
||||
}
|
||||
|
||||
pub struct MetricsLogReaderManager<R: Read + Seek> {
|
||||
shared_ref: Arc<MetricsLogReadingShared>,
|
||||
reader: MetricsLogReader<R>,
|
||||
rx: Receiver<MetricsLogReaderMessage>,
|
||||
metric_descriptors: HashMap<MetricId, MetricDescriptor>,
|
||||
metric_descriptors_dirty: bool,
|
||||
metric_descriptor_table: MetricDescriptorTable,
|
||||
checkpoints: Vec<(MetricsState, SeekToken)>,
|
||||
}
|
||||
|
||||
/// We use B-Tree maps because they sort properly. That's useful for ensuring the UI is consistent
|
||||
/// between re-runs and makes it easier to confidently order the UI and assign colours etc...
|
||||
pub type MetricDescriptorTable = BTreeMap<String, BTreeMap<BTreeMap<String, String>, MetricId>>;
|
||||
|
||||
impl<R: Read + Seek> MetricsLogReaderManager<R> {
|
||||
fn initial_scan(&mut self) -> anyhow::Result<()> {
|
||||
let mut state = MetricsState {
|
||||
at: UnixTimestampMilliseconds(0),
|
||||
gauges: HashMap::with_capacity(0),
|
||||
counters: HashMap::with_capacity(0),
|
||||
};
|
||||
let mut next_checkpoint_at = 0u64;
|
||||
|
||||
while let Some((old_seek_token, old_end_ts, frame)) = self.reader.read_frame_rewindable()? {
|
||||
state.at = old_end_ts;
|
||||
if state.at.0 > next_checkpoint_at {
|
||||
// Make a checkpoint here!
|
||||
self.checkpoints.push((state.clone(), old_seek_token));
|
||||
next_checkpoint_at = state.at.0 + CHECKPOINT_EVERY_MILLISECONDS;
|
||||
}
|
||||
|
||||
// Integrate the state...
|
||||
// We have no need for histogram state because they emit every time there are any
|
||||
// samples (not just on changes).
|
||||
for (metric_id, count) in frame.counter_updates {
|
||||
state.counters.insert(metric_id, count.0);
|
||||
}
|
||||
for (metric_id, value) in frame.gauge_updates {
|
||||
state.gauges.insert(metric_id, value);
|
||||
}
|
||||
|
||||
// Also keep track of new metrics
|
||||
for (new_metric, metric_descriptor) in frame.new_metrics {
|
||||
self.metric_descriptors
|
||||
.insert(new_metric, metric_descriptor);
|
||||
self.metric_descriptors_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_metric_descriptors(&mut self) {
|
||||
if !self.metric_descriptors_dirty {
|
||||
return;
|
||||
}
|
||||
self.metric_descriptors_dirty = false;
|
||||
|
||||
let mut metric_names_to_labels_to_ids: MetricDescriptorTable = BTreeMap::new();
|
||||
for (metric_id, metric_descriptor) in self.metric_descriptors.iter() {
|
||||
let labels_to_ids = metric_names_to_labels_to_ids
|
||||
.entry(metric_descriptor.name.to_string())
|
||||
.or_insert_with(BTreeMap::new);
|
||||
labels_to_ids.insert(metric_descriptor.labels.clone(), *metric_id);
|
||||
}
|
||||
|
||||
self.metric_descriptor_table = metric_names_to_labels_to_ids;
|
||||
}
|
||||
|
||||
fn find_checkpoint_just_before(
|
||||
&mut self,
|
||||
just_before_or_at: UnixTimestampMilliseconds,
|
||||
) -> anyhow::Result<SeekToken> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn seek_to_first_before(
|
||||
&mut self,
|
||||
just_before_or_at: UnixTimestampMilliseconds,
|
||||
) -> anyhow::Result<(SeekToken, Option<Frame>)> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn run(&mut self) -> anyhow::Result<()> {
|
||||
info!("Starting manager");
|
||||
self.initial_scan()?;
|
||||
self.update_metric_descriptors();
|
||||
info!("Initial scan done.");
|
||||
|
||||
while let Ok(msg) = self.rx.recv() {
|
||||
match msg {
|
||||
MetricsLogReaderMessage::LoadNewWindow {
|
||||
on_state_change,
|
||||
new_time_range,
|
||||
new_wanted_time_points,
|
||||
} => {
|
||||
self.shared_ref
|
||||
.loading_new_window
|
||||
.store(true, Ordering::SeqCst);
|
||||
on_state_change(false);
|
||||
todo!();
|
||||
|
||||
let start = if *new_time_range.start() == f64::NEG_INFINITY {
|
||||
// autoscale by getting the earliest time point of the metrics log
|
||||
self.reader.header.start_time.0
|
||||
} else {
|
||||
(*new_time_range.start() * 1000.0) as u64
|
||||
};
|
||||
let end = if *new_time_range.end() == f64::INFINITY {
|
||||
// get the first result before the end of time...
|
||||
// or in other words, the last result.
|
||||
let (_, frame) =
|
||||
self.seek_to_first_before(UnixTimestampMilliseconds(u64::MAX))?;
|
||||
if let Some(frame) = frame {
|
||||
frame.end_time.0
|
||||
} else {
|
||||
// well, we have no choice. Just use the current time.
|
||||
SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
} else {
|
||||
(*new_time_range.end() * 1000.0) as u64
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_and_run(
|
||||
shared_ref: Arc<MetricsLogReadingShared>,
|
||||
reader: MetricsLogReader<R>,
|
||||
rx: Receiver<MetricsLogReaderMessage>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut manager = MetricsLogReaderManager {
|
||||
shared_ref,
|
||||
reader,
|
||||
rx,
|
||||
metric_descriptors: Default::default(),
|
||||
metric_descriptors_dirty: false,
|
||||
metric_descriptor_table: Default::default(),
|
||||
checkpoints: Default::default(),
|
||||
};
|
||||
|
||||
manager.run()
|
||||
}
|
||||
}
|
||||
|
||||
/// Renderer for a bare metrics graph.
|
||||
/// Should support rendering both line graphs and histograms.
|
||||
/// Input interactions:
|
||||
/// - drag-select an area to zoom in
|
||||
/// - hover along line to show tooltip with statistics
|
||||
/// - double click to zoom back out?
|
||||
/// - right click for context menu with more options (this might be handled at a layer higher
|
||||
/// up; we'll see...)
|
||||
pub struct Graph {}
|
||||
|
||||
pub struct GraphState {
|
||||
pub x_scale: f64,
|
||||
pub y_min: f64,
|
||||
pub y_max: f64,
|
||||
pub x_last_time: UnixTimestampMilliseconds,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn draw(ui: &mut Ui) {
|
||||
let context_menu_id = ui.id().with("context menu");
|
||||
|
||||
EguiFrame::dark_canvas(ui.style()).show(ui, |ui| {
|
||||
// if wanted, we could do this:
|
||||
// ui.ctx().request_repaint();
|
||||
|
||||
// store scaling stuff in here?
|
||||
//ui.memory().data.
|
||||
|
||||
let desired_size = ui.available_width() * Vec2::new(1.0, 0.35);
|
||||
let response = ui.allocate_response(desired_size, Sense::click_and_drag());
|
||||
|
||||
if response.clicked_by(PointerButton::Secondary) {
|
||||
ui.memory().open_popup(context_menu_id);
|
||||
}
|
||||
|
||||
// not clickable:
|
||||
// egui::popup::show_tooltip_at(ui.ctx(), context_menu_id, Some(Pos2::new(320.0, 320.0)), |ui| {
|
||||
// ui.button("hrm?");
|
||||
// });
|
||||
|
||||
egui::popup_below_widget(ui, context_menu_id, &response, |ui| {
|
||||
ui.button("Blah");
|
||||
});
|
||||
|
||||
let display_rect = response.rect;
|
||||
|
||||
// Scale the graph to the appropriate axes.
|
||||
let x_axis = 0.0..=1.0;
|
||||
// This range is reversed because screen coordinates go down, but we'd like them to go
|
||||
// up since this is more of a mathematical graph.
|
||||
let y_axis = 1.0..=0.0;
|
||||
|
||||
let display_transform =
|
||||
emath::RectTransform::from_to(Rect::from_x_y_ranges(x_axis, y_axis), display_rect);
|
||||
|
||||
let stroke = Stroke::new(2.0, Color32::GREEN);
|
||||
|
||||
ui.painter().line_segment(
|
||||
[
|
||||
display_transform * Pos2::new(0.1, 0.3),
|
||||
display_transform * Pos2::new(0.9, 0.6),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
29
bare-metrics-gui/src/main.rs
Normal file
29
bare-metrics-gui/src/main.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use crate::graph::Graph;
|
||||
use eframe::egui::{CentralPanel, CtxRef};
|
||||
use eframe::epi::{App, Frame};
|
||||
use eframe::NativeOptions;
|
||||
|
||||
pub mod graph;
|
||||
|
||||
pub struct MetricsGui {}
|
||||
|
||||
impl App for MetricsGui {
|
||||
fn update(&mut self, ctx: &CtxRef, _frame: &mut Frame<'_>) {
|
||||
CentralPanel::default().show(ctx, |ui| {
|
||||
ui.label("Hah");
|
||||
Graph::draw(ui);
|
||||
});
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"Bare Metrics GUI"
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app = MetricsGui {};
|
||||
|
||||
let native_options = NativeOptions::default();
|
||||
|
||||
eframe::run_native(Box::new(app), native_options);
|
||||
}
|
Loading…
Reference in New Issue
Block a user