mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 04:48:14 +00:00
ProfileViewer: Begin work on a visualization tool for profiles :^)
We begin with a simple treeview that shows a recorded profile. To record and view a profile of a process with <PID>, simply do this: $ profile <PID> on ... wait while PID does something interesting ... $ profile <PID> off $ cat /proc/profile > my-profile.prof $ ProfileViewer my-profile.prof
This commit is contained in:
parent
0f393148da
commit
19d8c675f1
8 changed files with 350 additions and 0 deletions
24
DevTools/ProfileViewer/Makefile
Normal file
24
DevTools/ProfileViewer/Makefile
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
include ../../Makefile.common
|
||||||
|
|
||||||
|
OBJS = \
|
||||||
|
Profile.o \
|
||||||
|
ProfileModel.o \
|
||||||
|
main.o
|
||||||
|
|
||||||
|
APP = ProfileViewer
|
||||||
|
|
||||||
|
DEFINES += -DUSERLAND
|
||||||
|
|
||||||
|
all: $(APP)
|
||||||
|
|
||||||
|
$(APP): $(OBJS)
|
||||||
|
$(LD) -o $(APP) $(LDFLAGS) $(OBJS) -lgui -ldraw -lipc -lcore -lc
|
||||||
|
|
||||||
|
.cpp.o:
|
||||||
|
@echo "CXX $<"; $(CXX) $(CXXFLAGS) -o $@ -c $<
|
||||||
|
|
||||||
|
-include $(OBJS:%.o=%.d)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "CLEAN"; rm -f $(APP) $(OBJS) *.d
|
||||||
|
|
91
DevTools/ProfileViewer/Profile.cpp
Normal file
91
DevTools/ProfileViewer/Profile.cpp
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
#include "Profile.h"
|
||||||
|
#include "ProfileModel.h"
|
||||||
|
#include <AK/QuickSort.h>
|
||||||
|
#include <LibCore/CFile.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
Profile::Profile(const JsonArray& json, NonnullRefPtrVector<ProfileNode>&& roots)
|
||||||
|
: m_json(json)
|
||||||
|
, m_roots(move(roots))
|
||||||
|
{
|
||||||
|
m_model = ProfileModel::create(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile::~Profile()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
GModel& Profile::model()
|
||||||
|
{
|
||||||
|
return *m_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
OwnPtr<Profile> Profile::load_from_file(const StringView& path)
|
||||||
|
{
|
||||||
|
auto file = CFile::construct(path);
|
||||||
|
if (!file->open(CIODevice::ReadOnly)) {
|
||||||
|
fprintf(stderr, "Unable to open %s, error: %s\n", String(path).characters(), file->error_string());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto json = JsonValue::from_string(file->read_all());
|
||||||
|
if (!json.is_array()) {
|
||||||
|
fprintf(stderr, "Invalid format (not a JSON array)\n");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto samples = json.as_array();
|
||||||
|
|
||||||
|
NonnullRefPtrVector<ProfileNode> roots;
|
||||||
|
|
||||||
|
auto find_or_create_root = [&roots](const String& symbol, u32 address, u32 offset) -> ProfileNode& {
|
||||||
|
for (int i = 0; i < roots.size(); ++i) {
|
||||||
|
auto& root = roots[i];
|
||||||
|
if (root.symbol() == symbol) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto new_root = ProfileNode::create(symbol, address, offset);
|
||||||
|
roots.append(new_root);
|
||||||
|
return new_root;
|
||||||
|
};
|
||||||
|
|
||||||
|
samples.for_each([&](const JsonValue& sample) {
|
||||||
|
auto frames_value = sample.as_object().get("frames");
|
||||||
|
auto& frames = frames_value.as_array();
|
||||||
|
ProfileNode* node = nullptr;
|
||||||
|
for (int i = frames.size() - 1; i >= 0; --i) {
|
||||||
|
auto& frame = frames.at(i);
|
||||||
|
|
||||||
|
auto symbol = frame.as_object().get("symbol").as_string_or({});
|
||||||
|
auto address = frame.as_object().get("address").as_u32();
|
||||||
|
auto offset = frame.as_object().get("offset").as_u32();
|
||||||
|
|
||||||
|
if (symbol.is_empty())
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!node)
|
||||||
|
node = &find_or_create_root(symbol, address, offset);
|
||||||
|
else
|
||||||
|
node = &node->find_or_create_child(symbol, address, offset);
|
||||||
|
|
||||||
|
node->increment_sample_count();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto& root : roots) {
|
||||||
|
root.sort_children();
|
||||||
|
}
|
||||||
|
|
||||||
|
return NonnullOwnPtr<Profile>(NonnullOwnPtr<Profile>::Adopt, *new Profile(move(samples), move(roots)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProfileNode::sort_children()
|
||||||
|
{
|
||||||
|
quick_sort(m_children.begin(), m_children.end(), [](auto& a, auto& b) {
|
||||||
|
return a->sample_count() >= b->sample_count();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto& child : m_children)
|
||||||
|
child->sort_children();
|
||||||
|
}
|
88
DevTools/ProfileViewer/Profile.h
Normal file
88
DevTools/ProfileViewer/Profile.h
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/JsonArray.h>
|
||||||
|
#include <AK/JsonObject.h>
|
||||||
|
#include <AK/JsonValue.h>
|
||||||
|
#include <AK/NonnullRefPtrVector.h>
|
||||||
|
#include <AK/OwnPtr.h>
|
||||||
|
|
||||||
|
class GModel;
|
||||||
|
class ProfileModel;
|
||||||
|
|
||||||
|
class ProfileNode : public RefCounted<ProfileNode> {
|
||||||
|
public:
|
||||||
|
static NonnullRefPtr<ProfileNode> create(const String& symbol, u32 address, u32 offset)
|
||||||
|
{
|
||||||
|
return adopt(*new ProfileNode(symbol, address, offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const String& symbol() const { return m_symbol; }
|
||||||
|
u32 address() const { return m_address; }
|
||||||
|
u32 offset() const { return m_offset; }
|
||||||
|
|
||||||
|
u32 sample_count() const { return m_sample_count; }
|
||||||
|
|
||||||
|
int child_count() const { return m_children.size(); }
|
||||||
|
const Vector<NonnullRefPtr<ProfileNode>>& children() const { return m_children; }
|
||||||
|
|
||||||
|
void add_child(ProfileNode& child)
|
||||||
|
{
|
||||||
|
if (child.m_parent == this)
|
||||||
|
return;
|
||||||
|
ASSERT(!child.m_parent);
|
||||||
|
child.m_parent = this;
|
||||||
|
m_children.append(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileNode& find_or_create_child(const String& symbol, u32 address, u32 offset)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_children.size(); ++i) {
|
||||||
|
auto& child = m_children[i];
|
||||||
|
if (child->symbol() == symbol) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto new_child = ProfileNode::create(symbol, address, offset);
|
||||||
|
m_children.append(new_child);
|
||||||
|
return new_child;
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileNode* parent() { return m_parent; }
|
||||||
|
const ProfileNode* parent() const { return m_parent; }
|
||||||
|
|
||||||
|
void increment_sample_count() { ++m_sample_count; }
|
||||||
|
|
||||||
|
void sort_children();
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit ProfileNode(const String& symbol, u32 address, u32 offset)
|
||||||
|
: m_symbol(symbol)
|
||||||
|
, m_address(address)
|
||||||
|
, m_offset(offset)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileNode* m_parent { nullptr };
|
||||||
|
String m_symbol;
|
||||||
|
u32 m_address { 0 };
|
||||||
|
u32 m_offset { 0 };
|
||||||
|
u32 m_sample_count { 0 };
|
||||||
|
Vector<NonnullRefPtr<ProfileNode>> m_children;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Profile {
|
||||||
|
public:
|
||||||
|
static OwnPtr<Profile> load_from_file(const StringView& path);
|
||||||
|
~Profile();
|
||||||
|
|
||||||
|
GModel& model();
|
||||||
|
|
||||||
|
const NonnullRefPtrVector<ProfileNode>& roots() const { return m_roots; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Profile(const JsonArray&, NonnullRefPtrVector<ProfileNode>&&);
|
||||||
|
|
||||||
|
JsonArray m_json;
|
||||||
|
RefPtr<ProfileModel> m_model;
|
||||||
|
NonnullRefPtrVector<ProfileNode> m_roots;
|
||||||
|
};
|
83
DevTools/ProfileViewer/ProfileModel.cpp
Normal file
83
DevTools/ProfileViewer/ProfileModel.cpp
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
#include "ProfileModel.h"
|
||||||
|
#include "Profile.h"
|
||||||
|
#include <AK/StringBuilder.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
ProfileModel::ProfileModel(Profile& profile)
|
||||||
|
: m_profile(profile)
|
||||||
|
{
|
||||||
|
m_frame_icon.set_bitmap_for_size(16, GraphicsBitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileModel::~ProfileModel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
GModelIndex ProfileModel::index(int row, int column, const GModelIndex& parent) const
|
||||||
|
{
|
||||||
|
if (!parent.is_valid()) {
|
||||||
|
if (m_profile.roots().is_empty())
|
||||||
|
return {};
|
||||||
|
return create_index(row, column, &m_profile.roots().at(row));
|
||||||
|
}
|
||||||
|
auto& remote_parent = *static_cast<ProfileNode*>(parent.internal_data());
|
||||||
|
return create_index(row, column, remote_parent.children().at(row).ptr());
|
||||||
|
}
|
||||||
|
|
||||||
|
GModelIndex ProfileModel::parent_index(const GModelIndex& index) const
|
||||||
|
{
|
||||||
|
if (!index.is_valid())
|
||||||
|
return {};
|
||||||
|
auto& node = *static_cast<ProfileNode*>(index.internal_data());
|
||||||
|
if (!node.parent())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
// NOTE: If the parent has no parent, it's a root, so we have to look among the remote roots.
|
||||||
|
if (!node.parent()->parent()) {
|
||||||
|
for (int row = 0; row < m_profile.roots().size(); ++row) {
|
||||||
|
if (&m_profile.roots()[row] == node.parent())
|
||||||
|
return create_index(row, 0, node.parent());
|
||||||
|
}
|
||||||
|
ASSERT_NOT_REACHED();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < node.parent()->parent()->children().size(); ++row) {
|
||||||
|
if (node.parent()->parent()->children()[row].ptr() == node.parent())
|
||||||
|
return create_index(row, 0, node.parent());
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NOT_REACHED();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int ProfileModel::row_count(const GModelIndex& index) const
|
||||||
|
{
|
||||||
|
if (!index.is_valid())
|
||||||
|
return m_profile.roots().size();
|
||||||
|
auto& node = *static_cast<ProfileNode*>(index.internal_data());
|
||||||
|
return node.children().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ProfileModel::column_count(const GModelIndex&) const
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GVariant ProfileModel::data(const GModelIndex& index, Role role) const
|
||||||
|
{
|
||||||
|
auto* node = static_cast<ProfileNode*>(index.internal_data());
|
||||||
|
if (role == Role::Icon) {
|
||||||
|
return m_frame_icon;
|
||||||
|
}
|
||||||
|
if (role == Role::Display) {
|
||||||
|
return String::format("%s (%u)", node->symbol().characters(), node->sample_count());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProfileModel::update()
|
||||||
|
{
|
||||||
|
did_update();
|
||||||
|
}
|
28
DevTools/ProfileViewer/ProfileModel.h
Normal file
28
DevTools/ProfileViewer/ProfileModel.h
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibGUI/GModel.h>
|
||||||
|
|
||||||
|
class Profile;
|
||||||
|
|
||||||
|
class ProfileModel final : public GModel {
|
||||||
|
public:
|
||||||
|
static NonnullRefPtr<ProfileModel> create(Profile& profile)
|
||||||
|
{
|
||||||
|
return adopt(*new ProfileModel(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~ProfileModel() override;
|
||||||
|
|
||||||
|
virtual int row_count(const GModelIndex& = GModelIndex()) const override;
|
||||||
|
virtual int column_count(const GModelIndex& = GModelIndex()) const override;
|
||||||
|
virtual GVariant data(const GModelIndex&, Role = Role::Display) const override;
|
||||||
|
virtual GModelIndex index(int row, int column, const GModelIndex& parent = GModelIndex()) const override;
|
||||||
|
virtual GModelIndex parent_index(const GModelIndex&) const override;
|
||||||
|
virtual void update() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit ProfileModel(Profile&);
|
||||||
|
|
||||||
|
Profile& m_profile;
|
||||||
|
GIcon m_frame_icon;
|
||||||
|
};
|
33
DevTools/ProfileViewer/main.cpp
Normal file
33
DevTools/ProfileViewer/main.cpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#include "Profile.h"
|
||||||
|
#include <LibGUI/GApplication.h>
|
||||||
|
#include <LibGUI/GTreeView.h>
|
||||||
|
#include <LibGUI/GWindow.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (argc != 2) {
|
||||||
|
printf("usage: %s <profile-file>\n", argv[0]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto profile = Profile::load_from_file(argv[1]);
|
||||||
|
if (!profile) {
|
||||||
|
fprintf(stderr, "Unable to load profile '%s'\n", argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GApplication app(argc, argv);
|
||||||
|
|
||||||
|
auto window = GWindow::construct();
|
||||||
|
window->set_title("ProfileViewer");
|
||||||
|
window->set_rect(100, 100, 800, 600);
|
||||||
|
|
||||||
|
auto tree_view = GTreeView::construct(nullptr);
|
||||||
|
tree_view->set_model(profile->model());
|
||||||
|
|
||||||
|
window->set_main_widget(tree_view);
|
||||||
|
|
||||||
|
window->show();
|
||||||
|
return app.exec();
|
||||||
|
}
|
|
@ -98,6 +98,7 @@ cp ../Demos/Fire/Fire mnt/bin/Fire
|
||||||
cp ../DevTools/HackStudio/HackStudio mnt/bin/HackStudio
|
cp ../DevTools/HackStudio/HackStudio mnt/bin/HackStudio
|
||||||
cp ../DevTools/VisualBuilder/VisualBuilder mnt/bin/VisualBuilder
|
cp ../DevTools/VisualBuilder/VisualBuilder mnt/bin/VisualBuilder
|
||||||
cp ../DevTools/Inspector/Inspector mnt/bin/Inspector
|
cp ../DevTools/Inspector/Inspector mnt/bin/Inspector
|
||||||
|
cp ../DevTools/ProfileViewer/ProfileViewer mnt/bin/ProfileViewer
|
||||||
cp ../Games/Minesweeper/Minesweeper mnt/bin/Minesweeper
|
cp ../Games/Minesweeper/Minesweeper mnt/bin/Minesweeper
|
||||||
cp ../Games/Snake/Snake mnt/bin/Snake
|
cp ../Games/Snake/Snake mnt/bin/Snake
|
||||||
cp ../Servers/LookupServer/LookupServer mnt/bin/LookupServer
|
cp ../Servers/LookupServer/LookupServer mnt/bin/LookupServer
|
||||||
|
@ -136,6 +137,7 @@ ln -s Help mnt/bin/help
|
||||||
ln -s Browser mnt/bin/br
|
ln -s Browser mnt/bin/br
|
||||||
ln -s HackStudio mnt/bin/hs
|
ln -s HackStudio mnt/bin/hs
|
||||||
ln -s SystemMonitor mnt/bin/sm
|
ln -s SystemMonitor mnt/bin/sm
|
||||||
|
ln -s ProfileViewer mnt/bin/pv
|
||||||
echo "done"
|
echo "done"
|
||||||
|
|
||||||
mkdir -p mnt/boot/
|
mkdir -p mnt/boot/
|
||||||
|
|
|
@ -80,6 +80,7 @@ build_targets="$build_targets ../Demos/WidgetGallery"
|
||||||
build_targets="$build_targets ../DevTools/HackStudio"
|
build_targets="$build_targets ../DevTools/HackStudio"
|
||||||
build_targets="$build_targets ../DevTools/VisualBuilder"
|
build_targets="$build_targets ../DevTools/VisualBuilder"
|
||||||
build_targets="$build_targets ../DevTools/Inspector"
|
build_targets="$build_targets ../DevTools/Inspector"
|
||||||
|
build_targets="$build_targets ../DevTools/ProfileViewer"
|
||||||
|
|
||||||
build_targets="$build_targets ../Games/Minesweeper"
|
build_targets="$build_targets ../Games/Minesweeper"
|
||||||
build_targets="$build_targets ../Games/Snake"
|
build_targets="$build_targets ../Games/Snake"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue