This guide explains the core concepts and architecture for contributors.
The biggest performance win in memscope is never calling malloc/free in the main loop.
// Instead of:
process_t *p = malloc(sizeof(process_t)); // ❌ Slow, fragments memory
// ...
free(p);
// We use:
process_t *p = arena_alloc(arena, sizeof(process_t)); // ✅ O(1), just bump pointer
// At end of frame:
arena_reset(arena); // ✅ O(1), just reset pointer to startHow it works:
- At startup, allocate one big block (e.g., 64MB)
arena_alloc()just bumps a pointer forwardarena_reset()sets pointer back to start- All allocations are freed at once — no individual frees
Files: inc/util/arena.h, src/util/arena.c
We keep two arenas and two samples:
curr/arena_curr— Current frame dataprev/arena_prev— Previous frame for comparison
Each tick, we swap them:
sample_t *temp = prev; prev = curr; curr = temp;
Arena *ta = arena_prev; arena_prev = arena_curr; arena_curr = ta;
arena_reset(arena_curr); // Reset the "new current"
sample_capture(curr, arena_curr);This lets us compare current vs previous without copying data.
All data comes from Linux's /proc pseudo-filesystem:
| Path | Data |
|---|---|
/proc/[pid]/stat |
Basic stats: state, CPU time, RSS |
/proc/[pid]/status |
Detailed: VmRSS, VmSwap, UID |
/proc/[pid]/smaps_rollup |
Memory breakdown (fast, kernel 4.14+) |
/proc/[pid]/smaps |
Detailed memory maps (slower fallback) |
/proc/[pid]/io |
I/O stats (needs root) |
/proc/meminfo |
System-wide memory stats |
Files: src/proc/*.c
Linux recycles PIDs. PID 1234 might be Firefox now, Chrome later.
We detect this using start_time from /proc/[pid]/stat:
// In baseline hashmap
if (entry->pid == pid && entry->start_time == start_time) {
// Same process
} else {
// PID was reused — this is a new process
entry->first_rss = current_rss; // Reset baseline
}Files: src/ui/display.c (baseline_hash)
The Problem: User presses ↓ to select row 5, then presses Enter. Meanwhile, the list re-sorts and row 5 is now a different process!
The Solution: Freeze the UI state during rendering:
view_begin_frame(selected_idx, scroll, visible, group_mode);
// ... populate ViewState during render ...
view_end_frame();
// Later, when Enter is pressed:
pid_t pid = view_get_selected_pid(); // Returns the frozen PIDFiles: inc/ui/view.h, src/ui/view.c
┌─────────────────────────────────────────────────────────────────┐
│ MAIN LOOP │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ Input │───►│ Capture │───►│ Diff │───►│ Render │ │
│ │ (60fps) │ │ (1Hz) │ │ │ │ (60fps) │ │
│ └─────────┘ └──────────┘ └─────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
ui_poll_input sample_capture Compare ui_draw()
┌──────────┐ curr vs prev ui_draw_detail()
│ /proc/* │
│ parsing │
└──────────┘
Timing:
- Input polling: 60 FPS (smooth navigation)
- Data capture: 1 Hz (once per second)
- UI render: 60 FPS (responsive display)
| File | Purpose |
|---|---|
sample.c |
Orchestrates full system capture |
time.c |
Monotonic timing (avoids clock drift) |
engine.c |
Background data collection (unused currently) |
diff.c |
Sample comparison (unused currently) |
| File | Purpose |
|---|---|
procfs.c |
Low-level file reading with O_CLOEXEC |
scan.c |
Walks /proc to find numeric PID directories |
pid_stat.c |
Parses /proc/[pid]/stat |
pid_status.c |
Parses /proc/[pid]/status (VmRSS, UID) |
smaps.c |
3-level memory parsing with fallbacks |
meminfo.c |
System-wide /proc/meminfo |
pid_io.c |
I/O stats (requires root) |
| File | Purpose |
|---|---|
display.c |
Main rendering: list view, inspect view, colors |
view.c |
ViewState snapshot for stable process selection |
gauge.c |
Unicode block gauges [████████░░░░] |
sparkline.c |
Multi-row vertical bar graphs |
box.c |
Box-drawing utilities |
| File | Purpose |
|---|---|
arena.c |
Linear bump allocator |
log.c |
Logging with timestamps |
uid_cache.c |
UID → username caching |
proc_map.c |
PID hashmap for persistent entity tracking |
typedef struct {
pid_t pid;
char comm[16]; // Process name
char state; // R/S/D/Z/T
uint64_t rss_bytes; // Resident memory
uint64_t utime_ms; // User CPU time
uint64_t stime_ms; // Kernel CPU time
uint64_t start_time; // For PID reuse detection
// ... more fields
} process_snapshot_t;typedef struct {
uint64_t timestamp_ms;
uint64_t system_total_ram;
uint64_t system_free_ram;
size_t process_count;
process_snapshot_t *processes; // Array in arena
} sample_t;typedef struct {
uint64_t total_rss_kb;
uint64_t total_pss_kb;
uint64_t heap_kb; // Private memory
uint64_t anon_kb; // Anonymous pages
uint64_t shared_kb; // File-backed
uint64_t swap_kb; // Swapped out
} smaps_breakdown_t;typedef struct {
int selected_idx;
int scroll_offset;
size_t count;
ViewRow rows[VIEW_MAX_ROWS]; // Frozen snapshot
} ViewState;- Add field to
process_snapshot_tininc/core/sample.h - Parse it in
src/proc/pid_stat.corpid_status.c - Display it in
src/ui/display.cinui_draw() - Optionally add to ViewRow if needed for sorting
- Create new function in
display.c:void ui_draw_myview(...) - Add input handling in
ui_poll_input()for the key - Add state flag in
main.c(likeinspect_mode) - Call your function in the render section of main loop
- Add enum value in
display.c:SORT_MY_FIELD_DESC - Add comparison function:
static int cmp_myfield(...) - Add case in the sort switch
- Add keybind in
ui_poll_input()
- Indentation: 4 spaces (no tabs)
- Braces: K&R style
- Naming:
snake_casefor functions and variables - Types:
_tsuffix for typedefs - Headers: Include guards with
MEMSCOPE_prefix - Comments:
//for single line,/* */for blocks - Max line length: 100 chars (soft limit)
// Good
void my_function(int arg) {
if (condition) {
do_something();
}
}
// Bad
void my_function(int arg)
{
if(condition){
do_something();
}
}