diff options
Diffstat (limited to 'Applications/SystemMonitor')
18 files changed, 1113 insertions, 0 deletions
diff --git a/Applications/SystemMonitor/GraphWidget.cpp b/Applications/SystemMonitor/GraphWidget.cpp new file mode 100644 index 0000000000..3266e5526d --- /dev/null +++ b/Applications/SystemMonitor/GraphWidget.cpp @@ -0,0 +1,52 @@ +#include "GraphWidget.h" +#include <LibGUI/GPainter.h> + +GraphWidget::GraphWidget(GWidget* parent) + : GFrame(parent) +{ + set_frame_thickness(2); + set_frame_shape(FrameShape::Container); + set_frame_shadow(FrameShadow::Sunken); +} + +GraphWidget::~GraphWidget() +{ +} + +void GraphWidget::add_value(int value) +{ + m_values.enqueue(value); + update(); +} + +void GraphWidget::paint_event(GPaintEvent& event) +{ + GFrame::paint_event(event); + GPainter painter(*this); + painter.add_clip_rect(event.rect()); + painter.add_clip_rect(frame_inner_rect()); + painter.fill_rect(event.rect(), Color::Black); + + auto inner_rect = frame_inner_rect(); + float scale = (float)inner_rect.height() / (float)m_max; + + Point prev_point; + for (int i = 0; i < m_values.size(); ++i) { + int x = inner_rect.right() - (i * 2) + 1; + if (x < 0) + break; + float scaled_value = (float)m_values.at(m_values.size() - i - 1) * scale; + Point point = { x, inner_rect.bottom() - (int)scaled_value }; + if (i != 0) + painter.draw_line(prev_point, point, m_graph_color); + prev_point = point; + } + + if (!m_values.is_empty() && text_formatter) { + Rect text_rect = inner_rect.shrunken(8, 8); + text_rect.set_height(font().glyph_height()); + auto text = text_formatter(m_values.last(), m_max); + painter.draw_text(text_rect.translated(1, 1), text.characters(), TextAlignment::CenterRight, Color::Black); + painter.draw_text(text_rect, text.characters(), TextAlignment::CenterRight, m_text_color); + } +} diff --git a/Applications/SystemMonitor/GraphWidget.h b/Applications/SystemMonitor/GraphWidget.h new file mode 100644 index 0000000000..1d39beb1f1 --- /dev/null +++ b/Applications/SystemMonitor/GraphWidget.h @@ -0,0 +1,24 @@ +#include <AK/CircularQueue.h> +#include <LibGUI/GFrame.h> + +class GraphWidget final : public GFrame { +public: + explicit GraphWidget(GWidget* parent); + virtual ~GraphWidget() override; + + void set_max(int max) { m_max = max; } + void add_value(int); + + void set_graph_color(Color color) { m_graph_color = color; } + void set_text_color(Color color) { m_text_color = color; } + + Function<String(int value, int max)> text_formatter; + +private: + virtual void paint_event(GPaintEvent&) override; + + int m_max { 100 }; + CircularQueue<int, 4000> m_values; + Color m_graph_color; + Color m_text_color; +}; diff --git a/Applications/SystemMonitor/Makefile b/Applications/SystemMonitor/Makefile new file mode 100644 index 0000000000..9c760c378b --- /dev/null +++ b/Applications/SystemMonitor/Makefile @@ -0,0 +1,16 @@ +include ../../Makefile.common + +OBJS = \ + ProcessModel.o \ + ProcessTableView.o \ + MemoryStatsWidget.o \ + GraphWidget.o \ + ProcessStacksWidget.o \ + ProcessMemoryMapWidget.o \ + ProcessFileDescriptorMapWidget.o \ + NetworkStatisticsWidget.o \ + main.o + +APP = SystemMonitor + +include ../Makefile.common diff --git a/Applications/SystemMonitor/MemoryStatsWidget.cpp b/Applications/SystemMonitor/MemoryStatsWidget.cpp new file mode 100644 index 0000000000..6412dba161 --- /dev/null +++ b/Applications/SystemMonitor/MemoryStatsWidget.cpp @@ -0,0 +1,94 @@ +#include "MemoryStatsWidget.h" +#include "GraphWidget.h" +#include <AK/JsonObject.h> +#include <LibGUI/GBoxLayout.h> +#include <LibGUI/GLabel.h> +#include <LibGUI/GPainter.h> +#include <LibDraw/StylePainter.h> +#include <stdio.h> +#include <stdlib.h> + +MemoryStatsWidget::MemoryStatsWidget(GraphWidget& graph, GWidget* parent) + : GWidget(parent) + , m_graph(graph) + , m_proc_memstat("/proc/memstat") +{ + if (!m_proc_memstat.open(CIODevice::OpenMode::ReadOnly)) + ASSERT_NOT_REACHED(); + set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + set_preferred_size(0, 72); + + set_layout(make<GBoxLayout>(Orientation::Vertical)); + layout()->set_margins({ 0, 8, 0, 0 }); + layout()->set_spacing(3); + + auto build_widgets_for_label = [this](const String& description) -> GLabel* { + auto* container = new GWidget(this); + container->set_layout(make<GBoxLayout>(Orientation::Horizontal)); + container->set_size_policy(SizePolicy::Fixed, SizePolicy::Fixed); + container->set_preferred_size(275, 12); + auto* description_label = new GLabel(description, container); + description_label->set_font(Font::default_bold_font()); + description_label->set_text_alignment(TextAlignment::CenterLeft); + auto* label = new GLabel(container); + label->set_text_alignment(TextAlignment::CenterRight); + return label; + }; + + m_user_physical_pages_label = build_widgets_for_label("Userspace physical:"); + m_supervisor_physical_pages_label = build_widgets_for_label("Supervisor physical:"); + m_kmalloc_label = build_widgets_for_label("Kernel heap:"); + m_kmalloc_count_label = build_widgets_for_label("Calls kmalloc/kfree:"); + + refresh(); +} + +MemoryStatsWidget::~MemoryStatsWidget() +{ +} + +static inline size_t page_count_to_kb(size_t kb) +{ + return (kb * 4096) / 1024; +} + +static inline size_t bytes_to_kb(size_t bytes) +{ + return bytes / 1024; +} + +void MemoryStatsWidget::refresh() +{ + m_proc_memstat.seek(0); + + auto file_contents = m_proc_memstat.read_all(); + auto json = JsonValue::from_string(file_contents).as_object(); + + unsigned kmalloc_eternal_allocated = json.get("kmalloc_eternal_allocated").to_u32(); + (void)kmalloc_eternal_allocated; + unsigned kmalloc_allocated = json.get("kmalloc_allocated").to_u32(); + unsigned kmalloc_available = json.get("kmalloc_available").to_u32(); + unsigned user_physical_allocated = json.get("user_physical_allocated").to_u32(); + unsigned user_physical_available = json.get("user_physical_available").to_u32(); + unsigned super_physical_alloc = json.get("super_physical_allocated").to_u32(); + unsigned super_physical_free = json.get("super_physical_available").to_u32(); + unsigned kmalloc_call_count = json.get("kmalloc_call_count").to_u32(); + unsigned kfree_call_count = json.get("kfree_call_count").to_u32(); + + size_t kmalloc_sum_available = kmalloc_allocated + kmalloc_available; + size_t user_pages_available = user_physical_allocated + user_physical_available; + size_t supervisor_pages_available = super_physical_alloc + super_physical_free; + + m_kmalloc_label->set_text(String::format("%uK/%uK", bytes_to_kb(kmalloc_allocated), bytes_to_kb(kmalloc_sum_available))); + m_user_physical_pages_label->set_text(String::format("%uK/%uK", page_count_to_kb(user_physical_allocated), page_count_to_kb(user_pages_available))); + m_supervisor_physical_pages_label->set_text(String::format("%uK/%uK", page_count_to_kb(super_physical_alloc), page_count_to_kb(supervisor_pages_available))); + m_kmalloc_count_label->set_text(String::format("%u/%u (+%u)", kmalloc_call_count, kfree_call_count, kmalloc_call_count - kfree_call_count)); + + m_graph.set_max(page_count_to_kb(user_pages_available)); + m_graph.add_value(page_count_to_kb(user_physical_allocated)); +} + +void MemoryStatsWidget::timer_event(CTimerEvent&) +{ + refresh(); +} diff --git a/Applications/SystemMonitor/MemoryStatsWidget.h b/Applications/SystemMonitor/MemoryStatsWidget.h new file mode 100644 index 0000000000..e3912b9b94 --- /dev/null +++ b/Applications/SystemMonitor/MemoryStatsWidget.h @@ -0,0 +1,25 @@ +#pragma once + +#include <LibCore/CFile.h> +#include <LibGUI/GWidget.h> + +class GLabel; +class GraphWidget; + +class MemoryStatsWidget final : public GWidget { +public: + MemoryStatsWidget(GraphWidget& graph, GWidget* parent); + virtual ~MemoryStatsWidget() override; + + void refresh(); + +private: + virtual void timer_event(CTimerEvent&) override; + + GraphWidget& m_graph; + GLabel* m_user_physical_pages_label { nullptr }; + GLabel* m_supervisor_physical_pages_label { nullptr }; + GLabel* m_kmalloc_label { nullptr }; + GLabel* m_kmalloc_count_label { nullptr }; + CFile m_proc_memstat; +}; diff --git a/Applications/SystemMonitor/NetworkStatisticsWidget.cpp b/Applications/SystemMonitor/NetworkStatisticsWidget.cpp new file mode 100644 index 0000000000..4820755d5e --- /dev/null +++ b/Applications/SystemMonitor/NetworkStatisticsWidget.cpp @@ -0,0 +1,75 @@ +#include "NetworkStatisticsWidget.h" +#include <LibGUI/GBoxLayout.h> +#include <LibGUI/GGroupBox.h> +#include <LibGUI/GJsonArrayModel.h> +#include <LibGUI/GTableView.h> + +NetworkStatisticsWidget::NetworkStatisticsWidget(GWidget* parent) + : GWidget(parent) +{ + set_layout(make<GBoxLayout>(Orientation::Vertical)); + layout()->set_margins({ 4, 4, 4, 4 }); + set_fill_with_background_color(true); + set_background_color(Color::WarmGray); + + auto* adapters_group_box = new GGroupBox("Adapters", this); + adapters_group_box->set_layout(make<GBoxLayout>(Orientation::Vertical)); + adapters_group_box->layout()->set_margins({ 6, 16, 6, 6 }); + adapters_group_box->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + adapters_group_box->set_preferred_size(0, 120); + + m_adapter_table_view = new GTableView(adapters_group_box); + m_adapter_table_view->set_size_columns_to_fit_content(true); + + Vector<GJsonArrayModel::FieldSpec> net_adapters_fields; + net_adapters_fields.empend("name", "Name", TextAlignment::CenterLeft); + net_adapters_fields.empend("class_name", "Class", TextAlignment::CenterLeft); + net_adapters_fields.empend("mac_address", "MAC", TextAlignment::CenterLeft); + net_adapters_fields.empend("ipv4_address", "IPv4", TextAlignment::CenterLeft); + net_adapters_fields.empend("packets_in", "Pkt In", TextAlignment::CenterRight); + net_adapters_fields.empend("packets_out", "Pkt Out", TextAlignment::CenterRight); + net_adapters_fields.empend("bytes_in", "Bytes In", TextAlignment::CenterRight); + net_adapters_fields.empend("bytes_out", "Bytes Out", TextAlignment::CenterRight); + m_adapter_table_view->set_model(GJsonArrayModel::create("/proc/net/adapters", move(net_adapters_fields))); + + auto* sockets_group_box = new GGroupBox("Sockets", this); + sockets_group_box->set_layout(make<GBoxLayout>(Orientation::Vertical)); + sockets_group_box->layout()->set_margins({ 6, 16, 6, 6 }); + sockets_group_box->set_size_policy(SizePolicy::Fill, SizePolicy::Fill); + sockets_group_box->set_preferred_size(0, 0); + + m_socket_table_view = new GTableView(sockets_group_box); + m_socket_table_view->set_size_columns_to_fit_content(true); + + Vector<GJsonArrayModel::FieldSpec> net_tcp_fields; + net_tcp_fields.empend("peer_address", "Peer", TextAlignment::CenterLeft); + net_tcp_fields.empend("peer_port", "Port", TextAlignment::CenterRight); + net_tcp_fields.empend("local_address", "Local", TextAlignment::CenterLeft); + net_tcp_fields.empend("local_port", "Port", TextAlignment::CenterRight); + net_tcp_fields.empend("state", "State", TextAlignment::CenterLeft); + net_tcp_fields.empend("ack_number", "Ack#", TextAlignment::CenterRight); + net_tcp_fields.empend("sequence_number", "Seq#", TextAlignment::CenterRight); + net_tcp_fields.empend("packets_in", "Pkt In", TextAlignment::CenterRight); + net_tcp_fields.empend("packets_out", "Pkt Out", TextAlignment::CenterRight); + net_tcp_fields.empend("bytes_in", "Bytes In", TextAlignment::CenterRight); + net_tcp_fields.empend("bytes_out", "Bytes Out", TextAlignment::CenterRight); + m_socket_table_view->set_model(GJsonArrayModel::create("/proc/net/tcp", move(net_tcp_fields))); + + m_update_timer = new CTimer( + 1000, [this] { + update_models(); + }, + this); + + update_models(); +} + +NetworkStatisticsWidget::~NetworkStatisticsWidget() +{ +} + +void NetworkStatisticsWidget::update_models() +{ + m_adapter_table_view->model()->update(); + m_socket_table_view->model()->update(); +} diff --git a/Applications/SystemMonitor/NetworkStatisticsWidget.h b/Applications/SystemMonitor/NetworkStatisticsWidget.h new file mode 100644 index 0000000000..502474bee1 --- /dev/null +++ b/Applications/SystemMonitor/NetworkStatisticsWidget.h @@ -0,0 +1,20 @@ +#pragma once + +#include <LibCore/CTimer.h> +#include <LibGUI/GWidget.h> + +class GTableView; + +class NetworkStatisticsWidget final : public GWidget { + C_OBJECT(NetworkStatisticsWidget) +public: + explicit NetworkStatisticsWidget(GWidget* parent = nullptr); + virtual ~NetworkStatisticsWidget() override; + +private: + void update_models(); + + GTableView* m_adapter_table_view { nullptr }; + GTableView* m_socket_table_view { nullptr }; + CTimer* m_update_timer { nullptr }; +}; diff --git a/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp b/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp new file mode 100644 index 0000000000..bcc461ede8 --- /dev/null +++ b/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.cpp @@ -0,0 +1,36 @@ +#include "ProcessFileDescriptorMapWidget.h" +#include <LibGUI/GBoxLayout.h> +#include <LibGUI/GJsonArrayModel.h> +#include <LibGUI/GTableView.h> + +ProcessFileDescriptorMapWidget::ProcessFileDescriptorMapWidget(GWidget* parent) + : GWidget(parent) +{ + set_layout(make<GBoxLayout>(Orientation::Vertical)); + layout()->set_margins({ 4, 4, 4, 4 }); + m_table_view = new GTableView(this); + m_table_view->set_size_columns_to_fit_content(true); + + Vector<GJsonArrayModel::FieldSpec> pid_fds_fields; + pid_fds_fields.empend("fd", "FD", TextAlignment::CenterRight); + pid_fds_fields.empend("class", "Class", TextAlignment::CenterLeft); + pid_fds_fields.empend("offset", "Offset", TextAlignment::CenterRight); + pid_fds_fields.empend("absolute_path", "Path", TextAlignment::CenterLeft); + pid_fds_fields.empend("Access", TextAlignment::CenterLeft, [](auto& object) { + return object.get("seekable").to_bool() ? "Seekable" : "Sequential"; + }); + + m_table_view->set_model(GJsonArrayModel::create({}, move(pid_fds_fields))); +} + +ProcessFileDescriptorMapWidget::~ProcessFileDescriptorMapWidget() +{ +} + +void ProcessFileDescriptorMapWidget::set_pid(pid_t pid) +{ + if (m_pid == pid) + return; + m_pid = pid; + static_cast<GJsonArrayModel*>(m_table_view->model())->set_json_path(String::format("/proc/%d/fds", m_pid)); +} diff --git a/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h b/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h new file mode 100644 index 0000000000..a858281b6d --- /dev/null +++ b/Applications/SystemMonitor/ProcessFileDescriptorMapWidget.h @@ -0,0 +1,18 @@ +#pragma once + +#include <LibGUI/GWidget.h> + +class GTableView; + +class ProcessFileDescriptorMapWidget final : public GWidget { + C_OBJECT(ProcessFileDescriptorMapWidget); +public: + explicit ProcessFileDescriptorMapWidget(GWidget* parent); + virtual ~ProcessFileDescriptorMapWidget() override; + + void set_pid(pid_t); + +private: + GTableView* m_table_view { nullptr }; + pid_t m_pid { -1 }; +}; diff --git a/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp b/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp new file mode 100644 index 0000000000..938f8f5237 --- /dev/null +++ b/Applications/SystemMonitor/ProcessMemoryMapWidget.cpp @@ -0,0 +1,41 @@ +#include "ProcessMemoryMapWidget.h" +#include <LibGUI/GBoxLayout.h> +#include <LibGUI/GJsonArrayModel.h> +#include <LibGUI/GTableView.h> + +ProcessMemoryMapWidget::ProcessMemoryMapWidget(GWidget* parent) + : GWidget(parent) +{ + set_layout(make<GBoxLayout>(Orientation::Vertical)); + layout()->set_margins({ 4, 4, 4, 4 }); + m_table_view = new GTableView(this); + m_table_view->set_size_columns_to_fit_content(true); + Vector<GJsonArrayModel::FieldSpec> pid_vm_fields; + pid_vm_fields.empend("Address", TextAlignment::CenterLeft, [](auto& object) { + return String::format("%#x", object.get("address").to_u32()); + }); + pid_vm_fields.empend("size", "Size", TextAlignment::CenterRight); + pid_vm_fields.empend("amount_resident", "Resident", TextAlignment::CenterRight); + pid_vm_fields.empend("Access", TextAlignment::CenterLeft, [](auto& object) { + StringBuilder builder; + if (object.get("readable").to_bool()) + builder.append('R'); + if (object.get("writable").to_bool()) + builder.append('W'); + return builder.to_string(); + }); + pid_vm_fields.empend("name", "Name", TextAlignment::CenterLeft); + m_table_view->set_model(GJsonArrayModel::create({}, move(pid_vm_fields))); +} + +ProcessMemoryMapWidget::~ProcessMemoryMapWidget() +{ +} + +void ProcessMemoryMapWidget::set_pid(pid_t pid) +{ + if (m_pid == pid) + return; + m_pid = pid; + static_cast<GJsonArrayModel*>(m_table_view->model())->set_json_path(String::format("/proc/%d/vm", pid)); +} diff --git a/Applications/SystemMonitor/ProcessMemoryMapWidget.h b/Applications/SystemMonitor/ProcessMemoryMapWidget.h new file mode 100644 index 0000000000..fa1091286e --- /dev/null +++ b/Applications/SystemMonitor/ProcessMemoryMapWidget.h @@ -0,0 +1,18 @@ +#pragma once + +#include <LibGUI/GWidget.h> + +class GTableView; + +class ProcessMemoryMapWidget final : public GWidget { + C_OBJECT(ProcessMemoryMapWidget); +public: + explicit ProcessMemoryMapWidget(GWidget* parent); + virtual ~ProcessMemoryMapWidget() override; + + void set_pid(pid_t); + +private: + GTableView* m_table_view { nullptr }; + pid_t m_pid { -1 }; +}; diff --git a/Applications/SystemMonitor/ProcessModel.cpp b/Applications/SystemMonitor/ProcessModel.cpp new file mode 100644 index 0000000000..b2f78245c8 --- /dev/null +++ b/Applications/SystemMonitor/ProcessModel.cpp @@ -0,0 +1,242 @@ +#include "ProcessModel.h" +#include "GraphWidget.h" +#include <AK/JsonArray.h> +#include <AK/JsonObject.h> +#include <AK/JsonValue.h> +#include <LibC/SharedBuffer.h> +#include <LibCore/CProcessStatisticsReader.h> +#include <fcntl.h> +#include <stdio.h> + +ProcessModel::ProcessModel(GraphWidget& graph) + : m_graph(graph) +{ + m_generic_process_icon = GraphicsBitmap::load_from_file("/res/icons/gear16.png"); + m_high_priority_icon = GraphicsBitmap::load_from_file("/res/icons/highpriority16.png"); + m_low_priority_icon = GraphicsBitmap::load_from_file("/res/icons/lowpriority16.png"); + m_normal_priority_icon = GraphicsBitmap::load_from_file("/res/icons/normalpriority16.png"); +} + +ProcessModel::~ProcessModel() +{ +} + +int ProcessModel::row_count(const GModelIndex&) const +{ + return m_pids.size(); +} + +int ProcessModel::column_count(const GModelIndex&) const +{ + return Column::__Count; +} + +String ProcessModel::column_name(int column) const +{ + switch (column) { + case Column::Icon: + return ""; + case Column::PID: + return "PID"; + case Column::State: + return "State"; + case Column::User: + return "User"; + case Column::Priority: + return "Pr"; + case Column::Virtual: + return "Virtual"; + case Column::Physical: + return "Physical"; + case Column::CPU: + return "CPU"; + case Column::Name: + return "Name"; + case Column::Syscalls: + return "Syscalls"; + default: + ASSERT_NOT_REACHED(); + } +} + +GModel::ColumnMetadata ProcessModel::column_metadata(int column) const +{ + switch (column) { + case Column::Icon: + return { 16, TextAlignment::CenterLeft }; + case Column::PID: + return { 32, TextAlignment::CenterRight }; + case Column::State: + return { 75, TextAlignment::CenterLeft }; + case Column::Priority: + return { 16, TextAlignment::CenterLeft }; + case Column::User: + return { 50, TextAlignment::CenterLeft }; + case Column::Virtual: + return { 65, TextAlignment::CenterRight }; + case Column::Physical: + return { 65, TextAlignment::CenterRight }; + case Column::CPU: + return { 32, TextAlignment::CenterRight }; + case Column::Name: + return { 140, TextAlignment::CenterLeft }; + case Column::Syscalls: + return { 60, TextAlignment::CenterRight }; + default: + ASSERT_NOT_REACHED(); + } +} + +static String pretty_byte_size(size_t size) +{ + return String::format("%uK", size / 1024); +} + +GVariant ProcessModel::data(const GModelIndex& index, Role role) const +{ + ASSERT(is_valid(index)); + + auto it = m_processes.find(m_pids[index.row()]); + auto& process = *(*it).value; + + if (role == Role::Sort) { + switch (index.column()) { + case Column::Icon: + return 0; + case Column::PID: + return process.current_state.pid; + case Column::State: + return process.current_state.state; + case Column::User: + return process.current_state.user; + case Column::Priority: + if (process.current_state.priority == "Idle") + return 0; + if (process.current_state.priority == "Low") + return 1; + if (process.current_state.priority == "Normal") + return 2; + if (process.current_state.priority == "High") + return 3; + ASSERT_NOT_REACHED(); + return 3; + case Column::Virtual: + return (int)process.current_state.amount_virtual; + case Column::Physical: + return (int)process.current_state.amount_resident; + case Column::CPU: + return process.current_state.cpu_percent; + case Column::Name: + return process.current_state.name; + // FIXME: GVariant with unsigned? + case Column::Syscalls: + return (int)process.current_state.syscall_count; + } + ASSERT_NOT_REACHED(); + return {}; + } + + if (role == Role::Display) { + switch (index.column()) { + case Column::Icon: + if (process.current_state.icon_id != -1) { + auto icon_buffer = SharedBuffer::create_from_shared_buffer_id(process.current_state.icon_id); + if (icon_buffer) { + auto icon_bitmap = GraphicsBitmap::create_with_shared_buffer(GraphicsBitmap::Format::RGBA32, *icon_buffer, { 16, 16 }); + if (icon_bitmap) + return *icon_bitmap; + } + } + return *m_generic_process_icon; + case Column::PID: + return process.current_state.pid; + case Column::State: + return process.current_state.state; + case Column::User: + return process.current_state.user; + case Column::Priority: + if (process.current_state.priority == "Idle") + return String::empty(); + if (process.current_state.priority == "High") + return *m_high_priority_icon; + if (process.current_state.priority == "Low") + return *m_low_priority_icon; + if (process.current_state.priority == "Normal") + return *m_normal_priority_icon; + return process.current_state.priority; + case Column::Virtual: + return pretty_byte_size(process.current_state.amount_virtual); + case Column::Physical: + return pretty_byte_size(process.current_state.amount_resident); + case Column::CPU: + return process.current_state.cpu_percent; + case Column::Name: + return process.current_state.name; + // FIXME: It's weird that GVariant doesn't support unsigned ints. Should it? + case Column::Syscalls: + return (int)process.current_state.syscall_count; + } + } + + return {}; +} + +void ProcessModel::update() +{ + auto all_processes = CProcessStatisticsReader::get_all(); + + unsigned last_sum_times_scheduled = 0; + for (auto& it : m_processes) + last_sum_times_scheduled += it.value->current_state.times_scheduled; + + HashTable<pid_t> live_pids; + unsigned sum_times_scheduled = 0; + for (auto& it : all_processes) { + ProcessState state; + state.pid = it.value.pid; + state.times_scheduled = it.value.times_scheduled; + state.user = it.value.username; + state.priority = it.value.priority; + state.syscall_count = it.value.syscall_count; + state.state = it.value.state; + state.name = it.value.name; + state.amount_virtual = it.value.amount_virtual; + state.amount_resident = it.value.amount_resident; + state.icon_id = it.value.icon_id; + sum_times_scheduled += it.value.times_scheduled; + { + auto pit = m_processes.find(it.value.pid); + if (pit == m_processes.end()) + m_processes.set(it.value.pid, make<Process>()); + } + auto pit = m_processes.find(it.value.pid); + ASSERT(pit != m_processes.end()); + (*pit).value->previous_state = (*pit).value->current_state; + (*pit).value->current_state = state; + + live_pids.set(it.value.pid); + } + + m_pids.clear(); + float total_cpu_percent = 0; + Vector<pid_t, 16> pids_to_remove; + for (auto& it : m_processes) { + if (!live_pids.contains(it.key)) { + pids_to_remove.append(it.key); + continue; + } + auto& process = *it.value; + u32 times_scheduled_diff = process.current_state.times_scheduled - process.previous_state.times_scheduled; + process.current_state.cpu_percent = ((float)times_scheduled_diff * 100) / (float)(sum_times_scheduled - last_sum_times_scheduled); + if (it.key != 0) { + total_cpu_percent += process.current_state.cpu_percent; + m_pids.append(it.key); + } + } + for (auto pid : pids_to_remove) + m_processes.remove(pid); + + m_graph.add_value(total_cpu_percent); + + did_update(); +} diff --git a/Applications/SystemMonitor/ProcessModel.h b/Applications/SystemMonitor/ProcessModel.h new file mode 100644 index 0000000000..883da5d5cb --- /dev/null +++ b/Applications/SystemMonitor/ProcessModel.h @@ -0,0 +1,68 @@ +#pragma once + +#include <AK/AKString.h> +#include <AK/HashMap.h> +#include <AK/Vector.h> +#include <LibGUI/GModel.h> +#include <unistd.h> + +class GraphWidget; + +class ProcessModel final : public GModel { +public: + enum Column { + Icon = 0, + Name, + CPU, + State, + Priority, + User, + PID, + Virtual, + Physical, + Syscalls, + __Count + }; + + static NonnullRefPtr<ProcessModel> create(GraphWidget& graph) { return adopt(*new ProcessModel(graph)); } + virtual ~ProcessModel() override; + + virtual int row_count(const GModelIndex&) const override; + virtual int column_count(const GModelIndex&) const override; + virtual String column_name(int column) const override; + virtual ColumnMetadata column_metadata(int column) const override; + virtual GVariant data(const GModelIndex&, Role = Role::Display) const override; + virtual void update() override; + +private: + explicit ProcessModel(GraphWidget&); + + GraphWidget& m_graph; + + struct ProcessState { + pid_t pid; + unsigned times_scheduled; + String name; + String state; + String user; + String priority; + size_t amount_virtual; + size_t amount_resident; + unsigned syscall_count; + float cpu_percent; + int icon_id; + }; + + struct Process { + ProcessState current_state; + ProcessState previous_state; + }; + + HashMap<uid_t, String> m_usernames; + HashMap<pid_t, NonnullOwnPtr<Process>> m_processes; + Vector<pid_t> m_pids; + RefPtr<GraphicsBitmap> m_generic_process_icon; + RefPtr<GraphicsBitmap> m_high_priority_icon; + RefPtr<GraphicsBitmap> m_low_priority_icon; + RefPtr<GraphicsBitmap> m_normal_priority_icon; +}; diff --git a/Applications/SystemMonitor/ProcessStacksWidget.cpp b/Applications/SystemMonitor/ProcessStacksWidget.cpp new file mode 100644 index 0000000000..d7e4a822da --- /dev/null +++ b/Applications/SystemMonitor/ProcessStacksWidget.cpp @@ -0,0 +1,38 @@ +#include "ProcessStacksWidget.h" +#include <LibCore/CFile.h> +#include <LibCore/CTimer.h> +#include <LibGUI/GBoxLayout.h> + +ProcessStacksWidget::ProcessStacksWidget(GWidget* parent) + : GWidget(parent) +{ + set_layout(make<GBoxLayout>(Orientation::Vertical)); + layout()->set_margins({ 4, 4, 4, 4 }); + m_stacks_editor = new GTextEditor(GTextEditor::Type::MultiLine, this); + m_stacks_editor->set_readonly(true); + + m_timer = new CTimer(1000, [this] { refresh(); }, this); +} + +ProcessStacksWidget::~ProcessStacksWidget() +{ +} + +void ProcessStacksWidget::set_pid(pid_t pid) +{ + if (m_pid == pid) + return; + m_pid = pid; + refresh(); +} + +void ProcessStacksWidget::refresh() +{ + CFile file(String::format("/proc/%d/stack", m_pid)); + if (!file.open(CIODevice::ReadOnly)) { + m_stacks_editor->set_text(String::format("Unable to open %s", file.filename().characters())); + return; + } + + m_stacks_editor->set_text(file.read_all()); +} diff --git a/Applications/SystemMonitor/ProcessStacksWidget.h b/Applications/SystemMonitor/ProcessStacksWidget.h new file mode 100644 index 0000000000..9b0c6ac8f0 --- /dev/null +++ b/Applications/SystemMonitor/ProcessStacksWidget.h @@ -0,0 +1,21 @@ +#pragma once + +#include <LibGUI/GTextEditor.h> +#include <LibGUI/GWidget.h> + +class CTimer; + +class ProcessStacksWidget final : public GWidget { + C_OBJECT(ProcessStacksWidget) +public: + explicit ProcessStacksWidget(GWidget* parent); + virtual ~ProcessStacksWidget() override; + + void set_pid(pid_t); + void refresh(); + +private: + pid_t m_pid { -1 }; + GTextEditor* m_stacks_editor { nullptr }; + CTimer* m_timer { nullptr }; +}; diff --git a/Applications/SystemMonitor/ProcessTableView.cpp b/Applications/SystemMonitor/ProcessTableView.cpp new file mode 100644 index 0000000000..2871018182 --- /dev/null +++ b/Applications/SystemMonitor/ProcessTableView.cpp @@ -0,0 +1,42 @@ +#include "ProcessTableView.h" +#include "ProcessModel.h" +#include <LibGUI/GSortingProxyModel.h> +#include <stdio.h> + +ProcessTableView::ProcessTableView(GraphWidget& graph, GWidget* parent) + : GTableView(parent) +{ + set_size_columns_to_fit_content(true); + set_model(GSortingProxyModel::create(ProcessModel::create(graph))); + model()->set_key_column_and_sort_order(ProcessModel::Column::CPU, GSortOrder::Descending); + refresh(); + + on_selection = [this](auto&) { + if (on_process_selected) + on_process_selected(selected_pid()); + }; +} + +ProcessTableView::~ProcessTableView() +{ +} + +void ProcessTableView::refresh() +{ + model()->update(); +} + +void ProcessTableView::model_notification(const GModelNotification& notification) +{ + if (notification.type() == GModelNotification::ModelUpdated) { + // Do something? + return; + } +} + +pid_t ProcessTableView::selected_pid() const +{ + if (!model()->selected_index().is_valid()) + return -1; + return model()->data(model()->index(model()->selected_index().row(), ProcessModel::Column::PID), GModel::Role::Sort).as_int(); +} diff --git a/Applications/SystemMonitor/ProcessTableView.h b/Applications/SystemMonitor/ProcessTableView.h new file mode 100644 index 0000000000..98d3f3edb1 --- /dev/null +++ b/Applications/SystemMonitor/ProcessTableView.h @@ -0,0 +1,23 @@ +#pragma once + +#include <AK/Function.h> +#include <LibGUI/GTableView.h> +#include <unistd.h> + +class GraphWidget; +class ProcessModel; + +class ProcessTableView final : public GTableView { +public: + ProcessTableView(GraphWidget&, GWidget* parent); + virtual ~ProcessTableView() override; + + pid_t selected_pid() const; + + void refresh(); + + Function<void(pid_t)> on_process_selected; + +private: + virtual void model_notification(const GModelNotification&) override; +}; diff --git a/Applications/SystemMonitor/main.cpp b/Applications/SystemMonitor/main.cpp new file mode 100644 index 0000000000..de3de2f8db --- /dev/null +++ b/Applications/SystemMonitor/main.cpp @@ -0,0 +1,260 @@ +#include "GraphWidget.h" +#include "MemoryStatsWidget.h" +#include "NetworkStatisticsWidget.h" +#include "ProcessFileDescriptorMapWidget.h" +#include "ProcessMemoryMapWidget.h" +#include "ProcessStacksWidget.h" +#include "ProcessTableView.h" +#include <LibCore/CTimer.h> +#include <LibDraw/PNGLoader.h> +#include <LibGUI/GAction.h> +#include <LibGUI/GApplication.h> +#include <LibGUI/GBoxLayout.h> +#include <LibGUI/GGroupBox.h> +#include <LibGUI/GJsonArrayModel.h> +#include <LibGUI/GLabel.h> +#include <LibGUI/GMenuBar.h> +#include <LibGUI/GSortingProxyModel.h> +#include <LibGUI/GSplitter.h> +#include <LibGUI/GTabWidget.h> +#include <LibGUI/GToolBar.h> +#include <LibGUI/GWidget.h> +#include <LibGUI/GWindow.h> +#include <signal.h> +#include <stdio.h> +#include <unistd.h> + +static String human_readable_size(u32 size) +{ + if (size < (64 * KB)) + return String::format("%u", size); + if (size < MB) + return String::format("%u KB", size / KB); + if (size < GB) + return String::format("%u MB", size / MB); + return String::format("%u GB", size / GB); +} + +static GWidget* build_file_systems_tab(); + +int main(int argc, char** argv) +{ + GApplication app(argc, argv); + + auto* keeper = new GWidget; + keeper->set_layout(make<GBoxLayout>(Orientation::Vertical)); + keeper->set_fill_with_background_color(true); + keeper->set_background_color(Color::WarmGray); + keeper->layout()->set_margins({ 4, 4, 4, 4 }); + + auto* tabwidget = new GTabWidget(keeper); + + auto* process_container_splitter = new GSplitter(Orientation::Vertical, nullptr); + tabwidget->add_widget("Processes", process_container_splitter); + + auto* process_table_container = new GWidget(process_container_splitter); + + auto* graphs_container = new GWidget; + graphs_container->set_fill_with_background_color(true); + graphs_container->set_background_color(Color::WarmGray); + graphs_container->set_layout(make<GBoxLayout>(Orientation::Vertical)); + graphs_container->layout()->set_margins({ 4, 4, 4, 4 }); + + auto* cpu_graph_group_box = new GGroupBox("CPU usage", graphs_container); + cpu_graph_group_box->set_layout(make<GBoxLayout>(Orientation::Vertical)); + cpu_graph_group_box->layout()->set_margins({ 6, 16, 6, 6 }); + cpu_graph_group_box->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + cpu_graph_group_box->set_preferred_size(0, 120); + auto* cpu_graph = new GraphWidget(cpu_graph_group_box); + cpu_graph->set_max(100); + cpu_graph->set_text_color(Color::Green); + cpu_graph->set_graph_color(Color::from_rgb(0x00bb00)); + cpu_graph->text_formatter = [](int value, int) { + return String::format("%d%%", value); + }; + + auto* memory_graph_group_box = new GGroupBox("Memory usage", graphs_container); + memory_graph_group_box->set_layout(make<GBoxLayout>(Orientation::Vertical)); + memory_graph_group_box->layout()->set_margins({ 6, 16, 6, 6 }); + memory_graph_group_box->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + memory_graph_group_box->set_preferred_size(0, 120); + auto* memory_graph = new GraphWidget(memory_graph_group_box); + memory_graph->set_text_color(Color::Cyan); + memory_graph->set_graph_color(Color::from_rgb(0x00bbbb)); + memory_graph->text_formatter = [](int value, int max) { + return String::format("%d / %d KB", value, max); + }; + + tabwidget->add_widget("Graphs", graphs_container); + + tabwidget->add_widget("File systems", build_file_systems_tab()); + + auto* network_stats_widget = new NetworkStatisticsWidget(nullptr); + tabwidget->add_widget("Network", network_stats_widget); + + process_table_container->set_layout(make<GBoxLayout>(Orientation::Vertical)); + process_table_container->layout()->set_margins({ 4, 0, 4, 4 }); + process_table_container->layout()->set_spacing(0); + + auto* toolbar = new GToolBar(process_table_container); + toolbar->set_has_frame(false); + auto* process_table_view = new ProcessTableView(*cpu_graph, process_table_container); + auto* memory_stats_widget = new MemoryStatsWidget(*memory_graph, graphs_container); + + auto* refresh_timer = new CTimer(1000, [&] { + process_table_view->refresh(); + memory_stats_widget->refresh(); + }); + + auto kill_action = GAction::create("Kill process", GraphicsBitmap::load_from_file("/res/icons/kill16.png"), [process_table_view](const GAction&) { + pid_t pid = process_table_view->selected_pid(); + if (pid != -1) + kill(pid, SIGKILL); + }); + + auto stop_action = GAction::create("Stop process", GraphicsBitmap::load_from_file("/res/icons/stop16.png"), [process_table_view](const GAction&) { + pid_t pid = process_table_view->selected_pid(); + if (pid != -1) + kill(pid, SIGSTOP); + }); + + auto continue_action = GAction::create("Continue process", GraphicsBitmap::load_from_file("/res/icons/continue16.png"), [process_table_view](const GAction&) { + pid_t pid = process_table_view->selected_pid(); + if (pid != -1) + kill(pid, SIGCONT); + }); + + toolbar->add_action(kill_action); + toolbar->add_action(stop_action); + toolbar->add_action(continue_action); + + auto menubar = make<GMenuBar>(); + auto app_menu = make<GMenu>("System Monitor"); + app_menu->add_action(GAction::create("Quit", { Mod_Alt, Key_F4 }, [](const GAction&) { + GApplication::the().quit(0); + return; + })); + menubar->add_menu(move(app_menu)); + + auto process_menu = make<GMenu>("Process"); + process_menu->add_action(kill_action); + process_menu->add_action(stop_action); + process_menu->add_action(continue_action); + menubar->add_menu(move(process_menu)); + + auto process_context_menu = make<GMenu>("Process context menu"); + process_context_menu->add_action(kill_action); + process_context_menu->add_action(stop_action); + process_context_menu->add_action(continue_action); + process_table_view->on_context_menu_request = [&](const GModelIndex& index, const GContextMenuEvent& event) { + (void)index; + process_context_menu->popup(event.screen_position()); + }; + + auto frequency_menu = make<GMenu>("Frequency"); + frequency_menu->add_action(GAction::create("0.25 sec", [refresh_timer](auto&) { + refresh_timer->restart(250); + })); + frequency_menu->add_action(GAction::create("0.5 sec", [refresh_timer](auto&) { + refresh_timer->restart(500); + })); + frequency_menu->add_action(GAction::create("1 sec", [refresh_timer](auto&) { + refresh_timer->restart(1000); + })); + frequency_menu->add_action(GAction::create("3 sec", [refresh_timer](auto&) { + refresh_timer->restart(3000); + })); + frequency_menu->add_action(GAction::create("5 sec", [refresh_timer](auto&) { + refresh_timer->restart(5000); + })); + menubar->add_menu(move(frequency_menu)); + + auto help_menu = make<GMenu>("Help"); + help_menu->add_action(GAction::create("About", [](const GAction&) { + dbgprintf("FIXME: Implement Help/About\n"); + })); + menubar->add_menu(move(help_menu)); + + app.set_menubar(move(menubar)); + + auto* process_tab_widget = new GTabWidget(process_container_splitter); + + auto* open_files_widget = new ProcessFileDescriptorMapWidget(nullptr); + process_tab_widget->add_widget("Open files", open_files_widget); + + auto* memory_map_widget = new ProcessMemoryMapWidget(nullptr); + process_tab_widget->add_widget("Memory map", memory_map_widget); + + auto* stacks_widget = new ProcessStacksWidget(nullptr); + process_tab_widget->add_widget("Stacks", stacks_widget); + + process_table_view->on_process_selected = [&](pid_t pid) { + open_files_widget->set_pid(pid); + stacks_widget->set_pid(pid); + memory_map_widget->set_pid(pid); + }; + + auto* window = new GWindow; + window->set_title("System Monitor"); + window->set_rect(20, 200, 680, 400); + window->set_main_widget(keeper); + + window->show(); + + window->set_icon(load_png("/res/icons/16x16/app-system-monitor.png")); + + return app.exec(); +} + +GWidget* build_file_systems_tab() +{ + auto* fs_widget = new GWidget(nullptr); + fs_widget->set_layout(make<GBoxLayout>(Orientation::Vertical)); + fs_widget->layout()->set_margins({ 4, 4, 4, 4 }); + auto* fs_table_view = new GTableView(fs_widget); + fs_table_view->set_size_columns_to_fit_content(true); + + Vector<GJsonArrayModel::FieldSpec> df_fields; + df_fields.empend("mount_point", "Mount point", TextAlignment::CenterLeft); + df_fields.empend("class_name", "Class", TextAlignment::CenterLeft); + df_fields.empend( + "Size", TextAlignment::CenterRight, + [](const JsonObject& object) { + return human_readable_size(object.get("total_block_count").to_u32() * object.get("block_size").to_u32()); + }, + [](const JsonObject& object) { + return object.get("total_block_count").to_u32() * object.get("block_size").to_u32(); + }); + df_fields.empend( + "Used", TextAlignment::CenterRight, + [](const JsonObject& object) { + auto total_blocks = object.get("total_block_count").to_u32(); + auto free_blocks = object.get("free_block_count").to_u32(); + auto used_blocks = total_blocks - free_blocks; + return human_readable_size(used_blocks * object.get("block_size").to_u32()); }, + [](const JsonObject& object) { + auto total_blocks = object.get("total_block_count").to_u32(); + auto free_blocks = object.get("free_block_count").to_u32(); + auto used_blocks = total_blocks - free_blocks; + return used_blocks * object.get("block_size").to_u32(); + }); + df_fields.empend( + "Available", TextAlignment::CenterRight, + [](const JsonObject& object) { + return human_readable_size(object.get("free_block_count").to_u32() * object.get("block_size").to_u32()); + }, + [](const JsonObject& object) { + return object.get("free_block_count").to_u32() * object.get("block_size").to_u32(); + }); + df_fields.empend("Access", TextAlignment::CenterLeft, [](const JsonObject& object) { + return object.get("readonly").to_bool() ? "Read-only" : "Read/Write"; + }); + df_fields.empend("free_block_count", "Free blocks", TextAlignment::CenterRight); + df_fields.empend("total_block_count", "Total blocks", TextAlignment::CenterRight); + df_fields.empend("free_inode_count", "Free inodes", TextAlignment::CenterRight); + df_fields.empend("total_inode_count", "Total inodes", TextAlignment::CenterRight); + df_fields.empend("block_size", "Block size", TextAlignment::CenterRight); + fs_table_view->set_model(GSortingProxyModel::create(GJsonArrayModel::create("/proc/df", move(df_fields)))); + fs_table_view->model()->update(); + return fs_widget; +} |