Bladeren bron

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.
Stephan Unverwerth 3 jaren geleden
bovenliggende
commit
cf8427b7b4

+ 1 - 0
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

+ 18 - 0
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)

+ 5 - 0
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;

+ 217 - 0
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 {};
+}
+
+}

+ 54 - 0
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;
+};
+
+}

+ 20 - 0
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&) {