aboutsummaryrefslogtreecommitdiff
path: root/data/vue
diff options
context:
space:
mode:
Diffstat (limited to 'data/vue')
-rw-r--r--data/vue/App.vue347
-rw-r--r--data/vue/Plot.vue159
-rw-r--r--data/vue/Section.vue47
3 files changed, 553 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>
diff --git a/data/vue/Plot.vue b/data/vue/Plot.vue
new file mode 100644
index 0000000..5bbc89a
--- /dev/null
+++ b/data/vue/Plot.vue
@@ -0,0 +1,159 @@
+<template>
+ <div class="plot_container" :class="{ plot_maximized: maximized, plot_minimized: !maximized }" ref="plot_container">
+ <div class="plot" ref="plot_ref">
+ <button class="plot_button" @click="plot_toggle_maximize">
+ {{ maximized ? '-' : '+' }}
+ </button>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { defineProps, inject, onMounted, ref, useTemplateRef, watch } from 'vue'
+
+const props = defineProps({ is_heatmap: Boolean, section: String, name: String })
+
+const maximized = ref(false)
+
+const plot_ref = useTemplateRef('plot_ref')
+const plot_container = useTemplateRef('plot_container')
+
+const plots = inject('plots')
+const heatmaps = inject('heatmaps')
+const entries = inject('entries')
+const x_axis = inject('x_axis')
+const hm_left = inject('hm_left')
+const hm_px_count = inject('hm_px_count')
+const hm_px_pow = inject('hm_px_pow')
+const data = inject('data')
+
+const plot_toggle_maximize = () => {
+ maximized.value = !maximized.value
+ Plotly.Plots.resize(plot_ref.value)
+ document.body.style.overflow = maximized.value ? 'hidden' : 'visible'
+}
+
+const prevent_plotly_buttons_tab_focus = () => {
+ const focusableElements = plot_container.value.querySelectorAll('a, button, input, select')
+ focusableElements.forEach(elem => elem.setAttribute('tabindex', '-1'))
+}
+
+const heatmap_init = [{ colorscale: 'Electric', type: 'heatmap', x: [], y: [], z: [] }]
+
+const plot_init = () => {
+ const plot_config = plots.value[props.section][props.name]
+ const plot_defs = { mode: 'lines', line: { width: 1 }, x: [], y: [] }
+
+ switch (plot_config.type) {
+ case 'lines':
+ return Array.from(plot_config.cols, col => ({ ...plot_defs, name: col }))
+ case 'stack':
+ return Array.from(plot_config.cols, col => ({ ...plot_defs, stackgroup: 'sg', name: col }))
+ case 'stack_percent':
+ return Array.from(plot_config.cols, col => ({ ...plot_defs, stackgroup: 'sg', groupnorm: 'percent', name: col }))
+ }
+}
+
+onMounted(() => {
+ Plotly.newPlot(plot_ref.value, props.is_heatmap ? heatmap_init : plot_init(), {
+ font: { color: 'gray', family: 'monospace' },
+ legend: { maxheight: 100, orientation: 'h' },
+ margin: { b: 48, l: 48, r: 48, t: 48 },
+ paper_bgcolor: 'black',
+ plot_bgcolor: 'black',
+ title: { font: { size: 16 }, text: props.name, x: 0, xref: 'paper' },
+ xaxis: { gridcolor: '#111', tickfont: { color: 'gray' }, zerolinecolor: 'gray' },
+ yaxis: { gridcolor: '#111', tickfont: { color: 'gray' }, zerolinecolor: 'gray' },
+ }, {
+ displayModeBar: true,
+ responsive: true,
+ })
+
+ prevent_plotly_buttons_tab_focus()
+})
+
+const update_plot = new_data => {
+ const plot_config = plots.value[props.section][props.name]
+ const cols = plot_config.cols
+ const cols_count = cols.length
+ const table_data = new_data.plot_values[plot_config.table]
+ const traces = [...Array(cols_count).keys()]
+ const xs = Array(cols_count).fill(table_data.map(elem => elem[x_axis.value]))
+ const ys = cols.map(column => table_data.map(elem => elem[column]))
+
+ // Clear traces
+ if (new_data.redraw) {
+ const restyle = {
+ x: Array.from(cols, () => []),
+ y: Array.from(cols, () => []),
+ }
+
+ Plotly.restyle(plot_ref.value, restyle)
+ }
+
+ Plotly.extendTraces(plot_ref.value, { x: xs, y: ys }, traces, entries.value)
+}
+
+const update_heatmap = new_data => {
+ const heatmap_config = heatmaps.value[props.section][props.name]
+ const table_data = new_data.heatmap_values[heatmap_config.table]
+ const ys = [table_data.map(elem => elem[x_axis.value])]
+ const zs = [table_data.map(elem => elem.eva_render.split(' ').map(str => Number('0x' + str)))]
+
+ if (new_data.redraw) {
+ const px_size = Math.pow(2, Number(hm_px_pow.value))
+ const restyle = {
+ x: [Array.from(Array(Number(hm_px_count.value)).keys()).map(i => Number(hm_left.value) + i * px_size)],
+ y: [[]],
+ z: [[]],
+ }
+
+ Plotly.restyle(plot_ref.value, restyle)
+ }
+
+ Plotly.extendTraces(plot_ref.value, { y: ys, z: zs }, [0], entries.value)
+}
+
+watch(data, props.is_heatmap ? update_heatmap : update_plot)
+</script>
+
+<style>
+.plot_container {
+ background-color: black;
+ display: inline-block;
+ width: 100%;
+}
+
+.plot_maximized {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ top: 0;
+ z-index: 999;
+}
+
+.plot_minimized {
+ height: 400px;
+ position: relative;
+ z-index: 0;
+}
+
+.plot_button {
+ background-color: black;
+ border: 1.5px solid gray;
+ color: gray;
+ cursor: pointer;
+ font-family: monospace;
+ font-size: 18px;
+ height: 26px;
+ padding: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 26px;
+}
+
+.plot {
+ height: 100%;
+}
+</style>
diff --git a/data/vue/Section.vue b/data/vue/Section.vue
new file mode 100644
index 0000000..7f4d633
--- /dev/null
+++ b/data/vue/Section.vue
@@ -0,0 +1,47 @@
+<template>
+ <h2 class="section_header" @click="section_toggle_visible">
+ {{ name }} <span class="section_button">{{ visible ? '-' : '+' }}</span>
+ </h2>
+ <div :class="{ section_grid: grid }" v-if="visible">
+ <slot></slot>
+ </div>
+</template>
+
+<script setup>
+import { defineProps, inject, ref } from 'vue'
+
+const props = defineProps({ name: String, grid: Boolean, visible: Boolean, triggers_reload: Boolean })
+const visible = ref(props.visible)
+const trigger_reload = inject('trigger_reload')
+
+const section_toggle_visible = () => {
+ visible.value = !visible.value
+ if (props.triggers_reload) trigger_reload()
+}
+
+defineExpose({ visible })
+</script>
+
+<style>
+.section_header {
+ border-bottom: 1px solid gray;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: normal;
+}
+
+.section_button {
+ font-family: monospace;
+}
+
+.section_grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+}
+
+@media screen and (max-width: 800px) {
+ .section_grid {
+ grid-template-columns: 1fr;
+ }
+}
+</style>