mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-20 11:36:10 +00:00
LibWeb+LibWebView+WebContent: Add an about:processes page
The intent is that this will replace the separate Task Manager window. This will allow us to more easily add features such as actual process management, better rendering of the process table, etc. Included in this page is the ability to sort table rows. This also lays the ground work for more internal `about` pages, such as about:config.
This commit is contained in:
parent
9dcbf5562a
commit
843209c6a9
Notes:
github-actions[bot]
2025-03-19 14:04:39 +00:00
Author: https://github.com/trflynn89 Commit: https://github.com/LadybirdBrowser/ladybird/commit/843209c6a96 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3967 Reviewed-by: https://github.com/shannonbooth
21 changed files with 322 additions and 3 deletions
175
Base/res/ladybird/about-pages/processes.html
Normal file
175
Base/res/ladybird/about-pages/processes.html
Normal file
|
@ -0,0 +1,175 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Task Manager</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--table-border: gray;
|
||||
--table-row-odd: rgb(57, 57, 57);
|
||||
--table-row-hover: rgb(80, 79, 79);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--table-border: gray;
|
||||
--table-row-odd: rgb(229, 229, 229);
|
||||
--table-row-hover: rgb(199, 198, 198);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
|
||||
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
th:hover {
|
||||
background-color: var(--table-row-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th.sorted-ascending:after {
|
||||
content: " ▲";
|
||||
}
|
||||
|
||||
th.sorted-descending:after {
|
||||
content: " ▼";
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 4px;
|
||||
border: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
tbody tr:nth-of-type(2n + 1) {
|
||||
background-color: var(--table-row-odd);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: var(--table-row-hover);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="name">Name</th>
|
||||
<th id="pid">PID</th>
|
||||
<th id="cpu">CPU</th>
|
||||
<th id="memory">Memory</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="process-table"></tbody>
|
||||
</table>
|
||||
|
||||
<script type="text/javascript">
|
||||
const cpuFormatter = new Intl.NumberFormat([], {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
const memoryFormatter = new Intl.NumberFormat([], {
|
||||
style: "unit",
|
||||
unit: "byte",
|
||||
notation: "compact",
|
||||
unitDisplay: "narrow",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
const Direction = Object.freeze({
|
||||
ascending: 1,
|
||||
descending: 2,
|
||||
});
|
||||
|
||||
processes.processes = [];
|
||||
processes.sortDirection = Direction.ascending;
|
||||
processes.sortKey = "pid";
|
||||
|
||||
const renderSortedProcesses = () => {
|
||||
document.querySelectorAll("th").forEach(header => {
|
||||
header.classList.remove("sorted-ascending");
|
||||
header.classList.remove("sorted-descending");
|
||||
});
|
||||
|
||||
if (processes.sortDirection === Direction.ascending) {
|
||||
document.getElementById(processes.sortKey).classList.add("sorted-ascending");
|
||||
} else {
|
||||
document.getElementById(processes.sortKey).classList.add("sorted-descending");
|
||||
}
|
||||
|
||||
const multiplier = processes.sortDirection === Direction.ascending ? 1 : -1;
|
||||
|
||||
processes.processes.sort((lhs, rhs) => {
|
||||
const lhsValue = lhs[processes.sortKey];
|
||||
const rhsValue = rhs[processes.sortKey];
|
||||
|
||||
if (typeof lhsValue === "string") {
|
||||
return multiplier * lhsValue.localeCompare(rhsValue);
|
||||
}
|
||||
|
||||
return multiplier * (lhsValue - rhsValue);
|
||||
});
|
||||
|
||||
let oldTable = document.getElementById("process-table");
|
||||
|
||||
let newTable = document.createElement("tbody");
|
||||
newTable.setAttribute("id", "process-table");
|
||||
|
||||
const insertColumn = (row, value) => {
|
||||
let column = row.insertCell();
|
||||
column.innerText = value;
|
||||
};
|
||||
|
||||
processes.processes.forEach(process => {
|
||||
let row = newTable.insertRow();
|
||||
insertColumn(row, process.name);
|
||||
insertColumn(row, process.pid);
|
||||
insertColumn(row, cpuFormatter.format(process.cpu));
|
||||
insertColumn(row, memoryFormatter.format(process.memory));
|
||||
});
|
||||
|
||||
oldTable.parentNode.replaceChild(newTable, oldTable);
|
||||
};
|
||||
|
||||
processes.loadProcessStatistics = statistics => {
|
||||
processes.processes = JSON.parse(statistics);
|
||||
renderSortedProcesses();
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll("th").forEach(header => {
|
||||
header.addEventListener("click", () => {
|
||||
processes.sortDirection = header.classList.contains("sorted-descending")
|
||||
? Direction.ascending
|
||||
: Direction.descending;
|
||||
processes.sortKey = header.getAttribute("id");
|
||||
|
||||
renderSortedProcesses();
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
processes.updateProcessStatistics();
|
||||
}, 1000);
|
||||
|
||||
processes.updateProcessStatistics();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -563,6 +563,7 @@ set(SOURCES
|
|||
Internals/InternalAnimationTimeline.cpp
|
||||
Internals/Internals.cpp
|
||||
Internals/InternalsBase.cpp
|
||||
Internals/Processes.cpp
|
||||
IntersectionObserver/IntersectionObserver.cpp
|
||||
IntersectionObserver/IntersectionObserverEntry.cpp
|
||||
Layout/AudioBox.cpp
|
||||
|
|
|
@ -624,6 +624,7 @@ class RequestList;
|
|||
|
||||
namespace Web::Internals {
|
||||
class Internals;
|
||||
class Processes;
|
||||
}
|
||||
|
||||
namespace Web::IntersectionObserver {
|
||||
|
|
|
@ -79,7 +79,7 @@ void WindowEnvironmentSettingsObject::setup(Page& page, URL::URL const& creation
|
|||
|
||||
// Non-Standard: We cannot fully initialize window object until *after* the we set up
|
||||
// the realm's [[HostDefined]] internal slot as the internal slot contains the web platform intrinsics
|
||||
MUST(window.initialize_web_interfaces({}));
|
||||
MUST(window.initialize_web_interfaces({}, creation_url));
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:responsible-document
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
|
||||
#include <LibWeb/Infra/CharacterTypes.h>
|
||||
#include <LibWeb/Internals/Internals.h>
|
||||
#include <LibWeb/Internals/Processes.h>
|
||||
#include <LibWeb/Layout/Viewport.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
#include <LibWeb/Painting/PaintableBox.h>
|
||||
|
@ -721,7 +722,7 @@ void Window::set_internals_object_exposed(bool exposed)
|
|||
s_internals_object_exposed = exposed;
|
||||
}
|
||||
|
||||
WebIDL::ExceptionOr<void> Window::initialize_web_interfaces(Badge<WindowEnvironmentSettingsObject>)
|
||||
WebIDL::ExceptionOr<void> Window::initialize_web_interfaces(Badge<WindowEnvironmentSettingsObject>, URL::URL const& url)
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
add_window_exposed_interfaces(*this);
|
||||
|
@ -734,6 +735,14 @@ WebIDL::ExceptionOr<void> Window::initialize_web_interfaces(Badge<WindowEnvironm
|
|||
if (s_internals_object_exposed)
|
||||
define_direct_property("internals", realm.create<Internals::Internals>(realm), JS::default_attributes);
|
||||
|
||||
if (url.scheme() == "about"sv && url.paths().size() == 1) {
|
||||
auto const& path = url.paths().first();
|
||||
|
||||
if (path == "processes"sv) {
|
||||
define_direct_property("processes", realm.create<Internals::Processes>(realm), JS::default_attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ public:
|
|||
// https://html.spec.whatwg.org/multipage/interaction.html#history-action-activation
|
||||
bool has_history_action_activation() const;
|
||||
|
||||
WebIDL::ExceptionOr<void> initialize_web_interfaces(Badge<WindowEnvironmentSettingsObject>);
|
||||
WebIDL::ExceptionOr<void> initialize_web_interfaces(Badge<WindowEnvironmentSettingsObject>, URL::URL const&);
|
||||
|
||||
Vector<GC::Ref<Plugin>> pdf_viewer_plugin_objects();
|
||||
Vector<GC::Ref<MimeType>> pdf_viewer_mime_type_objects();
|
||||
|
|
35
Libraries/LibWeb/Internals/Processes.cpp
Normal file
35
Libraries/LibWeb/Internals/Processes.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Bindings/ProcessesPrototype.h>
|
||||
#include <LibWeb/Internals/Processes.h>
|
||||
#include <LibWeb/Page/Page.h>
|
||||
|
||||
namespace Web::Internals {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(Processes);
|
||||
|
||||
Processes::Processes(JS::Realm& realm)
|
||||
: InternalsBase(realm)
|
||||
{
|
||||
}
|
||||
|
||||
Processes::~Processes() = default;
|
||||
|
||||
void Processes::initialize(JS::Realm& realm)
|
||||
{
|
||||
Base::initialize(realm);
|
||||
WEB_SET_PROTOTYPE_FOR_INTERFACE(Processes);
|
||||
}
|
||||
|
||||
void Processes::update_process_statistics()
|
||||
{
|
||||
page().client().update_process_statistics();
|
||||
}
|
||||
|
||||
}
|
28
Libraries/LibWeb/Internals/Processes.h
Normal file
28
Libraries/LibWeb/Internals/Processes.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/Internals/InternalsBase.h>
|
||||
|
||||
namespace Web::Internals {
|
||||
|
||||
class Processes final : public InternalsBase {
|
||||
WEB_PLATFORM_OBJECT(Processes, InternalsBase);
|
||||
GC_DECLARE_ALLOCATOR(Processes);
|
||||
|
||||
public:
|
||||
virtual ~Processes() override;
|
||||
|
||||
void update_process_statistics();
|
||||
|
||||
private:
|
||||
explicit Processes(JS::Realm&);
|
||||
|
||||
virtual void initialize(JS::Realm&) override;
|
||||
};
|
||||
|
||||
}
|
4
Libraries/LibWeb/Internals/Processes.idl
Normal file
4
Libraries/LibWeb/Internals/Processes.idl
Normal file
|
@ -0,0 +1,4 @@
|
|||
[Exposed=Nobody]
|
||||
interface Processes {
|
||||
undefined updateProcessStatistics();
|
||||
};
|
|
@ -400,6 +400,8 @@ public:
|
|||
|
||||
virtual void page_did_mutate_dom([[maybe_unused]] FlyString const& type, [[maybe_unused]] DOM::Node const& target, [[maybe_unused]] DOM::NodeList& added_nodes, [[maybe_unused]] DOM::NodeList& removed_nodes, [[maybe_unused]] GC::Ptr<DOM::Node> previous_sibling, [[maybe_unused]] GC::Ptr<DOM::Node> next_sibling, [[maybe_unused]] Optional<String> const& attribute_name) { }
|
||||
|
||||
virtual void update_process_statistics() { }
|
||||
|
||||
virtual bool is_ready_to_paint() const = 0;
|
||||
|
||||
virtual DisplayListPlayerType display_list_player_type() const = 0;
|
||||
|
|
|
@ -266,6 +266,7 @@ libweb_js_bindings(IndexedDB/IDBTransaction)
|
|||
libweb_js_bindings(IndexedDB/IDBVersionChangeEvent)
|
||||
libweb_js_bindings(Internals/InternalAnimationTimeline)
|
||||
libweb_js_bindings(Internals/Internals)
|
||||
libweb_js_bindings(Internals/Processes)
|
||||
libweb_js_bindings(IntersectionObserver/IntersectionObserver)
|
||||
libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry)
|
||||
libweb_js_bindings(MathML/MathMLElement)
|
||||
|
|
|
@ -331,6 +331,19 @@ String Application::generate_process_statistics_html()
|
|||
return m_process_manager.generate_html();
|
||||
}
|
||||
|
||||
void Application::send_updated_process_statistics_to_view(ViewImplementation& view)
|
||||
{
|
||||
m_process_manager.update_all_process_statistics();
|
||||
auto statistics = m_process_manager.serialize_json();
|
||||
|
||||
StringBuilder builder;
|
||||
builder.append("processes.loadProcessStatistics(\""sv);
|
||||
builder.append_escaped_for_json(statistics);
|
||||
builder.append("\");"sv);
|
||||
|
||||
view.run_javascript(MUST(builder.to_string()));
|
||||
}
|
||||
|
||||
void Application::process_did_exit(Process&& process)
|
||||
{
|
||||
if (m_in_shutdown)
|
||||
|
|
|
@ -60,6 +60,8 @@ public:
|
|||
void update_process_statistics();
|
||||
String generate_process_statistics_html();
|
||||
|
||||
void send_updated_process_statistics_to_view(ViewImplementation&);
|
||||
|
||||
ErrorOr<LexicalPath> path_for_downloaded_file(StringView file) const;
|
||||
|
||||
enum class DevtoolsState {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/JsonArraySerializer.h>
|
||||
#include <AK/JsonObjectSerializer.h>
|
||||
#include <AK/NumberFormat.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
|
@ -202,4 +204,33 @@ String ProcessManager::generate_html()
|
|||
return builder.to_string_without_validation();
|
||||
}
|
||||
|
||||
String ProcessManager::serialize_json()
|
||||
{
|
||||
Threading::MutexLocker locker { m_lock };
|
||||
|
||||
StringBuilder builder;
|
||||
auto serializer = MUST(JsonArraySerializer<>::try_create(builder));
|
||||
|
||||
m_statistics.for_each_process([&](auto const& process) {
|
||||
auto& process_handle = find_process(process.pid).value();
|
||||
|
||||
auto type = WebView::process_name_from_type(process_handle.type());
|
||||
auto const& title = process_handle.title();
|
||||
|
||||
auto process_name = title.has_value()
|
||||
? MUST(String::formatted("{} - {}", type, *title))
|
||||
: String::from_utf8_without_validation(type.bytes());
|
||||
|
||||
auto object = MUST(serializer.add_object());
|
||||
MUST(object.add("name"sv, move(process_name)));
|
||||
MUST(object.add("pid"sv, process.pid));
|
||||
MUST(object.add("cpu"sv, process.cpu_percent));
|
||||
MUST(object.add("memory"sv, process.memory_usage_bytes));
|
||||
MUST(object.finish());
|
||||
});
|
||||
|
||||
MUST(serializer.finish());
|
||||
return MUST(builder.to_string());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ public:
|
|||
|
||||
void update_all_process_statistics();
|
||||
String generate_html();
|
||||
String serialize_json();
|
||||
|
||||
Function<void(Process&&)> on_process_exited;
|
||||
|
||||
|
|
|
@ -690,6 +690,12 @@ Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request
|
|||
return IPC::File {};
|
||||
}
|
||||
|
||||
void WebContentClient::update_process_statistics(u64 page_id)
|
||||
{
|
||||
if (auto view = view_for_page_id(page_id); view.has_value())
|
||||
WebView::Application::the().send_updated_process_statistics_to_view(*view);
|
||||
}
|
||||
|
||||
Optional<ViewImplementation&> WebContentClient::view_for_page_id(u64 page_id, SourceLocation location)
|
||||
{
|
||||
// Don't bother logging anything for the spare WebContent process. It will only receive a load notification for about:blank.
|
||||
|
|
|
@ -129,6 +129,7 @@ private:
|
|||
virtual void did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) override;
|
||||
virtual void did_allocate_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::ShareableBitmap, i32 back_bitmap_id, Gfx::ShareableBitmap) override;
|
||||
virtual Messages::WebContentClient::RequestWorkerAgentResponse request_worker_agent(u64 page_id) override;
|
||||
virtual void update_process_statistics(u64 page_id) override;
|
||||
|
||||
Optional<ViewImplementation&> view_for_page_id(u64, SourceLocation = SourceLocation::current());
|
||||
|
||||
|
|
|
@ -699,6 +699,11 @@ void PageClient::page_did_mutate_dom(FlyString const& type, Web::DOM::Node const
|
|||
client().async_did_mutate_dom(m_id, { type.to_string(), target.unique_id(), move(serialized_target), mutation.release_value() });
|
||||
}
|
||||
|
||||
void PageClient::update_process_statistics()
|
||||
{
|
||||
client().async_update_process_statistics(m_id);
|
||||
}
|
||||
|
||||
ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_path)
|
||||
{
|
||||
VERIFY(!m_webdriver);
|
||||
|
|
|
@ -174,6 +174,7 @@ private:
|
|||
virtual void page_did_allocate_backing_stores(i32 front_bitmap_id, Gfx::ShareableBitmap front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap back_bitmap) override;
|
||||
virtual IPC::File request_worker_agent() override;
|
||||
virtual void page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr<Web::DOM::Node> previous_sibling, GC::Ptr<Web::DOM::Node> next_sibling, Optional<String> const& attribute_name) override;
|
||||
virtual void update_process_statistics() override;
|
||||
|
||||
Web::Layout::Viewport* layout_root();
|
||||
void setup_palette();
|
||||
|
|
|
@ -108,4 +108,6 @@ endpoint WebContentClient
|
|||
did_find_in_page(u64 page_id, size_t current_match_index, Optional<size_t> total_match_count) =|
|
||||
|
||||
request_worker_agent(u64 page_id) => (IPC::File socket) // FIXME: Add required attributes to select a SharedWorker Agent
|
||||
|
||||
update_process_statistics(u64 page_id) =|
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ list(TRANSFORM BROWSER_ICONS PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/icons/brow
|
|||
set(ABOUT_PAGES
|
||||
about.html
|
||||
newtab.html
|
||||
processes.html
|
||||
)
|
||||
set(WEB_TEMPLATES
|
||||
directory.html
|
||||
|
|
Loading…
Add table
Reference in a new issue