WIP: GUI for seeing the metrics

This commit is contained in:
Olivier 'reivilibre' 2021-11-25 09:09:16 +00:00
parent f8e6eea2ff
commit a78d384252
2 changed files with 356 additions and 0 deletions

View 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,
);
});
}
}

View 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);
}