Files
Andrey Bocharnikov fee211d783 feat(StatusQ): account for the QML layer's HTTP traffic and cache it to disk
The QML engine gets a network access factory that hands out managers with a
disk cache and per-host accounting, so every image, animation and XHR the QML
layer issues can be seen and capped.

HttpStats is a GUI-thread singleton recording per-host totals split by whether
the bytes came from the network or from the cache. Bytes are taken from
downloadProgress rather than Content-Length, which is absent on some responses
and wrong on chunked ones. It is constructed on the GUI thread before any
factory worker can first-touch it with the wrong QObject affinity, and it
reports a finished cache clear on its own cacheCleared() signal — separate from
changed(), which fires on every reply and so cannot be what a reader hangs an
expensive re-measurement on.

The cache takes an explicit budget instead of Qt's 50 MB default, which a
single collectibles page can evict on its own.

The factory also sends Accept: image/webp,image/png,image/jpeg on Cloudinary
delivery URLs, so the f_auto in the rewritten media URLs has something to
resolve against. The list is narrow on purpose: f_auto returns AVIF or JPEG XL
to a client that claims them and Qt decodes neither. It is a statement about
this client's decoders, which is why it lives here and not in QML — see
docs/adr/0006-qt-http-cache.md.

All of it lives in networkaccessfactory.{h,cpp} rather than in externc.cpp,
which exists to be a thin C-ABI shim for Nim: the shim now casts the engine
pointer and delegates. That also lets Storybook install the same factory, so
media traffic measured on a Storybook page is the traffic the application
produces — and it lets the Accept policy be tested, which as a static function
in a .cpp with no header it could not be.

TestHttpStats and TestNetworkAccessFactory cover the thread affinity, both
clear paths, and the Accept policy as a table.
2026-08-06 14:26:30 +04:00

88 lines
2.8 KiB
C++

#pragma once
#include <QHash>
#include <QMutex>
#include <QObject>
#include <QPointer>
#include <QVariantList>
#include <QVariantMap>
#include <QVector>
class QNetworkDiskCache;
class QQmlEngine;
class QJSEngine;
/*!
* Per-host accounting of everything the QML engine's network access manager
* fetches. Fed by CountingNetworkAccessManager (src/networkaccessfactory.cpp),
* read by the HTTP statistics screen in settings.
*
* Network and cache are counted separately on purpose: a total that mixes them
* cannot show that a cache is working, which is the question the screen exists
* to answer.
*
* What this does NOT see, because it never passes through this manager:
* status-go, messaging, the web engine, and the thread_local managers in
* systemutilsinternal.cpp and clipboardutils.cpp. The screen says so itself.
*/
class HttpStats : public QObject
{
Q_OBJECT
public:
static HttpStats& instance();
static QObject* qmlInstance(QQmlEngine* engine, QJSEngine* scriptEngine);
//! Record one finished reply. Callable from any thread a manager lives on.
void record(const QString& host, qint64 bytes, bool fromCache);
//! Take note of a disk cache so the screen can report and clear it. Call
//! from the thread the cache lives on, right after it is configured.
void registerCache(QNetworkDiskCache* cache);
//! directory, size (measured on disk) and maximumSize of the disk cache.
//! Walks the directory, so call it on demand — not on every recorded reply.
Q_INVOKABLE QVariantMap cache() const;
//! Empty every registered cache. Required to reproduce a cold measurement
//! without reinstalling the application.
Q_INVOKABLE void clearCache();
//! One entry per host: host, networkRequests, networkBytes,
//! cacheRequests, cacheBytes. Ordered by total bytes, heaviest first.
Q_INVOKABLE QVariantList hosts() const;
//! networkRequests, networkBytes, cacheRequests, cacheBytes across all hosts.
Q_INVOKABLE QVariantMap totals() const;
Q_INVOKABLE void reset();
signals:
//! Emitted when a reply has been recorded, so an open screen can refresh.
void changed();
//! Emitted once per cache when clearCache() has finished on that cache's
//! thread. Separate from changed(), which fires on every reply and so must
//! not be what a reader hangs an expensive re-measurement on.
void cacheCleared();
private:
explicit HttpStats(QObject* parent = nullptr);
void notifyChanged();
void emitOnOwnThread(void (HttpStats::*signal)());
struct Entry {
qint64 networkRequests = 0;
qint64 networkBytes = 0;
qint64 cacheRequests = 0;
qint64 cacheBytes = 0;
};
mutable QMutex m_mutex;
QHash<QString, Entry> m_entries;
QString m_cacheDirectory;
qint64 m_cacheMaximumSize = 0;
QVector<QPointer<QNetworkDiskCache>> m_caches;
};