Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/plugins/score-plugin-library/Library/ProcessesItemModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <QElapsedTimer>
#include <QIcon>
#include <QMimeData>
#include <QThreadPool>
#include <QTimer>

namespace Library
Expand All @@ -35,15 +36,121 @@ ProcessesItemModel::ProcessesItemModel(
const score::GUIApplicationContext& ctx, QObject* parent)
: TreeNodeBasedItemModel<ProcessNode>{parent}
, context{ctx}
, m_alive{std::make_shared<std::atomic_bool>(true)}
{
auto& procs = ctx.interfaces<Process::ProcessFactoryList>();
procs.added.connect<&ProcessesItemModel::on_newPlugin>(*this);

// Node pointers handed to the search indexing worker are invalidated by
// resets and removals; drop any in-flight result when that happens.
auto invalidate = [this] { ++m_searchIndexGeneration; };
connect(this, &QAbstractItemModel::modelAboutToBeReset, this, invalidate);
connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, this, invalidate);

// Some scanners (LV2, RecursiveWatch commits...) add nodes without
// begin/endInsertRows, so there is no reliable "node added" signal: poll
// instead. The sweep is a cheap in-memory tree walk when there is nothing
// new to index.
m_searchIndexTimer.setInterval(3000);
connect(&m_searchIndexTimer, &QTimer::timeout, this, [this] { indexForSearch(); });
m_searchIndexTimer.start();

auto& lib = context.settings<Library::Settings::Model>();
con(lib, &Library::Settings::Model::rescanLibrary, this, &ProcessesItemModel::rescan);
rescan();
}

ProcessesItemModel::~ProcessesItemModel()
{
*m_alive = false;
}

void ProcessesItemModel::indexForSearch()
{
if(m_searchIndexRunning)
return;

struct Item
{
ProcessNode* node{};
Process::ProcessModelFactory* factory{};
QString customData;
QString prettyName;
};

// GUI thread: collect the nodes which still need their search data, and
// resolve their factory here so that the worker never touches the
// interface list (which can grow when addons are loaded).
auto items = std::make_shared<std::vector<Item>>();
auto& factories = context.interfaces<Process::ProcessFactoryList>();
auto collect = [&](auto&& self, ProcessNode& node) -> void {
if(node.key != Process::ProcessModelFactory::ConcreteKey{}
&& node.searchString.isEmpty())
{
if(auto* f = factories.get(node.key))
items->push_back({&node, f, node.customData, node.prettyName});
}
for(auto& child : node)
self(self, child);
};
collect(collect, m_root);

if(items->empty())
return;

m_searchIndexRunning = true;
const auto generation = m_searchIndexGeneration;

// Worker thread: descriptor() is potentially very slow (it may parse the
// file behind the node); this is the whole reason this runs off the GUI
// thread. Results are delivered in packets to avoid clobbering the main
// loop with tens of thousands of queued events.
QThreadPool::globalInstance()->start(
[this, items = std::move(items), generation, alive = m_alive] {
constexpr std::size_t packetSize = 100;
std::vector<std::pair<ProcessNode*, QString>> packet;
packet.reserve(packetSize);

auto flush = [&] {
if(packet.empty())
return;
QMetaObject::invokeMethod(
this, [this, generation, packet = std::move(packet)] {
if(generation != m_searchIndexGeneration)
return;
for(const auto& [node, str] : packet)
node->searchString = str;
}, Qt::QueuedConnection);
packet.clear();
packet.reserve(packetSize);
};

for(const Item& item : *items)
{
if(!*alive)
return;

const auto desc = item.factory->descriptor(item.customData);
QStringList parts{item.prettyName, desc.prettyName, desc.description};
parts += desc.tags;
parts.removeAll(QString{});
parts.removeDuplicates();

// Never leave the string empty: an empty searchString means
// "not indexed yet" and the node would be rescanned forever.
QString str = parts.isEmpty() ? QStringLiteral("|") : parts.join(u'|').toLower();

packet.emplace_back(item.node, std::move(str));
if(packet.size() >= packetSize)
flush();
}
flush();

QMetaObject::invokeMethod(
this, [this] { m_searchIndexRunning = false; }, Qt::QueuedConnection);
});
}

ProcessNode& ProcessesItemModel::addCategory(const QString& c)
{
auto split = c.split("/");
Expand Down
24 changes: 24 additions & 0 deletions src/plugins/score-plugin-library/Library/ProcessesItemModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

#include <QDir>
#include <QIcon>
#include <QTimer>

#include <atomic>
#include <memory>

#include <nano_observer.hpp>
#include <score_plugin_library_export.h>
Expand All @@ -32,6 +36,12 @@ namespace Library
struct ProcessData : Process::ProcessData
{
QIcon icon;

//! Lower-case, '|'-separated search data (name, description, tags...).
//! Empty until the async deep scan has indexed this node; the search
//! filter then matches against it in-memory, without ever calling
//! ProcessModelFactory::descriptor() on the search path.
QString searchString;
};

using ProcessNode = TreeNode<ProcessData>;
Expand All @@ -54,6 +64,7 @@ class SCORE_PLUGIN_LIBRARY_EXPORT ProcessesItemModel
using QAbstractItemModel::endResetModel;

ProcessesItemModel(const score::GUIApplicationContext& ctx, QObject* parent);
~ProcessesItemModel();

void rescan();
QModelIndex find(const Process::ProcessModelFactory::ConcreteKey& k);
Expand All @@ -77,8 +88,21 @@ class SCORE_PLUGIN_LIBRARY_EXPORT ProcessesItemModel

private:
ProcessNode& addCategory(const QString& cat);

// Second-level async scan: computes ProcessData::searchString (tags,
// description...) on a worker thread, off the fast name-only scan and off
// the search path. Results come back to the GUI thread in packets.
void indexForSearch();

const score::GUIApplicationContext& context;
ProcessNode m_root;

QTimer m_searchIndexTimer;
//! Bumped whenever ProcessNode pointers may dangle (model reset, row
//! removal); in-flight indexing results from older generations are dropped.
uint64_t m_searchIndexGeneration{};
bool m_searchIndexRunning{};
std::shared_ptr<std::atomic_bool> m_alive;
};

/** Utility class to organize a library in subcategories that depend
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ bool ProcessFilterProxy::filterAcceptsRowItself(
auto model = static_cast<ProcessesItemModel*>(sourceModel());
auto& node = model->nodeFromModelIndex(index);

return node.prettyName.contains(m_textPattern, Qt::CaseInsensitive);
if(node.prettyName.contains(m_textPattern, Qt::CaseInsensitive))
return true;

// Also match against the search data (tags, description...) computed
// asynchronously by ProcessesItemModel::indexForSearch(). Purely in-memory:
// the search path never calls into the factories.
return node.searchString.contains(m_textPattern, Qt::CaseInsensitive);
}
}
Loading