ladybird/Userland/Applets/ResourceGraph/main.cpp
Tom 7e77a2ec40 Everywhere: Improve CPU usage calculation
As threads come and go, we can't simply account for how many time
slices the threads at any given point may have been using. We need to
also account for threads that have since disappeared. This means we
also need to track how many time slices we have expired globally.

However, because this doesn't account for context switches outside of
the system timer tick values may still be under-reported. To solve this
we will need to track more accurate time information on each context
switch.

This also fixes top's cpu usage calculation which was still based on
the number of context switches.

Fixes #6473
2021-07-18 22:08:26 +02:00

272 lines
8.9 KiB
C++

/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2020, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteBuffer.h>
#include <AK/CircularQueue.h>
#include <AK/JsonObject.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibCore/ProcessStatisticsReader.h>
#include <LibGUI/Application.h>
#include <LibGUI/Frame.h>
#include <LibGUI/Painter.h>
#include <LibGUI/Window.h>
#include <LibGfx/Palette.h>
#include <serenity.h>
#include <spawn.h>
#include <stdio.h>
enum class GraphType {
CPU,
Memory,
};
class GraphWidget final : public GUI::Frame {
C_OBJECT(GraphWidget);
public:
static constexpr size_t history_size = 24;
GraphWidget(GraphType graph_type, Optional<Gfx::Color> graph_color, Optional<Gfx::Color> graph_error_color)
: m_graph_type(graph_type)
{
set_frame_thickness(1);
m_graph_color = graph_color.value_or(palette().menu_selection());
m_graph_error_color = graph_error_color.value_or(Color::Red);
start_timer(1000);
}
private:
virtual void timer_event(Core::TimerEvent&) override
{
switch (m_graph_type) {
case GraphType::CPU: {
u64 busy, idle, scheduled_diff;
if (get_cpu_usage(busy, idle, scheduled_diff)) {
auto busy_diff = busy - m_last_cpu_busy;
m_last_cpu_busy = busy;
m_last_cpu_idle = idle;
float cpu = scheduled_diff > 0 ? (float)busy_diff / (float)scheduled_diff : 0;
m_history.enqueue(cpu);
m_tooltip = String::formatted("CPU usage: {:.1}%", 100 * cpu);
} else {
m_history.enqueue(-1);
m_tooltip = StringView("Unable to determine CPU usage");
}
break;
}
case GraphType::Memory: {
u64 allocated, available;
if (get_memory_usage(allocated, available)) {
double total_memory = allocated + available;
double memory = (double)allocated / total_memory;
m_history.enqueue(memory);
m_tooltip = String::formatted("Memory: {} MiB of {:.1} MiB in use", allocated / MiB, total_memory / MiB);
} else {
m_history.enqueue(-1);
m_tooltip = StringView("Unable to determine memory usage");
}
break;
}
default:
VERIFY_NOT_REACHED();
}
set_tooltip(m_tooltip);
update();
}
virtual void paint_event(GUI::PaintEvent& event) override
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.add_clip_rect(frame_inner_rect());
painter.fill_rect(event.rect(), Color::Black);
int i = m_history.capacity() - m_history.size();
auto rect = frame_inner_rect();
for (auto value : m_history) {
if (value >= 0) {
painter.draw_line(
{ rect.x() + i, rect.bottom() },
{ rect.x() + i, rect.top() + (int)(roundf(rect.height() - (value * rect.height()))) },
m_graph_color);
} else {
painter.draw_line(
{ rect.x() + i, rect.top() },
{ rect.x() + i, rect.bottom() },
m_graph_error_color);
}
++i;
}
}
virtual void mousedown_event(GUI::MouseEvent& event) override
{
if (event.button() != GUI::MouseButton::Left)
return;
pid_t child_pid;
const char* argv[] = { "SystemMonitor", "-t", "graphs", nullptr };
if ((errno = posix_spawn(&child_pid, "/bin/SystemMonitor", nullptr, nullptr, const_cast<char**>(argv), environ))) {
perror("posix_spawn");
} else {
if (disown(child_pid) < 0)
perror("disown");
}
}
bool get_cpu_usage(u64& busy, u64& idle, u64& scheduled_diff)
{
busy = 0;
idle = 0;
scheduled_diff = 0;
auto all_processes = Core::ProcessStatisticsReader::get_all(m_proc_all);
if (!all_processes.has_value() || all_processes.value().processes.is_empty())
return false;
if (m_last_total_sum.has_value())
scheduled_diff = all_processes->total_ticks_scheduled - m_last_total_sum.value();
m_last_total_sum = all_processes->total_ticks_scheduled;
for (auto& it : all_processes.value().processes) {
for (auto& jt : it.threads) {
if (it.pid == 0)
idle += jt.ticks_user + jt.ticks_kernel;
else
busy += jt.ticks_user + jt.ticks_kernel;
}
}
return true;
}
bool get_memory_usage(u64& allocated, u64& available)
{
if (m_proc_mem) {
// Seeking to the beginning causes a data refresh!
if (!m_proc_mem->seek(0, Core::SeekMode::SetPosition))
return false;
} else {
auto proc_memstat = Core::File::construct("/proc/memstat");
if (!proc_memstat->open(Core::OpenMode::ReadOnly))
return false;
m_proc_mem = move(proc_memstat);
}
auto file_contents = m_proc_mem->read_all();
auto json = JsonValue::from_string(file_contents);
VERIFY(json.has_value());
auto& obj = json.value().as_object();
unsigned kmalloc_allocated = obj.get("kmalloc_allocated").to_u32();
unsigned kmalloc_available = obj.get("kmalloc_available").to_u32();
auto user_physical_allocated = obj.get("user_physical_allocated").to_u64();
auto user_physical_committed = obj.get("user_physical_committed").to_u64();
auto user_physical_uncommitted = obj.get("user_physical_uncommitted").to_u64();
unsigned kmalloc_bytes_total = kmalloc_allocated + kmalloc_available;
unsigned kmalloc_pages_total = (kmalloc_bytes_total + PAGE_SIZE - 1) / PAGE_SIZE;
u64 total_userphysical_and_swappable_pages = kmalloc_pages_total + user_physical_allocated + user_physical_committed + user_physical_uncommitted;
allocated = kmalloc_allocated + ((user_physical_allocated + user_physical_committed) * PAGE_SIZE);
available = (total_userphysical_and_swappable_pages * PAGE_SIZE) - allocated;
return true;
}
GraphType m_graph_type;
Gfx::Color m_graph_color;
Gfx::Color m_graph_error_color;
CircularQueue<float, history_size> m_history;
u64 m_last_cpu_busy { 0 };
u64 m_last_cpu_idle { 0 };
Optional<u64> m_last_total_sum;
String m_tooltip;
RefPtr<Core::File> m_proc_all;
RefPtr<Core::File> m_proc_mem;
};
int main(int argc, char** argv)
{
if (pledge("stdio recvfd sendfd proc exec rpath unix", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio recvfd sendfd proc exec rpath", nullptr) < 0) {
perror("pledge");
return 1;
}
const char* cpu = nullptr;
const char* memory = nullptr;
Core::ArgsParser args_parser;
args_parser.add_option(cpu, "Create CPU graph", "cpu", 'C', "cpu");
args_parser.add_option(memory, "Create memory graph", "memory", 'M', "memory");
args_parser.parse(argc, argv);
if (!cpu && !memory) {
printf("At least one of --cpu or --memory must be used");
return 1;
}
NonnullRefPtrVector<GUI::Window> applet_windows;
auto create_applet = [&](GraphType graph_type, StringView spec) {
auto parts = spec.split_view(',');
dbgln("Create applet: {} with spec '{}'", (int)graph_type, spec);
if (parts.size() != 2)
return;
auto name = parts[0];
auto graph_color = Gfx::Color::from_string(parts[1]);
auto window = GUI::Window::construct();
window->set_title(name);
window->set_window_type(GUI::WindowType::Applet);
window->resize(GraphWidget::history_size + 2, 15);
window->set_main_widget<GraphWidget>(graph_type, graph_color, Optional<Gfx::Color> {});
window->show();
applet_windows.append(move(window));
};
if (cpu)
create_applet(GraphType::CPU, cpu);
if (memory)
create_applet(GraphType::Memory, memory);
if (unveil("/res", "r") < 0) {
perror("unveil");
return 1;
}
// FIXME: This is required by Core::ProcessStatisticsReader.
// It would be good if we didn't depend on that.
if (unveil("/etc/passwd", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/proc/all", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/proc/memstat", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/bin/SystemMonitor", "x") < 0) {
perror("unveil");
return 1;
}
unveil(nullptr, nullptr);
return app->exec();
}