Safeguard disk cache loading with checksum verification

When loading a QML component from the disk cache, compare the checksum
of the dependent types against the checksum when the cache was created.
Any change in the meta-object of a dependent type should trigger a
re-creation discard of the cache and consequent re-creation (in the
test-case).

Unfortunately this also requires extending the existing hack in the unit
test to deal with the low second precision on HFS+ in order to pass the
tests.

Change-Id: Ib8e899347680f7be676788388e9c23a09b0277e3
Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
Simon Hausmann 2016-07-28 17:40:36 +02:00
parent 1534dd6d97
commit d77e544a56
5 changed files with 207 additions and 45 deletions

View File

@ -301,6 +301,25 @@ void CompilationUnit::finalize(QQmlEnginePrivate *engine)
totalObjectCount = objectCount;
}
bool CompilationUnit::verifyChecksum(QQmlEngine *engine,
const ResolvedTypeReferenceMap &dependentTypes) const
{
if (dependentTypes.isEmpty()) {
for (size_t i = 0; i < sizeof(data->dependencyMD5Checksum); ++i) {
if (data->dependencyMD5Checksum[i] != 0)
return false;
}
return true;
}
QCryptographicHash hash(QCryptographicHash::Md5);
if (!dependentTypes.addToHash(&hash, engine))
return false;
QByteArray checksum = hash.result();
Q_ASSERT(checksum.size() == sizeof(data->dependencyMD5Checksum));
return memcmp(data->dependencyMD5Checksum, checksum.constData(),
sizeof(data->dependencyMD5Checksum)) == 0;
}
bool CompilationUnit::saveToDisk(QString *errorString)
{
errorString->clear();

View File

@ -865,6 +865,9 @@ struct Q_QML_PRIVATE_EXPORT CompilationUnit : public QQmlRefCount
QVector<QQmlScriptData *> dependentScripts;
ResolvedTypeReferenceMap resolvedTypes;
bool verifyChecksum(QQmlEngine *engine,
const ResolvedTypeReferenceMap &dependentTypes) const;
int metaTypeId;
int listMetaTypeId;
bool isRegisteredWithEngine;

View File

@ -2136,20 +2136,15 @@ bool QQmlTypeData::tryLoadFromDiskCache()
return true;
}
void QQmlTypeData::rebuildTypeAndPropertyCaches()
void QQmlTypeData::createTypeAndPropertyCaches(const QQmlRefPointer<QQmlTypeNameCache> &importCache,
const QV4::CompiledData::ResolvedTypeReferenceMap &resolvedTypeCache)
{
Q_ASSERT(m_compiledData);
m_compiledData->importCache = importCache;
m_compiledData->resolvedTypes = resolvedTypeCache;
QQmlEnginePrivate * const engine = QQmlEnginePrivate::get(typeLoader()->engine());
{
QQmlCompileError error = buildTypeResolutionCaches(&m_compiledData->importCache, &m_compiledData->resolvedTypes);
if (error.isSet()) {
setError(error);
return;
}
}
{
QQmlPropertyCacheCreator<QV4::CompiledData::CompilationUnit> propertyCacheCreator(&m_compiledData->propertyCaches, engine, m_compiledData, &m_importCache);
QQmlCompileError error = propertyCacheCreator.buildMetaObjects();
@ -2231,21 +2226,41 @@ void QQmlTypeData::done()
}
}
QQmlRefPointer<QQmlTypeNameCache> importCache;
QV4::CompiledData::ResolvedTypeReferenceMap resolvedTypeCache;
{
QQmlCompileError error = buildTypeResolutionCaches(&importCache, &resolvedTypeCache);
if (error.isSet()) {
setError(error);
return;
}
}
QQmlEngine *const engine = typeLoader()->engine();
// verify if any dependencies changed if we're using a cache
if (m_document.isNull() && !m_compiledData->verifyChecksum(engine, resolvedTypeCache)) {
if (!loadFromSource())
return;
m_backupSourceCode.clear();
m_compiledData = nullptr;
}
if (!m_document.isNull()) {
// Compile component
compile();
compile(importCache, resolvedTypeCache);
} else {
rebuildTypeAndPropertyCaches();
createTypeAndPropertyCaches(importCache, resolvedTypeCache);
}
if (isError())
return;
{
QQmlEnginePrivate * const engine = QQmlEnginePrivate::get(typeLoader()->engine());
QQmlEnginePrivate *const enginePrivate = QQmlEnginePrivate::get(engine);
{
// Sanity check property bindings
QQmlPropertyValidator validator(engine, m_importCache, m_compiledData);
QQmlPropertyValidator validator(enginePrivate, m_importCache, m_compiledData);
QVector<QQmlCompileError> errors = validator.validate();
if (!errors.isEmpty()) {
setError(errors);
@ -2253,7 +2268,7 @@ void QQmlTypeData::done()
}
}
m_compiledData->finalize(engine);
m_compiledData->finalize(enginePrivate);
}
{
@ -2336,19 +2351,43 @@ bool QQmlTypeData::loadImplicitImport()
void QQmlTypeData::dataReceived(const Data &data)
{
QString error;
m_backupSourceCode = data.readAll(&error, &m_sourceTimeStamp);
// if we failed to read the source code, process it _after_ we've tried
// to use the disk cache, in order to support scenarios where the source
// was removed deliberately.
if (tryLoadFromDiskCache())
return;
qint64 sourceTimeStamp;
QString error;
QString code = QString::fromUtf8(data.readAll(&error, &sourceTimeStamp));
if (isError())
return;
if (!error.isEmpty()) {
setError(error);
return;
}
if (!loadFromSource())
return;
continueLoadFromIR();
}
void QQmlTypeData::initializeFromCachedUnit(const QQmlPrivate::CachedQmlUnit *unit)
{
QQmlEngine *qmlEngine = typeLoader()->engine();
m_document.reset(new QmlIR::Document(QV8Engine::getV4(qmlEngine)->debugger() != 0));
m_document->jsModule.sourceTimeStamp = sourceTimeStamp;
unit->loadIR(m_document.data(), unit);
continueLoadFromIR();
}
bool QQmlTypeData::loadFromSource()
{
QString code = QString::fromUtf8(m_backupSourceCode);
QQmlEngine *qmlEngine = typeLoader()->engine();
m_document.reset(new QmlIR::Document(QV8Engine::getV4(qmlEngine)->debugger() != 0));
m_document->jsModule.sourceTimeStamp = m_sourceTimeStamp;
QmlIR::IRBuilder compiler(QV8Engine::get(qmlEngine)->illegalNames());
if (!compiler.generateFromQml(code, finalUrlString(), m_document.data())) {
QList<QQmlError> errors;
@ -2362,18 +2401,9 @@ void QQmlTypeData::dataReceived(const Data &data)
errors << e;
}
setError(errors);
return;
return false;
}
continueLoadFromIR();
}
void QQmlTypeData::initializeFromCachedUnit(const QQmlPrivate::CachedQmlUnit *unit)
{
QQmlEngine *qmlEngine = typeLoader()->engine();
m_document.reset(new QmlIR::Document(QV8Engine::getV4(qmlEngine)->debugger() != 0));
unit->loadIR(m_document.data(), unit);
continueLoadFromIR();
return true;
}
void QQmlTypeData::continueLoadFromIR()
@ -2468,18 +2498,10 @@ QString QQmlTypeData::stringAt(int index) const
return m_document->jsGenerator.stringTable.stringForIndex(index);
}
void QQmlTypeData::compile()
void QQmlTypeData::compile(const QQmlRefPointer<QQmlTypeNameCache> &importCache, const QV4::CompiledData::ResolvedTypeReferenceMap &resolvedTypeCache)
{
Q_ASSERT(m_compiledData.isNull());
QQmlRefPointer<QQmlTypeNameCache> importCache;
QV4::CompiledData::ResolvedTypeReferenceMap resolvedTypeCache;
QQmlCompileError error = buildTypeResolutionCaches(&importCache, &resolvedTypeCache);
if (error.isSet()) {
setError(error);
return;
}
QQmlTypeCompiler compiler(QQmlEnginePrivate::get(typeLoader()->engine()), this, m_document.data(), importCache, resolvedTypeCache);
m_compiledData = compiler.compile();
if (!m_compiledData) {

View File

@ -442,18 +442,24 @@ protected:
private:
bool tryLoadFromDiskCache();
bool loadFromSource();
void continueLoadFromIR();
void resolveTypes();
QQmlCompileError buildTypeResolutionCaches(
QQmlRefPointer<QQmlTypeNameCache> *importCache,
QV4::CompiledData::ResolvedTypeReferenceMap *resolvedTypeCache
) const;
void compile();
void rebuildTypeAndPropertyCaches();
void compile(const QQmlRefPointer<QQmlTypeNameCache> &importCache,
const QV4::CompiledData::ResolvedTypeReferenceMap &resolvedTypeCache);
void createTypeAndPropertyCaches(const QQmlRefPointer<QQmlTypeNameCache> &importCache,
const QV4::CompiledData::ResolvedTypeReferenceMap &resolvedTypeCache);
bool resolveType(const QString &typeName, int &majorVersion, int &minorVersion, TypeReference &ref);
virtual void scriptImported(QQmlScriptBlob *blob, const QV4::CompiledData::Location &location, const QString &qualifier, const QString &nameSpace);
qint64 m_sourceTimeStamp = 0;
QByteArray m_backupSourceCode; // used when cache verification fails.
QScopedPointer<QmlIR::Document> m_document;
QV4::CompiledData::TypeReferenceMap m_typeReferences;

View File

@ -48,8 +48,40 @@ private slots:
void regenerateAfterChange();
void registerImportForImplicitComponent();
void basicVersionChecks();
void recompileAfterChange();
};
// A wrapper around QQmlComponent to ensure the temporary reference counts
// on the type data as a result of the main thread <> loader thread communication
// are dropped. Regular Synchronous loading will leave us with an event posted
// to the gui thread and an extra refcount that will only be dropped after the
// event delivery. A plain sendPostedEvents() however is insufficient because
// we can't be sure that the event is posted after the constructor finished.
class CleanlyLoadingComponent : public QQmlComponent
{
public:
CleanlyLoadingComponent(QQmlEngine *engine, const QUrl &url)
: QQmlComponent(engine, url, QQmlComponent::Asynchronous)
{ waitForLoad(); }
CleanlyLoadingComponent(QQmlEngine *engine, const QString &fileName)
: QQmlComponent(engine, fileName, QQmlComponent::Asynchronous)
{ waitForLoad(); }
void waitForLoad()
{
QTRY_VERIFY(status() == QQmlComponent::Ready || status() == QQmlComponent::Error);
}
};
static void waitForFileSystem()
{
// On macOS with HFS+ the precision of file times is measured in seconds, so to ensure that
// the newly written file has a modification date newer than an existing cache file, we must
// wait.
// Similar effects of lacking precision have been observed on some Linux systems.
QThread::sleep(1);
}
struct TestCompiler
{
TestCompiler(QQmlEngine *engine)
@ -67,10 +99,8 @@ struct TestCompiler
closeMapping();
engine->clearComponentCache();
// Qt API limits the precision of QFileInfo::modificationTime() to seconds, so to ensure that
// the newly written file has a modification date newer than an existing cache file, we must
// wait.
QThread::sleep(1);
waitForFileSystem();
{
QFile f(testFilePath);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
@ -83,7 +113,7 @@ struct TestCompiler
}
}
QQmlComponent component(engine, testFilePath);
CleanlyLoadingComponent component(engine, testFilePath);
if (!component.isReady()) {
lastErrorString = component.errorString();
return false;
@ -321,6 +351,88 @@ void tst_qmldiskcache::basicVersionChecks()
}
}
class TypeVersion1 : public QObject
{
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged);
public:
int m_value = 0;
int value() const { return m_value; }
void setValue(int v) { m_value = v; emit valueChanged(); }
signals:
void valueChanged();
};
// Same as TypeVersion1 except the property type changed!
class TypeVersion2 : public QObject
{
Q_OBJECT
Q_PROPERTY(QString value READ value WRITE setValue NOTIFY valueChanged);
public:
QString m_value;
QString value() const { return m_value; }
void setValue(QString v) { m_value = v; emit valueChanged(); }
signals:
void valueChanged();
};
void tst_qmldiskcache::recompileAfterChange()
{
QQmlEngine engine;
TestCompiler testCompiler(&engine);
QVERIFY(testCompiler.tempDir.isValid());
const QByteArray contents = QByteArrayLiteral("import TypeTest 1.0\n"
"TypeThatWillChange {\n"
"}");
qmlRegisterType<TypeVersion1>("TypeTest", 1, 0, "TypeThatWillChange");
{
testCompiler.clearCache();
QVERIFY2(testCompiler.compile(contents), qPrintable(testCompiler.lastErrorString));
QVERIFY2(testCompiler.verify(), qPrintable(testCompiler.lastErrorString));
}
QDateTime initialCacheTimeStamp = QFileInfo(testCompiler.cacheFilePath).lastModified();
{
CleanlyLoadingComponent component(&engine, testCompiler.testFilePath);
QScopedPointer<TypeVersion1> obj(qobject_cast<TypeVersion1*>(component.create()));
QVERIFY(!obj.isNull());
QCOMPARE(QFileInfo(testCompiler.cacheFilePath).lastModified(), initialCacheTimeStamp);
}
engine.clearComponentCache();
{
CleanlyLoadingComponent component(&engine, testCompiler.testFilePath);
QScopedPointer<TypeVersion1> obj(qobject_cast<TypeVersion1*>(component.create()));
QVERIFY(!obj.isNull());
QCOMPARE(QFileInfo(testCompiler.cacheFilePath).lastModified(), initialCacheTimeStamp);
}
engine.clearComponentCache();
qmlClearTypeRegistrations();
qmlRegisterType<TypeVersion2>("TypeTest", 1, 0, "TypeThatWillChange");
waitForFileSystem();
{
CleanlyLoadingComponent component(&engine, testCompiler.testFilePath);
QScopedPointer<TypeVersion2> obj(qobject_cast<TypeVersion2*>(component.create()));
QVERIFY(!obj.isNull());
QVERIFY(QFileInfo(testCompiler.cacheFilePath).lastModified() > initialCacheTimeStamp);
}
}
QTEST_MAIN(tst_qmldiskcache)
#include "tst_qmldiskcache.moc"