mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 14:37:46 +00:00
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.
This commit is contained in:
parent
e6df1c9988
commit
cf8427b7b4
6 changed files with 315 additions and 0 deletions
|
@ -14,6 +14,7 @@ set(SOURCES
|
||||||
ProfileModel.cpp
|
ProfileModel.cpp
|
||||||
SamplesModel.cpp
|
SamplesModel.cpp
|
||||||
SignpostsModel.cpp
|
SignpostsModel.cpp
|
||||||
|
SourceModel.cpp
|
||||||
TimelineContainer.cpp
|
TimelineContainer.cpp
|
||||||
TimelineHeader.cpp
|
TimelineHeader.cpp
|
||||||
TimelineTrack.cpp
|
TimelineTrack.cpp
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include "DisassemblyModel.h"
|
#include "DisassemblyModel.h"
|
||||||
#include "ProfileModel.h"
|
#include "ProfileModel.h"
|
||||||
#include "SamplesModel.h"
|
#include "SamplesModel.h"
|
||||||
|
#include "SourceModel.h"
|
||||||
#include <AK/HashTable.h>
|
#include <AK/HashTable.h>
|
||||||
#include <AK/LexicalPath.h>
|
#include <AK/LexicalPath.h>
|
||||||
#include <AK/NonnullOwnPtrVector.h>
|
#include <AK/NonnullOwnPtrVector.h>
|
||||||
|
@ -554,6 +555,23 @@ GUI::Model* Profile::disassembly_model()
|
||||||
return m_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)
|
ProfileNode::ProfileNode(Process const& process)
|
||||||
: m_root(true)
|
: m_root(true)
|
||||||
, m_process(process)
|
, m_process(process)
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include "ProfileModel.h"
|
#include "ProfileModel.h"
|
||||||
#include "SamplesModel.h"
|
#include "SamplesModel.h"
|
||||||
#include "SignpostsModel.h"
|
#include "SignpostsModel.h"
|
||||||
|
#include "SourceModel.h"
|
||||||
#include <AK/Bitmap.h>
|
#include <AK/Bitmap.h>
|
||||||
#include <AK/FlyString.h>
|
#include <AK/FlyString.h>
|
||||||
#include <AK/JsonArray.h>
|
#include <AK/JsonArray.h>
|
||||||
|
@ -147,6 +148,7 @@ public:
|
||||||
GUI::Model& samples_model();
|
GUI::Model& samples_model();
|
||||||
GUI::Model& signposts_model();
|
GUI::Model& signposts_model();
|
||||||
GUI::Model* disassembly_model();
|
GUI::Model* disassembly_model();
|
||||||
|
GUI::Model* source_model();
|
||||||
|
|
||||||
const Process* find_process(pid_t pid, EventSerialNumber serial) const
|
const Process* find_process(pid_t pid, EventSerialNumber serial) const
|
||||||
{
|
{
|
||||||
|
@ -157,6 +159,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_disassembly_index(const GUI::ModelIndex&);
|
void set_disassembly_index(const GUI::ModelIndex&);
|
||||||
|
void set_source_index(const GUI::ModelIndex&);
|
||||||
|
|
||||||
const Vector<NonnullRefPtr<ProfileNode>>& roots() const { return m_roots; }
|
const Vector<NonnullRefPtr<ProfileNode>>& roots() const { return m_roots; }
|
||||||
|
|
||||||
|
@ -281,8 +284,10 @@ private:
|
||||||
RefPtr<SamplesModel> m_samples_model;
|
RefPtr<SamplesModel> m_samples_model;
|
||||||
RefPtr<SignpostsModel> m_signposts_model;
|
RefPtr<SignpostsModel> m_signposts_model;
|
||||||
RefPtr<DisassemblyModel> m_disassembly_model;
|
RefPtr<DisassemblyModel> m_disassembly_model;
|
||||||
|
RefPtr<SourceModel> m_source_model;
|
||||||
|
|
||||||
GUI::ModelIndex m_disassembly_index;
|
GUI::ModelIndex m_disassembly_index;
|
||||||
|
GUI::ModelIndex m_source_index;
|
||||||
|
|
||||||
Vector<NonnullRefPtr<ProfileNode>> m_roots;
|
Vector<NonnullRefPtr<ProfileNode>> m_roots;
|
||||||
Vector<size_t> m_filtered_event_indices;
|
Vector<size_t> m_filtered_event_indices;
|
||||||
|
|
217
Userland/DevTools/Profiler/SourceModel.cpp
Normal file
217
Userland/DevTools/Profiler/SourceModel.cpp
Normal file
|
@ -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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
Userland/DevTools/Profiler/SourceModel.h
Normal file
54
Userland/DevTools/Profiler/SourceModel.h
Normal file
|
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -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 = [&] {
|
tree_view->on_selection_change = [&] {
|
||||||
update_disassembly_model();
|
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) {
|
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();
|
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"));
|
auto samples_tab = TRY(tab_widget->try_add_tab<GUI::Widget>("Samples"));
|
||||||
samples_tab->set_layout<GUI::VerticalBoxLayout>();
|
samples_tab->set_layout<GUI::VerticalBoxLayout>();
|
||||||
samples_tab->layout()->set_margins(4);
|
samples_tab->layout()->set_margins(4);
|
||||||
|
@ -255,11 +273,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
profile->set_show_percentages(action.is_checked());
|
profile->set_show_percentages(action.is_checked());
|
||||||
tree_view->update();
|
tree_view->update();
|
||||||
disassembly_view->update();
|
disassembly_view->update();
|
||||||
|
source_view->update();
|
||||||
});
|
});
|
||||||
percent_action->set_checked(false);
|
percent_action->set_checked(false);
|
||||||
TRY(view_menu->try_add_action(percent_action));
|
TRY(view_menu->try_add_action(percent_action));
|
||||||
|
|
||||||
TRY(view_menu->try_add_action(disassembly_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"));
|
auto help_menu = TRY(window->try_add_menu("&Help"));
|
||||||
TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) {
|
TRY(help_menu->try_add_action(GUI::CommonActions::make_help_action([](auto&) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue