From e18111c1a3908bf3d04a094344280d3f5ace5a3a Mon Sep 17 00:00:00 2001 From: Ursache Vladimir Date: Wed, 25 Feb 2015 04:14:27 +0200 Subject: Optimize QApplication startup time by caching the Compose file Please find the explanation at the top of qtablegenerator.cpp. Change-Id: Ib1a5ee49d382034520ed0871bb524b7931cf330a Reviewed-by: Thiago Macieira --- .../platforminputcontexts/compose/compose.pro | 2 +- .../compose/generator/qtablegenerator.cpp | 235 ++++++++++++++++++--- .../compose/generator/qtablegenerator.h | 4 +- 3 files changed, 206 insertions(+), 35 deletions(-) diff --git a/src/plugins/platforminputcontexts/compose/compose.pro b/src/plugins/platforminputcontexts/compose/compose.pro index a9da36c473..a4b5280e64 100644 --- a/src/plugins/platforminputcontexts/compose/compose.pro +++ b/src/plugins/platforminputcontexts/compose/compose.pro @@ -5,7 +5,7 @@ PLUGIN_EXTENDS = - PLUGIN_CLASS_NAME = QComposePlatformInputContextPlugin load(qt_plugin) -QT += gui-private +QT += core-private gui-private DEFINES += X11_PREFIX='\\"$$QMAKE_X11_PREFIX\\"' diff --git a/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.cpp b/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.cpp index 4784a6e828..dca5fe871e 100644 --- a/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.cpp +++ b/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.cpp @@ -36,8 +36,12 @@ #include #include #include +#include #include #include +#include +#include +#include #include @@ -48,12 +52,191 @@ #include // strncasecmp #include // LC_CTYPE +static const quint32 SupportedCacheVersion = 1; + +/* + In short on how and why the "Compose" file is cached: + + The "Compose" file is large, for en_US it's likely located at: + /usr/share/X11/locale/en_US.UTF-8/Compose + and it has about 6000 string lines. + Q(Gui)Applications parse this file each time they're created. On modern CPUs + it incurs a 4-10 ms startup penalty of each Qt gui app, on older CPUs - + tens of ms or more. + Since the "Compose" file (almost) never changes using a pre-parsed + cache file instead of the "Compose" file is a good idea to improve Qt5 + application startup time by about 5+ ms (or tens of ms on older CPUs). + + The cache file contains the contents of the QComposeCacheFileHeader struct at the + beginning followed by the pre-parsed contents of the "Compose" file. + + struct QComposeCacheFileHeader stores + (a) The cache version - in the unlikely event that some day one might need + to break compatibility. + (b) The (cache) file size. + (c) The lastModified field tracks if anything changed since the last time + the cache file was saved. + If anything did change then we read the compose file and save (cache) it + in binary/pre-parsed format, which should happen extremely rarely if at all. +*/ + +struct QComposeCacheFileHeader +{ + quint32 cacheVersion; + // The compiler will add 4 padding bytes anyway. + // Reserve them explicitly to possibly use in the future. + quint32 reserved; + quint64 fileSize; + qint64 lastModified; +}; + +// localHostName() copied from qtbase/src/corelib/io/qlockfile_unix.cpp +static QByteArray localHostName() +{ + QByteArray hostName(512, Qt::Uninitialized); + if (gethostname(hostName.data(), hostName.size()) == -1) + return QByteArray(); + hostName.truncate(strlen(hostName.data())); + return hostName; +} + +/* + Reads metadata about the Compose file. Later used to determine if the + compose cache should be updated. The fileSize field will be zero on failure. +*/ +static QComposeCacheFileHeader readFileMetadata(const QString &path) +{ + QComposeCacheFileHeader info; + info.reserved = 0; + info.fileSize = 0; + const QByteArray pathBytes = QFile::encodeName(path); + QT_STATBUF st; + if (QT_STAT(pathBytes.data(), &st) != 0) + return info; + info.lastModified = st.st_mtime; + info.fileSize = st.st_size; + return info; +} + +static const QString getCacheFilePath() +{ + QFile machineIdFile("/var/lib/dbus/machine-id"); + QString machineId; + if (machineIdFile.exists()) { + if (machineIdFile.open(QIODevice::ReadOnly)) + machineId = QString::fromLatin1(machineIdFile.readAll().trimmed()); + } + if (machineId.isEmpty()) + machineId = localHostName(); + const QString dirPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); + + if (QSysInfo::ByteOrder == QSysInfo::BigEndian) + return dirPath + QLatin1String("/qt_compose_cache_big_endian_") + machineId; + return dirPath + QLatin1String("/qt_compose_cache_little_endian_") + machineId; +} + +// Returns empty vector on failure +static QVector loadCache(const QComposeCacheFileHeader &composeInfo) +{ + QVector vec; + const QString cacheFilePath = getCacheFilePath(); + QFile inputFile(cacheFilePath); + + if (!inputFile.open(QIODevice::ReadOnly)) + return vec; + QComposeCacheFileHeader cacheInfo; + // use a "buffer" variable to make the line after this one more readable. + char *buffer = reinterpret_cast(&cacheInfo); + + if (inputFile.read(buffer, sizeof cacheInfo) != sizeof cacheInfo) + return vec; + if (cacheInfo.fileSize == 0) + return vec; + // using "!=" just in case someone replaced with a backup that existed before + if (cacheInfo.lastModified != composeInfo.lastModified) + return vec; + if (cacheInfo.cacheVersion != SupportedCacheVersion) + return vec; + const QByteArray pathBytes = QFile::encodeName(cacheFilePath); + QT_STATBUF st; + if (QT_STAT(pathBytes.data(), &st) != 0) + return vec; + const off_t fileSize = st.st_size; + if (fileSize > 1024 * 1024 * 5) { + // The cache file size is usually about 150KB, so if its size is over + // say 5MB then somebody inflated the file, abort. + return vec; + } + const off_t bufferSize = fileSize - (sizeof cacheInfo); + const size_t elemSize = sizeof (struct QComposeTableElement); + const int elemCount = bufferSize / elemSize; + const QByteArray ba = inputFile.read(bufferSize); + const char *data = ba.data(); + // Since we know the number of the (many) elements and their size in + // advance calling vector.reserve(..) seems reasonable. + vec.reserve(elemCount); + + for (int i = 0; i < elemCount; i++) { + const QComposeTableElement *elem = + reinterpret_cast(data + (i * elemSize)); + vec.push_back(*elem); + } + return vec; +} + +// Returns true on success, false otherwise. +static bool saveCache(const QComposeCacheFileHeader &info, const QVector &vec) +{ + const QString filePath = getCacheFilePath(); + QSaveFile outputFile(filePath); + + if (!outputFile.open(QIODevice::WriteOnly)) + return false; + const char *data = reinterpret_cast(&info); + + if (outputFile.write(data, sizeof info) != sizeof info) + return false; + data = reinterpret_cast(vec.constData()); + const qint64 size = vec.size() * (sizeof (struct QComposeTableElement)); + + if (outputFile.write(data, size) != size) + return false; + return outputFile.commit(); +} + TableGenerator::TableGenerator() : m_state(NoErrors), m_systemComposeDir(QString()) { initPossibleLocations(); - findComposeFile(); - orderComposeTable(); + QString composeFilePath = findComposeFile(); +#ifdef DEBUG_GENERATOR +// don't use cache when in debug mode. + if (!composeFilePath.isEmpty()) + qDebug() << "Using Compose file from: " << composeFilePath; +#else + QComposeCacheFileHeader fileInfo = readFileMetadata(composeFilePath); + if (fileInfo.fileSize != 0) + m_composeTable = loadCache(fileInfo); +#endif + if (m_composeTable.isEmpty() && cleanState()) { + if (composeFilePath.isEmpty()) { + m_state = MissingComposeFile; + } else { + QFile composeFile(composeFilePath); + composeFile.open(QIODevice::ReadOnly); + parseComposeFile(&composeFile); + orderComposeTable(); + if (m_composeTable.isEmpty()) { + m_state = EmptyTable; +#ifndef DEBUG_GENERATOR +// don't save cache when in debug mode + } else { + fileInfo.cacheVersion = SupportedCacheVersion; + saveCache(fileInfo, m_composeTable); +#endif + } + } + } #ifdef DEBUG_GENERATOR printComposeTable(); #endif @@ -76,53 +259,39 @@ void TableGenerator::initPossibleLocations() m_possibleLocations.append(QStringLiteral(X11_PREFIX "/lib/X11/locale")); } -void TableGenerator::findComposeFile() +QString TableGenerator::findComposeFile() { - bool found = false; // check if XCOMPOSEFILE points to a Compose file if (qEnvironmentVariableIsSet("XCOMPOSEFILE")) { - QString composeFile(qgetenv("XCOMPOSEFILE")); - if (composeFile.endsWith(QLatin1String("Compose"))) - found = processFile(composeFile); + QString path(qgetenv("XCOMPOSEFILE")); + if (path.endsWith(QLatin1String("Compose"))) + return path; else qWarning("Qt Warning: XCOMPOSEFILE doesn't point to a valid Compose file"); -#ifdef DEBUG_GENERATOR - if (found) - qDebug() << "Using Compose file from: " << composeFile; -#endif } + // check if user’s home directory has a file named .XCompose - if (!found && cleanState()) { - QString composeFile = qgetenv("HOME") + QStringLiteral("/.XCompose"); - if (QFile(composeFile).exists()) - found = processFile(composeFile); -#ifdef DEBUG_GENERATOR - if (found) - qDebug() << "Using Compose file from: " << composeFile; -#endif + if (cleanState()) { + QString path = qgetenv("HOME") + QStringLiteral("/.XCompose"); + if (QFile(path).exists()) + return path; } + // check for the system provided compose files - if (!found && cleanState()) { + if (cleanState()) { QString table = composeTableForLocale(); if (cleanState()) { if (table.isEmpty()) // no table mappings for the system's locale in the compose.dir m_state = UnsupportedLocale; - else - found = processFile(systemComposeDir() + QLatin1Char('/') + table); -#ifdef DEBUG_GENERATOR - if (found) - qDebug() << "Using Compose file from: " << - systemComposeDir() + QLatin1Char('/') + table; -#endif + else { + QString path = QDir(systemComposeDir()).filePath(table); + if (QFile(path).exists()) + return path; + } } } - - if (found && m_composeTable.isEmpty()) - m_state = EmptyTable; - - if (!found) - m_state = MissingComposeFile; + return QString(); } QString TableGenerator::composeTableForLocale() diff --git a/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.h b/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.h index 468da4cad1..8ad081bea5 100644 --- a/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.h +++ b/src/plugins/platforminputcontexts/compose/generator/qtablegenerator.h @@ -43,6 +43,8 @@ //#define DEBUG_GENERATOR +/* Whenever QComposeTableElement gets modified supportedCacheVersion + from qtablegenerator.cpp must be bumped. */ struct QComposeTableElement { uint keys[QT_KEYSEQUENCE_MAX_LEN]; uint value; @@ -107,7 +109,7 @@ protected: void parseKeySequence(char *line); void parseIncludeInstruction(QString line); - void findComposeFile(); + QString findComposeFile(); bool findSystemComposeDir(); QString systemComposeDir(); QString composeTableForLocale(); -- cgit v1.2.3