diff options
Diffstat (limited to 'data/vue/App.vue')
| -rw-r--r-- | data/vue/App.vue | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/data/vue/App.vue b/data/vue/App.vue new file mode 100644 index 0000000..cb04565 --- /dev/null +++ b/data/vue/App.vue @@ -0,0 +1,347 @@ +<template> + <div ref="top_pad"></div> + <div class="top_bar" ref="top_bar"> + <h1> + Salis data server » + <span class="opts_name"> + {{ opts.name }} + {{ query_in_progress ? '⧖' : '✓' }} + </span> + </h1> + <form @change="trigger_reload"> + <span class="nobr">Entries: <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">X-low: <input v-model="x_low" /></span><wbr /> + <span class="nobr">X-high: <input v-model="x_high" /></span><wbr /> + <span class="nobr">Left: <input class="input_small" v-model="hm_left" /></span><wbr /> + <span class="nobr">Px-count: <input class="input_small" v-model="hm_px_count" /></span><wbr /> + <span class="nobr">Px-pow: <input class="input_small" v-model="hm_px_pow" /></span><wbr /> + <span class="reset_button" @click="reset_inputs">↻</span> + </form> + </div> + <Section name="Options" visible> + <table> + <tr v-for="opt_fmt in opt_fmts"> + <td>{{ opt_fmt[0] }}:</td> + <td>{{ opt_fmt[2](opts[opt_fmt[1]]) }}</td> + </tr> + </table> + </Section> + <!-- Render plots after simulation options have been loaded --> + <div v-if="loaded"> + <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> + +<script setup> +import { onMounted, provide, useTemplateRef, ref, watch } from 'vue' + +import Plot from './Plot.vue' +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${(2 ** v).toString(16)}` : '' +const disabled = v => v ? 'disabled' : 'enabled' + +const opt_fmts = [ + ['Ancestor', 'anc', id], + ['Architecture', 'arch', id], + ['Auto-save interval', 'auto_save_pow', hex_pow], + ['Clones', 'clones', id], + ['Cores', 'cores', id], + ['Data push interval', 'data_push_pow', hex_pow], + ['Mutator flip bit', 'muta_flip', id], + ['Mutator range', 'muta_pow', hex_pow], + ['Memory vector size', 'mvec_pow', hex_pow], + ['Save file compression', 'no_compress', disabled], + ['Seed', 'seed', hex], +] + +let visible_plot_tables = [] +let visible_heatmap_tables = [] +let query_timeout = null +let plot_x_low = 0 +let plot_redraw = false +let mvec_size = 0 + +const int_max = Number.MAX_SAFE_INTEGER +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(int_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 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 adjust_top_bar = () => top_pad.value.style.height = `${Math.round(top_bar.value.getBoundingClientRect().height)}px` + +const update_visible_tables = () => { + 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) { + 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, 1, uint32_max, entries_def, id) + sanitize(nth, 1, uint32_max, nth_def, id) + sanitize(x_low, 0, int_max, 0, hex) + sanitize(x_high, 1, int_max, int_max, hex) + + if (opts.value.mvec_loop) { + sanitize(hm_left, 0, uint32_max, 0, hex) + sanitize(hm_px_count, 1, hm_max_pixels, 2 ** 10, 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, 2 ** 10, hex) + sanitize(hm_px_pow, 0, max_hm_px_pow(), max_hm_px_pow(), hex) + } + + plot_x_low = Number(x_low.value) + plot_redraw = true + + query() +} + +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, is_heatmap, x_high_now) => { + const params = { + table: table, + entries: entries.value, + nth: nth.value, + x_axis: x_axis.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 text_table = await resp_table.text() + + return JSON.parse(text_table) +} + +const query = async () => { + if (query_in_progress.value) return + + clearTimeout(query_timeout) + query_in_progress.value = true + + 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]])) + + // 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) +} + +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(), 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). + x_axes.value.push(...Array(opts.value.cores).keys().map(i => `cycl_${i}`)) +}) + +watch(loaded, _ => { + window.addEventListener('resize', adjust_top_bar) + + adjust_top_bar() + 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: black; + color: gray; + font-family: sans-serif; +} + +h1 { + font-size: 18px; + font-weight: 600; + margin: 8px 0; +} + +input, select { + background-color: gray; + border: none; + color: black; + font-family: monospace; + font-size: 12px; + margin: 0 4px; + padding: 2px; + width: 120px; +} + +table { + border-collapse: collapse; + border-spacing: 0; + height: 100%; + width: 100%; +} + +tr:nth-child(odd) { + background-color: #111; +} + +td { + font-family: monospace; + font-size: 14px; + margin: 0; + padding: 0; +} + +.top_bar { + background-color: #000; + border-bottom: 1px solid gray; + left: 0; + padding: 8px; + position: fixed; + top: 0; + width: 100%; + z-index: 1; +} + +.opts_name { + color: #b58900; + font-weight: normal; +} + +.nobr { + 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> |
