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
parent 9dcbf5562a
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/Internals.cpp
Internals/InternalsBase.cpp
Internals/Processes.cpp
IntersectionObserver/IntersectionObserver.cpp
IntersectionObserver/IntersectionObserverEntry.cpp
Layout/AudioBox.cpp

View file

@ -624,6 +624,7 @@ class RequestList;
namespace Web::Internals {
class Internals;
class Processes;
}
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
// 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

View file

@ -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 {};
}

View file

@ -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();

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 update_process_statistics() { }
virtual bool is_ready_to_paint() 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(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)

View file

@ -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)

View file

@ -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 {

View file

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

View file

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

View file

@ -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.

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_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());

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

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

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) =|
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
about.html
newtab.html
processes.html
)
set(WEB_TEMPLATES
directory.html