mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 10:57:35 +00:00
Kernel+Profiler: Improve profiling subsystem
This turns the perfcore format into more a log than it was before, which lets us properly log process, thread and region creation/destruction. This also makes it unnecessary to dump the process' regions every time it is scheduled like we did before. Incidentally this also fixes 'profile -c' because we previously ended up incorrectly dumping the parent's region map into the profile data. Log-based mmap support enables profiling shared libraries which are loaded at runtime, e.g. via dlopen(). This enables profiling both the parent and child process for programs which use execve(). Previously we'd discard the profiling data for the old process. The Profiler tool has been updated to not treat thread IDs as process IDs anymore. This enables support for processes with more than one thread. Also, there's a new widget to filter which process should be displayed.
This commit is contained in:
parent
f57c57966b
commit
eb798d5538
26 changed files with 658 additions and 292 deletions
|
@ -1,11 +1,13 @@
|
|||
set(SOURCES
|
||||
DisassemblyModel.cpp
|
||||
main.cpp
|
||||
IndividualSampleModel.cpp
|
||||
Profile.cpp
|
||||
IndividualSampleModel.cpp
|
||||
Process.cpp
|
||||
ProcessPickerWidget.cpp
|
||||
Profile.cpp
|
||||
ProfileModel.cpp
|
||||
ProfileTimelineWidget.cpp
|
||||
SamplesModel.cpp
|
||||
ProfileTimelineWidget.cpp
|
||||
SamplesModel.cpp
|
||||
)
|
||||
|
||||
serenity_app(Profiler ICON app-profiler)
|
||||
|
|
|
@ -48,10 +48,14 @@ DisassemblyModel::DisassemblyModel(Profile& profile, ProfileNode& node)
|
|||
kernel_elf = make<ELF::Image>((const u8*)m_kernel_file->data(), m_kernel_file->size());
|
||||
elf = kernel_elf.ptr();
|
||||
} else {
|
||||
// FIXME: This is kinda rickety looking with all the -> -> ->
|
||||
auto library_data = node.process(profile)->library_metadata->library_containing(node.address());
|
||||
auto process = node.process(profile, node.timestamp());
|
||||
if (!process) {
|
||||
dbgln("no process for address {}", node.address());
|
||||
return;
|
||||
}
|
||||
auto library_data = process->library_metadata.library_containing(node.address());
|
||||
if (!library_data) {
|
||||
dbgln("no library data");
|
||||
dbgln("no library data for address {}", node.address());
|
||||
return;
|
||||
}
|
||||
elf = &library_data->object->elf;
|
||||
|
|
119
Userland/DevTools/Profiler/Process.cpp
Normal file
119
Userland/DevTools/Profiler/Process.cpp
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Process.h"
|
||||
|
||||
Thread* Process::find_thread(pid_t tid, u64 timestamp)
|
||||
{
|
||||
auto it = threads.find(tid);
|
||||
if (it == threads.end())
|
||||
return nullptr;
|
||||
for (auto& thread : it->value) {
|
||||
if (thread.start_valid < timestamp && (thread.end_valid == 0 || thread.end_valid > timestamp))
|
||||
return &thread;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Process::handle_thread_create(pid_t tid, u64 timestamp)
|
||||
{
|
||||
auto it = threads.find(tid);
|
||||
if (it == threads.end()) {
|
||||
threads.set(tid, {});
|
||||
it = threads.find(tid);
|
||||
}
|
||||
|
||||
auto thread = Thread { tid, timestamp, 0 };
|
||||
it->value.append(move(thread));
|
||||
}
|
||||
|
||||
void Process::handle_thread_exit(pid_t tid, u64 timestamp)
|
||||
{
|
||||
auto* thread = find_thread(tid, timestamp);
|
||||
if (!thread)
|
||||
return;
|
||||
thread->end_valid = timestamp;
|
||||
}
|
||||
|
||||
HashMap<String, OwnPtr<MappedObject>> g_mapped_object_cache;
|
||||
|
||||
static MappedObject* get_or_create_mapped_object(const String& path)
|
||||
{
|
||||
if (auto it = g_mapped_object_cache.find(path); it != g_mapped_object_cache.end())
|
||||
return it->value.ptr();
|
||||
|
||||
auto file_or_error = MappedFile::map(path);
|
||||
if (file_or_error.is_error()) {
|
||||
g_mapped_object_cache.set(path, {});
|
||||
return nullptr;
|
||||
}
|
||||
auto elf = ELF::Image(file_or_error.value()->bytes());
|
||||
if (!elf.is_valid()) {
|
||||
g_mapped_object_cache.set(path, {});
|
||||
return nullptr;
|
||||
}
|
||||
auto new_mapped_object = adopt_own(*new MappedObject {
|
||||
.file = file_or_error.release_value(),
|
||||
.elf = elf,
|
||||
});
|
||||
auto* ptr = new_mapped_object.ptr();
|
||||
g_mapped_object_cache.set(path, move(new_mapped_object));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void LibraryMetadata::handle_mmap(FlatPtr base, size_t size, const String& name)
|
||||
{
|
||||
String path;
|
||||
if (name.contains("Loader.so"))
|
||||
path = "Loader.so";
|
||||
else if (!name.contains(":"))
|
||||
return;
|
||||
else
|
||||
path = name.substring(0, name.view().find_first_of(":").value());
|
||||
|
||||
String full_path;
|
||||
if (name.contains(".so"))
|
||||
full_path = String::formatted("/usr/lib/{}", path);
|
||||
else
|
||||
full_path = path;
|
||||
|
||||
auto* mapped_object = get_or_create_mapped_object(full_path);
|
||||
if (!mapped_object) {
|
||||
full_path = String::formatted("/usr/local/lib/{}", path);
|
||||
mapped_object = get_or_create_mapped_object(full_path);
|
||||
if (!mapped_object)
|
||||
return;
|
||||
}
|
||||
|
||||
FlatPtr text_base {};
|
||||
mapped_object->elf.for_each_program_header([&](const ELF::Image::ProgramHeader& ph) {
|
||||
if (ph.is_executable())
|
||||
text_base = ph.vaddr().get();
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
m_libraries.set(name, adopt_own(*new Library { base, size, name, text_base, mapped_object }));
|
||||
}
|
||||
|
||||
String LibraryMetadata::Library::symbolicate(FlatPtr ptr, u32* offset) const
|
||||
{
|
||||
if (!object)
|
||||
return String::formatted("?? <{:p}>", ptr);
|
||||
|
||||
return object->elf.symbolicate(ptr - base + text_base, offset);
|
||||
}
|
||||
|
||||
const LibraryMetadata::Library* LibraryMetadata::library_containing(FlatPtr ptr) const
|
||||
{
|
||||
for (auto& it : m_libraries) {
|
||||
if (!it.value)
|
||||
continue;
|
||||
auto& library = *it.value;
|
||||
if (ptr >= library.base && ptr < (library.base + library.size))
|
||||
return &library;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
68
Userland/DevTools/Profiler/Process.h
Normal file
68
Userland/DevTools/Profiler/Process.h
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/MappedFile.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibELF/Image.h>
|
||||
|
||||
struct MappedObject {
|
||||
NonnullRefPtr<MappedFile> file;
|
||||
ELF::Image elf;
|
||||
};
|
||||
|
||||
extern HashMap<String, OwnPtr<MappedObject>> g_mapped_object_cache;
|
||||
|
||||
class LibraryMetadata {
|
||||
public:
|
||||
struct Library {
|
||||
FlatPtr base;
|
||||
size_t size;
|
||||
String name;
|
||||
FlatPtr text_base;
|
||||
MappedObject* object { nullptr };
|
||||
|
||||
String symbolicate(FlatPtr, u32* offset) const;
|
||||
};
|
||||
|
||||
void handle_mmap(FlatPtr base, size_t size, const String& name);
|
||||
const Library* library_containing(FlatPtr) const;
|
||||
|
||||
private:
|
||||
mutable HashMap<String, OwnPtr<Library>> m_libraries;
|
||||
};
|
||||
|
||||
struct Thread {
|
||||
pid_t tid;
|
||||
u64 start_valid;
|
||||
u64 end_valid { 0 };
|
||||
|
||||
bool valid_at(u64 timestamp) const
|
||||
{
|
||||
return timestamp >= start_valid && (end_valid == 0 || timestamp <= end_valid);
|
||||
}
|
||||
};
|
||||
|
||||
struct Process {
|
||||
pid_t pid {};
|
||||
String executable;
|
||||
HashMap<int, Vector<Thread>> threads {};
|
||||
LibraryMetadata library_metadata {};
|
||||
u64 start_valid;
|
||||
u64 end_valid { 0 };
|
||||
|
||||
Thread* find_thread(pid_t tid, u64 timestamp);
|
||||
void handle_thread_create(pid_t tid, u64 timestamp);
|
||||
void handle_thread_exit(pid_t tid, u64 timestamp);
|
||||
|
||||
bool valid_at(u64 timestamp) const
|
||||
{
|
||||
return timestamp >= start_valid && (end_valid == 0 || timestamp <= end_valid);
|
||||
}
|
||||
};
|
48
Userland/DevTools/Profiler/ProcessPickerWidget.cpp
Normal file
48
Userland/DevTools/Profiler/ProcessPickerWidget.cpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Gunnar Beutner <gunnar@beutner.name>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ProcessPickerWidget.h"
|
||||
#include "Profile.h"
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/ItemListModel.h>
|
||||
#include <LibGUI/Label.h>
|
||||
|
||||
ProcessPickerWidget::ProcessPickerWidget(Profile& profile)
|
||||
: m_profile(profile)
|
||||
{
|
||||
set_layout<GUI::HorizontalBoxLayout>();
|
||||
set_fixed_height(30);
|
||||
|
||||
set_frame_shape(Gfx::FrameShape::NoFrame);
|
||||
|
||||
auto& label = add<GUI::Label>("Process:");
|
||||
label.set_fixed_width(50);
|
||||
label.set_text_alignment(Gfx::TextAlignment::CenterRight);
|
||||
|
||||
m_process_combo = add<GUI::ComboBox>();
|
||||
m_process_combo->set_only_allow_values_from_model(true);
|
||||
|
||||
m_processes.append("All processes");
|
||||
|
||||
for (auto& process : m_profile.processes())
|
||||
m_processes.append(String::formatted("{}: {}", process.pid, process.executable));
|
||||
|
||||
m_process_combo->set_model(*GUI::ItemListModel<String>::create(m_processes));
|
||||
m_process_combo->set_selected_index(0);
|
||||
m_process_combo->on_change = [this](auto&, const GUI::ModelIndex& index) {
|
||||
if (index.row() == 0) {
|
||||
m_profile.clear_process_filter();
|
||||
} else {
|
||||
auto& process = m_profile.processes()[index.row() - 1];
|
||||
auto end_valid = process.end_valid == 0 ? m_profile.last_timestamp() : process.end_valid;
|
||||
m_profile.set_process_filter(process.pid, process.start_valid, end_valid);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ProcessPickerWidget::~ProcessPickerWidget()
|
||||
{
|
||||
}
|
31
Userland/DevTools/Profiler/ProcessPickerWidget.h
Normal file
31
Userland/DevTools/Profiler/ProcessPickerWidget.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Gunnar Beutner <gunnar@beutner.name>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/ComboBox.h>
|
||||
#include <LibGUI/Frame.h>
|
||||
|
||||
class Profile;
|
||||
|
||||
class ProcessPickerWidget final : public GUI::Frame {
|
||||
C_OBJECT(ProcessPickerWidget)
|
||||
public:
|
||||
virtual ~ProcessPickerWidget() override;
|
||||
|
||||
struct Process {
|
||||
pid_t pid;
|
||||
String name;
|
||||
};
|
||||
|
||||
private:
|
||||
explicit ProcessPickerWidget(Profile&);
|
||||
|
||||
Profile& m_profile;
|
||||
Vector<String> m_processes;
|
||||
|
||||
RefPtr<GUI::ComboBox> m_process_combo;
|
||||
};
|
|
@ -11,6 +11,7 @@
|
|||
#include <AK/HashTable.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/MappedFile.h>
|
||||
#include <AK/NonnullOwnPtrVector.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <LibCore/File.h>
|
||||
|
@ -60,12 +61,10 @@ GUI::Model& Profile::samples_model()
|
|||
|
||||
void Profile::rebuild_tree()
|
||||
{
|
||||
u32 filtered_event_count = 0;
|
||||
Vector<NonnullRefPtr<ProfileNode>> roots;
|
||||
|
||||
auto find_or_create_root = [&roots](FlyString object_name, String symbol, u32 address, u32 offset, u64 timestamp, pid_t pid) -> ProfileNode& {
|
||||
for (size_t i = 0; i < roots.size(); ++i) {
|
||||
auto& root = roots[i];
|
||||
for (auto root : roots) {
|
||||
if (root->symbol() == symbol) {
|
||||
return root;
|
||||
}
|
||||
|
@ -84,18 +83,23 @@ void Profile::rebuild_tree()
|
|||
live_allocations.remove(event.ptr);
|
||||
});
|
||||
|
||||
Optional<size_t> first_filtered_event_index;
|
||||
m_filtered_event_indices.clear();
|
||||
|
||||
for (size_t event_index = 0; event_index < m_events.size(); ++event_index) {
|
||||
auto& event = m_events.at(event_index);
|
||||
|
||||
if (has_timestamp_filter_range()) {
|
||||
auto timestamp = event.timestamp;
|
||||
if (timestamp < m_timestamp_filter_range_start || timestamp > m_timestamp_filter_range_end)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first_filtered_event_index.has_value())
|
||||
first_filtered_event_index = event_index;
|
||||
if (has_process_filter()) {
|
||||
if (event.pid != m_process_filter_pid || event.timestamp < m_process_filter_start_valid || event.timestamp > m_process_filter_end_valid)
|
||||
continue;
|
||||
}
|
||||
|
||||
m_filtered_event_indices.append(event_index);
|
||||
|
||||
if (event.type == "malloc" && !live_allocations.contains(event.ptr))
|
||||
continue;
|
||||
|
@ -130,9 +134,9 @@ void Profile::rebuild_tree()
|
|||
|
||||
// FIXME: More cheating with intentional mixing of TID/PID here:
|
||||
if (!node)
|
||||
node = &find_or_create_root(object_name, symbol, address, offset, event.timestamp, event.tid);
|
||||
node = &find_or_create_root(object_name, symbol, address, offset, event.timestamp, event.pid);
|
||||
else
|
||||
node = &node->find_or_create_child(object_name, symbol, address, offset, event.timestamp, event.tid);
|
||||
node = &node->find_or_create_child(object_name, symbol, address, offset, event.timestamp, event.pid);
|
||||
|
||||
node->increment_event_count();
|
||||
if (is_innermost_frame) {
|
||||
|
@ -156,11 +160,11 @@ void Profile::rebuild_tree()
|
|||
|
||||
// FIXME: More PID/TID mixing cheats here:
|
||||
if (!node) {
|
||||
node = &find_or_create_root(object_name, symbol, address, offset, event.timestamp, event.tid);
|
||||
node = &find_or_create_root(object_name, symbol, address, offset, event.timestamp, event.pid);
|
||||
root = node;
|
||||
root->will_track_seen_events(m_events.size());
|
||||
} else {
|
||||
node = &node->find_or_create_child(object_name, symbol, address, offset, event.timestamp, event.tid);
|
||||
node = &node->find_or_create_child(object_name, symbol, address, offset, event.timestamp, event.pid);
|
||||
}
|
||||
|
||||
if (!root->has_seen_event(event_index)) {
|
||||
|
@ -177,14 +181,10 @@ void Profile::rebuild_tree()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
++filtered_event_count;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -201,45 +201,6 @@ Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const St
|
|||
|
||||
auto& object = json.value().as_object();
|
||||
|
||||
auto processes_value = object.get("processes");
|
||||
if (processes_value.is_null())
|
||||
return String { "Invalid perfcore format (no processes)" };
|
||||
|
||||
if (!processes_value.is_array())
|
||||
return String { "Invalid perfcore format (processes is not an array)" };
|
||||
|
||||
Vector<Process> sampled_processes;
|
||||
|
||||
for (auto& process_value : processes_value.as_array().values()) {
|
||||
if (!process_value.is_object())
|
||||
return String { "Invalid perfcore format (process value is not an object)" };
|
||||
auto& process = process_value.as_object();
|
||||
auto regions_value = process.get("regions");
|
||||
if (!regions_value.is_array())
|
||||
return String { "Invalid perfcore format (regions is not an array)" };
|
||||
|
||||
Process sampled_process {
|
||||
.pid = (pid_t)process.get("pid").to_i32(),
|
||||
.executable = process.get("executable").to_string(),
|
||||
.threads = {},
|
||||
.regions = {},
|
||||
.library_metadata = make<LibraryMetadata>(regions_value.as_array()),
|
||||
};
|
||||
|
||||
for (auto& region_value : regions_value.as_array().values()) {
|
||||
if (!region_value.is_object())
|
||||
return String { "Invalid perfcore format (region is not an object)" };
|
||||
auto& region = region_value.as_object();
|
||||
sampled_process.regions.append(Process::Region {
|
||||
.name = region.get("name").to_string(),
|
||||
.base = region.get("base").to_u32(),
|
||||
.size = region.get("size").to_u32(),
|
||||
});
|
||||
}
|
||||
|
||||
sampled_processes.append(move(sampled_process));
|
||||
}
|
||||
|
||||
auto file_or_error = MappedFile::map("/boot/Kernel");
|
||||
OwnPtr<ELF::Image> kernel_elf;
|
||||
if (!file_or_error.is_error())
|
||||
|
@ -250,9 +211,9 @@ Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const St
|
|||
return String { "Malformed profile (events is not an array)" };
|
||||
|
||||
auto& perf_events = events_value.as_array();
|
||||
if (perf_events.is_empty())
|
||||
return String { "No events captured (targeted process was never on CPU)" };
|
||||
|
||||
NonnullOwnPtrVector<Process> all_processes;
|
||||
HashMap<pid_t, Process*> current_processes;
|
||||
Vector<Event> events;
|
||||
|
||||
for (auto& perf_event_value : perf_events.values()) {
|
||||
|
@ -262,6 +223,7 @@ Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const St
|
|||
|
||||
event.timestamp = perf_event.get("timestamp").to_number<u64>();
|
||||
event.type = perf_event.get("type").to_string();
|
||||
event.pid = perf_event.get("pid").to_i32();
|
||||
event.tid = perf_event.get("tid").to_i32();
|
||||
|
||||
if (event.type == "malloc") {
|
||||
|
@ -269,6 +231,65 @@ Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const St
|
|||
event.size = perf_event.get("size").to_number<size_t>();
|
||||
} else if (event.type == "free") {
|
||||
event.ptr = perf_event.get("ptr").to_number<FlatPtr>();
|
||||
} else if (event.type == "mmap") {
|
||||
event.ptr = perf_event.get("ptr").to_number<FlatPtr>();
|
||||
event.size = perf_event.get("size").to_number<size_t>();
|
||||
event.name = perf_event.get("name").to_string();
|
||||
|
||||
auto it = current_processes.find(event.pid);
|
||||
if (it != current_processes.end())
|
||||
it->value->library_metadata.handle_mmap(event.ptr, event.size, event.name);
|
||||
continue;
|
||||
} else if (event.type == "munmap") {
|
||||
event.ptr = perf_event.get("ptr").to_number<FlatPtr>();
|
||||
event.size = perf_event.get("size").to_number<size_t>();
|
||||
continue;
|
||||
} else if (event.type == "process_create") {
|
||||
event.parent_pid = perf_event.get("parent_pid").to_number<FlatPtr>();
|
||||
event.executable = perf_event.get("executable").to_string();
|
||||
|
||||
auto sampled_process = adopt_own(*new Process {
|
||||
.pid = event.pid,
|
||||
.executable = event.executable,
|
||||
.start_valid = event.timestamp,
|
||||
});
|
||||
|
||||
current_processes.set(sampled_process->pid, sampled_process);
|
||||
all_processes.append(move(sampled_process));
|
||||
continue;
|
||||
} else if (event.type == "process_exec") {
|
||||
event.executable = perf_event.get("executable").to_string();
|
||||
|
||||
auto old_process = current_processes.get(event.pid).value();
|
||||
old_process->end_valid = event.timestamp - 1;
|
||||
|
||||
current_processes.remove(event.pid);
|
||||
|
||||
auto sampled_process = adopt_own(*new Process {
|
||||
.pid = event.pid,
|
||||
.executable = event.executable,
|
||||
.start_valid = event.timestamp });
|
||||
|
||||
current_processes.set(sampled_process->pid, sampled_process);
|
||||
all_processes.append(move(sampled_process));
|
||||
continue;
|
||||
} else if (event.type == "process_exit") {
|
||||
auto old_process = current_processes.get(event.pid).value();
|
||||
old_process->end_valid = event.timestamp - 1;
|
||||
|
||||
current_processes.remove(event.pid);
|
||||
continue;
|
||||
} else if (event.type == "thread_create") {
|
||||
event.parent_tid = perf_event.get("parent_tid").to_i32();
|
||||
auto it = current_processes.find(event.pid);
|
||||
if (it != current_processes.end())
|
||||
it->value->handle_thread_create(event.tid, event.timestamp);
|
||||
continue;
|
||||
} else if (event.type == "thread_exit") {
|
||||
auto it = current_processes.find(event.pid);
|
||||
if (it != current_processes.end())
|
||||
it->value->handle_thread_exit(event.tid, event.timestamp);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto stack_array = perf_event.get("stack").as_array();
|
||||
|
@ -283,22 +304,19 @@ Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const St
|
|||
if (kernel_elf) {
|
||||
symbol = kernel_elf->symbolicate(ptr, &offset);
|
||||
} else {
|
||||
symbol = "??";
|
||||
symbol = String::formatted("?? <{:p}>", ptr);
|
||||
}
|
||||
} else {
|
||||
auto it = sampled_processes.find_if([&](auto& entry) {
|
||||
// FIXME: This doesn't support multi-threaded programs!
|
||||
return entry.pid == event.tid;
|
||||
});
|
||||
auto it = current_processes.find(event.pid);
|
||||
// FIXME: This logic is kinda gnarly, find a way to clean it up.
|
||||
LibraryMetadata* library_metadata {};
|
||||
if (!it.is_end())
|
||||
library_metadata = it->library_metadata.ptr();
|
||||
if (it != current_processes.end())
|
||||
library_metadata = &it->value->library_metadata;
|
||||
if (auto* library = library_metadata ? library_metadata->library_containing(ptr) : nullptr) {
|
||||
object_name = library->name;
|
||||
symbol = library->symbolicate(ptr, &offset);
|
||||
} else {
|
||||
symbol = "??";
|
||||
symbol = String::formatted("?? <{:p}>", ptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,7 +332,21 @@ Result<NonnullOwnPtr<Profile>, String> Profile::load_from_perfcore_file(const St
|
|||
events.append(move(event));
|
||||
}
|
||||
|
||||
return adopt_own(*new Profile(move(sampled_processes), move(events)));
|
||||
if (events.is_empty())
|
||||
return String { "No events captured (targeted process was never on CPU)" };
|
||||
|
||||
quick_sort(all_processes, [](auto& a, auto& b) {
|
||||
if (a.pid == b.pid)
|
||||
return a.start_valid < b.start_valid;
|
||||
else
|
||||
return a.pid < b.pid;
|
||||
});
|
||||
|
||||
Vector<Process> processes;
|
||||
for (auto& it : all_processes)
|
||||
processes.append(move(it));
|
||||
|
||||
return adopt_own(*new Profile(move(processes), move(events)));
|
||||
}
|
||||
|
||||
void ProfileNode::sort_children()
|
||||
|
@ -344,6 +376,32 @@ void Profile::clear_timestamp_filter_range()
|
|||
m_samples_model->update();
|
||||
}
|
||||
|
||||
void Profile::set_process_filter(pid_t pid, u64 start_valid, u64 end_valid)
|
||||
{
|
||||
if (m_has_process_filter && m_process_filter_pid == pid && m_process_filter_start_valid == start_valid && m_process_filter_end_valid == end_valid)
|
||||
return;
|
||||
m_has_process_filter = true;
|
||||
|
||||
m_process_filter_pid = pid;
|
||||
m_process_filter_start_valid = start_valid;
|
||||
m_process_filter_end_valid = end_valid;
|
||||
|
||||
rebuild_tree();
|
||||
if (m_disassembly_model)
|
||||
m_disassembly_model->update();
|
||||
m_samples_model->update();
|
||||
}
|
||||
void Profile::clear_process_filter()
|
||||
{
|
||||
if (!m_has_process_filter)
|
||||
return;
|
||||
m_has_process_filter = false;
|
||||
rebuild_tree();
|
||||
if (m_disassembly_model)
|
||||
m_disassembly_model->update();
|
||||
m_samples_model->update();
|
||||
}
|
||||
|
||||
void Profile::set_inverted(bool inverted)
|
||||
{
|
||||
if (m_inverted == inverted)
|
||||
|
@ -381,87 +439,6 @@ GUI::Model* Profile::disassembly_model()
|
|||
return m_disassembly_model;
|
||||
}
|
||||
|
||||
HashMap<String, OwnPtr<MappedObject>> g_mapped_object_cache;
|
||||
|
||||
static MappedObject* get_or_create_mapped_object(const String& path)
|
||||
{
|
||||
if (auto it = g_mapped_object_cache.find(path); it != g_mapped_object_cache.end())
|
||||
return it->value.ptr();
|
||||
|
||||
auto file_or_error = MappedFile::map(path);
|
||||
if (file_or_error.is_error()) {
|
||||
g_mapped_object_cache.set(path, {});
|
||||
return nullptr;
|
||||
}
|
||||
auto elf = ELF::Image(file_or_error.value()->bytes());
|
||||
if (!elf.is_valid()) {
|
||||
g_mapped_object_cache.set(path, {});
|
||||
return nullptr;
|
||||
}
|
||||
auto new_mapped_object = adopt_own(*new MappedObject {
|
||||
.file = file_or_error.release_value(),
|
||||
.elf = move(elf),
|
||||
});
|
||||
auto* ptr = new_mapped_object.ptr();
|
||||
g_mapped_object_cache.set(path, move(new_mapped_object));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
LibraryMetadata::LibraryMetadata(JsonArray regions)
|
||||
: m_regions(move(regions))
|
||||
{
|
||||
for (auto& region_value : m_regions.values()) {
|
||||
auto& region = region_value.as_object();
|
||||
auto base = region.get("base").as_u32();
|
||||
auto size = region.get("size").as_u32();
|
||||
auto name = region.get("name").as_string();
|
||||
|
||||
String path;
|
||||
if (name.contains("Loader.so"))
|
||||
path = "Loader.so";
|
||||
else if (!name.contains(":"))
|
||||
continue;
|
||||
else
|
||||
path = name.substring(0, name.view().find_first_of(":").value());
|
||||
|
||||
if (name.contains(".so"))
|
||||
path = String::formatted("/usr/lib/{}", path);
|
||||
|
||||
auto* mapped_object = get_or_create_mapped_object(path);
|
||||
if (!mapped_object)
|
||||
continue;
|
||||
|
||||
FlatPtr text_base {};
|
||||
mapped_object->elf.for_each_program_header([&](const ELF::Image::ProgramHeader& ph) {
|
||||
if (ph.is_executable())
|
||||
text_base = ph.vaddr().get();
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
m_libraries.set(name, adopt_own(*new Library { base, size, name, text_base, mapped_object }));
|
||||
}
|
||||
}
|
||||
|
||||
String LibraryMetadata::Library::symbolicate(FlatPtr ptr, u32* offset) const
|
||||
{
|
||||
if (!object)
|
||||
return "??"sv;
|
||||
|
||||
return object->elf.symbolicate(ptr - base + text_base, offset);
|
||||
}
|
||||
|
||||
const LibraryMetadata::Library* LibraryMetadata::library_containing(FlatPtr ptr) const
|
||||
{
|
||||
for (auto& it : m_libraries) {
|
||||
if (!it.value)
|
||||
continue;
|
||||
auto& library = *it.value;
|
||||
if (ptr >= library.base && ptr < (library.base + library.size))
|
||||
return &library;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ProfileNode::ProfileNode(const String& object_name, String symbol, u32 address, u32 offset, u64 timestamp, pid_t pid)
|
||||
: m_symbol(move(symbol))
|
||||
, m_pid(pid)
|
||||
|
@ -478,7 +455,7 @@ ProfileNode::ProfileNode(const String& object_name, String symbol, u32 address,
|
|||
m_object_name = LexicalPath(object).basename();
|
||||
}
|
||||
|
||||
const Process* ProfileNode::process(Profile& profile) const
|
||||
const Process* ProfileNode::process(Profile& profile, u64 timestamp) const
|
||||
{
|
||||
return profile.find_process(m_pid);
|
||||
return profile.find_process(m_pid, timestamp);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "Process.h"
|
||||
#include <AK/Bitmap.h>
|
||||
#include <AK/FlyString.h>
|
||||
#include <AK/JsonArray.h>
|
||||
|
@ -24,49 +25,6 @@ class Profile;
|
|||
class ProfileModel;
|
||||
class SamplesModel;
|
||||
|
||||
struct MappedObject {
|
||||
NonnullRefPtr<MappedFile> file;
|
||||
ELF::Image elf;
|
||||
};
|
||||
|
||||
extern HashMap<String, OwnPtr<MappedObject>> g_mapped_object_cache;
|
||||
|
||||
class LibraryMetadata {
|
||||
public:
|
||||
explicit LibraryMetadata(JsonArray regions);
|
||||
|
||||
struct Library {
|
||||
FlatPtr base;
|
||||
size_t size;
|
||||
String name;
|
||||
FlatPtr text_base;
|
||||
MappedObject* object { nullptr };
|
||||
|
||||
String symbolicate(FlatPtr, u32* offset) const;
|
||||
};
|
||||
|
||||
const Library* library_containing(FlatPtr) const;
|
||||
|
||||
private:
|
||||
mutable HashMap<String, OwnPtr<Library>> m_libraries;
|
||||
JsonArray m_regions;
|
||||
};
|
||||
|
||||
struct Process {
|
||||
pid_t pid {};
|
||||
String executable;
|
||||
HashTable<int> threads;
|
||||
|
||||
struct Region {
|
||||
String name;
|
||||
FlatPtr base {};
|
||||
size_t size {};
|
||||
};
|
||||
Vector<Region> regions;
|
||||
|
||||
NonnullOwnPtr<LibraryMetadata> library_metadata;
|
||||
};
|
||||
|
||||
class ProfileNode : public RefCounted<ProfileNode> {
|
||||
public:
|
||||
static NonnullRefPtr<ProfileNode> create(FlyString object_name, String symbol, u32 address, u32 offset, u64 timestamp, pid_t pid)
|
||||
|
@ -137,7 +95,7 @@ public:
|
|||
|
||||
pid_t pid() const { return m_pid; }
|
||||
|
||||
const Process* process(Profile&) const;
|
||||
const Process* process(Profile&, u64 timestamp) const;
|
||||
|
||||
private:
|
||||
explicit ProfileNode(const String& object_name, String symbol, u32 address, u32 offset, u64 timestamp, pid_t);
|
||||
|
@ -165,10 +123,10 @@ public:
|
|||
GUI::Model& samples_model();
|
||||
GUI::Model* disassembly_model();
|
||||
|
||||
const Process* find_process(pid_t pid) const
|
||||
const Process* find_process(pid_t pid, u64 timestamp) const
|
||||
{
|
||||
auto it = m_processes.find_if([&](auto& entry) {
|
||||
return entry.pid == pid;
|
||||
return entry.pid == pid && entry.valid_at(timestamp);
|
||||
});
|
||||
return it.is_end() ? nullptr : &(*it);
|
||||
}
|
||||
|
@ -189,15 +147,18 @@ public:
|
|||
String type;
|
||||
FlatPtr ptr { 0 };
|
||||
size_t size { 0 };
|
||||
String name;
|
||||
int parent_pid { 0 };
|
||||
int parent_tid { 0 };
|
||||
String executable;
|
||||
int pid { 0 };
|
||||
int tid { 0 };
|
||||
bool in_kernel { false };
|
||||
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; }
|
||||
const Vector<size_t>& filtered_event_indices() const { return m_filtered_event_indices; }
|
||||
|
||||
u64 length_in_ms() const { return m_last_timestamp - m_first_timestamp; }
|
||||
u64 first_timestamp() const { return m_first_timestamp; }
|
||||
|
@ -208,6 +169,10 @@ public:
|
|||
void clear_timestamp_filter_range();
|
||||
bool has_timestamp_filter_range() const { return m_has_timestamp_filter_range; }
|
||||
|
||||
void set_process_filter(pid_t pid, u64 start_valid, u64 end_valid);
|
||||
void clear_process_filter();
|
||||
bool has_process_filter() const { return m_has_process_filter; }
|
||||
|
||||
bool is_inverted() const { return m_inverted; }
|
||||
void set_inverted(bool);
|
||||
|
||||
|
@ -216,6 +181,8 @@ public:
|
|||
bool show_percentages() const { return m_show_percentages; }
|
||||
void set_show_percentages(bool);
|
||||
|
||||
const Vector<Process>& processes() const { return m_processes; }
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_event_in_filter_range(Callback callback)
|
||||
{
|
||||
|
@ -241,8 +208,7 @@ private:
|
|||
GUI::ModelIndex m_disassembly_index;
|
||||
|
||||
Vector<NonnullRefPtr<ProfileNode>> m_roots;
|
||||
u32 m_filtered_event_count { 0 };
|
||||
size_t m_first_filtered_event_index { 0 };
|
||||
Vector<size_t> m_filtered_event_indices;
|
||||
u64 m_first_timestamp { 0 };
|
||||
u64 m_last_timestamp { 0 };
|
||||
|
||||
|
@ -253,6 +219,11 @@ private:
|
|||
u64 m_timestamp_filter_range_start { 0 };
|
||||
u64 m_timestamp_filter_range_end { 0 };
|
||||
|
||||
bool m_has_process_filter { false };
|
||||
pid_t m_process_filter_pid { 0 };
|
||||
u64 m_process_filter_start_valid { 0 };
|
||||
u64 m_process_filter_end_valid { 0 };
|
||||
|
||||
u32 m_deepest_stack_depth { 0 };
|
||||
bool m_inverted { false };
|
||||
bool m_show_top_functions { false };
|
||||
|
|
|
@ -108,12 +108,12 @@ GUI::Variant ProfileModel::data(const GUI::ModelIndex& index, GUI::ModelRole rol
|
|||
if (role == GUI::ModelRole::Display) {
|
||||
if (index.column() == Column::SampleCount) {
|
||||
if (m_profile.show_percentages())
|
||||
return ((float)node->event_count() / (float)m_profile.filtered_event_count()) * 100.0f;
|
||||
return ((float)node->event_count() / (float)m_profile.filtered_event_indices().size()) * 100.0f;
|
||||
return node->event_count();
|
||||
}
|
||||
if (index.column() == Column::SelfCount) {
|
||||
if (m_profile.show_percentages())
|
||||
return ((float)node->self_count() / (float)m_profile.filtered_event_count()) * 100.0f;
|
||||
return ((float)node->self_count() / (float)m_profile.filtered_event_indices().size()) * 100.0f;
|
||||
return node->self_count();
|
||||
}
|
||||
if (index.column() == Column::ObjectName)
|
||||
|
|
|
@ -22,7 +22,7 @@ SamplesModel::~SamplesModel()
|
|||
|
||||
int SamplesModel::row_count(const GUI::ModelIndex&) const
|
||||
{
|
||||
return m_profile.filtered_event_count();
|
||||
return m_profile.filtered_event_indices().size();
|
||||
}
|
||||
|
||||
int SamplesModel::column_count(const GUI::ModelIndex&) const
|
||||
|
@ -37,6 +37,8 @@ String SamplesModel::column_name(int column) const
|
|||
return "#";
|
||||
case Column::Timestamp:
|
||||
return "Timestamp";
|
||||
case Column::ProcessID:
|
||||
return "PID";
|
||||
case Column::ThreadID:
|
||||
return "TID";
|
||||
case Column::ExecutableName:
|
||||
|
@ -50,7 +52,7 @@ String SamplesModel::column_name(int column) const
|
|||
|
||||
GUI::Variant SamplesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
u32 event_index = m_profile.first_filtered_event_index() + index.row();
|
||||
u32 event_index = m_profile.filtered_event_indices()[index.row()];
|
||||
auto& event = m_profile.events().at(event_index);
|
||||
|
||||
if (role == GUI::ModelRole::Custom) {
|
||||
|
@ -61,12 +63,14 @@ GUI::Variant SamplesModel::data(const GUI::ModelIndex& index, GUI::ModelRole rol
|
|||
if (index.column() == Column::SampleIndex)
|
||||
return event_index;
|
||||
|
||||
if (index.column() == Column::ProcessID)
|
||||
return event.pid;
|
||||
|
||||
if (index.column() == Column::ThreadID)
|
||||
return event.tid;
|
||||
|
||||
if (index.column() == Column::ExecutableName) {
|
||||
// FIXME: More abuse of the PID/TID relationship:
|
||||
if (auto* process = m_profile.find_process(event.tid))
|
||||
if (auto* process = m_profile.find_process(event.pid, event.timestamp))
|
||||
return process->executable;
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ public:
|
|||
enum Column {
|
||||
SampleIndex,
|
||||
Timestamp,
|
||||
ProcessID,
|
||||
ThreadID,
|
||||
ExecutableName,
|
||||
InnermostStackFrame,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
#include "IndividualSampleModel.h"
|
||||
#include "ProcessPickerWidget.h"
|
||||
#include "Profile.h"
|
||||
#include "ProfileTimelineWidget.h"
|
||||
#include <LibCore/ArgsParser.h>
|
||||
|
@ -86,6 +87,7 @@ int main(int argc, char** argv)
|
|||
main_widget.set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
main_widget.add<ProfileTimelineWidget>(*profile);
|
||||
main_widget.add<ProcessPickerWidget>(*profile);
|
||||
|
||||
auto& tab_widget = main_widget.add<GUI::TabWidget>();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue