Browse Source

Profiler: Add a new "Samples" view to the main UI

You can now view the individual samples in a profile one by one with
the new "Samples" view. The "old" main view moves into a "Call Tree"
tab (but it remains the default view.)

When you select a sample in the samples view, we show you the full
symbolicated backtrace in a separate view on the right hand side. :^)
Andreas Kling 4 years ago
parent
commit
1fb1279cfd

+ 4 - 2
Userland/DevTools/Profiler/CMakeLists.txt

@@ -1,9 +1,11 @@
 set(SOURCES
     DisassemblyModel.cpp
     main.cpp
-    Profile.cpp
+        IndividualSampleModel.cpp
+        Profile.cpp
     ProfileModel.cpp
-    ProfileTimelineWidget.cpp
+        ProfileTimelineWidget.cpp
+        SamplesModel.cpp
 )
 
 serenity_app(Profiler ICON app-profiler)

+ 91 - 0
Userland/DevTools/Profiler/IndividualSampleModel.cpp

@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "IndividualSampleModel.h"
+#include "Profile.h"
+#include <AK/StringBuilder.h>
+#include <stdio.h>
+
+IndividualSampleModel::IndividualSampleModel(Profile& profile, size_t event_index)
+    : m_profile(profile)
+    , m_event_index(event_index)
+{
+}
+
+IndividualSampleModel::~IndividualSampleModel()
+{
+}
+
+int IndividualSampleModel::row_count(const GUI::ModelIndex&) const
+{
+    auto& event = m_profile.events().at(m_event_index);
+    return event.frames.size();
+}
+
+int IndividualSampleModel::column_count(const GUI::ModelIndex&) const
+{
+    return Column::__Count;
+}
+
+String IndividualSampleModel::column_name(int column) const
+{
+    switch (column) {
+    case Column::Address:
+        return "Address";
+    case Column::ObjectName:
+        return "Object";
+    case Column::Symbol:
+        return "Symbol";
+    default:
+        VERIFY_NOT_REACHED();
+    }
+}
+
+GUI::Variant IndividualSampleModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+    auto& event = m_profile.events().at(m_event_index);
+    auto& frame = event.frames[event.frames.size() - index.row() - 1];
+
+    if (role == GUI::ModelRole::Display) {
+        if (index.column() == Column::Address)
+            return String::formatted("{:08x}", frame.address);
+
+        if (index.column() == Column::Symbol) {
+            return frame.symbol;
+        }
+
+        if (index.column() == Column::ObjectName) {
+            return frame.object_name;
+        }
+        return {};
+    }
+    return {};
+}
+
+void IndividualSampleModel::update()
+{
+    did_update(Model::InvalidateAllIndexes);
+}

+ 60 - 0
Userland/DevTools/Profiler/IndividualSampleModel.h

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Model.h>
+
+class Profile;
+
+class IndividualSampleModel final : public GUI::Model {
+public:
+    static NonnullRefPtr<IndividualSampleModel> create(Profile& profile, size_t event_index)
+    {
+        return adopt(*new IndividualSampleModel(profile, event_index));
+    }
+
+    enum Column {
+        Address,
+        ObjectName,
+        Symbol,
+        __Count
+    };
+
+    virtual ~IndividualSampleModel() override;
+
+    virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+    virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+    virtual String column_name(int) const override;
+    virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+    virtual void update() override;
+
+private:
+    IndividualSampleModel(Profile&, size_t event_index);
+
+    Profile& m_profile;
+    const size_t m_event_index { 0 };
+};

+ 17 - 8
Userland/DevTools/Profiler/Profile.cpp

@@ -27,6 +27,7 @@
 #include "Profile.h"
 #include "DisassemblyModel.h"
 #include "ProfileModel.h"
+#include "SamplesModel.h"
 #include <AK/HashTable.h>
 #include <AK/LexicalPath.h>
 #include <AK/MappedFile.h>
@@ -55,6 +56,7 @@ Profile::Profile(String executable_path, Vector<Event> events, NonnullOwnPtr<Lib
     m_last_timestamp = m_events.last().timestamp;
 
     m_model = ProfileModel::create(*this);
+    m_samples_model = SamplesModel::create(*this);
 
     for (auto& event : m_events) {
         m_deepest_stack_depth = max((u32)event.frames.size(), m_deepest_stack_depth);
@@ -72,6 +74,11 @@ GUI::Model& Profile::model()
     return *m_model;
 }
 
+GUI::Model& Profile::samples_model()
+{
+    return *m_samples_model;
+}
+
 void Profile::rebuild_tree()
 {
     u32 filtered_event_count = 0;
@@ -91,18 +98,14 @@ void Profile::rebuild_tree()
 
     HashTable<FlatPtr> live_allocations;
 
-    for (auto& event : m_events) {
-        if (has_timestamp_filter_range()) {
-            auto timestamp = event.timestamp;
-            if (timestamp < m_timestamp_filter_range_start || timestamp > m_timestamp_filter_range_end)
-                continue;
-        }
-
+    for_each_event_in_filter_range([&](auto& event) {
         if (event.type == "malloc")
             live_allocations.set(event.ptr);
         else if (event.type == "free")
             live_allocations.remove(event.ptr);
-    }
+    });
+
+    Optional<size_t> first_filtered_event_index;
 
     for (size_t event_index = 0; event_index < m_events.size(); ++event_index) {
         auto& event = m_events.at(event_index);
@@ -112,6 +115,9 @@ void Profile::rebuild_tree()
                 continue;
         }
 
+        if (!first_filtered_event_index.has_value())
+            first_filtered_event_index = event_index;
+
         if (event.type == "malloc" && !live_allocations.contains(event.ptr))
             continue;
 
@@ -197,6 +203,7 @@ void Profile::rebuild_tree()
     sort_profile_nodes(roots);
 
     m_filtered_event_count = filtered_event_count;
+    m_first_filtered_event_index = first_filtered_event_index.value_or(0);
     m_roots = move(roots);
     m_model->update();
 }
@@ -307,6 +314,7 @@ void Profile::set_timestamp_filter_range(u64 start, u64 end)
     m_timestamp_filter_range_end = max(start, end);
 
     rebuild_tree();
+    m_samples_model->update();
 }
 
 void Profile::clear_timestamp_filter_range()
@@ -315,6 +323,7 @@ void Profile::clear_timestamp_filter_range()
         return;
     m_has_timestamp_filter_range = false;
     rebuild_tree();
+    m_samples_model->update();
 }
 
 void Profile::set_inverted(bool inverted)

+ 18 - 0
Userland/DevTools/Profiler/Profile.h

@@ -41,6 +41,7 @@
 
 class ProfileModel;
 class DisassemblyModel;
+class SamplesModel;
 
 class ProfileNode : public RefCounted<ProfileNode> {
 public:
@@ -132,6 +133,7 @@ public:
     ~Profile();
 
     GUI::Model& model();
+    GUI::Model& samples_model();
     GUI::Model* disassembly_model();
 
     void set_disassembly_index(const GUI::ModelIndex&);
@@ -154,6 +156,7 @@ public:
         Vector<Frame> frames;
     };
 
+    u32 first_filtered_event_index() const { return m_first_filtered_event_index; }
     u32 filtered_event_count() const { return m_filtered_event_count; }
 
     const Vector<Event>& events() const { return m_events; }
@@ -200,6 +203,19 @@ public:
 
     const LibraryMetadata& libraries() const { return *m_library_metadata; }
 
+    template<typename Callback>
+    void for_each_event_in_filter_range(Callback callback)
+    {
+        for (auto& event : m_events) {
+            if (has_timestamp_filter_range()) {
+                auto timestamp = event.timestamp;
+                if (timestamp < m_timestamp_filter_range_start || timestamp > m_timestamp_filter_range_end)
+                    continue;
+            }
+            callback(event);
+        }
+    }
+
 private:
     Profile(String executable_path, Vector<Event>, NonnullOwnPtr<LibraryMetadata>);
 
@@ -208,12 +224,14 @@ private:
     String m_executable_path;
 
     RefPtr<ProfileModel> m_model;
+    RefPtr<SamplesModel> m_samples_model;
     RefPtr<DisassemblyModel> m_disassembly_model;
 
     GUI::ModelIndex m_disassembly_index;
 
     Vector<NonnullRefPtr<ProfileNode>> m_roots;
     u32 m_filtered_event_count { 0 };
+    size_t m_first_filtered_event_index { 0 };
     u64 m_first_timestamp { 0 };
     u64 m_last_timestamp { 0 };
 

+ 95 - 0
Userland/DevTools/Profiler/SamplesModel.cpp

@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "SamplesModel.h"
+#include "Profile.h"
+#include <AK/StringBuilder.h>
+#include <stdio.h>
+
+SamplesModel::SamplesModel(Profile& profile)
+    : m_profile(profile)
+{
+    m_user_frame_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
+    m_kernel_frame_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object-red.png"));
+}
+
+SamplesModel::~SamplesModel()
+{
+}
+
+int SamplesModel::row_count(const GUI::ModelIndex&) const
+{
+    return m_profile.filtered_event_count();
+}
+
+int SamplesModel::column_count(const GUI::ModelIndex&) const
+{
+    return Column::__Count;
+}
+
+String SamplesModel::column_name(int column) const
+{
+    switch (column) {
+    case Column::SampleIndex:
+        return "#";
+    case Column::Timestamp:
+        return "Timestamp";
+    case Column::InnermostStackFrame:
+        return "Innermost Frame";
+    default:
+        VERIFY_NOT_REACHED();
+    }
+}
+
+GUI::Variant SamplesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+    u32 event_index = m_profile.first_filtered_event_index() + index.row();
+    auto& event = m_profile.events().at(event_index);
+
+    if (role == GUI::ModelRole::Custom) {
+        return event_index;
+    }
+
+    if (role == GUI::ModelRole::Display) {
+        if (index.column() == Column::SampleIndex)
+            return event_index;
+
+        if (index.column() == Column::Timestamp) {
+            return (u32)event.timestamp;
+        }
+
+        if (index.column() == Column::InnermostStackFrame) {
+            return event.frames.last().symbol;
+        }
+        return {};
+    }
+    return {};
+}
+
+void SamplesModel::update()
+{
+    did_update(Model::InvalidateAllIndexes);
+}

+ 62 - 0
Userland/DevTools/Profiler/SamplesModel.h

@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/Model.h>
+
+class Profile;
+
+class SamplesModel final : public GUI::Model {
+public:
+    static NonnullRefPtr<SamplesModel> create(Profile& profile)
+    {
+        return adopt(*new SamplesModel(profile));
+    }
+
+    enum Column {
+        SampleIndex,
+        Timestamp,
+        InnermostStackFrame,
+        __Count
+    };
+
+    virtual ~SamplesModel() override;
+
+    virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+    virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+    virtual String column_name(int) const override;
+    virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+    virtual void update() override;
+
+private:
+    explicit SamplesModel(Profile&);
+
+    Profile& m_profile;
+
+    GUI::Icon m_user_frame_icon;
+    GUI::Icon m_kernel_frame_icon;
+};

+ 22 - 1
Userland/DevTools/Profiler/main.cpp

@@ -24,6 +24,7 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+#include "IndividualSampleModel.h"
 #include "Profile.h"
 #include "ProfileTimelineWidget.h"
 #include <LibCore/ArgsParser.h>
@@ -44,6 +45,7 @@
 #include <LibGUI/Model.h>
 #include <LibGUI/ProcessChooser.h>
 #include <LibGUI/Splitter.h>
+#include <LibGUI/TabWidget.h>
 #include <LibGUI/TableView.h>
 #include <LibGUI/TreeView.h>
 #include <LibGUI/Window.h>
@@ -100,7 +102,12 @@ int main(int argc, char** argv)
 
     main_widget.add<ProfileTimelineWidget>(*profile);
 
-    auto& bottom_splitter = main_widget.add<GUI::VerticalSplitter>();
+    auto& tab_widget = main_widget.add<GUI::TabWidget>();
+
+    auto& tree_tab = tab_widget.add_tab<GUI::Widget>("Call Tree");
+    tree_tab.set_layout<GUI::VerticalBoxLayout>();
+    tree_tab.layout()->set_margins({ 4, 4, 4, 4 });
+    auto& bottom_splitter = tree_tab.add<GUI::VerticalSplitter>();
 
     auto& tree_view = bottom_splitter.add<GUI::TreeView>();
     tree_view.set_should_fill_selected_rows(true);
@@ -114,6 +121,20 @@ int main(int argc, char** argv)
         disassembly_view.set_model(profile->disassembly_model());
     };
 
+    auto& samples_tab = tab_widget.add_tab<GUI::Widget>("Samples");
+    samples_tab.set_layout<GUI::VerticalBoxLayout>();
+    samples_tab.layout()->set_margins({ 4, 4, 4, 4 });
+
+    auto& samples_splitter = samples_tab.add<GUI::HorizontalSplitter>();
+    auto& samples_table_view = samples_splitter.add<GUI::TableView>();
+    samples_table_view.set_model(profile->samples_model());
+
+    auto& individual_sample_view = samples_splitter.add<GUI::TableView>();
+    samples_table_view.on_selection = [&](const GUI::ModelIndex& index) {
+        auto model = IndividualSampleModel::create(*profile, index.data(GUI::ModelRole::Custom).to_integer<size_t>());
+        individual_sample_view.set_model(move(model));
+    };
+
     auto menubar = GUI::MenuBar::construct();
     auto& app_menu = menubar->add_menu("Profiler");
     app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); }));