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:
parent
1534dd6d97
commit
d77e544a56
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue