diff --git a/Userland/DevTools/Profiler/CMakeLists.txt b/Userland/DevTools/Profiler/CMakeLists.txt index e3eacb425b..2380373878 100644 --- a/Userland/DevTools/Profiler/CMakeLists.txt +++ b/Userland/DevTools/Profiler/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES main.cpp IndividualSampleModel.cpp FlameGraphView.cpp + FilesystemEventModel.cpp Gradient.cpp Process.cpp Profile.cpp diff --git a/Userland/DevTools/Profiler/FilesystemEventModel.cpp b/Userland/DevTools/Profiler/FilesystemEventModel.cpp new file mode 100644 index 0000000000..96b9835cf1 --- /dev/null +++ b/Userland/DevTools/Profiler/FilesystemEventModel.cpp @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2022, Jakub Berkop + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "FilesystemEventModel.h" +#include "Profile.h" +#include +#include +#include +#include + +namespace Profiler { + +FileEventModel::FileEventModel(Profile& profile) + : m_profile(profile) +{ +} + +FileEventModel::~FileEventModel() +{ +} + +FileEventNode& FileEventNode::find_or_create_node(String const& searched_path) +{ + // TODO: Optimize this function. + + if (searched_path == ""sv) { + return *this; + } + + auto lex_path = LexicalPath(searched_path); + auto parts = lex_path.parts(); + auto current = parts.take_first(); + + StringBuilder sb; + sb.join("/", parts); + auto new_s = sb.to_string(); + + for (auto& child : m_children) { + if (child->m_path == current) { + return child->find_or_create_node(new_s); + } + } + + if (m_parent) { + for (auto& child : m_children) { + if (child->m_path == current) { + return child->find_or_create_node(new_s); + } + } + return create_recursively(searched_path); + } else { + if (!searched_path.starts_with("/"sv)) { + m_children.append(create(searched_path, this)); + return *m_children.last(); + } + + return create_recursively(searched_path); + } +} + +FileEventNode& FileEventNode::create_recursively(String new_path) +{ + auto const lex_path = LexicalPath(new_path); + auto parts = lex_path.parts(); + + if (parts.size() == 1) { + auto new_node = FileEventNode::create(new_path, this); + m_children.append(new_node); + return *new_node.ptr(); + } else { + auto new_node = FileEventNode::create(parts.take_first(), this); + m_children.append(new_node); + + StringBuilder sb; + sb.join("/", parts); + + return new_node->create_recursively(sb.to_string()); + } +} + +void FileEventNode::for_each_parent_node(Function callback) +{ + auto* current = this; + while (current) { + callback(*current); + current = current->m_parent; + } +} + +GUI::ModelIndex FileEventModel::index(int row, int column, GUI::ModelIndex const& parent) const +{ + if (!parent.is_valid()) { + return create_index(row, column, m_profile.file_event_nodes()->children()[row].ptr()); + } + auto& remote_parent = *static_cast(parent.internal_data()); + return create_index(row, column, remote_parent.children().at(row).ptr()); +} + +GUI::ModelIndex FileEventModel::parent_index(GUI::ModelIndex const& index) const +{ + if (!index.is_valid()) + return {}; + auto& node = *static_cast(index.internal_data()); + if (!node.parent()) + return {}; + + if (node.parent()->parent()) { + const auto& children = node.parent()->parent()->children(); + + for (size_t row = 0; row < children.size(); ++row) { + if (children.at(row).ptr() == node.parent()) { + return create_index(row, index.column(), node.parent()); + } + } + } + + const auto& children = node.parent()->children(); + + for (size_t row = 0; row < children.size(); ++row) { + if (children.at(row).ptr() == &node) { + return create_index(row, index.column(), node.parent()); + } + } + + VERIFY_NOT_REACHED(); + return {}; +} + +int FileEventModel::row_count(GUI::ModelIndex const& index) const +{ + if (!index.is_valid()) + return m_profile.file_event_nodes()->children().size(); + auto& node = *static_cast(index.internal_data()); + return node.children().size(); +} + +int FileEventModel::column_count(GUI::ModelIndex const&) const +{ + return Column::__Count; +} + +String FileEventModel::column_name(int column) const +{ + switch (column) { + case Column::Path: + return "Path"; + case Column::Count: + return "Event Count"; + case Column::Duration: + return "Duration [ms]"; + default: + VERIFY_NOT_REACHED(); + return {}; + } +} + +GUI::Variant FileEventModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const +{ + if (role == GUI::ModelRole::TextAlignment) { + if (index.column() == Path) + return Gfx::TextAlignment::CenterLeft; + return Gfx::TextAlignment::CenterRight; + } + + auto* node = static_cast(index.internal_data()); + + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::Count) { + return node->count(); + } + + if (index.column() == Column::Path) { + return node->path(); + } + + if (index.column() == Column::Duration) { + return node->duration(); + } + + return {}; + } + + return {}; +} + +} diff --git a/Userland/DevTools/Profiler/FilesystemEventModel.h b/Userland/DevTools/Profiler/FilesystemEventModel.h new file mode 100644 index 0000000000..afea32653a --- /dev/null +++ b/Userland/DevTools/Profiler/FilesystemEventModel.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022, Jakub Berkop + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Profiler { + +class Profile; + +class FileEventNode : public RefCounted { +public: + static NonnullRefPtr create(String const& path, FileEventNode* parent = nullptr) + { + return adopt_ref(*new FileEventNode(path, parent)); + } + + FileEventNode& find_or_create_node(String const&); + + Vector>& children() { return m_children; } + Vector> const& children() const { return m_children; } + + FileEventNode* parent() { return m_parent; }; + + FileEventNode& create_recursively(String); + + void for_each_parent_node(Function callback); + + String const& path() const { return m_path; } + + void increment_count() { m_count++; } + u64 count() const { return m_count; } + + void add_to_duration(u64 duration) { duration += duration; } + u64 duration() const { return m_duration; } + +private: + FileEventNode(String const& path, FileEventNode* parent = nullptr) + : m_path(path) + , m_count(0) + , m_duration(0) + , m_parent(parent) {}; + + String m_path; + u64 m_count; + u64 m_duration; + + Vector> m_children; + FileEventNode* m_parent = nullptr; +}; + +class FileEventModel final : public GUI::Model { +public: + static NonnullRefPtr create(Profile& profile) + { + return adopt_ref(*new FileEventModel(profile)); + } + + enum Column { + Path, + Count, + Duration, + __Count + }; + + virtual ~FileEventModel() override; + + virtual int row_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override; + virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override; + virtual String column_name(int) const override; + virtual GUI::Variant data(GUI::ModelIndex const&, GUI::ModelRole) const override; + virtual GUI::ModelIndex index(int row, int column, GUI::ModelIndex const& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(GUI::ModelIndex const&) const override; + virtual int tree_column() const override { return Column::Path; } + virtual bool is_column_sortable(int) const override { return false; } + virtual bool is_searchable() const override { return true; } + +private: + explicit FileEventModel(Profile&); + + Profile& m_profile; + + GUI::Icon m_user_frame_icon; + GUI::Icon m_kernel_frame_icon; +}; + +} diff --git a/Userland/DevTools/Profiler/Profile.cpp b/Userland/DevTools/Profiler/Profile.cpp index 1502e550fb..c94426fc0d 100644 --- a/Userland/DevTools/Profiler/Profile.cpp +++ b/Userland/DevTools/Profiler/Profile.cpp @@ -36,6 +36,7 @@ static void sort_profile_nodes(Vector>& nodes) Profile::Profile(Vector processes, Vector events) : m_processes(move(processes)) , m_events(move(events)) + , m_file_event_nodes(FileEventNode::create("")) { for (size_t i = 0; i < m_events.size(); ++i) { if (m_events[i].data.has()) @@ -48,6 +49,7 @@ Profile::Profile(Vector processes, Vector events) m_model = ProfileModel::create(*this); m_samples_model = SamplesModel::create(*this); m_signposts_model = SignpostsModel::create(*this); + m_file_event_model = FileEventModel::create(*this); rebuild_tree(); } @@ -101,6 +103,7 @@ void Profile::rebuild_tree() m_filtered_event_indices.clear(); m_filtered_signpost_indices.clear(); + m_file_event_nodes->children().clear(); for (size_t event_index = 0; event_index < m_events.size(); ++event_index) { auto& event = m_events.at(event_index); @@ -205,6 +208,21 @@ void Profile::rebuild_tree() } } } + + if (event.data.has()) { + auto const& read_event = event.data.get(); + auto& event_node = m_file_event_nodes->find_or_create_node(read_event.path); + + event_node.for_each_parent_node([&](FileEventNode& node) { + node.increment_count(); + + // Fixme: Currently events record 'timestamp' and 'start_timestamp' in ms resolution, + // which results in most durations equal to zero. Increasing the resolution should + // make the information more accurate. + auto const duration = event.timestamp - read_event.start_timestamp; + node.add_to_duration(duration); + }); + } } sort_profile_nodes(roots); @@ -581,6 +599,11 @@ GUI::Model* Profile::source_model() return m_source_model; } +GUI::Model* Profile::file_event_model() +{ + return m_file_event_model; +} + ProfileNode::ProfileNode(Process const& process) : m_root(true) , m_process(process) diff --git a/Userland/DevTools/Profiler/Profile.h b/Userland/DevTools/Profiler/Profile.h index 2881b8201f..27f24d4817 100644 --- a/Userland/DevTools/Profiler/Profile.h +++ b/Userland/DevTools/Profiler/Profile.h @@ -7,6 +7,7 @@ #pragma once #include "DisassemblyModel.h" +#include "FilesystemEventModel.h" #include "Process.h" #include "Profile.h" #include "ProfileModel.h" @@ -149,6 +150,7 @@ public: GUI::Model& signposts_model(); GUI::Model* disassembly_model(); GUI::Model* source_model(); + GUI::Model* file_event_model(); Process const* find_process(pid_t pid, EventSerialNumber serial) const { @@ -235,6 +237,7 @@ public: Vector const& events() const { return m_events; } Vector const& filtered_event_indices() const { return m_filtered_event_indices; } Vector const& filtered_signpost_indices() const { return m_filtered_signpost_indices; } + NonnullRefPtr const& file_event_nodes() { return m_file_event_nodes; } u64 length_in_ms() const { return m_last_timestamp - m_first_timestamp; } u64 first_timestamp() const { return m_first_timestamp; } @@ -293,6 +296,7 @@ private: RefPtr m_signposts_model; RefPtr m_disassembly_model; RefPtr m_source_model; + RefPtr m_file_event_model; GUI::ModelIndex m_disassembly_index; GUI::ModelIndex m_source_index; @@ -313,6 +317,8 @@ private: Vector m_process_filters; + NonnullRefPtr m_file_event_nodes; + bool m_inverted { false }; bool m_show_top_functions { false }; bool m_show_percentages { false }; diff --git a/Userland/DevTools/Profiler/main.cpp b/Userland/DevTools/Profiler/main.cpp index 59ace5eee7..3fd79f0b75 100644 --- a/Userland/DevTools/Profiler/main.cpp +++ b/Userland/DevTools/Profiler/main.cpp @@ -252,6 +252,16 @@ ErrorOr serenity_main(Main::Arguments arguments) timeline_view->on_selection_change = [&] { statusbar_update(); }; flamegraph_view->on_hover_change = [&] { statusbar_update(); }; + auto filesystem_events_tab = TRY(tab_widget->try_add_tab("Filesystem events")); + filesystem_events_tab->set_layout(); + filesystem_events_tab->layout()->set_margins(4); + + auto filesystem_events_tree_view = TRY(filesystem_events_tab->try_add()); + filesystem_events_tree_view->set_should_fill_selected_rows(true); + filesystem_events_tree_view->set_column_headers_visible(true); + filesystem_events_tree_view->set_selection_behavior(GUI::TreeView::SelectionBehavior::SelectRows); + filesystem_events_tree_view->set_model(profile->file_event_model()); + auto file_menu = TRY(window->try_add_menu("&File")); TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); })));