render.rs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. //! Rendering for `sm121 report`: the Markdown table (stdout mode) and the
  2. //! embedded HTML page (serve mode), over the collected [`ReportRow`]s.
  3. use super::provenance;
  4. use super::{PAGE_TEMPLATE, PerfCell, ReportEntry, ReportRow};
  5. pub(crate) fn latest_score(r: &ReportRow) -> Option<&ReportEntry> {
  6. r.reports
  7. .iter()
  8. .rev()
  9. .find(|e| e.pct.is_some() || e.points.is_some())
  10. }
  11. pub(crate) fn latest_perf(r: &ReportRow) -> Option<(&PerfCell, Option<&PerfCell>)> {
  12. r.reports.iter().rev().find_map(|e| {
  13. let c1 = e.perf.iter().find(|p| p.d == 0 && p.c == 1);
  14. c1.map(|c1| (c1, e.perf.iter().find(|p| p.d == 0 && p.c == 2)))
  15. })
  16. }
  17. /// Normalized score, whole numbers without decimals (81, not 81.0).
  18. pub(crate) fn fmt_pct(pct: f64) -> String {
  19. if (pct - pct.trunc()).abs() < 1e-9 {
  20. format!("{}", pct as i64)
  21. } else {
  22. format!("{pct:.1}")
  23. }
  24. }
  25. pub(crate) fn fmt_tps(c: Option<&PerfCell>) -> String {
  26. c.map(|c| format!("{:.1}", c.tg))
  27. .unwrap_or_else(|| "—".into())
  28. }
  29. pub(crate) fn markdown(rows: &[ReportRow]) -> String {
  30. let mut s = format!(
  31. "# {}\n\n",
  32. fluent_i18n::t!("report-title", { "count" => rows.len() })
  33. );
  34. s.push_str(&format!(
  35. "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
  36. t("report-col-model"),
  37. t("report-col-quant"),
  38. t("report-col-recipe"),
  39. t("report-col-prov"),
  40. t("report-col-engine"),
  41. t("report-col-nodes"),
  42. t("report-col-reports"),
  43. t("report-col-last"),
  44. t("report-col-up"),
  45. t("report-col-launches"),
  46. t("report-col-points"),
  47. t("report-col-score"),
  48. t("report-col-tps1"),
  49. t("report-col-tps2"),
  50. ));
  51. s.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|\n");
  52. for r in rows {
  53. let last = r.reports.last();
  54. let latest = latest_score(r);
  55. let perf = latest_perf(r);
  56. s.push_str(&format!(
  57. "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
  58. r.model,
  59. r.quant,
  60. r.name,
  61. provenance::cell(r),
  62. r.engine,
  63. r.nodes
  64. .map(|n| format!("{n}×{}", r.tp))
  65. .unwrap_or_else(|| format!("1×{}", r.tp)),
  66. r.reports.len(),
  67. last.map(|e| match &e.time {
  68. Some(t) => format!("{} {t}", e.date),
  69. None => e.date.clone(),
  70. })
  71. .unwrap_or("—".into()),
  72. r.up.map(|u| format!("● {}", crate::stamp::fmt_ts(u)))
  73. .unwrap_or_else(|| "—".into()),
  74. r.runs,
  75. latest
  76. .and_then(|e| e.points.clone())
  77. .unwrap_or_else(|| "—".into()),
  78. latest
  79. .and_then(|e| e.pct)
  80. .map(fmt_pct)
  81. .unwrap_or_else(|| "—".into()),
  82. fmt_tps(perf.map(|(a, _)| a)),
  83. fmt_tps(perf.and_then(|(_, b)| b)),
  84. ));
  85. }
  86. s
  87. }
  88. pub(crate) fn t(key: &str) -> String {
  89. fluent_i18n::t!(key)
  90. }
  91. /// The interactive page: template with `{{DATA}}` replaced by the JSON
  92. /// rows (`<` escaped so `</script>` in data can't break out).
  93. pub(crate) fn html_page(rows: &[ReportRow]) -> String {
  94. let json = serde_json::to_string(rows).unwrap_or_else(|_| "[]".into());
  95. PAGE_TEMPLATE.replace("{{DATA}}", &json.replace('<', "\\u003c"))
  96. }
  97. /// Same page mounted under a URL prefix — the dashboard nests it at
  98. /// `/recipes`, so the page's own absolute paths (its SSE stream and the
  99. /// `/r/<recipe>/<file>` viewer links) have to move with it.
  100. pub(crate) fn html_page_at(rows: &[ReportRow], prefix: &str) -> String {
  101. let page = html_page(rows);
  102. if prefix.is_empty() {
  103. return page;
  104. }
  105. page.replace("/events", &format!("{prefix}/events"))
  106. .replace("/r/", &format!("{prefix}/r/"))
  107. // marks the page as hosted (dashboard window): no own branding, and
  108. // the live chip only warns on a broken stream
  109. .replacen("<body>", "<body class=\"embed\">", 1)
  110. }