A project to help visualize memory exactly as it really is.
Low-level programmers constantly talk about concepts like "padding" and "alignment" and "intrusive data structures". All of these have to do with the actual layout of data in memory. However, the prevailing method for understanding and debugging such code is "think very hard". We can do better.
In debuggers, there seems to be a gap between the "watch window", with higher-level language-aware views of data (e.g. the contents of structs and arrays), and the "memory window", with a raw view of bytes in memory. I believe there is an in-between space we can visualize for greater understanding.
What is LLMV, actually?
Right now, it's a JS library that produces this "tape"-style view of memory. I have been finding this design to be very versatile, capable of displaying anything from structs to strings to arrays with relative ease. In particular, I find that this design makes it easy to see padding and other garbage space, which is normally not made visible at all.
The LLMV library itself is rather unopinionated. You simply give it a big JS object describing the data you want to visualize, and it renders it. But, this is actually where a lot of its power comes from, because the same visual structure can handle many different types of data.
However, practical use requires us to automate the process of visualizing data from real-world programs. For the jam, I created a companion C library that streamlines the process of dumping visualization information to a buffer. The JS library can then read from this buffer and use the type/address/size/offset information to automate various kinds of visuals.
Again, the C library is unopinionated, but again this is part of its flexibility. You programmatically traverse your data structures, and write out data structures as you go. This allows LLMV to handle structs, arrays, linked lists, or any other data structure. For example, here is the code to dump information about a pool allocator:
int pool_viz(Pool* p) { llmv_writer w = llmv_new_writer(vizbuf(), VIZBUF_SIZE); llmv_start_struct(&w, "Pool", p); llmv_structfield(&w, p, const char*, name); llmv_structfield(&w, p, unsigned char*, buf); llmv_structfield(&w, p, size_t, buf_len); llmv_structfield(&w, p, size_t, chunk_size); llmv_structfield(&w, p, int, count); llmv_structfield(&w, p, PoolFreeNode*, head); llmv_end(&w); llmv_cstring(&w, p->name); llmv_start(&w, "Pool_buf", p->buf, p->buf_len); llmv_end(&w); PoolFreeNode* n = p->head; while (n) { llmv_start(&w, "PoolFreeNode", n, p->chunk_size); llmv_structfield(&w, n, PoolFreeNode*, next); llmv_end(&w); n = n->next; } llmv_close(&w); return w.err; }
The end result is that I can pretty quickly follow this process:
- Write some straightforward C code to traverse a data structure and dump the results.
- Read the results into JS.
- Display the results in JS, customizing how various fields are displayed using a variety of built-in components.
Where to go from here?
Unfortunately I really didn't achieve a lot of the goals I had at the start:
- Display arrays in any reasonable way at all. (Right now I can only display very short ones.)
- Provide facilities for drawing arrows for pointers, both within a region and across regions. (Useful for linked lists and trees.)
- When hovering on a pointer or object, draw an arrow into any other corresponding memory regions. (Useful for showing which allocator an object is in, for example.)
- Track a stack of allocator ranges, so that any address can be traced back to its allocator, and the allocator state itself visualized.
- Zoom in and out (ugh why was this so hard for me)
I do think the basic design has potential. I may keep working on this in the future, especially if it might be useful for Orca's dev tools or something.