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