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:
Timothy Flynn 2025-03-16 10:49:28 -04:00 committed by Tim Flynn
commit 843209c6a9
Notes: github-actions[bot] 2025-03-19 14:04:39 +00:00
21 changed files with 322 additions and 3 deletions

View 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>

View file

@ -563,6 +563,7 @@ set(SOURCES
Internals/InternalAnimationTimeline.cpp Internals/InternalAnimationTimeline.cpp
Internals/Internals.cpp Internals/Internals.cpp
Internals/InternalsBase.cpp Internals/InternalsBase.cpp
Internals/Processes.cpp
IntersectionObserver/IntersectionObserver.cpp IntersectionObserver/IntersectionObserver.cpp
IntersectionObserver/IntersectionObserverEntry.cpp IntersectionObserver/IntersectionObserverEntry.cpp
Layout/AudioBox.cpp Layout/AudioBox.cpp

View file

@ -624,6 +624,7 @@ class RequestList;
namespace Web::Internals { namespace Web::Internals {
class Internals; class Internals;
class Processes;
} }
namespace Web::IntersectionObserver { namespace Web::IntersectionObserver {

View file

@ -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 // 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 // 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 // https://html.spec.whatwg.org/multipage/window-object.html#script-settings-for-window-objects:responsible-document

View file

@ -60,6 +60,7 @@
#include <LibWeb/HighResolutionTime/TimeOrigin.h> #include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/CharacterTypes.h> #include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Internals/Internals.h> #include <LibWeb/Internals/Internals.h>
#include <LibWeb/Internals/Processes.h>
#include <LibWeb/Layout/Viewport.h> #include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Page/Page.h> #include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h> #include <LibWeb/Painting/PaintableBox.h>
@ -721,7 +722,7 @@ void Window::set_internals_object_exposed(bool exposed)
s_internals_object_exposed = 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(); auto& realm = this->realm();
add_window_exposed_interfaces(*this); add_window_exposed_interfaces(*this);
@ -734,6 +735,14 @@ WebIDL::ExceptionOr<void> Window::initialize_web_interfaces(Badge<WindowEnvironm
if (s_internals_object_exposed) if (s_internals_object_exposed)
define_direct_property("internals", realm.create<Internals::Internals>(realm), JS::default_attributes); 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 {}; return {};
} }

View file

@ -148,7 +148,7 @@ public:
// https://html.spec.whatwg.org/multipage/interaction.html#history-action-activation // https://html.spec.whatwg.org/multipage/interaction.html#history-action-activation
bool has_history_action_activation() const; 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<Plugin>> pdf_viewer_plugin_objects();
Vector<GC::Ref<MimeType>> pdf_viewer_mime_type_objects(); Vector<GC::Ref<MimeType>> pdf_viewer_mime_type_objects();

View 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();
}
}

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

View file

@ -0,0 +1,4 @@
[Exposed=Nobody]
interface Processes {
undefined updateProcessStatistics();
};

View file

@ -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 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 bool is_ready_to_paint() const = 0;
virtual DisplayListPlayerType display_list_player_type() const = 0; virtual DisplayListPlayerType display_list_player_type() const = 0;

View file

@ -266,6 +266,7 @@ libweb_js_bindings(IndexedDB/IDBTransaction)
libweb_js_bindings(IndexedDB/IDBVersionChangeEvent) libweb_js_bindings(IndexedDB/IDBVersionChangeEvent)
libweb_js_bindings(Internals/InternalAnimationTimeline) libweb_js_bindings(Internals/InternalAnimationTimeline)
libweb_js_bindings(Internals/Internals) libweb_js_bindings(Internals/Internals)
libweb_js_bindings(Internals/Processes)
libweb_js_bindings(IntersectionObserver/IntersectionObserver) libweb_js_bindings(IntersectionObserver/IntersectionObserver)
libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry) libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry)
libweb_js_bindings(MathML/MathMLElement) libweb_js_bindings(MathML/MathMLElement)

View file

@ -331,6 +331,19 @@ String Application::generate_process_statistics_html()
return m_process_manager.generate_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) void Application::process_did_exit(Process&& process)
{ {
if (m_in_shutdown) if (m_in_shutdown)

View file

@ -60,6 +60,8 @@ public:
void update_process_statistics(); void update_process_statistics();
String generate_process_statistics_html(); String generate_process_statistics_html();
void send_updated_process_statistics_to_view(ViewImplementation&);
ErrorOr<LexicalPath> path_for_downloaded_file(StringView file) const; ErrorOr<LexicalPath> path_for_downloaded_file(StringView file) const;
enum class DevtoolsState { enum class DevtoolsState {

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <AK/JsonArraySerializer.h>
#include <AK/JsonObjectSerializer.h>
#include <AK/NumberFormat.h> #include <AK/NumberFormat.h>
#include <AK/String.h> #include <AK/String.h>
#include <LibCore/EventLoop.h> #include <LibCore/EventLoop.h>
@ -202,4 +204,33 @@ String ProcessManager::generate_html()
return builder.to_string_without_validation(); 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());
}
} }

View file

@ -37,6 +37,7 @@ public:
void update_all_process_statistics(); void update_all_process_statistics();
String generate_html(); String generate_html();
String serialize_json();
Function<void(Process&&)> on_process_exited; Function<void(Process&&)> on_process_exited;

View file

@ -690,6 +690,12 @@ Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request
return IPC::File {}; 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) 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. // Don't bother logging anything for the spare WebContent process. It will only receive a load notification for about:blank.

View file

@ -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_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 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 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()); Optional<ViewImplementation&> view_for_page_id(u64, SourceLocation = SourceLocation::current());

View file

@ -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() }); 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) ErrorOr<void> PageClient::connect_to_webdriver(ByteString const& webdriver_ipc_path)
{ {
VERIFY(!m_webdriver); VERIFY(!m_webdriver);

View file

@ -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 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 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 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(); Web::Layout::Viewport* layout_root();
void setup_palette(); void setup_palette();

View file

@ -108,4 +108,6 @@ endpoint WebContentClient
did_find_in_page(u64 page_id, size_t current_match_index, Optional<size_t> total_match_count) =| 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 request_worker_agent(u64 page_id) => (IPC::File socket) // FIXME: Add required attributes to select a SharedWorker Agent
update_process_statistics(u64 page_id) =|
} }

View file

@ -64,6 +64,7 @@ list(TRANSFORM BROWSER_ICONS PREPEND "${LADYBIRD_SOURCE_DIR}/Base/res/icons/brow
set(ABOUT_PAGES set(ABOUT_PAGES
about.html about.html
newtab.html newtab.html
processes.html
) )
set(WEB_TEMPLATES set(WEB_TEMPLATES
directory.html directory.html