diff options
| author | Paul Oliver <contact@pauloliver.dev> | 2026-04-11 14:07:37 +0200 |
|---|---|---|
| committer | Paul Oliver <contact@pauloliver.dev> | 2026-04-14 23:05:25 +0200 |
| commit | b1f78f2cddbcf1e137acb13c31b46e06d3012c58 (patch) | |
| tree | f16ff77de4ddbb06aac2a8497ba2448d3846def9 | |
| parent | 0eadabbd642de773ce3187310eb4a52fd5dcd455 (diff) | |
Adds heatmaps
| -rw-r--r-- | arch/dummy/arch_vars.py | 1 | ||||
| -rw-r--r-- | arch/v1/arch.c | 40 | ||||
| -rw-r--r-- | arch/v1/arch_vars.py | 10 | ||||
| -rw-r--r-- | core.c | 94 | ||||
| -rw-r--r-- | data/compress.c | 56 | ||||
| -rw-r--r-- | data/render.c | 86 | ||||
| -rw-r--r-- | data/vue/App.vue | 212 | ||||
| -rw-r--r-- | data/vue/Plot.vue | 104 | ||||
| -rw-r--r-- | data/vue/Section.vue | 4 | ||||
| -rwxr-xr-x | salis.py | 80 |
10 files changed, 490 insertions, 197 deletions
diff --git a/arch/dummy/arch_vars.py b/arch/dummy/arch_vars.py index 6e37eca..c8d5e55 100644 --- a/arch/dummy/arch_vars.py +++ b/arch/dummy/arch_vars.py @@ -25,3 +25,4 @@ class ArchVars: ] self.plots = {} + self.heatmaps = {} diff --git a/arch/v1/arch.c b/arch/v1/arch.c index 152a97d..49d9db5 100644 --- a/arch/v1/arch.c +++ b/arch/v1/arch.c @@ -47,7 +47,7 @@ void arch_core_save(FILE *f, const struct Core *core) { fwrite(&core->wdea, sizeof(uint64_t), 1, f); #if defined(DATA_PUSH_PATH) - //fwrite(core->weva, sizeof(uint64_t), MVEC_SIZE, f); + fwrite(core->weva, sizeof(uint64_t), MVEC_SIZE, f); #endif } #endif @@ -64,7 +64,7 @@ void arch_core_load(FILE *f, struct Core *core) { fread(&core->wdea, sizeof(uint64_t), 1, f); #if defined(DATA_PUSH_PATH) - //fread(core->weva, sizeof(uint64_t), MVEC_SIZE, f); + fread(core->weva, sizeof(uint64_t), MVEC_SIZE, f); #endif } #endif @@ -626,19 +626,22 @@ void _write(struct Core *core, uint64_t pix) { if (_is_writeable_by(core, *regs[0], pix)) { // Store write event uint8_t inst = *regs[1] % INST_COUNT; + uint8_t inst_rep = *regs[1] % INST_CAP; + uint64_t addr = *regs[0]; ++core->iwrt[inst]; + ++core->weva[addr]; - if (mvec_is_in_mb0_of_proc(core, *regs[0], pix)) { + if (mvec_is_in_mb0_of_proc(core, addr, pix)) { ++core->wmb0; - } else if (mvec_is_in_mb1_of_proc(core, *regs[0], pix)) { + } else if (mvec_is_in_mb1_of_proc(core, addr, pix)) { ++core->wmb1; } else { ++core->wdea; } // Write instruction - mvec_set_inst(core, *regs[0], *regs[1] % INST_CAP); + mvec_set_inst(core, addr, inst_rep); } _increment_ip(core, pix); @@ -860,7 +863,7 @@ void arch_push_data_header() { } // Memory events - char *eprefs[] = { /* "wev" */ }; + char *eprefs[] = { "wev" }; int eprefs_cnt = sizeof(eprefs) / sizeof(eprefs[0]); for (int i = 0; i < CORES; ++i) { @@ -984,16 +987,16 @@ void arch_push_data_line() { } // TODO: insert write memory events - char *eprefs[] = { /* "wev" */ }; + char *eprefs[] = { "wev" }; int eprefs_cnt = sizeof(eprefs) / sizeof(eprefs[0]); for (int i = 0; i < CORES; ++i) { for (int j = 0; j < eprefs_cnt; ++j) { uint64_t *in = NULL; - //if (!strcmp("wev", eprefs[j])) { - // in = g_cores[i].weva; - //} + if (!strcmp("wev", eprefs[j])) { + in = g_cores[i].weva; + } // Compress event data size_t size = sizeof(uint64_t) * MVEC_SIZE; @@ -1001,18 +1004,7 @@ void arch_push_data_line() { assert(out); z_stream strm = { 0 }; - strm.zalloc = NULL; - strm.zfree = NULL; - strm.opaque = NULL; - - deflateInit(&strm, Z_DEFAULT_COMPRESSION); - - strm.avail_in = size; - strm.avail_out = size; - strm.next_in = (Bytef *)in; - strm.next_out = (Bytef *)out; - - deflate(&strm, Z_FINISH); + salis_deflate(&strm, size, (Bytef *)in, (Bytef *)out); // Insert blob const void *blob = out; @@ -1039,7 +1031,7 @@ void arch_push_data_line() { blob_size, g_steps ); - deflateEnd(&strm); + salis_deflate_end(&strm); free(out); } } @@ -1055,7 +1047,7 @@ void arch_push_data_line() { core->wmb1 = 0; core->wdea = 0; - //memset(core->weva, 0, sizeof(uint64_t) * MVEC_SIZE); + memset(core->weva, 0, sizeof(uint64_t) * MVEC_SIZE); } } #endif diff --git a/arch/v1/arch_vars.py b/arch/v1/arch_vars.py index 65c5e87..b9d4c4f 100644 --- a/arch/v1/arch_vars.py +++ b/arch/v1/arch_vars.py @@ -83,7 +83,7 @@ class ArchVars: ("uint64_t", "wmb1", ""), # writes within mb1 counter ("uint64_t", "wdea", ""), # writes within dead code counter - #("uint64_t", "weva", f"[{2 ** args.mvec_pow}]"), # write events array + ("uint64_t", "weva", f"[{2 ** args.mvec_pow}]"), # write events array ] self.data_is_compressed = True @@ -138,3 +138,11 @@ class ArchVars: } for i in range(args.cores) }, } + + self.heatmaps = { + "Events": { + f"wev_{i}": { + "table": f"wev_{i}", + } for i in range(args.cores) + } + } @@ -14,6 +14,10 @@ #define MALL_FLAG 0x80 #define UINT64_HALF 0x8000000000000000ul +#if defined(COMPRESS) || defined(DATA_PUSH_PATH) +#include "data/compress.c" +#endif + struct Proc { #define PROC_FIELD(type, name) type name; PROC_FIELDS @@ -46,7 +50,7 @@ struct Core { uint64_t edea; // executions within dead code counter uint64_t aeva[MVEC_SIZE]; // allocation events array - //uint64_t eeva[MVEC_SIZE]; // execution events array + uint64_t eeva[MVEC_SIZE]; // execution events array #define CORE_DATA_FIELD(type, name, suff) type name suff; CORE_DATA_FIELDS @@ -418,7 +422,7 @@ void core_save(FILE *f, const struct Core *core) { fwrite(core->mvec, sizeof(uint8_t), MVEC_SIZE, f); #if defined(DATA_PUSH_PATH) fwrite(core->aeva, sizeof(uint64_t), MVEC_SIZE, f); - //fwrite(core->eeva, sizeof(uint64_t), MVEC_SIZE, f); + fwrite(core->eeva, sizeof(uint64_t), MVEC_SIZE, f); #endif arch_core_save(f, core); @@ -514,7 +518,7 @@ void core_load(FILE *f, struct Core *core) { fread(core->mvec, sizeof(uint8_t), MVEC_SIZE, f); #if defined(DATA_PUSH_PATH) fread(core->aeva, sizeof(uint64_t), MVEC_SIZE, f); - //fread(core->eeva, sizeof(uint64_t), MVEC_SIZE, f); + fread(core->eeva, sizeof(uint64_t), MVEC_SIZE, f); #endif arch_core_load(f, core); @@ -564,17 +568,25 @@ void core_step(struct Core *core) { // Save execution event locations in database assert(mvec_proc_is_live(core, core->pcur)); - struct Proc *proc = proc_fetch(core, core->pcur); + uint64_t pcur_ip = arch_proc_ip_addr(core, core->pcur); - if (mvec_is_in_mb0_of_proc(core, proc->ip, core->pcur)) { + if (mvec_is_in_mb0_of_proc(core, pcur_ip, core->pcur)) { ++core->emb0; - } else if (mvec_is_in_mb1_of_proc(core, proc->ip, core->pcur)) { + } else if (mvec_is_in_mb1_of_proc(core, pcur_ip, core->pcur)) { ++core->emb1; - } else if (mvec_is_alloc(core, proc->ip)) { + } else if (mvec_is_alloc(core, pcur_ip)) { ++core->eliv; } else { ++core->edea; } + +#if defined(MVEC_LOOP) + core->eeva[mvec_loop(pcur_ip)]++; +#else + if (pcur_ip < MVEC_SIZE) { + core->eeva[pcur_ip]++; + } +#endif #endif arch_proc_step(core, core->pcur); @@ -634,18 +646,7 @@ void salis_save(const char *path) { assert(out); z_stream strm = { 0 }; - strm.zalloc = NULL; - strm.zfree = NULL; - strm.opaque = NULL; - - deflateInit(&strm, Z_DEFAULT_COMPRESSION); - - strm.avail_in = size; - strm.avail_out = size; - strm.next_in = (Bytef *)in; - strm.next_out = (Bytef *)out; - - deflate(&strm, Z_FINISH); + salis_deflate(&strm, size, (Bytef *)in, (Bytef *)out); FILE *fx = fopen(path, "wb"); assert(fx); @@ -654,7 +655,7 @@ void salis_save(const char *path) { fwrite(out, sizeof(char), strm.total_out, fx); fclose(fx); - deflateEnd(&strm); + salis_deflate_end(&strm); free(in); free(out); @@ -762,7 +763,7 @@ void salis_push_data_header() { ); // Memory events - char *eprefs[] = { "aev" /*, "eev" */ }; + char *eprefs[] = { "aev", "eev" }; int eprefs_cnt = sizeof(eprefs) / sizeof(eprefs[0]); for (int i = 0; i < CORES; ++i) { @@ -798,10 +799,8 @@ void salis_push_data_line() { struct Core *core = &g_cores[i]; for (uint64_t j = core->pfst; j <= core->plst; ++j) { - const struct Proc *proc = proc_get(core, j); - - amb0[i] += (double)proc->mb0s; - amb1[i] += (double)proc->mb1s; + amb0[i] += (double)arch_proc_mb0_size(core, j); + amb1[i] += (double)arch_proc_mb1_size(core, j); } amb0[i] /= core->pnum; @@ -851,7 +850,7 @@ void salis_push_data_line() { ); // TODO: insert execute memory events - char *eprefs[] = { "aev" /*, "eev" */ }; + char *eprefs[] = { "aev", "eev" }; int eprefs_cnt = sizeof(eprefs) / sizeof(eprefs[0]); for (int i = 0; i < CORES; ++i) { @@ -860,9 +859,9 @@ void salis_push_data_line() { if (!strcmp("aev", eprefs[j])) { in = g_cores[i].aeva; - } // else if (!strcmp("eev", eprefs[j])) { - // in = g_cores[i].eeva; - // } + } else if (!strcmp("eev", eprefs[j])) { + in = g_cores[i].eeva; + } // Compress event data size_t size = sizeof(uint64_t) * MVEC_SIZE; @@ -870,18 +869,7 @@ void salis_push_data_line() { assert(out); z_stream strm = { 0 }; - strm.zalloc = NULL; - strm.zfree = NULL; - strm.opaque = NULL; - - deflateInit(&strm, Z_DEFAULT_COMPRESSION); - - strm.avail_in = size; - strm.avail_out = size; - strm.next_in = (Bytef *)in; - strm.next_out = (Bytef *)out; - - deflate(&strm, Z_FINISH); + salis_deflate(&strm, size, (Bytef *)in, (Bytef *)out); // Insert blob const void *blob = out; @@ -908,7 +896,7 @@ void salis_push_data_line() { blob_size, g_steps ); - deflateEnd(&strm); + salis_deflate_end(&strm); free(out); } } @@ -923,7 +911,7 @@ void salis_push_data_line() { core->edea = 0; memset(core->aeva, 0, sizeof(uint64_t) * MVEC_SIZE); - //memset(core->eeva, 0, sizeof(uint64_t) * MVEC_SIZE); + memset(core->eeva, 0, sizeof(uint64_t) * MVEC_SIZE); } // Push arch-specific data @@ -988,24 +976,8 @@ void salis_load() { assert(out); z_stream strm = { 0 }; - strm.next_in = (Bytef *)in; - strm.avail_in = x_size; - strm.zalloc = NULL; - strm.zfree = NULL; - strm.opaque = NULL; - - inflateInit(&strm); - - strm.avail_out = size; - strm.next_out = (Bytef *)out; - -#if defined(NDEBUG) - inflate(&strm, Z_FINISH); -#else - assert(inflate(&strm, Z_FINISH)); -#endif - - inflateEnd(&strm); + salis_inflate(&strm, x_size, size, (Bytef *)in, (Bytef *)out); + salis_inflate_end(&strm); FILE *f = fmemopen(out, size, "rb"); #else diff --git a/data/compress.c b/data/compress.c new file mode 100644 index 0000000..df61123 --- /dev/null +++ b/data/compress.c @@ -0,0 +1,56 @@ +void salis_deflate(z_stream *strm, size_t size, Bytef *in, Bytef *out) { + assert(strm); + assert(size); + assert(in); + assert(out); + + strm->zalloc = NULL; + strm->zfree = NULL; + strm->opaque = NULL; + + deflateInit(strm, Z_DEFAULT_COMPRESSION); + + strm->avail_in = size; + strm->avail_out = size; + strm->next_in = in; + strm->next_out = out; + + deflate(strm, Z_FINISH); +} + +void salis_deflate_end(z_stream *strm) { + assert(strm); + + deflateEnd(strm); +} + +void salis_inflate(z_stream *strm, size_t avail_in, size_t size, Bytef *in, Bytef *out) { + assert(strm); + assert(avail_in); + assert(size); + assert(in); + assert(out); + + strm->next_in = in; + strm->avail_in = avail_in; + strm->zalloc = NULL; + strm->zfree = NULL; + strm->opaque = NULL; + + inflateInit(strm); + + strm->avail_out = size; + strm->next_out = out; + +#if defined(NDEBUG) + inflate(strm, Z_FINISH); +#else + assert(inflate(strm, Z_FINISH)); +#endif +} + +void salis_inflate_end(z_stream *strm) { + assert(strm); + + inflateEnd(strm); +} diff --git a/data/render.c b/data/render.c new file mode 100644 index 0000000..f9da65d --- /dev/null +++ b/data/render.c @@ -0,0 +1,86 @@ +#include <sqlite3ext.h> +SQLITE_EXTENSION_INIT1 + +#include <assert.h> +#include <stdint.h> +#include <stdio.h> +#include <zlib.h> + +#include "compress.c" + +#define EVA_SIZE (sizeof(uint64_t) * MVEC_SIZE) + +void eva_render(sqlite3_context *context, int argc, sqlite3_value **argv) { + assert(context); + assert(argc == 4); + assert(argv); + + (void)argc; + + size_t left = (size_t)sqlite3_value_int(argv[0]); +#if defined(MVEC_LOOP) + left %= MVEC_SIZE; +#endif + + size_t px_count = (size_t)sqlite3_value_int(argv[1]); + size_t px_pow = (size_t)sqlite3_value_int(argv[2]); + size_t px_res = 1 << px_pow; +#if !defined(MVEC_LOOP) +#if !defined(NDEBUG) + size_t right = left + px_res * px_count; +#endif + assert(left < MVEC_SIZE); + assert(right <= MVEC_SIZE); +#endif + + const void *blob = sqlite3_value_blob(argv[3]); + size_t blob_size = (size_t)sqlite3_value_bytes(argv[3]); + + // Inflate blob + size_t out_size = sizeof(uint64_t) * px_count; + uint64_t *eva = sqlite3_malloc(EVA_SIZE); + uint64_t *out = sqlite3_malloc(out_size); + z_stream strm = { 0 }; + salis_inflate(&strm, blob_size, EVA_SIZE, (Bytef *)blob, (Bytef *)eva); + salis_inflate_end(&strm); + + // Render image + for (size_t i = 0; i < px_count; i++) { + out[i] = 0; + + for (size_t j = 0; j < px_res; j++) { + size_t in_coord = left + i * px_res + j; +#if defined(MVEC_LOOP) + in_coord %= MVEC_SIZE; +#endif + out[i] += eva[in_coord]; + } + } + + sqlite3_free(eva); + + // Transform rendered image into textual representation + // A comma-separated list of hexadecimal integers + char *csv = sqlite3_malloc(px_count * 17 + 1); + char *ptr = csv; + + for (size_t i = 0; i < px_count; i++) { + ptr += sprintf(ptr, "%lx ", out[i]); + } + + *(--ptr) = '\0'; + sqlite3_free(out); + + sqlite3_result_text(context, csv, -1, sqlite3_free); +} + +int sqlite3_render_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) { + assert(db); + assert(pzErrMsg); + assert(pApi); + + (void)pzErrMsg; + + SQLITE_EXTENSION_INIT2(pApi); + return sqlite3_create_function(db, "eva_render", 4, SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS | SQLITE_UTF8, NULL, eva_render, NULL, NULL); +} 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> diff --git a/data/vue/Plot.vue b/data/vue/Plot.vue index 3c08d73..5bbc89a 100644 --- a/data/vue/Plot.vue +++ b/data/vue/Plot.vue @@ -11,7 +11,7 @@ <script setup> import { defineProps, inject, onMounted, ref, useTemplateRef, watch } from 'vue' -const props = defineProps({ section: String, name: String }) +const props = defineProps({ is_heatmap: Boolean, section: String, name: String }) const maximized = ref(false) @@ -19,8 +19,12 @@ 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 = () => { @@ -34,32 +38,32 @@ const prevent_plotly_buttons_tab_focus = () => { focusableElements.forEach(elem => elem.setAttribute('tabindex', '-1')) } -onMounted(() => { - const plot_config = plots.value[props.section][props.name] +const heatmap_init = [{ colorscale: 'Electric', type: 'heatmap', x: [], y: [], z: [] }] - switch (plot_config.type) { - case 'lines': - var data_defs = { mode: 'lines', line: { width: 1 }} - break - case 'stack': - var data_defs = { mode: 'lines', line: { width: 1 }, stackgroup: 'stackgroup' } - break - case 'stack_percent': - var data_defs = { mode: 'lines', line: { width: 1 }, stackgroup: 'stackgroup', groupnorm: 'percent' } - break - } +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 })) + } +} - const columns = plot_config.cols - const data = Array.from(columns, column => ({ ...data_defs, x: [], y: [], name: column })) - - Plotly.newPlot(plot_ref.value, data, { - legend: { font: { color: '#586e75', family: 'monospace' }, maxheight: 100, orientation: 'h' }, - margin: { b: 32, l: 32, r: 32, t: 32 }, - paper_bgcolor: '#002b36', - plot_bgcolor: '#002b36', - title: { font: { color: '#586e75' }, text: props.name, x: 0, xref: 'paper' }, - xaxis: { gridcolor: '#073642', tickfont: { color: '#586e75' }, zerolinecolor: '#586e75' }, - yaxis: { gridcolor: '#073642', tickfont: { color: '#586e75' }, zerolinecolor: '#586e75' }, +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, @@ -68,32 +72,54 @@ onMounted(() => { prevent_plotly_buttons_tab_focus() }) -watch(data, new_data => { +const update_plot = new_data => { const plot_config = plots.value[props.section][props.name] - const columns = plot_config.cols - const column_count = columns.length - const table_data = new_data.values[plot_config.table] - const traces = [...Array(column_count).keys()] - const xs = Array(column_count).fill(table_data.map(elem => elem[x_axis.value])) - const ys = columns.map(column => table_data.map(elem => elem[column])) + 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(columns, () => []), - y: Array.from(columns, () => []), + 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: #002b36; + background-color: black; display: inline-block; width: 100%; } @@ -113,9 +139,9 @@ watch(data, new_data => { } .plot_button { - background-color: #002b36; - border: 1.5px solid #586e75; - color: #586e75; + background-color: black; + border: 1.5px solid gray; + color: gray; cursor: pointer; font-family: monospace; font-size: 18px; diff --git a/data/vue/Section.vue b/data/vue/Section.vue index f0202c0..7f4d633 100644 --- a/data/vue/Section.vue +++ b/data/vue/Section.vue @@ -24,9 +24,9 @@ defineExpose({ visible }) <style> .section_header { - border-bottom: 1px solid #586e75; + border-bottom: 1px solid gray; cursor: pointer; - font-size: 18px; + font-size: 16px; font-weight: normal; } @@ -70,16 +70,16 @@ option_list = [ ["d", "data-push-pow", "POW", "data aggregation interval exponent (interval == 2^{POW} >= {sync-pow}); a value of 0 disables data aggregation (requires 'sqlite')", 28, False, ipos, [new]], ["f", "force", None, "overwrite existing simulation of given name", False, False, bool, [new]], ["F", "muta-flip", None, "cosmic rays flip bits instead of randomizing whole bytes", False, False, bool, [bench, new]], - ["g", "compiler", "CC", "C compiler to use", "gcc", False, str, [bench, load, new]], - ["G", "compiler-flags", "FLAGS", "base set of flags to pass to C compiler", "-Wall -Wextra -Werror", False, str, [bench, load, new]], + ["g", "compiler", "CC", "C compiler to use", "gcc", False, str, [bench, load, new, serve]], + ["G", "compiler-flags", "FLAGS", "base set of flags to pass to C compiler", "-Wall -Wextra -Werror", False, str, [bench, load, new, serve]], ["M", "muta-pow", "POW", "mutator range exponent (range == 2^{POW})", 32, False, ipos, [bench, new]], ["m", "mvec-pow", "POW", "memory vector size exponent (size == 2^{POW})", 20, False, ipos, [bench, new]], ["n", "name", "NAME", "name of new or loaded simulation", "def.sim", False, str, [load, new, serve]], - ["o", "optimized", None, "builds salis binary with optimizations", False, False, bool, [bench, load, new]], + ["o", "optimized", None, "build with optimizations", False, False, bool, [bench, load, new, serve]], ["P", "port", "PORT", "port number for data server", 8080, False, iport, [serve]], ["p", "pre-cmd", "CMD", "shell command to wrap call to executable (e.g. gdb, time, valgrind, etc.)", None, False, str, [bench, load, new]], ["s", "seed", "SEED", "seed value for new simulation; a value of 0 disables cosmic rays; a value of -1 creates a random seed", 0, False, seed, [bench, new]], - ["T", "keep-temp-dir", None, "delete temporary directory on exit", False, False, bool, [bench, load, new]], + ["T", "keep-temp-dir", None, "delete temporary directory on exit", False, False, bool, [bench, load, new, serve]], ["t", "thread-gap", "N", "memory gap between cores in bytes (may help reduce cache misses)", 0x100, False, inat, [bench, load, new]], ["u", "ui", uis, "user interface", "curses", False, str, [load, new]], ["U", "update", None, "update vendors (call 'git commit' afterwards to track updated files, if any)", False, False, bool, [serve]], @@ -216,8 +216,42 @@ if args.command in ["serve"]: info("Connecting to SQLite database:", sim_db) db_con = sqlite3.connect(sim_db) db_con.row_factory = sqlite3.Row + db_con.enable_load_extension(True) db_cur = db_con.cursor() + # Build SQLite event-array render extension + sqlx_flags = set() + sqlx_defines = set() + sqlx_links = set() + + sqlx_flags.update({*args.compiler_flags.split(), "-shared", "-fPIC", "-Idata"}) + sqlx_defines.add(f"-DMVEC_SIZE={2 ** args.mvec_pow}ul") + + if arch_vars.mvec_loop: sqlx_defines.add("-DMVEC_LOOP") + + if args.optimized: + sqlx_flags.add("-O3") + sqlx_defines.add("-DNDEBUG") + else: + sqlx_flags.add("-ggdb") + + sqlx_links.add("-lz") + + sqlx_tempdir = TemporaryDirectory(prefix="salis_sqlx_", delete=not args.keep_temp_dir) + info("Created a temporary salis SQLite extension directory at:", sqlx_tempdir.name) + + sqlx_so = os.path.join(sqlx_tempdir.name, "render.so") + info("Building salis SQLite extension at:", sqlx_so) + + sqlx_build_cmd = [args.compiler, "data/render.c", "-o", sqlx_so] + sqlx_build_cmd.extend(sqlx_flags) + sqlx_build_cmd.extend(sqlx_defines) + sqlx_build_cmd.extend(sqlx_links) + + info("Using build command:", sqlx_build_cmd) + subprocess.run(sqlx_build_cmd, check=True) + db_cur.execute(f"SELECT load_extension('{sqlx_so}')") + # Generate configuration so front-end knows how to render the plots. # Each architecture may also provide its own set of plots, which will be merged with the # default dictionary below. @@ -256,10 +290,26 @@ if args.command in ["serve"]: }, } + heatmaps = { + "Events": { + f"aev_{i}": { + "table": f"aev_{i}", + } for i in range(args.cores) + } | { + f"eev_{i}": { + "table": f"eev_{i}", + } for i in range(args.cores) + }, + } + for key in arch_vars.plots: plots[key] = (plots[key] if key in plots else {}) | arch_vars.plots[key] + for key in arch_vars.heatmaps: + heatmaps[key] = (heatmaps[key] if key in heatmaps else {}) | arch_vars.heatmaps[key] + info("Generated plot configuration:", plots) + info("Generated heatmap configuration:", heatmaps) # NOTE: this server implementation is very minimal and has no built-in security. # Please do not put this on the internet! Only run the data server within secure @@ -292,8 +342,9 @@ if args.command in ["serve"]: if bits.path == "/": return self.send_file_as("data/index.html", "text/html") if bits.path.split("/")[1] in ["js", "vendor", "vue"]: return self.send_file_as("data" + bits.path, "text/javascript") - if bits.path == "/opts": return self.send_as_json(opt_vars | {"name": args.name}) + if bits.path == "/opts": return self.send_as_json(opt_vars | {"mvec_loop": arch_vars.mvec_loop, "name": args.name}) if bits.path == "/plots": return self.send_as_json(plots) + if bits.path == "/heatmaps": return self.send_as_json(heatmaps) if bits.path == "/data": http_query = urllib.parse.parse_qs(bits.query) @@ -303,11 +354,28 @@ if args.command in ["serve"]: x_axis = http_query["x_axis"][0] x_low = http_query["x_low"][0] x_high = http_query["x_high"][0] - sql_query = f"SELECT * FROM (SELECT rowid, * FROM {table} WHERE {x_axis} >= {x_low} AND {x_axis} <= {x_high} AND rowid % {nth} == 0 ORDER BY {x_axis} DESC LIMIT {entries}) ORDER BY {x_axis} ASC;" + is_eva = http_query["is_eva"][0] + + if is_eva == "true": + hm_left = http_query["hm_left"][0] + hm_px_count = http_query["hm_px_count"][0] + hm_px_pow = http_query["hm_px_pow"][0] + selects = ", ".join([f"cycl_{i}" for i in range(args.cores)]) + f", eva_render({hm_left}, {hm_px_count}, {hm_px_pow}, evts) as eva_render, step" + else: + selects = "*" + + sql_query = f"SELECT * FROM (SELECT rowid, {selects} FROM {table} WHERE {x_axis} >= {x_low} AND {x_axis} <= {x_high} AND rowid % {nth} == 0 ORDER BY {x_axis} DESC LIMIT {entries}) ORDER BY {x_axis} ASC;" sql_res = db_cur.execute(sql_query) sql_list = [dict(row) for row in sql_res.fetchall()] return self.send_as_json(sql_list) + if bits.path == "/x_high": + http_query = urllib.parse.parse_qs(bits.query) + x_axis = http_query["x_axis"][0] + sql_query = f"SELECT {x_axis} as x_high FROM general ORDER BY {x_axis} DESC LIMIT 1;" + sql_dict = dict(db_cur.execute(sql_query).fetchone()) + return self.send_as_json(sql_dict) + self.log_error(f"Unsupported endpoint: {bits.path}") self.send_response(400) self.end_headers() |
