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:
Ursache Vladimir 2015-02-25 04:14:27 +02:00 committed by Giuseppe D'Angelo
parent 4cee9928f2
commit e18111c1a3
3 changed files with 206 additions and 35 deletions

View File

@ -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\\"'

View File

@ -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 users 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()

View File

@ -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();