1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 15:57:45 +00:00

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. :^)
This commit is contained in:
Andreas Kling 2021-02-27 18:33:30 +01:00
parent 2c1f71055f
commit 1fb1279cfd
8 changed files with 369 additions and 11 deletions

View file

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

View file

@ -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);
}

View file

@ -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 };
};

View file

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

View file

@ -41,6 +41,7 @@
class ProfileModel; class ProfileModel;
class DisassemblyModel; class DisassemblyModel;
class SamplesModel;
class ProfileNode : public RefCounted<ProfileNode> { class ProfileNode : public RefCounted<ProfileNode> {
public: public:
@ -132,6 +133,7 @@ public:
~Profile(); ~Profile();
GUI::Model& model(); GUI::Model& model();
GUI::Model& samples_model();
GUI::Model* disassembly_model(); GUI::Model* disassembly_model();
void set_disassembly_index(const GUI::ModelIndex&); void set_disassembly_index(const GUI::ModelIndex&);
@ -154,6 +156,7 @@ public:
Vector<Frame> frames; 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; } u32 filtered_event_count() const { return m_filtered_event_count; }
const Vector<Event>& events() const { return m_events; } const Vector<Event>& events() const { return m_events; }
@ -200,6 +203,19 @@ public:
const LibraryMetadata& libraries() const { return *m_library_metadata; } 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: private:
Profile(String executable_path, Vector<Event>, NonnullOwnPtr<LibraryMetadata>); Profile(String executable_path, Vector<Event>, NonnullOwnPtr<LibraryMetadata>);
@ -208,12 +224,14 @@ private:
String m_executable_path; String m_executable_path;
RefPtr<ProfileModel> m_model; RefPtr<ProfileModel> m_model;
RefPtr<SamplesModel> m_samples_model;
RefPtr<DisassemblyModel> m_disassembly_model; RefPtr<DisassemblyModel> m_disassembly_model;
GUI::ModelIndex m_disassembly_index; GUI::ModelIndex m_disassembly_index;
Vector<NonnullRefPtr<ProfileNode>> m_roots; Vector<NonnullRefPtr<ProfileNode>> m_roots;
u32 m_filtered_event_count { 0 }; u32 m_filtered_event_count { 0 };
size_t m_first_filtered_event_index { 0 };
u64 m_first_timestamp { 0 }; u64 m_first_timestamp { 0 };
u64 m_last_timestamp { 0 }; u64 m_last_timestamp { 0 };

View file

@ -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);
}

View file

@ -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;
};

View file

@ -24,6 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
#include "IndividualSampleModel.h"
#include "Profile.h" #include "Profile.h"
#include "ProfileTimelineWidget.h" #include "ProfileTimelineWidget.h"
#include <LibCore/ArgsParser.h> #include <LibCore/ArgsParser.h>
@ -44,6 +45,7 @@
#include <LibGUI/Model.h> #include <LibGUI/Model.h>
#include <LibGUI/ProcessChooser.h> #include <LibGUI/ProcessChooser.h>
#include <LibGUI/Splitter.h> #include <LibGUI/Splitter.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TableView.h> #include <LibGUI/TableView.h>
#include <LibGUI/TreeView.h> #include <LibGUI/TreeView.h>
#include <LibGUI/Window.h> #include <LibGUI/Window.h>
@ -100,7 +102,12 @@ int main(int argc, char** argv)
main_widget.add<ProfileTimelineWidget>(*profile); 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>(); auto& tree_view = bottom_splitter.add<GUI::TreeView>();
tree_view.set_should_fill_selected_rows(true); 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()); 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 menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("Profiler"); auto& app_menu = menubar->add_menu("Profiler");
app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); })); app_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) { app->quit(); }));