diff options
author | Jarek Kobus <jaroslaw.kobus@qt.io> | 2023-05-12 21:38:33 +0200 |
---|---|---|
committer | Jarek Kobus <jaroslaw.kobus@qt.io> | 2023-05-19 07:17:01 +0000 |
commit | 686a40d199864c1b2e29defc1ba67fd73f5aaaf0 (patch) | |
tree | 1b578e5175a9a999bcec23d877db5649efab18ac | |
parent | 9c78ef983a1203515a7552784a119f1ba3df4a71 (diff) |
SubDirFileIterator: Add manual performance test
Compare the total time spent on iterating a big file tree
with 3 different time iterators:
- Utils::SubDirFileIterator
- manually written iterator using QDir::entryInfoList()
- QDirIterator
The iterator run through about one million files (including
about ~100K directories). The more files above this
number to be iterated over, the bigger the relative time
difference of SubDirFileIterator compared to other iterators.
The number of generated files depends on the running
machine's core number. In my case:
Number of cores: 24
Number of generated files: 898753
Number of generated directories: 112345
Time spent on generating file tree: ~2 seconds
Time spent on iterating using SubDirIterator: ~80 seconds
Time spent on iterating using manual iterator: ~8 seconds
Time spent on iterating using QDirIterator: ~4 seconds
Time spent on removing generated file tree: ~2 seconds
Task-number: QTCREATORBUG-28892
Change-Id: I94d7cf0169a470820dc27f39c9cdb4150eea51c1
Reviewed-by: hjk <hjk@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
-rw-r--r-- | tests/manual/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/manual/manual.qbs | 1 | ||||
-rw-r--r-- | tests/manual/subdirfileiterator/CMakeLists.txt | 10 | ||||
-rw-r--r-- | tests/manual/subdirfileiterator/subdirfileiterator.qbs | 22 | ||||
-rw-r--r-- | tests/manual/subdirfileiterator/tst_subdirfileiterator.cpp | 257 |
5 files changed, 291 insertions, 0 deletions
diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt index c171a70deb..fb0b611f4f 100644 --- a/tests/manual/CMakeLists.txt +++ b/tests/manual/CMakeLists.txt @@ -16,5 +16,6 @@ add_subdirectory(proparser) # add_subdirectory(qt4projectmanager) # add_subdirectory(search) add_subdirectory(shootout) +add_subdirectory(subdirfileiterator) add_subdirectory(tasktree) add_subdirectory(widgets) diff --git a/tests/manual/manual.qbs b/tests/manual/manual.qbs index e3ebf0707a..8e157a9216 100644 --- a/tests/manual/manual.qbs +++ b/tests/manual/manual.qbs @@ -13,6 +13,7 @@ Project { "pluginview/pluginview.qbs", "proparser/testreader.qbs", "shootout/shootout.qbs", + "subdirfileiterator/subdirfileiterator.qbs", "tasktree/tasktree.qbs", "widgets/widgets.qbs", ] diff --git a/tests/manual/subdirfileiterator/CMakeLists.txt b/tests/manual/subdirfileiterator/CMakeLists.txt new file mode 100644 index 0000000000..9613e197b7 --- /dev/null +++ b/tests/manual/subdirfileiterator/CMakeLists.txt @@ -0,0 +1,10 @@ +file(RELATIVE_PATH RELATIVE_TEST_PATH "${PROJECT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}") +file(RELATIVE_PATH TEST_RELATIVE_LIBEXEC_PATH "/${RELATIVE_TEST_PATH}" "/${IDE_LIBEXEC_PATH}") + +add_qtc_test(tst_manual_subdirfileiterator + MANUALTEST + DEFINES "TEST_RELATIVE_LIBEXEC_PATH=\"${TEST_RELATIVE_LIBEXEC_PATH}\"" + DEPENDS Utils app_version + SOURCES + tst_subdirfileiterator.cpp +) diff --git a/tests/manual/subdirfileiterator/subdirfileiterator.qbs b/tests/manual/subdirfileiterator/subdirfileiterator.qbs new file mode 100644 index 0000000000..852fb9b545 --- /dev/null +++ b/tests/manual/subdirfileiterator/subdirfileiterator.qbs @@ -0,0 +1,22 @@ +import qbs.FileInfo + +QtcManualtest { + name: "Manual SubDirFileIterator test" + type: ["application"] + + Depends { name: "Utils" } + Depends { name: "app_version_header" } + + files: [ + "tst_subdirfileiterator.cpp", + ] + + cpp.defines: { + var defines = base; + var absLibExecPath = FileInfo.joinPaths(qbs.installRoot, qbs.installPrefix, + qtc.ide_libexec_path); + var relLibExecPath = FileInfo.relativePath(destinationDirectory, absLibExecPath); + defines.push('TEST_RELATIVE_LIBEXEC_PATH="' + relLibExecPath + '"'); + return defines; + } +} diff --git a/tests/manual/subdirfileiterator/tst_subdirfileiterator.cpp b/tests/manual/subdirfileiterator/tst_subdirfileiterator.cpp new file mode 100644 index 0000000000..4093030a71 --- /dev/null +++ b/tests/manual/subdirfileiterator/tst_subdirfileiterator.cpp @@ -0,0 +1,257 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <app/app_version.h> + +#include <utils/async.h> +#include <utils/filesearch.h> +#include <utils/launcherinterface.h> +#include <utils/scopedtimer.h> +#include <utils/temporarydirectory.h> + +#include <QDirIterator> +#include <QObject> +#include <QTemporaryDir> +#include <QtTest> + +#include <unordered_set> + +using namespace Tasking; +using namespace Utils; + +static const int s_subDirsCount = 8; +static const int s_subFilesCount = 8; +static const int s_treeDepth = 4; + +static const QDir::Filters s_filters = QDir::Dirs | QDir::Files | QDir::Hidden + | QDir::NoDotAndDotDot; + +static const char s_dirPrefix[] = "dir_"; +static const char s_filePrefix[] = "file_"; + +static int expectedDirsCountHelper(int depth) +{ + if (depth == 1) + return s_subDirsCount + 1; // +1 -> dir itself + return expectedDirsCountHelper(depth - 1) * s_subDirsCount + 1; // +1 -> dir itself +} + +static int expectedDirsCount(int tasksCount) +{ + return expectedDirsCountHelper(s_treeDepth) * tasksCount + 1; // +1 -> root temp dir +} + +static int expectedFilesCountHelper(int depth) +{ + if (depth == 0) + return s_subFilesCount; + return expectedFilesCountHelper(depth - 1) * s_subDirsCount + s_subFilesCount; +} + +static int expectedFilesCount(int tasksCount) +{ + return expectedFilesCountHelper(s_treeDepth) * tasksCount + 1; // +1 -> fileTemplate.txt +} + +static void generate(QPromise<void> &promise, const QString &parentPath, + const QString &templateFile, int depth) +{ + const QDir parentDir(parentPath); + for (int i = 0; i < s_subFilesCount; ++i) + QFile::copy(templateFile, parentDir.filePath(QString("%1%2").arg(s_filePrefix).arg(i))); + + if (depth == 0) + return; + + if (promise.isCanceled()) + return; + + const QString templateDir("dirTemplate"); + if (!parentDir.mkdir(templateDir)) { + promise.future().cancel(); + return; + } + + const QString templateDirPath = parentDir.filePath(templateDir); + generate(promise, templateDirPath, templateFile, depth - 1); + + if (promise.isCanceled()) + return; + + for (int i = 0; i < s_subDirsCount - 1; ++i) { + const QString dirName = QString("%1%2").arg(s_dirPrefix).arg(i); + const FilePath source = FilePath::fromString(templateDirPath); + const FilePath destination = FilePath::fromString(parentDir.filePath(dirName)); + if (!source.copyRecursively(destination)) { + promise.future().cancel(); + return; + } + } +} + +class tst_SubDirFileIterator : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + TemporaryDirectory::setMasterTemporaryDirectory( + QDir::tempPath() + "/" + Core::Constants::IDE_CASED_ID + "-XXXXXX"); + + const QString libExecPath(qApp->applicationDirPath() + '/' + + QLatin1String(TEST_RELATIVE_LIBEXEC_PATH)); + LauncherInterface::setPathToLauncher(libExecPath); + + qDebug() << "This manual test compares the performance of the SubDirFileIterator with " + "a manually written iterator using QDir::entryInfoList()."; + QTC_SCOPED_TIMER("GENERATING TEMPORARY FILES TREE"); + const int tasksCount = QThread::idealThreadCount(); + m_filesCount = expectedFilesCount(tasksCount); + m_dirsCount = expectedDirsCount(tasksCount); + m_tempDir.reset(new QTemporaryDir); + qDebug() << "Generating on" << tasksCount << "cores..."; + qDebug() << "Generating" << m_filesCount << "files..."; + qDebug() << "Generating" << m_dirsCount << "dirs..."; + qDebug() << "Generating inside" << m_tempDir->path() << "dir..."; + const QString templateFile = m_tempDir->filePath("fileTemplate.txt"); + { + QFile file(templateFile); + QVERIFY(file.open(QIODevice::ReadWrite)); + file.write("X"); + } + + // Parallelize tree generation + const QDir parentDir(m_tempDir->path()); + const auto onSetup = [](const QString &parentPath, const QString &templateFile) { + return [parentPath, templateFile](Async<void> &async) { + async.setConcurrentCallData(generate, parentPath, templateFile, s_treeDepth); + }; + }; + QList<TaskItem> tasks {parallel}; + for (int i = 0; i < tasksCount; ++i) { + const QString dirName = QString("%1%2").arg(s_dirPrefix).arg(i); + QVERIFY(parentDir.mkdir(dirName)); + tasks.append(AsyncTask<void>(onSetup(parentDir.filePath(dirName), templateFile))); + } + + TaskTree taskTree(tasks); + QVERIFY(taskTree.runBlocking()); + } + + void cleanupTestCase() + { + QTC_SCOPED_TIMER("CLEANING UP"); + + // Parallelize tree removal + const auto removeTree = [](const QString &parentPath) { + FilePath::fromString(parentPath).removeRecursively(); + }; + + const QDir parentDir(m_tempDir->path()); + const auto onSetup = [removeTree](const QString &parentPath) { + return [parentPath, removeTree](Async<void> &async) { + async.setConcurrentCallData(removeTree, parentPath); + }; + }; + QList<TaskItem> tasks {parallel}; + const int tasksCount = QThread::idealThreadCount(); + for (int i = 0; i < tasksCount; ++i) { + const QString dirName = QString("%1%2").arg(s_dirPrefix).arg(i); + tasks.append(AsyncTask<void>(onSetup(parentDir.filePath(dirName)))); + } + + TaskTree taskTree(tasks); + QVERIFY(taskTree.runBlocking()); + + m_tempDir.reset(); + Singleton::deleteAll(); + } + + void testSubDirFileIterator() + { + QTC_SCOPED_TIMER("ITERATING with SubDirFileIterator"); + int filesCount = 0; + { + const FilePath root(FilePath::fromString(m_tempDir->path())); + SubDirFileIterator it({root}, {}, {}); + auto i = it.begin(); + const auto e = it.end(); + while (i != e) { + ++filesCount; + ++i; + if (filesCount % 100000 == 0) + qDebug() << filesCount << '/' << m_filesCount << "files visited so far..."; + } + } + qDebug() << "Visited" << filesCount << "files."; + } + + void testManualIterator() + { + QTC_SCOPED_TIMER("ITERATING with manual iterator"); + int filesCount = 0; + int dirsCount = 0; + { + const QDir root(m_tempDir->path()); + std::unordered_set<QString> visitedFiles; + std::unordered_set<QString> visitedDirs; + visitedDirs.emplace(root.absolutePath()); + QFileInfoList workingList = root.entryInfoList(s_filters); + ++dirsCount; // for root itself + while (!workingList.isEmpty()) { + const QFileInfo fi = workingList.takeLast(); + const QString absoluteFilePath = fi.absoluteFilePath(); + if (fi.isDir()) { + if (!visitedDirs.emplace(absoluteFilePath).second) + continue; + if (fi.isSymbolicLink() && !visitedDirs.emplace(fi.symLinkTarget()).second) + continue; + ++dirsCount; + workingList.append(QDir(absoluteFilePath).entryInfoList(s_filters)); + } else { + if (!visitedFiles.emplace(absoluteFilePath).second) + continue; + if (fi.isSymbolicLink() && !visitedFiles.emplace(fi.symLinkTarget()).second) + continue; + ++filesCount; + if (filesCount % 100000 == 0) + qDebug() << filesCount << '/' << m_filesCount << "files visited so far..."; + } + } + } + qDebug() << "Visited" << filesCount << "files and" << dirsCount << "directories."; + } + + void testQDirIterator() + { + QTC_SCOPED_TIMER("ITERATING with QDirIterator"); + int filesCount = 0; + int dirsCount = 0; + { + ++dirsCount; // for root itself + QDirIterator it(m_tempDir->path(), s_filters, QDirIterator::Subdirectories + | QDirIterator::FollowSymlinks); + while (it.hasNext()) { + const QFileInfo fi = it.nextFileInfo(); + if (fi.isDir()) { + ++dirsCount; + } else { + ++filesCount; + if (filesCount % 100000 == 0) + qDebug() << filesCount << '/' << m_filesCount << "files visited so far..."; + } + } + } + qDebug() << "Visited" << filesCount << "files and" << dirsCount << "directories."; + } + +private: + int m_dirsCount = 0; + int m_filesCount = 0; + std::unique_ptr<QTemporaryDir> m_tempDir; +}; + +QTEST_GUILESS_MAIN(tst_SubDirFileIterator) + +#include "tst_subdirfileiterator.moc" |