blob: 917e86d754a7316c506c8e90562c7f2a58fe398f [file] [log] [blame]
// SPDX-License-Identifier: LGPL-2.1
/*
* Copyright (C) 2017 VMware Inc, Yordan Karadzhov <ykaradzhov@vmware.com>
*/
/**
* @file KsGLWidget.cpp
* @brief OpenGL widget for plotting trace graphs.
*/
// OpenGL
#include <GL/glut.h>
#include <GL/gl.h>
// KernelShark
#include "KsGLWidget.hpp"
#include "KsUtils.hpp"
#include "KsPlugins.hpp"
#include "KsDualMarker.hpp"
/** Create a default (empty) OpenGL widget. */
KsGLWidget::KsGLWidget(QWidget *parent)
: QOpenGLWidget(parent),
_hMargin(20),
_vMargin(30),
_vSpacing(20),
_mState(nullptr),
_data(nullptr),
_rubberBand(QRubberBand::Rectangle, this),
_rubberBandOrigin(0, 0),
_dpr(1)
{
setMouseTracking(true);
/*
* Using the old Signal-Slot syntax because QWidget::update has
* overloads.
*/
connect(&_model, SIGNAL(modelReset()), this, SLOT(update()));
}
/** Reimplemented function used to set up all required OpenGL resources. */
void KsGLWidget::initializeGL()
{
_dpr = QApplication::desktop()->devicePixelRatio();
ksplot_init_opengl(_dpr);
}
/**
* Reimplemented function used to reprocess all graphs whene the widget has
* been resized.
*/
void KsGLWidget::resizeGL(int w, int h)
{
ksplot_resize_opengl(w, h);
if(!_data)
return;
/*
* From the size of the widget, calculate the number of bins.
* One bin will correspond to one pixel.
*/
int nBins = width() - _hMargin * 2;
/*
* Reload the data. The range of the histogram is the same
* but the number of bins changes.
*/
ksmodel_set_bining(_model.histo(),
nBins,
_model.histo()->min,
_model.histo()->max);
_model.fill(_data->rows(), _data->size());
}
/** Reimplemented function used to plot trace graphs. */
void KsGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT);
loadColors();
/* Draw the time axis. */
if(_data)
_drawAxisX();
/* Process and draw all graphs by using the built-in logic. */
_makeGraphs(_cpuList, _taskList);
for (auto const &g: _graphs)
g->draw(1.5 * _dpr);
/* Process and draw all plugin-specific shapes. */
_makePluginShapes(_cpuList, _taskList);
while (!_shapes.empty()) {
auto s = _shapes.front();
s->draw();
delete s;
_shapes.pop_front();
}
/*
* Update and draw the markers. Make sure that the active marker
* is drawn on top.
*/
_mState->updateMarkers(*_data, this);
_mState->passiveMarker().draw();
_mState->activeMarker().draw();
}
/** Reimplemented event handler used to receive mouse press events. */
void KsGLWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
_posMousePress = _posInRange(event->pos().x());
_rangeBoundInit(_posMousePress);
}
}
int KsGLWidget::_getLastTask(struct kshark_trace_histo *histo,
int bin, int cpu)
{
kshark_context *kshark_ctx(nullptr);
kshark_entry_collection *col;
int pid;
if (!kshark_instance(&kshark_ctx))
return KS_EMPTY_BIN;
col = kshark_find_data_collection(kshark_ctx->collections,
KsUtils::matchCPUVisible,
cpu);
for (int b = bin; b >= 0; --b) {
pid = ksmodel_get_pid_back(histo, b, cpu, false, col, nullptr);
if (pid >= 0)
return pid;
}
return ksmodel_get_pid_back(histo, LOWER_OVERFLOW_BIN,
cpu,
false,
col,
nullptr);
}
int KsGLWidget::_getLastCPU(struct kshark_trace_histo *histo,
int bin, int pid)
{
kshark_context *kshark_ctx(nullptr);
kshark_entry_collection *col;
int cpu;
if (!kshark_instance(&kshark_ctx))
return KS_EMPTY_BIN;
col = kshark_find_data_collection(kshark_ctx->collections,
kshark_match_pid,
pid);
for (int b = bin; b >= 0; --b) {
cpu = ksmodel_get_cpu_back(histo, b, pid, false, col, nullptr);
if (cpu >= 0)
return cpu;
}
return ksmodel_get_cpu_back(histo, LOWER_OVERFLOW_BIN,
pid,
false,
col,
nullptr);
}
/** Reimplemented event handler used to receive mouse move events. */
void KsGLWidget::mouseMoveEvent(QMouseEvent *event)
{
int bin, cpu, pid;
size_t row;
bool ret;
if (_rubberBand.isVisible())
_rangeBoundStretched(_posInRange(event->pos().x()));
bin = event->pos().x() - _hMargin;
cpu = getPlotCPU(event->pos());
pid = getPlotPid(event->pos());
ret = _find(bin, cpu, pid, 5, false, &row);
if (ret) {
emit found(row);
} else {
if (cpu >= 0) {
pid = _getLastTask(_model.histo(), bin, cpu);
}
if (pid > 0) {
cpu = _getLastCPU(_model.histo(), bin, pid);
}
emit notFound(ksmodel_bin_ts(_model.histo(), bin), cpu, pid);
}
}
/** Reimplemented event handler used to receive mouse release events. */
void KsGLWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
size_t posMouseRel = _posInRange(event->pos().x());
int min, max;
if (_posMousePress < posMouseRel) {
min = _posMousePress - _hMargin;
max = posMouseRel - _hMargin;
} else {
max = _posMousePress - _hMargin;
min = posMouseRel - _hMargin;
}
_rangeChanged(min, max);
}
}
/** Reimplemented event handler used to receive mouse double click events. */
void KsGLWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
_findAndSelect(event);
}
/** Reimplemented event handler used to receive mouse wheel events. */
void KsGLWidget::wheelEvent(QWheelEvent * event)
{
int zoomFocus;
if (_mState->activeMarker()._isSet &&
_mState->activeMarker().isVisible()) {
/*
* Use the position of the marker as a focus point for the
* zoom.
*/
zoomFocus = _mState->activeMarker()._bin;
} else {
/*
* Use the position of the mouse as a focus point for the
* zoom.
*/
zoomFocus = event->pos().x() - _hMargin;
}
if (event->delta() > 0) {
_model.zoomIn(.05, zoomFocus);
} else {
_model.zoomOut(.05, zoomFocus);
}
_mState->updateMarkers(*_data, this);
}
/** Reimplemented event handler used to receive key press events. */
void KsGLWidget::keyPressEvent(QKeyEvent *event)
{
if (event->isAutoRepeat())
return;
switch (event->key()) {
case Qt::Key_Plus:
emit zoomIn();
return;
case Qt::Key_Minus:
emit zoomOut();
return;
case Qt::Key_Left:
emit scrollLeft();
return;
case Qt::Key_Right:
emit scrollRight();
return;
default:
QOpenGLWidget::keyPressEvent(event);
return;
}
}
/** Reimplemented event handler used to receive key release events. */
void KsGLWidget::keyReleaseEvent(QKeyEvent *event)
{
if (event->isAutoRepeat())
return;
if(event->key() == Qt::Key_Plus ||
event->key() == Qt::Key_Minus ||
event->key() == Qt::Key_Left ||
event->key() == Qt::Key_Right) {
emit stopUpdating();
return;
}
QOpenGLWidget::keyPressEvent(event);
return;
}
/**
* @brief Load and show trace data.
*
* @param data: Input location for the KsDataStore object.
* KsDataStore::loadDataFile() must be called first.
*/
void KsGLWidget::loadData(KsDataStore *data)
{
uint64_t tMin, tMax;
int nCPUs, nBins;
_data = data;
/*
* From the size of the widget, calculate the number of bins.
* One bin will correspond to one pixel.
*/
nBins = width() - _hMargin * 2;
nCPUs = tep_get_cpus(_data->tep());
_model.reset();
/* Now load the entire set of trace data. */
tMin = _data->rows()[0]->ts;
tMax = _data->rows()[_data->size() - 1]->ts;
ksmodel_set_bining(_model.histo(), nBins, tMin, tMax);
_model.fill(_data->rows(), _data->size());
/* Make a default CPU list. All CPUs will be plotted. */
_cpuList = {};
for (int i = 0; i < nCPUs; ++i)
_cpuList.append(i);
/* Make a default task list. No tasks will be plotted. */
_taskList = {};
loadColors();
_makeGraphs(_cpuList, _taskList);
}
/**
* Create a Hash table of Rainbow colors. The sorted Pid values are mapped to
* the palette of Rainbow colors.
*/
void KsGLWidget::loadColors()
{
_pidColors.clear();
_pidColors = KsPlot::getTaskColorTable();
_cpuColors.clear();
_cpuColors = KsPlot::getCPUColorTable();
}
/**
* Position the graphical elements of the marker according to the current
* position of the graphs inside the GL widget.
*/
void KsGLWidget::setMark(KsGraphMark *mark)
{
mark->_mark.setDPR(_dpr);
mark->_mark.setX(mark->_bin + _hMargin);
mark->_mark.setY(_vMargin / 2 + 2, height() - _vMargin);
if (mark->_cpu >= 0) {
mark->_mark.setCPUY(_graphs[mark->_cpu]->getBase());
mark->_mark.setCPUVisible(true);
} else {
mark->_mark.setCPUVisible(false);
}
if (mark->_task >= 0) {
mark->_mark.setTaskY(_graphs[mark->_task]->getBase());
mark->_mark.setTaskVisible(true);
} else {
mark->_mark.setTaskVisible(false);
}
}
/**
* @brief Check if a given KernelShark entry is ploted.
*
* @param e: Input location for the KernelShark entry.
* @param graphCPU: Output location for index of the CPU graph to which this
* entry belongs. If such a graph does not exist the outputted
* value is "-1".
* @param graphTask: Output location for index of the Task graph to which this
* entry belongs. If such a graph does not exist the
* outputted value is "-1".
*/
void KsGLWidget::findGraphIds(const kshark_entry &e,
int *graphCPU,
int *graphTask)
{
int graph(0);
bool cpuFound(false), taskFound(false);
/*
* Loop over all CPU graphs and try to find the one that
* contains the entry.
*/
for (auto const &c: _cpuList) {
if (c == e.cpu) {
cpuFound = true;
break;
}
++graph;
}
if (cpuFound)
*graphCPU = graph;
else
*graphCPU = -1;
/*
* Loop over all Task graphs and try to find the one that
* contains the entry.
*/
graph = _cpuList.count();
for (auto const &p: _taskList) {
if (p == e.pid) {
taskFound = true;
break;
}
++graph;
}
if (taskFound)
*graphTask = graph;
else
*graphTask = -1;
}
void KsGLWidget::_drawAxisX()
{
KsPlot::Point a0(_hMargin, _vMargin / 4), a1(_hMargin, _vMargin / 2);
KsPlot::Point b0(width()/2, _vMargin / 4), b1(width() / 2, _vMargin / 2);
KsPlot::Point c0(width() - _hMargin, _vMargin / 4),
c1(width() - _hMargin, _vMargin / 2);
int lineSize = 2 * _dpr;
a0._size = c0._size = _dpr;
a0.draw();
c0.draw();
KsPlot::drawLine(a0, a1, {}, lineSize);
KsPlot::drawLine(b0, b1, {}, lineSize);
KsPlot::drawLine(c0, c1, {}, lineSize);
KsPlot::drawLine(a0, c0, {}, lineSize);
}
void KsGLWidget::_makeGraphs(QVector<int> cpuList, QVector<int> taskList)
{
/* The very first thing to do is to clean up. */
for (auto &g: _graphs)
delete g;
_graphs.resize(0);
if (!_data || !_data->size())
return;
auto lamAddGraph = [&](KsPlot::Graph *graph) {
/*
* Calculate the base level of the CPU graph inside the widget.
* Remember that the "Y" coordinate is inverted.
*/
if (!graph)
return;
int base = _vMargin +
_vSpacing * _graphs.count() +
KS_GRAPH_HEIGHT * (_graphs.count() + 1);
graph->setBase(base);
_graphs.append(graph);
};
/* Create CPU graphs according to the cpuList. */
for (auto const &cpu: cpuList)
lamAddGraph(_newCPUGraph(cpu));
/* Create Task graphs taskList to the taskList. */
for (auto const &pid: taskList)
lamAddGraph(_newTaskGraph(pid));
}
void KsGLWidget::_makePluginShapes(QVector<int> cpuList, QVector<int> taskList)
{
kshark_context *kshark_ctx(nullptr);
kshark_event_handler *evt_handlers;
KsCppArgV cppArgv;
if (!kshark_instance(&kshark_ctx))
return;
cppArgv._histo = _model.histo();
cppArgv._shapes = &_shapes;
for (int g = 0; g < cpuList.count(); ++g) {
cppArgv._graph = _graphs[g];
evt_handlers = kshark_ctx->event_handlers;
while (evt_handlers) {
evt_handlers->draw_func(cppArgv.toC(),
cpuList[g],
KSHARK_PLUGIN_CPU_DRAW);
evt_handlers = evt_handlers->next;
}
}
for (int g = 0; g < taskList.count(); ++g) {
cppArgv._graph = _graphs[cpuList.count() + g];
evt_handlers = kshark_ctx->event_handlers;
while (evt_handlers) {
evt_handlers->draw_func(cppArgv.toC(),
taskList[g],
KSHARK_PLUGIN_TASK_DRAW);
evt_handlers = evt_handlers->next;
}
}
}
KsPlot::Graph *KsGLWidget::_newCPUGraph(int cpu)
{
/* The CPU graph needs to know only the colors of the tasks. */
KsPlot::Graph *graph = new KsPlot::Graph(_model.histo(),
&_pidColors,
&_pidColors);
graph->setZeroSuppressed(true);
kshark_context *kshark_ctx(nullptr);
kshark_entry_collection *col;
if (!kshark_instance(&kshark_ctx))
return nullptr;
graph->setHMargin(_hMargin);
graph->setHeight(KS_GRAPH_HEIGHT);
col = kshark_find_data_collection(kshark_ctx->collections,
KsUtils::matchCPUVisible,
cpu);
graph->setDataCollectionPtr(col);
graph->fillCPUGraph(cpu);
return graph;
}
KsPlot::Graph *KsGLWidget::_newTaskGraph(int pid)
{
/*
* The Task graph needs to know the colors of the tasks and the colors
* of the CPUs.
*/
KsPlot::Graph *graph = new KsPlot::Graph(_model.histo(),
&_pidColors,
&_cpuColors);
kshark_context *kshark_ctx(nullptr);
kshark_entry_collection *col;
if (!kshark_instance(&kshark_ctx))
return nullptr;
graph->setHMargin(_hMargin);
graph->setHeight(KS_GRAPH_HEIGHT);
col = kshark_find_data_collection(kshark_ctx->collections,
kshark_match_pid, pid);
if (!col) {
/*
* If a data collection for this task does not exist,
* register a new one.
*/
col = kshark_register_data_collection(kshark_ctx,
_data->rows(),
_data->size(),
kshark_match_pid, pid,
25);
}
/*
* Data collections are efficient only when used on graphs, having a
* lot of empty bins.
* TODO: Determine the optimal criteria to decide whether to use or
* not use data collection for this graph.
*/
if (_data->size() < 1e6 &&
col && col->size &&
_data->size() / col->size < 100) {
/*
* No need to use collection in this case. Free the collection
* data, but keep the collection registered. This will prevent
* from recalculating the same collection next time when this
* task is ploted.
*/
kshark_reset_data_collection(col);
}
graph->setDataCollectionPtr(col);
graph->fillTaskGraph(pid);
return graph;
}
/**
* @brief Find the KernelShark entry under the the cursor.
*
* @param point: The position of the cursor.
* @param variance: The variance of the position (range) in which an entry will
* be searched.
* @param joined: It True, search also in the associated CPU/Task graph.
* @param index: Output location for the index of the entry under the cursor.
* If no entry has been found, the outputted value is zero.
*
* @returns True, if an entry has been found, otherwise False.
*/
bool KsGLWidget::find(const QPoint &point, int variance, bool joined,
size_t *index)
{
/*
* Get the bin, pid and cpu numbers.
* Remember that one bin corresponds to one pixel.
*/
int bin = point.x() - _hMargin;
int cpu = getPlotCPU(point);
int pid = getPlotPid(point);
return _find(bin, cpu, pid, variance, joined, index);
}
int KsGLWidget::_getNextCPU(int pid, int bin)
{
kshark_context *kshark_ctx(nullptr);
kshark_entry_collection *col;
int cpu;
if (!kshark_instance(&kshark_ctx))
return KS_EMPTY_BIN;
col = kshark_find_data_collection(kshark_ctx->collections,
kshark_match_pid,
pid);
if (!col)
return KS_EMPTY_BIN;
for (int i = bin; i < _model.histo()->n_bins; ++i) {
cpu = ksmodel_get_cpu_front(_model.histo(), i, pid,
false, col, nullptr);
if (cpu >= 0)
return cpu;
}
return KS_EMPTY_BIN;
}
bool KsGLWidget::_find(int bin, int cpu, int pid,
int variance, bool joined, size_t *row)
{
int hSize = _model.histo()->n_bins;
ssize_t found;
if (bin < 0 || bin > hSize || (cpu < 0 && pid < 0)) {
/*
* The click is outside of the range of the histogram.
* Do nothing.
*/
*row = 0;
return false;
}
auto lamGetEntryByCPU = [&](int b) {
/* Get the first data entry in this bin. */
found = ksmodel_first_index_at_cpu(_model.histo(),
b, cpu);
if (found < 0) {
/*
* The bin is empty or the entire connect of the bin
* has been filtered.
*/
return false;
}
*row = found;
return true;
};
auto lamGetEntryByPid = [&](int b) {
/* Get the first data entry in this bin. */
found = ksmodel_first_index_at_pid(_model.histo(),
b, pid);
if (found < 0) {
/*
* The bin is empty or the entire connect of the bin
* has been filtered.
*/
return false;
}
*row = found;
return true;
};
auto lamFindEntryByCPU = [&](int b) {
/*
* The click is over the CPU graphs. First try the exact
* match.
*/
if (lamGetEntryByCPU(bin))
return true;
/* Now look for a match, nearby the position of the click. */
for (int i = 1; i < variance; ++i) {
if (bin + i <= hSize && lamGetEntryByCPU(bin + i))
return true;
if (bin - i >= 0 && lamGetEntryByCPU(bin - i))
return true;
}
*row = 0;
return false;
};
auto lamFindEntryByPid = [&](int b) {
/*
* The click is over the Task graphs. First try the exact
* match.
*/
if (lamGetEntryByPid(bin))
return true;
/* Now look for a match, nearby the position of the click. */
for (int i = 1; i < variance; ++i) {
if ((bin + i <= hSize) && lamGetEntryByPid(bin + i))
return true;
if ((bin - i >= 0) && lamGetEntryByPid(bin - i))
return true;
}
*row = 0;
return false;
};
if (cpu >= 0)
return lamFindEntryByCPU(bin);
if (pid >= 0) {
bool ret = lamFindEntryByPid(bin);
/*
* If no entry has been found and we have a joined search, look
* for an entry on the next CPU used by this task.
*/
if (!ret && joined) {
cpu = _getNextCPU(pid, bin);
ret = lamFindEntryByCPU(bin);
}
return ret;
}
*row = 0;
return false;
}
bool KsGLWidget::_findAndSelect(QMouseEvent *event)
{
size_t row;
bool found = find(event->pos(), 10, true, &row);
if (found) {
emit select(row);
emit updateView(row, true);
}
return found;
}
void KsGLWidget::_rangeBoundInit(int x)
{
/*
* Set the origin of the rubber band that shows the new range. Only
* the X coordinate of the origin matters. The Y coordinate will be
* set to zero.
*/
_rubberBandOrigin.rx() = x;
_rubberBandOrigin.ry() = 0;
_rubberBand.setGeometry(_rubberBandOrigin.x(),
_rubberBandOrigin.y(),
0, 0);
/* Make the rubber band visible, although its size is zero. */
_rubberBand.show();
}
void KsGLWidget::_rangeBoundStretched(int x)
{
QPoint pos;
pos.rx() = x;
pos.ry() = this->height();
/*
* Stretch the rubber band between the origin position and the current
* position of the mouse. Only the X coordinate matters. The Y
* coordinate will be the height of the widget.
*/
if (_rubberBandOrigin.x() < pos.x()) {
_rubberBand.setGeometry(QRect(_rubberBandOrigin.x(),
_rubberBandOrigin.y(),
pos.x() - _rubberBandOrigin.x(),
pos.y() - _rubberBandOrigin.y()));
} else {
_rubberBand.setGeometry(QRect(pos.x(),
_rubberBandOrigin.y(),
_rubberBandOrigin.x() - pos.x(),
pos.y() - _rubberBandOrigin.y()));
}
}
void KsGLWidget::_rangeChanged(int binMin, int binMax)
{
size_t nBins = _model.histo()->n_bins;
int binMark = _mState->activeMarker()._bin;
uint64_t min, max;
/* The rubber band is no longer needed. Make it invisible. */
_rubberBand.hide();
if ( (binMax - binMin) < 4) {
/* Most likely this is an accidental click. Do nothing. */
return;
}
/*
* Calculate the new range of the histogram. The number of bins will
* stay the same.
*/
min = ksmodel_bin_ts(_model.histo(), binMin);
max = ksmodel_bin_ts(_model.histo(), binMax);
if (max - min < nBins) {
/*
* The range cannot be smaller than the number of bins.
* Do nothing.
*/
return;
}
/* Recalculate the model and update the markers. */
ksmodel_set_bining(_model.histo(), nBins, min, max);
_model.fill(_data->rows(), _data->size());
_mState->updateMarkers(*_data, this);
/*
* If the Marker is inside the new range, make sure that it will
* be visible in the table. Note that for this check we use the
* bin number of the marker, retrieved before its update.
*/
if (_mState->activeMarker()._isSet &&
binMark < binMax && binMark > binMin) {
emit updateView(_mState->activeMarker()._pos, true);
return;
}
/*
* Find the first bin which contains unfiltered data and send a signal
* to the View widget to make this data visible.
*/
for (int bin = 0; bin < _model.histo()->n_bins; ++bin) {
int64_t row = ksmodel_first_index_at_bin(_model.histo(), bin);
if (row != KS_EMPTY_BIN &&
(_data->rows()[row]->visible & KS_TEXT_VIEW_FILTER_MASK)) {
emit updateView(row, false);
return;
}
}
}
int KsGLWidget::_posInRange(int x)
{
int posX;
if (x < _hMargin)
posX = _hMargin;
else if (x > (width() - _hMargin))
posX = width() - _hMargin;
else
posX = x;
return posX;
}
/** Get the CPU Id of the Graph plotted at given position. */
int KsGLWidget::getPlotCPU(const QPoint &point)
{
int cpuId, y = point.y();
if (_cpuList.count() == 0)
return -1;
cpuId = (y - _vMargin + _vSpacing / 2) / (_vSpacing + KS_GRAPH_HEIGHT);
if (cpuId < 0 || cpuId >= _cpuList.count())
return -1;
return _cpuList[cpuId];
}
/** Get the CPU Id of the Graph plotted at given position. */
int KsGLWidget::getPlotPid(const QPoint &point)
{
int pidId, y = point.y();
if (_taskList.count() == 0)
return -1;
pidId = (y - _vMargin -
_cpuList.count()*(KS_GRAPH_HEIGHT + _vSpacing) +
_vSpacing / 2) / (_vSpacing + KS_GRAPH_HEIGHT);
if (pidId < 0 || pidId >= _taskList.count())
return -1;
return _taskList[pidId];
}