diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0165ec1..84c9b5c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,6 +24,8 @@ per-process I/O statistics, ELF metadata, and thread information. - One-shot mode for a specific PID - Live mode dashboard with section filtering and refresh loop +- Default live section is `Overview` (`5`) with 32-sample trend lanes + (CPU, RSS, RX/s, TX/s, WR/s) - Uses procfs endpoints provided by the kernel module ### Tests diff --git a/README.md b/README.md index 6e9738b..7c4d30e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ - **CPU Usage Tracking**: Real-time CPU percentage calculation per process and thread - **ELF Section Analysis**: Binary base address and section boundaries - **Proc Interface**: Easy access through `/proc/proclens_module/` -- **Live Dashboard Mode**: No-arg mode refreshes every 1s with switchable Memory, Network, Threads, and I/O sections +- **Live Dashboard Mode**: No-arg mode refreshes every 1s with a condensed Overview page, recent trend plots, and switchable Memory, Network, Threads, and I/O sections - **Comprehensive Testing**: Unit tests and QEMU-based E2E testing - **Code Quality**: Pre-configured static analysis (sparse, cppcheck, checkpatch) @@ -72,9 +72,10 @@ Running without arguments starts a live dashboard: - Auto-refreshes every 1 second -- Defaults to section `1` (memory-related output) -- Shows quick controls at the top: `1` memory, `2` network, `3` threads, `4` I/O -- Shows command hints in-app: `1/2/3/4` switch sections, `0` changes PID +- Defaults to section `5` (overview summary) +- Overview includes 32-sample block-style histogram trend lanes for CPU, RSS, RX/s, TX/s, and WR/s based on recent snapshots +- Shows quick controls at the top: `1` memory, `2` network, `3` threads, `4` I/O, `5` overview +- Shows command hints in-app: `1/2/3/4/5` switch sections, `0` changes PID - Prints timestamps at top and bottom in `YY/MM/DD HH:MM:SS` - Keeps a snapshot history (up to 120 entries) for in-app navigation - Press Up/`k` for older snapshots, Down/`j` for newer snapshots, `f` to resume live follow diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md index 187394b..247b1b4 100644 --- a/docs/TECHNICAL.md +++ b/docs/TECHNICAL.md @@ -121,19 +121,31 @@ Simple C program that supports two modes: Prompts for a PID, then enters a 1-second refresh loop with section filtering. Each refresh prints start/end timestamps in `YY/MM/DD HH:MM:SS`. Snapshots are kept in an in-memory ring buffer (120 entries) for history browsing. +Default section is `5` (Overview). + +Overview mode includes trend lanes rendered from recent snapshot history: +- CPU% +- RSS +- RX/s +- TX/s +- WR/s + +Each lane uses 32 samples and block-style histogram glyphs, with one blank line +between lanes for readability. Controls shown in the header: -- `1` - Memory section (default) +- `1` - Memory section - `2` - Network section - `3` - Thread section - `4` - I/O section +- `5` - Overview section (default) - `0` - Prompt for a new PID (switch process) - `Up` or `k` - Older snapshot - `Down` or `j` - Newer snapshot - `f` - Return to live-follow mode -Keys `1`, `2`, `3`, `4` switch sections instantly (no Enter required). Pressing `0` temporarily -restores cooked terminal mode so the user can type a PID, then returns to raw mode. +Keys `1`, `2`, `3`, `4`, `5` switch sections instantly (no Enter required). Pressing `0` +temporarily restores cooked terminal mode so the user can type a PID, then returns to raw mode. ### I/O Stats diff --git a/docs/TESTING.md b/docs/TESTING.md index 2815e7c..95385a8 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -181,13 +181,16 @@ Expected behavior: - If `proclens_module` is not loaded, program exits immediately with status `-1` - If required proc files are missing (`pid`, `det`, `threads`), program exits immediately with status `-1` - Screen refreshes every 1 second -- Header shows controls (`1` memory, `2` network, `3` threads, `4` I/O, `0` switch PID) +- Header shows controls (`1` memory, `2` network, `3` threads, `4` I/O, `5` overview, `0` switch PID) - Each refresh shows `Snapshot start` and `Snapshot end` timestamps in `YY/MM/DD HH:MM:SS` +- Overview page shows 32-sample block-style histogram trend lanes for CPU, RSS, RX/s, TX/s, and WR/s once enough samples are collected +- Trend lanes are separated by one blank line (for example between RX/s and TX/s) - `Up`/`k` moves to older snapshots, `Down`/`j` moves toward newer snapshots - While browsing old snapshots, header indicates history-browsing mode - Press `f` to return to live-follow mode -- Default section is memory (`1`) +- Default section is overview (`5`) - Press `4` to view only I/O statistics (`[io]` section) +- Press `5` to return to the condensed overview page - Pressing `0` prompts for a new PID and continues live refresh for that PID Note: Live mode requires a real TTY for raw terminal input (`tcgetattr`/`tcsetattr`). diff --git a/docs/demo.gif b/docs/demo.gif index f36a9ce..7697d24 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/src/proclens.c b/src/proclens.c index a590c0d..4a2ab2a 100644 --- a/src/proclens.c +++ b/src/proclens.c @@ -80,16 +80,36 @@ static int set_raw_mode(void) return 0; } -#define PID_INPUT_MAX 20 -#define PROC_BUF_SIZE 262144 -#define MAX_SNAPSHOTS 120 -#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +#define PID_INPUT_MAX 20 +#define PROC_BUF_SIZE 262144 +#define MAX_SNAPSHOTS 120 +#define OVERVIEW_PLOT_WIDTH 32 +#define OVERVIEW_PLOT_BUF_SIZE ((OVERVIEW_PLOT_WIDTH * 4) + 1) +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) enum view_mode { VIEW_MEMORY = 1, VIEW_NETWORK = 2, VIEW_THREADS = 3, VIEW_IO = 4, + VIEW_OVERVIEW = 5, +}; + +struct overview_thread_entry { + char line[256]; + unsigned long long cpu_permyriad; + int valid; +}; + +struct overview_talker_entry { + int rank; + unsigned int fd; + char proto[16]; + char family[16]; + unsigned long long rx_bytes; + unsigned long long tx_bytes; + unsigned long long total_bytes; + int valid; }; struct live_snapshot { @@ -100,6 +120,24 @@ struct live_snapshot { char *threads_content; }; +static struct live_snapshot *get_snapshot_by_offset(struct live_snapshot *history, + int history_count, + int history_next, + int browse_offset); + +struct overview_metrics { + unsigned long long cpu_permyriad; + unsigned long long rss_kb; + unsigned long long rx_bytes; + unsigned long long tx_bytes; + unsigned long long write_bytes; + int has_cpu; + int has_rss; + int has_rx; + int has_tx; + int has_write; +}; + static void print_logo_text(void) { printf("%s%s\n", color_code(C_CYAN), color_code(C_BOLD)); @@ -116,6 +154,12 @@ static void print_logo_text(void) puts(" "); puts(" "); /* clang-format on */ + printf("%s%slatest release:%s https://github.com/navidpadid/ProcLens/releases/latest\n", + color_code(C_CYAN), color_code(C_BOLD), color_code(C_RESET)); + printf("%s%sversion:%s %s\n", color_code(C_CYAN), color_code(C_BOLD), color_code(C_RESET), + PROCLENS_VERSION); + printf("%s%slike ProcLens?%s consider giving it a star on GitHub.\n", color_code(C_CYAN), + color_code(C_BOLD), color_code(C_RESET)); printf("%s\n", color_code(C_RESET)); } @@ -332,6 +376,674 @@ static int is_memory_section_start(const char *line) strncmp(line, "Memory Layout Visualization:", 28) == 0; } +static int read_content_line(const char **cursor, char *line, size_t line_size) +{ + size_t line_len = 0; + + if (!cursor || !*cursor || !**cursor || !line || line_size < 2) + return 0; + + while ((*cursor)[line_len] && (*cursor)[line_len] != '\n' && line_len < line_size - 2) + line_len++; + + memcpy(line, *cursor, line_len); + if ((*cursor)[line_len] == '\n') { + line[line_len++] = '\n'; + *cursor += line_len; + } else { + *cursor += line_len; + } + line[line_len] = '\0'; + return 1; +} + +static void trim_newline(char *line) +{ + size_t len; + + if (!line) + return; + + len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[len - 1] = '\0'; + len--; + } +} + +static void copy_truncated_string(char *dst, size_t dst_size, const char *src) +{ + size_t len; + + if (!dst || dst_size == 0) + return; + + if (!src) { + dst[0] = '\0'; + return; + } + + len = strlen(src); + if (len >= dst_size) + len = dst_size - 1; + + memcpy(dst, src, len); + dst[len] = '\0'; +} + +static int +copy_line_with_prefix(const char *content, const char *prefix, char *out, size_t out_size) +{ + char line[2048]; + const char *cursor = content; + size_t prefix_len; + + if (!content || !prefix || !out || out_size == 0) + return 0; + + prefix_len = strlen(prefix); + while (read_content_line(&cursor, line, sizeof(line))) { + if (strncmp(line, prefix, prefix_len) == 0) { + snprintf(out, out_size, "%s", line); + return 1; + } + } + + return 0; +} + +static int parse_first_u64(const char *line, unsigned long long *value) +{ + const char *cursor; + + if (!line || !value) + return 0; + + cursor = line; + while (*cursor && (*cursor < '0' || *cursor > '9')) + cursor++; + + if (*cursor == '\0') + return 0; + + return sscanf(cursor, "%llu", value) == 1; +} + +static void extract_overview_metrics(const struct live_snapshot *snapshot, + struct overview_metrics *metrics) +{ + char line[256]; + unsigned long long whole; + unsigned long long frac; + + if (!metrics) + return; + + memset(metrics, 0, sizeof(*metrics)); + if (!snapshot || !snapshot->det_content) + return; + + if (copy_line_with_prefix(snapshot->det_content, "CPU Usage:", line, sizeof(line)) && + sscanf(line, "CPU Usage: %llu.%llu%%", &whole, &frac) == 2) { + metrics->cpu_permyriad = (whole * 100ULL) + frac; + metrics->has_cpu = 1; + } + + if (copy_line_with_prefix(snapshot->det_content, " RSS (Resident):", line, sizeof(line)) && + parse_first_u64(line, &metrics->rss_kb)) { + metrics->has_rss = 1; + } + + if (copy_line_with_prefix(snapshot->det_content, "rx_bytes:", line, sizeof(line)) && + parse_first_u64(line, &metrics->rx_bytes)) { + metrics->has_rx = 1; + } + + if (copy_line_with_prefix(snapshot->det_content, "tx_bytes:", line, sizeof(line)) && + parse_first_u64(line, &metrics->tx_bytes)) { + metrics->has_tx = 1; + } + + if (copy_line_with_prefix(snapshot->det_content, "write_bytes:", line, sizeof(line)) && + parse_first_u64(line, &metrics->write_bytes)) { + metrics->has_write = 1; + } +} + +static void +format_compact_size(unsigned long long value, char *out, size_t out_size, const char *suffix) +{ + static const char *const units[] = {"B", "KB", "MB", "GB", "TB"}; + unsigned long long scaled = value; + int unit_index = 0; + + if (!out || out_size == 0) + return; + + while (scaled >= 1024 && unit_index < (int)ARRAY_SIZE(units) - 1) { + scaled /= 1024; + unit_index++; + } + + if (suffix) + snprintf(out, out_size, "%llu %s%s", scaled, units[unit_index], suffix); + else + snprintf(out, out_size, "%llu %s", scaled, units[unit_index]); +} + +static void +render_sparkline(const unsigned long long *values, int value_count, char *out, size_t out_size) +{ + static const char *const levels[] = {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}; + unsigned long long min_value; + unsigned long long max_value; + unsigned long long range; + size_t out_len = 0; + int i; + int levels_count; + + if (!out || out_size == 0) + return; + + out[0] = '\0'; + if (!values || value_count <= 0) + return; + + min_value = values[0]; + max_value = values[0]; + for (i = 1; i < value_count; i++) { + if (values[i] < min_value) + min_value = values[i]; + if (values[i] > max_value) + max_value = values[i]; + } + + range = max_value - min_value; + levels_count = (int)ARRAY_SIZE(levels); + for (i = 0; i < value_count; i++) { + const char *glyph; + int level_index; + size_t glyph_len; + + if (range == 0) + glyph = max_value == 0 ? levels[0] : levels[levels_count - 1]; + else { + level_index = (int)(((values[i] - min_value) * (levels_count - 1)) / range); + if (values[i] > 0 && level_index < levels_count - 1) + level_index++; + glyph = levels[level_index]; + } + + glyph_len = strlen(glyph); + if (out_len + glyph_len >= out_size) + break; + + memcpy(out + out_len, glyph, glyph_len); + out_len += glyph_len; + } + out[out_len] = '\0'; +} + +static int collect_overview_series(const struct live_snapshot *history, + int history_count, + int history_next, + int browse_offset, + unsigned long long *cpu_values, + unsigned long long *rss_values, + unsigned long long *rx_rate_values, + unsigned long long *tx_rate_values, + unsigned long long *write_rate_values, + int max_values) +{ + struct overview_metrics previous_metrics; + int start_offset; + int offset; + int points = 0; + int have_previous = 0; + + if (!history || history_count <= 0 || max_values <= 0) + return 0; + + start_offset = browse_offset + max_values - 1; + if (start_offset >= history_count) + start_offset = history_count - 1; + + memset(&previous_metrics, 0, sizeof(previous_metrics)); + for (offset = start_offset; offset >= browse_offset; offset--) { + struct live_snapshot *snap; + struct overview_metrics metrics; + + snap = get_snapshot_by_offset((struct live_snapshot *)history, history_count, + history_next, offset); + if (!snap) + continue; + + extract_overview_metrics(snap, &metrics); + cpu_values[points] = metrics.has_cpu ? metrics.cpu_permyriad : 0; + rss_values[points] = metrics.has_rss ? metrics.rss_kb : 0; + + if (have_previous && metrics.has_rx && previous_metrics.has_rx && + metrics.rx_bytes >= previous_metrics.rx_bytes) { + rx_rate_values[points] = metrics.rx_bytes - previous_metrics.rx_bytes; + } else { + rx_rate_values[points] = 0; + } + + if (have_previous && metrics.has_tx && previous_metrics.has_tx && + metrics.tx_bytes >= previous_metrics.tx_bytes) { + tx_rate_values[points] = metrics.tx_bytes - previous_metrics.tx_bytes; + } else { + tx_rate_values[points] = 0; + } + + if (have_previous && metrics.has_write && previous_metrics.has_write && + metrics.write_bytes >= previous_metrics.write_bytes) { + write_rate_values[points] = + metrics.write_bytes - previous_metrics.write_bytes; + } else { + write_rate_values[points] = 0; + } + + previous_metrics = metrics; + have_previous = 1; + points++; + } + + return points; +} + +static void print_overview_plots(const struct live_snapshot *snapshot, + const struct live_snapshot *history, + int history_count, + int history_next, + int browse_offset) +{ + unsigned long long cpu_values[OVERVIEW_PLOT_WIDTH]; + unsigned long long rss_values[OVERVIEW_PLOT_WIDTH]; + unsigned long long rx_rate_values[OVERVIEW_PLOT_WIDTH]; + unsigned long long tx_rate_values[OVERVIEW_PLOT_WIDTH]; + unsigned long long write_rate_values[OVERVIEW_PLOT_WIDTH]; + struct overview_metrics current_metrics; + char cpu_plot[OVERVIEW_PLOT_BUF_SIZE]; + char rss_plot[OVERVIEW_PLOT_BUF_SIZE]; + char rx_plot[OVERVIEW_PLOT_BUF_SIZE]; + char tx_plot[OVERVIEW_PLOT_BUF_SIZE]; + char write_plot[OVERVIEW_PLOT_BUF_SIZE]; + char rss_label[32]; + char rx_label[32]; + char tx_label[32]; + char write_label[32]; + unsigned long long current_rx_rate = 0; + unsigned long long current_tx_rate = 0; + unsigned long long current_write_rate = 0; + int point_count; + + memset(cpu_values, 0, sizeof(cpu_values)); + memset(rss_values, 0, sizeof(rss_values)); + memset(rx_rate_values, 0, sizeof(rx_rate_values)); + memset(tx_rate_values, 0, sizeof(tx_rate_values)); + memset(write_rate_values, 0, sizeof(write_rate_values)); + + point_count = collect_overview_series( + history, history_count, history_next, browse_offset, cpu_values, rss_values, + rx_rate_values, tx_rate_values, write_rate_values, ARRAY_SIZE(cpu_values)); + render_sparkline(cpu_values, point_count, cpu_plot, sizeof(cpu_plot)); + render_sparkline(rss_values, point_count, rss_plot, sizeof(rss_plot)); + render_sparkline(rx_rate_values, point_count, rx_plot, sizeof(rx_plot)); + render_sparkline(tx_rate_values, point_count, tx_plot, sizeof(tx_plot)); + render_sparkline(write_rate_values, point_count, write_plot, sizeof(write_plot)); + + extract_overview_metrics(snapshot, ¤t_metrics); + if (point_count > 0) { + current_rx_rate = rx_rate_values[point_count - 1]; + current_tx_rate = tx_rate_values[point_count - 1]; + current_write_rate = write_rate_values[point_count - 1]; + } + + format_compact_size(current_metrics.rss_kb * 1024ULL, rss_label, sizeof(rss_label), NULL); + format_compact_size(current_rx_rate, rx_label, sizeof(rx_label), "/s"); + format_compact_size(current_tx_rate, tx_label, sizeof(tx_label), "/s"); + format_compact_size(current_write_rate, write_label, sizeof(write_label), "/s"); + + printf("%s%sTRENDS%s\n", color_code(C_CYAN), color_code(C_BOLD), color_code(C_RESET)); + if (point_count <= 0) { + puts(" collecting samples..."); + puts(""); + return; + } + + printf("%s CPU%% |%s| %4llu.%02llu%%%s\n", color_code(C_CYAN), cpu_plot, + current_metrics.cpu_permyriad / 100ULL, current_metrics.cpu_permyriad % 100ULL, + color_code(C_RESET)); + puts(""); + printf("%s RSS |%s| %s%s\n", color_code(C_CYAN), rss_plot, rss_label, + color_code(C_RESET)); + puts(""); + printf("%s RX/s |%s| %s%s\n", color_code(C_CYAN), rx_plot, rx_label, + color_code(C_RESET)); + puts(""); + printf("%s TX/s |%s| %s%s\n", color_code(C_CYAN), tx_plot, tx_label, + color_code(C_RESET)); + puts(""); + printf("%s WR/s |%s| %s%s\n", color_code(C_CYAN), write_plot, write_label, + color_code(C_RESET)); + puts(""); +} + +static int collect_top_talkers(const char *det_content, + struct overview_talker_entry *talkers, + int max_talkers, + int *saw_none) +{ + char line[2048]; + const char *cursor = det_content; + int in_top_talkers = 0; + int count = 0; + + if (saw_none) + *saw_none = 0; + + if (!det_content || !talkers || max_talkers <= 0) + return 0; + + while (read_content_line(&cursor, line, sizeof(line))) { + if (!in_top_talkers) { + if (strncmp(line, "top_talkers:", 12) == 0) + in_top_talkers = 1; + continue; + } + + if (strncmp(line, " #", 3) == 0) { + if (count < max_talkers) { + int parsed; + struct overview_talker_entry *entry = &talkers[count]; + + memset(entry, 0, sizeof(*entry)); + parsed = sscanf( + line, + " #%d FD %u Proto: %15s Family: %15s RX bytes=%llu TX bytes=%llu Total bytes=%llu", + &entry->rank, &entry->fd, entry->proto, entry->family, + &entry->rx_bytes, &entry->tx_bytes, &entry->total_bytes); + if (parsed == 7) + entry->valid = 1; + } + count++; + continue; + } + + if (strncmp(line, " none", 6) == 0) { + if (saw_none) + *saw_none = 1; + break; + } + + if (strncmp(line, "Open Sockets:", 13) == 0 || strncmp(line, "[io]", 4) == 0) + break; + } + + return count; +} + +static void maybe_insert_top_thread(struct overview_thread_entry *top_threads, + int top_len, + const char *line, + unsigned long long cpu_permyriad) +{ + int i; + int insert_at = -1; + + for (i = 0; i < top_len; i++) { + if (!top_threads[i].valid || cpu_permyriad > top_threads[i].cpu_permyriad) { + insert_at = i; + break; + } + } + + if (insert_at < 0) + return; + + for (i = top_len - 1; i > insert_at; i--) + top_threads[i] = top_threads[i - 1]; + + top_threads[insert_at].cpu_permyriad = cpu_permyriad; + top_threads[insert_at].valid = 1; + copy_truncated_string(top_threads[insert_at].line, sizeof(top_threads[insert_at].line), + line); + trim_newline(top_threads[insert_at].line); +} + +static int collect_top_threads(const char *threads_content, + struct overview_thread_entry *top_threads, + int top_len) +{ + char line[2048]; + const char *cursor = threads_content; + int count = 0; + + if (!threads_content || !top_threads || top_len <= 0) + return 0; + + while (read_content_line(&cursor, line, sizeof(line))) { + int tid; + char name[32]; + unsigned long long cpu_whole; + unsigned long long cpu_frac; + char state; + int priority; + int nice_value; + char affinity[64]; + int parsed; + + if (strncmp(line, "TID", 3) == 0 || strncmp(line, "-----", 5) == 0 || + strncmp(line, "Total threads:", 14) == 0 || + strncmp(line, "--------------------------------", 32) == 0) + continue; + + parsed = sscanf(line, "%d %31s %llu.%llu %c %d %d %63s", &tid, name, &cpu_whole, + &cpu_frac, &state, &priority, &nice_value, affinity); + if (parsed != 8) + continue; + + maybe_insert_top_thread(top_threads, top_len, line, + (cpu_whole * 100ULL) + cpu_frac); + count++; + } + + return count; +} + +static void print_overview_view(const struct live_snapshot *snapshot, + const struct live_snapshot *history, + int history_count, + int history_next, + int browse_offset) +{ + char rss_line[256]; + char vsz_line[256]; + char swap_line[256]; + char major_faults[256]; + char minor_faults[256]; + char sockets_total[256]; + char rx_bytes[256]; + char tx_bytes[256]; + char retransmits[256]; + char drops[256]; + char read_bytes[256]; + char write_bytes[256]; + char syscr[256]; + char syscw[256]; + char io_intensity[256]; + char io_status[256]; + char total_threads[256]; + struct overview_talker_entry top_talkers[2] = {0}; + struct overview_thread_entry top_threads[3] = {0}; + int top_talker_count; + int top_thread_count; + int saw_no_talkers = 0; + int i; + + memset(rss_line, 0, sizeof(rss_line)); + memset(vsz_line, 0, sizeof(vsz_line)); + memset(swap_line, 0, sizeof(swap_line)); + memset(major_faults, 0, sizeof(major_faults)); + memset(minor_faults, 0, sizeof(minor_faults)); + memset(sockets_total, 0, sizeof(sockets_total)); + memset(rx_bytes, 0, sizeof(rx_bytes)); + memset(tx_bytes, 0, sizeof(tx_bytes)); + memset(retransmits, 0, sizeof(retransmits)); + memset(drops, 0, sizeof(drops)); + memset(read_bytes, 0, sizeof(read_bytes)); + memset(write_bytes, 0, sizeof(write_bytes)); + memset(syscr, 0, sizeof(syscr)); + memset(syscw, 0, sizeof(syscw)); + memset(io_intensity, 0, sizeof(io_intensity)); + memset(io_status, 0, sizeof(io_status)); + memset(total_threads, 0, sizeof(total_threads)); + + copy_line_with_prefix(snapshot->det_content, " RSS (Resident):", rss_line, + sizeof(rss_line)); + copy_line_with_prefix(snapshot->det_content, " VSZ (Virtual):", vsz_line, + sizeof(vsz_line)); + copy_line_with_prefix(snapshot->det_content, " Swap Usage:", swap_line, sizeof(swap_line)); + copy_line_with_prefix(snapshot->det_content, " - Major:", major_faults, + sizeof(major_faults)); + copy_line_with_prefix(snapshot->det_content, " - Minor:", minor_faults, + sizeof(minor_faults)); + copy_line_with_prefix(snapshot->det_content, "sockets_total:", sockets_total, + sizeof(sockets_total)); + copy_line_with_prefix(snapshot->det_content, "rx_bytes:", rx_bytes, sizeof(rx_bytes)); + copy_line_with_prefix(snapshot->det_content, "tx_bytes:", tx_bytes, sizeof(tx_bytes)); + copy_line_with_prefix(snapshot->det_content, "tcp_retransmits:", retransmits, + sizeof(retransmits)); + copy_line_with_prefix(snapshot->det_content, "drops:", drops, sizeof(drops)); + copy_line_with_prefix(snapshot->det_content, "read_bytes:", read_bytes, sizeof(read_bytes)); + copy_line_with_prefix(snapshot->det_content, "write_bytes:", write_bytes, + sizeof(write_bytes)); + copy_line_with_prefix(snapshot->det_content, "syscr:", syscr, sizeof(syscr)); + copy_line_with_prefix(snapshot->det_content, "syscw:", syscw, sizeof(syscw)); + copy_line_with_prefix(snapshot->det_content, "io_intensity:", io_intensity, + sizeof(io_intensity)); + copy_line_with_prefix(snapshot->det_content, "status:", io_status, sizeof(io_status)); + copy_line_with_prefix(snapshot->threads_content, "Total threads:", total_threads, + sizeof(total_threads)); + + trim_newline(rss_line); + trim_newline(vsz_line); + trim_newline(swap_line); + trim_newline(major_faults); + trim_newline(minor_faults); + trim_newline(sockets_total); + trim_newline(rx_bytes); + trim_newline(tx_bytes); + trim_newline(retransmits); + trim_newline(drops); + trim_newline(read_bytes); + trim_newline(write_bytes); + trim_newline(syscr); + trim_newline(syscw); + trim_newline(io_intensity); + trim_newline(io_status); + trim_newline(total_threads); + + top_talker_count = collect_top_talkers(snapshot->det_content, top_talkers, + ARRAY_SIZE(top_talkers), &saw_no_talkers); + top_thread_count = collect_top_threads(snapshot->threads_content, top_threads, + ARRAY_SIZE(top_threads)); + + printf("%s%sOVERVIEW%s\n", color_code(C_GREEN), color_code(C_BOLD), color_code(C_RESET)); + puts("---------------------------------------------------------------"); + print_overview_plots(snapshot, history, history_count, history_next, browse_offset); + + printf("%s%sMEMORY SNAPSHOT%s\n", color_code(C_BLUE), color_code(C_BOLD), + color_code(C_RESET)); + if (rss_line[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), rss_line, color_code(C_RESET)); + if (vsz_line[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), vsz_line, color_code(C_RESET)); + if (swap_line[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), swap_line, color_code(C_RESET)); + if (major_faults[0] != '\0' || minor_faults[0] != '\0') { + puts(" Page Faults:"); + if (major_faults[0] != '\0') + printf("%s%s%s\n", color_code(C_CYAN), major_faults, color_code(C_RESET)); + if (minor_faults[0] != '\0') + printf("%s%s%s\n", color_code(C_CYAN), minor_faults, color_code(C_RESET)); + } + puts(""); + + printf("%s%sNETWORK SNAPSHOT%s\n", color_code(C_GREEN), color_code(C_BOLD), + color_code(C_RESET)); + if (sockets_total[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), sockets_total, color_code(C_RESET)); + if (rx_bytes[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), rx_bytes, color_code(C_RESET)); + if (tx_bytes[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), tx_bytes, color_code(C_RESET)); + if (retransmits[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), retransmits, color_code(C_RESET)); + if (drops[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), drops, color_code(C_RESET)); + puts(" Top talkers:"); + if (top_talker_count > 0) { + printf("%s RANK FD PROTO FAMILY RX_BYTES TX_BYTES TOTAL_BYTES%s\n", + color_code(C_CYAN), color_code(C_RESET)); + printf("%s ---- --- ------ ---------- --------- --------- -----------%s\n", + color_code(C_CYAN), color_code(C_RESET)); + for (i = 0; i < top_talker_count && i < (int)ARRAY_SIZE(top_talkers); i++) { + if (!top_talkers[i].valid) + continue; + printf("%s #%-3d %-3u %-6s %-10s %-9llu %-9llu %-9llu%s\n", + color_code(C_MAGENTA), top_talkers[i].rank, top_talkers[i].fd, + top_talkers[i].proto, top_talkers[i].family, top_talkers[i].rx_bytes, + top_talkers[i].tx_bytes, top_talkers[i].total_bytes, + color_code(C_RESET)); + } + } else if (saw_no_talkers) { + puts(" none"); + } else { + puts(" unavailable"); + } + puts(""); + + printf("%s%sI/O SNAPSHOT%s\n", color_code(C_GREEN), color_code(C_BOLD), + color_code(C_RESET)); + if (io_status[0] != '\0') { + printf("%s%s%s\n", color_code(C_CYAN), io_status, color_code(C_RESET)); + } else { + if (read_bytes[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), read_bytes, color_code(C_RESET)); + if (write_bytes[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), write_bytes, color_code(C_RESET)); + if (syscr[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), syscr, color_code(C_RESET)); + if (syscw[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), syscw, color_code(C_RESET)); + if (io_intensity[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), io_intensity, color_code(C_RESET)); + } + puts(""); + + printf("%s%sTHREAD HOTSPOTS%s\n", color_code(C_MAGENTA), color_code(C_BOLD), + color_code(C_RESET)); + if (total_threads[0] != '\0') + printf("%s%s%s\n", color_code(C_YELLOW), total_threads, color_code(C_RESET)); + if (top_thread_count > 0) { + puts(" Top threads by CPU:"); + printf("%s TID NAME CPU(%%) STATE PRIORITY NICE CPU_AFFINITY%s\n", + color_code(C_CYAN), color_code(C_RESET)); + printf("%s ----- --------------- ------- ----- -------- ---- ----------------%s\n", + color_code(C_CYAN), color_code(C_RESET)); + for (i = 0; i < (int)ARRAY_SIZE(top_threads); i++) { + if (!top_threads[i].valid) + continue; + printf("%s %s%s\n", color_code(C_MAGENTA), top_threads[i].line, + color_code(C_RESET)); + } + } else { + puts(" Top threads by CPU: unavailable"); + } +} + /* * Print leading lines of det output (PID, Name, CPU Usage) that appear * before any memory section or network section header. @@ -627,6 +1339,9 @@ static const char *view_name(int view) if (view == VIEW_IO) return "I/O"; + if (view == VIEW_OVERVIEW) + return "Overview"; + return "Unknown"; } @@ -648,9 +1363,9 @@ print_live_header(const struct live_snapshot *snap, int browse_offset, int histo view_name(snap->view)); printf("%sSnapshot index:%s %d/%d\n", color_code(C_YELLOW), color_code(C_RESET), history_count - browse_offset, history_count); - puts("Sections: [1] Memory [2] Network [3] Threads [4] I/O"); + puts("Sections: [1] Memory [2] Network [3] Threads [4] I/O [5] Overview"); puts("History: [Up/k] older [Down/j] newer [f] follow live"); - puts("Commands: 1/2/3/4 switch view, 0 change PID, Ctrl+C exit"); + puts("Commands: 1/2/3/4/5 switch view, 0 change PID, Ctrl+C exit"); if (browse_offset > 0) puts("Mode: browsing history (auto-refresh paused)"); else @@ -676,7 +1391,7 @@ static void free_snapshot(struct live_snapshot *snap) snap->threads_content = NULL; snap->pid[0] = '\0'; snap->captured_at[0] = '\0'; - snap->view = VIEW_MEMORY; + snap->view = VIEW_OVERVIEW; } static void @@ -746,7 +1461,8 @@ static int capture_live_snapshot(const char *pid_str, int view, struct live_snap if (read_proc_file_alloc("det", &snapshot->det_content) < 0) goto fail; - if (view == VIEW_THREADS && read_proc_file_alloc("threads", &snapshot->threads_content) < 0) + if ((view == VIEW_THREADS || view == VIEW_OVERVIEW) && + read_proc_file_alloc("threads", &snapshot->threads_content) < 0) goto fail; return 0; @@ -756,7 +1472,11 @@ static int capture_live_snapshot(const char *pid_str, int view, struct live_snap return -1; } -static void print_live_snapshot(const struct live_snapshot *snapshot) +static void print_live_snapshot(const struct live_snapshot *snapshot, + const struct live_snapshot *history, + int history_count, + int history_next, + int browse_offset) { if (!snapshot->det_content) return; @@ -771,6 +1491,12 @@ static void print_live_snapshot(const struct live_snapshot *snapshot) print_cmdline(snapshot->pid); print_det_preamble(snapshot->det_content); + if (snapshot->view == VIEW_OVERVIEW) { + print_overview_view(snapshot, history, history_count, history_next, browse_offset); + print_live_footer(snapshot->captured_at); + return; + } + if (snapshot->view == VIEW_MEMORY) { print_memory_view(snapshot->det_content); print_live_footer(snapshot->captured_at); @@ -850,7 +1576,7 @@ static void run_live_mode(void) struct live_snapshot snapshot; char pid_user[PID_INPUT_MAX]; int key; - int view = VIEW_MEMORY; + int view = VIEW_OVERVIEW; int browse_offset = 0; int history_count = 0; int history_next = 0; @@ -879,7 +1605,8 @@ static void run_live_mode(void) get_snapshot_by_offset(history, history_count, history_next, browse_offset); if (current) { print_live_header(current, browse_offset, history_count); - print_live_snapshot(current); + print_live_snapshot(current, history, history_count, history_next, + browse_offset); } fflush(stdout); @@ -906,6 +1633,10 @@ static void run_live_mode(void) view = VIEW_IO; browse_offset = 0; clear_snapshot_history(history, &history_count, &history_next); + } else if (key == '5') { + view = VIEW_OVERVIEW; + browse_offset = 0; + clear_snapshot_history(history, &history_count, &history_next); } else if (key == 'k') { if (browse_offset + 1 < history_count) browse_offset++; @@ -947,6 +1678,7 @@ static void print_usage(void) printf(" 2 Network section\n"); printf(" 3 Threads section\n"); printf(" 4 I/O section\n"); + printf(" 5 Overview section\n"); printf(" 0 Change PID\n"); printf(" Up/k Older snapshot Down/j Newer snapshot\n"); printf(" f Resume live follow\n"); diff --git a/src/proclens_tests.c b/src/proclens_tests.c index d9d2e1e..a6d3b30 100644 --- a/src/proclens_tests.c +++ b/src/proclens_tests.c @@ -391,6 +391,161 @@ static void test_io_view_starts_from_io_section(void) assert(strstr(output_buf, "io_intensity: 456 B")); } +static void test_overview_view_combines_sections(void) +{ + struct live_snapshot history[MAX_SNAPSHOTS]; + struct live_snapshot snap; + struct live_snapshot *current; + int history_count = 0; + int history_next = 0; + const char *det = + "Process ID: 4321\n" + "Name: demo\n" + "CPU Usage: 77.50%\n" + "Memory Pressure Statistics:\n" + " RSS (Resident): 4096 KB\n" + " VSZ (Virtual): 8192 KB\n" + " Swap Usage: 64 KB\n" + " Page Faults:\n" + " - Major: 3\n" + " - Minor: 44\n" + "[network]\n" + "sockets_total: 4 (tcp: 3, udp: 1, unix: 0)\n" + "rx_bytes: 1200\n" + "tx_bytes: 2300\n" + "tcp_retransmits: 9\n" + "drops: 1\n" + "top_talkers:\n" + " #1 FD 9 Proto: TCP Family: AF_INET RX bytes=900 TX bytes=1200 Total bytes=2100\n" + " #2 FD 4 Proto: UDP Family: AF_INET RX bytes=300 TX bytes=100 Total bytes=400\n" + "Open Sockets:\n" + "[io]\n" + "syscr: 12\n" + "syscw: 13\n" + "read_bytes: 1024\n" + "write_bytes: 2048\n" + "io_intensity: 3072\n"; + const char *det_older_1 = "Process ID: 4321\n" + "Name: demo\n" + "CPU Usage: 12.50%\n" + "Memory Pressure Statistics:\n" + " RSS (Resident): 2048 KB\n" + "[network]\n" + "rx_bytes: 100\n" + "tx_bytes: 150\n" + "top_talkers:\n" + " none\n" + "Open Sockets:\n" + "[io]\n" + "write_bytes: 300\n"; + const char *det_older_2 = "Process ID: 4321\n" + "Name: demo\n" + "CPU Usage: 30.00%\n" + "Memory Pressure Statistics:\n" + " RSS (Resident): 3072 KB\n" + "[network]\n" + "rx_bytes: 400\n" + "tx_bytes: 600\n" + "top_talkers:\n" + " none\n" + "Open Sockets:\n" + "[io]\n" + "write_bytes: 900\n"; + const char *threads = + "TID NAME CPU(%) STATE PRIORITY NICE CPU_AFFINITY\n" + "----- --------------- ------- ----- -------- ---- ----------------\n" + "1001 worker-a 42.50 R 0 0 0,1\n" + "1002 worker-b 31.25 S 0 0 0,1\n" + "1003 io-loop 8.00 D 0 0 0,1\n" + "---------------------------------------------------------------\n" + "Total threads: 3\n"; + + reset_mocks(); + memset(history, 0, sizeof(history)); + memset(&snap, 0, sizeof(snap)); + snap.view = VIEW_OVERVIEW; + snap.det_content = strdup(det_older_1); + snap.threads_content = strdup(threads); + append_snapshot(history, &history_count, &history_next, &snap); + + memset(&snap, 0, sizeof(snap)); + snap.view = VIEW_OVERVIEW; + snap.det_content = strdup(det_older_2); + snap.threads_content = strdup(threads); + append_snapshot(history, &history_count, &history_next, &snap); + + memset(&snap, 0, sizeof(snap)); + snap.view = VIEW_OVERVIEW; + snprintf(snap.pid, sizeof(snap.pid), "%s", "4321"); + snprintf(snap.captured_at, sizeof(snap.captured_at), "%s", "26/04/10 12:34:56"); + snap.det_content = strdup(det); + snap.threads_content = strdup(threads); + append_snapshot(history, &history_count, &history_next, &snap); + current = get_snapshot_by_offset(history, history_count, history_next, 0); + + print_live_snapshot(current, history, history_count, history_next, 0); + + assert(strstr(output_buf, "OVERVIEW")); + assert(strstr(output_buf, "TRENDS")); + assert(strstr(output_buf, "CPU%")); + assert(strstr(output_buf, "|▁")); + assert(strstr(output_buf, "█|")); + assert(strstr(output_buf, "RX/s")); + assert(strstr(output_buf, "TX/s")); + { + char *rx_line = strstr(output_buf, "RX/s |"); + char *tx_line = strstr(output_buf, "TX/s |"); + char *p; + int has_blank_line = 0; + + assert(rx_line); + assert(tx_line); + assert(tx_line > rx_line); + + for (p = rx_line; p + 1 < tx_line; p++) { + if (p[0] == '\n' && p[1] == '\n') { + has_blank_line = 1; + break; + } + } + assert(has_blank_line); + } + assert(strstr(output_buf, "WR/s")); + assert(strstr(output_buf, "MEMORY SNAPSHOT")); + assert(strstr(output_buf, "NETWORK SNAPSHOT")); + assert(strstr(output_buf, "I/O SNAPSHOT")); + assert(strstr(output_buf, "THREAD HOTSPOTS")); + assert(strstr(output_buf, "CPU Usage: 77.50%")); + assert(strstr(output_buf, "RSS (Resident): 4096 KB")); + assert(strstr(output_buf, "sockets_total: 4 (tcp: 3, udp: 1, unix: 0)")); + assert(strstr(output_buf, + "RANK FD PROTO FAMILY RX_BYTES TX_BYTES TOTAL_BYTES")); + assert(strstr(output_buf, "#1 9 TCP AF_INET")); + assert(strstr(output_buf, "io_intensity: 3072")); + assert(strstr(output_buf, "Total threads: 3")); + assert(strstr(output_buf, + "TID NAME CPU(%) STATE PRIORITY NICE CPU_AFFINITY")); + assert(strstr(output_buf, "worker-a")); + assert(strstr(output_buf, "worker-b")); + + clear_snapshot_history(history, &history_count, &history_next); +} + +static void test_capture_live_snapshot_reads_threads_for_overview(void) +{ + struct live_snapshot snap; + int rc; + + reset_mocks(); + rc = capture_live_snapshot("99", VIEW_OVERVIEW, &snap); + + assert(rc == 0); + assert(snap.det_content != NULL); + assert(snap.threads_content != NULL); + + free_snapshot(&snap); +} + static void test_format_current_time_layout(void) { char buf[32]; @@ -425,12 +580,13 @@ static void test_live_header_and_footer_show_timestamps(void) memset(&snap, 0, sizeof(snap)); snprintf(snap.pid, sizeof(snap.pid), "%s", "123"); snprintf(snap.captured_at, sizeof(snap.captured_at), "%s", "26/03/26 12:34:56"); - snap.view = VIEW_MEMORY; + snap.view = VIEW_OVERVIEW; print_live_header(&snap, 0, 1); print_live_footer(snap.captured_at); assert(strstr(output_buf, "Snapshot start: ")); assert(strstr(output_buf, "Snapshot end: ")); + assert(strstr(output_buf, "[5] Overview")); } static void test_snapshot_history_offset_navigation(void) @@ -477,6 +633,8 @@ int main(void) test_memory_view_filters_network_section(); test_network_view_starts_from_network_section(); test_io_view_starts_from_io_section(); + test_overview_view_combines_sections(); + test_capture_live_snapshot_reads_threads_for_overview(); test_format_current_time_layout(); test_live_header_and_footer_show_timestamps(); test_snapshot_history_offset_navigation();