blob: 4639fe383573eb9e2dd5a26d982a44008cc47f22 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This uses SDL+OpenGL as documented at
// <https://github.com/ocornut/imgui/blob/master/examples/example_sdl_opengl2/main.cpp>.
#include <assert.h>
#include <dirent.h>
#include <err.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>
#define restrict __restrict__
#include <algorithm>
#include <memory>
#include <unordered_map>
#include <vector>
#include <SDL.h>
#include <SDL_opengl.h>
#include <imgui.h>
#include "ProggyTiny.ttf.h"
#include "imgui_impl_opengl2.h"
#include "imgui_impl_sdl.h"
#include "implot.h"
#include "common.h"
/* partitionalloc structs, must be kept roughly in sync */
struct PartitionBucket {
unsigned long active_slot_spans_head;
unsigned long empty_slot_spans_head;
unsigned long decommitted_slot_spans_head;
uint32_t slot_size;
uint32_t num_system_pages_per_slot_span : 8;
uint32_t num_full_slot_spans : 24;
};
struct PartitionSuperPageExtentEntry {
unsigned long root;
unsigned long extent_base;
unsigned long extent_end;
unsigned long next;
};
struct __attribute__((packed)) SlotSpanMetadata {
unsigned long freelist_head;
unsigned long next_slot_span;
unsigned long bucket;
uint32_t marked_full : 1;
uint32_t num_allocated_slots : 13;
uint32_t num_unprovisioned_slots : 13;
uint32_t can_store_raw_size : 1;
uint32_t freelist_is_sorted : 1;
uint32_t unused1 : (32 - 1 - 2 * 13 - 1 - 1);
uint16_t in_empty_cache : 1;
uint16_t empty_cache_index : 7;
uint16_t unused2 : (16 - 1 - 7);
};
struct PartitionPage {
union {
struct SlotSpanMetadata span;
size_t raw_size; /* SubsequentPageMetadata */
struct PartitionSuperPageExtentEntry head;
struct {
char pad[32 - sizeof(uint16_t)];
uint16_t slot_span_metadata_offset;
};
};
};
static_assert(sizeof(struct PartitionPage) == 32);
struct ThreadCacheBucket {
unsigned long freelist_head;
unsigned char count;
unsigned char limit;
unsigned short slot_size;
};
#define SUPERPAGE_SIZE 0x200000UL
#define SUPERPAGE_MASK 0x1fffffUL
#define SUPERPAGE_PAGES 512
#define PAGES_PER_SPAN 4UL
#define SPANS_PER_SUPERPAGE (SUPERPAGE_PAGES / PAGES_PER_SPAN)
#define NUM_TCACHE_BUCKETS 41
#define PAGEMAP_SOFT_DIRTY 0x0080000000000000ULL
#define PAGEMAP_SWAP 0x4000000000000000ULL
#define PAGEMAP_PRESENT 0x8000000000000000ULL
#define PAGEMAP_EXCLUSIVE 0x0100000000000000ULL
#define VMA_R 1
#define VMA_W 2
#define VMA_X 4
#define VMA_SHARED 8
struct vma {
unsigned long start, end;
unsigned char perms;
unsigned long inode;
char* path; /* borrow from task_state::maps_buf */
bool pa_superpage;
};
struct pa_bucket {
struct PartitionBucket data;
unsigned long addr;
unsigned long span_pa_pages;
unsigned long root;
unsigned long objects_per_span;
unsigned long tcache_count;
std::vector<struct SlotSpanMetadata*> bucket_spans;
char size_str[21];
};
#define SLOT_STATE_USED 0
#define SLOT_STATE_FREE 1
#define SLOT_STATE_UNPROVISIONED 2
#define SLOT_STATE_TCACHE 3
struct span_info {
struct pa_bucket* bucket;
std::vector<unsigned char> slot_states;
bool decommitted;
};
struct partition;
struct superpage {
unsigned long addr;
struct superpage* extent_head;
bool direct_mapped;
struct partition* partition;
uint64_t pagemap[SUPERPAGE_PAGES];
struct PartitionPage meta_page[SPANS_PER_SUPERPAGE];
struct span_info span_info[SPANS_PER_SUPERPAGE];
bool ospage_has_allocations[SUPERPAGE_PAGES];
bool ospage_has_tcache[SUPERPAGE_PAGES];
bool ospage_has_unallocated[SUPERPAGE_PAGES];
};
struct partition {
unsigned long addr;
unsigned long superpage_count;
std::unordered_map<unsigned long, std::unique_ptr<struct pa_bucket>>
all_buckets;
};
struct thread_state {
pid_t tid;
/* from procfs */
char comm[32];
unsigned long minflt;
unsigned long majflt;
unsigned long utime;
unsigned long stime;
unsigned long starttime;
unsigned long cpu;
unsigned long delayacct;
unsigned long voluntary_ctxt_switches;
unsigned long nonvoluntary_ctxt_switches;
/* from ptrace (cached to minimize interference) */
unsigned long fsbase;
/* from memory peek */
unsigned long stackblock;
unsigned long stackblock_size;
unsigned long stack_phys_used;
unsigned long stack_phys_dirty;
unsigned char should_purge;
struct ThreadCacheBucket tcache_buckets[NUM_TCACHE_BUCKETS];
/* from previous */
unsigned long flt_const_cycles;
unsigned long cpu_const_cycles;
unsigned long switches_const_cycles;
};
struct task_state {
unsigned long collect_cycle;
bool reader_active;
char* maps_buf; /* owned; borrows in vma::path */
struct vma* vmas;
struct vma* stack_vma;
size_t vma_count;
struct superpage* superpages;
size_t superpage_count;
bool probed_payloads;
std::unordered_map<unsigned long, std::unique_ptr<struct partition>>
partitions;
std::unordered_map<pid_t, std::unique_ptr<struct thread_state>> threads;
struct thread_state* main_thread;
/* overall PA stats */
unsigned long stats_history_len;
#define STATS_HISTORY_MAX 300U
double physical_allocated_KiB[STATS_HISTORY_MAX];
double physical_tcache_KiB[STATS_HISTORY_MAX];
double physical_free_KiB[STATS_HISTORY_MAX];
ImU64 full_pages[STATS_HISTORY_MAX];
ImU64 partial_pages[STATS_HISTORY_MAX];
ImU64 tcache_and_free_pages[STATS_HISTORY_MAX];
ImU64 free_pages[STATS_HISTORY_MAX];
};
struct task {
/* const */
pid_t pid;
int task_fd;
int pidfd;
unsigned long pthread_block_offset;
unsigned long pthread_stackblock_offset;
unsigned long pthread_stackblock_size_offset;
unsigned long thread_cache_registry_addr;
unsigned long thread_cache_should_purge_offset;
unsigned int tls_key;
/* owned by collector */
int maps_fd;
int mem_fd;
int pagemap_fd;
size_t old_maps_len; /* hint from last read */
unsigned long collect_cycle;
/* locked */
struct task_state* cur_state;
/* shared */
volatile bool enable_collection;
volatile bool probe_payloads;
};
static int peek_buf(const struct task* t,
void* dst,
unsigned long src,
size_t len) {
if (pread(t->mem_fd, dst, len, src) == (ssize_t)len)
return 0;
memset(dst, '\0', len);
return -1;
}
static int open_task(struct task* restrict out, pid_t pid) {
out->pid = pid;
out->collect_cycle = 0;
out->enable_collection = true;
out->probe_payloads = false;
out->cur_state = NULL;
out->pidfd = syscall(__NR_pidfd_open, pid, 0);
if (out->pidfd == -1)
perror("pidfd_open");
char path[40];
sprintf(path, "/proc/%d", pid);
out->task_fd = open(path, O_PATH);
if (out->task_fd == -1)
return -1;
out->maps_fd = openat(out->task_fd, "maps", O_RDONLY);
if (out->maps_fd == -1)
goto err_maps;
out->old_maps_len = 0x1000;
out->mem_fd = openat(out->task_fd, "mem", O_RDONLY);
if (out->mem_fd == -1)
goto err_mem;
out->pagemap_fd = openat(out->task_fd, "pagemap", O_RDONLY);
if (out->pagemap_fd == -1)
goto err_pagemap;
{
Dwfl* dwfl = addrlookup_init(pid);
Dwfl_Module* libpthread_module = addrlookup_find_lib(dwfl, "/libpthread-");
unsigned long pthread_bias;
void* pthread_cu = lookup_cu(dwfl, libpthread_module,
"pthread_getspecific.c", &pthread_bias);
out->pthread_block_offset = addrlookup_get_struct_offset(
pthread_cu, NULL, 0, "pthread", "specific_1stblock");
out->pthread_stackblock_offset = addrlookup_get_struct_offset(
pthread_cu, NULL, 0, "pthread", "stackblock");
out->pthread_stackblock_size_offset = addrlookup_get_struct_offset(
pthread_cu, NULL, 0, "pthread", "stackblock_size");
unsigned long thread_cache_bias;
void* thread_cache_cu =
lookup_cu(dwfl, NULL,
"../../base/allocator/partition_allocator/src/"
"partition_alloc/thread_cache.cc",
&thread_cache_bias);
const char* nspath[] = {"base", "internal", NULL};
out->thread_cache_registry_addr = addrlookup_get_variable_address(
thread_cache_cu, thread_cache_bias, nspath, 3, "g_instance");
unsigned long tls_key_addr = addrlookup_get_variable_address(
thread_cache_cu, thread_cache_bias, nspath, 2, "g_thread_cache_key");
out->thread_cache_should_purge_offset = addrlookup_get_struct_offset(
thread_cache_cu, nspath, 2, "ThreadCache", "should_purge_");
if (peek_buf(out, &out->tls_key, tls_key_addr, sizeof(out->tls_key)))
err(1, "unable to read g_thread_cache_key");
addrlookup_finish(dwfl);
printf(
"g_instance=0x%lx; offsetof(struct pthread, specific_1stblock)=0x%lx; "
"g_thread_cache_key=0x%x\n",
out->thread_cache_registry_addr, out->pthread_block_offset,
out->tls_key);
}
return 0;
err_pagemap:
close(out->mem_fd);
err_mem:
close(out->maps_fd);
err_maps:
close(out->task_fd);
return -1;
}
static int pagemap_read(const struct task* t,
uint64_t* out,
unsigned long addr,
size_t num_pages) {
off_t off = addr / PAGE_SIZE * sizeof(*out);
size_t len = sizeof(*out) * num_pages;
return (pread(t->pagemap_fd, out, len, off) == (ssize_t)len) ? 0 : -1;
}
#if 0
// checks whether page is present or swapped.
// returns false on error.
// returns true for zeropage.
static bool page_present(const struct task *t, unsigned long addr) {
uint64_t entry;
if (pagemap_read(t, &entry, addr, 1))
return false; // error
return entry & 0xc000000000000000ULL;
}
#endif
// static const ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
static const ImVec4 clear_color = ImVec4(0.0f, 0.0f, 0.00f, 1.00f);
static const ImVec4 highlight_color = ImVec4(0.5f, 0.0f, 0.0f, 1.0f);
static const ImVec4 swap_color = ImVec4(1.00f, 0.00f, 0.00f, 1.00f);
// static const ImVec4 not_present_color = ImVec4(0.00f, 0.50f, 0.00f, 1.00f);
static const ImVec4 not_present_color = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
static const ImVec4 exclusive_color = ImVec4(0.0f, 0.3f, 1.0f, 1.00f);
static const ImVec4 shared_color = ImVec4(1.00f, 1.00f, 0.00f, 1.00f);
static const ImVec4 dirty_color = ImVec4(0.0f, 0.7f, 0.7f, 1.00f);
static const ImVec4 span_color_active = ImVec4(1.0f, 0.0f, 0.0f, 1.0f);
static const ImVec4 span_color_decommitted = ImVec4(0.0f, 1.0f, 0.0f, 1.0f);
void append_legend(const char* name,
const char* label,
const ImVec4& color,
const char* help_text) {
ImGui::SameLine(0, 20);
ImGui::ColorButton(name, color, ImGuiColorEditFlags_NoTooltip,
ImVec2(12, 12));
ImGui::SameLine();
ImGui::TextUnformatted(label);
if (help_text && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 50.0f);
ImGui::TextUnformatted(help_text);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
}
/* read a complete file, independent of read offset. caller initializes out_len
* with vague size hint. */
static int read_whole_file(int fd,
char** restrict outp,
size_t* restrict out_len) {
size_t len = *out_len;
if (len < 64)
len = 64;
if (len < SIZE_MAX / 4)
len = len + (len >> 3); // some extra space
char* buf = (char*)malloc(len);
if (!buf)
return -1;
size_t offset = 0;
ssize_t res;
while ((res = pread(fd, buf + offset, len - offset, offset)) > 0) {
offset += res;
if (offset == len) {
len = len + (len >> 2);
char* buf_new = (char*)realloc(buf, len);
if (buf_new == NULL) {
free(buf);
return -1;
}
buf = buf_new;
}
}
if (res == -1) {
free(buf);
return -1;
}
*outp = buf;
*out_len = offset;
return 0;
}
static int collect_mmap(struct task* t, struct task_state* restrict state) {
size_t len = t->old_maps_len;
char* buf;
if (read_whole_file(t->maps_fd, &buf, &len))
return -1;
char* end = buf + len;
// count VMAs
char* line_start = buf;
char* eol;
state->vma_count = 0;
while ((eol = (char*)memchr(line_start, '\n', end - line_start)) != NULL) {
line_start = eol + 1;
state->vma_count++;
}
state->vmas = new struct vma[state->vma_count];
state->stack_vma = NULL;
{
line_start = buf;
size_t vma_idx = 0;
while ((eol = (char*)memchr(line_start, '\n', end - line_start)) != NULL) {
struct vma* vma = &state->vmas[vma_idx++];
*eol = '\0';
char* itemp = line_start;
line_start = eol + 1; // this may be ==end
char* endp;
vma->start = strtoul(itemp, &endp, 16);
if (*endp != '-')
goto err_parse;
itemp = endp + 1;
vma->end = strtoul(itemp, &endp, 16);
if (*endp != ' ')
goto err_parse;
itemp = endp + 1;
if (eol - itemp < 5)
goto err_parse;
vma->perms = ((itemp[0] == 'r') ? VMA_R : 0) |
((itemp[1] == 'w') ? VMA_W : 0) |
((itemp[2] == 'x') ? VMA_X : 0) |
((itemp[3] == 's') ? VMA_SHARED : 0);
if (itemp[4] != ' ')
goto err_parse;
// offset
itemp = itemp + 5;
endp = strchr(itemp, ' ');
if (!endp)
goto err_parse;
// dev
itemp = endp + 1;
endp = strchr(itemp, ' ');
if (!endp)
goto err_parse;
// inode
itemp = endp + 1;
vma->inode = strtoul(itemp, &endp, 10);
if (*endp != ' ' && *endp)
goto err_parse;
while (*endp == ' ')
endp++;
vma->path = (*endp) ? endp : NULL;
if (vma->path && strcmp(vma->path, "[stack]") == 0) {
state->stack_vma = vma;
}
}
}
state->maps_buf = buf;
t->old_maps_len = len;
return 0;
err_parse:
delete[] state->vmas;
free(buf);
return -1;
}
static int find_pa_regions(struct task* t, struct task_state* restrict state) {
size_t pa_superpage_count = 0;
for (size_t vma_idx = 1; vma_idx < state->vma_count - 1; vma_idx++) {
/* look for a metadata page at page offset 1 inside a 2MiB-aligned region...
*/
struct vma* vma = &state->vmas[vma_idx];
vma->pa_superpage = false;
if ((vma->start & SUPERPAGE_MASK) != PAGE_SIZE)
continue;
if (vma->end != vma->start + PAGE_SIZE)
continue;
if (vma->perms != (VMA_R | VMA_W) || vma->inode)
continue;
unsigned long super_base = vma->start & ~SUPERPAGE_MASK;
unsigned long super_end = super_base + SUPERPAGE_SIZE;
/* ... surrounded by guard pages (1 before, 2 after) ... */
struct vma* prev = &state->vmas[vma_idx - 1];
if (prev->end != vma->start || prev->perms != 0)
continue;
struct vma* next = &state->vmas[vma_idx + 1];
if (next->start != vma->end || next->end - next->start < 0x2000 ||
next->perms != 0 || next->inode)
continue;
/* ... and with the whole superpage 2MiB region mapped. */
for (size_t idx2 = vma_idx + 1; true; idx2++) {
if (idx2 == state->vma_count)
goto next_vma;
if (state->vmas[idx2].start != state->vmas[idx2 - 1].end)
goto next_vma;
if (state->vmas[idx2].perms & (VMA_X | VMA_SHARED))
goto next_vma;
if (state->vmas[idx2].inode)
goto next_vma;
if (state->vmas[idx2].end >= super_end)
break;
}
vma->pa_superpage = true;
pa_superpage_count++;
next_vma:;
}
state->superpages = new struct superpage[pa_superpage_count];
size_t sp_idx = 0;
struct superpage* extent_head = NULL;
unsigned long meta_page_end = 0;
for (size_t vma_idx = 1; vma_idx < state->vma_count - 1; vma_idx++) {
struct vma* vma = &state->vmas[vma_idx];
if (!vma->pa_superpage)
continue;
struct superpage* sp = &state->superpages[sp_idx];
sp->addr = vma->start & ~SUPERPAGE_MASK;
if (pagemap_read(t, sp->pagemap, sp->addr, SUPERPAGE_PAGES)) {
// Something went very wrong... set the whole range to "not present"
// and try to continue anyway.
memset(sp->pagemap, '\0', sizeof(sp->pagemap));
}
if (peek_buf(t, &sp->meta_page, sp->addr + PAGE_SIZE,
sizeof(sp->meta_page))) {
continue;
}
// check that the root pointer points to readable memory
unsigned long dummy;
if (peek_buf(t, &dummy, sp->meta_page[0].head.root, sizeof(unsigned long)))
continue;
if (sp->meta_page[1].span.bucket - sp->addr < 2 * PAGE_SIZE)
sp->direct_mapped = true;
if (sp->meta_page[0].head.extent_base == sp->addr) {
// point of no return
meta_page_end = sp->meta_page[0].head.extent_end;
extent_head = sp;
} else if (sp->addr < meta_page_end &&
sp->meta_page[0].head.extent_base == 0) {
} else {
continue;
}
sp->extent_head = extent_head;
unsigned long partition_addr = sp->meta_page[0].head.root;
if (!state->partitions.contains(partition_addr)) {
std::unique_ptr<struct partition> new_part =
std::make_unique<struct partition>();
new_part->addr = partition_addr;
new_part->superpage_count = 0;
state->partitions.insert({partition_addr, std::move(new_part)});
}
auto part_iter = state->partitions.find(partition_addr);
assert(part_iter != state->partitions.end());
struct partition* partition = part_iter->second.get();
sp->partition = partition;
partition->superpage_count++;
for (unsigned long span = 0; span < SPANS_PER_SUPERPAGE; span++)
sp->span_info[span].bucket = nullptr;
for (unsigned long span = 1; span < SPANS_PER_SUPERPAGE;) {
unsigned long bucket_addr = sp->meta_page[span].span.bucket;
// printf("span %lu: bucket_addr=0x%lx slot_span_metadata_offset=%u\n",
// span, bucket_addr, sp->meta_page[span].slot_span_metadata_offset);
if (bucket_addr == 0) {
span++;
continue;
}
if (!partition->all_buckets.contains(bucket_addr)) {
std::unique_ptr<struct pa_bucket> new_bucket =
std::make_unique<struct pa_bucket>();
new_bucket->addr = bucket_addr;
new_bucket->root = sp->meta_page[0].head.root;
if (peek_buf(t, &new_bucket->data, bucket_addr,
sizeof(new_bucket->data))) {
fprintf(stderr, "failed to fetch bucket 0x%lx\n", bucket_addr);
memset(&new_bucket->data, '\0', sizeof(new_bucket->data));
}
new_bucket->span_pa_pages =
(new_bucket->data.num_system_pages_per_slot_span +
(PAGES_PER_SPAN - 1)) /
PAGES_PER_SPAN;
if (new_bucket->data.slot_size >= 16) {
new_bucket->objects_per_span =
new_bucket->data.num_system_pages_per_slot_span * PAGE_SIZE /
new_bucket->data.slot_size;
} else {
// avoid DIV/0
new_bucket->objects_per_span = 0;
}
new_bucket->tcache_count = 0;
char size_str[11];
snprintf(size_str, sizeof(size_str), "%x", new_bucket->data.slot_size);
int charidx;
int outcharidx;
for (charidx = 0, outcharidx = 0; size_str[charidx]; charidx++) {
if (charidx % 2 == 0)
new_bucket->size_str[outcharidx++] = '\n';
new_bucket->size_str[outcharidx++] = size_str[charidx];
}
new_bucket->size_str[outcharidx] = '\0';
partition->all_buckets.insert({bucket_addr, std::move(new_bucket)});
}
auto bucket_iter = partition->all_buckets.find(bucket_addr);
assert(bucket_iter != partition->all_buckets.end());
struct pa_bucket* bucket = bucket_iter->second.get();
sp->span_info[span].bucket = bucket;
bucket->bucket_spans.push_back(&sp->meta_page[span].span);
// printf("span %lu for bucket 0x%x bytes, 0x%x pages: pa_pages=%lu\n",
// span, bucket->data.slot_size,
// bucket->data.num_system_pages_per_slot_span, bucket->span_pa_pages);
unsigned long span_pa_pages;
if (bucket->span_pa_pages > 0 &&
bucket->span_pa_pages <= SPANS_PER_SUPERPAGE) {
// for (unsigned long j=1; j<bucket->span_pa_pages; j++) {
// printf(" slot_span_metadata_offset=%u\n",
// sp->meta_page[span+j].slot_span_metadata_offset);
//}
span_pa_pages = bucket->span_pa_pages;
} else {
span_pa_pages = 1; // probably broken, but avoid endless loop at least
}
std::vector<unsigned char>& slot_states = sp->span_info[span].slot_states;
slot_states.resize(bucket->objects_per_span, SLOT_STATE_USED);
unsigned long unprovisioned =
sp->meta_page[span].span.num_unprovisioned_slots;
if (unprovisioned > bucket->objects_per_span) {
fprintf(stderr,
"bogus unprovisioned @ SP=0x%lx span=%lu: bucket=0x%lx/0x%lx "
"slot_size=0x%x, system_pages_per_slot_span=0x%x, "
"objects_per_span=0x%lx, unprovisioned=0x%lx\n",
sp->addr, span, bucket_addr, bucket->addr,
bucket->data.slot_size,
bucket->data.num_system_pages_per_slot_span,
bucket->objects_per_span, unprovisioned);
unprovisioned = 0;
}
for (unsigned long idx = bucket->objects_per_span - unprovisioned;
idx < bucket->objects_per_span; idx++)
slot_states.at(idx) = SLOT_STATE_UNPROVISIONED;
if (state->probed_payloads) {
unsigned long span_start = sp->addr + span * PAGES_PER_SPAN * PAGE_SIZE;
unsigned long freelist_ptr = sp->meta_page[span].span.freelist_head;
while (freelist_ptr) {
// validate
if (freelist_ptr < span_start) {
fprintf(stderr, "bogus freelist pointer: 0x%lx not in span 0x%lx\n",
freelist_ptr, span_start);
break;
}
unsigned long span_offset = freelist_ptr - span_start;
if (span_offset % bucket->data.slot_size != 0) {
fprintf(
stderr,
"bogus freelist pointer: offset 0x%lx not aligned to 0x%x\n",
span_offset, bucket->data.slot_size);
break;
}
unsigned long slot_idx = span_offset / bucket->data.slot_size;
if (slot_idx >= bucket->objects_per_span - unprovisioned) {
fprintf(stderr,
"bogus freelist pointer: slot 0x%lx >= 0x%lx - 0x%lx\n",
slot_idx, bucket->objects_per_span, unprovisioned);
break;
}
// mark
slot_states.at(slot_idx) = SLOT_STATE_FREE;
// fetch next
unsigned long encoded_freeptr[2];
if (peek_buf(t, encoded_freeptr, freelist_ptr,
sizeof(encoded_freeptr))) {
fprintf(stderr, "freelist walk failed read\n");
break;
}
if (encoded_freeptr[0] != ~encoded_freeptr[1]) {
fprintf(stderr, "encoded freeptr is inconsistent\n");
break;
}
freelist_ptr = __builtin_bswap64(encoded_freeptr[0]);
}
}
sp->span_info[span].decommitted =
(sp->meta_page[span].span.freelist_head == 0 &&
sp->meta_page[span].span.num_allocated_slots == 0);
/* step to next span, must be at end of loop */
span += span_pa_pages;
}
sp_idx++;
}
state->superpage_count = sp_idx;
// exit(1);
return 0;
}
static int read_thread_state(int pid, struct user_regs_struct* out) {
int r;
// attach and asynchronously request stop
r = ptrace(PTRACE_ATTACH, pid, 0, 0);
if (r == -1)
return -1;
// wait for SIGSTOP, but reinject everything else
while (1) {
int status;
if (waitpid(pid, &status, 0) != pid)
err(1, "waitpid on ptrace child");
if (WIFEXITED(status) || WIFSIGNALED(status)) {
// we raced, it's dead
errno = ESRCH;
return -1;
}
assert(WIFSTOPPED(status));
int sig = WSTOPSIG(status);
if (sig == SIGSTOP) {
// wheee, it's stopped now!
break;
} else {
// bleh. reinject and loop back.
if (ptrace(PTRACE_CONT, pid, NULL, sig))
err(1, "reinject signal");
}
}
// grab register state
if (ptrace(PTRACE_GETREGS, pid, NULL, out))
err(1, "PTRACE_GETREGS");
if (ptrace(PTRACE_DETACH, pid, 0, 0))
err(1, "PTRACE_DETACH");
return 0;
}
/*
static int cmp_superpage(const void *a_, const void *b_) {
struct superpage **a = (struct superpage **)a_;
struct superpage **b = (struct superpage **)b_;
if ((*a)->addr < (*b)->addr)
return -1;
if ((*a)->addr > (*b)->addr)
return 1;
return 0;
}
*/
static struct superpage* find_superpage(struct task_state* state,
unsigned long sp_addr) {
if (state->superpage_count == 0)
return NULL;
sp_addr = sp_addr & ~SUPERPAGE_MASK;
size_t low = 0;
size_t high = state->superpage_count - 1;
while (1) {
size_t middle = (low + high) / 2;
if (state->superpages[middle].addr == sp_addr) {
return &state->superpages[middle];
} else if (low == high) {
return NULL;
} else if (state->superpages[middle].addr > sp_addr) {
if (low == middle)
return NULL;
high = middle - 1;
} else {
low = middle + 1;
}
}
}
static void collect_threads(struct task* t,
struct task_state* state,
struct task_state* old_state) {
state->main_thread = NULL;
int tasks_fd = openat(t->task_fd, "task", O_RDONLY);
if (tasks_fd == -1)
return;
DIR* tasks_dir = fdopendir(tasks_fd);
if (!tasks_dir) {
close(tasks_fd);
return;
}
while (1) {
next_item:;
struct dirent* dent = readdir(tasks_dir);
if (dent == NULL)
break;
pid_t tid = atoi(dent->d_name);
if (tid == 0)
continue;
char stat_path[50];
snprintf(stat_path, sizeof(stat_path), "%d/stat", tid);
int stat_fd = openat(dirfd(tasks_dir), stat_path, O_RDONLY);
if (stat_fd == -1)
continue;
char stat_buf[0x1000];
ssize_t stat_len = read(stat_fd, stat_buf, sizeof(stat_buf) - 1);
close(stat_fd);
if (stat_len <= 0)
continue;
stat_buf[stat_len] = '\0';
std::unique_ptr<struct thread_state> thread =
std::make_unique<struct thread_state>();
thread->tid = tid;
// field 2: comm
char* comm_start_paren = strchr(stat_buf, '(');
if (comm_start_paren == NULL)
continue;
char* comm_end_paren = strchr(comm_start_paren, ')');
if (comm_end_paren == NULL)
continue;
char* p = comm_end_paren;
size_t comm_len =
std::min((size_t)(comm_end_paren - (comm_start_paren + 1)),
sizeof(thread->comm) - 1);
memcpy(thread->comm, comm_start_paren + 1, comm_len);
thread->comm[comm_len] = '\0';
// https://man7.org/linux/man-pages/man5/proc.5.html
// field 10: minflt
// field 12: majflt
// field 14: utime (in ticks)
// field 15: stime (in ticks)
// field 22: starttime (in ticks)
// field 39: cpu
// field 42: block io delay (including swapin) (in ticks)
for (int i = 2; i < 10; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->minflt = strtoul(p, NULL, 10);
for (int i = 10; i < 12; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->majflt = strtoul(p, NULL, 10);
for (int i = 12; i < 14; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->utime = strtoul(p, NULL, 10);
for (int i = 14; i < 15; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->stime = strtoul(p, NULL, 10);
for (int i = 15; i < 22; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->starttime = strtoul(p, NULL, 10);
for (int i = 22; i < 39; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->cpu = strtoul(p, NULL, 10);
for (int i = 39; i < 42; i++) {
p = strchr(p, ' ');
if (!p)
goto next_item;
p++;
}
thread->delayacct = strtoul(p, NULL, 10);
char status_path[50];
snprintf(status_path, sizeof(status_path), "%d/status", tid);
int status_fd = openat(dirfd(tasks_dir), status_path, O_RDONLY);
if (status_fd == -1)
continue;
char status_buf[0x1000];
ssize_t status_len = read(status_fd, status_buf, sizeof(status_buf) - 1);
close(status_fd);
if (status_len <= 0)
continue;
status_buf[status_len] = '\0';
p = strstr(status_buf, "\nvoluntary_ctxt_switches:\t");
if (!p)
continue;
p += strlen("\nvoluntary_ctxt_switches:\t");
thread->voluntary_ctxt_switches = strtoul(p, NULL, 10);
p = strstr(status_buf, "\nnonvoluntary_ctxt_switches:\t");
if (!p)
continue;
p += strlen("\nnonvoluntary_ctxt_switches:\t");
thread->nonvoluntary_ctxt_switches = strtoul(p, NULL, 10);
struct thread_state* old_thread = NULL;
if (old_state) {
auto thread_iter = old_state->threads.find(tid);
if (thread_iter != state->threads.end()) {
old_thread = thread_iter->second.get();
if (old_thread->starttime != thread->starttime)
old_thread = NULL;
}
}
if (old_thread) {
thread->fsbase = old_thread->fsbase;
} else {
struct user_regs_struct regs;
if (read_thread_state(tid, &regs)) {
// only print errors on first iteration
if (!old_state)
perror(
"unable to read thread state, maybe needs root privs because of "
"Yama or maybe GDB/strace is already attached");
continue;
}
thread->fsbase = regs.fs_base;
}
thread->flt_const_cycles =
(old_thread && old_thread->majflt == thread->majflt &&
old_thread->minflt == thread->minflt)
? old_thread->flt_const_cycles + 1
: 0;
thread->cpu_const_cycles = (old_thread && old_thread->cpu == thread->cpu)
? old_thread->cpu_const_cycles + 1
: 0;
thread->switches_const_cycles = (old_thread &&
old_thread->voluntary_ctxt_switches ==
thread->voluntary_ctxt_switches &&
old_thread->nonvoluntary_ctxt_switches ==
thread->nonvoluntary_ctxt_switches)
? old_thread->switches_const_cycles + 1
: 0;
unsigned long tcache_addr;
if (peek_buf(
t, &tcache_addr,
thread->fsbase + t->pthread_block_offset + 0x10 * t->tls_key + 0x8,
sizeof(tcache_addr)) == 0) {
peek_buf(t, thread->tcache_buckets, tcache_addr,
sizeof(thread->tcache_buckets));
peek_buf(t, &thread->should_purge,
tcache_addr + t->thread_cache_should_purge_offset,
sizeof(thread->should_purge));
}
if (state->probed_payloads) {
for (unsigned long bucket_idx = 0; bucket_idx < NUM_TCACHE_BUCKETS;
bucket_idx++) {
unsigned long freelist_ptr =
thread->tcache_buckets[bucket_idx].freelist_head;
while (freelist_ptr) {
struct superpage* sp = find_superpage(state, freelist_ptr);
if (sp == NULL) {
fprintf(stderr, "unable to find superpage for freelist ptr 0x%lx\n",
freelist_ptr);
break;
}
unsigned long offset_in_superpage = freelist_ptr & SUPERPAGE_MASK;
unsigned long raw_span_idx =
offset_in_superpage / (PAGES_PER_SPAN * PAGE_SIZE);
unsigned long metadata_offset =
sp->meta_page[raw_span_idx].slot_span_metadata_offset;
if (metadata_offset > raw_span_idx) {
fprintf(stderr, "slot_span_metadata_offset impossibly big\n");
break;
}
unsigned long span = raw_span_idx - metadata_offset;
struct pa_bucket* bucket = sp->span_info[span].bucket;
if (bucket == NULL) {
fprintf(stderr, "tcache walk unable to find bucket\n");
break;
}
unsigned long span_start =
sp->addr + span * PAGES_PER_SPAN * PAGE_SIZE;
unsigned long span_offset = freelist_ptr - span_start;
if (span_offset % bucket->data.slot_size != 0) {
fprintf(stderr,
"tcache: bogus freelist pointer: offset 0x%lx not aligned "
"to 0x%x\n",
span_offset, bucket->data.slot_size);
break;
}
unsigned long slot_idx = span_offset / bucket->data.slot_size;
if (slot_idx >= bucket->objects_per_span) {
fprintf(stderr,
"tcache: bogus freelist pointer: slot 0x%lx >= 0x%lx\n",
slot_idx, bucket->objects_per_span);
break;
}
// mark
std::vector<unsigned char>& slot_states =
sp->span_info[span].slot_states;
slot_states.at(slot_idx) = SLOT_STATE_TCACHE;
bucket->tcache_count++;
// fetch next
unsigned long encoded_freeptr[2];
if (peek_buf(t, encoded_freeptr, freelist_ptr,
sizeof(encoded_freeptr))) {
fprintf(stderr, "tcache: freelist walk failed read\n");
break;
}
if (encoded_freeptr[0] != ~encoded_freeptr[1]) {
fprintf(stderr, "tcache: encoded freeptr is inconsistent\n");
break;
}
freelist_ptr = __builtin_bswap64(encoded_freeptr[0]);
}
}
}
thread->stack_phys_used = 0;
thread->stack_phys_dirty = 0;
if (tid != t->pid) {
peek_buf(t, &thread->stackblock,
thread->fsbase + t->pthread_stackblock_offset,
sizeof(thread->stackblock));
peek_buf(t, &thread->stackblock_size,
thread->fsbase + t->pthread_stackblock_size_offset,
sizeof(thread->stackblock_size));
} else if (state->stack_vma) {
thread->stackblock = state->stack_vma->start;
thread->stackblock_size = state->stack_vma->end - state->stack_vma->start;
} else {
thread->stackblock = 0;
thread->stackblock_size = 0;
}
// sanity check
if (thread->stackblock_size < 1024UL * 1024 * 1024 &&
thread->stackblock < thread->stackblock + thread->stackblock_size &&
thread->stackblock % PAGE_SIZE == 0 &&
thread->stackblock_size % PAGE_SIZE == 0 && thread->stackblock != 0) {
unsigned long num_pages = thread->stackblock_size / PAGE_SIZE;
for (unsigned long page = 0; page < num_pages; page += 16) {
unsigned long remaining_pages = std::min(num_pages - page, 16UL);
uint64_t pagemap[16];
if (pagemap_read(t, pagemap, thread->stackblock + page * PAGE_SIZE,
remaining_pages)) {
thread->stack_phys_used = 0;
break;
}
for (unsigned long i = 0; i < remaining_pages; i++) {
if (pagemap[i] & (PAGEMAP_PRESENT | PAGEMAP_SWAP)) {
if (pagemap[i] & PAGEMAP_SOFT_DIRTY)
thread->stack_phys_dirty += PAGE_SIZE;
thread->stack_phys_used += PAGE_SIZE;
}
}
}
}
if (tid == t->pid)
state->main_thread = thread.get();
state->threads.insert({tid, std::move(thread)});
}
closedir(tasks_dir);
}
static void compute_usage_stats(struct task* t,
struct task_state* state,
struct task_state* old_state) {
if (old_state) {
unsigned long offset =
(old_state->stats_history_len == STATS_HISTORY_MAX) ? 1 : 0;
state->stats_history_len = old_state->stats_history_len - offset;
#define COPY_STATS_ARRAY(NAME) \
memcpy(state->NAME, old_state->NAME + offset, \
sizeof(state->NAME[0]) * state->stats_history_len)
COPY_STATS_ARRAY(physical_allocated_KiB);
COPY_STATS_ARRAY(physical_tcache_KiB);
COPY_STATS_ARRAY(physical_free_KiB);
COPY_STATS_ARRAY(full_pages);
COPY_STATS_ARRAY(partial_pages);
COPY_STATS_ARRAY(tcache_and_free_pages);
COPY_STATS_ARRAY(free_pages);
} else {
state->stats_history_len = 0;
}
if (!state->probed_payloads) {
state->stats_history_len = 0;
return;
}
state->physical_allocated_KiB[state->stats_history_len] = 0;
state->physical_tcache_KiB[state->stats_history_len] = 0;
state->physical_free_KiB[state->stats_history_len] = 0;
state->full_pages[state->stats_history_len] = 0;
state->partial_pages[state->stats_history_len] = 0;
state->tcache_and_free_pages[state->stats_history_len] = 0;
state->free_pages[state->stats_history_len] = 0;
for (unsigned long superpage_idx = 0; superpage_idx < state->superpage_count;
superpage_idx++) {
struct superpage* sp = &state->superpages[superpage_idx];
for (unsigned long i = 0; i < SUPERPAGE_PAGES; i++) {
sp->ospage_has_allocations[i] = false;
sp->ospage_has_tcache[i] = false;
sp->ospage_has_unallocated[i] = false;
}
for (unsigned long span = 1; span < SPANS_PER_SUPERPAGE;) {
struct span_info* sinfo = &sp->span_info[span];
if (!sinfo->bucket) {
span++;
continue;
}
if (span + sinfo->bucket->span_pa_pages >= SPANS_PER_SUPERPAGE) {
fprintf(stderr, "span doesn't fit\n");
break;
}
for (unsigned int slot = 0; slot < sinfo->bucket->objects_per_span;
slot++) {
unsigned long slot_offset = slot * sinfo->bucket->data.slot_size;
unsigned long slot_offset_end =
slot_offset + sinfo->bucket->data.slot_size;
unsigned char slot_state = sinfo->slot_states.at(slot);
for (unsigned long offset = slot_offset; true;) {
unsigned long page_idx = span * PAGES_PER_SPAN + offset / PAGE_SIZE;
bool present =
(sp->pagemap[page_idx] & (PAGEMAP_PRESENT | PAGEMAP_EXCLUSIVE)) ==
(PAGEMAP_PRESENT | PAGEMAP_EXCLUSIVE) ||
(sp->pagemap[page_idx] & PAGEMAP_SWAP) != 0;
unsigned long next_page = (offset & PAGE_MASK) + PAGE_SIZE;
unsigned long fragment_end = std::min(next_page, slot_offset_end);
if (present) {
if (slot_state == SLOT_STATE_USED) {
state->physical_allocated_KiB[state->stats_history_len] +=
(fragment_end - offset) / 1024.0f;
sp->ospage_has_allocations[page_idx] = true;
}
if (slot_state == SLOT_STATE_TCACHE) {
state->physical_tcache_KiB[state->stats_history_len] +=
(fragment_end - offset) / 1024.0f;
sp->ospage_has_tcache[page_idx] = true;
}
if (slot_state == SLOT_STATE_FREE)
state->physical_free_KiB[state->stats_history_len] +=
(fragment_end - offset) / 1024.0f;
if (slot_state != SLOT_STATE_USED)
sp->ospage_has_unallocated[page_idx] = true;
}
if (slot_offset_end <= next_page)
break;
offset = next_page;
}
}
/* go to next */
span += sinfo->bucket->span_pa_pages;
}
for (unsigned long page = 0; page < SUPERPAGE_PAGES; page++) {
if ((sp->pagemap[page] & (PAGEMAP_PRESENT | PAGEMAP_EXCLUSIVE)) !=
(PAGEMAP_PRESENT | PAGEMAP_EXCLUSIVE) &&
(sp->pagemap[page] & PAGEMAP_SWAP) == 0)
continue;
if (!sp->ospage_has_unallocated[page]) {
state->full_pages[state->stats_history_len]++;
} else if (sp->ospage_has_allocations[page]) {
state->partial_pages[state->stats_history_len]++;
} else if (sp->ospage_has_tcache[page]) {
state->tcache_and_free_pages[state->stats_history_len]++;
} else {
state->free_pages[state->stats_history_len]++;
}
}
}
state->stats_history_len++;
}
static struct task* global_task;
static pthread_mutex_t update_lock = PTHREAD_MUTEX_INITIALIZER;
static uint32_t sdl_force_repaint_event;
static int try_collect(struct task* t) {
int ret = 1;
struct task_state* old_state =
t->cur_state; // we are the updater, we can read the pointer without
// locking
t->collect_cycle++;
struct task_state* state = new struct task_state;
if (!state)
return 1;
state->collect_cycle = t->collect_cycle;
state->reader_active = false;
state->probed_payloads = t->probe_payloads;
if (collect_mmap(t, state))
goto err_mmap;
if (find_pa_regions(t, state))
goto err_find_pa;
collect_threads(t, state, old_state);
compute_usage_stats(t, state, old_state);
// we've collected a new state, swap out the old one
{
if (pthread_mutex_lock(&update_lock))
errx(1, "pthread_mutex_lock");
t->cur_state = state;
state = (!old_state || old_state->reader_active) ? NULL : old_state;
if (pthread_mutex_unlock(&update_lock))
errx(1, "pthread_mutex_unlock");
}
ret = 0;
if (state == NULL)
return ret;
delete[] state->superpages;
err_find_pa:
delete[] state->vmas;
free(state->maps_buf);
err_mmap:
delete state;
return ret;
}
static void* collector_thread_fn(void* data_) {
prctl(PR_SET_NAME, "collector");
while (true) {
usleep(200000);
struct task* t = global_task;
if (!t->enable_collection)
continue;
if (try_collect(t)) {
printf("collector_thread: try_collect() failed\n");
continue;
}
SDL_Event ev = {.type = sdl_force_repaint_event};
SDL_PushEvent(&ev);
}
}
static bool show_soft_dirty = false;
static bool enable_logscale = false;
static bool show_logical_page_state = false;
static ImFont* small_font;
void render_superpages_legend(struct task_state* state) {
if (show_logical_page_state) {
ImGui::TextUnformatted("Legend (logical state, OS pages):");
} else {
ImGui::TextUnformatted("Legend (physical state, OS pages):");
}
if (state->probed_payloads) {
append_legend("legend:uncommitted", "uncommitted[?]", not_present_color,
"does not exclusively use RAM or swap space.\n"
"no page present / zeropage / CoW.\n"
"to distinguish further, restart target and monitor,\n"
"and don't enable swap-disturbing probes.");
if (show_logical_page_state) {
append_legend("legend:full", "fully used", exclusive_color, NULL);
append_legend(
"legend:partial", "partially used [?]", shared_color,
"includes pages with thread cache but no actually allocated memory");
append_legend("legend:free", "completely free", swap_color,
"unused except for freelist pointers");
} else {
append_legend(
"legend:committed", "committed[?]", exclusive_color,
"uses RAM (not shared with any other process) or swap space.\n");
}
} else {
append_legend("legend:not-present", "not-present", not_present_color, NULL);
append_legend("legend:exclusive", "exclusive[?]", exclusive_color,
"normal anonymous memory.\npresent in RAM.\nnot shared with "
"any other process.");
append_legend("legend:shared", "copy-on-write[?]", shared_color,
"copy-on-write memory.\n"
"normally created via one of:\n"
" - fork()\n"
" - read fault on not-present memory (zeropage)\n"
" - accidentally by probing memory that used to be "
"not-present (zeropage)");
append_legend("legend:swap", "swap[?]", swap_color,
"swapped out by the kernel.\n"
"WARNING:\n"
"inspecting heap metadata swaps metadata memory back in!");
}
if (show_soft_dirty) {
append_legend("legend:dirty", "dirty[?]", dirty_color,
"modified after soft-dirty state was last reset");
}
ImGui::TextUnformatted("Legend (span state, painted as border):");
append_legend("legend:pa-normal", "active", span_color_active, NULL);
append_legend("legend:pa-decom", "decommitted", span_color_decommitted, NULL);
}
void render_superpages(struct task* task, struct task_state* state) {
unsigned long page_tables_phys = state->superpage_count * PAGE_SIZE / 1024;
unsigned long pagemap_phys = 0;
unsigned long swap_size = 0;
for (size_t super_idx = 0; super_idx < state->superpage_count; super_idx++) {
struct superpage* sp = &state->superpages[super_idx];
for (size_t i = 0; i < SUPERPAGE_SIZE / PAGE_SIZE; i++) {
uint64_t entry = sp->pagemap[i];
if (entry & PAGEMAP_SWAP)
swap_size += PAGE_SIZE / 1024;
if (entry & (PAGEMAP_EXCLUSIVE | PAGEMAP_SWAP))
pagemap_phys += PAGE_SIZE / 1024;
}
}
ImGui::Text(
"%lu superpages; %lu KiB virtual; %lu KiB private allocated (including "
"%lu KiB metadata and %lu KiB swap; NOT COUNTING kernel overhead like "
"struct page and %lu KiB L1 page tables) [?]",
state->superpage_count, SUPERPAGE_SIZE * state->superpage_count / 1024,
pagemap_phys, state->superpage_count * PAGE_SIZE / 1024, swap_size,
page_tables_phys);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("NOTE: swap is always accounted as private");
static bool wide_display = false;
ImGui::Checkbox("wide display (for 4K screens)", &wide_display);
#define OSPAGE_WIDTH_NARROW 2.0f
#define OSPAGE_WIDTH_WIDE 4.0f
#define OSPAGE_HEIGHT 7.0f
#define OSPAGE_SPACING 1.0f
#define PAPAGE_SPACING_NARROW 2.0f
#define PAPAGE_SPACING_WIDE 5.0f
#define BUCKET_BORDER_WIDTH 1.0f
#define PAPAGE_HEIGHT \
(BUCKET_BORDER_WIDTH + OSPAGE_HEIGHT + BUCKET_BORDER_WIDTH)
float OSPAGE_WIDTH = wide_display ? OSPAGE_WIDTH_WIDE : OSPAGE_WIDTH_NARROW;
float PAPAGE_SPACING =
wide_display ? PAPAGE_SPACING_WIDE : PAPAGE_SPACING_NARROW;
if (task->probe_payloads) {
ImGui::Checkbox("show logical page states", &show_logical_page_state);
} else {
show_logical_page_state = false;
}
render_superpages_legend(state);
for (size_t super_idx = 0; super_idx < state->superpage_count; super_idx++) {
struct superpage* sp = &state->superpages[super_idx];
ImGui::PushFont(small_font);
ImGui::Text("%014lx", sp->addr);
ImGui::PopFont();
ImGui::SameLine(0.0f, 8.0f);
for (unsigned int span = 0; span < SPANS_PER_SUPERPAGE;) {
struct pa_bucket* bucket = sp->span_info[span].bucket;
struct SlotSpanMetadata* span_meta = &sp->meta_page[span].span;
unsigned long pa_pages = bucket ? bucket->span_pa_pages : 1;
if (pa_pages > SPANS_PER_SUPERPAGE - span)
pa_pages = SPANS_PER_SUPERPAGE - span; // hack in case of error
unsigned long os_pages = pa_pages * PAGES_PER_SPAN;
ImGui::SameLine(0.0f, PAPAGE_SPACING);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
const ImVec2 pos = ImGui::GetCursorScreenPos();
float span_width = os_pages * OSPAGE_WIDTH +
(os_pages - os_pages / 4) * OSPAGE_SPACING +
(os_pages / 4 - 1) * PAPAGE_SPACING;
if (bucket) {
ImU32 span_color =
ImColor(sp->span_info[span].decommitted ? span_color_decommitted
: span_color_active);
draw_list->AddRect(ImVec2(pos.x - BUCKET_BORDER_WIDTH, pos.y),
ImVec2(pos.x + span_width + BUCKET_BORDER_WIDTH,
pos.y + BUCKET_BORDER_WIDTH + OSPAGE_HEIGHT +
BUCKET_BORDER_WIDTH),
ImColor(span_color), 0.0f, ImDrawFlags_None);
}
for (unsigned int i = 0; i < os_pages; i++) {
float os_page_x = pos.x + i * OSPAGE_WIDTH +
(i - i / 4) * OSPAGE_SPACING +
(i / 4) * PAPAGE_SPACING;
float os_page_y = pos.y + BUCKET_BORDER_WIDTH;
int os_page_idx = i + span * PAGES_PER_SPAN;
assert(os_page_idx >= 0 && os_page_idx < 512);
uint64_t entry = sp->pagemap[os_page_idx];
const ImVec4* color;
if (show_logical_page_state) {
if ((entry & (PAGEMAP_PRESENT | PAGEMAP_EXCLUSIVE)) !=
(PAGEMAP_PRESENT | PAGEMAP_EXCLUSIVE) &&
(entry & PAGEMAP_SWAP) == 0) {
color = &not_present_color;
} else if (!sp->ospage_has_unallocated[os_page_idx]) {
color = &exclusive_color;
} else if (sp->ospage_has_allocations[os_page_idx] ||
sp->ospage_has_tcache[os_page_idx]) {
color = &shared_color;
} else {
color = &swap_color;
}
} else {
if (show_soft_dirty && (entry & PAGEMAP_SOFT_DIRTY)) {
color = &dirty_color;
} else if (entry & PAGEMAP_SWAP) {
color = state->probed_payloads ? &exclusive_color : &swap_color;
} else if ((entry & PAGEMAP_PRESENT) == 0) {
color = &not_present_color;
} else if (entry & PAGEMAP_EXCLUSIVE) {
color = &exclusive_color;
} else {
color = state->probed_payloads ? &not_present_color : &shared_color;
}
}
draw_list->AddRectFilled(
ImVec2(os_page_x, os_page_y),
ImVec2(os_page_x + OSPAGE_WIDTH, os_page_y + OSPAGE_HEIGHT),
ImColor(*color), 0.0f, ImDrawFlags_None);
}
ImGui::Dummy(ImVec2(span_width, BUCKET_BORDER_WIDTH + OSPAGE_HEIGHT +
BUCKET_BORDER_WIDTH));
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (span == 0)
ImGui::Text("METADATA/GUARD PAGE");
if (span == SPANS_PER_SUPERPAGE - 1)
ImGui::Text("GUARD PAGE");
if (bucket) {
ImGui::Text("bucket 0x%lx\n", bucket->addr);
ImGui::Text("slot size: %u\n", bucket->data.slot_size);
unsigned long waste =
bucket->data.num_system_pages_per_slot_span * PAGE_SIZE -
bucket->objects_per_span * bucket->data.slot_size;
ImGui::Text("system pages per slot span: %u\n",
bucket->data.num_system_pages_per_slot_span);
ImGui::Text("objects per span: %lu\n", bucket->objects_per_span);
ImGui::Text("padding wasted per span (outside slot): %lu\n", waste);
ImGui::Text("allocated slots: %d (%.0f%%)\n",
span_meta->num_allocated_slots,
100.0f * span_meta->num_allocated_slots /
(float)bucket->objects_per_span);
ImGui::Text("unprovisioned slots: %d (%.0f%%)\n",
span_meta->num_unprovisioned_slots,
100.0f * span_meta->num_unprovisioned_slots /
(float)bucket->objects_per_span);
if (pa_pages > 1 && bucket->objects_per_span == 1) {
unsigned long raw_size = sp->meta_page[span + 1].raw_size;
ImGui::Text("raw size: %lu (%.0f%%)\n", raw_size,
100.0f * raw_size / (float)bucket->data.slot_size);
}
if (sp->span_info[span].decommitted) {
ImGui::Text("*** DECOMMITTED ***");
} else if (state->probed_payloads) {
std::string slot_states_formatted;
unsigned long line_len = 0;
for (unsigned char slot_state : sp->span_info[span].slot_states) {
if (slot_state == SLOT_STATE_USED) {
slot_states_formatted.push_back('_');
} else if (slot_state == SLOT_STATE_FREE) {
slot_states_formatted.push_back('#');
} else if (slot_state == SLOT_STATE_UNPROVISIONED) {
slot_states_formatted.push_back('U');
} else if (slot_state == SLOT_STATE_TCACHE) {
slot_states_formatted.push_back('T');
} else {
slot_states_formatted.push_back('?');
}
line_len++;
if (line_len % 64 == 0)
slot_states_formatted.push_back('\n');
}
ImGui::Text(
"\nLegend: [_] used [#] free [U] unprovisioned [T] "
"thread cache");
ImGui::Text("%s", slot_states_formatted.c_str());
} else {
ImGui::Text("enable swap-disturbing heap probes for details");
}
}
ImGui::EndTooltip();
}
span += pa_pages;
}
}
}
static bool bucket_cmp_size(struct pa_bucket* a, struct pa_bucket* b) {
return a->data.slot_size < b->data.slot_size;
}
void render_buckets(struct task* task, struct task_state* state) {
if (!ImGui::BeginTabBar("partition", ImGuiTabBarFlags_None))
return;
for (auto& [part_addr, partition_ref] : state->partitions) {
struct partition* partition = partition_ref.get();
char tab_name[128];
snprintf(tab_name, IM_ARRAYSIZE(tab_name), "root 0x%lx (%lu superpages)",
partition->addr, partition->superpage_count);
if (!ImGui::BeginTabItem(tab_name))
continue;
std::vector<struct pa_bucket*> buckets;
for (auto& [addr, bucket_ref] : partition->all_buckets) {
buckets.push_back(bucket_ref.get());
}
std::sort(buckets.begin(), buckets.end(), bucket_cmp_size);
std::vector<char*> bucket_labels;
std::vector<float> bucket_vmem_allocated;
std::vector<float> bucket_vmem_tcache;
for (struct pa_bucket* bucket : buckets) {
bucket_labels.push_back(bucket->size_str);
unsigned int bucket_allocated = 0;
for (struct SlotSpanMetadata* span : bucket->bucket_spans) {
if (span->num_allocated_slots <=
bucket->objects_per_span) // sanity check
bucket_allocated += span->num_allocated_slots;
}
bucket_vmem_allocated.push_back(bucket_allocated *
bucket->data.slot_size / 1024.0);
bucket_vmem_tcache.push_back(bucket->tcache_count *
bucket->data.slot_size / 1024.0);
}
// allocated objects per bucket
ImPlot::SetNextPlotTicksX(0, buckets.size() - 1, buckets.size(),
bucket_labels.data());
if (ImPlot::BeginPlot(
"allocated virtual memory by bucket (_NOT_ physical memory)",
"bucket", "virtual memory (KiB)", ImVec2(-1, 200),
ImPlotFlags_NoChild, 0,
enable_logscale ? ImPlotAxisFlags_LogScale : 0)) {
ImPlot::SetLegendLocation(ImPlotLocation_North,
ImPlotOrientation_Horizontal);
if (state->probed_payloads) {
ImPlot::PlotBars("allocated", bucket_vmem_allocated.data(),
bucket_vmem_allocated.size(), 0.3, -0.15);
ImPlot::PlotBars("per-thread cache", bucket_vmem_tcache.data(),
bucket_vmem_tcache.size(), 0.3, +0.15);
} else {
ImPlot::PlotBars("allocated", bucket_vmem_allocated.data(),
bucket_vmem_allocated.size());
}
ImPlot::EndPlot();
}
#if 0
// physical memory per bucket
ImPlot::SetNextPlotTicksX(0, buckets.size()-1, buckets.size(), bucket_labels.data());
if (ImPlot::BeginPlot("_physical_ memory by bucket", "bucket", "physical memory (KiB)",
ImVec2(-1, 200), ImPlotFlags_NoChild, 0, enable_logscale ? ImPlotAxisFlags_LogScale : 0)) {
ImPlot::SetLegendLocation(ImPlotLocation_North, ImPlotOrientation_Horizontal);
ImPlot::PlotBars("allocated", bucket_pmem_allocated.data(), bucket_pmem_allocated.size()/*, 0.3, -0.15*/);
ImPlot::EndPlot();
}
#endif
ImGui::Text("per-bucket histograms of allocated slots per span:");
ImGui::BeginChild("buckets", ImVec2(0, 0), true,
ImGuiWindowFlags_HorizontalScrollbar);
bool can_stack = false;
for (struct pa_bucket* bucket : buckets) {
if (can_stack)
ImGui::SameLine();
ImGui::PushID(bucket->data.slot_size);
ImGui::BeginGroup();
// ImGui::Text("size %u: %d spans\n", bucket->data.slot_size,
// (int)bucket->bucket_spans.size()); if (bucket->bucket_spans.size() ==
// 1)
// ImGui::Text("xxx %d\n", bucket->bucket_spans[0]->num_allocated_slots);
// ImGui::Text("allocated slots:\n");
// ImU32 allocated_histogram_values[bucket->objects_per_span+1];
// memset(allocated_histogram_values, '\0',
// sizeof(allocated_histogram_values));
std::vector<ImU32> allocated_slots_by_bucket;
for (struct SlotSpanMetadata* span : bucket->bucket_spans) {
if (span->num_allocated_slots <= bucket->objects_per_span)
// allocated_histogram_values[allocated_slots]++;
allocated_slots_by_bucket.push_back(span->num_allocated_slots);
}
// ImGui::PlotHistogram("##allocated", allocated_histogram_values,
// IM_ARRAYSIZE(allocated_histogram_values),
// 0, NULL, FLT_MAX, FLT_MAX, ImVec2(0, 80));
ImPlot::SetNextPlotLimits(0, bucket->objects_per_span, 0,
bucket->bucket_spans.size(), ImGuiCond_Always);
ImPlot::PushColormap(ImPlotColormap_Dark);
if (ImPlot::BeginPlot("##allocated objects per bucket", NULL, NULL,
ImVec2(256, 128), ImPlotFlags_NoChild, 0,
enable_logscale ? ImPlotAxisFlags_LogScale : 0)) {
// ImPlot::PlotStems("number of buckets", allocated_histogram_values,
// bucket->objects_per_span+1);
char legend_str[30];
snprintf(legend_str, sizeof(legend_str), "0x%x",
bucket->data.slot_size);
int bins = std::min(bucket->objects_per_span + 1, 64UL);
ImPlot::PlotHistogram(legend_str, allocated_slots_by_bucket.data(),
allocated_slots_by_bucket.size(), bins, false,
false, ImPlotRange(0, bucket->objects_per_span));
ImPlot::EndPlot();
}
ImPlot::PopColormap();
ImGui::EndGroup();
ImGui::PopID();
float last_x2 = ImGui::GetItemRectMax().x;
float next_x2 = last_x2 + ImGui::GetStyle().ItemSpacing.x +
ImGui::GetItemRectSize().x;
can_stack = (next_x2 < ImGui::GetWindowPos().x +
ImGui::GetWindowContentRegionMax().x);
}
ImGui::EndChild();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
void render_threads(struct task* task, struct task_state* state) {
const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x;
unsigned long total_stack_phys_used = 0;
unsigned long total_stack_phys_dirty = 0;
unsigned long total_cache_bytes = 0;
for (auto& [tid, thread_ref] : state->threads) {
struct thread_state* thread = thread_ref.get();
total_stack_phys_used += thread->stack_phys_used;
total_stack_phys_dirty += thread->stack_phys_dirty;
for (int i = 0; i < NUM_TCACHE_BUCKETS; i++) {
total_cache_bytes +=
thread->tcache_buckets[i].count * thread->tcache_buckets[i].slot_size;
}
}
char stack_dirty_total_text[50];
if (show_soft_dirty) {
snprintf(stack_dirty_total_text, sizeof(stack_dirty_total_text), "%lu KiB",
total_stack_phys_dirty / 1024);
} else {
strcpy(stack_dirty_total_text, "N/A [requires enabling soft dirty]");
}
ImGui::Text("Total stack memory: total %lu KiB, dirty %s",
total_stack_phys_used / 1024, stack_dirty_total_text);
ImGui::Text(
"Total thread cache memory (WITHOUT accounting for any kind of "
"overhead): %.1f KiB",
total_cache_bytes / 1024.0f);
if (!ImGui::BeginTable("threads", 8 + NUM_TCACHE_BUCKETS,
ImGuiTableFlags_Borders |
ImGuiTableFlags_SizingFixedFit |
ImGuiTableFlags_NoHostExtendX))
return;
ImGui::TableSetupColumn("TID", ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 7);
ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 15);
ImGui::TableSetupColumn("CPU", ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 3);
ImGui::TableSetupColumn("voluntary/\nforced\nswitches",
ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 10);
ImGui::TableSetupColumn("minor/major\nfaults",
ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 11);
ImGui::TableSetupColumn("stack\ntotal/\ndirty",
ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 8);
ImGui::TableSetupColumn("purge\npending", ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 7);
ImGui::TableSetupColumn("cache RAM\nWITHOUT\nOVERHEAD",
ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 9);
for (int i = 0; i < NUM_TCACHE_BUCKETS; i++) {
if (state->main_thread) {
unsigned short slot_size =
state->main_thread->tcache_buckets[i].slot_size;
char bucket_name[10];
snprintf(bucket_name, sizeof(bucket_name), "%hx", slot_size);
ImGui::TableSetupColumn(bucket_name, ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * (slot_size ? 4 : 1));
} else {
ImGui::TableSetupColumn("????", ImGuiTableColumnFlags_WidthFixed,
TEXT_BASE_WIDTH * 4);
}
}
ImGui::TableHeadersRow();
for (auto& [tid, thread_ref] : state->threads) {
struct thread_state* thread = thread_ref.get();
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", thread->tid);
ImGui::TableNextColumn();
ImGui::Text("%s", thread->comm);
ImGui::TableNextColumn();
if (thread->cpu_const_cycles < 3)
ImGui::TableSetBgColor(
ImGuiTableBgTarget_CellBg,
ImGui::GetColorU32(ImVec4(0.8f - thread->cpu_const_cycles * 0.3f,
0.0f, 0.0f, 1.0f)));
ImGui::Text("%lu", thread->cpu);
ImGui::TableNextColumn();
if (thread->switches_const_cycles < 3)
ImGui::TableSetBgColor(
ImGuiTableBgTarget_CellBg,
ImGui::GetColorU32(ImVec4(0.8f - thread->switches_const_cycles * 0.3f,
0.0f, 0.0f, 1.0f)));
ImGui::Text("%lu\n%lu", thread->voluntary_ctxt_switches,
thread->nonvoluntary_ctxt_switches);
ImGui::TableNextColumn();
if (thread->flt_const_cycles < 3)
ImGui::TableSetBgColor(
ImGuiTableBgTarget_CellBg,
ImGui::GetColorU32(ImVec4(0.8f - thread->flt_const_cycles * 0.3f,
0.0f, 0.0f, 1.0f)));
ImGui::Text("%lu\n%lu", thread->minflt, thread->majflt);
ImGui::TableNextColumn();
if (thread->stack_phys_used == 0) {
ImGui::Text("???\n???");
} else {
ImGui::Text("%6lu K", thread->stack_phys_used / 1024);
if (show_soft_dirty) {
ImGui::Text("%6lu K", thread->stack_phys_dirty / 1024);
} else {
ImGui::Text("N/A");
}
}
ImGui::TableNextColumn();
if (thread->should_purge) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
ImGui::GetColorU32(highlight_color));
ImGui::Text("X");
} else {
ImGui::Text(" ");
}
ImGui::TableNextColumn();
unsigned long cache_bytes = 0;
for (int i = 0; i < NUM_TCACHE_BUCKETS; i++) {
cache_bytes +=
thread->tcache_buckets[i].count * thread->tcache_buckets[i].slot_size;
}
ImGui::Text("%6.1lf K", cache_bytes / 1024.0f);
for (int i = 0; i < NUM_TCACHE_BUCKETS; i++) {
ImGui::TableNextColumn();
if (thread->tcache_buckets[i].count == 0) {
ImGui::TableSetBgColor(
ImGuiTableBgTarget_CellBg,
ImGui::GetColorU32(ImVec4(0.0f, 0.5f, 0.0f, 1.0f)));
}
if (thread->tcache_buckets[i].count != 0 ||
thread->tcache_buckets[i].limit != 0) {
ImGui::Text("%hhu/\n%hhu", thread->tcache_buckets[i].count,
thread->tcache_buckets[i].limit);
}
}
}
ImGui::EndTable();
}
void render_overview(struct task* task, struct task_state* state) {
if (state->probed_payloads) {
unsigned long all_span_pages =
state->full_pages[state->stats_history_len - 1] +
state->partial_pages[state->stats_history_len - 1] +
state->tcache_and_free_pages[state->stats_history_len - 1] +
state->free_pages[state->stats_history_len - 1];
double all_span_pages_KiB = all_span_pages * PAGE_SIZE / 1024.0f;
ImGui::Text(
"total physical memory in spans (*excluding* metadata pages and such): "
"%lu KiB",
all_span_pages * (PAGE_SIZE / 1024));
ImGui::Text("physical memory per slot state:");
ImGui::Text(
" %.1f KiB (%.2f%%) slot-allocated",
state->physical_allocated_KiB[state->stats_history_len - 1],
100.0f * state->physical_allocated_KiB[state->stats_history_len - 1] /
all_span_pages_KiB);
ImGui::Text(" %.1f KiB (%.2f%%) thread-cache-slots",
state->physical_tcache_KiB[state->stats_history_len - 1],
100.0f *
state->physical_tcache_KiB[state->stats_history_len - 1] /
all_span_pages_KiB);
ImGui::Text(" %.1f KiB (%.2f%%) free",
state->physical_free_KiB[state->stats_history_len - 1],
100.0f *
state->physical_free_KiB[state->stats_history_len - 1] /
all_span_pages_KiB);
ImGui::Text("OS physical page stats:");
ImGui::Text(" full: %lu (%.2f%%)\n",
(unsigned long)state->full_pages[state->stats_history_len - 1],
100.0f * state->full_pages[state->stats_history_len - 1] /
all_span_pages);
ImGui::Text(
" partially used: %lu (%.2f%%)\n",
(unsigned long)state->partial_pages[state->stats_history_len - 1],
100.0f * state->partial_pages[state->stats_history_len - 1] /
all_span_pages);
ImGui::Text(" tcache+free: %lu (%.2f%%)\n",
(unsigned long)
state->tcache_and_free_pages[state->stats_history_len - 1],
100.0f *
state->tcache_and_free_pages[state->stats_history_len - 1] /
all_span_pages);
ImGui::Text(" free: %lu (%.2f%%)\n",
(unsigned long)state->free_pages[state->stats_history_len - 1],
100.0f * state->free_pages[state->stats_history_len - 1] /
all_span_pages);
double max_slot_mem = 0;
ImU64 max_span_pages = 0;
for (unsigned long i = 0; i < state->stats_history_len; i++) {
max_slot_mem = std::max({max_slot_mem, state->physical_allocated_KiB[i],
state->physical_tcache_KiB[i],
state->physical_free_KiB[i]});
max_span_pages = std::max(
{max_span_pages, state->full_pages[i], state->partial_pages[i],
state->tcache_and_free_pages[i], state->free_pages[i]});
}
ImPlot::SetNextPlotLimitsX(0, STATS_HISTORY_MAX, ImGuiCond_Always);
ImPlot::SetNextPlotLimitsY(0, max_slot_mem, ImGuiCond_Always);
if (ImPlot::BeginPlot("physical memory by slot state", "time",
"KiB (physical)", ImVec2(-1, 200),
ImPlotFlags_NoChild)) {
ImPlot::PlotLine("allocated", state->physical_allocated_KiB,
state->stats_history_len);
ImPlot::PlotLine("thread cache", state->physical_tcache_KiB,
state->stats_history_len);
ImPlot::PlotLine("free", state->physical_free_KiB,
state->stats_history_len);
ImPlot::EndPlot();
}
ImPlot::SetNextPlotLimitsX(0, STATS_HISTORY_MAX, ImGuiCond_Always);
ImPlot::SetNextPlotLimitsY(0, max_span_pages, ImGuiCond_Always);
if (ImPlot::BeginPlot("physical OS pages (in spans) by state of contained "
"spans (including partial overlap)",
"time", "pages (physical)", ImVec2(-1, 200),
ImPlotFlags_NoChild)) {
ImPlot::PlotLine("fully allocated", state->full_pages,
state->stats_history_len);
ImPlot::PlotLine("partially allocated", state->partial_pages,
state->stats_history_len);
ImPlot::PlotLine("free except for tcache", state->tcache_and_free_pages,
state->stats_history_len);
ImPlot::PlotLine("completely free", state->free_pages,
state->stats_history_len);
ImPlot::EndPlot();
}
} else {
ImGui::Text(
"<enable swap-disturbing heap probes for slot-state-related stats>");
}
}
int main(int argc, char** argv) {
if (argc != 2)
errx(1, "usage: %s <pid>", argv[0]);
struct task task;
if (open_task(&task, atoi(argv[1])))
err(1, "unable to open task");
global_task = &task; // for now we only have one task
if (try_collect(&task))
err(1, "initial info collection failed");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0)
errx(1, "SDL_Init: %s", SDL_GetError());
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_WindowFlags window_flags =
(SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE |
SDL_WINDOW_ALLOW_HIGHDPI /*| SDL_WINDOW_MAXIMIZED*/
);
SDL_Window* window =
SDL_CreateWindow("PartitionAlloc inspector", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, 1920, 1080, window_flags);
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_context);
SDL_GL_SetSwapInterval(1);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::StyleColorsDark();
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
ImGui_ImplOpenGL2_Init();
io.Fonts->AddFontDefault();
small_font = io.Fonts->AddFontFromMemoryCompressedTTF(
ProggyTiny_compressed_data, ProggyTiny_compressed_size, 10.0f);
sdl_force_repaint_event = SDL_RegisterEvents(1);
pthread_t collector_thread;
if (pthread_create(&collector_thread, NULL, collector_thread_fn, NULL))
errx(1, "pthread_create");
while (1) {
SDL_Event event;
bool need_repaint = false;
while (true) {
if (need_repaint) {
if (!SDL_PollEvent(&event))
break;
} else {
if (!SDL_WaitEvent(&event))
break;
}
if (ImGui_ImplSDL2_ProcessEvent(&event))
need_repaint = true;
if (event.type == SDL_QUIT)
goto exit;
if (event.type == SDL_WINDOWEVENT &&
event.window.event == SDL_WINDOWEVENT_CLOSE &&
event.window.windowID == SDL_GetWindowID(window))
goto exit;
if (event.type == sdl_force_repaint_event)
need_repaint = true;
if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN ||
event.type == SDL_MOUSEBUTTONUP)
need_repaint = true;
}
if (pthread_mutex_lock(&update_lock))
errx(1, "pthread_mutex_lock");
struct task_state* state = task.cur_state;
state->reader_active = true;
if (pthread_mutex_unlock(&update_lock))
errx(1, "pthread_mutex_unlock");
ImGui_ImplOpenGL2_NewFrame();
ImGui_ImplSDL2_NewFrame(window);
ImGui::NewFrame();
// START actual rendering
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::Begin("PA heap state", NULL,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoSavedSettings);
bool freeze = !task.enable_collection;
ImGui::Checkbox("freeze", &freeze);
task.enable_collection = !freeze;
ImGui::SameLine(0.0f, 20.0f);
bool probe_payloads = task.probe_payloads;
ImGui::Checkbox("swap-disturbing heap probes [?]", &probe_payloads);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Allow probing all PA heap memory, not just metadata.\n"
"This can cause zeropage PTEs to be created\n"
"and disturbs swapping.\n"
"Therefore, slightly less information can be shown\n"
"about OS page state, and performance of the target\n"
"may be impacted further.\n"
"This is destructive; once it has been enabled once,\n"
"the process's memory will permanently look weird.");
}
task.probe_payloads = probe_payloads;
ImGui::SameLine(0.0f, 20.0f);
ImGui::Checkbox("show soft-dirty", &show_soft_dirty);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Show which pages have been modified since the\n"
"soft-dirty bits were reset.");
}
if (show_soft_dirty) {
ImGui::SameLine();
if (ImGui::Button("reset soft-dirty")) {
int soft_dirty_fd = openat(task.task_fd, "clear_refs", O_WRONLY);
if (soft_dirty_fd >= 0) {
if (write(soft_dirty_fd, "4", 1) != 1)
perror("write clear_refs");
close(soft_dirty_fd);
} else {
perror("open clear_refs");
}
}
}
ImGui::SameLine(0.0f, 20.0f);
if (probe_payloads) {
ImGui::Text("<forced pageout unavailable [?]>");
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"forced pageout is unavailable because\n"
"swap-disturbing heap probes are enabled.");
}
} else {
if (ImGui::Button("force pageout [?]")) {
for (size_t super_idx = 0; super_idx < state->superpage_count;
super_idx++) {
struct superpage* sp = &state->superpages[super_idx];
struct iovec iov = {.iov_base = (void*)sp->addr,
.iov_len = SUPERPAGE_SIZE};
int madv_ret = syscall(__NR_process_madvise, task.pidfd, &iov, 1,
MADV_PAGEOUT, 0);
printf("process_madvise says %d (%m)\n", madv_ret);
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("requires root privileges!\nrequires recent kernel");
}
#if BROKEN
ImGui::SameLine(0.0f, 20.0f);
ImGui::Checkbox("log scale", &enable_logscale);
#endif
if (ImGui::BeginTabBar("maintabbar", ImGuiTabBarFlags_None)) {
if (ImGui::BeginTabItem("overview")) {
render_overview(&task, state);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("superpages")) {
render_superpages(&task, state);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("buckets")) {
render_buckets(&task, state);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("threads")) {
render_threads(&task, state);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
// END actual rendering
if (pthread_mutex_lock(&update_lock))
errx(1, "pthread_mutex_lock");
if (task.cur_state == state) {
state->reader_active = false;
state = NULL;
}
if (pthread_mutex_unlock(&update_lock))
errx(1, "pthread_mutex_unlock");
if (state) {
// state has been updated in the meantime, we have to clean up
delete[] state->superpages;
delete[] state->vmas;
free(state->maps_buf);
delete state;
}
// ImPlot::ShowDemoWindow();
ImGui::Render();
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w,
clear_color.z * clear_color.w, clear_color.w);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}
exit:
ImGui_ImplOpenGL2_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
SDL_GL_DeleteContext(gl_context);
SDL_DestroyWindow(window);
SDL_Quit();
exit(0); // don't return!
}