From b1f78f2cddbcf1e137acb13c31b46e06d3012c58 Mon Sep 17 00:00:00 2001
From: Paul Oliver <contact@pauloliver.dev>
Date: Sat, 11 Apr 2026 14:07:37 +0200
Subject: Adds heatmaps

---
 data/vue/App.vue | 212 ++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 148 insertions(+), 64 deletions(-)

(limited to 'data/vue/App.vue')

diff --git a/data/vue/App.vue b/data/vue/App.vue
index e672911..73923dd 100644
--- a/data/vue/App.vue
+++ b/data/vue/App.vue
@@ -9,11 +9,15 @@
       </span>
     </h1>
     <form @change="trigger_reload">
-      <span class="nobr">Entries (max): <input class="input_small" v-model="entries" /></span><wbr />
-      <span class="nobr">nth: <input class="input_small" v-model="nth" /></span><wbr />
-      <span class="nobr">X-axis: <select class="input_small" v-model="x_axis"><option v-for="axis in x_axes">{{ axis }}</option></select></span><wbr />
+      <span class="nobr">Entries: <input v-model="entries" /></span><wbr />
+      <span class="nobr">nth: <input v-model="nth" /></span><wbr />
+      <span class="nobr">X-axis: <select v-model="x_axis"><option v-for="axis in x_axes">{{ axis }}</option></select></span><wbr />
       <span class="nobr">X-low: <input v-model="x_low" /></span><wbr />
-      <span class="nobr">X-high: <input v-model="x_high" /></span>
+      <span class="nobr">X-high: <input v-model="x_high" /></span><wbr />
+      <span class="nobr">Left: <input v-model="hm_left" /></span><wbr />
+      <span class="nobr">Px-count: <input v-model="hm_px_count" /></span><wbr />
+      <span class="nobr">Px-pow: <input v-model="hm_px_pow" /></span><wbr />
+      <span class="reset_button" @click="reset_inputs">↻</span>
     </form>
   </div>
   <Section name="Options" visible>
@@ -29,6 +33,9 @@
     <Section :name="section" grid ref="plot_sections" :visible="section === 'General'" triggers_reload v-for="(section_plots, section) in plots">
       <Plot :name="name" :section="section" v-for="(_, name) in section_plots" />
     </Section>
+    <Section :name="section" grid ref="heatmap_sections" triggers_reload v-for="(section_heatmaps, section) in heatmaps">
+      <Plot is_heatmap :name="name" :section="section" v-for="(_, name) in section_heatmaps" />
+    </Section>
   </div>
 </template>
 
@@ -41,7 +48,7 @@ import Section from './Section.vue'
 const root = window.location.href
 const id = v => v
 const hex = v => v !== undefined ? `0x${v.toString(16)}` : ''
-const hex_pow = v => v !== undefined ? `0x${Math.pow(2, v).toString(16)}` : ''
+const hex_pow = v => v !== undefined ? `0x${(2 ** v).toString(16)}` : ''
 const disabled = v => v ? 'disabled' : 'enabled'
 
 const opt_fmts = [
@@ -58,88 +65,120 @@ const opt_fmts = [
   ['Seed', 'seed', hex],
 ]
 
-let visible_tables = []
+let visible_plot_tables = []
+let visible_heatmap_tables = []
 let query_timeout = null
-let plot_x_low = 0n
+let plot_x_low = 0
 let plot_redraw = false
+let mvec_size = 0
+
+const uint32_max = (2 ** 32) - 1
+const hm_max_pixels = 2 ** 11
+
+const entries_def = 2000
+const nth_def = 1
+const x_axis_def = 'rowid'
+const x_low_def = hex(0)
+const x_high_def = hex(uint32_max)
+const hm_left_def = hex(0)
+const hm_px_count_def = hex(2 ** 10)
+
+const entries = ref(entries_def)
+const nth = ref(nth_def)
+const x_axes = ref(['rowid', 'step'])
+const x_axis = ref(x_axis_def)
+const x_low = ref(x_low_def)
+const x_high = ref(x_high_def)
+const hm_left = ref(hm_left_def)
+const hm_px_count = ref(hm_px_count_def)
+const hm_px_pow = ref(hex(0))
 
 const opts = ref({})
 const plots = ref({})
+const heatmaps = ref({})
 const loaded = ref(false)
 
-const entries = ref(2000)
-const nth = ref(BigInt(1))
-const x_axes = ref(['rowid', 'step'])
-const x_axis = ref(x_axes.value[0])
-const x_low = ref(hex(BigInt(0)))
-const x_high = ref(hex(BigInt(Math.pow(2, 64))))
-
 const query_in_progress = ref(false)
 const data = ref([])
 
 const top_pad = useTemplateRef('top_pad')
 const top_bar = useTemplateRef('top_bar')
 const plot_sections = useTemplateRef('plot_sections')
+const heatmap_sections = useTemplateRef('heatmap_sections')
 
 const update_visible_tables = () => {
-  const section_visibility = plot_sections.value.map(section => section.visible)
-  visible_tables = Object.entries(plots.value).filter((_, i) => section_visibility[i]).map((section, _) => [...new Set(Object.entries(section[1]).map(plot => plot[1].table))]).flat()
+  const plot_section_visibility = plot_sections.value.map(section => section.visible)
+  const heatmap_section_visibility = heatmap_sections.value.map(section => section.visible)
+  visible_plot_tables = Object.entries(plots.value).filter((_, i) => plot_section_visibility[i]).map((section, _) => [...new Set(Object.entries(section[1]).map(plot => plot[1].table))]).flat()
+  visible_heatmap_tables = Object.entries(heatmaps.value).filter((_, i) => heatmap_section_visibility[i]).map((section, _) => [...new Set(Object.entries(section[1]).map(plot => plot[1].table))]).flat()
 }
 
 const sanitize = (input, min, max, def, fmt) => {
-  if (isNaN(Number(input.value)) || input.value === '' || input.value < min || input.value > max) {
+  if (isNaN(Number(input.value)) || input.value === '' || input.value < min || input.value >= max) {
     input.value = fmt(def)
   }
 }
 
+const max_hm_px_pow = () => Math.floor(Math.log2((mvec_size - Number(hm_left.value)) / Number(hm_px_count.value)))
+
 const trigger_reload = () => {
   update_visible_tables()
 
-  sanitize(entries, 1n, BigInt(Math.pow(2, 64)), 2000n, id)
-  sanitize(nth, 1n, BigInt(Math.pow(2, 64)), 1n, id)
-  sanitize(x_low, 0n, BigInt(Math.pow(2, 64)), 0n, hex)
-  sanitize(x_high, 1n, BigInt(Math.pow(2, 64)), BigInt(Math.pow(2, 64)), hex)
+  sanitize(entries, 1, uint32_max, 2000, id)
+  sanitize(nth, 1, uint32_max, 1, id)
+  sanitize(x_low, 0, uint32_max, 0, hex)
+  sanitize(x_high, 1, uint32_max, uint32_max, hex)
+
+  if (opts.value.mvec_loop) {
+    sanitize(hm_left, 0, uint32_max, 0, hex)
+    sanitize(hm_px_count, 1, hm_max_pixels, hm_max_pixels, hex)
+    sanitize(hm_px_pow, 0, uint32_max, uint32_max, hex)
+  } else {
+    sanitize(hm_left, 0, mvec_size, 0, hex)
+    sanitize(hm_px_count, 1, hm_max_pixels, hm_max_pixels, hex)
+    sanitize(hm_px_pow, 0, max_hm_px_pow(), max_hm_px_pow(), hex)
+  }
 
-  plot_x_low = x_low.value
+  plot_x_low = Number(x_low.value)
   plot_redraw = true
 
   query()
 }
 
-const pad_top_bar = () => {
-  top_pad.value.style.height = `${Math.round(top_bar.value.getBoundingClientRect().height)}px`
-}
-
-const reviver = (_, val, { source }) => {
-  if (Number.isInteger(val) && !Number.isSafeInteger(val)) {
-    try { return BigInt(source) } catch {}
-  }
-
-  return val
+const reset_inputs = () => {
+  entries.value = entries_def
+  nth.value = nth_def
+  x_axis.value = x_axis_def
+  x_low.value = x_low_def
+  x_high.value = x_high_def
+  hm_left.value = hm_left_def
+  hm_px_count.value = hm_px_count_def
+  hm_px_pow.value = max_hm_px_pow()
+
+  trigger_reload()
 }
 
-const query_table = async table => {
+const query_table = async (table, is_heatmap, x_high_now) => {
   const params = {
     table: table,
     entries: entries.value,
     nth: nth.value,
     x_axis: x_axis.value,
-    x_low: Number(plot_x_low),
-    x_high: Number(x_high.value),
+    x_low: plot_x_low,
+    x_high: x_high_now,
+    is_eva: is_heatmap,
+    ...is_heatmap ? {
+      hm_left: Number(hm_left.value),
+      hm_px_count: Number(hm_px_count.value),
+      hm_px_pow: Number(hm_px_pow.value),
+    } : {},
   }
 
   const search_params = new URLSearchParams(params)
   const resp_table = await fetch(root + `data?${search_params}`, { method: 'GET' })
-  const resp_json = JSON.parse(await resp_table.text(), reviver)
+  const text_table = await resp_table.text()
 
-  // Keep track of the highest x-axis value fetched so far.
-  // Future queries will set this as the minimum, which prevents re-fetching already stored data.
-  if (resp_json.length) {
-    const x_last = BigInt(resp_json.slice(-1)[0][x_axis.value] + 1)
-    plot_x_low = plot_x_low > x_last ? plot_x_low : x_last
-  }
-
-  return resp_json
+  return JSON.parse(text_table)
 }
 
 const query = async () => {
@@ -148,27 +187,51 @@ const query = async () => {
   clearTimeout(query_timeout)
   query_in_progress.value = true
 
-  const query_results = await Promise.all(visible_tables.map(query_table))
-  const query_values = Object.fromEntries(visible_tables.map((key, i) => [key, query_results[i]]))
+  const high_params = new URLSearchParams({ x_axis: x_axis.value })
+  const resp_x_high = await fetch(root + `x_high?${high_params}`, { method: 'GET' })
+  const text_x_high = await resp_x_high.text()
+  const json_x_high = JSON.parse(text_x_high)
+  const x_high_max = json_x_high.x_high + 1
+  const x_high_val = Number(x_high.value)
+  const x_high_now = x_high_max < x_high_val ? x_high_max : x_high_val;
+
+  const plot_query_results = await Promise.all(visible_plot_tables.map(table => query_table(table, false, x_high_now)))
+  const heatmap_query_results = await Promise.all(visible_heatmap_tables.map(table => query_table(table, true, x_high_now)))
+  const plot_query_values = Object.fromEntries(visible_plot_tables.map((table, i) => [table, plot_query_results[i]]))
+  const heatmap_query_values = Object.fromEntries(visible_heatmap_tables.map((table, i) => [table, heatmap_query_results[i]]))
 
-  data.value = { redraw: plot_redraw, values: query_values }
+  // Keep track of the highest x-axis value fetched so far.
+  // Future queries will set this as the minimum, which prevents re-fetching already stored data.
+  plot_x_low = x_high_now
+
+  data.value = { redraw: plot_redraw, plot_values: plot_query_values, heatmap_values: heatmap_query_values }
   plot_redraw = false
 
   query_in_progress.value = false
   query_timeout = setTimeout(query, 10000)
 }
 
-onMounted(async () => {
-  window.onresize = _ => pad_top_bar()
-  pad_top_bar()
+const with_big_ints = (_, val, { source }) => {
+  if (Number.isInteger(val) && !Number.isSafeInteger(val)) {
+    try { return BigInt(source) } catch {}
+  }
 
+  return val
+}
+
+onMounted(async () => {
   const resp_opts = await fetch(root + 'opts', { method: 'GET' })
   const resp_plots = await fetch(root + 'plots', { method: 'GET' })
+  const resp_heatmaps = await fetch(root + 'heatmaps', { method: 'GET' })
 
-  opts.value = JSON.parse(await resp_opts.text(), reviver)
-  plots.value = JSON.parse(await resp_plots.text(), reviver)
+  opts.value = JSON.parse(await resp_opts.text(), with_big_ints)
+  plots.value = JSON.parse(await resp_plots.text())
+  heatmaps.value = JSON.parse(await resp_heatmaps.text())
   loaded.value = true
 
+  mvec_size = 2 ** opts.value.mvec_pow
+  hm_px_pow.value = hex(max_hm_px_pow())
+
   // All tables should include one cycle column for each core.
   // This allows normalizing the plots against each core's cycle count
   // (i.e. making `cycl_#` the plots' x-axis).
@@ -176,37 +239,45 @@ onMounted(async () => {
 })
 
 watch(loaded, _ => {
+  top_pad.value.style.height = `${Math.round(top_bar.value.getBoundingClientRect().height)}px`
+
   update_visible_tables()
   query()
 }, { flush: 'post' })
 
 provide('plots', plots)
+provide('heatmaps', heatmaps)
 provide('entries', entries)
 provide('x_axis', x_axis)
+provide('hm_left', hm_left)
+provide('hm_px_count', hm_px_count)
+provide('hm_px_pow', hm_px_pow)
 provide('data', data)
 provide('trigger_reload', trigger_reload)
 </script>
 
 <style>
 html {
-  background-color: #002b36;
-  color: #586e75;
+  background-color: black;
+  color: gray;
   font-family: sans-serif;
 }
 
 h1 {
-  font-size: 20px;
+  font-size: 18px;
   font-weight: 600;
+  margin: 8px 0;
 }
 
 input, select {
-  background-color: #586e75;
+  background-color: gray;
   border: none;
-  color: #002b36;
+  color: black;
   font-family: monospace;
-  font-size: 14px;
+  font-size: 12px;
   margin: 0 4px;
   padding: 2px;
+  width: 80px;
 }
 
 table {
@@ -217,7 +288,7 @@ table {
 }
 
 tr:nth-child(odd) {
-  background-color: #073642;
+  background-color: #111;
 }
 
 td {
@@ -228,7 +299,7 @@ td {
 }
 
 .top_bar {
-  background-color: #073642;
+  background-color: #111;
   left: 0;
   padding: 8px;
   position: fixed;
@@ -243,12 +314,25 @@ td {
 }
 
 .nobr {
-  line-height: 32px;
-  margin-right: 16px;
+  font-size: 12px;
+  line-height: 28px;
+  margin-right: 6px;
   white-space: nowrap;
 }
 
-.input_small {
-  width: 80px;
+.reset_button {
+  background-color: black;
+  border: 1.5px solid gray;
+  color: gray;
+  cursor: pointer;
+  display: inline-block;
+  font-family: monospace;
+  font-size: 14px;
+  height: 16px;
+  line-height: 16px;
+  margin: 0 4px;
+  padding: 2px;
+  text-align: center;
+  width: 16px;
 }
 </style>
-- 
cgit v1.3

