precord 0.7.13

Command line tool for recording process or system performance data
use crate::opt::{ProcessCategory, SystemCategory};
use crate::types::{ProcessInfo, SystemMetrics};
use serde_json::json;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::Path;

const CHART_HEIGHT: usize = 800;
const CHART_PADDING_LEFT: usize = 50;
const CHART_PADDING_RIGHT: usize = 300;
const CHART_PADDING_TOP_BOTTOM: usize = 100;

pub fn consume<P: AsRef<Path>>(
    output: P,
    proc_category: &[ProcessCategory],
    sys_category: &[SystemCategory],
    timestamps: &[chrono::DateTime<chrono::Local>],
    processes: &[ProcessInfo],
    system_metrics: &[SystemMetrics],
) {
    if timestamps.is_empty() {
        return;
    }

    let mut titles = vec![];
    let mut grids = vec![];
    let mut x_axis = vec![];
    let mut y_axis = vec![];
    let mut series = vec![];
    let mut legends = vec![];
    let mut data_zooms = vec![];
    let mut tooltips = vec![];

    for ci in 0..proc_category.len() {
        let mut max_value: f32 = proc_category[ci].lower_bound();
        let category_title = format!("Process {:?}", proc_category[ci]);
        let unit = proc_category[ci].unit();
        let mut total = vec![];
        let mut legend_c = vec![];
        let mut tooltip = HashMap::new();

        for p in processes {
            let avg = p.avg_value(ci);

            total.extend_from_slice(
                vec![0.0; p.values[ci].len().saturating_sub(total.len())].as_slice(),
            );

            let data: Vec<_> = p.values[ci]
                .iter()
                .copied()
                .enumerate()
                .zip(timestamps.into_iter())
                .map(|((i, v), t)| {
                    max_value = max_value.max(v);
                    total[i] += v;

                    json!([t, v])
                })
                .collect();
            let name = format!("{} / AVG({:.2}{}) / {}", p.pid, avg, unit, &p.name);
            series.push(json!({
                "name": &name,
                "type": "line",
                "showSymbol": false,
                "xAxisIndex": grids.len(),
                "yAxisIndex": grids.len(),
                "data": data,
                "markLine": {
                    "data": [{"type": "average"}],
                },
                "emphasis": {
                    "focus": "series",
                },
            }));
            legend_c.push(json!({
                "name": &name,
            }));
            tooltip.insert(name, p.command.clone());
        }

        if processes.len() > 1 {
            let avg: f32 = total.iter().copied().sum::<f32>() / total.len() as f32;

            let data: Vec<_> = total
                .into_iter()
                .zip(timestamps.into_iter())
                .map(|(v, t)| {
                    max_value = max_value.max(v);

                    json!([t, v])
                })
                .collect();
            series.push(json!({
                "name": format!("Total / AVG({:.2}{})", avg, unit),
                "type": "line",
                "showSymbol": false,
                "xAxisIndex": grids.len(),
                "yAxisIndex": grids.len(),
                "data": data,
                "markLine": {
                    "data": [{"type": "average"}],
                },
                "emphasis": {
                    "focus": "series",
                },
            }));
            legend_c.push(json!({
                "name": format!("Total / AVG({:.2}{})", avg, unit),
            }));
        }

        x_axis.push(json!({
            "gridIndex": grids.len(),
            "type": "time",
        }));
        y_axis.push(json!({
            "gridIndex": grids.len(),
            "min": 0.0,
            "max": max_value.ceil(),
            "axisLabel": {
                "formatter": format!("{{value}}{}", unit),
            },
        }));
        titles.push(json!({
            "text": category_title,
            "textAlign": "left",
            "top": format!("{}px", 800 * grids.len() + 20),
            "left": CHART_PADDING_LEFT - 10,
        }));
        legends.push(json!({
            "type": "scroll",
            "orient": "vertical",
            "right": 0,
            "top": format!("{}px", 800 * grids.len() + 100),
            "data": legend_c,
            "tooltip": {
                "show": true,
                "extraCssText": "max-width:500px; white-space:normal;",
            },
        }));
        data_zooms.push(json!({
            "xAxisIndex": [grids.len()],
            "top": format!("{}px", 800 * grids.len() + 730),
        }));
        grids.push(json!({
            "height": format!("{}px", CHART_HEIGHT - CHART_PADDING_TOP_BOTTOM * 2),
            "left": CHART_PADDING_LEFT,
            "top": format!("{}px", CHART_HEIGHT * grids.len() + CHART_PADDING_TOP_BOTTOM),
            "right": CHART_PADDING_RIGHT,
        }));
        tooltips.push(tooltip);
    }

    for (i, &sys_c) in sys_category.into_iter().enumerate() {
        let metrics = &system_metrics[i];
        let max_value = metrics.max().unwrap_or(0.).max(sys_c.lower_bound());
        let category_title = format!("System {:?}", sys_c);
        let unit = sys_c.unit();
        let mut legend_c = vec![];
        let tooltip = HashMap::new();

        for (si, row) in metrics.rows.iter().enumerate() {
            let avg = metrics.row_avg(si).unwrap_or(0.0);
            let data: Vec<_> = row
                .iter()
                .copied()
                .zip(timestamps.into_iter())
                .map(|(v, t)| json!([t, v]))
                .collect();
            let name = format!("{:?}{} / AVG({:.2}{})", sys_c, si, avg, unit);
            series.push(json!({
                "name": &name,
                "type": "line",
                "showSymbol": false,
                "xAxisIndex": grids.len(),
                "yAxisIndex": grids.len(),
                "data": data,
                "markLine": {
                    "data": [{"type": "average"}],
                },
                "emphasis": {
                    "focus": "series",
                },
            }));
            legend_c.push(json!({
                "name": &name,
            }));
        }

        x_axis.push(json!({
            "gridIndex": grids.len(),
            "type": "time",
        }));
        y_axis.push(json!({
            "gridIndex": grids.len(),
            "min": 0.0,
            "max": max_value.ceil(),
            "axisLabel": {
                "formatter": format!("{{value}}{}", unit),
            },
        }));
        titles.push(json!({
            "text": category_title,
            "textAlign": "left",
            "top": format!("{}px", 800 * grids.len()),
            "left": CHART_PADDING_LEFT - 10,
        }));
        legends.push(json!({
            "type": "scroll",
            "orient": "vertical",
            "right": 0,
            "top": format!("{}px", 800 * grids.len() + 100),
            "data": legend_c,
            "tooltip": {
                "show": true,
                "extraCssText": "max-width:500px; white-space:normal;",
            },
        }));
        data_zooms.push(json!({
            "xAxisIndex": [grids.len()],
            "top": format!("{}px", 800 * grids.len() + 730),
        }));
        grids.push(json!({
            "height": format!("{}px", CHART_HEIGHT - CHART_PADDING_TOP_BOTTOM * 2),
            "left": CHART_PADDING_LEFT,
            "top": format!("{}px", CHART_HEIGHT * grids.len() + CHART_PADDING_TOP_BOTTOM),
            "right": CHART_PADDING_RIGHT,
        }));
        tooltips.push(tooltip);
    }

    let grid_len = grids.len();
    let option = json!({
        "tooltip": {
            "show": true,
            "trigger": "axis",
        },
        "grid": grids,
        "title": titles,
        "xAxis": x_axis,
        "yAxis": y_axis,
        "legend": legends,
        "series": series,
        "dataZoom": data_zooms,
    });

    let html_content = r#"
   <!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js"></script>
    <style>
        #main {
            margin: 20px auto;
        }
    </style>
  </head>
  <body>
    <div id="main" style="height: "#
        .to_string()
        + &(800 * grid_len).to_string()
        + &r#"px;"></div>
    <script>
      var myChart = echarts.init(document.getElementById('main'), null, { renderer: 'svg' });
      var option = "#
            .to_string()
        + &option.to_string()
        + r#";

      var tooltips = "#
        + &serde_json::to_string(&tooltips).unwrap()
        + r#";
      option.legend.forEach(function(l, i) {
        l.tooltip.formatter = function(name) {
            console.log(name);
            var t = tooltips[i][name.name];
            t = t ? "<br/>" + t : "";
            return "<b>" + name.name + "</b>" + t;
        };
      });

      myChart.setOption(option);
      window.addEventListener('resize', function() {
        myChart.resize();
      });
    </script>
  </body>
</html>
    "#;

    let mut file = File::create(output).unwrap();
    file.write_all(html_content.as_bytes()).unwrap();
    file.sync_all().unwrap();
}