/* io_graph_dialog.cpp * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "io_graph_dialog.h" #include #include "file.h" #include #include "epan/stats_tree_priv.h" #include "epan/uat-int.h" #include #include #include #include #include #include "progress_frame.h" #include "wireshark_application.h" #include #include //provides some default colors #include #include #include #include #include #include #include #include #include #include #include #include // Bugs and uncertainties: // - Regular (non-stacked) bar graphs are drawn on top of each other on the Z axis. // The QCP forum suggests drawing them side by side: // http://www.qcustomplot.com/index.php/support/forum/62 // - We retap and redraw more than we should. // - Smoothing doesn't seem to match GTK+ // To do: // - Use scroll bars? // - Scroll during live captures // - Set ticks per pixel (e.g. pressing "2" sets 2 tpp). const qreal graph_line_width_ = 1.0; const int DEFAULT_MOVING_AVERAGE = 0; // Don't accidentally zoom into a 1x1 rect if you happen to click on the graph // in zoom mode. const int min_zoom_pixels_ = 20; const int stat_update_interval_ = 200; // ms // Saved graph settings typedef struct _io_graph_settings_t { gboolean enabled; char* name; char* dfilter; guint color; guint32 style; guint32 yaxis; char* yfield; guint32 sma_period; } io_graph_settings_t; static const value_string graph_style_vs[] = { { IOGraph::psLine, "Line" }, { IOGraph::psImpulse, "Impulse" }, { IOGraph::psBar, "Bar" }, { IOGraph::psStackedBar, "Stacked Bar" }, { IOGraph::psDot, "Dot" }, { IOGraph::psSquare, "Square" }, { IOGraph::psDiamond, "Diamond" }, { 0, NULL } }; static const value_string y_axis_vs[] = { { IOG_ITEM_UNIT_PACKETS, "Packets" }, { IOG_ITEM_UNIT_BYTES, "Bytes" }, { IOG_ITEM_UNIT_BITS, "Bits" }, { IOG_ITEM_UNIT_CALC_SUM, "SUM(Y Field)" }, { IOG_ITEM_UNIT_CALC_FRAMES, "COUNT FRAMES(Y Field)" }, { IOG_ITEM_UNIT_CALC_FIELDS, "COUNT FIELDS(Y Field)" }, { IOG_ITEM_UNIT_CALC_MAX, "MAX(Y Field)" }, { IOG_ITEM_UNIT_CALC_MIN, "MIN(Y Field)" }, { IOG_ITEM_UNIT_CALC_AVERAGE, "AVG(Y Field)" }, { IOG_ITEM_UNIT_CALC_LOAD, "LOAD(Y Field)" }, { 0, NULL } }; static const value_string moving_avg_vs[] = { { 0, "None" }, { 10, "10 interval SMA" }, { 20, "20 interval SMA" }, { 50, "50 interval SMA" }, { 100, "100 interval SMA" }, { 200, "200 interval SMA" }, { 500, "500 interval SMA" }, { 1000, "1000 interval SMA" }, { 0, NULL } }; static io_graph_settings_t *iog_settings_ = NULL; static guint num_io_graphs_ = 0; static uat_t *iog_uat_ = NULL; extern "C" { //Allow the enable/disable field to be a checkbox, but for backwards compatibility, //the strings have to be "Enabled"/"Disabled", not "TRUE"/"FALSE" #define UAT_BOOL_ENABLE_CB_DEF(basename,field_name,rec_t) \ static void basename ## _ ## field_name ## _set_cb(void* rec, const char* buf, guint len, const void* UNUSED_PARAMETER(u1), const void* UNUSED_PARAMETER(u2)) {\ char* tmp_str = g_strndup(buf,len); \ if ((g_strcmp0(tmp_str, "Enabled") == 0) || \ (g_strcmp0(tmp_str, "TRUE") == 0)) \ ((rec_t*)rec)->field_name = 1; \ else \ ((rec_t*)rec)->field_name = 0; \ g_free(tmp_str); } \ static void basename ## _ ## field_name ## _tostr_cb(void* rec, char** out_ptr, unsigned* out_len, const void* UNUSED_PARAMETER(u1), const void* UNUSED_PARAMETER(u2)) {\ *out_ptr = g_strdup_printf("%s",((rec_t*)rec)->field_name ? "Enabled" : "Disabled"); \ *out_len = (unsigned)strlen(*out_ptr); } static gboolean uat_fld_chk_enable(void* u1 _U_, const char* strptr, guint len, const void* u2 _U_, const void* u3 _U_, char** err) { char* str = g_strndup(strptr,len); if ((g_strcmp0(str, "Enabled") == 0) || (g_strcmp0(str, "Disabled") == 0) || (g_strcmp0(str, "TRUE") == 0) || //just for UAT functionality (g_strcmp0(str, "FALSE") == 0)) { *err = NULL; g_free(str); return TRUE; } //User should never see this unless they are manually modifying UAT *err = g_strdup_printf("invalid value: %s (must be Enabled or Disabled)", str); g_free(str); return FALSE; } #define UAT_FLD_BOOL_ENABLE(basename,field_name,title,desc) \ {#field_name, title, PT_TXTMOD_BOOL,{uat_fld_chk_enable,basename ## _ ## field_name ## _set_cb,basename ## _ ## field_name ## _tostr_cb},{0,0,0},0,desc,FLDFILL} //"Custom" handler for sma_period enumeration for backwards compatibility static void io_graph_sma_period_set_cb(void* rec, const char* buf, guint len, const void* vs, const void* u2 _U_) { guint i; char* str = g_strndup(buf,len); const char* cstr; ((io_graph_settings_t*)rec)->sma_period = 0; //Original UAT had just raw numbers and not enumerated values with "interval SMA" if (strstr(str, "interval SMA") == NULL) { if (strcmp(str, "None") == 0) { //Valid enumerated value } else if (strcmp(str, "0") == 0) { g_free(str); str = g_strdup("None"); } else { char *str2 = g_strdup_printf("%s interval SMA", str); g_free(str); str = str2; } } for(i=0; ( cstr = ((const value_string*)vs)[i].strptr ) ;i++) { if (g_str_equal(cstr,str)) { ((io_graph_settings_t*)rec)->sma_period = (guint32)((const value_string*)vs)[i].value; g_free(str); return; } } g_free(str); } //Duplicated because macro covers both functions static void io_graph_sma_period_tostr_cb(void* rec, char** out_ptr, unsigned* out_len, const void* vs, const void* u2 _U_) { guint i; for(i=0;((const value_string*)vs)[i].strptr;i++) { if ( ((const value_string*)vs)[i].value == ((io_graph_settings_t*)rec)->sma_period ) { *out_ptr = g_strdup(((const value_string*)vs)[i].strptr); *out_len = (unsigned)strlen(*out_ptr); return; } } *out_ptr = g_strdup("None"); *out_len = (unsigned)strlen("None"); } static gboolean sma_period_chk_enum(void* u1 _U_, const char* strptr, guint len, const void* v, const void* u3 _U_, char** err) { char *str = g_strndup(strptr,len); guint i; const value_string* vs = (const value_string *)v; //Original UAT had just raw numbers and not enumerated values with "interval SMA" if (strstr(str, "interval SMA") == NULL) { if (strcmp(str, "None") == 0) { //Valid enumerated value } else if (strcmp(str, "0") == 0) { g_free(str); str = g_strdup("None"); } else { char *str2 = g_strdup_printf("%s interval SMA", str); g_free(str); str = str2; } } for(i=0;vs[i].strptr;i++) { if (g_strcmp0(vs[i].strptr,str) == 0) { *err = NULL; g_free(str); return TRUE; } } *err = g_strdup_printf("invalid value: %s",str); g_free(str); return FALSE; } #define UAT_FLD_SMA_PERIOD(basename,field_name,title,enum,desc) \ {#field_name, title, PT_TXTMOD_ENUM,{sma_period_chk_enum,basename ## _ ## field_name ## _set_cb,basename ## _ ## field_name ## _tostr_cb},{&(enum),&(enum),&(enum)},&(enum),desc,FLDFILL} UAT_BOOL_ENABLE_CB_DEF(io_graph, enabled, io_graph_settings_t) UAT_CSTRING_CB_DEF(io_graph, name, io_graph_settings_t) UAT_DISPLAY_FILTER_CB_DEF(io_graph, dfilter, io_graph_settings_t) UAT_COLOR_CB_DEF(io_graph, color, io_graph_settings_t) UAT_VS_DEF(io_graph, style, io_graph_settings_t, guint32, 0, "Line") UAT_VS_DEF(io_graph, yaxis, io_graph_settings_t, guint32, 0, "Packets") UAT_PROTO_FIELD_CB_DEF(io_graph, yfield, io_graph_settings_t) static uat_field_t io_graph_fields[] = { UAT_FLD_BOOL_ENABLE(io_graph, enabled, "Enabled", "Graph visibility"), UAT_FLD_CSTRING(io_graph, name, "Graph Name", "The name of the graph"), UAT_FLD_DISPLAY_FILTER(io_graph, dfilter, "Display Filter", "Graph packets matching this display filter"), UAT_FLD_COLOR(io_graph, color, "Color", "Graph color (#RRGGBB)"), UAT_FLD_VS(io_graph, style, "Style", graph_style_vs, "Graph style (Line, Bars, etc.)"), UAT_FLD_VS(io_graph, yaxis, "Y Axis", y_axis_vs, "Y Axis units"), UAT_FLD_PROTO_FIELD(io_graph, yfield, "Y Field", "Apply calculations to this field"), UAT_FLD_SMA_PERIOD(io_graph, sma_period, "SMA Period", moving_avg_vs, "Simple moving average period"), UAT_END_FIELDS }; static void* io_graph_copy_cb(void* dst_ptr, const void* src_ptr, size_t) { io_graph_settings_t* dst = (io_graph_settings_t *)dst_ptr; const io_graph_settings_t* src = (const io_graph_settings_t *)src_ptr; dst->enabled = src->enabled; dst->name = g_strdup(src->name); dst->dfilter = g_strdup(src->dfilter); dst->color = src->color; dst->style = src->style; dst->yaxis = src->yaxis; dst->yfield = g_strdup(src->yfield); dst->sma_period = src->sma_period; return dst; } static void io_graph_free_cb(void* p) { io_graph_settings_t *iogs = (io_graph_settings_t *)p; g_free(iogs->name); g_free(iogs->dfilter); g_free(iogs->yfield); } } // extern "C" IOGraphDialog::IOGraphDialog(QWidget &parent, CaptureFile &cf) : WiresharkDialog(parent, cf), ui(new Ui::IOGraphDialog), uat_model_(NULL), uat_delegate_(NULL), base_graph_(NULL), tracer_(NULL), start_time_(0.0), mouse_drags_(true), rubber_band_(NULL), stat_timer_(NULL), need_replot_(false), need_retap_(false), auto_axes_(true) { ui->setupUi(this); loadGeometry(); setWindowSubtitle(tr("IO Graphs")); setAttribute(Qt::WA_DeleteOnClose, true); QCustomPlot *iop = ui->ioPlot; QPushButton *save_bt = ui->buttonBox->button(QDialogButtonBox::Save); save_bt->setText(tr("Save As" UTF8_HORIZONTAL_ELLIPSIS)); QPushButton *copy_bt = ui->buttonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole); connect (copy_bt, SIGNAL(clicked()), this, SLOT(copyAsCsvClicked())); QPushButton *close_bt = ui->buttonBox->button(QDialogButtonBox::Close); if (close_bt) { close_bt->setDefault(true); } stat_timer_ = new QTimer(this); connect(stat_timer_, SIGNAL(timeout()), this, SLOT(updateStatistics())); stat_timer_->start(stat_update_interval_); // Intervals (ms) ui->intervalComboBox->addItem(tr("1 ms"), 1); ui->intervalComboBox->addItem(tr("10 ms"), 10); ui->intervalComboBox->addItem(tr("100 ms"), 100); ui->intervalComboBox->addItem(tr("1 sec"), 1000); ui->intervalComboBox->addItem(tr("10 sec"), 10000); ui->intervalComboBox->addItem(tr("1 min"), 60000); ui->intervalComboBox->addItem(tr("10 min"), 600000); ui->intervalComboBox->setCurrentIndex(3); ui->todCheckBox->setChecked(false); ui->dragRadioButton->setChecked(mouse_drags_); ctx_menu_.addAction(ui->actionZoomIn); ctx_menu_.addAction(ui->actionZoomInX); ctx_menu_.addAction(ui->actionZoomInY); ctx_menu_.addAction(ui->actionZoomOut); ctx_menu_.addAction(ui->actionZoomOutX); ctx_menu_.addAction(ui->actionZoomOutY); ctx_menu_.addAction(ui->actionReset); ctx_menu_.addSeparator(); ctx_menu_.addAction(ui->actionMoveRight10); ctx_menu_.addAction(ui->actionMoveLeft10); ctx_menu_.addAction(ui->actionMoveUp10); ctx_menu_.addAction(ui->actionMoveDown10); ctx_menu_.addAction(ui->actionMoveRight1); ctx_menu_.addAction(ui->actionMoveLeft1); ctx_menu_.addAction(ui->actionMoveUp1); ctx_menu_.addAction(ui->actionMoveDown1); ctx_menu_.addSeparator(); ctx_menu_.addAction(ui->actionGoToPacket); ctx_menu_.addSeparator(); ctx_menu_.addAction(ui->actionDragZoom); ctx_menu_.addAction(ui->actionToggleTimeOrigin); ctx_menu_.addAction(ui->actionCrosshairs); iop->xAxis->setLabel(tr("Time (s)")); iop->setMouseTracking(true); iop->setEnabled(true); QCPPlotTitle *title = new QCPPlotTitle(iop); iop->plotLayout()->insertRow(0); iop->plotLayout()->addElement(0, 0, title); title->setText(tr("Wireshark IO Graphs: %1").arg(cap_file_.fileTitle())); tracer_ = new QCPItemTracer(iop); iop->addItem(tracer_); loadProfileGraphs(); if (num_io_graphs_ > 0) { for (guint i = 0; i < num_io_graphs_; i++) { createIOGraph(i); } } else { addDefaultGraph(true, 0); addDefaultGraph(true, 1); } toggleTracerStyle(true); iop->setFocus(); iop->rescaleAxes(); //XXX - resize columns? ProgressFrame::addToButtonBox(ui->buttonBox, &parent); connect(iop, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(graphClicked(QMouseEvent*))); connect(iop, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(mouseMoved(QMouseEvent*))); connect(iop, SIGNAL(mouseRelease(QMouseEvent*)), this, SLOT(mouseReleased(QMouseEvent*))); disconnect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept())); } IOGraphDialog::~IOGraphDialog() { cap_file_.stopLoading(); foreach(IOGraph* iog, ioGraphs_) { delete iog; } delete ui; ui = NULL; } void IOGraphDialog::addGraph(bool checked, QString name, QString dfilter, int color_idx, IOGraph::PlotStyles style, io_graph_item_unit_t value_units, QString yfield, int moving_average) { // should not fail, but you never know. if (!uat_model_->insertRows(uat_model_->rowCount(), 1)) { qDebug() << "Failed to add a new record"; return; } int currentRow = uat_model_->rowCount() - 1; const QModelIndex &new_index = uat_model_->index(currentRow, 0); //populate model with data uat_model_->setData(uat_model_->index(currentRow, colEnabled), checked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole); uat_model_->setData(uat_model_->index(currentRow, colName), name); uat_model_->setData(uat_model_->index(currentRow, colDFilter), dfilter); uat_model_->setData(uat_model_->index(currentRow, colColor), QColor(color_idx)); uat_model_->setData(uat_model_->index(currentRow, colStyle), val_to_str_const(style, graph_style_vs, "None")); uat_model_->setData(uat_model_->index(currentRow, colYAxis), value_units); uat_model_->setData(uat_model_->index(currentRow, colYField), yfield); uat_model_->setData(uat_model_->index(currentRow, colSMAPeriod), moving_average); // due to an EditTrigger, this will also start editing. ui->graphUat->setCurrentIndex(new_index); createIOGraph(currentRow); } void IOGraphDialog::addGraph(bool copy_from_current) { const QModelIndex ¤t = ui->graphUat->currentIndex(); if (copy_from_current && !current.isValid()) return; if (copy_from_current) { // should not fail, but you never know. if (!uat_model_->insertRows(uat_model_->rowCount(), 1)) { qDebug() << "Failed to add a new record"; return; } const QModelIndex &new_index = uat_model_->index(uat_model_->rowCount() - 1, 0); if (copy_from_current) { uat_model_->copyRow(new_index.row(), current.row()); } ui->graphUat->setCurrentIndex(new_index); } else { addDefaultGraph(false); const QModelIndex &new_index = uat_model_->index(uat_model_->rowCount() - 1, 0); ui->graphUat->setCurrentIndex(new_index); } } void IOGraphDialog::createIOGraph(int currentRow) { // XXX - Should IOGraph have it's own list that has to sync with UAT? ioGraphs_.append(new IOGraph(ui->ioPlot)); IOGraph* iog = ioGraphs_[currentRow]; connect(this, SIGNAL(recalcGraphData(capture_file *, bool)), iog, SLOT(recalcGraphData(capture_file *, bool))); connect(this, SIGNAL(reloadValueUnitFields()), iog, SLOT(reloadValueUnitField())); connect(&cap_file_, SIGNAL(captureFileClosing()), iog, SLOT(captureFileClosing())); connect(iog, SIGNAL(requestRetap()), this, SLOT(scheduleRetap())); connect(iog, SIGNAL(requestRecalc()), this, SLOT(scheduleRecalc())); connect(iog, SIGNAL(requestReplot()), this, SLOT(scheduleReplot())); syncGraphSettings(currentRow); if (iog->visible()) { scheduleRetap(); } } void IOGraphDialog::addDefaultGraph(bool enabled, int idx) { switch (idx % 2) { case 0: addGraph(enabled, tr("All packets"), QString(), ColorUtils::graphColor(idx), IOGraph::psLine, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE); break; default: addGraph(enabled, tr("TCP errors"), "tcp.analysis.flags", ColorUtils::graphColor(idx), IOGraph::psBar, IOG_ITEM_UNIT_PACKETS, QString(), DEFAULT_MOVING_AVERAGE); break; } } // Sync the settings from UAT model to its IOGraph. // Disables the graph if any errors are found. // // NOTE: Setting dfilter, yaxis and yfield here will all end up in setFilter() and this // has a chicken-and-egg problem because setFilter() depends on previous assigned // values for filter_, val_units_ and vu_field_. Setting values in wrong order // may give unpredicted results because setFilter() does not always set filter_ // on errors. // TODO: The issues in the above note should be fixed and setFilter() should not be // called so frequently. void IOGraphDialog::syncGraphSettings(int row) { if (!uat_model_->index(row, colEnabled).isValid() || (ioGraphs_.size() <= row)) return; IOGraph *iog = ioGraphs_[row]; bool visible = uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool(); bool retap = !iog->visible() && visible; iog->setName(uat_model_->data(uat_model_->index(row, colName), Qt::DisplayRole).toString()); iog->setFilter(uat_model_->data(uat_model_->index(row, colDFilter), Qt::DisplayRole).toString()); /* plot style depend on the value unit, so set it first. */ iog->setValueUnits(uat_model_->data(uat_model_->index(row, colYAxis), Qt::DisplayRole).toUInt()); iog->setValueUnitField(uat_model_->data(uat_model_->index(row, colYField), Qt::DisplayRole).toString()); iog->setColor(QRgb(uat_model_->data(uat_model_->index(row, colColor), Qt::DisplayRole).toUInt())); iog->setPlotStyle(uat_model_->data(uat_model_->index(row, colStyle), Qt::DisplayRole).toUInt()); iog->moving_avg_period_ = uat_model_->data(uat_model_->index(row, colSMAPeriod), Qt::DisplayRole).toUInt(); iog->setInterval(ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt()); if (!iog->configError().isEmpty()) { hint_err_ = iog->configError(); visible = false; retap = false; } iog->setVisible(visible); getGraphInfo(); mouseMoved(NULL); // Update hint updateLegend(); if (visible) { if (retap) { scheduleRetap(); } else { scheduleReplot(); } } } void IOGraphDialog::updateWidgets() { WiresharkDialog::updateWidgets(); } void IOGraphDialog::scheduleReplot(bool now) { need_replot_ = true; if (now) updateStatistics(); // A plot finished, force an update of the legend now in case a time unit // was involved (which might append "(ms)" to the label). updateLegend(); } void IOGraphDialog::scheduleRecalc(bool now) { need_recalc_ = true; if (now) updateStatistics(); } void IOGraphDialog::scheduleRetap(bool now) { need_retap_ = true; if (now) updateStatistics(); } void IOGraphDialog::reloadFields() { emit reloadValueUnitFields(); } void IOGraphDialog::keyPressEvent(QKeyEvent *event) { int pan_pixels = event->modifiers() & Qt::ShiftModifier ? 1 : 10; switch(event->key()) { case Qt::Key_Minus: case Qt::Key_Underscore: // Shifted minus on U.S. keyboards case Qt::Key_O: // GTK+ case Qt::Key_R: zoomAxes(false); break; case Qt::Key_Plus: case Qt::Key_Equal: // Unshifted plus on U.S. keyboards case Qt::Key_I: // GTK+ zoomAxes(true); break; case Qt::Key_X: // Zoom X axis only if(event->modifiers() & Qt::ShiftModifier){ zoomXAxis(false); // upper case X -> Zoom out } else { zoomXAxis(true); // lower case x -> Zoom in } break; case Qt::Key_Y: // Zoom Y axis only if(event->modifiers() & Qt::ShiftModifier){ zoomYAxis(false); // upper case Y -> Zoom out } else { zoomYAxis(true); // lower case y -> Zoom in } break; case Qt::Key_Right: case Qt::Key_L: panAxes(pan_pixels, 0); break; case Qt::Key_Left: case Qt::Key_H: panAxes(-1 * pan_pixels, 0); break; case Qt::Key_Up: case Qt::Key_K: panAxes(0, pan_pixels); break; case Qt::Key_Down: case Qt::Key_J: panAxes(0, -1 * pan_pixels); break; case Qt::Key_Space: toggleTracerStyle(); break; case Qt::Key_0: case Qt::Key_ParenRight: // Shifted 0 on U.S. keyboards case Qt::Key_Home: resetAxes(); break; case Qt::Key_G: on_actionGoToPacket_triggered(); break; case Qt::Key_T: on_actionToggleTimeOrigin_triggered(); break; case Qt::Key_Z: on_actionDragZoom_triggered(); break; } QDialog::keyPressEvent(event); } void IOGraphDialog::reject() { if (!iog_uat_) return; //There is no "rejection" of the UAT created. Just save what we have if (iog_uat_->changed) { gchar *err = NULL; if (!uat_save(iog_uat_, &err)) { report_failure("Error while saving %s: %s", iog_uat_->name, err); g_free(err); } if (iog_uat_->post_update_cb) { iog_uat_->post_update_cb(); } } QDialog::reject(); } void IOGraphDialog::zoomAxes(bool in) { QCustomPlot *iop = ui->ioPlot; double h_factor = iop->axisRect()->rangeZoomFactor(Qt::Horizontal); double v_factor = iop->axisRect()->rangeZoomFactor(Qt::Vertical); auto_axes_ = false; if (!in) { h_factor = pow(h_factor, -1); v_factor = pow(v_factor, -1); } iop->xAxis->scaleRange(h_factor, iop->xAxis->range().center()); iop->yAxis->scaleRange(v_factor, iop->yAxis->range().center()); iop->replot(); } void IOGraphDialog::zoomXAxis(bool in) { QCustomPlot *iop = ui->ioPlot; double h_factor = iop->axisRect()->rangeZoomFactor(Qt::Horizontal); auto_axes_ = false; if (!in) { h_factor = pow(h_factor, -1); } iop->xAxis->scaleRange(h_factor, iop->xAxis->range().center()); iop->replot(); } void IOGraphDialog::zoomYAxis(bool in) { QCustomPlot *iop = ui->ioPlot; double v_factor = iop->axisRect()->rangeZoomFactor(Qt::Vertical); auto_axes_ = false; if (!in) { v_factor = pow(v_factor, -1); } iop->yAxis->scaleRange(v_factor, iop->yAxis->range().center()); iop->replot(); } void IOGraphDialog::panAxes(int x_pixels, int y_pixels) { QCustomPlot *iop = ui->ioPlot; double h_pan = 0.0; double v_pan = 0.0; auto_axes_ = false; h_pan = iop->xAxis->range().size() * x_pixels / iop->xAxis->axisRect()->width(); v_pan = iop->yAxis->range().size() * y_pixels / iop->yAxis->axisRect()->height(); // The GTK+ version won't pan unless we're zoomed. Should we do the same here? if (h_pan) { iop->xAxis->moveRange(h_pan); iop->replot(); } if (v_pan) { iop->yAxis->moveRange(v_pan); iop->replot(); } } void IOGraphDialog::toggleTracerStyle(bool force_default) { if (!tracer_->visible() && !force_default) return; if (!ui->ioPlot->graph(0)) return; QPen sp_pen = ui->ioPlot->graph(0)->pen(); QCPItemTracer::TracerStyle tstyle = QCPItemTracer::tsCrosshair; QPen tr_pen = QPen(tracer_->pen()); QColor tr_color = sp_pen.color(); if (force_default || tracer_->style() != QCPItemTracer::tsCircle) { tstyle = QCPItemTracer::tsCircle; tr_color.setAlphaF(1.0); tr_pen.setWidthF(1.5); } else { tr_color.setAlphaF(0.5); tr_pen.setWidthF(1.0); } tracer_->setStyle(tstyle); tr_pen.setColor(tr_color); tracer_->setPen(tr_pen); ui->ioPlot->replot(); } // Returns the IOGraph which is most likely to be used by the user. This is the // currently selected, visible graph or the first visible graph otherwise. IOGraph *IOGraphDialog::currentActiveGraph() const { QModelIndex index = ui->graphUat->currentIndex(); if (index.isValid()) { return ioGraphs_[index.row()]; } //if no currently selected item, go with first item enabled for (int row = 0; row < uat_model_->rowCount(); row++) { if (uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool()) return ioGraphs_[row]; } return NULL; } // Scan through our graphs and gather information. // QCPItemTracers can only be associated with QCPGraphs. Find the first one // and associate it with our tracer. Set bar stacking order while we're here. void IOGraphDialog::getGraphInfo() { base_graph_ = NULL; QCPBars *prev_bars = NULL; start_time_ = 0.0; tracer_->setGraph(NULL); IOGraph *selectedGraph = currentActiveGraph(); if (uat_model_ != NULL) { //all graphs may not be created yet, so bounds check the graph array for (int row = 0; ((row < uat_model_->rowCount()) && (row < ioGraphs_.size())); row++) { if (uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool()) { IOGraph* iog = ioGraphs_[row]; QCPGraph *graph = iog->graph(); QCPBars *bars = iog->bars(); if (graph && (!base_graph_ || iog == selectedGraph)) { base_graph_ = graph; } else if (bars && (uat_model_->data(uat_model_->index(row, colStyle), Qt::DisplayRole).toString().compare(val_to_str_const(IOGraph::psStackedBar, graph_style_vs, "None")) == 0) && iog->visible()) { bars->moveBelow(NULL); // Remove from existing stack bars->moveBelow(prev_bars); prev_bars = bars; } if (iog->visible()) { double iog_start = iog->startOffset(); if (start_time_ == 0.0 || iog_start < start_time_) { start_time_ = iog_start; } } } } } if (base_graph_ && base_graph_->data()->size() > 0) { tracer_->setGraph(base_graph_); tracer_->setVisible(true); } } void IOGraphDialog::updateLegend() { QCustomPlot *iop = ui->ioPlot; QSet vu_label_set; QString intervalText = ui->intervalComboBox->itemText(ui->intervalComboBox->currentIndex()); iop->legend->setVisible(false); iop->yAxis->setLabel(QString()); // Find unique labels if (uat_model_ != NULL) { for (int row = 0; row < uat_model_->rowCount(); row++) { if (uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool() && ioGraphs_[row]) { IOGraph *iog = ioGraphs_[row]; QString label(iog->valueUnitLabel()); if (!iog->scaledValueUnit().isEmpty()) { label += " (" + iog->scaledValueUnit() + ")"; } vu_label_set.insert(label); } } } // Nothing. if (vu_label_set.size() < 1) { return; } // All the same. Use the Y Axis label. if (vu_label_set.size() == 1) { iop->yAxis->setLabel(vu_label_set.values()[0] + "/" + intervalText); return; } // Differing labels. Create a legend with a Title label at top. // Legend Title thanks to: http://www.qcustomplot.com/index.php/support/forum/443 QCPStringLegendItem* legendTitle = qobject_cast(iop->legend->elementAt(0)); if (legendTitle == NULL) { legendTitle = new QCPStringLegendItem(iop->legend, QString("")); iop->legend->insertRow(0); iop->legend->addElement(0, 0, legendTitle); } legendTitle->setText(QString(intervalText + " Intervals ")); if (uat_model_ != NULL) { for (int row = 0; row < uat_model_->rowCount(); row++) { IOGraph *iog = ioGraphs_[row]; if (iog) { if (uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool()) { iog->addToLegend(); } else { iog->removeFromLegend(); } } } } iop->legend->setVisible(true); } QRectF IOGraphDialog::getZoomRanges(QRect zoom_rect) { QRectF zoom_ranges = QRectF(); if (zoom_rect.width() < min_zoom_pixels_ && zoom_rect.height() < min_zoom_pixels_) { return zoom_ranges; } QCustomPlot *iop = ui->ioPlot; QRect zr = zoom_rect.normalized(); QRect ar = iop->axisRect()->rect(); if (ar.intersects(zr)) { QRect zsr = ar.intersected(zr); zoom_ranges.setX(iop->xAxis->range().lower + iop->xAxis->range().size() * (zsr.left() - ar.left()) / ar.width()); zoom_ranges.setWidth(iop->xAxis->range().size() * zsr.width() / ar.width()); // QRects grow down zoom_ranges.setY(iop->yAxis->range().lower + iop->yAxis->range().size() * (ar.bottom() - zsr.bottom()) / ar.height()); zoom_ranges.setHeight(iop->yAxis->range().size() * zsr.height() / ar.height()); } return zoom_ranges; } void IOGraphDialog::graphClicked(QMouseEvent *event) { QCustomPlot *iop = ui->ioPlot; if (event->button() == Qt::RightButton) { // XXX We should find some way to get ioPlot to handle a // contextMenuEvent instead. ctx_menu_.exec(event->globalPos()); } else if (mouse_drags_) { if (iop->axisRect()->rect().contains(event->pos())) { iop->setCursor(QCursor(Qt::ClosedHandCursor)); } on_actionGoToPacket_triggered(); } else { if (!rubber_band_) { rubber_band_ = new QRubberBand(QRubberBand::Rectangle, iop); } rb_origin_ = event->pos(); rubber_band_->setGeometry(QRect(rb_origin_, QSize())); rubber_band_->show(); } iop->setFocus(); } void IOGraphDialog::mouseMoved(QMouseEvent *event) { QCustomPlot *iop = ui->ioPlot; QString hint; Qt::CursorShape shape = Qt::ArrowCursor; if (!hint_err_.isEmpty()) { hint += QString("%1 ").arg(hint_err_); } if (event) { if (event->buttons().testFlag(Qt::LeftButton)) { if (mouse_drags_) { shape = Qt::ClosedHandCursor; } else { shape = Qt::CrossCursor; } } else if (iop->axisRect()->rect().contains(event->pos())) { if (mouse_drags_) { shape = Qt::OpenHandCursor; } else { shape = Qt::CrossCursor; } } iop->setCursor(QCursor(shape)); } if (mouse_drags_) { double ts = 0; packet_num_ = 0; int interval_packet = -1; if (event && tracer_->graph()) { tracer_->setGraphKey(iop->xAxis->pixelToCoord(event->pos().x())); ts = tracer_->position->key(); if (IOGraph *iog = currentActiveGraph()) { interval_packet = iog->packetFromTime(ts); } } if (interval_packet < 0) { hint += tr("Hover over the graph for details."); } else { QString msg = tr("No packets in interval"); QString val; if (interval_packet > 0) { packet_num_ = (guint32) interval_packet; msg = QString("%1 %2") .arg(!file_closed_ ? tr("Click to select packet") : tr("Packet")) .arg(packet_num_); val = " = " + QString::number(tracer_->position->value(), 'g', 4); } hint += tr("%1 (%2s%3).") .arg(msg) .arg(QString::number(ts, 'g', 4)) .arg(val); } iop->replot(); } else { if (event && rubber_band_ && rubber_band_->isVisible()) { rubber_band_->setGeometry(QRect(rb_origin_, event->pos()).normalized()); QRectF zoom_ranges = getZoomRanges(QRect(rb_origin_, event->pos())); if (zoom_ranges.width() > 0.0 && zoom_ranges.height() > 0.0) { hint += tr("Release to zoom, x = %1 to %2, y = %3 to %4") .arg(zoom_ranges.x()) .arg(zoom_ranges.x() + zoom_ranges.width()) .arg(zoom_ranges.y()) .arg(zoom_ranges.y() + zoom_ranges.height()); } else { hint += tr("Unable to select range."); } } else { hint += tr("Click to select a portion of the graph."); } } hint.prepend(""); hint.append(""); ui->hintLabel->setText(hint); } void IOGraphDialog::mouseReleased(QMouseEvent *event) { QCustomPlot *iop = ui->ioPlot; auto_axes_ = false; if (rubber_band_) { rubber_band_->hide(); if (!mouse_drags_) { QRectF zoom_ranges = getZoomRanges(QRect(rb_origin_, event->pos())); if (zoom_ranges.width() > 0.0 && zoom_ranges.height() > 0.0) { iop->xAxis->setRangeLower(zoom_ranges.x()); iop->xAxis->setRangeUpper(zoom_ranges.x() + zoom_ranges.width()); iop->yAxis->setRangeLower(zoom_ranges.y()); iop->yAxis->setRangeUpper(zoom_ranges.y() + zoom_ranges.height()); iop->replot(); } } } else if (iop->cursor().shape() == Qt::ClosedHandCursor) { iop->setCursor(QCursor(Qt::OpenHandCursor)); } } void IOGraphDialog::resetAxes() { QCustomPlot *iop = ui->ioPlot; QCPRange x_range = iop->xAxis->scaleType() == QCPAxis::stLogarithmic ? iop->xAxis->range().sanitizedForLogScale() : iop->xAxis->range(); double pixel_pad = 10.0; // per side iop->rescaleAxes(true); double axis_pixels = iop->xAxis->axisRect()->width(); iop->xAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, x_range.center()); axis_pixels = iop->yAxis->axisRect()->height(); iop->yAxis->scaleRange((axis_pixels + (pixel_pad * 2)) / axis_pixels, iop->yAxis->range().center()); auto_axes_ = true; iop->replot(); } void IOGraphDialog::updateStatistics() { if (!isVisible()) return; if (need_retap_ && !file_closed_) { need_retap_ = false; cap_file_.retapPackets(); // The user might have closed the window while tapping, which means // we might no longer exist. } else { if (need_recalc_ && !file_closed_) { need_recalc_ = false; need_replot_ = true; int enabled_graphs = 0; if (uat_model_ != NULL) { for (int row = 0; row < uat_model_->rowCount(); row++) { if (uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool()) { ++enabled_graphs; } } } // With multiple visible graphs, disable Y scaling to avoid // multiple, distinct units. emit recalcGraphData(cap_file_.capFile(), enabled_graphs == 1); if (!tracer_->graph()) { if (base_graph_ && base_graph_->data()->size() > 0) { tracer_->setGraph(base_graph_); tracer_->setVisible(true); } else { tracer_->setVisible(false); } } } if (need_replot_) { need_replot_ = false; if (auto_axes_) { resetAxes(); } ui->ioPlot->replot(); } } } void IOGraphDialog::loadProfileGraphs() { if (iog_uat_ == NULL) { iog_uat_ = uat_new("I/O Graphs", sizeof(io_graph_settings_t), "io_graphs", TRUE, &iog_settings_, &num_io_graphs_, 0, /* doesn't affect anything that requires a GUI update */ "ChStatIOGraphs", io_graph_copy_cb, NULL, io_graph_free_cb, NULL, NULL, io_graph_fields); char* err = NULL; if (!uat_load(iog_uat_, &err)) { report_failure("Error while loading %s: %s. Default graph values will be used", iog_uat_->name, err); g_free(err); uat_clear(iog_uat_); } } uat_model_ = new UatModel(NULL, iog_uat_); uat_delegate_ = new UatDelegate; ui->graphUat->setModel(uat_model_); ui->graphUat->setItemDelegate(uat_delegate_); connect(uat_model_, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(modelDataChanged(QModelIndex))); connect(uat_model_, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(modelRowsRemoved())); } // Slots void IOGraphDialog::on_intervalComboBox_currentIndexChanged(int) { int interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt(); bool need_retap = false; if (uat_model_ != NULL) { for (int row = 0; row < uat_model_->rowCount(); row++) { IOGraph *iog = ioGraphs_[row]; if (iog) { iog->setInterval(interval); if (iog->visible()) { need_retap = true; } } } } if (need_retap) { scheduleRetap(true); } updateLegend(); } void IOGraphDialog::on_todCheckBox_toggled(bool checked) { double orig_start = start_time_; bool orig_auto = auto_axes_; ui->ioPlot->xAxis->setTickLabelType(checked ? QCPAxis::ltDateTime : QCPAxis::ltNumber); auto_axes_ = false; scheduleRecalc(true); auto_axes_ = orig_auto; getGraphInfo(); ui->ioPlot->xAxis->moveRange(start_time_ - orig_start); mouseMoved(NULL); // Update hint } void IOGraphDialog::on_graphUat_currentItemChanged(const QModelIndex ¤t, const QModelIndex&) { if (current.isValid()) { ui->deleteToolButton->setEnabled(true); ui->copyToolButton->setEnabled(true); } else { ui->deleteToolButton->setEnabled(false); ui->copyToolButton->setEnabled(false); } } void IOGraphDialog::modelDataChanged(const QModelIndex &index) { bool recalc = false; switch (index.column()) { case colYAxis: case colSMAPeriod: recalc = true; } syncGraphSettings(index.row()); if (recalc) { scheduleRecalc(true); } else { scheduleReplot(true); } } void IOGraphDialog::on_resetButton_clicked() { resetAxes(); } void IOGraphDialog::on_newToolButton_clicked() { addGraph(); } void IOGraphDialog::on_deleteToolButton_clicked() { const QModelIndex ¤t = ui->graphUat->currentIndex(); if (uat_model_ && current.isValid()) { delete ioGraphs_[current.row()]; ioGraphs_.remove(current.row()); if (!uat_model_->removeRows(current.row(), 1)) { qDebug() << "Failed to remove row"; } } // We should probably be smarter about this. hint_err_.clear(); mouseMoved(NULL); } void IOGraphDialog::on_copyToolButton_clicked() { addGraph(true); } void IOGraphDialog::on_dragRadioButton_toggled(bool checked) { if (checked) mouse_drags_ = true; ui->ioPlot->setInteractions( QCP::iRangeDrag | QCP::iRangeZoom ); } void IOGraphDialog::on_zoomRadioButton_toggled(bool checked) { if (checked) mouse_drags_ = false; ui->ioPlot->setInteractions(0); } void IOGraphDialog::on_logCheckBox_toggled(bool checked) { QCustomPlot *iop = ui->ioPlot; iop->yAxis->setScaleType(checked ? QCPAxis::stLogarithmic : QCPAxis::stLinear); iop->replot(); } void IOGraphDialog::on_actionReset_triggered() { on_resetButton_clicked(); } void IOGraphDialog::on_actionZoomIn_triggered() { zoomAxes(true); } void IOGraphDialog::on_actionZoomInX_triggered() { zoomXAxis(true); } void IOGraphDialog::on_actionZoomInY_triggered() { zoomYAxis(true); } void IOGraphDialog::on_actionZoomOut_triggered() { zoomAxes(false); } void IOGraphDialog::on_actionZoomOutX_triggered() { zoomXAxis(false); } void IOGraphDialog::on_actionZoomOutY_triggered() { zoomYAxis(false); } void IOGraphDialog::on_actionMoveUp10_triggered() { panAxes(0, 10); } void IOGraphDialog::on_actionMoveLeft10_triggered() { panAxes(-10, 0); } void IOGraphDialog::on_actionMoveRight10_triggered() { panAxes(10, 0); } void IOGraphDialog::on_actionMoveDown10_triggered() { panAxes(0, -10); } void IOGraphDialog::on_actionMoveUp1_triggered() { panAxes(0, 1); } void IOGraphDialog::on_actionMoveLeft1_triggered() { panAxes(-1, 0); } void IOGraphDialog::on_actionMoveRight1_triggered() { panAxes(1, 0); } void IOGraphDialog::on_actionMoveDown1_triggered() { panAxes(0, -1); } void IOGraphDialog::on_actionGoToPacket_triggered() { if (tracer_->visible() && !file_closed_ && packet_num_ > 0) { emit goToPacket(packet_num_); } } void IOGraphDialog::on_actionDragZoom_triggered() { if (mouse_drags_) { ui->zoomRadioButton->toggle(); } else { ui->dragRadioButton->toggle(); } } void IOGraphDialog::on_actionToggleTimeOrigin_triggered() { } void IOGraphDialog::on_actionCrosshairs_triggered() { } void IOGraphDialog::on_buttonBox_helpRequested() { wsApp->helpTopicAction(HELP_STATS_IO_GRAPH_DIALOG); } // XXX - Copied from tcp_stream_dialog. This should be common code. void IOGraphDialog::on_buttonBox_accepted() { QString file_name, extension; QDir path(wsApp->lastOpenDir()); QString pdf_filter = tr("Portable Document Format (*.pdf)"); QString png_filter = tr("Portable Network Graphics (*.png)"); QString bmp_filter = tr("Windows Bitmap (*.bmp)"); // Gaze upon my beautiful graph with lossy artifacts! QString jpeg_filter = tr("JPEG File Interchange Format (*.jpeg *.jpg)"); QString csv_filter = tr("Comma Separated Values (*.csv)"); QString filter = QString("%1;;%2;;%3;;%4;;%5") .arg(pdf_filter) .arg(png_filter) .arg(bmp_filter) .arg(jpeg_filter) .arg(csv_filter); QString save_file = path.canonicalPath(); if (!file_closed_) { save_file += QString("/%1").arg(cap_file_.fileTitle()); } file_name = QFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save Graph As" UTF8_HORIZONTAL_ELLIPSIS)), save_file, filter, &extension); if (file_name.length() > 0) { bool save_ok = false; if (extension.compare(pdf_filter) == 0) { save_ok = ui->ioPlot->savePdf(file_name); } else if (extension.compare(png_filter) == 0) { save_ok = ui->ioPlot->savePng(file_name); } else if (extension.compare(bmp_filter) == 0) { save_ok = ui->ioPlot->saveBmp(file_name); } else if (extension.compare(jpeg_filter) == 0) { save_ok = ui->ioPlot->saveJpg(file_name); } else if (extension.compare(csv_filter) == 0) { save_ok = saveCsv(file_name); } // else error dialog? if (save_ok) { path = QDir(file_name); wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData()); } } } void IOGraphDialog::makeCsv(QTextStream &stream) const { QList activeGraphs; int ui_interval = ui->intervalComboBox->itemData(ui->intervalComboBox->currentIndex()).toInt(); int max_interval = 0; stream << "\"Interval start\""; if (uat_model_ != NULL) { for (int row = 0; row < uat_model_->rowCount(); row++) { if (uat_model_->data(uat_model_->index(row, colEnabled), Qt::DisplayRole).toBool() && (ioGraphs_[row] != NULL)) { activeGraphs.append(ioGraphs_[row]); if (max_interval < ioGraphs_[row]->maxInterval()) { max_interval = ioGraphs_[row]->maxInterval(); } QString name = ioGraphs_[row]->name().toUtf8(); name = QString("\"%1\"").arg(name.replace("\"", "\"\"")); // RFC 4180 stream << "," << name; } } } stream << endl; for (int interval = 0; interval <= max_interval; interval++) { double interval_start = (double)interval * ((double)ui_interval / 1000.0); stream << interval_start; foreach (IOGraph *iog, activeGraphs) { double value = 0.0; if (interval <= iog->maxInterval()) { value = iog->getItemValue(interval, cap_file_.capFile()); } stream << "," << value; } stream << endl; } } void IOGraphDialog::copyAsCsvClicked() { QString csv; QTextStream stream(&csv, QIODevice::Text); makeCsv(stream); wsApp->clipboard()->setText(stream.readAll()); } bool IOGraphDialog::saveCsv(const QString &file_name) const { QFile save_file(file_name); save_file.open(QFile::WriteOnly); QTextStream out(&save_file); makeCsv(out); return true; } // IOGraph IOGraph::IOGraph(QCustomPlot *parent) : parent_(parent), visible_(false), graph_(NULL), bars_(NULL), val_units_(IOG_ITEM_UNIT_FIRST), hf_index_(-1), cur_idx_(-1) { Q_ASSERT(parent_ != NULL); graph_ = parent_->addGraph(parent_->xAxis, parent_->yAxis); Q_ASSERT(graph_ != NULL); GString *error_string; error_string = register_tap_listener("frame", this, "", TL_REQUIRES_PROTO_TREE, tapReset, tapPacket, tapDraw); if (error_string) { // QMessageBox::critical(this, tr("%1 failed to register tap listener").arg(name_), // error_string->str); // config_err_ = error_string->str; g_string_free(error_string, TRUE); } } IOGraph::~IOGraph() { remove_tap_listener(this); if (graph_) { parent_->removeGraph(graph_); } if (bars_) { parent_->removePlottable(bars_); } } // Construct a full filter string from the display filter and value unit / Y axis. // Check for errors and sets config_err_ if any are found. void IOGraph::setFilter(const QString &filter) { GString *error_string; QString full_filter(filter.trimmed()); config_err_.clear(); // Make sure we have a good display filter if (!full_filter.isEmpty()) { dfilter_t *dfilter; bool status; gchar *err_msg; status = dfilter_compile(full_filter.toUtf8().constData(), &dfilter, &err_msg); dfilter_free(dfilter); if (!status) { config_err_ = QString::fromUtf8(err_msg); g_free(err_msg); filter_ = full_filter; return; } } // Check our value unit + field combo. error_string = check_field_unit(vu_field_.toUtf8().constData(), NULL, val_units_); if (error_string) { config_err_ = error_string->str; g_string_free(error_string, TRUE); return; } // Make sure vu_field_ survives edt tree pruning by adding it to our filter // expression. if (val_units_ >= IOG_ITEM_UNIT_CALC_SUM && !vu_field_.isEmpty() && hf_index_ >= 0) { if (full_filter.isEmpty()) { full_filter = vu_field_; } else { full_filter += QString(" && (%1)").arg(vu_field_); } } error_string = set_tap_dfilter(this, full_filter.toUtf8().constData()); if (error_string) { config_err_ = error_string->str; g_string_free(error_string, TRUE); return; } else { if (filter_.compare(filter) && visible_) { emit requestRetap(); } filter_ = filter; } } void IOGraph::applyCurrentColor() { if (graph_) { graph_->setPen(QPen(color_, graph_line_width_)); } else if (bars_) { bars_->setPen(QPen(QBrush(ColorUtils::graphColor(0)), graph_line_width_)); // ...or omit it altogether? bars_->setBrush(color_); } } void IOGraph::setVisible(bool visible) { bool old_visibility = visible_; visible_ = visible; if (graph_) { graph_->setVisible(visible_); } if (bars_) { bars_->setVisible(visible_); } if (old_visibility != visible_) { emit requestReplot(); } } void IOGraph::setName(const QString &name) { name_ = name; if (graph_) { graph_->setName(name_); } if (bars_) { bars_->setName(name_); } } QRgb IOGraph::color() { return color_.color().rgb(); } void IOGraph::setColor(const QRgb color) { color_ = QBrush(color); applyCurrentColor(); } void IOGraph::setPlotStyle(int style) { // Switch plottable if needed switch (style) { case psBar: case psStackedBar: if (graph_) { bars_ = new QCPBars(parent_->xAxis, parent_->yAxis); parent_->addPlottable(bars_); parent_->removeGraph(graph_); graph_ = NULL; } break; default: if (bars_) { graph_ = parent_->addGraph(parent_->xAxis, parent_->yAxis); parent_->removePlottable(bars_); bars_ = NULL; } break; } setValueUnits(val_units_); if (graph_) { graph_->setLineStyle(QCPGraph::lsNone); graph_->setScatterStyle(QCPScatterStyle::ssNone); } switch (style) { case psLine: if (graph_) { graph_->setLineStyle(QCPGraph::lsLine); } break; case psImpulse: if (graph_) { graph_->setLineStyle(QCPGraph::lsImpulse); } break; case psDot: if (graph_) { graph_->setScatterStyle(QCPScatterStyle::ssDisc); } break; case psSquare: if (graph_) { graph_->setScatterStyle(QCPScatterStyle::ssSquare); } break; case psDiamond: if (graph_) { graph_->setScatterStyle(QCPScatterStyle::ssDiamond); } break; case psBar: case IOGraph::psStackedBar: // Stacking set in scanGraphs bars_->moveBelow(NULL); break; } setName(name_); applyCurrentColor(); } const QString IOGraph::valueUnitLabel() { return val_to_str_const(val_units_, y_axis_vs, "Unknown"); } void IOGraph::setValueUnits(int val_units) { if (val_units >= IOG_ITEM_UNIT_FIRST && val_units <= IOG_ITEM_UNIT_LAST) { int old_val_units = val_units_; val_units_ = (io_graph_item_unit_t)val_units; if (old_val_units != val_units) { setFilter(filter_); // Check config & prime vu field if (val_units < IOG_ITEM_UNIT_CALC_SUM) { emit requestRecalc(); } } } } void IOGraph::setValueUnitField(const QString &vu_field) { int old_hf_index = hf_index_; vu_field_ = vu_field.trimmed(); hf_index_ = -1; header_field_info *hfi = proto_registrar_get_byname(vu_field_.toUtf8().constData()); if (hfi) { hf_index_ = hfi->id; } if (old_hf_index != hf_index_) { setFilter(filter_); // Check config & prime vu field } } bool IOGraph::addToLegend() { if (graph_) { return graph_->addToLegend(); } if (bars_) { return bars_->addToLegend(); } return false; } bool IOGraph::removeFromLegend() { if (graph_) { return graph_->removeFromLegend(); } if (bars_) { return bars_->removeFromLegend(); } return false; } double IOGraph::startOffset() { if (graph_ && graph_->keyAxis()->tickLabelType() == QCPAxis::ltDateTime && graph_->data()->size() > 0) { return graph_->data()->keys()[0]; } if (bars_ && bars_->keyAxis()->tickLabelType() == QCPAxis::ltDateTime && bars_->data()->size() > 0) { return bars_->data()->keys()[0]; } return 0.0; } int IOGraph::packetFromTime(double ts) { int idx = ts * 1000 / interval_; if (idx >= 0 && idx < (int) cur_idx_) { switch (val_units_) { case IOG_ITEM_UNIT_CALC_MAX: case IOG_ITEM_UNIT_CALC_MIN: return items_[idx].extreme_frame_in_invl; default: return items_[idx].last_frame_in_invl; } } return -1; } void IOGraph::clearAllData() { cur_idx_ = -1; reset_io_graph_items(items_, max_io_items_); if (graph_) { graph_->clearData(); } if (bars_) { bars_->clearData(); } start_time_ = 0.0; } void IOGraph::recalcGraphData(capture_file *cap_file, bool enable_scaling) { /* Moving average variables */ unsigned int mavg_in_average_count = 0, mavg_left = 0, mavg_right = 0; unsigned int mavg_to_remove = 0, mavg_to_add = 0; double mavg_cumulated = 0; QCPAxis *x_axis = NULL; if (graph_) { graph_->clearData(); x_axis = graph_->keyAxis(); } if (bars_) { bars_->clearData(); x_axis = bars_->keyAxis(); } if (moving_avg_period_ > 0 && cur_idx_ >= 0) { /* "Warm-up phase" - calculate average on some data not displayed; * just to make sure average on leftmost and rightmost displayed * values is as reliable as possible */ guint64 warmup_interval = 0; // for (; warmup_interval < first_interval; warmup_interval += interval_) { // mavg_cumulated += get_it_value(io, i, (int)warmup_interval/interval_); // mavg_in_average_count++; // mavg_left++; // } mavg_cumulated += getItemValue((int)warmup_interval/interval_, cap_file); mavg_in_average_count++; for (warmup_interval = interval_; ((warmup_interval < (0 + (moving_avg_period_ / 2) * (guint64)interval_)) && (warmup_interval <= (cur_idx_ * (guint64)interval_))); warmup_interval += interval_) { mavg_cumulated += getItemValue((int)warmup_interval / interval_, cap_file); mavg_in_average_count++; mavg_right++; } mavg_to_add = (unsigned int)warmup_interval; } for (int i = 0; i <= cur_idx_; i++) { double ts = (double) i * interval_ / 1000; if (x_axis && x_axis->tickLabelType() == QCPAxis::ltDateTime) { ts += start_time_; } double val = getItemValue(i, cap_file); if (moving_avg_period_ > 0) { if (i != 0) { mavg_left++; if (mavg_left > moving_avg_period_ / 2) { mavg_left--; mavg_in_average_count--; mavg_cumulated -= getItemValue((int)mavg_to_remove / interval_, cap_file); mavg_to_remove += interval_; } if (mavg_to_add <= (unsigned int) cur_idx_ * interval_) { mavg_in_average_count++; mavg_cumulated += getItemValue((int)mavg_to_add / interval_, cap_file); mavg_to_add += interval_; } else { mavg_right--; } } if (mavg_in_average_count > 0) { val = mavg_cumulated / mavg_in_average_count; } } if (graph_) { graph_->addData(ts, val); } if (bars_) { bars_->addData(ts, val); } // qDebug() << "=rgd i" << i << ts << val; } // attempt to rescale time values to specific units if (enable_scaling) { calculateScaledValueUnit(); } else { scaled_value_unit_.clear(); } emit requestReplot(); } void IOGraph::calculateScaledValueUnit() { // Reset unit and recalculate if needed. scaled_value_unit_.clear(); // If there is no field, scaling is not possible. if (hf_index_ < 0) { return; } switch (val_units_) { case IOG_ITEM_UNIT_CALC_SUM: case IOG_ITEM_UNIT_CALC_MAX: case IOG_ITEM_UNIT_CALC_MIN: case IOG_ITEM_UNIT_CALC_AVERAGE: // Unit is not yet known, continue detecting it. break; default: // Unit is Packets, Bytes, Bits, etc. return; } if (proto_registrar_get_ftype(hf_index_) == FT_RELATIVE_TIME) { // find maximum absolute value and scale accordingly double maxValue = 0; if (graph_) { maxValue = maxValueFromGraphData(*graph_->data()); } else if (bars_) { maxValue = maxValueFromGraphData(*bars_->data()); } // If the maximum value is zero, then either we have no data or // everything is zero, do not scale the unit in this case. if (maxValue == 0) { return; } // XXX GTK+ always uses "ms" for log scale, should we do that too? int value_multiplier; if (maxValue >= 1.0) { scaled_value_unit_ = "s"; value_multiplier = 1; } else if (maxValue >= 0.001) { scaled_value_unit_ = "ms"; value_multiplier = 1000; } else { scaled_value_unit_ = "us"; value_multiplier = 1000000; } if (graph_) { scaleGraphData(*graph_->data(), value_multiplier); } else if (bars_) { scaleGraphData(*bars_->data(), value_multiplier); } } } template double IOGraph::maxValueFromGraphData(const DataMap &map) { double maxValue = 0; typename DataMap::const_iterator it = map.constBegin(); while (it != map.constEnd()) { maxValue = MAX(fabs((*it).value), maxValue); ++it; } return maxValue; } template void IOGraph::scaleGraphData(DataMap &map, int scalar) { if (scalar != 1) { typename DataMap::iterator it = map.begin(); while (it != map.end()) { (*it).value *= scalar; ++it; } } } void IOGraph::captureFileClosing() { remove_tap_listener(this); } void IOGraph::reloadValueUnitField() { if (vu_field_.length() > 0) { setValueUnitField(vu_field_); } } void IOGraph::setInterval(int interval) { interval_ = interval; } // Get the value at the given interval (idx) for the current value unit. // Adapted from get_it_value in gtk/io_stat.c. double IOGraph::getItemValue(int idx, const capture_file *cap_file) const { double value = 0; /* FIXME: loss of precision, visible on the graph for small values */ int adv_type; const io_graph_item_t *item; guint32 interval; g_assert(idx < max_io_items_); item = &items_[idx]; // Basic units switch (val_units_) { case IOG_ITEM_UNIT_PACKETS: return item->frames; case IOG_ITEM_UNIT_BYTES: return item->bytes; case IOG_ITEM_UNIT_BITS: return (item->bytes * 8); case IOG_ITEM_UNIT_CALC_FRAMES: return item->frames; case IOG_ITEM_UNIT_CALC_FIELDS: return item->fields; default: /* If it's COUNT_TYPE_ADVANCED but not one of the * generic ones we'll get it when we switch on the * adv_type below. */ break; } if (hf_index_ < 0) { return 0; } // Advanced units adv_type = proto_registrar_get_ftype(hf_index_); switch (adv_type) { case FT_UINT8: case FT_UINT16: case FT_UINT24: case FT_UINT32: case FT_UINT64: case FT_INT8: case FT_INT16: case FT_INT24: case FT_INT32: case FT_INT64: switch (val_units_) { case IOG_ITEM_UNIT_CALC_SUM: value = item->int_tot; break; case IOG_ITEM_UNIT_CALC_MAX: value = item->int_max; break; case IOG_ITEM_UNIT_CALC_MIN: value = item->int_min; break; case IOG_ITEM_UNIT_CALC_AVERAGE: if (item->fields) { value = (double)item->int_tot / item->fields; } else { value = 0; } break; default: break; } break; case FT_FLOAT: switch (val_units_) { case IOG_ITEM_UNIT_CALC_SUM: value = item->float_tot; break; case IOG_ITEM_UNIT_CALC_MAX: value = item->float_max; break; case IOG_ITEM_UNIT_CALC_MIN: value = item->float_min; break; case IOG_ITEM_UNIT_CALC_AVERAGE: if (item->fields) { value = item->float_tot / item->fields; } else { value = 0; } break; default: break; } break; case FT_DOUBLE: switch (val_units_) { case IOG_ITEM_UNIT_CALC_SUM: value = item->double_tot; break; case IOG_ITEM_UNIT_CALC_MAX: value = item->double_max; break; case IOG_ITEM_UNIT_CALC_MIN: value = item->double_min; break; case IOG_ITEM_UNIT_CALC_AVERAGE: if (item->fields) { value = item->double_tot / item->fields; } else { value = 0; } break; default: break; } break; case FT_RELATIVE_TIME: switch (val_units_) { case IOG_ITEM_UNIT_CALC_MAX: value = nstime_to_sec(&item->time_max); break; case IOG_ITEM_UNIT_CALC_MIN: value = nstime_to_sec(&item->time_min); break; case IOG_ITEM_UNIT_CALC_SUM: value = nstime_to_sec(&item->time_tot); break; case IOG_ITEM_UNIT_CALC_AVERAGE: if (item->fields) { value = nstime_to_sec(&item->time_tot) / item->fields; } else { value = 0; } break; case IOG_ITEM_UNIT_CALC_LOAD: // "LOAD graphs plot the QUEUE-depth of the connection over time" // (for response time fields such as smb.time, rpc.time, etc.) // This interval is expressed in milliseconds. if (idx == cur_idx_ && cap_file) { interval = (guint32)(nstime_to_msec(&cap_file->elapsed_time) + 0.5); interval -= (interval_ * idx); } else { interval = interval_; } value = nstime_to_msec(&item->time_tot) / interval; break; default: break; } break; default: break; } return value; } // "tap_reset" callback for register_tap_listener void IOGraph::tapReset(void *iog_ptr) { IOGraph *iog = static_cast(iog_ptr); if (!iog) return; // qDebug() << "=tapReset" << iog->name_; iog->clearAllData(); } // "tap_packet" callback for register_tap_listener gboolean IOGraph::tapPacket(void *iog_ptr, packet_info *pinfo, epan_dissect_t *edt, const void *) { IOGraph *iog = static_cast(iog_ptr); if (!pinfo || !iog) { return FALSE; } int idx = get_io_graph_index(pinfo, iog->interval_); bool recalc = false; /* some sanity checks */ if ((idx < 0) || (idx >= max_io_items_)) { iog->cur_idx_ = max_io_items_ - 1; return FALSE; } /* update num_items */ if (idx > iog->cur_idx_) { iog->cur_idx_ = (guint32) idx; recalc = true; } /* set start time */ if (iog->start_time_ == 0.0) { nstime_t start_nstime; nstime_set_zero(&start_nstime); nstime_delta(&start_nstime, &pinfo->abs_ts, &pinfo->rel_ts); iog->start_time_ = nstime_to_sec(&start_nstime); } epan_dissect_t *adv_edt = NULL; /* For ADVANCED mode we need to keep track of some more stuff than just frame and byte counts */ if (iog->val_units_ >= IOG_ITEM_UNIT_CALC_SUM) { adv_edt = edt; } if (!update_io_graph_item(iog->items_, idx, pinfo, adv_edt, iog->hf_index_, iog->val_units_, iog->interval_)) { return FALSE; } // qDebug() << "=tapPacket" << iog->name_ << idx << iog->hf_index_ << iog->val_units_ << iog->num_items_; if (recalc) { emit iog->requestRecalc(); } return TRUE; } // "tap_draw" callback for register_tap_listener void IOGraph::tapDraw(void *iog_ptr) { IOGraph *iog = static_cast(iog_ptr); if (!iog) return; emit iog->requestRecalc(); if (iog->graph_) { // qDebug() << "=tapDraw g" << iog->name_ << iog->graph_->data()->keys().size(); } if (iog->bars_) { // qDebug() << "=tapDraw b" << iog->name_ << iog->bars_->data()->keys().size(); } } // Stat command + args static void io_graph_init(const char *, void*) { wsApp->emitStatCommandSignal("IOGraph", NULL, NULL); } static stat_tap_ui io_stat_ui = { REGISTER_STAT_GROUP_GENERIC, NULL, "io,stat", io_graph_init, 0, NULL }; extern "C" { void register_tap_listener_qt_iostat(void) { register_stat_tap_ui(&io_stat_ui, NULL); } } /* * Editor modelines * * Local Variables: * c-basic-offset: 4 * tab-width: 8 * indent-tabs-mode: nil * End: * * ex: set shiftwidth=4 tabstop=8 expandtab: * :indentSize=4:tabSize=8:noTabs=true: */