Serenity Operating System
1/*
2 * Copyright (c) 2021, Stephan Unverwerth <s.unverwerth@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "SourceModel.h"
8#include "Gradient.h"
9#include "Profile.h"
10#include <LibDebug/DebugInfo.h>
11#include <LibGfx/Font/FontDatabase.h>
12#include <LibSymbolication/Symbolication.h>
13#include <stdio.h>
14
15namespace Profiler {
16
17class SourceFile final {
18public:
19 struct Line {
20 DeprecatedString content;
21 size_t num_samples { 0 };
22 };
23
24 static constexpr StringView source_root_path = "/usr/src/serenity/"sv;
25
26 SourceFile(StringView filename)
27 {
28 DeprecatedString source_file_name = filename.replace("../../"sv, source_root_path, ReplaceMode::FirstOnly);
29
30 auto try_read_lines = [&]() -> ErrorOr<void> {
31 auto unbuffered_file = TRY(Core::File::open(source_file_name, Core::File::OpenMode::Read));
32 auto file = TRY(Core::BufferedFile::create(move(unbuffered_file)));
33
34 Array<u8, 1024> buffer;
35 while (!file->is_eof())
36 m_lines.append({ TRY(file->read_line(buffer)), 0 });
37
38 return {};
39 };
40
41 auto maybe_error = try_read_lines();
42 if (maybe_error.is_error()) {
43 dbgln("Could not map source file \"{}\". Tried {}. {} (errno={})", filename, source_file_name, maybe_error.error().string_literal(), maybe_error.error().code());
44 return;
45 }
46 }
47
48 void try_add_samples(size_t line, size_t samples)
49 {
50 if (line < 1 || line - 1 >= m_lines.size())
51 return;
52
53 m_lines[line - 1].num_samples += samples;
54 }
55
56 Vector<Line> const& lines() const { return m_lines; }
57
58private:
59 Vector<Line> m_lines;
60};
61
62SourceModel::SourceModel(Profile& profile, ProfileNode& node)
63 : m_profile(profile)
64 , m_node(node)
65{
66 FlatPtr base_address = 0;
67 Debug::DebugInfo const* debug_info;
68 if (auto maybe_kernel_base = Symbolication::kernel_base(); maybe_kernel_base.has_value() && m_node.address() >= *maybe_kernel_base) {
69 if (!g_kernel_debuginfo_object.has_value())
70 return;
71 base_address = maybe_kernel_base.release_value();
72 if (g_kernel_debug_info == nullptr)
73 g_kernel_debug_info = make<Debug::DebugInfo>(g_kernel_debuginfo_object->elf, DeprecatedString::empty(), base_address);
74 debug_info = g_kernel_debug_info.ptr();
75 } else {
76 auto const& process = node.process();
77 auto const* library_data = process.library_metadata.library_containing(node.address());
78 if (!library_data) {
79 dbgln("no library data for address {:p}", node.address());
80 return;
81 }
82 base_address = library_data->base;
83 debug_info = &library_data->load_debug_info(base_address);
84 }
85
86 VERIFY(debug_info != nullptr);
87
88 // Try to read all source files contributing to the selected function and aggregate the samples by line.
89 HashMap<DeprecatedString, SourceFile> source_files;
90 for (auto const& pair : node.events_per_address()) {
91 auto position = debug_info->get_source_position(pair.key - base_address);
92 if (position.has_value()) {
93 auto it = source_files.find(position.value().file_path);
94 if (it == source_files.end()) {
95 source_files.set(position.value().file_path, SourceFile(position.value().file_path));
96 it = source_files.find(position.value().file_path);
97 }
98
99 it->value.try_add_samples(position.value().line_number, pair.value);
100 }
101 }
102
103 // Process source file map and turn content into view model
104 for (auto const& file_iterator : source_files) {
105 u32 line_number = 0;
106 for (auto const& line_iterator : file_iterator.value.lines()) {
107 line_number++;
108
109 m_source_lines.append({
110 (u32)line_iterator.num_samples,
111 line_iterator.num_samples * 100.0f / node.event_count(),
112 file_iterator.key,
113 line_number,
114 line_iterator.content,
115 });
116 }
117 }
118}
119
120int SourceModel::row_count(GUI::ModelIndex const&) const
121{
122 return m_source_lines.size();
123}
124
125DeprecatedString SourceModel::column_name(int column) const
126{
127 switch (column) {
128 case Column::SampleCount:
129 return m_profile.show_percentages() ? "% Samples" : "# Samples";
130 case Column::SourceCode:
131 return "Source Code";
132 case Column::Location:
133 return "Location";
134 case Column::LineNumber:
135 return "Line";
136 default:
137 VERIFY_NOT_REACHED();
138 return {};
139 }
140}
141
142struct ColorPair {
143 Color background;
144 Color foreground;
145};
146
147static Optional<ColorPair> color_pair_for(SourceLineData const& line)
148{
149 if (line.percent == 0)
150 return {};
151
152 Color background = color_for_percent(line.percent);
153 Color foreground;
154 if (line.percent > 50)
155 foreground = Color::White;
156 else
157 foreground = Color::Black;
158 return ColorPair { background, foreground };
159}
160
161GUI::Variant SourceModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const
162{
163 auto const& line = m_source_lines[index.row()];
164
165 if (role == GUI::ModelRole::BackgroundColor) {
166 auto colors = color_pair_for(line);
167 if (!colors.has_value())
168 return {};
169 return colors.value().background;
170 }
171
172 if (role == GUI::ModelRole::ForegroundColor) {
173 auto colors = color_pair_for(line);
174 if (!colors.has_value())
175 return {};
176 return colors.value().foreground;
177 }
178
179 if (role == GUI::ModelRole::Font) {
180 if (index.column() == Column::SourceCode)
181 return Gfx::FontDatabase::default_fixed_width_font();
182 return {};
183 }
184
185 if (role == GUI::ModelRole::Display) {
186 if (index.column() == Column::SampleCount) {
187 if (m_profile.show_percentages())
188 return ((float)line.event_count / (float)m_node.event_count()) * 100.0f;
189 return line.event_count;
190 }
191
192 if (index.column() == Column::Location)
193 return line.location;
194
195 if (index.column() == Column::LineNumber)
196 return line.line_number;
197
198 if (index.column() == Column::SourceCode)
199 return line.source_code;
200
201 return {};
202 }
203 return {};
204}
205
206}