Skip to content

Commit

Permalink
feat: support regex search
Browse files Browse the repository at this point in the history
Since we use QSortFilterProxyModel we get this for free.
This patch adds support for regex search by not escaping the search term
if it is prefix with %.
For the flamegraph there is a custom implementation, which changes the
current QString::contains to a QRegularExpression::match call.

Closes: #666
  • Loading branch information
lievenhey committed Oct 10, 2024
1 parent 36bedef commit 593437c
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 43 deletions.
37 changes: 25 additions & 12 deletions src/flamegraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -546,14 +546,13 @@ struct SearchResults
qint64 directCost = 0;
};

SearchResults applySearch(FrameGraphicsItem* item, const QString& searchValue)
SearchResults applySearch(FrameGraphicsItem* item, const QRegularExpression& expression)
{
SearchResults result;
if (searchValue.isEmpty()) {
if (expression.pattern().isEmpty()) {
result.matchType = NoSearch;
} else if (item->symbol().symbol.contains(searchValue, Qt::CaseInsensitive)
|| (searchValue == QLatin1String("??") && item->symbol().symbol.isEmpty())
|| item->symbol().binary.contains(searchValue, Qt::CaseInsensitive)) {
} else if (expression.match(item->symbol().symbol).hasMatch() || expression.match(item->symbol().binary).hasMatch()
|| (expression.pattern() == QLatin1String("\\?\\?") && item->symbol().symbol.isEmpty())) {
result.directCost += item->cost();
result.matchType = DirectMatch;
}
Expand All @@ -562,7 +561,7 @@ SearchResults applySearch(FrameGraphicsItem* item, const QString& searchValue)
const auto children = item->childItems();
for (auto* child : children) {
auto* childFrame = static_cast<FrameGraphicsItem*>(child);
auto childMatch = applySearch(childFrame, searchValue);
auto childMatch = applySearch(childFrame, expression);
if (result.matchType != DirectMatch
&& (childMatch.matchType == DirectMatch || childMatch.matchType == ChildMatch)) {
result.matchType = ChildMatch;
Expand Down Expand Up @@ -805,13 +804,26 @@ FlameGraph::FlameGraph(QWidget* parent, Qt::WindowFlags flags)
searchInput->setMinimumWidth(200);
layout->addWidget(searchInput);

auto regexCheckBox = new QCheckBox(widget);
regexCheckBox->setText(tr("Regex Search"));
layout->addWidget(regexCheckBox);

searchInput->setPlaceholderText(i18n("Search..."));
searchInput->setToolTip(i18n("<qt>Search the flame graph for a symbol.</qt>"));
searchInput->setClearButtonEnabled(true);
connect(searchInput, &QLineEdit::textChanged, this, &FlameGraph::setSearchValue);
connect(this, &FlameGraph::uiResetRequested, this, [this, searchInput] {
connect(searchInput, &QLineEdit::textChanged, this,
[this](const QString& value) { this->setSearchValue(value, m_useRegex); });
auto applyRegexCheckBox = [this](bool checked) { this->setSearchValue(m_search, checked); };
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
connect(regexCheckBox, &QCheckBox::stateChanged, this, applyRegexCheckBox);
#else
connect(regexCheckBox, &QCheckBox::checkStateChanged, this, applyRegexCheckBox);
#endif
connect(this, &FlameGraph::uiResetRequested, this, [this, searchInput, regexCheckBox] {
m_search.clear();
m_useRegex = false;
searchInput->clear();
regexCheckBox->setChecked(false);
});
},
this);
Expand Down Expand Up @@ -1140,7 +1152,7 @@ void FlameGraph::setData(FrameGraphicsItem* rootItem)
m_scene->addItem(rootItem);

if (!m_search.isEmpty()) {
setSearchValue(m_search);
setSearchValue(m_search, m_useRegex);
}
if (!m_hoveredStacks.isEmpty()) {
hoverStacks(rootItem, m_hoveredStacks);
Expand Down Expand Up @@ -1204,15 +1216,16 @@ void FlameGraph::selectItem(FrameGraphicsItem* item)
setTooltipItem(item);
}

void FlameGraph::setSearchValue(const QString& value)
void FlameGraph::setSearchValue(const QString& value, bool useRegex)
{
if (!m_rootItem) {
return;
}

m_search = value;

auto match = applySearch(m_rootItem, value);
m_useRegex = useRegex;
auto regex = useRegex ? value : QRegularExpression::escape(value);
auto match = applySearch(m_rootItem, QRegularExpression(regex));

if (value.isEmpty()) {
m_searchResultsLabel->hide();
Expand Down
3 changes: 2 additions & 1 deletion src/flamegraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class FlameGraph : public QWidget

private slots:
void setData(FrameGraphicsItem* rootItem);
void setSearchValue(const QString& value);
void setSearchValue(const QString& value, bool useRegex);
void navigateBack();
void navigateForward();

Expand Down Expand Up @@ -88,6 +88,7 @@ private slots:
bool m_collapseRecursion = false;
bool m_buildingScene = false;
QString m_search;
bool m_useRegex = false;
// cost threshold in percent, items below that value will not be shown
static const constexpr double DEFAULT_COST_THRESHOLD = 0.1;
double m_costThreshold = DEFAULT_COST_THRESHOLD;
Expand Down
2 changes: 1 addition & 1 deletion src/recordpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ RecordPage::RecordPage(QWidget* parent)
m_recordHost->setPids(pids);
});

ResultsUtil::connectFilter(ui->processesFilterBox, m_processProxyModel);
ResultsUtil::connectFilter(ui->processesFilterBox, m_processProxyModel, ui->regexCheckBox);

connect(m_watcher, &QFutureWatcher<ProcDataList>::finished, this, &RecordPage::updateProcessesFinished);

Expand Down
42 changes: 32 additions & 10 deletions src/recordpage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,6 @@
<property name="text">
<string>Process Filter:</string>
</property>
<property name="buddy">
<cstring>processesFilterBox</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="processesFilterBox">
<property name="toolTip">
<string>Filter the process list by process name or process ID</string>
</property>
</widget>
</item>
<item row="1" column="0">
Expand Down Expand Up @@ -185,6 +175,38 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="processesFilterBox">
<property name="toolTip">
<string>Filter the process list by process name or process ID</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="regexCheckBox">
<property name="text">
<string>Regex Search</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
Expand Down
3 changes: 2 additions & 1 deletion src/resultsbottomuppage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ ResultsBottomUpPage::ResultsBottomUpPage(FilterAndZoomStack* filterStack, PerfPa
ui->setupUi(this);

auto bottomUpCostModel = new BottomUpModel(this);
ResultsUtil::setupTreeView(ui->bottomUpTreeView, contextMenu, ui->bottomUpSearch, bottomUpCostModel);
ResultsUtil::setupTreeView(ui->bottomUpTreeView, contextMenu, ui->bottomUpSearch, ui->regexCheckBox,
bottomUpCostModel);
ResultsUtil::setupCostDelegate(bottomUpCostModel, ui->bottomUpTreeView);
ResultsUtil::setupContextMenu(ui->bottomUpTreeView, contextMenu, bottomUpCostModel, filterStack, this);

Expand Down
9 changes: 8 additions & 1 deletion src/resultsbottomuppage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@
<item>
<widget class="QLineEdit" name="bottomUpSearch">
<property name="toolTip">
<string>Filter the call graph tree.</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Filter the call graph tree.&lt;br/&gt;Prefix with '%' to turn it into an regex.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="regexCheckBox">
<property name="text">
<string>Regex Search</string>
</property>
</widget>
</item>
Expand Down
2 changes: 1 addition & 1 deletion src/resultscallercalleepage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ ResultsCallerCalleePage::ResultsCallerCalleePage(FilterAndZoomStack* filterStack
m_callerCalleeProxy = new CallerCalleeProxy<CallerCalleeModel>(this);
m_callerCalleeProxy->setSourceModel(m_callerCalleeCostModel);
m_callerCalleeProxy->setSortRole(CallerCalleeModel::SortRole);
ResultsUtil::connectFilter(ui->callerCalleeFilter, m_callerCalleeProxy);
ResultsUtil::connectFilter(ui->callerCalleeFilter, m_callerCalleeProxy, ui->regexCheckBox);
ui->callerCalleeTableView->setSortingEnabled(true);
ui->callerCalleeTableView->setModel(m_callerCalleeProxy);
ResultsUtil::setupContextMenu(ui->callerCalleeTableView, contextMenu, m_callerCalleeCostModel, filterStack, this,
Expand Down
7 changes: 7 additions & 0 deletions src/resultscallercalleepage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="regexCheckBox">
<property name="text">
<string>Regex Search</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
Expand Down
3 changes: 2 additions & 1 deletion src/resultstopdownpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ ResultsTopDownPage::ResultsTopDownPage(FilterAndZoomStack* filterStack, PerfPars
ui->setupUi(this);

auto topDownCostModel = new TopDownModel(this);
ResultsUtil::setupTreeView(ui->topDownTreeView, contextMenu, ui->topDownSearch, topDownCostModel);
ResultsUtil::setupTreeView(ui->topDownTreeView, contextMenu, ui->topDownSearch, ui->regexCheckBox,
topDownCostModel);
ResultsUtil::setupCostDelegate(topDownCostModel, ui->topDownTreeView);
ResultsUtil::setupContextMenu(ui->topDownTreeView, contextMenu, topDownCostModel, filterStack, this);

Expand Down
9 changes: 8 additions & 1 deletion src/resultstopdownpage.ui
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@
<item>
<widget class="QLineEdit" name="topDownSearch">
<property name="toolTip">
<string>Filter the call graph tree.</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Filter the call graph tree.&lt;br/&gt;Prefix with '%' to turn it into an regex.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="regexCheckBox">
<property name="text">
<string>Regex Search</string>
</property>
</widget>
</item>
Expand Down
27 changes: 20 additions & 7 deletions src/resultsutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "resultsutil.h"

#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QHeaderView>
Expand All @@ -33,8 +34,9 @@ void setupHeaderView(QTreeView* view, CostContextMenu* contextMenu)
view->setHeader(new CostHeaderView(contextMenu, view));
}

void connectFilter(QLineEdit* filter, QSortFilterProxyModel* proxy)
void connectFilter(QLineEdit* filter, QSortFilterProxyModel* proxy, QCheckBox* regexCheckBox)
{
Q_ASSERT(regexCheckBox);
auto* timer = new QTimer(filter);
timer->setSingleShot(true);

Expand All @@ -44,17 +46,28 @@ void connectFilter(QLineEdit* filter, QSortFilterProxyModel* proxy)
proxy->setFilterKeyColumn(-1);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);

QObject::connect(timer, &QTimer::timeout, proxy, [filter, proxy]() {
proxy->setFilterRegularExpression(QRegularExpression::escape(filter->text()));
});
auto setFilterNeedle = [filter, proxy, regexCheckBox]() {
auto useRegex = regexCheckBox->isChecked();
const auto needle = filter->text();
proxy->setFilterRegularExpression(useRegex ? needle : QRegularExpression::escape(needle));
};

QObject::connect(timer, &QTimer::timeout, proxy, setFilterNeedle);
if (regexCheckBox) {
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
QObject::connect(regexCheckBox, &QCheckBox::stateChanged, proxy, setFilterNeedle);
#else
QObject::connect(regexCheckBox, &QCheckBox::checkStateChanged, proxy, setFilterNeedle);
#endif
}
QObject::connect(filter, &QLineEdit::textChanged, timer, [timer]() { timer->start(300); });
}

void setupTreeView(QTreeView* view, CostContextMenu* contextMenu, QLineEdit* filter, QSortFilterProxyModel* model,
int initialSortColumn, int sortRole)
void setupTreeView(QTreeView* view, CostContextMenu* contextMenu, QLineEdit* filter, QCheckBox* regexSearchCheckbox,
QSortFilterProxyModel* model, int initialSortColumn, int sortRole)
{
model->setSortRole(sortRole);
connectFilter(filter, model);
connectFilter(filter, model, regexSearchCheckbox);

view->setModel(model);
setupHeaderView(view, contextMenu);
Expand Down
14 changes: 8 additions & 6 deletions src/resultsutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class QComboBox;
class QLineEdit;
class QSortFilterProxyModel;
class QAbstractItemModel;
class QCheckBox;

namespace Data {
class Costs;
Expand All @@ -31,18 +32,19 @@ class CostContextMenu;
namespace ResultsUtil {
void setupHeaderView(QTreeView* view, CostContextMenu* contextMenu);

void connectFilter(QLineEdit* filter, QSortFilterProxyModel* proxy);
void connectFilter(QLineEdit* filter, QSortFilterProxyModel* proxy, QCheckBox* regexCheckBox);

void setupTreeView(QTreeView* view, CostContextMenu* contextMenu, QLineEdit* filter, QSortFilterProxyModel* model,
int initialSortColumn, int sortRole);
void setupTreeView(QTreeView* view, CostContextMenu* contextMenu, QLineEdit* filter, QCheckBox* regexSearchCheckBox,
QSortFilterProxyModel* model, int initialSortColumn, int sortRole);

template<typename Model>
void setupTreeView(QTreeView* view, CostContextMenu* costContextMenu, QLineEdit* filter, Model* model)
void setupTreeView(QTreeView* view, CostContextMenu* costContextMenu, QLineEdit* filter, QCheckBox* regexSearchCheckBox,
Model* model)
{
auto* proxy = new CostProxy<Model>(view);
proxy->setSourceModel(model);
setupTreeView(view, costContextMenu, filter, qobject_cast<QSortFilterProxyModel*>(proxy), Model::InitialSortColumn,
Model::SortRole);
setupTreeView(view, costContextMenu, filter, regexSearchCheckBox, qobject_cast<QSortFilterProxyModel*>(proxy),
Model::InitialSortColumn, Model::SortRole);
}

void setupCostDelegate(QAbstractItemModel* model, QTreeView* view, int sortRole, int totalCostRole, int numBaseColumns);
Expand Down
2 changes: 1 addition & 1 deletion src/timelinewidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ TimeLineWidget::TimeLineWidget(PerfParser* parser, QMenu* filterMenu, FilterAndZ
timeLineProxy->setSortRole(EventModel::SortRole);
timeLineProxy->setFilterKeyColumn(EventModel::ThreadColumn);
timeLineProxy->setFilterRole(Qt::DisplayRole);
ResultsUtil::connectFilter(ui->timeLineSearch, timeLineProxy);
ResultsUtil::connectFilter(ui->timeLineSearch, timeLineProxy, ui->regexCheckBox);
ui->timeLineView->setModel(timeLineProxy);
ui->timeLineView->setSortingEnabled(true);
// ensure the vertical scroll bar is always shown, otherwise the timeline
Expand Down
7 changes: 7 additions & 0 deletions src/timelinewidget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="regexCheckBox">
<property name="text">
<string>Regex Search</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
Expand Down

0 comments on commit 593437c

Please sign in to comment.