mirror of
https://github.com/status-im/status-app.git
synced 2026-08-27 15:11:20 +00:00
* perf(statusq): event-loop-driven QML incubation controller The QQuickWindow default incubation controller collapses its time budget when frames are slow, starving async Loaders exactly when load is heaviest — on low-end devices this stretches section incubation into multi-second skeletons. Drive incubation from a permanently armed timer instead: 20ms slices per event-loop tick, a 12ms gentle phase during the first 300ms of a burst so section-switch animations keep their frame budget, and a 128ms idle backstop so a missed count callback can never wedge incubation. Input stays responsive between ticks. A/B on the community section page (STORYBOOK_INCUBATION_MS=20, gentle phase off): idle machine 857 -> 441ms for a 210-channel load; CPU- contended machine (the low-end condition) 5.4-8.1s with the default controller vs a flat 400-490ms with this one. * fixup! perf(statusq): event-loop-driven QML incubation controller Without it the QML test engine runs async Loaders on Qt's render-loop budget — the configuration the benchmark measured at tens of seconds per section load, and the one nothing in the app ever uses. Tests were pinning a configuration that does not ship, and paying for it with 120 s waits. externc casts to QQmlEngine now: the test harness owns a plain one, not a QQmlApplicationEngine. The HomePage grid lookup waits for its delegate instead of assuming it exists on the same tick. * fixup! perf(statusq): event-loop-driven QML incubation controller * chore(statusq): env-gated incubation debug HUD STATUS_INCUBATION_HUD=1 shows a top-right overlay with the live incubator count (16ms refresh + 1s peak) and the controller pacing phase (idle/gentle/boost, plus held gentle hints). Debug-only; drop before merge if unwanted.
210 lines
7.8 KiB
C++
210 lines
7.8 KiB
C++
#include <QDirIterator>
|
|
#include <QGuiApplication>
|
|
#include <QQmlApplicationEngine>
|
|
#include <QQmlComponent>
|
|
#include <QQmlContext>
|
|
#include <QQuickStyle>
|
|
#include <QStandardPaths>
|
|
#include <QtWebView>
|
|
|
|
#include <Storybook/storybooksetup.h>
|
|
#include <Storybook/qmlfilesserver.h>
|
|
|
|
#include <memory>
|
|
|
|
#include <StatusQ/networkaccessfactory.h>
|
|
#include <StatusQ/typesregistration.h>
|
|
|
|
extern "C" void statusq_installBoostedIncubationController(void* engine, int msPerTick,
|
|
int gentlePeriodMs, int boostGapMs);
|
|
|
|
using namespace Qt::Literals::StringLiterals;
|
|
|
|
// Mirrors NETWORK_DISK_CACHE_SIZE in src/constants.nim.
|
|
constexpr qint64 networkDiskCacheSize = 512ll * 1024 * 1024;
|
|
|
|
void loadContextPropertiesMocks(const char* storybookRoot, QQmlApplicationEngine& engine);
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
bool hasExplicitStyleSet = false;
|
|
for (size_t i = 1; i < argc; i++)
|
|
{
|
|
// Qt uses these standard/builtin args as soon as it sees them;
|
|
// so process before creating qApp instance
|
|
if (qstrcmp(argv[i], "-style") == 0) {
|
|
hasExplicitStyleSet = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Required by the WalletConnectSDK view
|
|
QtWebView::initialize();
|
|
|
|
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
|
|
|
QGuiApplication app(argc, argv);
|
|
QGuiApplication::setOrganizationName(u"Status"_s);
|
|
QGuiApplication::setOrganizationDomain(u"status.im"_s);
|
|
QGuiApplication::setApplicationName(u"Status Desktop Storybook"_s);
|
|
QGuiApplication::setApplicationDisplayName(u"%1 [Qt %2]"_s.arg(
|
|
QGuiApplication::applicationName(), qVersion()));
|
|
|
|
if (!hasExplicitStyleSet)
|
|
QQuickStyle::setStyle(u"Universal"_s); // only used as a basic style for SB itself
|
|
|
|
QCommandLineParser cmdParser;
|
|
cmdParser.addHelpOption();
|
|
cmdParser.addPositionalArgument(u"page"_s, u"Open the given page on startup"_s);
|
|
|
|
#ifdef ANDROID
|
|
static constexpr auto defaultMode = "remote";
|
|
#else
|
|
static constexpr auto defaultMode = "local";
|
|
#endif
|
|
|
|
QCommandLineOption modeOption(QStringList() << u"m"_s << u"mode"_s,
|
|
u"mode (local or remote)"_s,
|
|
u"mode"_s, defaultMode);
|
|
|
|
cmdParser.addOption(modeOption);
|
|
cmdParser.process(app.arguments());
|
|
|
|
const QString mode = cmdParser.value(modeOption);
|
|
|
|
if (mode != u"local"_s && mode != u"remote"_s) {
|
|
qWarning() << "Invalid mode, use 'local' or 'remote'";
|
|
return 0;
|
|
}
|
|
|
|
qputenv("QT_QUICK_CONTROLS_HOVER_ENABLED", QByteArrayLiteral("1"));
|
|
auto chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS");
|
|
if(!chromiumFlags.contains("--disable-seccomp-filter-sandbox")) {
|
|
chromiumFlags +=" --disable-seccomp-filter-sandbox";
|
|
}
|
|
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags);
|
|
|
|
QStringList additionalImportPaths;
|
|
additionalImportPaths << u"qrc:/"_s;
|
|
|
|
if (mode == u"local"_s) {
|
|
additionalImportPaths << QStringList {
|
|
STATUSQ_MODULE_IMPORT_PATH,
|
|
QML_IMPORT_ROOT u"/../ui/app"_s,
|
|
QML_IMPORT_ROOT u"/../ui/imports"_s,
|
|
QML_IMPORT_ROOT u"/src"_s,
|
|
QML_IMPORT_ROOT u"/pages"_s,
|
|
QML_IMPORT_ROOT u"/stubs"_s,
|
|
};
|
|
|
|
StorybookSetup::registerTypesLocal(
|
|
additionalImportPaths,
|
|
QML_IMPORT_ROOT u"/pages"_s,
|
|
QCoreApplication::applicationDirPath() + u"/QmlTests"_s,
|
|
QML_IMPORT_ROOT u"/qmlTests/tests"_s);
|
|
} else {
|
|
additionalImportPaths << u"http://localhost:8080/0"_s;
|
|
|
|
StorybookSetup::registerTypesRemote(QUrl(u"http://localhost:8080/version"_s),
|
|
QUrl(u"http://localhost:8080/pages"_s),
|
|
QUrl(u"http://localhost:8080"_s));
|
|
}
|
|
|
|
QQmlApplicationEngine engine;
|
|
|
|
// Same disk cache and Accept hints the application installs, so media traffic
|
|
// measured in Storybook is the traffic the application would produce.
|
|
Status::setupNetworkAccessManagerFactory(
|
|
&engine, QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u"/netcache"_s,
|
|
networkDiskCacheSize);
|
|
|
|
// A/B hook for the app's boosted incubation controller (externc.cpp):
|
|
// STORYBOOK_INCUBATION_MS=20 makes async section loads incubate with a
|
|
// fixed event-loop budget instead of the render-loop-driven default;
|
|
// STORYBOOK_INCUBATION_GENTLE_MS / STORYBOOK_INCUBATION_GAP_MS tune the
|
|
// gentle window and the pause between boosted bites. An unparsable or
|
|
// non-positive budget leaves the default controller in place.
|
|
bool incubationMsValid = false;
|
|
const int incubationMs =
|
|
qEnvironmentVariable("STORYBOOK_INCUBATION_MS").toInt(&incubationMsValid);
|
|
if (incubationMsValid && incubationMs > 0)
|
|
statusq_installBoostedIncubationController(
|
|
&engine, incubationMs,
|
|
qEnvironmentVariableIntValue("STORYBOOK_INCUBATION_GENTLE_MS"),
|
|
qEnvironmentVariableIntValue("STORYBOOK_INCUBATION_GAP_MS"));
|
|
|
|
for (auto& path : std::as_const(additionalImportPaths))
|
|
engine.addImportPath(path);
|
|
|
|
StorybookSetup::configureEngine(&engine, mode == u"local"_s);
|
|
registerStatusQTypes();
|
|
|
|
loadContextPropertiesMocks(QML_IMPORT_ROOT, engine);
|
|
|
|
const QUrl url("qrc:/main.qml");
|
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
|
|
&app, [url](const QUrl &objUrl) {
|
|
if (url == objUrl)
|
|
QCoreApplication::exit(-1);
|
|
}, Qt::QueuedConnection);
|
|
|
|
const auto args = cmdParser.positionalArguments();
|
|
if (!args.isEmpty())
|
|
engine.setInitialProperties({{u"currentPage"_s, args.constFirst()}});
|
|
|
|
if (mode == u"remote"_s) {
|
|
auto server = new QmlFilesServer({
|
|
STATUSQ_MODULE_IMPORT_PATH,
|
|
// stubs first to give precedence over real stores
|
|
QML_IMPORT_ROOT u"/stubs"_s,
|
|
QML_IMPORT_ROOT u"/../ui/app"_s,
|
|
QML_IMPORT_ROOT u"/../ui/imports"_s,
|
|
QML_IMPORT_ROOT u"/src"_s,
|
|
QML_IMPORT_ROOT u"/pages"_s,
|
|
}, QML_IMPORT_ROOT u"/pages"_s, true, &engine);
|
|
|
|
server->start(8080);
|
|
}
|
|
|
|
engine.load(url);
|
|
|
|
qInfo() << "Storybook started, Qt runtime version:" << qVersion()
|
|
<< "; built against version:" << QLibraryInfo::version().toString()
|
|
<< "installed in:" << QLibraryInfo::path(QLibraryInfo::PrefixPath)
|
|
<< "; QQC style:" << QQuickStyle::name()
|
|
<< "; QPA:" << qApp->platformName();
|
|
|
|
return QGuiApplication::exec();
|
|
}
|
|
|
|
void loadContextPropertiesMocks(const char* storybookRoot, QQmlApplicationEngine& engine) {
|
|
QDirIterator it(QML_IMPORT_ROOT u"/stubs/nim/sectionmocks"_s, QDirIterator::Subdirectories);
|
|
|
|
while (it.hasNext()) {
|
|
it.next();
|
|
if (it.fileInfo().isFile() && it.fileInfo().suffix() == u"qml"_s) {
|
|
auto component = std::make_unique<QQmlComponent>(&engine, QUrl::fromLocalFile(it.filePath()));
|
|
if (component->status() != QQmlComponent::Ready) {
|
|
qWarning() << "Failed to load mock for" << it.filePath() << component->errorString();
|
|
continue;
|
|
}
|
|
|
|
auto objPtr = std::unique_ptr<QObject>(component->create());
|
|
if(!objPtr) {
|
|
qWarning() << "Failed to create mock for" << it.filePath();
|
|
continue;
|
|
}
|
|
|
|
if(!objPtr->property("contextPropertyName").isValid()) {
|
|
qInfo() << "Not a mock, missing property name \"contextPropertyName\"";
|
|
continue;
|
|
}
|
|
|
|
auto contextPropertyName = objPtr->property("contextPropertyName").toString();
|
|
auto obj = objPtr.release();
|
|
obj->setParent(&engine);
|
|
engine.rootContext()->setContextProperty(contextPropertyName, obj);
|
|
}
|
|
}
|
|
}
|