diff --git a/userspace/programs/CMakeLists.txt b/userspace/programs/CMakeLists.txt index f46562d7..135432ac 100644 --- a/userspace/programs/CMakeLists.txt +++ b/userspace/programs/CMakeLists.txt @@ -47,6 +47,7 @@ set(USERSPACE_PROGRAMS sync tee test + top tr true TaskBar diff --git a/userspace/programs/top/CMakeLists.txt b/userspace/programs/top/CMakeLists.txt new file mode 100644 index 00000000..9e3e27e9 --- /dev/null +++ b/userspace/programs/top/CMakeLists.txt @@ -0,0 +1,9 @@ +set(SOURCES + main.cpp +) + +add_executable(top ${SOURCES}) +banan_link_library(top ban) +banan_link_library(top libc) + +install(TARGETS top OPTIONAL) diff --git a/userspace/programs/top/main.cpp b/userspace/programs/top/main.cpp new file mode 100644 index 00000000..e175e8f9 --- /dev/null +++ b/userspace/programs/top/main.cpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +winsize g_winsize; + +struct ProcessInfo +{ + pid_t pid; + BAN::String virt; + BAN::String phys; + uint32_t cpu_load; + BAN::String command; +}; +BAN::Vector g_process_infos; +BAN::HashMap g_process_prev_us; + +struct MemInfo +{ + size_t total_kib; + size_t free_kib; + size_t used_kib; +}; +MemInfo g_meminfo; + +void update_process_info(uint64_t delta_ms) +{ + g_process_infos.clear(); + const auto prev_us = BAN::move(g_process_prev_us); + + DIR* dirp = opendir("/proc"); + if (dirp == nullptr) + return; + + const dirent* dent; + while ((dent = readdir(dirp))) + { + char* endp; + const pid_t pid = strtol(dent->d_name, &endp, 10); + if (*endp) + continue; + + int pid_fd = openat(dirfd(dirp), dent->d_name, O_RDONLY); + if (pid_fd == -1) + continue; + + ProcessInfo info {}; + info.pid = pid; + + const auto read_from = [pid_fd](const char* file, void* buffer, size_t size) { + int fd = openat(pid_fd, file, O_RDONLY); + ASSERT(fd >= 0); + read(fd, buffer, size); + close(fd); + }; + + { + const auto bytes_to_string = [](size_t size) -> BAN::String { + if (size < 1024) + return MUST(BAN::String::formatted("{}", size)); + + size = size / 1024 * 10; + + size_t suffix_idx = 0; + for (; size >= 10240; size /= 1024) + suffix_idx++; + + constexpr char suffix[] { 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q' }; + if (size >= 100) + return MUST(BAN::String::formatted("{}{}", size / 10, suffix[suffix_idx])); + return MUST(BAN::String::formatted("{}.{}{}", size / 10, size % 10, suffix[suffix_idx])); + }; + + proc_meminfo_t meminfo; + read_from("meminfo", &meminfo, sizeof(meminfo)); + info.virt = bytes_to_string(meminfo.virt_pages * meminfo.page_size); + info.phys = bytes_to_string(meminfo.phys_pages * meminfo.page_size); + } + + { + char buffer[32]; + read_from("cputime", buffer, sizeof(buffer)); + buffer[sizeof(buffer) - 1] = '\0'; + + const auto cpu_us = strtoull(buffer, nullptr, 10) / 1000; + + uint64_t cpu_delta_us = cpu_us; + if (auto it = prev_us.find(pid); it != prev_us.end()) + cpu_delta_us -= it->value; + info.cpu_load = cpu_delta_us / delta_ms; + + MUST(g_process_prev_us.insert(pid, cpu_us)); + } + + { + char buffer[128]; + read_from("cmdline", buffer, sizeof(buffer)); + buffer[sizeof(buffer) - 1] = '\0'; + info.command = buffer; + } + + MUST(g_process_infos.push_back(BAN::move(info))); + + close(pid_fd); + } + + closedir(dirp); + + BAN::sort::sort(g_process_infos.begin(), g_process_infos.end(), [](auto& a, auto& b) { + return a.cpu_load > b.cpu_load; + }); +} + +void update_info(uint64_t delta_ms) +{ + update_process_info(delta_ms); + + { + int fd = open("/proc/meminfo", O_RDONLY); + ASSERT(fd >= 0); + full_meminfo_t meminfo; + read(fd, &meminfo, sizeof(meminfo)); + close(fd); + + g_meminfo.total_kib = (meminfo.free_pages + meminfo.used_pages) * meminfo.page_size / 1024; + g_meminfo.free_kib = meminfo.free_pages * meminfo.page_size / 1024; + g_meminfo.used_kib = meminfo.used_pages * meminfo.page_size / 1024; + } +} + +void render_info() +{ + size_t header_rows = 0; + + printf("\e[%zuHProcesses: %zu total\e[K", + ++header_rows, + g_process_infos.size() + ); + + printf("\e[%zuHMemory (MiB): %zu.%03zu total, %zu.%03zu free, %zu.%01zu used\e[K", + ++header_rows, + g_meminfo.total_kib / 1024, g_meminfo.total_kib % 1024 * 1000 / 1024, + g_meminfo.free_kib / 1024, g_meminfo.free_kib % 1024 * 1000 / 1024, + g_meminfo.used_kib / 1024, g_meminfo.used_kib % 1024 * 1000 / 1024 + ); + + printf("\e[%zuH\e[K", + ++header_rows + ); + + printf("\e[%zuH\e[7m PID VIRT PHYS %%CPU COMMAND\e[K\e[27m", + ++header_rows + ); + + const size_t count = BAN::Math::min(g_winsize.ws_row - header_rows, g_process_infos.size()); + for (size_t i = 0; i < count; i++) + { + const auto& info = g_process_infos[i]; + printf("\e[%zuH", i + header_rows + 1); + printf("%5d %5s %5s %3u.%01u %s", + info.pid, + info.virt.data(), + info.phys.data(), + info.cpu_load / 10, info.cpu_load % 10, + info.command.data() + ); + printf("\e[K"); + } + + printf("\e[J"); + + fflush(stdout); +} + +int main() +{ + signal(SIGWINCH, [](int) { tcgetwinsize(STDOUT_FILENO, &g_winsize); }); + if (tcgetwinsize(STDOUT_FILENO, &g_winsize) == -1) + { + fprintf(stderr, "could not get size of STDOUT\n"); + return 1; + } + + static volatile bool is_running = true; + signal(SIGINT, [](int) { is_running = false; }); + + const auto get_current_ms = []() -> uint64_t { + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000 + ts.tv_nsec / 1'000'000; + }; + + constexpr uint64_t update_freq_ms = 1000; + + printf("\e[?25l"); fflush(stdout); + + auto prev_update_ms = 0; + while (is_running) + { + const auto current_ms = get_current_ms(); + if (current_ms >= prev_update_ms + update_freq_ms) + { + update_info(current_ms - prev_update_ms); + + prev_update_ms += update_freq_ms; + if (prev_update_ms + update_freq_ms < current_ms) + prev_update_ms = current_ms; + } + + render_info(); + + const auto timeout_ms = (prev_update_ms + update_freq_ms) - current_ms; + const timespec timeout_ts { + .tv_sec = static_cast((timeout_ms / 1000)), + .tv_nsec = static_cast((timeout_ms % 1000) * 1'000'000) + }; + nanosleep(&timeout_ts, nullptr); + } + + printf("\e[?25h\n"); +}