Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support regex search #676

Merged
merged 1 commit into from
Nov 18, 2024
Merged
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
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()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be !expression.isValid() || expression.pattern().isEmpty()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it isn't valid we should not get here, at least not when executed from the UI (the function could possibly assert that or do an early-exit for the invalid expression)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack, that lgtm as-is

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"));
milianw marked this conversation as resolved.
Show resolved Hide resolved
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
7 changes: 7 additions & 0 deletions src/resultsbottomuppage.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
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
7 changes: 7 additions & 0 deletions src/resultstopdownpage.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
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