diff --git a/Source/WebCore/loader/DocumentThreadableLoader.cpp b/Source/WebCore/loader/DocumentThreadableLoader.cpp index ac87c3f70e0a..6db064aee550 100644 --- a/Source/WebCore/loader/DocumentThreadableLoader.cpp +++ b/Source/WebCore/loader/DocumentThreadableLoader.cpp @@ -474,6 +474,12 @@ void DocumentThreadableLoader::notifyFinished(CachedResource& resource, const Ne ASSERT(m_client); ASSERT_UNUSED(resource, &resource == m_resource); +#if PLATFORM(QT) + LocalFrame* localFrame = document().frame(); + if (localFrame) + localFrame->loader().client().dispatchDidFinishResourceLoad(resource); +#endif + if (m_resource->errorOccurred()) didFail(m_resource->resourceLoaderIdentifier(), m_resource->resourceError()); else @@ -595,6 +601,13 @@ void DocumentThreadableLoader::loadRequest(ResourceRequest&& request, SecurityCh auto cachedResource = m_document->protectedCachedResourceLoader()->requestRawResource(WTFMove(newRequest)); m_resource = cachedResource.value_or(nullptr); + +#if PLATFORM(QT) + LocalFrame* localFrame = document().frame(); + if (localFrame && m_resource.get()) + localFrame->loader().client().dispatchDidStartResourceLoad(*m_resource.get()); +#endif + if (CachedResourceHandle resource = m_resource) resource->addClient(*this); else diff --git a/Source/WebCore/loader/ImageLoader.cpp b/Source/WebCore/loader/ImageLoader.cpp index 88b8bd2cf7e3..dc2604f78d6b 100644 --- a/Source/WebCore/loader/ImageLoader.cpp +++ b/Source/WebCore/loader/ImageLoader.cpp @@ -469,6 +469,12 @@ void ImageLoader::notifyFinished(CachedResource& resource, const NetworkLoadMetr { LOG_WITH_STREAM(LazyLoading, stream << "ImageLoader " << this << " notifyFinished - hasPendingLoadEvent " << m_hasPendingLoadEvent); +#if PLATFORM(QT) + LocalFrame* localFrame = element().document().frame(); + if (localFrame) + localFrame->loader().client().dispatchDidFinishResourceLoad(resource); +#endif + ASSERT(m_failedLoadURL.isEmpty()); ASSERT_UNUSED(resource, &resource == m_image.get()); @@ -690,6 +696,12 @@ void ImageLoader::dispatchPendingBeforeLoadEvent() return; if (!element().document().hasLivingRenderTree()) return; +#if PLATFORM(QT) + LocalFrame* localFrame = element().document().frame(); + if (localFrame) + localFrame->loader().client().dispatchDidStartResourceLoad(*m_image.get()); +#endif + m_hasPendingBeforeLoadEvent = false; if (!element().isConnected()) return; diff --git a/Source/WebCore/loader/LocalFrameLoaderClient.h b/Source/WebCore/loader/LocalFrameLoaderClient.h index 843cce5a11a9..26d88e2eb8a7 100644 --- a/Source/WebCore/loader/LocalFrameLoaderClient.h +++ b/Source/WebCore/loader/LocalFrameLoaderClient.h @@ -41,6 +41,10 @@ #include #include +#if PLATFORM(QT) +#include "CachedResource.h" +#endif + #if ENABLE(APPLICATION_MANIFEST) #include "ApplicationManifest.h" #endif @@ -160,6 +164,10 @@ class WEBCORE_EXPORT LocalFrameLoaderClient : public FrameLoaderClient { virtual void dispatchDidFinishLoading(DocumentLoader*, IsMainResourceLoad, ResourceLoaderIdentifier) = 0; virtual void dispatchDidFailLoading(DocumentLoader*, IsMainResourceLoad, ResourceLoaderIdentifier, const ResourceError&) = 0; virtual bool dispatchDidLoadResourceFromMemoryCache(DocumentLoader*, const ResourceRequest&, const ResourceResponse&, int length) = 0; +#if PLATFORM(QT) + virtual void dispatchDidStartResourceLoad(const CachedResource&) { } + virtual void dispatchDidFinishResourceLoad(const CachedResource&) { } +#endif virtual void dispatchDidDispatchOnloadEvents() = 0; virtual void dispatchDidReceiveServerRedirectForProvisionalLoad() = 0; diff --git a/Source/WebKitLegacy/PlatformQt.cmake b/Source/WebKitLegacy/PlatformQt.cmake index cc96fa834975..d45ca8d7132f 100644 --- a/Source/WebKitLegacy/PlatformQt.cmake +++ b/Source/WebKitLegacy/PlatformQt.cmake @@ -215,6 +215,7 @@ set(QtWebKit_PUBLIC_FRAMEWORK_HEADERS qt/Api/qwebhistory.h qt/Api/qwebhistoryinterface.h qt/Api/qwebkitglobal.h + qt/Api/qwebresourcetypes.h qt/Api/qwebkitplatformplugin.h qt/Api/qwebpluginfactory.h qt/Api/qwebscriptworld.h @@ -238,6 +239,7 @@ ecm_generate_headers( QWebHistoryInterface QWebKitPlatformPlugin,QWebHapticFeedbackPlayer,QWebFullScreenVideoHandler,QWebNotificationData,QWebNotificationPresenter,QWebSelectData,QWebSelectMethod,QWebSpellChecker,QWebTouchModifier QWebPluginFactory + QWebResourceTypes QWebSecurityOrigin QWebSettings COMMON_HEADER diff --git a/Source/WebKitLegacy/qt/Api/qwebresourcetypes.h b/Source/WebKitLegacy/qt/Api/qwebresourcetypes.h new file mode 100644 index 000000000000..2a4262469d4e --- /dev/null +++ b/Source/WebKitLegacy/qt/Api/qwebresourcetypes.h @@ -0,0 +1,44 @@ +/* + Copyright (C) 2025 Michael Nutt + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef QWEBRESOURCETYPES_H +#define QWEBRESOURCETYPES_H + +#include +#include +#include + +// These types are duplicated from WebCore to provide a clean public API +// They have the same layout and field names so Qt signals/slots can pass them directly + +struct QWEBKIT_EXPORT QtResourceTimingInfo { + qint64 domainLookupMs = -1; + qint64 connectMs = -1; + qint64 sslMs = -1; + qint64 requestMs = -1; + qint64 responseMs = -1; + qint64 totalMs = 0; +}; + +struct QWEBKIT_EXPORT QtResourceRequestInfo { + QString initiatorType; + QString resourceType; +}; + +#endif // QWEBRESOURCETYPES_H \ No newline at end of file diff --git a/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.cpp b/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.cpp index 55efb0d96f81..0e3f1076ddb3 100644 --- a/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.cpp +++ b/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.cpp @@ -235,6 +235,11 @@ void FrameLoaderClientQt::setFrame(QWebFrameAdapter* webFrame, LocalFrame* frame connect(this, SIGNAL(titleChanged(QString)), m_webFrame->handle(), SIGNAL(titleChanged(QString))); + + connect(this, SIGNAL(resourceLoadStarted(const QUrl&, const QtResourceRequestInfo&, bool)), + m_webFrame->pageAdapter->handle(), SIGNAL(resourceLoadStarted(const QUrl&, const QtResourceRequestInfo&, bool))); + connect(this, SIGNAL(resourceLoadFinished(const QUrl, qint64, const QtResourceTimingInfo&, bool)), + m_webFrame->pageAdapter->handle(), SIGNAL(resourceLoadFinished(const QUrl&, qint64, const QtResourceTimingInfo&, bool))); } bool FrameLoaderClientQt::hasWebView() const @@ -1392,6 +1397,116 @@ RefPtr FrameLoaderClientQt::createHistoryItemTree(bool clipAtTarget return coreMainFrame->loader().history().createItemTree(*m_frame, clipAtTarget, itemID); } + +void FrameLoaderClientQt::dispatchDidStartResourceLoad(const WebCore::CachedResource& resource) +{ + QUrl qurl(resource.url()); + + bool isCoalesced = resource.numberOfClients() == 0; + + QtResourceRequestInfo requestInfo; + requestInfo.resourceType = resourceTypeToString(resource.type()); + requestInfo.initiatorType = resource.initiatorType().string(); + + Q_EMIT resourceLoadStarted(qurl, requestInfo, isCoalesced); +} + +void FrameLoaderClientQt::dispatchDidFinishResourceLoad(const WebCore::CachedResource& resource) +{ + QUrl qurl(resource.url()); + qint64 size = resource.encodedSize(); + bool success = (resource.status() != WebCore::CachedResource::Status::LoadError && + resource.status() != WebCore::CachedResource::Status::DecodeError); + + // Create timing info from resource + QtResourceTimingInfo timing; + if (auto networkMetrics = const_cast(resource).takeNetworkLoadMetrics()) { + timing = extractTimingInfo(*networkMetrics); + } else { + timing.totalMs = 0; // Fallback + } + + Q_EMIT resourceLoadFinished(qurl, size, timing, success); +} + +QString FrameLoaderClientQt::resourceTypeToString(WebCore::CachedResource::Type type) const +{ + switch (type) { + case WebCore::CachedResource::Type::MainResource: + return QStringLiteral("document"); + case WebCore::CachedResource::Type::ImageResource: + return QStringLiteral("image"); + case WebCore::CachedResource::Type::CSSStyleSheet: + return QStringLiteral("stylesheet"); + case WebCore::CachedResource::Type::Script: + return QStringLiteral("script"); + case WebCore::CachedResource::Type::FontResource: + return QStringLiteral("font"); + case WebCore::CachedResource::Type::SVGFontResource: + return QStringLiteral("svg-font"); + case WebCore::CachedResource::Type::MediaResource: + return QStringLiteral("media"); + case WebCore::CachedResource::Type::RawResource: + return QStringLiteral("xhr"); + case WebCore::CachedResource::Type::Icon: + return QStringLiteral("icon"); + case WebCore::CachedResource::Type::Beacon: + return QStringLiteral("beacon"); + case WebCore::CachedResource::Type::Ping: + return QStringLiteral("ping"); +#if ENABLE(XSLT) + case WebCore::CachedResource::Type::XSLStyleSheet: + return QStringLiteral("xsl"); +#endif + case WebCore::CachedResource::Type::LinkPrefetch: + return QStringLiteral("prefetch"); +#if ENABLE(VIDEO) + case WebCore::CachedResource::Type::TextTrackResource: + return QStringLiteral("track"); +#endif +#if ENABLE(APPLICATION_MANIFEST) + case WebCore::CachedResource::Type::ApplicationManifest: + return QStringLiteral("manifest"); +#endif + case WebCore::CachedResource::Type::SVGDocumentResource: + return QStringLiteral("svg"); + } + return QStringLiteral("unknown"); +} + +QtResourceTimingInfo FrameLoaderClientQt::extractTimingInfo(const WebCore::NetworkLoadMetrics& metrics) const +{ + QtResourceTimingInfo info; + + // Extract detailed timing breakdown from NetworkLoadMetrics (W3C Resource Timing API) + if (metrics.domainLookupStart.secondsSinceEpoch().value() > 0 && metrics.domainLookupEnd.secondsSinceEpoch().value() > 0) { + info.domainLookupMs = (metrics.domainLookupEnd - metrics.domainLookupStart).millisecondsAs(); + } + + if (metrics.connectStart.secondsSinceEpoch().value() > 0 && metrics.connectEnd.secondsSinceEpoch().value() > 0) { + info.connectMs = (metrics.connectEnd - metrics.connectStart).millisecondsAs(); + } + + if (metrics.secureConnectionStart.secondsSinceEpoch().value() > 0 && metrics.connectEnd.secondsSinceEpoch().value() > 0) { + info.sslMs = (metrics.connectEnd - metrics.secureConnectionStart).millisecondsAs(); + } + + if (metrics.requestStart.secondsSinceEpoch().value() > 0 && metrics.responseStart.secondsSinceEpoch().value() > 0) { + info.requestMs = (metrics.responseStart - metrics.requestStart).millisecondsAs(); + } + + if (metrics.responseStart.secondsSinceEpoch().value() > 0 && metrics.responseEnd.secondsSinceEpoch().value() > 0) { + info.responseMs = (metrics.responseEnd - metrics.responseStart).millisecondsAs(); + } + + // Calculate total timing + if (metrics.fetchStart.secondsSinceEpoch().value() > 0 && metrics.responseEnd.secondsSinceEpoch().value() > 0) { + info.totalMs = (metrics.responseEnd - metrics.fetchStart).millisecondsAs(); + } + + return info; +} + } #include "moc_FrameLoaderClientQt.cpp" diff --git a/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.h b/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.h index b949b4e2d8c1..f451359e042b 100644 --- a/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.h +++ b/Source/WebKitLegacy/qt/WebCoreSupport/FrameLoaderClientQt.h @@ -30,13 +30,16 @@ #ifndef FrameLoaderClientQt_h #define FrameLoaderClientQt_h +#include #include #include +#include #include #include #include #include +#include "qwebresourcetypes.h" #include #include @@ -68,6 +71,8 @@ class FrameLoaderClientQt final : public QObject, public LocalFrameLoaderClient Q_SIGNALS: void titleChanged(const QString& title); void unsupportedContent(QNetworkReply*); + void resourceLoadStarted(const QUrl& url, const QtResourceRequestInfo& requestInfo, bool fromCache); + void resourceLoadFinished(const QUrl& url, qint64 size, const QtResourceTimingInfo& timing, bool success); public: FrameLoaderClientQt(WebCore::FrameLoader&); @@ -97,6 +102,8 @@ class FrameLoaderClientQt final : public QObject, public LocalFrameLoaderClient void dispatchDidFinishLoading(WebCore::DocumentLoader*, WebCore::IsMainResourceLoad, WebCore::ResourceLoaderIdentifier) override; void dispatchDidFailLoading(WebCore::DocumentLoader*, WebCore::IsMainResourceLoad, WebCore::ResourceLoaderIdentifier, const WebCore::ResourceError&) override; bool dispatchDidLoadResourceFromMemoryCache(WebCore::DocumentLoader*, const WebCore::ResourceRequest&, const WebCore::ResourceResponse&, int) override; + void dispatchDidStartResourceLoad(const WebCore::CachedResource&) override; + void dispatchDidFinishResourceLoad(const WebCore::CachedResource&) override; void dispatchDidDispatchOnloadEvents() override; void dispatchDidReceiveServerRedirectForProvisionalLoad() override; @@ -246,6 +253,10 @@ private Q_SLOTS: // QTFIXME: consider introducing some sort of flags for storing state bool m_isDisplayingErrorPage; bool m_shouldSuppressLoadStarted; + + // Helper methods for resource tracking + QString resourceTypeToString(WebCore::CachedResource::Type type) const; + QtResourceTimingInfo extractTimingInfo(const WebCore::NetworkLoadMetrics& metrics) const; }; } diff --git a/Source/WebKitLegacy/qt/WidgetApi/qwebpage.cpp b/Source/WebKitLegacy/qt/WidgetApi/qwebpage.cpp index 8a1d927621f0..2740b8236b69 100644 --- a/Source/WebKitLegacy/qt/WidgetApi/qwebpage.cpp +++ b/Source/WebKitLegacy/qt/WidgetApi/qwebpage.cpp @@ -3439,6 +3439,31 @@ bool QWebPage::recentlyAudible() const \sa loadStarted(), ErrorPageExtension */ +/*! + \fn void QWebPage::resourceLoadStarted(const QUrl& url, const QtResourceRequestInfo& requestInfo, bool fromCache) + + This signal is emitted when a resource (image, stylesheet, script, etc.) begins loading. + \a url contains the URL of the resource being loaded. + \a requestInfo contains detailed information about the resource request including HTTP method, + headers, priority, and other request metadata. + \a fromCache indicates whether the resource is being loaded from memory cache. + + \sa resourceLoadFinished(), QtResourceRequestInfo +*/ + +/*! + \fn void QWebPage::resourceLoadFinished(const QUrl& url, qint64 size, const QtResourceTimingInfo& timing, bool success) + + This signal is emitted when a resource finishes loading (successfully or with error). + \a url contains the URL of the resource that finished loading. + \a size contains the actual size in bytes of the loaded resource. + \a timing contains detailed timing information about the resource load including + DNS lookup, connection, request/response times, and cache status. + \a success indicates whether the resource loaded successfully. + + \sa resourceLoadStarted(), QtResourceTimingInfo +*/ + /*! \fn void QWebPage::linkHovered(const QString &link, const QString &title, const QString &textContent) diff --git a/Source/WebKitLegacy/qt/WidgetApi/qwebpage.h b/Source/WebKitLegacy/qt/WidgetApi/qwebpage.h index 26add58205d0..999c7d57a84a 100644 --- a/Source/WebKitLegacy/qt/WidgetApi/qwebpage.h +++ b/Source/WebKitLegacy/qt/WidgetApi/qwebpage.h @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -67,6 +68,7 @@ namespace WebCore { class FrameLoadRequest; } + class QWEBKITWIDGETS_EXPORT QWebPage : public QObject { Q_OBJECT @@ -431,6 +433,8 @@ class QWEBKITWIDGETS_EXPORT QWebPage : public QObject { void loadStarted(); void loadProgress(int progress); void loadFinished(bool ok); + void resourceLoadStarted(const QUrl& url, const QtResourceRequestInfo& requestInfo, bool fromCache); + void resourceLoadFinished(const QUrl& url, qint64 size, const QtResourceTimingInfo& timing, bool success); void linkHovered(const QString &link, const QString &title, const QString &textContent); void statusBarMessage(const QString& text);