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 <thiago.macieira@intel.com>
This commit is contained in:
parent
4cee9928f2
commit
e18111c1a3
@ -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\\"'
|
||||
|
||||
|
@ -36,8 +36,12 @@
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QTextCodec>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QSaveFile>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <private/qcore_unix_p.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@ -48,12 +52,191 @@
|
||||
#include <strings.h> // strncasecmp
|
||||
#include <clocale> // 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<QComposeTableElement> loadCache(const QComposeCacheFileHeader &composeInfo)
|
||||
{
|
||||
QVector<QComposeTableElement> 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<char*>(&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<const QComposeTableElement*>(data + (i * elemSize));
|
||||
vec.push_back(*elem);
|
||||
}
|
||||
return vec;
|
||||
}
|
||||
|
||||
// Returns true on success, false otherwise.
|
||||
static bool saveCache(const QComposeCacheFileHeader &info, const QVector<QComposeTableElement> &vec)
|
||||
{
|
||||
const QString filePath = getCacheFilePath();
|
||||
QSaveFile outputFile(filePath);
|
||||
|
||||
if (!outputFile.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
const char *data = reinterpret_cast<const char*>(&info);
|
||||
|
||||
if (outputFile.write(data, sizeof info) != sizeof info)
|
||||
return false;
|
||||
data = reinterpret_cast<const char*>(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();
|
||||
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()
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user