diff options
author | Stephan Unverwerth <s.unverwerth@serenityos.org> | 2021-12-27 01:25:58 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-12-28 23:17:24 +0100 |
commit | cf8427b7b46798f7e26929bb05dc5ffa33c359a5 (patch) | |
tree | 1281982d6f6857bc3e7a3713ce5060a17cc4b294 | |
parent | e6df1c998804cca1848c634411b42d277d9ff265 (diff) | |
download | serenity-cf8427b7b46798f7e26929bb05dc5ffa33c359a5.zip |
Profiler: Add source code view
This adds a new view mode to profiler which displays source lines and
samples that occured at those lines. This view can be opened via the
menu or by pressing CTRL-S.
It does this by mapping file names from DWARF to "/usr/src/serenity/..."
i.e. source code should be copied to /usr/src/serenity/Userland and
/usr/src/serenity/Kernel to be visible in this mode.
Currently *all* files contributing to the selected function are loaded
completely which could be a lot of data when dealing with lots of
inlined code.
-rw-r--r-- | Userland/DevTools/Profiler/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/DevTools/Profiler/Profile.cpp | 18 | ||||
-rw-r--r-- | Userland/DevTools/Profiler/Profile.h | 5 | ||||
-rw-r--r-- | Userland/DevTools/Profiler/SourceModel.cpp | 217 | ||||
-rw-r--r-- | Userland/DevTools/Profiler/SourceModel.h | 54 | ||||
-rw-r--r-- | Userland/DevTools/Profiler/main.cpp | 20 |
6 files changed, 315 insertions, 0 deletions
diff --git a/Userland/DevTools/Profiler/CMakeLists.txt b/Userland/DevTools/Profiler/CMakeLists.txt index d40ca81379..e10a82b7d2 100644 --- a/Userland/DevTools/Profiler/CMakeLists.txt +++ b/Userland/DevTools/Profiler/CMakeLists.txt @@ -14,6 +14,7 @@ set(SOURCES ProfileModel.cpp SamplesModel.cpp SignpostsModel.cpp + SourceModel.cpp TimelineContainer.cpp TimelineHeader.cpp TimelineTrack.cpp diff --git a/Userland/DevTools/Profiler/Profile.cpp b/Userland/DevTools/Profiler/Profile.cpp index 46349dc859..3b3df1025c 100644 --- a/Userland/DevTools/Profiler/Profile.cpp +++ b/Userland/DevTools/Profiler/Profile.cpp @@ -8,6 +8,7 @@ #include "DisassemblyModel.h" #include "ProfileModel.h" #include "SamplesModel.h" +#include "SourceModel.h" #include <AK/HashTable.h> #include <AK/LexicalPath.h> #include <AK/NonnullOwnPtrVector.h> @@ -554,6 +555,23 @@ GUI::Model* Profile::disassembly_model() return m_disassembly_model; } +void Profile::set_source_index(GUI::ModelIndex const& index) +{ + if (m_source_index == index) + return; + m_source_index = index; + auto* node = static_cast<ProfileNode*>(index.internal_data()); + if (!node) + m_source_model = nullptr; + else + m_source_model = SourceModel::create(*this, *node); +} + +GUI::Model* Profile::source_model() +{ + return m_source_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 e153a9bc70..78ff901eab 100644 --- a/Userland/DevTools/Profiler/Profile.h +++ b/Userland/DevTools/Profiler/Profile.h @@ -12,6 +12,7 @@ #include "ProfileModel.h" #include "SamplesModel.h" #include "SignpostsModel.h" +#include "SourceModel.h" #include <AK/Bitmap.h> #include <AK/FlyString.h> #include <AK/JsonArray.h> @@ -147,6 +148,7 @@ public: GUI::Model& samples_model(); GUI::Model& signposts_model(); GUI::Model* disassembly_model(); + GUI::Model* source_model(); const Process* find_process(pid_t pid, EventSerialNumber serial) const { @@ -157,6 +159,7 @@ public: } void set_disassembly_index(const GUI::ModelIndex&); + void set_source_index(const GUI::ModelIndex&); const Vector<NonnullRefPtr<ProfileNode>>& roots() const { return m_roots; } @@ -281,8 +284,10 @@ private: RefPtr<SamplesModel> m_samples_model; RefPtr<SignpostsModel> m_signposts_model; RefPtr<DisassemblyModel> m_disassembly_model; + RefPtr<SourceModel> m_source_model; GUI::ModelIndex m_disassembly_index; + GUI::ModelIndex m_source_index; Vector<NonnullRefPtr<ProfileNode>> m_roots; Vector<size_t> m_filtered_event_indices; diff --git a/Userland/DevTools/Profiler/SourceModel.cpp b/Userland/DevTools/Profiler/SourceModel.cpp new file mode 100644 index 0000000000..844d1e9064 --- /dev/null +++ b/Userland/DevTools/Profiler/SourceModel.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2021, Stephan Unverwerth <s.unverwerth@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SourceModel.h" +#include "Profile.h" +#include <LibDebug/DebugInfo.h> +#include <LibGUI/Painter.h> +#include <LibSymbolication/Symbolication.h> +#include <stdio.h> + +namespace Profiler { + +class SourceFile final { +public: + struct Line { + String content; + size_t num_samples { 0 }; + }; + + static constexpr StringView source_root_path = "/usr/src/serenity/"sv; + +public: + SourceFile(StringView filename) + { + String source_file_name = filename.replace("../../", source_root_path); + + auto maybe_file = Core::File::open(source_file_name, Core::OpenMode::ReadOnly); + if (maybe_file.is_error()) { + dbgln("Could not map source file \"{}\". Tried {}. {} (errno={})", filename, source_file_name, maybe_file.error().string_literal(), maybe_file.error().code()); + return; + } + + auto file = maybe_file.value(); + + while (!file->eof()) + m_lines.append({ file->read_line(1024), 0 }); + } + + void try_add_samples(size_t line, size_t samples) + { + if (line < 1 || line - 1 >= m_lines.size()) + return; + + m_lines[line - 1].num_samples += samples; + } + + Vector<Line> const& lines() const { return m_lines; } + +private: + Vector<Line> m_lines; +}; + +static Gfx::Bitmap const& heat_gradient() +{ + static RefPtr<Gfx::Bitmap> bitmap; + if (!bitmap) { + bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRx8888, { 101, 1 }).release_value_but_fixme_should_propagate_errors(); + GUI::Painter painter(*bitmap); + painter.fill_rect_with_gradient(Orientation::Horizontal, bitmap->rect(), Color::from_rgb(0xffc080), Color::from_rgb(0xff3000)); + } + return *bitmap; +} + +static Color color_for_percent(int percent) +{ + VERIFY(percent >= 0 && percent <= 100); + return heat_gradient().get_pixel(percent, 0); +} + +SourceModel::SourceModel(Profile& profile, ProfileNode& node) + : m_profile(profile) + , m_node(node) +{ + FlatPtr base_address = 0; + Debug::DebugInfo const* debug_info; + if (auto maybe_kernel_base = Symbolication::kernel_base(); maybe_kernel_base.has_value() && m_node.address() >= *maybe_kernel_base) { + if (!g_kernel_debuginfo_object.has_value()) + return; + base_address = maybe_kernel_base.release_value(); + if (g_kernel_debug_info == nullptr) + g_kernel_debug_info = make<Debug::DebugInfo>(g_kernel_debuginfo_object->elf, String::empty(), base_address); + debug_info = g_kernel_debug_info.ptr(); + } else { + auto const& process = node.process(); + auto const* library_data = process.library_metadata.library_containing(node.address()); + if (!library_data) { + dbgln("no library data for address {:p}", node.address()); + return; + } + base_address = library_data->base; + debug_info = &library_data->load_debug_info(base_address); + } + + VERIFY(debug_info != nullptr); + + // Try to read all source files contributing to the selected function and aggregate the samples by line. + HashMap<String, SourceFile> source_files; + for (auto const& pair : node.events_per_address()) { + auto position = debug_info->get_source_position(pair.key - base_address); + if (position.has_value()) { + auto it = source_files.find(position.value().file_path); + if (it == source_files.end()) { + source_files.set(position.value().file_path, SourceFile(position.value().file_path)); + it = source_files.find(position.value().file_path); + } + + it->value.try_add_samples(position.value().line_number, pair.value); + } + } + + // Process source file map and turn content into view model + for (auto const& file_iterator : source_files) { + u32 line_number = 0; + for (auto const& line_iterator : file_iterator.value.lines()) { + line_number++; + + m_source_lines.append({ + (u32)line_iterator.num_samples, + line_iterator.num_samples * 100.0f / node.event_count(), + file_iterator.key, + line_number, + line_iterator.content, + }); + } + } +} + +int SourceModel::row_count(GUI::ModelIndex const&) const +{ + return m_source_lines.size(); +} + +String SourceModel::column_name(int column) const +{ + switch (column) { + case Column::SampleCount: + return m_profile.show_percentages() ? "% Samples" : "# Samples"; + case Column::SourceCode: + return "Source Code"; + case Column::Location: + return "Location"; + case Column::LineNumber: + return "Line"; + default: + VERIFY_NOT_REACHED(); + return {}; + } +} + +struct ColorPair { + Color background; + Color foreground; +}; + +static Optional<ColorPair> color_pair_for(SourceLineData const& line) +{ + if (line.percent == 0) + return {}; + + Color background = color_for_percent(line.percent); + Color foreground; + if (line.percent > 50) + foreground = Color::White; + else + foreground = Color::Black; + return ColorPair { background, foreground }; +} + +GUI::Variant SourceModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const +{ + auto const& line = m_source_lines[index.row()]; + + if (role == GUI::ModelRole::BackgroundColor) { + auto colors = color_pair_for(line); + if (!colors.has_value()) + return {}; + return colors.value().background; + } + + if (role == GUI::ModelRole::ForegroundColor) { + auto colors = color_pair_for(line); + if (!colors.has_value()) + return {}; + return colors.value().foreground; + } + + if (role == GUI::ModelRole::Font) { + if (index.column() == Column::SourceCode) + return Gfx::FontDatabase::default_fixed_width_font(); + return {}; + } + + if (role == GUI::ModelRole::Display) { + if (index.column() == Column::SampleCount) { + if (m_profile.show_percentages()) + return ((float)line.event_count / (float)m_node.event_count()) * 100.0f; + return line.event_count; + } + + if (index.column() == Column::Location) + return line.location; + + if (index.column() == Column::LineNumber) + return line.line_number; + + if (index.column() == Column::SourceCode) + return line.source_code; + + return {}; + } + return {}; +} + +} diff --git a/Userland/DevTools/Profiler/SourceModel.h b/Userland/DevTools/Profiler/SourceModel.h new file mode 100644 index 0000000000..9e2fa08f9f --- /dev/null +++ b/Userland/DevTools/Profiler/SourceModel.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021, Stephan Unverwerth <s.unverwerth@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibGUI/Model.h> + +namespace Profiler { + +class Profile; +class ProfileNode; + +struct SourceLineData { + u32 event_count { 0 }; + float percent { 0 }; + String location; + u32 line_number { 0 }; + String source_code; +}; + +class SourceModel final : public GUI::Model { +public: + static NonnullRefPtr<SourceModel> create(Profile& profile, ProfileNode& node) + { + return adopt_ref(*new SourceModel(profile, node)); + } + + enum Column { + SampleCount, + Location, + LineNumber, + SourceCode, + __Count + }; + + virtual int row_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override; + virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override { return Column::__Count; } + virtual String column_name(int) const override; + virtual GUI::Variant data(GUI::ModelIndex const&, GUI::ModelRole) const override; + virtual bool is_column_sortable(int) const override { return false; } + +private: + SourceModel(Profile&, ProfileNode&); + + Profile& m_profile; + ProfileNode& m_node; + + Vector<SourceLineData> m_source_lines; +}; + +} diff --git a/Userland/DevTools/Profiler/main.cpp b/Userland/DevTools/Profiler/main.cpp index 2e014d5766..bb34effc92 100644 --- a/Userland/DevTools/Profiler/main.cpp +++ b/Userland/DevTools/Profiler/main.cpp @@ -153,8 +153,21 @@ ErrorOr<int> serenity_main(Main::Arguments arguments) } }; + auto source_view = TRY(bottom_splitter->try_add<GUI::TableView>()); + source_view->set_visible(false); + + auto update_source_model = [&] { + if (source_view->is_visible() && !tree_view->selection().is_empty()) { + profile->set_source_index(tree_view->selection().first()); + source_view->set_model(profile->source_model()); + } else { + source_view->set_model(nullptr); + } + }; + tree_view->on_selection_change = [&] { update_disassembly_model(); + update_source_model(); }; auto disassembly_action = GUI::Action::create_checkable("Show &Disassembly", { Mod_Ctrl, Key_D }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/x86.png").release_value_but_fixme_should_propagate_errors(), [&](auto& action) { @@ -162,6 +175,11 @@ ErrorOr<int> serenity_main(Main::Arguments arguments) update_disassembly_model(); }); + auto source_action = GUI::Action::create_checkable("Show &Source", { Mod_Ctrl, Key_S }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/x86.png").release_value_but_fixme_should_propagate_errors(), [&](auto& action) { + source_view->set_visible(action.is_checked()); + update_source_model(); + }); + auto samples_tab = TRY(tab_widget->try_add_tab<GUI::Widget>("Samples")); samples_tab->set_layout<GUI::VerticalBoxLayout>(); samples_tab->layout()->set_margins(4); @@ -255,11 +273,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments) profile->set_show_percentages(action.is_checked()); tree_view->update(); disassembly_view->update(); + source_view->update(); }); percent_action->set_checked(false); TRY(view_menu->try_add_action(percent_action)); TRY(view_menu->try_add_action(disassembly_action)); + TRY(view_menu->try_add_action(source_action)); auto help_menu = TRY(window->try_add_menu("&Help")); TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) { |