From 576ed31ce1e1f4492bbc35bbcd200563563358d6 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:45 +0200 Subject: [PATCH 1/8] figures: fix the out of bounds check of the figure result recorder FigureRecorder accepted a figure index equal to the number of series the figure has, and then called setValue() one past the end. The check was '>' where the index is zero based, so only an index beyond that was refused. Reject it at initialization instead. A model that used the boundary index by mistake now stops with an error naming the figure and the bound, rather than writing into a series that does not exist. --- WHATSNEW | 8 ++++++++ src/inet/common/figures/FigureRecorder.cc | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/WHATSNEW b/WHATSNEW index 2acaff561e1..346b834b8e6 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -29,6 +29,14 @@ Notable backward incompatible changes are the following: adaptive rate control, either where a station transmits to several peers or where it sends group-addressed traffic. +2. Figure result recorder bounds check + + A @statistic(record=figure; targetFigure=name:N) with N equal to the number of + items the figure displays was accepted and then wrote one past the end. It is + now rejected at initialization, so a model that used the boundary index by + mistake stops with an error instead of writing into an item that does not + exist. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/common/figures/FigureRecorder.cc b/src/inet/common/figures/FigureRecorder.cc index ac372d69f4d..4dde7f4b983 100644 --- a/src/inet/common/figures/FigureRecorder.cc +++ b/src/inet/common/figures/FigureRecorder.cc @@ -34,7 +34,7 @@ void FigureRecorder::init(Context *ctx) if (!figure) throw cRuntimeError("Figure '%s' in module '%s' not found", figureName.c_str(), module->getFullPath().c_str()); indicatorFigure = check_and_cast(figure); - if (series > indicatorFigure->getNumSeries()) + if (series < 0 || series >= indicatorFigure->getNumSeries()) throw cRuntimeError("series :%d is out of bounds, figure '%s' supports %d series", series, figureName.c_str(), indicatorFigure->getNumSeries()); } From f44c9de1188443ed6cfe420bc692ab5f66f8dff9 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:46 +0200 Subject: [PATCH 2/8] figures: rename the series of an indicator figure to items ~IIndicatorFigure called the values it displays series. That word only fits ~PlotFigure, where a series really is a sequence of values over time; for a gauge, a counter or a thermometer the indexed thing is a single value that happens to have an identity of its own. Rename it to item: getNumItems() instead of getNumSeries(), and the index parameter of setValue() to index. Only ~PlotFigure and ~FigureRecorder follow, because the name of a parameter is not part of a signature, so the figures that display a single value are untouched. getNumSeries() stays for one release as a deprecated method, and the default getNumItems() calls it, so an indicator figure implemented outside INET keeps working until its author renames the override. Note that overriding a deprecated method is not a use of its name, so the compiler does not warn at the override itself; the deprecation is visible in the header and at any remaining call site. ~PlotFigure keeps its own getNumSeries(), also deprecated, so that a caller of it does not silently get the interface default of 1 instead of the series count. No behavior change beyond the deprecation. --- WHATSNEW | 16 ++++++++++++++++ src/inet/common/figures/FigureRecorder.cc | 12 ++++++------ src/inet/common/figures/FigureRecorder.h | 2 +- src/inet/common/figures/IIndicatorFigure.h | 19 ++++++++++++++++++- src/inet/common/figures/PlotFigure.h | 2 ++ 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 346b834b8e6..4749809a8c9 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -37,6 +37,22 @@ Notable backward incompatible changes are the following: mistake stops with an error instead of writing into an item that does not exist. +3. Indicator figure items + + The getNumSeries() method of the IIndicatorFigure C++ interface has been + renamed to getNumItems(), and the first parameter of setValue() from series to + index. "Series" only describes what PlotFigure displays, where an item really + is a sequence of values over time; for a gauge, a counter or a thermometer the + indexed thing is a single value that happens to have an identity of its own. + + getNumSeries() is kept for one release as a deprecated method, and the default + implementation of getNumItems() calls it, so an indicator figure implemented + outside INET keeps working unchanged until its author renames the override. + Overriding a deprecated method is not itself a use of its name, so the compiler + does not warn at the override; the deprecation is visible in the header and at + any remaining call site. + + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/common/figures/FigureRecorder.cc b/src/inet/common/figures/FigureRecorder.cc index 4dde7f4b983..49690417715 100644 --- a/src/inet/common/figures/FigureRecorder.cc +++ b/src/inet/common/figures/FigureRecorder.cc @@ -21,26 +21,26 @@ void FigureRecorder::init(Context *ctx) if (!figureSpec) figureSpec = ctx->statisticName; std::string figureName; - int series; + int index; if (const char *lastColon = strrchr(figureSpec, ':')) { figureName = std::string(figureSpec, lastColon - figureSpec).c_str(); - series = utils::atoul(lastColon + 1); + index = utils::atoul(lastColon + 1); } else { figureName = figureSpec; - series = 0; + index = 0; } cFigure *figure = module->getCanvas()->getFigureByPath(figureName.c_str()); if (!figure) throw cRuntimeError("Figure '%s' in module '%s' not found", figureName.c_str(), module->getFullPath().c_str()); indicatorFigure = check_and_cast(figure); - if (series < 0 || series >= indicatorFigure->getNumSeries()) - throw cRuntimeError("series :%d is out of bounds, figure '%s' supports %d series", series, figureName.c_str(), indicatorFigure->getNumSeries()); + if (index < 0 || index >= indicatorFigure->getNumItems()) + throw cRuntimeError("Item index %d is out of bounds, figure '%s' displays %d items", index, figureName.c_str(), indicatorFigure->getNumItems()); } void FigureRecorder::collect(simtime_t_cref t, double value, cObject *details) { - indicatorFigure->setValue(series, t, value); + indicatorFigure->setValue(index, t, value); } } // namespace inet diff --git a/src/inet/common/figures/FigureRecorder.h b/src/inet/common/figures/FigureRecorder.h index dda743e3b05..a68062fd213 100644 --- a/src/inet/common/figures/FigureRecorder.h +++ b/src/inet/common/figures/FigureRecorder.h @@ -15,7 +15,7 @@ class INET_API FigureRecorder : public cNumericResultRecorder { protected: IIndicatorFigure *indicatorFigure = nullptr; - int series = 0; + int index = 0; protected: virtual void init(Context *ctx) override; diff --git a/src/inet/common/figures/IIndicatorFigure.h b/src/inet/common/figures/IIndicatorFigure.h index 91974276781..1ecc7c88949 100644 --- a/src/inet/common/figures/IIndicatorFigure.h +++ b/src/inet/common/figures/IIndicatorFigure.h @@ -17,8 +17,25 @@ class INET_API IIndicatorFigure public: virtual ~IIndicatorFigure() {} virtual const cFigure::Point getSize() const = 0; + + /** + * @deprecated Renamed to getNumItems(); override that instead. A figure that + * still overrides this method keeps working for one release, because the + * default implementation of getNumItems() calls it. + */ + [[deprecated("renamed to getNumItems(), override getNumItems() instead")]] virtual int getNumSeries() const { return 1; } - virtual void setValue(int series, simtime_t timestamp, double value) = 0; + + virtual int getNumItems() const { + // deliberately calls the deprecated method, so that a figure which has not + // moved its override yet still reports its own number of items +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return getNumSeries(); +#pragma GCC diagnostic pop + } + + virtual void setValue(int index, simtime_t timestamp, double value) = 0; virtual void refreshDisplay() {} }; diff --git a/src/inet/common/figures/PlotFigure.h b/src/inet/common/figures/PlotFigure.h index b2565efc90f..0d8e116013f 100644 --- a/src/inet/common/figures/PlotFigure.h +++ b/src/inet/common/figures/PlotFigure.h @@ -67,7 +67,9 @@ class INET_API PlotFigure : public cGroupFigure, public inet::IIndicatorFigure virtual void refreshDisplay() override; virtual void setNumSeries(int numSeries); + [[deprecated("renamed to getNumItems()")]] virtual int getNumSeries() const override { return numSeries; } + virtual int getNumItems() const override { return numSeries; } virtual void setValue(int series, simtime_t timestamp, double value) override { setValue(series, timestamp.dbl(), value); } virtual void setValue(int series, double x, double y); From f737023023bc98c966b353f7da276d360c774e6b Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:46 +0200 Subject: [PATCH 3/8] figures: add a bar chart indicator figure with named bars An indicator figure displayed either one value, or a fixed number of them identified by index. A quantity that exists per peer, per source or per flow fits neither: the set of them only becomes known while the simulation is running, and they carry names rather than indices. Add BarChartFigure, registered as the "barChart" figure type. Its items are bars, and the number of them is not fixed: setNumItems() sets it and setItemLabel() names each, the same way ~PlotFigure::setNumSeries() sets the number of its series and setLineColor() configures each. Bars are displayed in index order, so whoever sets them decides the order. A bar height represents the value over the minValue..maxValue range, or over the autoscaled range of the current values when maxValue is not given. barColor accepts a list of colors interpolated over the same range, so the color of a bar carries the value even where the chart is too small to read. Like the other instrument figures, everything about its appearance is a figure attribute parsed from a property, so none of it has to appear as a parameter of whatever displays the figure. --- WHATSNEW | 6 + src/inet/common/figures/BarChartFigure.cc | 391 ++++++++++++++++++++++ src/inet/common/figures/BarChartFigure.h | 157 +++++++++ 3 files changed, 554 insertions(+) create mode 100644 src/inet/common/figures/BarChartFigure.cc create mode 100644 src/inet/common/figures/BarChartFigure.h diff --git a/WHATSNEW b/WHATSNEW index 4749809a8c9..ae21ff67d34 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -81,6 +81,12 @@ Notable backward compatible changes are the following: to the MAC address string when no host owns the address. +4. A bar chart indicator figure + + The new "barChart" figure type displays several named values at once, one bar + each, with a value to color gradient and an optional autoscaled range. Unlike + the other indicator figures the number of its items is not fixed. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/common/figures/BarChartFigure.cc b/src/inet/common/figures/BarChartFigure.cc new file mode 100644 index 00000000000..f3f4e5083b3 --- /dev/null +++ b/src/inet/common/figures/BarChartFigure.cc @@ -0,0 +1,391 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/common/figures/BarChartFigure.h" + +#include + +namespace inet { + +Register_Figure("barChart", BarChartFigure); + +static const char *PKEY_POS = "pos"; +static const char *PKEY_MIN_VALUE = "minValue"; +static const char *PKEY_MAX_VALUE = "maxValue"; +static const char *PKEY_BAR_WIDTH = "barWidth"; +static const char *PKEY_BAR_SPACING = "barSpacing"; +static const char *PKEY_BAR_HEIGHT = "barHeight"; +static const char *PKEY_BAR_COLOR = "barColor"; +static const char *PKEY_BAR_LINE_COLOR = "barLineColor"; +static const char *PKEY_VALUE_FORMAT = "valueFormat"; +static const char *PKEY_VALUE_FONT = "valueFont"; +static const char *PKEY_VALUE_COLOR = "valueColor"; +static const char *PKEY_LABEL_FONT = "labelFont"; +static const char *PKEY_LABEL_COLOR = "labelColor"; +static const char *PKEY_LABEL_ANGLE = "labelAngle"; +static const char *PKEY_TITLE = "title"; +static const char *PKEY_TITLE_FONT = "titleFont"; +static const char *PKEY_TITLE_COLOR = "titleColor"; + +BarChartFigure::BarChartFigure(const char *name) : cGroupFigure(name) +{ + baselineFigure = new cLineFigure("baseline"); + baselineFigure->setLineColor(Color("grey50")); + baselineFigure->setVisible(false); + addFigure(baselineFigure); + titleFigure = new cLabelFigure("title"); + titleFigure->setAnchor(ANCHOR_N); + titleFigure->setFont(titleFont); + titleFigure->setColor(titleColor); + titleFigure->setVisible(false); + addFigure(titleFigure); + refreshTextMetrics(); +} + +void BarChartFigure::refreshTextMetrics() +{ + titleExtent = title.empty() ? Point(0, 0) : getTextExtent(titleFont, title.c_str()); + valueTextHeight = getTextExtent(valueFont, "0").y; + for (auto& bar : bars) + bar.labelExtent = getTextExtent(labelFont, bar.label.c_str()); +} + +const char **BarChartFigure::getAllowedPropertyKeys() const +{ + static const char *keys[32]; + if (!keys[0]) { + const char *localKeys[] = { + PKEY_POS, PKEY_MIN_VALUE, PKEY_MAX_VALUE, PKEY_BAR_WIDTH, PKEY_BAR_SPACING, + PKEY_BAR_HEIGHT, PKEY_BAR_COLOR, PKEY_BAR_LINE_COLOR, PKEY_VALUE_FORMAT, + PKEY_VALUE_FONT, PKEY_VALUE_COLOR, PKEY_LABEL_FONT, PKEY_LABEL_COLOR, + PKEY_LABEL_ANGLE, PKEY_TITLE, PKEY_TITLE_FONT, PKEY_TITLE_COLOR, nullptr + }; + concatArrays(keys, cGroupFigure::getAllowedPropertyKeys(), localKeys); + } + return keys; +} + +void BarChartFigure::parse(cProperty *property) +{ + cGroupFigure::parse(property); + + const char *s; + if (property->getNumValues(PKEY_POS) != 0) + setPosition(parsePoint(property, PKEY_POS, 0)); + if ((s = property->getValue(PKEY_MIN_VALUE)) != nullptr) + setMinValue(opp_atof(s)); + if ((s = property->getValue(PKEY_MAX_VALUE)) != nullptr) + setMaxValue(opp_atof(s)); + if ((s = property->getValue(PKEY_BAR_WIDTH)) != nullptr) + setBarWidth(opp_atof(s)); + if ((s = property->getValue(PKEY_BAR_SPACING)) != nullptr) + setBarSpacing(opp_atof(s)); + if ((s = property->getValue(PKEY_BAR_HEIGHT)) != nullptr) + setBarHeight(opp_atof(s)); + if (property->getNumValues(PKEY_BAR_COLOR) != 0) { + // a single color, or a value to color gradient given as a list of colors + std::vector colors; + for (int i = 0; i < property->getNumValues(PKEY_BAR_COLOR); i++) { + cStringTokenizer tokenizer(property->getValue(PKEY_BAR_COLOR, i), " ,"); + while (tokenizer.hasMoreTokens()) + colors.push_back(parseColor(tokenizer.nextToken())); + } + setBarColors(colors); + } + if ((s = property->getValue(PKEY_BAR_LINE_COLOR)) != nullptr) + setBarLineColor(parseColor(s)); + if ((s = property->getValue(PKEY_VALUE_FORMAT)) != nullptr) + setValueFormat(s); + if ((s = property->getValue(PKEY_VALUE_FONT)) != nullptr) + setValueFont(parseFont(s)); + if ((s = property->getValue(PKEY_VALUE_COLOR)) != nullptr) + setValueColor(parseColor(s)); + if ((s = property->getValue(PKEY_LABEL_FONT)) != nullptr) + setLabelFont(parseFont(s)); + if ((s = property->getValue(PKEY_LABEL_COLOR)) != nullptr) + setLabelColor(parseColor(s)); + if ((s = property->getValue(PKEY_LABEL_ANGLE)) != nullptr) + setLabelAngle(math::deg2rad(opp_atof(s))); + if ((s = property->getValue(PKEY_TITLE)) != nullptr) + setTitle(s); + if ((s = property->getValue(PKEY_TITLE_FONT)) != nullptr) + setTitleFont(parseFont(s)); + if ((s = property->getValue(PKEY_TITLE_COLOR)) != nullptr) + setTitleColor(parseColor(s)); +} + +void BarChartFigure::setNumItems(int numItems) +{ + while ((int)bars.size() > numItems) { + auto& bar = bars.back(); + delete removeFigure(bar.barFigure); + delete removeFigure(bar.valueFigure); + delete removeFigure(bar.labelFigure); + bars.pop_back(); + } + while ((int)bars.size() < numItems) { + Bar bar; + bar.barFigure = new cRectangleFigure("bar"); + bar.barFigure->setFilled(true); + bar.barFigure->setLineColor(barLineColor); + bar.barFigure->setVisible(false); + addFigure(bar.barFigure); + bar.valueFigure = new cLabelFigure("value"); + bar.valueFigure->setAnchor(ANCHOR_S); + bar.valueFigure->setFont(valueFont); + bar.valueFigure->setColor(valueColor); + bar.valueFigure->setVisible(false); + addFigure(bar.valueFigure); + bar.labelFigure = new cLabelFigure("label"); + bar.labelFigure->setAnchor(ANCHOR_NE); + bar.labelFigure->setAngle(labelAngle); + bar.labelFigure->setFont(labelFont); + bar.labelFigure->setColor(labelColor); + addFigure(bar.labelFigure); + bars.push_back(bar); + } + layout(); +} + +void BarChartFigure::setItemLabel(int index, const char *label) +{ + auto& bar = bars.at(index); + bar.label = label; + bar.labelFigure->setText(label); + bar.labelExtent = getTextExtent(labelFont, label); + layout(); +} + +const char *BarChartFigure::getItemLabel(int index) const +{ + return bars.at(index).label.c_str(); +} + +void BarChartFigure::setValue(int index, simtime_t timestamp, double value) +{ + if (index < 0 || index >= (int)bars.size()) + throw cRuntimeError(this, "Item index %d is out of bounds, the figure displays %d items", index, (int)bars.size()); + bars[index].value = value; + layout(); +} + +void BarChartFigure::layout() +{ + double titleHeight = title.empty() ? 0 : titleExtent.y + 3; + double valueHeight = valueFormat.empty() ? 0 : valueTextHeight + 2; + double chartWidth = bars.empty() ? 0 : bars.size() * barWidth + (bars.size() - 1) * barSpacing; + double baselineY = position.y + titleHeight + valueHeight + barHeight; + + // the rotated bar labels are anchored at the bar center and hang below and + // to the left of the baseline; the chart is indented to make room for them + double labelTextWidth = 0; + double labelTextHeight = 0; + for (auto& bar : bars) { + labelTextWidth = std::max(labelTextWidth, bar.labelExtent.x); + labelTextHeight = std::max(labelTextHeight, bar.labelExtent.y); + } + double cosAngle = std::abs(std::cos(labelAngle)); + double sinAngle = std::abs(std::sin(labelAngle)); + double labelWidth = labelTextWidth * cosAngle + labelTextHeight * sinAngle; + double labelHeight = labelTextWidth * sinAngle + labelTextHeight * cosAngle; + double indent = std::max(0.0, labelWidth - barWidth / 2); + double chartX = position.x + indent; + + size = Point(indent + chartWidth + 2, titleHeight + valueHeight + barHeight + labelHeight + 2); + + titleFigure->setVisible(!title.empty()); + titleFigure->setText(title.c_str()); + titleFigure->setPosition(Point(chartX + chartWidth / 2, position.y)); + + baselineFigure->setVisible(!bars.empty()); + baselineFigure->setStart(Point(chartX - 1, baselineY)); + baselineFigure->setEnd(Point(chartX + chartWidth + 1, baselineY)); + + // the value range the bar heights and colors are mapped to + double effectiveMaxValue = maxValue; + if (!(effectiveMaxValue > minValue)) { + effectiveMaxValue = minValue; + for (auto& bar : bars) + if (!std::isnan(bar.value) && bar.value > effectiveMaxValue) + effectiveMaxValue = bar.value; + } + double range = effectiveMaxValue - minValue; + + for (size_t i = 0; i < bars.size(); i++) { + auto& bar = bars[i]; + double x = chartX + i * (barWidth + barSpacing); + double fraction = std::isnan(bar.value) || range <= 0 ? 0 : (bar.value - minValue) / range; + fraction = std::max(0.0, std::min(1.0, fraction)); + double height = barHeight * fraction; + + bar.barFigure->setVisible(!std::isnan(bar.value) && height > 0); + bar.barFigure->setBounds(Rectangle(x, baselineY - height, barWidth, height)); + bar.barFigure->setFillColor(getBarColor(bar.value, effectiveMaxValue)); + bar.barFigure->setTooltip((bar.label + ": " + formatValue(bar.value)).c_str()); + + bar.valueFigure->setVisible(!valueFormat.empty() && !std::isnan(bar.value)); + bar.valueFigure->setText(formatValue(bar.value).c_str()); + bar.valueFigure->setPosition(Point(x + barWidth / 2, baselineY - height - 1)); + + bar.labelFigure->setPosition(Point(x + barWidth / 2, baselineY + 2)); + } +} + +std::string BarChartFigure::formatValue(double value) const +{ + if (std::isnan(value)) + return "-"; + char buffer[64]; + snprintf(buffer, sizeof(buffer), valueFormat.c_str(), value); + return buffer; +} + +cFigure::Color BarChartFigure::getBarColor(double value, double maxForScale) const +{ + if (barColors.empty()) + return Color("grey"); + if (barColors.size() == 1 || std::isnan(value)) + return barColors[0]; + double range = maxForScale - minValue; + double fraction = range > 0 ? (value - minValue) / range : 0; + fraction = std::max(0.0, std::min(1.0, fraction)); + double position = fraction * (barColors.size() - 1); + int index = (int)std::floor(position); + if (index >= (int)barColors.size() - 1) + return barColors.back(); + double weight = position - index; + auto& color = barColors[index]; + auto& nextColor = barColors[index + 1]; + auto interpolate = [&](uint8_t c, uint8_t nextC) { return (uint8_t)std::round(c + (nextC - c) * weight); }; + return Color(interpolate(color.red, nextColor.red), interpolate(color.green, nextColor.green), interpolate(color.blue, nextColor.blue)); +} + +cFigure::Point BarChartFigure::getTextExtent(const Font& font, const char *text) const +{ + double width, height, ascent; + getSimulation()->getEnvir()->getTextExtent(font, text, width, height, ascent); + return Point(width, height); +} + +void BarChartFigure::setPosition(const Point& position) +{ + this->position = position; + layout(); +} + +void BarChartFigure::setMinValue(double value) +{ + minValue = value; + layout(); +} + +void BarChartFigure::setMaxValue(double value) +{ + maxValue = value; + layout(); +} + +void BarChartFigure::setBarWidth(double width) +{ + barWidth = width; + layout(); +} + +void BarChartFigure::setBarSpacing(double spacing) +{ + barSpacing = spacing; + layout(); +} + +void BarChartFigure::setBarHeight(double height) +{ + barHeight = height; + layout(); +} + +void BarChartFigure::setBarColors(const std::vector& colors) +{ + barColors = colors; + layout(); +} + +void BarChartFigure::setBarLineColor(const Color& color) +{ + barLineColor = color; + for (auto& bar : bars) + bar.barFigure->setLineColor(color); +} + +void BarChartFigure::setValueFormat(const char *format) +{ + valueFormat = format; + layout(); +} + +void BarChartFigure::setValueFont(const Font& font) +{ + valueFont = font; + for (auto& bar : bars) + bar.valueFigure->setFont(font); + refreshTextMetrics(); + layout(); +} + +void BarChartFigure::setValueColor(const Color& color) +{ + valueColor = color; + for (auto& bar : bars) + bar.valueFigure->setColor(color); +} + +void BarChartFigure::setLabelFont(const Font& font) +{ + labelFont = font; + for (auto& bar : bars) + bar.labelFigure->setFont(font); + refreshTextMetrics(); + layout(); +} + +void BarChartFigure::setLabelColor(const Color& color) +{ + labelColor = color; + for (auto& bar : bars) + bar.labelFigure->setColor(color); +} + +void BarChartFigure::setLabelAngle(double angle) +{ + labelAngle = angle; + for (auto& bar : bars) + bar.labelFigure->setAngle(angle); + layout(); +} + +void BarChartFigure::setTitle(const char *title) +{ + this->title = title; + refreshTextMetrics(); + layout(); +} + +void BarChartFigure::setTitleFont(const Font& font) +{ + titleFont = font; + titleFigure->setFont(font); + refreshTextMetrics(); + layout(); +} + +void BarChartFigure::setTitleColor(const Color& color) +{ + titleColor = color; + titleFigure->setColor(color); +} + +} // namespace inet + diff --git a/src/inet/common/figures/BarChartFigure.h b/src/inet/common/figures/BarChartFigure.h new file mode 100644 index 00000000000..aa225f714f2 --- /dev/null +++ b/src/inet/common/figures/BarChartFigure.h @@ -0,0 +1,157 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_BARCHARTFIGURE_H +#define __INET_BARCHARTFIGURE_H + +#include +#include + +#include "inet/common/INETMath.h" +#include "inet/common/figures/IIndicatorFigure.h" + +namespace inet { + +/** + * A small bar chart indicator figure. Its items are bars: the height and + * optionally the color of a bar represent the last value of that item, and its + * label is displayed under it, rotated. + * + * In contrast with the other indicator figures, the number of bars is not fixed: + * setNumItems() sets it, the same way ~PlotFigure::setNumSeries() sets the number + * of its series. This makes the chart usable for a quantity that exists per peer, + * per source or per flow, where the set of them only becomes known while the + * simulation is running. Bars are displayed in index order, so whoever sets them + * decides the order. + * + * A value is mapped to a bar height between zero and barHeight over the + * minValue..maxValue range. If maxValue is not greater than minValue, then the + * chart is autoscaled to its own current maximum. The same range is used to + * interpolate the bar color if more than one bar color is given. + * + * The chart is laid out in pixels, from its position towards the bottom right, + * except for the rotated bar labels, which are kept inside the bounding box + * returned by getSize() by indenting the chart accordingly. + */ +class INET_API BarChartFigure : public cGroupFigure, public IIndicatorFigure +{ + protected: + class Bar { + public: + std::string label; + double value = NaN; + Point labelExtent = Point(0, 0); // the size of the label text, measured when it changes + cRectangleFigure *barFigure = nullptr; + cLabelFigure *valueFigure = nullptr; + cLabelFigure *labelFigure = nullptr; + }; + + Point position = Point(0, 0); + double minValue = 0; + double maxValue = 0; // autoscale if not greater than minValue + double barWidth = 12; + double barSpacing = 8; + double barHeight = 60; // height of a bar displaying maxValue + std::vector barColors = { Color("blue") }; + Color barLineColor = Color("grey30"); + std::string valueFormat = "%g"; // empty means no value labels + Font valueFont = Font("", 8); + Color valueColor = Color("black"); + Font labelFont = Font("", 8); + Color labelColor = Color("black"); + double labelAngle = M_PI / 4; // radians, counterclockwise + std::string title; + Font titleFont = Font("", 9); + Color titleColor = Color("black"); + + std::vector bars; + cLabelFigure *titleFigure = nullptr; + cLineFigure *baselineFigure = nullptr; + Point size = Point(0, 0); + Point titleExtent = Point(0, 0); + double valueTextHeight = 0; + + protected: + virtual void parse(cProperty *property) override; + virtual const char **getAllowedPropertyKeys() const override; + + // Measures the texts the layout depends on; called when a text or a font changes. + virtual void refreshTextMetrics(); + virtual void layout(); + virtual std::string formatValue(double value) const; + virtual Color getBarColor(double value, double maxForScale) const; + virtual Point getTextExtent(const Font& font, const char *text) const; + + public: + BarChartFigure(const char *name = nullptr); + + virtual const Point getSize() const override { return size; } + virtual int getNumItems() const override { return bars.size(); } + virtual void setValue(int index, simtime_t timestamp, double value) override; + virtual void refreshDisplay() override { layout(); } + + // Sets the number of bars, adding or removing them at the end as needed. + virtual void setNumItems(int numItems); + virtual void setItemLabel(int index, const char *label); + virtual const char *getItemLabel(int index) const; + + const Point& getPosition() const { return position; } + void setPosition(const Point& position); + + double getMinValue() const { return minValue; } + void setMinValue(double value); + + double getMaxValue() const { return maxValue; } + void setMaxValue(double value); + + double getBarWidth() const { return barWidth; } + void setBarWidth(double width); + + double getBarSpacing() const { return barSpacing; } + void setBarSpacing(double spacing); + + double getBarHeight() const { return barHeight; } + void setBarHeight(double height); + + const std::vector& getBarColors() const { return barColors; } + void setBarColors(const std::vector& colors); + + const Color& getBarLineColor() const { return barLineColor; } + void setBarLineColor(const Color& color); + + const char *getValueFormat() const { return valueFormat.c_str(); } + void setValueFormat(const char *format); + + const Font& getValueFont() const { return valueFont; } + void setValueFont(const Font& font); + + const Color& getValueColor() const { return valueColor; } + void setValueColor(const Color& color); + + const Font& getLabelFont() const { return labelFont; } + void setLabelFont(const Font& font); + + const Color& getLabelColor() const { return labelColor; } + void setLabelColor(const Color& color); + + double getLabelAngle() const { return labelAngle; } + void setLabelAngle(double angle); + + const char *getTitle() const { return title.c_str(); } + void setTitle(const char *title); + + const Font& getTitleFont() const { return titleFont; } + void setTitleFont(const Font& font); + + const Color& getTitleColor() const { return titleColor; } + void setTitleColor(const Color& color); +}; + +} // namespace inet + +#endif + From d09fda84887abbad675c8ee855af5728d9b09a33 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:46 +0200 Subject: [PATCH 4/8] visualizer: choose the figure that displays a statistic ~StatisticCanvasVisualizer displayed a statistic with a text label, or with an indicator figure taken from a figure template property along its module path (the propertyName parameter). A template cannot be given in an ini file and cannot refer to module parameters, so choosing a gauge instead of a label, or merely changing its scale, meant editing a NED file. Add the figure parameter, which takes the attributes of the figure the same way an @figure property gives them: *.visualizer.statisticVisualizer.figure = {type: "gauge", size: [60, 60], maxValue: 100} The parameter, unlike the template, can be set from an ini file and can refer to module parameters; the template, unlike the parameter, can replace a figure a derived visualizer already defaults to, and therefore takes precedence. Along the way, the figure type is now also looked up among the types registered with Register_Figure(), not only as an inet::Figure class; an indicator figure is given the value in the display unit (what the text label would display) rather than the raw value; and the size reserved for the figure among the annotations of the network node is updated when it changes, as it does in a counter gaining digits. --- .../common/StatisticCanvasVisualizer.cc | 122 ++++++++++++++---- .../canvas/common/StatisticCanvasVisualizer.h | 15 +++ .../common/StatisticCanvasVisualizer.ned | 15 ++- 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc index 769967f6c6e..838077ebf7a 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc @@ -9,7 +9,6 @@ #include "inet/common/ModuleAccess.h" #include "inet/common/figures/BoxedLabelFigure.h" -#include "inet/common/figures/IIndicatorFigure.h" namespace inet { @@ -17,6 +16,23 @@ namespace visualizer { Define_Module(StatisticCanvasVisualizer); +static std::string getFigureAttributeValue(const char *key, const cValue& value) +{ + switch (value.getType()) { + case cValue::BOOL: + return value.boolValue() ? "true" : "false"; + case cValue::INT: + case cValue::DOUBLE: + if (!opp_isempty(value.getUnit())) + throw cRuntimeError("Figure attribute '%s' must be dimensionless, but it has unit '%s'", key, value.getUnit()); + return value.getType() == cValue::INT ? std::to_string(value.intValue()) : opp_stringf("%.15g", value.doubleValue()); + case cValue::STRING: + return value.stringValue(); + default: + throw cRuntimeError("Invalid value for figure attribute '%s': %s", key, value.str().c_str()); + } +} + StatisticCanvasVisualizer::StatisticCanvasVisualization::StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, int moduleId, simsignal_t signal, const char *unit) : StatisticVisualization(moduleId, signal, unit), networkNodeVisualization(networkNodeVisualization), @@ -40,12 +56,83 @@ void StatisticCanvasVisualizer::initialize(int stage) } } -StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::createStatisticVisualization(cComponent *source, simsignal_t signal) +cProperty *StatisticCanvasVisualizer::findFigureTemplateProperty() { - cFigure *figure = nullptr; const char *propertyName = par("propertyName"); + if (opp_isempty(propertyName)) + return nullptr; const char *propertyIndex = par("propertyIndex"); - if (!strcmp(propertyName, "")) { + for (cModule *module = this; module != nullptr; module = module->getParentModule()) { + auto property = module->getProperties()->get(propertyName, opp_isempty(propertyIndex) ? nullptr : propertyIndex); + if (property != nullptr) + return property; + } + throw cRuntimeError("Cannot find property: @%s[%s] on the module path: %s", propertyName, propertyIndex, getFullPath().c_str()); +} + +cFigure *StatisticCanvasVisualizer::createIndicatorFigure() +{ + // the attributes of the figure are given either in a figure template property along the + // module path of this visualizer, or directly in the figure parameter; the template takes + // precedence, so that it can also replace the figure a derived visualizer defaults to + if (auto property = findFigureTemplateProperty()) + return createFigure(property); + auto figureParameter = par("figure").objectValue(); + auto figureAttributes = dynamic_cast(figureParameter); + if (figureParameter != nullptr && figureAttributes == nullptr) + throw cRuntimeError("The figure parameter must contain the attributes of a figure, e.g. {type: \"gauge\", maxValue: 100}"); + if (figureAttributes != nullptr && figureAttributes->size() != 0) { + cProperty property("figure"); + for (auto& field : figureAttributes->getFields()) { + const char *key = field.first.c_str(); + const cValue& value = field.second; + property.addKey(key); + // an attribute that takes several values, e.g. pos, is given as an array + if (value.containsObject()) { + auto values = dynamic_cast(value.objectValue()); + if (values == nullptr) + throw cRuntimeError("Invalid value for figure attribute '%s': %s", key, value.str().c_str()); + for (int i = 0; i < values->size(); i++) + property.setValue(key, i, getFigureAttributeValue(key, values->get(i)).c_str()); + } + else + property.setValue(key, 0, getFigureAttributeValue(key, value).c_str()); + } + return createFigure(&property); + } + return nullptr; +} + +cFigure *StatisticCanvasVisualizer::createFigure(cProperty *property) const +{ + const char *type = property->getValue("type"); + if (opp_isempty(type)) + throw cRuntimeError("The type of the figure is not specified"); + // for backward compatibility, a type also selects the inet::Figure class, + // even if the figure class is not registered with Register_Figure() + std::string className = type; + className[0] = toupper(className[0]); + className = "inet::" + className + "Figure"; + auto figure = dynamic_cast(createOneIfClassIsKnown(className.c_str())); + if (figure == nullptr) + figure = getCanvas()->createFigure(type); + figure->parse(property); + return figure; +} + +void StatisticCanvasVisualizer::setAnnotationSize(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, const cFigure::Point& size, cFigure::Point& lastSize) const +{ + // avoid invalidating the annotation layout of the network node when nothing changed + if (size.x != lastSize.x || size.y != lastSize.y) { + lastSize = size; + networkNodeVisualization->setAnnotationSize(figure, size); + } +} + +StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::createStatisticVisualization(cComponent *source, simsignal_t signal) +{ + cFigure *figure = createIndicatorFigure(); + if (figure == nullptr) { auto boxedLabelFigure = new BoxedLabelFigure("statistic"); boxedLabelFigure->setFont(font); boxedLabelFigure->setText(""); @@ -56,23 +143,6 @@ StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::crea figure->setTooltip("This label represents the current value of a statistic"); } else { - cProperty *property = nullptr; - cModule *current = this; - while (current != nullptr) { - property = current->getProperties()->get(propertyName, strcmp(propertyIndex, "") ? propertyIndex : nullptr); - if (property != nullptr) - break; - current = current->getParentModule(); - } - if (property == nullptr) - throw cRuntimeError("Cannot find property: @%s[%s] on the module path: %s", propertyName, propertyIndex, getFullPath().c_str()); - std::string classname = property->getValue("type"); - classname[0] = toupper(classname[0]); - classname = "inet::" + classname + "Figure"; - figure = check_and_cast(createOneIfClassIsKnown(classname.c_str())); - if (figure == nullptr) - throw cRuntimeError("Cannot create figure with type: %s", property->getValue("type")); - figure->parse(property); figure->setName("statistic"); std::string tooltip = std::string("This figure represents the value of ") + statisticName + " in " + source->getFullPath(); figure->setTooltip(tooltip.c_str()); @@ -109,10 +179,14 @@ void StatisticCanvasVisualizer::removeStatisticVisualization(const StatisticVisu void StatisticCanvasVisualizer::refreshStatisticVisualization(const StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::refreshStatisticVisualization(statisticVisualization); - auto statisticCanvasVisualization = static_cast(statisticVisualization); + auto statisticCanvasVisualization = static_cast(const_cast(statisticVisualization)); auto figure = statisticCanvasVisualization->figure; - if (auto indicatorFigure = dynamic_cast(figure)) - indicatorFigure->setValue(0, simTime(), statisticVisualization->recorder->getLastValue()); + if (auto indicatorFigure = dynamic_cast(figure)) { + // the value in the display unit, the same value the text label would display + indicatorFigure->setValue(0, simTime(), statisticVisualization->printValue); + // the size of a figure may depend on its value, e.g. that of a counter + setAnnotationSize(statisticCanvasVisualization->networkNodeVisualization, figure, indicatorFigure->getSize(), statisticCanvasVisualization->annotationSize); + } else { auto boxedLabelFigure = check_and_cast(figure); boxedLabelFigure->setText(getText(statisticVisualization).c_str()); diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h index df94830ffb1..aa6268307db 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h @@ -9,6 +9,7 @@ #define __INET_STATISTICCANVASVISUALIZER_H #include "inet/common/ModuleRefByPar.h" +#include "inet/common/figures/IIndicatorFigure.h" #include "inet/visualizer/base/StatisticVisualizerBase.h" #include "inet/visualizer/canvas/scene/NetworkNodeCanvasVisualization.h" #include "inet/visualizer/canvas/scene/NetworkNodeCanvasVisualizer.h" @@ -24,6 +25,7 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase public: NetworkNodeCanvasVisualization *networkNodeVisualization = nullptr; cFigure *figure = nullptr; + cFigure::Point annotationSize = cFigure::Point(NaN, NaN); public: StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, int moduleId, simsignal_t signal, const char *unit); @@ -37,6 +39,19 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase protected: virtual void initialize(int stage) override; + // Creates the figure that displays the value as configured by the `figure` parameter, + // or by the figure template property named by the `propertyName` parameter; returns + // nullptr if neither is given. + virtual cFigure *createIndicatorFigure(); + // Returns the figure template property named by the `propertyName` and `propertyIndex` + // parameters, looked up along the module path, or nullptr if `propertyName` is empty. + virtual cProperty *findFigureTemplateProperty(); + // Creates the figure of the type given in the property (see Register_Figure()) and + // configures it from the other attributes of the property. + virtual cFigure *createFigure(cProperty *property) const; + // Updates the size the network node visualizer reserves for a figure, if it changed. + virtual void setAnnotationSize(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, const cFigure::Point& size, cFigure::Point& lastSize) const; + virtual StatisticVisualization *createStatisticVisualization(cComponent *source, simsignal_t signal) override; virtual void addStatisticVisualization(const StatisticVisualization *statisticVisualization) override; virtual void removeStatisticVisualization(const StatisticVisualization *statisticVisualization) override; diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned index 4297fc42b05..0180a4af829 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned @@ -18,8 +18,9 @@ import inet.visualizer.contract.IStatisticVisualizer; // filled white rectangle. // // The statistic value is displayed with a label by default. Alternatively, any -// indicator figure can be used by configuring the property parameters and -// providing a figure template along the module path of the statistic visualizer. +// indicator figure can display it, either configured in the `figure` parameter, +// or by providing a figure template along the module path of the statistic +// visualizer and referring to it with the property parameters. // // @see ~StatisticOsgVisualizer, ~StatisticVisualizer, ~StatisticVisualizerBase, ~IStatisticVisualizer // @@ -27,7 +28,15 @@ simple StatisticCanvasVisualizer extends StatisticVisualizerBase like IStatistic { parameters: double zIndex = default(10); // Determines the drawing order of figures relative to other visualizers - string propertyName = default(""); // Optional property name of a figure template along the module path of the visualizer + + // The figure that displays the value, given as the attributes of a figure the + // same way as in an @figure property, e.g. {type: "gauge", minValue: 0, maxValue: + // 100, tickSize: 20}. The type selects the figure class registered with + // Register_Figure(), the other attributes are the parameters of that particular + // figure. Empty means a text label. + object figure = default({}); + + string propertyName = default(""); // Optional property name of a figure template along the module path of the visualizer, an alternative to the figure parameter, taking precedence over it string propertyIndex = default(""); // Optional property index of a figure template along the module path of the visualizer @class(StatisticCanvasVisualizer); } From 43940c20f5f3788dba966d166c069fdda4be5f57 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:46 +0200 Subject: [PATCH 5/8] visualizer: split the values of a statistic into several items ~StatisticVisualizerBase displayed the last value of a statistic per signal source. A quantity that exists per peer or per flow forces a choice between an aggregate that hides the distribution and one visualizer instance per value with no visual relationship between them, while the recording side already handles it with demux(). Add the splitBy parameter, which determines whether the values a signal source emits are split into several statistics, and what identifies each of them: - "details": one statistic per distinct details object emitted with the value, the live counterpart of the demux() result filter - "flow": one statistic per packet flow of the source, demultiplexing its signal by the flow tag (statisticExpression contains demuxFlow()), so an item can display a count or a throughput rather than the raw value StatisticVisualization now holds the items it displays instead of a single value. An unsplit statistic has one item whose label is empty, so there is one visualization class and one code path rather than one of each per case. The visualizer owns the label to index mapping -- items are displayed in label order, so an item's index is its position among them -- and the figure is only ever given a number of items, their labels, and their values. How the items are displayed is not for the visualizer to decide: that is what the figure is for. A visualizer displaying several values without a figure configured defaults to a bar chart. The per frame refresh this adds is the first thing that touches a visualization outside a signal receipt from a live source, so it is also the first that can meet a deleted one. A visualization whose module is gone is dropped before its figure is read, and the network node visualization is looked up rather than remembered, because a node visualization is a figure group that is deleted with its node and takes the statistic figure with it. --- .../base/StatisticVisualizerBase.cc | 150 +++++++++++++++--- .../visualizer/base/StatisticVisualizerBase.h | 89 ++++++++--- .../base/StatisticVisualizerBase.ned | 13 ++ .../common/StatisticCanvasVisualizer.cc | 131 ++++++++++++--- .../canvas/common/StatisticCanvasVisualizer.h | 25 ++- .../common/StatisticCanvasVisualizer.ned | 10 +- .../osg/common/StatisticOsgVisualizer.cc | 6 +- .../osg/common/StatisticOsgVisualizer.h | 6 +- 8 files changed, 351 insertions(+), 79 deletions(-) diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.cc b/src/inet/visualizer/base/StatisticVisualizerBase.cc index 8ebcb090248..8bbe2e6c6ae 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.cc +++ b/src/inet/visualizer/base/StatisticVisualizerBase.cc @@ -7,6 +7,8 @@ #include "inet/visualizer/base/StatisticVisualizerBase.h" +#include + #include "inet/common/ModuleAccess.h" #include "omnetpp/cstatisticbuilder.h" @@ -75,9 +77,19 @@ void StatisticVisualizerBase::initialize(int stage) opacity = par("opacity"); placementHint = parsePlacement(par("placementHint")); placementPriority = par("placementPriority"); + const char *splitBy = par("splitBy"); + if (!strcmp(splitBy, "none")) + splitMode = SPLIT_NONE; + else if (!strcmp(splitBy, "details")) + splitMode = SPLIT_DETAILS; + else if (!strcmp(splitBy, "flow")) + splitMode = SPLIT_FLOW; + else + throw cRuntimeError("Unknown splitBy parameter value: '%s'", splitBy); if (displayStatistics) { if (opp_isempty(signalName)) throw cRuntimeError("The signalName parameter must be not empty"); + subscribedSignal = registerSignal(signalName); subscribe(); } } @@ -95,7 +107,7 @@ void StatisticVisualizerBase::handleParameterChange(const char *name) void StatisticVisualizerBase::subscribe() { - visualizationSubjectModule->subscribe(registerSignal(signalName), this); + visualizationSubjectModule->subscribe(subscribedSignal, this); } void StatisticVisualizerBase::unsubscribe() @@ -103,7 +115,7 @@ void StatisticVisualizerBase::unsubscribe() // NOTE: lookup the module again because it may have been deleted first auto visualizationSubjectModule = findModuleFromPar(par("visualizationSubjectModule"), this); if (visualizationSubjectModule != nullptr) - visualizationSubjectModule->unsubscribe(registerSignal(signalName), this); + visualizationSubjectModule->unsubscribe(subscribedSignal, this); } void StatisticVisualizerBase::addResultRecorder(cComponent *source, simsignal_t signal) @@ -149,7 +161,7 @@ StatisticVisualizerBase::LastValueRecorder *StatisticVisualizerBase::findResultR return nullptr; } -std::string StatisticVisualizerBase::getText(const StatisticVisualization *statisticVisualization) +std::string StatisticVisualizerBase::getText(const StatisticVisualization *statisticVisualization) const { DirectiveResolver directiveResolver(this, statisticVisualization); return format.formatString(&directiveResolver); @@ -169,7 +181,7 @@ const char *StatisticVisualizerBase::getUnit(cComponent *source) return statisticUnit; } -std::string StatisticVisualizerBase::getRecordingMode() +std::string StatisticVisualizerBase::getRecordingMode() const { if (*statisticExpression == '\0') return "statisticVisualizerLastValueRecorder"; @@ -177,46 +189,59 @@ std::string StatisticVisualizerBase::getRecordingMode() return std::string("statisticVisualizerLastValueRecorder(") + statisticExpression + std::string(")"); } -const StatisticVisualizerBase::StatisticVisualization *StatisticVisualizerBase::getStatisticVisualization(cComponent *source, simsignal_t signal) +StatisticVisualizerBase::StatisticVisualization *StatisticVisualizerBase::getStatisticVisualization(int moduleId) { - auto key = std::pair(source->getId(), signal); - auto it = statisticVisualizations.find(key); - return (it == statisticVisualizations.end()) ? nullptr : it->second; + auto it = statisticVisualizations.find(moduleId); + return it == statisticVisualizations.end() ? nullptr : it->second; } -void StatisticVisualizerBase::addStatisticVisualization(const StatisticVisualization *statisticVisualization) +StatisticVisualizerBase::StatisticVisualization *StatisticVisualizerBase::getOrCreateStatisticVisualization(cComponent *module, simsignal_t signal) { - auto key = std::pair(statisticVisualization->moduleId, statisticVisualization->signal); - statisticVisualizations[key] = statisticVisualization; + auto statisticVisualization = getStatisticVisualization(module->getId()); + if (statisticVisualization == nullptr) { + statisticVisualization = createStatisticVisualization(module, signal); + if (statisticVisualization == nullptr) + return nullptr; // the module is not visualized + addStatisticVisualization(statisticVisualization); + } + return statisticVisualization; } -void StatisticVisualizerBase::removeStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticVisualizerBase::addStatisticVisualization(StatisticVisualization *statisticVisualization) { - auto key = std::pair(statisticVisualization->moduleId, statisticVisualization->signal); - statisticVisualizations.erase(statisticVisualizations.find(key)); + statisticVisualizations[statisticVisualization->moduleId] = statisticVisualization; +} + +void StatisticVisualizerBase::removeStatisticVisualization(StatisticVisualization *statisticVisualization) +{ + statisticVisualizations.erase(statisticVisualization->moduleId); } void StatisticVisualizerBase::removeAllStatisticVisualizations() { - std::vector removedStatisticVisualizations; + std::vector removedStatisticVisualizations; for (auto it : statisticVisualizations) removedStatisticVisualizations.push_back(it.second); - for (auto it : removedStatisticVisualizations) { - removeStatisticVisualization(it); - delete it; + for (auto statisticVisualization : removedStatisticVisualizations) { + removeStatisticVisualization(statisticVisualization); + delete statisticVisualization; } + registeredSourceIds.clear(); } void StatisticVisualizerBase::processSignal(cComponent *source, simsignal_t signal, std::function receiveSignal) { - auto statisticVisualization = getStatisticVisualization(source, signal); + auto statisticVisualization = getStatisticVisualization(source->getId()); if (statisticVisualization != nullptr) refreshStatisticVisualization(statisticVisualization); else { if (sourceFilter.matches(check_and_cast(source))) { - auto statisticVisualization = createStatisticVisualization(source, signal); + statisticVisualization = createStatisticVisualization(source, signal); + if (statisticVisualization == nullptr) + return; // the module is not visualized addResultRecorder(source, signal); - statisticVisualization->recorder = getResultRecorder(source, signal); + // the statistic is neither split nor grouped, so it has a single, unlabelled item + statisticVisualization->items[""].recorder = getResultRecorder(source, signal); auto listeners = source->getLocalSignalListeners(signal); receiveSignal(listeners[listeners.size() - 1]); addStatisticVisualization(statisticVisualization); @@ -225,9 +250,10 @@ void StatisticVisualizerBase::processSignal(cComponent *source, simsignal_t sign } } -void StatisticVisualizerBase::refreshStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticVisualizerBase::refreshStatisticVisualization(StatisticVisualization *statisticVisualization) { - double value = statisticVisualization->recorder->getLastValue(); + auto& item = statisticVisualization->items[""]; + double value = item.recorder->getLastValue(); if (std::isnan(value) || units.empty()) { statisticVisualization->printValue = value; statisticVisualization->printUnit = statisticVisualization->unit == nullptr ? "" : statisticVisualization->unit; @@ -240,6 +266,84 @@ void StatisticVisualizerBase::refreshStatisticVisualization(const StatisticVisua break; } } + item.value = statisticVisualization->printValue; +} + +double StatisticVisualizerBase::convertToDisplayUnit(double value) const +{ + if (std::isnan(value) || units.empty() || opp_isempty(statisticUnit)) + return value; + return cNEDValue::convertUnit(value, statisticUnit, units[0].c_str()); +} + +void StatisticVisualizerBase::processSplitValue(cComponent *source, double value, cObject *details) +{ + // the statistic is identified by the details object emitted with the value, the same way + // as the demux() result filter identifies the statistics it demultiplexes a signal into; + // a value emitted without a details object (e.g. the rate of a group addressed frame) + // belongs to no statistic + std::string label = details != nullptr ? details->getFullName() : ""; + if (label.empty()) + return; + auto module = check_and_cast(source); + if (!sourceFilter.matches(module)) + return; + auto statisticVisualization = getOrCreateStatisticVisualization(module, subscribedSignal); + if (statisticVisualization == nullptr) + return; + statisticVisualization->items[label].value = convertToDisplayUnit(value); +} + +void StatisticVisualizerBase::registerSource(cComponent *source, simsignal_t signal) +{ + auto module = check_and_cast(source); + if (registeredSourceIds.find(module->getId()) != registeredSourceIds.end()) + return; // already registered + if (!sourceFilter.matches(module)) + return; + registeredSourceIds.insert(module->getId()); + auto statisticVisualization = getOrCreateStatisticVisualization(module, signal); + if (statisticVisualization == nullptr) + return; + // the values come from the result recorders built from statisticExpression, so that an + // item can display e.g. a count or a throughput rather than the raw value of the signal + addResultRecorder(source, signal); + // when splitting by flow, statisticExpression contains demuxFlow(), so the recorder chain + // creates a separate recorder per flow as the flows appear, see refreshFlowItemValues() +} + +void StatisticVisualizerBase::refreshFlowItemValues() const +{ + for (auto& it : statisticVisualizations) { + auto statisticVisualization = it.second; + auto source = getSimulation()->getModule(statisticVisualization->moduleId); + if (source == nullptr) + continue; + std::vector recorders; + for (auto listener : source->getLocalSignalListeners(subscribedSignal)) + if (auto resultListener = dynamic_cast(listener)) + collectResultRecorders(resultListener, recorders); + for (auto recorder : recorders) { + const char *label = recorder->getDemuxLabel(); + if (opp_isempty(label)) + continue; // the recorder of the undemultiplexed statistic is not an item + auto& item = statisticVisualization->items[label]; + item.recorder = recorder; + item.value = convertToDisplayUnit(recorder->getLastValue()); + } + } +} + +void StatisticVisualizerBase::collectResultRecorders(cResultListener *resultListener, std::vector& recorders) const +{ + if (auto resultRecorder = dynamic_cast(resultListener)) { + if (getRecordingMode() == resultRecorder->getRecordingMode() && !strcmp(statisticName, resultRecorder->getStatisticName())) + recorders.push_back(resultRecorder); + } + else if (auto resultFilter = dynamic_cast(resultListener)) { + for (auto delegate : resultFilter->getDelegates()) + collectResultRecorders(delegate, recorders); + } } } // namespace visualizer diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.h b/src/inet/visualizer/base/StatisticVisualizerBase.h index 43153316c54..ac216ca3c01 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.h +++ b/src/inet/visualizer/base/StatisticVisualizerBase.h @@ -9,6 +9,10 @@ #define __INET_STATISTICVISUALIZERBASE_H #include +#include +#include +#include +#include #include "inet/common/StringFormat.h" #include "inet/visualizer/base/VisualizerBase.h" @@ -22,6 +26,14 @@ namespace visualizer { class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener { public: + // Determines whether the values a signal source emits are split into several + // statistics, and what identifies each of them. + enum SplitMode { + SPLIT_NONE, // the source emits the values of a single statistic + SPLIT_DETAILS, // one statistic per distinct details object emitted with the value + SPLIT_FLOW, // one statistic per packet flow of the source, demultiplexed by the flow tag + }; + class INET_API LastValueRecorder : public cNumericResultRecorder { protected: double lastValue = NaN; @@ -34,12 +46,23 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener double getLastValue() const { return lastValue; } }; + // Displays the last value of a statistic, or of several statistics at once when the + // values of a source are split. Each value is one item of the figure, identified by its + // label; an unsplit statistic has a single item whose label is empty. The items are not + // known in advance, they appear as their labels do, which is the live counterpart of the + // demux() result filter used for recording. class INET_API StatisticVisualization { public: - LastValueRecorder *recorder = nullptr; - const int moduleId = -1; + class INET_API Item { + public: + LastValueRecorder *recorder = nullptr; // provides the value of this item + double value = NaN; // the last value, in the display unit + }; + + const int moduleId = -1; // the signal source the visualization belongs to const simsignal_t signal = -1; const char *unit = nullptr; + std::map items; // item label -> item; displayed in label order mutable double printValue = NaN; mutable const char *printUnit = nullptr; @@ -65,9 +88,11 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener bool displayStatistics = false; ModuleFilter sourceFilter; const char *signalName = nullptr; + simsignal_t subscribedSignal = SIMSIGNAL_NULL; // the signal named by signalName, resolved once const char *statisticName = nullptr; const char *statisticUnit = nullptr; const char *statisticExpression = nullptr; + SplitMode splitMode = SPLIT_NONE; StringFormat format; std::vector units; cFigure::Font font; @@ -78,7 +103,8 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener double placementPriority; //@} - std::map, const StatisticVisualization *> statisticVisualizations; + std::map statisticVisualizations; // module id -> visualization + std::set registeredSourceIds; // signal sources whose result recorders are already attached protected: virtual void initialize(int stage) override; @@ -91,28 +117,55 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener virtual void addResultRecorder(cComponent *source, simsignal_t signal); virtual LastValueRecorder *getResultRecorder(cComponent *source, simsignal_t signal); virtual LastValueRecorder *findResultRecorder(cResultListener *resultListener); - virtual std::string getText(const StatisticVisualization *statisticVisualization); + virtual std::string getText(const StatisticVisualization *statisticVisualization) const; virtual const char *getUnit(cComponent *source); - virtual std::string getRecordingMode(); - - virtual StatisticVisualization *createStatisticVisualization(cComponent *source, simsignal_t signal) = 0; - virtual const StatisticVisualization *getStatisticVisualization(cComponent *source, simsignal_t signal); - virtual void addStatisticVisualization(const StatisticVisualization *statisticVisualization); - virtual void removeStatisticVisualization(const StatisticVisualization *statisticVisualization); + virtual std::string getRecordingMode() const; + // Converts a value from the statistic unit to the first display unit, if both are given. + virtual double convertToDisplayUnit(double value) const; + + // Creates the visualization of one module, along with the figure that displays it; + // returns nullptr if the module is not visualized. + virtual StatisticVisualization *createStatisticVisualization(cComponent *module, simsignal_t signal) = 0; + virtual StatisticVisualization *getStatisticVisualization(int moduleId); + virtual StatisticVisualization *getOrCreateStatisticVisualization(cComponent *module, simsignal_t signal); + virtual void addStatisticVisualization(StatisticVisualization *statisticVisualization); + virtual void removeStatisticVisualization(StatisticVisualization *statisticVisualization); virtual void removeAllStatisticVisualizations(); - virtual void refreshStatisticVisualization(const StatisticVisualization *statisticVisualization); + virtual void refreshStatisticVisualization(StatisticVisualization *statisticVisualization); virtual void processSignal(cComponent *source, simsignal_t signal, std::function receiveSignal); + /** @name Splitting the values of one signal into several items */ + //@{ + // Stores the value of a signal as the last value of the item identified by the + // details object emitted with it (SPLIT_DETAILS). + virtual void processSplitValue(cComponent *source, double value, cObject *details); + // Registers a signal source as the source of the per flow items (SPLIT_FLOW) by + // attaching the result recorders that provide the values. + virtual void registerSource(cComponent *source, simsignal_t signal); + // Updates the values of the items from their result recorders, before rendering. + virtual void refreshFlowItemValues() const; + // Collects all result recorders in the result listener chain of a signal, including + // the ones created per flow by a demuxFlow() result filter. + virtual void collectResultRecorders(cResultListener *resultListener, std::vector& recorders) const; + //@} + public: #define PROCESS_SIGNAL(value) { processSignal(source, signal, [=] (cIListener *listener) { listener->receiveSignal(source, signal, value, details); }); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, bool b, cObject *details) override { PROCESS_SIGNAL(b); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t l, cObject *details) override { PROCESS_SIGNAL(l); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, uintval_t l, cObject *details) override { PROCESS_SIGNAL(l); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, double d, cObject *details) override { PROCESS_SIGNAL(d); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, const SimTime& t, cObject *details) override { PROCESS_SIGNAL(t); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, const char *s, cObject *details) override { PROCESS_SIGNAL(s); } - virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *obj, cObject *details) override { PROCESS_SIGNAL(obj); } +#define PROCESS_NUMERIC_SIGNAL(value, doubleValue) { \ + if (splitMode == SPLIT_NONE) PROCESS_SIGNAL(value) \ + else if (splitMode == SPLIT_DETAILS) processSplitValue(source, doubleValue, details); \ + else registerSource(source, signal); } +#define PROCESS_NONNUMERIC_SIGNAL(value) { \ + if (splitMode == SPLIT_NONE) PROCESS_SIGNAL(value) \ + else if (splitMode != SPLIT_DETAILS) registerSource(source, signal); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, bool b, cObject *details) override { PROCESS_NUMERIC_SIGNAL(b, b); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t l, cObject *details) override { PROCESS_NUMERIC_SIGNAL(l, l); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, uintval_t l, cObject *details) override { PROCESS_NUMERIC_SIGNAL(l, l); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, double d, cObject *details) override { PROCESS_NUMERIC_SIGNAL(d, d); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, const SimTime& t, cObject *details) override { PROCESS_NUMERIC_SIGNAL(t, t.dbl()); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, const char *s, cObject *details) override { PROCESS_NONNUMERIC_SIGNAL(s); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *obj, cObject *details) override { PROCESS_NONNUMERIC_SIGNAL(obj); } }; } // namespace visualizer diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.ned b/src/inet/visualizer/base/StatisticVisualizerBase.ned index 8a159b82c3a..1b5fe4e021c 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.ned +++ b/src/inet/visualizer/base/StatisticVisualizerBase.ned @@ -48,6 +48,19 @@ simple StatisticVisualizerBase extends VisualizerBase string placementHint = default("top"); // Annotation placement hint, space separated list of any, top, bottom, left, right, topLeft, topCenter, topRight, etc. double placementPriority = default(0); // Determines the order of annotation positioning + // Determines whether the values a signal source emits are split into several + // statistics, and what identifies each of them: + // - "none": the source emits the values of a single statistic + // - "details": one statistic per distinct details object emitted with the value, + // i.e. the live counterpart of the demux() result filter + // - "flow": one statistic per packet flow of the source, in which case + // `statisticExpression` must contain demuxFlow(); the values then come from the + // result recorders it builds, so an item can display e.g. a count or a throughput + // The values a source is split into are displayed together, as the items of the one + // figure of that source, which must be a figure that displays several items. + string splitBy @enum("none","details","flow") = default("none"); + + @class(StatisticVisualizerBase); } diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc index 838077ebf7a..c16db5207f6 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc @@ -8,6 +8,7 @@ #include "inet/visualizer/canvas/common/StatisticCanvasVisualizer.h" #include "inet/common/ModuleAccess.h" +#include "inet/common/figures/BarChartFigure.h" #include "inet/common/figures/BoxedLabelFigure.h" namespace inet { @@ -33,9 +34,10 @@ static std::string getFigureAttributeValue(const char *key, const cValue& value) } } -StatisticCanvasVisualizer::StatisticCanvasVisualization::StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, int moduleId, simsignal_t signal, const char *unit) : +StatisticCanvasVisualizer::StatisticCanvasVisualization::StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, int networkNodeId, cFigure *figure, int moduleId, simsignal_t signal, const char *unit) : StatisticVisualization(moduleId, signal, unit), networkNodeVisualization(networkNodeVisualization), + networkNodeId(networkNodeId), figure(figure) { } @@ -116,6 +118,8 @@ cFigure *StatisticCanvasVisualizer::createFigure(cProperty *property) const auto figure = dynamic_cast(createOneIfClassIsKnown(className.c_str())); if (figure == nullptr) figure = getCanvas()->createFigure(type); + // parse() validates the attribute keys against the figure's own getAllowedPropertyKeys(), + // so a misspelled one is an error here just as it is in an @figure property figure->parse(property); return figure; } @@ -129,10 +133,17 @@ void StatisticCanvasVisualizer::setAnnotationSize(NetworkNodeCanvasVisualization } } -StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::createStatisticVisualization(cComponent *source, simsignal_t signal) +StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::createStatisticVisualization(cComponent *module, simsignal_t signal) { + auto networkNode = getContainingNode(check_and_cast(module)); + // find, not get: get throws when the node has no visualization, which is a legitimate + // configuration whenever the scene visualizer's nodeFilter is narrower than sourceFilter + auto networkNodeVisualization = networkNodeVisualizer->findNetworkNodeVisualization(networkNode); + if (networkNodeVisualization == nullptr) + return nullptr; // the network node is not visualized cFigure *figure = createIndicatorFigure(); - if (figure == nullptr) { + if (figure == nullptr && splitMode == SPLIT_NONE) { + // a single value is displayed with a text label by default auto boxedLabelFigure = new BoxedLabelFigure("statistic"); boxedLabelFigure->setFont(font); boxedLabelFigure->setText(""); @@ -143,57 +154,131 @@ StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::crea figure->setTooltip("This label represents the current value of a statistic"); } else { + if (figure == nullptr) { + // several values need a figure that displays several items, a bar chart by default + cProperty property("figure"); + property.addKey("type"); + property.setValue("type", 0, "barChart"); + figure = createFigure(&property); + } + if (dynamic_cast(figure) == nullptr) { + std::string className = figure->getClassName(); + delete figure; + throw cRuntimeError("Cannot display statistic values with a figure of class %s, because it is not an indicator figure", className.c_str()); + } figure->setName("statistic"); - std::string tooltip = std::string("This figure represents the value of ") + statisticName + " in " + source->getFullPath(); + std::string tooltip = std::string("This figure represents the value of ") + statisticName + " in " + module->getFullPath(); figure->setTooltip(tooltip.c_str()); } figure->setTags((std::string("statistic ") + tags).c_str()); - figure->setAssociatedObject(source); + figure->setAssociatedObject(module); figure->setZIndex(zIndex); - auto networkNode = getContainingNode(check_and_cast(source)); - auto networkNodeVisualization = networkNodeVisualizer->getNetworkNodeVisualization(networkNode); - return new StatisticCanvasVisualization(networkNodeVisualization, figure, source->getId(), signal, getUnit(source)); + return new StatisticCanvasVisualization(networkNodeVisualization, networkNode->getId(), figure, module->getId(), signal, getUnit(module)); } -void StatisticCanvasVisualizer::addStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticCanvasVisualizer::addStatisticVisualization(StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::addStatisticVisualization(statisticVisualization); - auto statisticCanvasVisualization = static_cast(statisticVisualization); + auto statisticCanvasVisualization = static_cast(statisticVisualization); auto figure = statisticCanvasVisualization->figure; if (auto indicatorFigure = dynamic_cast(figure)) { auto size = indicatorFigure->getSize(); - statisticCanvasVisualization->networkNodeVisualization->addAnnotation(statisticCanvasVisualization->figure, cFigure::Rectangle(0.0, 0.0, size.x, size.y), placementHint, placementPriority); + statisticCanvasVisualization->annotationSize = size; + statisticCanvasVisualization->networkNodeVisualization->addAnnotation(figure, cFigure::Rectangle(0.0, 0.0, size.x, size.y), placementHint, placementPriority); + } + else { + auto boxedLabelFigure = check_and_cast(figure); + statisticCanvasVisualization->networkNodeVisualization->addAnnotation(figure, boxedLabelFigure->getBounds(), placementHint, placementPriority); } - else if (auto boxedLabelFigure = check_and_cast(figure)) - statisticCanvasVisualization->networkNodeVisualization->addAnnotation(statisticCanvasVisualization->figure, boxedLabelFigure->getBounds(), placementHint, placementPriority); } -void StatisticCanvasVisualizer::removeStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticCanvasVisualizer::removeStatisticVisualization(StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::removeStatisticVisualization(statisticVisualization); - auto statisticCanvasVisualization = static_cast(statisticVisualization); - if (networkNodeVisualizer != nullptr) - statisticCanvasVisualization->networkNodeVisualization->removeAnnotation(statisticCanvasVisualization->figure); + auto statisticCanvasVisualization = static_cast(statisticVisualization); + // the cached network node visualization may already be gone: it is a figure group, and it + // is destroyed with its network node, taking the statistic figure with it. Look it up + // instead of trusting the pointer, and when it is gone leave the figure to it. + if (networkNodeVisualizer != nullptr) { + auto networkNode = getSimulation()->getModule(statisticCanvasVisualization->networkNodeId); + auto networkNodeVisualization = networkNode != nullptr ? networkNodeVisualizer->findNetworkNodeVisualization(networkNode) : nullptr; + if (networkNodeVisualization != nullptr) + networkNodeVisualization->removeAnnotation(statisticCanvasVisualization->figure); + else + statisticCanvasVisualization->figure = nullptr; + } } -void StatisticCanvasVisualizer::refreshStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticCanvasVisualizer::refreshStatisticVisualization(StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::refreshStatisticVisualization(statisticVisualization); - auto statisticCanvasVisualization = static_cast(const_cast(statisticVisualization)); + refreshFigure(static_cast(statisticVisualization)); +} + +void StatisticCanvasVisualizer::refreshDisplay() const +{ + VisualizerBase::refreshDisplay(); + // the visualization of a deleted module must go before anything reads its figure; the + // removal changes state, which is why refreshDisplay() being const is stepped around here + const_cast(this)->removeVisualizationsOfDeletedModules(); + if (splitMode == SPLIT_NONE) + return; // a single value is refreshed when its signal is received + if (splitMode == SPLIT_FLOW) + refreshFlowItemValues(); + for (auto& it : statisticVisualizations) + refreshFigure(static_cast(it.second)); +} + +void StatisticCanvasVisualizer::refreshFigure(StatisticCanvasVisualization *statisticCanvasVisualization) const +{ auto figure = statisticCanvasVisualization->figure; + auto& items = statisticCanvasVisualization->items; if (auto indicatorFigure = dynamic_cast(figure)) { - // the value in the display unit, the same value the text label would display - indicatorFigure->setValue(0, simTime(), statisticVisualization->printValue); - // the size of a figure may depend on its value, e.g. that of a counter + if ((int)items.size() != indicatorFigure->getNumItems()) + setFigureItems(figure, items); + int index = 0; + for (auto& item : items) + indicatorFigure->setValue(index++, simTime(), item.second.value); + indicatorFigure->refreshDisplay(); + // the size of a figure may depend on its value, e.g. that of a counter, and on the + // number of its items, as in a bar chart that gained a bar setAnnotationSize(statisticCanvasVisualization->networkNodeVisualization, figure, indicatorFigure->getSize(), statisticCanvasVisualization->annotationSize); } else { auto boxedLabelFigure = check_and_cast(figure); - boxedLabelFigure->setText(getText(statisticVisualization).c_str()); + boxedLabelFigure->setText(getText(statisticCanvasVisualization).c_str()); statisticCanvasVisualization->networkNodeVisualization->setAnnotationSize(figure, boxedLabelFigure->getBounds().getSize()); } } +void StatisticCanvasVisualizer::setFigureItems(cFigure *figure, const std::map& items) const +{ + // only a bar chart can be given a changing set of labelled items so far; another + // indicator figure displays the single item of an unsplit, ungrouped statistic + auto barChartFigure = dynamic_cast(figure); + if (barChartFigure == nullptr) { + if (items.size() > 1) + throw cRuntimeError("Cannot display %d values with a figure of class %s, because only a bar chart displays a changing set of labelled items", (int)items.size(), figure->getClassName()); + return; + } + barChartFigure->setNumItems(items.size()); + int index = 0; + for (auto& item : items) + barChartFigure->setItemLabel(index++, item.first.c_str()); +} + +void StatisticCanvasVisualizer::removeVisualizationsOfDeletedModules() +{ + std::vector removedStatisticVisualizations; + for (auto& it : statisticVisualizations) + if (getSimulation()->getModule(it.second->moduleId) == nullptr) + removedStatisticVisualizations.push_back(it.second); + for (auto statisticVisualization : removedStatisticVisualizations) { + removeStatisticVisualization(statisticVisualization); + delete statisticVisualization; + } +} + } // namespace visualizer } // namespace inet diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h index aa6268307db..5eaa965450f 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h @@ -24,11 +24,12 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase class INET_API StatisticCanvasVisualization : public StatisticVisualization { public: NetworkNodeCanvasVisualization *networkNodeVisualization = nullptr; + const int networkNodeId = -1; // the network node the figure is displayed above cFigure *figure = nullptr; cFigure::Point annotationSize = cFigure::Point(NaN, NaN); public: - StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, int moduleId, simsignal_t signal, const char *unit); + StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, int networkNodeId, cFigure *figure, int moduleId, simsignal_t signal, const char *unit); virtual ~StatisticCanvasVisualization(); }; @@ -38,8 +39,9 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase protected: virtual void initialize(int stage) override; + virtual void refreshDisplay() const override; - // Creates the figure that displays the value as configured by the `figure` parameter, + // Creates the figure that displays the value(s) as configured by the `figure` parameter, // or by the figure template property named by the `propertyName` parameter; returns // nullptr if neither is given. virtual cFigure *createIndicatorFigure(); @@ -52,10 +54,21 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase // Updates the size the network node visualizer reserves for a figure, if it changed. virtual void setAnnotationSize(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, const cFigure::Point& size, cFigure::Point& lastSize) const; - virtual StatisticVisualization *createStatisticVisualization(cComponent *source, simsignal_t signal) override; - virtual void addStatisticVisualization(const StatisticVisualization *statisticVisualization) override; - virtual void removeStatisticVisualization(const StatisticVisualization *statisticVisualization) override; - virtual void refreshStatisticVisualization(const StatisticVisualization *statisticVisualization) override; + virtual StatisticVisualization *createStatisticVisualization(cComponent *module, simsignal_t signal) override; + virtual void addStatisticVisualization(StatisticVisualization *statisticVisualization) override; + virtual void removeStatisticVisualization(StatisticVisualization *statisticVisualization) override; + virtual void refreshStatisticVisualization(StatisticVisualization *statisticVisualization) override; + + // Displays the items of a visualization on its figure: their last values, preceded by + // their number and labels whenever the set of items changed. + virtual void refreshFigure(StatisticCanvasVisualization *statisticCanvasVisualization) const; + // Gives the figure the number of items and their labels. The visualizer owns the label + // to index mapping: the items are displayed in label order, so the index of an item is + // its position among them. + virtual void setFigureItems(cFigure *figure, const std::map& items) const; + // Drops the visualizations whose module has been deleted. When the network node itself + // is gone its visualization took the figure with it, so neither may be touched again. + virtual void removeVisualizationsOfDeletedModules(); }; } // namespace visualizer diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned index 0180a4af829..cd0c5b34a30 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned @@ -20,7 +20,10 @@ import inet.visualizer.contract.IStatisticVisualizer; // The statistic value is displayed with a label by default. Alternatively, any // indicator figure can display it, either configured in the `figure` parameter, // or by providing a figure template along the module path of the statistic -// visualizer and referring to it with the property parameters. +// visualizer and referring to it with the property parameters. Displaying +// several values at once, as configured by the `splitBy` and `groupBy` parameters +// of the base module, needs a bar chart: it is the only figure so far that displays +// a changing set of labelled items. // // @see ~StatisticOsgVisualizer, ~StatisticVisualizer, ~StatisticVisualizerBase, ~IStatisticVisualizer // @@ -29,11 +32,12 @@ simple StatisticCanvasVisualizer extends StatisticVisualizerBase like IStatistic parameters: double zIndex = default(10); // Determines the drawing order of figures relative to other visualizers - // The figure that displays the value, given as the attributes of a figure the + // The figure that displays the value(s), given as the attributes of a figure the // same way as in an @figure property, e.g. {type: "gauge", minValue: 0, maxValue: // 100, tickSize: 20}. The type selects the figure class registered with // Register_Figure(), the other attributes are the parameters of that particular - // figure. Empty means a text label. + // figure. Empty means a text label, or a bar chart ({type: "barChart"}) if + // `groupBy` is not "none". object figure = default({}); string propertyName = default(""); // Optional property name of a figure template along the module path of the visualizer, an alternative to the figure parameter, taking precedence over it diff --git a/src/inet/visualizer/osg/common/StatisticOsgVisualizer.cc b/src/inet/visualizer/osg/common/StatisticOsgVisualizer.cc index 4fc47bb14a6..af121819ca1 100644 --- a/src/inet/visualizer/osg/common/StatisticOsgVisualizer.cc +++ b/src/inet/visualizer/osg/common/StatisticOsgVisualizer.cc @@ -50,14 +50,14 @@ StatisticVisualizerBase::StatisticVisualization *StatisticOsgVisualizer::createS return new StatisticOsgVisualization(networkNodeVisualization, geode, source->getId(), signal, getUnit(source)); } -void StatisticOsgVisualizer::addStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticOsgVisualizer::addStatisticVisualization(StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::addStatisticVisualization(statisticVisualization); auto statisticOsgVisualization = static_cast(statisticVisualization); statisticOsgVisualization->networkNodeVisualization->addAnnotation(statisticOsgVisualization->node, osg::Vec3d(100, 18, 0), 1.0); } -void StatisticOsgVisualizer::removeStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticOsgVisualizer::removeStatisticVisualization(StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::removeStatisticVisualization(statisticVisualization); auto statisticOsgVisualization = static_cast(statisticVisualization); @@ -65,7 +65,7 @@ void StatisticOsgVisualizer::removeStatisticVisualization(const StatisticVisuali statisticOsgVisualization->networkNodeVisualization->removeAnnotation(statisticOsgVisualization->node); } -void StatisticOsgVisualizer::refreshStatisticVisualization(const StatisticVisualization *statisticVisualization) +void StatisticOsgVisualizer::refreshStatisticVisualization(StatisticVisualization *statisticVisualization) { StatisticVisualizerBase::refreshStatisticVisualization(statisticVisualization); auto statisticOsgVisualization = static_cast(statisticVisualization); diff --git a/src/inet/visualizer/osg/common/StatisticOsgVisualizer.h b/src/inet/visualizer/osg/common/StatisticOsgVisualizer.h index 2194f867404..84585facac1 100644 --- a/src/inet/visualizer/osg/common/StatisticOsgVisualizer.h +++ b/src/inet/visualizer/osg/common/StatisticOsgVisualizer.h @@ -38,9 +38,9 @@ class INET_API StatisticOsgVisualizer : public StatisticVisualizerBase virtual void initialize(int stage) override; virtual StatisticVisualization *createStatisticVisualization(cComponent *source, simsignal_t signal) override; - virtual void addStatisticVisualization(const StatisticVisualization *statisticVisualization) override; - virtual void removeStatisticVisualization(const StatisticVisualization *statisticVisualization) override; - virtual void refreshStatisticVisualization(const StatisticVisualization *statisticVisualization) override; + virtual void addStatisticVisualization(StatisticVisualization *statisticVisualization) override; + virtual void removeStatisticVisualization(StatisticVisualization *statisticVisualization) override; + virtual void refreshStatisticVisualization(StatisticVisualization *statisticVisualization) override; }; } // namespace visualizer From be9b6f019a1b39380c2a252549ce4e0f65e1d0b3 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:46 +0200 Subject: [PATCH 6/8] visualizer: group the statistics of a network node onto one figure Splitting puts the values of one signal source on that source's figure. Which statistics share a figure is a separate decision, and the other useful answer is per network node: a quantity that exists once per source, displayed for every matching source of a node on a single figure, e.g. the throughput of each of a host's applications. Add the groupBy parameter, which determines which statistics are displayed together as the items of a single figure: - "none": each statistic is displayed on a figure of its own - "source": the statistics of one signal source, i.e. the ones splitBy split its values into - "networkNode": the matching signal sources of one network node, one item per source. An item is labelled with the path of its source relative to the node (e.g. wlan[0].mac), which is unique by construction where the bare module name is not -- the MACs of several network interfaces all share one name, and would merge into a single item. For a source that is a direct submodule of the node the path is just its name (e.g. app[0]). The values then come from the result recorders built from statisticExpression, so an item can display a count or a throughput rather than the raw value of the signal. The error that rejects an unsupported combination names all three rules the guard enforces. This is the first mode whose items hold the result recorder of a module other than the one the visualization is keyed on, and the first whose items can go away. A recorder is deleted with its source module (cResultListener::unsubscribedFrom), and a network node visualization is a figure group that is deleted with its node, taking the statistic figure with it. So an item whose source is gone is dropped before the values are read, a visualization whose module is gone is dropped before its figure is touched, and the network node visualization is looked up rather than remembered, because the remembered pointer is exactly what goes stale. Because the set of items is no longer only grown, the figure is told to relabel on a version counter rather than on the item count, which a removal and an addition between two refreshes would leave unchanged. Four module tests cover it. The first reads the bar chart itself through a probe -- how many bars, and their labels in index order -- because the visualizer records nothing to a result file, so asserting on the traffic would pass just as well with the visualizer switched off. The second asserts that a figure which cannot display labelled items is rejected, which is also what proves the visualizer ran at all. The third deletes a signal source and the fourth deletes a whole network node, each asserting what is left on the figure afterwards. --- WHATSNEW | 20 ++++ .../base/StatisticVisualizerBase.cc | 71 +++++++++++++- .../visualizer/base/StatisticVisualizerBase.h | 46 ++++++--- .../base/StatisticVisualizerBase.ned | 20 +++- .../common/StatisticCanvasVisualizer.cc | 12 ++- .../canvas/common/StatisticCanvasVisualizer.h | 1 + tests/module/StatisticVisualizerItems_1.test | 92 ++++++++++++++++++ tests/module/StatisticVisualizerItems_2.test | 78 +++++++++++++++ tests/module/StatisticVisualizerItems_3.test | 94 ++++++++++++++++++ tests/module/StatisticVisualizerItems_4.test | 95 +++++++++++++++++++ tests/module/lib/BarChartProbe.cc | 51 ++++++++++ tests/module/lib/BarChartProbe.ned | 11 +++ tests/module/lib/Makefile | 1 + 13 files changed, 571 insertions(+), 21 deletions(-) create mode 100644 tests/module/StatisticVisualizerItems_1.test create mode 100644 tests/module/StatisticVisualizerItems_2.test create mode 100644 tests/module/StatisticVisualizerItems_3.test create mode 100644 tests/module/StatisticVisualizerItems_4.test create mode 100644 tests/module/lib/BarChartProbe.cc create mode 100644 tests/module/lib/BarChartProbe.ned diff --git a/WHATSNEW b/WHATSNEW index ae21ff67d34..e3682b1c043 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -87,6 +87,26 @@ Notable backward compatible changes are the following: each, with a value to color gradient and an optional autoscaled range. Unlike the other indicator figures the number of its items is not fixed. +5. Displaying several values of a statistic at once + + ~StatisticVisualizerBase gained the splitBy and groupBy parameters, so every + statistic visualizer has them. splitBy splits the values a signal source emits + into several statistics, by the details object emitted with the value or by the + packet's flow; groupBy determines which statistics are displayed together as the + items of one figure, those of a source or those of a network node. The default is + neither, which is what the visualizer did before. + + The figure that displays a statistic can now be given in the new figure + parameter, as the attributes of a figure the way an @figure property gives them, + which a template property along the module path could not be from an ini file. + A figure type is also looked up among the types registered with + Register_Figure(), not only as an inet::Figure class. + + An indicator figure driven by the statistic visualizer is now given the value in + the display unit, the one the text label would show, rather than the raw value. + A model that configures such a figure through the propertyName template and + lists several units sees the figure follow the same unit as the label. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.cc b/src/inet/visualizer/base/StatisticVisualizerBase.cc index 8bbe2e6c6ae..acbd9f4f1dd 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.cc +++ b/src/inet/visualizer/base/StatisticVisualizerBase.cc @@ -86,6 +86,22 @@ void StatisticVisualizerBase::initialize(int stage) splitMode = SPLIT_FLOW; else throw cRuntimeError("Unknown splitBy parameter value: '%s'", splitBy); + const char *groupBy = par("groupBy"); + if (!strcmp(groupBy, "none")) + groupMode = GROUP_NONE; + else if (!strcmp(groupBy, "source")) + groupMode = GROUP_SOURCE; + else if (!strcmp(groupBy, "networkNode")) + groupMode = GROUP_NETWORK_NODE; + else + throw cRuntimeError("Unknown groupBy parameter value: '%s'", groupBy); + // splitting the values of a source produces the items of that source's own group, + // and the items of a network node's group are its sources themselves + if (!((splitMode == SPLIT_NONE && groupMode != GROUP_SOURCE) || + (splitMode != SPLIT_NONE && groupMode == GROUP_SOURCE))) + throw cRuntimeError("Cannot split the statistic by '%s' and group it by '%s': " + "splitting requires groupBy = \"source\", grouping by source requires a splitBy " + "other than \"none\", and grouping by network node requires splitBy = \"none\"", splitBy, groupBy); if (displayStatistics) { if (opp_isempty(signalName)) throw cRuntimeError("The signalName parameter must be not empty"); @@ -291,7 +307,11 @@ void StatisticVisualizerBase::processSplitValue(cComponent *source, double value auto statisticVisualization = getOrCreateStatisticVisualization(module, subscribedSignal); if (statisticVisualization == nullptr) return; - statisticVisualization->items[label].value = convertToDisplayUnit(value); + auto& item = statisticVisualization->items[label]; + if (item.sourceModuleId == -1) + statisticVisualization->itemsVersion++; + item.sourceModuleId = module->getId(); + item.value = convertToDisplayUnit(value); } void StatisticVisualizerBase::registerSource(cComponent *source, simsignal_t signal) @@ -302,16 +322,61 @@ void StatisticVisualizerBase::registerSource(cComponent *source, simsignal_t sig if (!sourceFilter.matches(module)) return; registeredSourceIds.insert(module->getId()); - auto statisticVisualization = getOrCreateStatisticVisualization(module, signal); + // when grouping by network node, the sources of one node are displayed together + auto networkNode = groupMode == GROUP_NETWORK_NODE ? getContainingNode(module) : nullptr; + auto statisticVisualization = getOrCreateStatisticVisualization(networkNode != nullptr ? networkNode : module, signal); if (statisticVisualization == nullptr) return; // the values come from the result recorders built from statisticExpression, so that an // item can display e.g. a count or a throughput rather than the raw value of the signal addResultRecorder(source, signal); + if (groupMode == GROUP_NETWORK_NODE) { + auto& item = statisticVisualization->items[getSourceItemLabel(module, networkNode)]; + item.sourceModuleId = module->getId(); + item.recorder = getResultRecorder(source, signal); + statisticVisualization->itemsVersion++; + } // when splitting by flow, statisticExpression contains demuxFlow(), so the recorder chain // creates a separate recorder per flow as the flows appear, see refreshFlowItemValues() } +std::string StatisticVisualizerBase::getSourceItemLabel(cModule *module, cModule *networkNode) const +{ + // the label must identify the item among the ones of the same network node, so it is + // the path of the signal source relative to the network node, e.g. wlan[0].mac; the name + // alone is ambiguous for several similarly named submodules, e.g. the MACs of the + // network interfaces of a node + if (module == networkNode) + return module->getFullName(); + return module->getFullPath().substr(networkNode->getFullPath().length() + 1); +} + +void StatisticVisualizerBase::removeItemsOfDeletedSources() const +{ + for (auto& it : statisticVisualizations) { + auto statisticVisualization = it.second; + for (auto item = statisticVisualization->items.begin(); item != statisticVisualization->items.end(); ) { + if (item->second.sourceModuleId != -1 && getSimulation()->getModule(item->second.sourceModuleId) == nullptr) { + registeredSourceIds.erase(item->second.sourceModuleId); + item = statisticVisualization->items.erase(item); + statisticVisualization->itemsVersion++; + } + else + ++item; + } + } +} + +void StatisticVisualizerBase::refreshSourceItemValues() const +{ + // the result recorder of a deleted source is deleted with it + // (cResultListener::unsubscribedFrom), so the item must go before it is read + removeItemsOfDeletedSources(); + for (auto& it : statisticVisualizations) + for (auto& item : it.second->items) + item.second.value = convertToDisplayUnit(item.second.recorder->getLastValue()); +} + void StatisticVisualizerBase::refreshFlowItemValues() const { for (auto& it : statisticVisualizations) { @@ -328,6 +393,8 @@ void StatisticVisualizerBase::refreshFlowItemValues() const if (opp_isempty(label)) continue; // the recorder of the undemultiplexed statistic is not an item auto& item = statisticVisualization->items[label]; + if (item.recorder == nullptr) + statisticVisualization->itemsVersion++; item.recorder = recorder; item.value = convertToDisplayUnit(recorder->getLastValue()); } diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.h b/src/inet/visualizer/base/StatisticVisualizerBase.h index ac216ca3c01..e0abf5f80cb 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.h +++ b/src/inet/visualizer/base/StatisticVisualizerBase.h @@ -34,6 +34,14 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener SPLIT_FLOW, // one statistic per packet flow of the source, demultiplexed by the flow tag }; + // Determines which statistics are displayed together as the items of a single + // figure, which then needs to be one that displays several items, e.g. a bar chart. + enum GroupMode { + GROUP_NONE, // each statistic is displayed on a figure of its own + GROUP_SOURCE, // the statistics of one signal source are displayed together + GROUP_NETWORK_NODE, // the statistics of the signal sources of one network node are displayed together + }; + class INET_API LastValueRecorder : public cNumericResultRecorder { protected: double lastValue = NaN; @@ -47,22 +55,28 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener }; // Displays the last value of a statistic, or of several statistics at once when the - // values of a source are split. Each value is one item of the figure, identified by its - // label; an unsplit statistic has a single item whose label is empty. The items are not - // known in advance, they appear as their labels do, which is the live counterpart of the - // demux() result filter used for recording. + // values of a source are split or the sources of a network node are grouped. Each value + // is one item of the figure, identified by its label; a statistic that is neither split + // nor grouped has a single item whose label is empty. The items are not known in + // advance, they appear as their labels do, which is the live counterpart of the demux() + // result filter used for recording. class INET_API StatisticVisualization { public: class INET_API Item { public: + int sourceModuleId = -1; // the module the recorder is attached to, -1 if there is none LastValueRecorder *recorder = nullptr; // provides the value of this item double value = NaN; // the last value, in the display unit }; - const int moduleId = -1; // the signal source the visualization belongs to + const int moduleId = -1; // the module the visualization belongs to: the signal source, or the network node when grouping by network node const simsignal_t signal = -1; const char *unit = nullptr; std::map items; // item label -> item; displayed in label order + // Incremented whenever a split or grouped item is added or removed, so that the figure + // can tell a changed set of items from changed values without relying on the size alone. + // The single unlabelled item of an unsplit statistic never changes, so it does not count. + int itemsVersion = 0; mutable double printValue = NaN; mutable const char *printUnit = nullptr; @@ -93,6 +107,7 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener const char *statisticUnit = nullptr; const char *statisticExpression = nullptr; SplitMode splitMode = SPLIT_NONE; + GroupMode groupMode = GROUP_NONE; StringFormat format; std::vector units; cFigure::Font font; @@ -104,7 +119,7 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener //@} std::map statisticVisualizations; // module id -> visualization - std::set registeredSourceIds; // signal sources whose result recorders are already attached + mutable std::set registeredSourceIds; // signal sources whose result recorders are already attached protected: virtual void initialize(int stage) override; @@ -135,15 +150,24 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener virtual void refreshStatisticVisualization(StatisticVisualization *statisticVisualization); virtual void processSignal(cComponent *source, simsignal_t signal, std::function receiveSignal); - /** @name Splitting the values of one signal into several items */ + /** @name Splitting one signal, and grouping several sources, into several items */ //@{ // Stores the value of a signal as the last value of the item identified by the // details object emitted with it (SPLIT_DETAILS). virtual void processSplitValue(cComponent *source, double value, cObject *details); - // Registers a signal source as the source of the per flow items (SPLIT_FLOW) by - // attaching the result recorders that provide the values. + // Registers a signal source as one item of its network node's group (GROUP_NETWORK_NODE), + // or as the source of the per flow items (SPLIT_FLOW), by attaching the result recorders + // that provide the values. virtual void registerSource(cComponent *source, simsignal_t signal); + // Returns the label identifying the item of a signal source among the ones displayed + // together in the visualization of its network node (GROUP_NETWORK_NODE). + virtual std::string getSourceItemLabel(cModule *module, cModule *networkNode) const; // Updates the values of the items from their result recorders, before rendering. + virtual void refreshSourceItemValues() const; + // Removes the items whose signal source has been deleted. Their result recorders are + // deleted with the source (cResultListener::unsubscribedFrom), so neither the recorder + // nor the item may outlive it. + virtual void removeItemsOfDeletedSources() const; virtual void refreshFlowItemValues() const; // Collects all result recorders in the result listener chain of a signal, including // the ones created per flow by a demuxFlow() result filter. @@ -153,11 +177,11 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener public: #define PROCESS_SIGNAL(value) { processSignal(source, signal, [=] (cIListener *listener) { listener->receiveSignal(source, signal, value, details); }); } #define PROCESS_NUMERIC_SIGNAL(value, doubleValue) { \ - if (splitMode == SPLIT_NONE) PROCESS_SIGNAL(value) \ + if (splitMode == SPLIT_NONE && groupMode == GROUP_NONE) PROCESS_SIGNAL(value) \ else if (splitMode == SPLIT_DETAILS) processSplitValue(source, doubleValue, details); \ else registerSource(source, signal); } #define PROCESS_NONNUMERIC_SIGNAL(value) { \ - if (splitMode == SPLIT_NONE) PROCESS_SIGNAL(value) \ + if (splitMode == SPLIT_NONE && groupMode == GROUP_NONE) PROCESS_SIGNAL(value) \ else if (splitMode != SPLIT_DETAILS) registerSource(source, signal); } virtual void receiveSignal(cComponent *source, simsignal_t signal, bool b, cObject *details) override { PROCESS_NUMERIC_SIGNAL(b, b); } virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t l, cObject *details) override { PROCESS_NUMERIC_SIGNAL(l, l); } diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.ned b/src/inet/visualizer/base/StatisticVisualizerBase.ned index 1b5fe4e021c..f77bcd9bf90 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.ned +++ b/src/inet/visualizer/base/StatisticVisualizerBase.ned @@ -54,12 +54,24 @@ simple StatisticVisualizerBase extends VisualizerBase // - "details": one statistic per distinct details object emitted with the value, // i.e. the live counterpart of the demux() result filter // - "flow": one statistic per packet flow of the source, in which case - // `statisticExpression` must contain demuxFlow(); the values then come from the - // result recorders it builds, so an item can display e.g. a count or a throughput - // The values a source is split into are displayed together, as the items of the one - // figure of that source, which must be a figure that displays several items. + // `statisticExpression` must contain demuxFlow() + // Splitting requires `groupBy` = "source", because the statistics of one source are + // what is displayed together. string splitBy @enum("none","details","flow") = default("none"); + // Determines which statistics are displayed together as the items of a single + // figure, which then needs to be one that displays several items, such as a bar + // chart: + // - "none": each statistic is displayed on a figure of its own + // - "source": the statistics of one signal source are displayed together, i.e. the + // ones `splitBy` split its values into; requires a `splitBy` other than "none", + // because without splitting a source has a single statistic + // - "networkNode": the matching signal sources of one network node are displayed + // together, one item per source; requires `splitBy` = "none" + // With `groupBy` = "networkNode" and with `splitBy` = "flow" the values come from the + // result recorders built from `statisticExpression`, so an item can display e.g. a + // count or a throughput rather than the value of the signal. + string groupBy @enum("none","source","networkNode") = default("none"); @class(StatisticVisualizerBase); } diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc index c16db5207f6..161a63a52a8 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc @@ -142,7 +142,7 @@ StatisticVisualizerBase::StatisticVisualization *StatisticCanvasVisualizer::crea if (networkNodeVisualization == nullptr) return nullptr; // the network node is not visualized cFigure *figure = createIndicatorFigure(); - if (figure == nullptr && splitMode == SPLIT_NONE) { + if (figure == nullptr && splitMode == SPLIT_NONE && groupMode == GROUP_NONE) { // a single value is displayed with a text label by default auto boxedLabelFigure = new BoxedLabelFigure("statistic"); boxedLabelFigure->setFont(font); @@ -221,9 +221,11 @@ void StatisticCanvasVisualizer::refreshDisplay() const // the visualization of a deleted module must go before anything reads its figure; the // removal changes state, which is why refreshDisplay() being const is stepped around here const_cast(this)->removeVisualizationsOfDeletedModules(); - if (splitMode == SPLIT_NONE) + if (splitMode == SPLIT_NONE && groupMode == GROUP_NONE) return; // a single value is refreshed when its signal is received - if (splitMode == SPLIT_FLOW) + if (groupMode == GROUP_NETWORK_NODE) + refreshSourceItemValues(); + else if (splitMode == SPLIT_FLOW) refreshFlowItemValues(); for (auto& it : statisticVisualizations) refreshFigure(static_cast(it.second)); @@ -234,8 +236,10 @@ void StatisticCanvasVisualizer::refreshFigure(StatisticCanvasVisualization *stat auto figure = statisticCanvasVisualization->figure; auto& items = statisticCanvasVisualization->items; if (auto indicatorFigure = dynamic_cast(figure)) { - if ((int)items.size() != indicatorFigure->getNumItems()) + if (statisticCanvasVisualization->displayedItemsVersion != statisticCanvasVisualization->itemsVersion) { setFigureItems(figure, items); + statisticCanvasVisualization->displayedItemsVersion = statisticCanvasVisualization->itemsVersion; + } int index = 0; for (auto& item : items) indicatorFigure->setValue(index++, simTime(), item.second.value); diff --git a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h index 5eaa965450f..4579af2fafb 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.h @@ -27,6 +27,7 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase const int networkNodeId = -1; // the network node the figure is displayed above cFigure *figure = nullptr; cFigure::Point annotationSize = cFigure::Point(NaN, NaN); + int displayedItemsVersion = -1; // the version of the item set the figure was last given public: StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, int networkNodeId, cFigure *figure, int moduleId, simsignal_t signal, const char *unit); diff --git a/tests/module/StatisticVisualizerItems_1.test b/tests/module/StatisticVisualizerItems_1.test new file mode 100644 index 00000000000..4706d324536 --- /dev/null +++ b/tests/module/StatisticVisualizerItems_1.test @@ -0,0 +1,92 @@ +%description: +Tests that the statistic visualizer displays several statistics at once, as the items +of a single bar chart figure. + +Three UDP streams flow from the server to three sink applications on the receiver. +The visualizer groups the matching signal sources of the receiver node (groupBy = +"networkNode"), so the packet count of each application becomes one item of a single +bar chart above the node. + +The visualizer only does anything when the simulation has a GUI, so the test runs +Cmdenv's fake GUI, which is also what makes refreshDisplay() run. That exercises the +whole path: a visualization gaining an item per signal source, the visualizer +resolving each item label to an index, and BarChartFigure sizing, labelling, scaling +and laying out its bars while the values change. + +The assertion reads the bar chart itself, through BarChartProbe: the number of bars +and their labels in index order. A result file cannot show any of this -- the +visualizer's recorder records nothing -- so asserting on the received packet counts +would pass just as well with the visualizer switched off. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.StandardHost; +import inet.test.moduletest.lib.BarChartProbe; +import inet.visualizer.canvas.integrated.IntegratedCanvasVisualizer; + +network StatisticVisualizerItemsTest +{ + submodules: + probe: BarChartProbe; + visualizer: IntegratedCanvasVisualizer; + configurator: Ipv4NetworkConfigurator; + server: StandardHost; + switch_: EthernetSwitch; + receiver: StandardHost; + connections: + server.ethg++ <--> Eth100M <--> switch_.ethg++; + switch_.ethg++ <--> Eth100M <--> receiver.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +network = StatisticVisualizerItemsTest +ned-path = .;../../../../src +sim-time-limit = 3s +cmdenv-express-mode = true +**.vector-recording = false + +# the visualizer is inactive without a GUI; the fake GUI also drives refreshDisplay() +cmdenv-fake-gui = true +cmdenv-fake-gui-before-event-probability = 0.1 +cmdenv-fake-gui-after-event-probability = 0.1 + +*.server.numApps = 3 +*.server.app[*].typename = "UdpBasicApp" +*.server.app[*].destAddresses = "receiver" +*.server.app[*].messageLength = 1000B +*.server.app[0].destPort = 1000 +*.server.app[1].destPort = 1001 +*.server.app[2].destPort = 1002 +*.server.app[0].sendInterval = 10ms +*.server.app[1].sendInterval = 25ms +*.server.app[2].sendInterval = 60ms + +*.receiver.numApps = 3 +*.receiver.app[*].typename = "UdpSink" +*.receiver.app[0].localPort = 1000 +*.receiver.app[1].localPort = 1001 +*.receiver.app[2].localPort = 1002 + +*.visualizer.statisticVisualizer.displayStatistics = true +*.visualizer.statisticVisualizer.groupBy = "networkNode" +*.visualizer.statisticVisualizer.signalName = "packetReceived" +*.visualizer.statisticVisualizer.sourceFilter = "*.receiver.app[*]" +*.visualizer.statisticVisualizer.statisticExpression = "count" +*.visualizer.statisticVisualizer.figure = {type: "barChart", valueFormat: "%.0f", title: "packets"} + +%#-------------------------------------------------------------------------------------------------------------- +%contains: test.out +barChart items=3 +barChart item 0 label=app[0] +barChart item 1 label=app[1] +barChart item 2 label=app[2] +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: diff --git a/tests/module/StatisticVisualizerItems_2.test b/tests/module/StatisticVisualizerItems_2.test new file mode 100644 index 00000000000..546f1bf08b0 --- /dev/null +++ b/tests/module/StatisticVisualizerItems_2.test @@ -0,0 +1,78 @@ +%description: +Tests that grouping several statistics onto a figure that can only display one value +is rejected, rather than silently showing one of them. + +The scenario is the one of StatisticVisualizerItems_1 -- three sink applications +whose packet counts are grouped onto one figure of the receiver node -- but the +configured figure is a gauge, which has a single unlabelled item. The visualizer must +stop with an error as soon as the second item appears. + +This also proves that the visualizer really runs under Cmdenv's fake GUI: without the +GUI the whole visualizer is inactive, and this run would pass silently. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.StandardHost; +import inet.visualizer.canvas.integrated.IntegratedCanvasVisualizer; + +network StatisticVisualizerItemsTest +{ + submodules: + visualizer: IntegratedCanvasVisualizer; + configurator: Ipv4NetworkConfigurator; + server: StandardHost; + switch_: EthernetSwitch; + receiver: StandardHost; + connections: + server.ethg++ <--> Eth100M <--> switch_.ethg++; + switch_.ethg++ <--> Eth100M <--> receiver.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +network = StatisticVisualizerItemsTest +ned-path = .;../../../../src +sim-time-limit = 3s +cmdenv-express-mode = true +**.vector-recording = false + +# the visualizer is inactive without a GUI; the fake GUI also drives refreshDisplay() +cmdenv-fake-gui = true +cmdenv-fake-gui-before-event-probability = 0.1 +cmdenv-fake-gui-after-event-probability = 0.1 + +*.server.numApps = 3 +*.server.app[*].typename = "UdpBasicApp" +*.server.app[*].destAddresses = "receiver" +*.server.app[*].messageLength = 1000B +*.server.app[0].destPort = 1000 +*.server.app[1].destPort = 1001 +*.server.app[2].destPort = 1002 +*.server.app[0].sendInterval = 10ms +*.server.app[1].sendInterval = 25ms +*.server.app[2].sendInterval = 60ms + +*.receiver.numApps = 3 +*.receiver.app[*].typename = "UdpSink" +*.receiver.app[0].localPort = 1000 +*.receiver.app[1].localPort = 1001 +*.receiver.app[2].localPort = 1002 + +*.visualizer.statisticVisualizer.displayStatistics = true +*.visualizer.statisticVisualizer.groupBy = "networkNode" +*.visualizer.statisticVisualizer.signalName = "packetReceived" +*.visualizer.statisticVisualizer.sourceFilter = "*.receiver.app[*]" +*.visualizer.statisticVisualizer.statisticExpression = "count" +*.visualizer.statisticVisualizer.figure = {type: "gauge", minValue: 0, maxValue: 1000} + +%#-------------------------------------------------------------------------------------------------------------- +%exitcode: 1 +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "" test.* > test_err.out || true +%contains: test_err.out +because only a bar chart displays a changing set of labelled items diff --git a/tests/module/StatisticVisualizerItems_3.test b/tests/module/StatisticVisualizerItems_3.test new file mode 100644 index 00000000000..ace766e09fb --- /dev/null +++ b/tests/module/StatisticVisualizerItems_3.test @@ -0,0 +1,94 @@ +%description: +Tests that an item whose signal source is deleted disappears from the figure, +instead of the visualizer reading the result recorder that was deleted with it. + +The scenario is the one of StatisticVisualizerItems_1 -- three sending applications +whose packet counts are the three items of one bar chart above the server -- but a +scenario manager deletes the middle application while the simulation runs. The +recorder an item reads belongs to its own source module and is deleted with it +(cResultListener::unsubscribedFrom), so the item has to go before the next refresh +reads it. The remaining two bars must keep their own labels rather than shifting. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.StandardHost; +import inet.common.scenario.ScenarioManager; +import inet.test.moduletest.lib.BarChartProbe; +import inet.visualizer.canvas.integrated.IntegratedCanvasVisualizer; + +network StatisticVisualizerItemsDeletionTest +{ + submodules: + scenarioManager: ScenarioManager; + probe: BarChartProbe; + visualizer: IntegratedCanvasVisualizer; + configurator: Ipv4NetworkConfigurator; + server: StandardHost; + switch_: EthernetSwitch; + receiver: StandardHost; + connections: + server.ethg++ <--> Eth100M <--> switch_.ethg++; + switch_.ethg++ <--> Eth100M <--> receiver.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%file: scenario.xml + + + + + + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +network = StatisticVisualizerItemsDeletionTest +ned-path = .;../../../../src +sim-time-limit = 3s +cmdenv-express-mode = true +**.vector-recording = false + +# the visualizer is inactive without a GUI; the fake GUI also drives refreshDisplay() +cmdenv-fake-gui = true +cmdenv-fake-gui-before-event-probability = 0.1 +cmdenv-fake-gui-after-event-probability = 0.1 + +*.server.numApps = 3 +*.server.app[*].typename = "UdpBasicApp" +*.server.app[*].destAddresses = "receiver" +*.server.app[*].messageLength = 1000B +*.server.app[0].destPort = 1000 +*.server.app[1].destPort = 1001 +*.server.app[2].destPort = 1002 +*.server.app[0].sendInterval = 10ms +*.server.app[1].sendInterval = 25ms +*.server.app[2].sendInterval = 60ms + +*.receiver.numApps = 3 +*.receiver.app[*].typename = "UdpSink" +*.receiver.app[0].localPort = 1000 +*.receiver.app[1].localPort = 1001 +*.receiver.app[2].localPort = 1002 + +*.scenarioManager.script = xmldoc("scenario.xml") + +*.visualizer.statisticVisualizer.displayStatistics = true +*.visualizer.statisticVisualizer.groupBy = "networkNode" +*.visualizer.statisticVisualizer.signalName = "packetSent" +*.visualizer.statisticVisualizer.sourceFilter = "*.server.app[*]" +*.visualizer.statisticVisualizer.statisticExpression = "count" +*.visualizer.statisticVisualizer.figure = {type: "barChart", valueFormat: "%.0f", title: "packets"} + +%#-------------------------------------------------------------------------------------------------------------- +%contains: test.out +barChart items=2 +barChart item 0 label=app[0] +barChart item 1 label=app[2] +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: diff --git a/tests/module/StatisticVisualizerItems_4.test b/tests/module/StatisticVisualizerItems_4.test new file mode 100644 index 00000000000..fb449de3779 --- /dev/null +++ b/tests/module/StatisticVisualizerItems_4.test @@ -0,0 +1,95 @@ +%description: +Tests that an item whose signal source is deleted disappears from the figure, +instead of the visualizer reading the result recorder that was deleted with it. + +The scenario is the one of StatisticVisualizerItems_1 -- three sending applications +whose packet counts are the three items of one bar chart above the server -- but a +scenario manager deletes the whole server node while the simulation runs. + +Deleting a network node deletes its visualization, which is a figure group, and that +takes the statistic figure with it. The visualizer keeps its own pointer to both, and +its refresh runs on every frame, so the visualization has to be dropped -- without +touching the annotation or the figure, which no longer exist. Nothing may be left to +display, and the run must end cleanly rather than reading freed memory. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.StandardHost; +import inet.common.scenario.ScenarioManager; +import inet.test.moduletest.lib.BarChartProbe; +import inet.visualizer.canvas.integrated.IntegratedCanvasVisualizer; + +network StatisticVisualizerItemsNodeDeletionTest +{ + submodules: + scenarioManager: ScenarioManager; + probe: BarChartProbe; + visualizer: IntegratedCanvasVisualizer; + configurator: Ipv4NetworkConfigurator; + server: StandardHost; + switch_: EthernetSwitch; + receiver: StandardHost; + connections: + server.ethg++ <--> Eth100M <--> switch_.ethg++; + switch_.ethg++ <--> Eth100M <--> receiver.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%file: scenario.xml + + + + + + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +network = StatisticVisualizerItemsNodeDeletionTest +ned-path = .;../../../../src +sim-time-limit = 3s +cmdenv-express-mode = true +**.vector-recording = false + +# the visualizer is inactive without a GUI; the fake GUI also drives refreshDisplay() +cmdenv-fake-gui = true +cmdenv-fake-gui-before-event-probability = 0.1 +cmdenv-fake-gui-after-event-probability = 0.1 + +*.server.numApps = 3 +*.server.app[*].typename = "UdpBasicApp" +*.server.app[*].destAddresses = "receiver" +*.server.app[*].messageLength = 1000B +*.server.app[0].destPort = 1000 +*.server.app[1].destPort = 1001 +*.server.app[2].destPort = 1002 +*.server.app[0].sendInterval = 10ms +*.server.app[1].sendInterval = 25ms +*.server.app[2].sendInterval = 60ms + +*.receiver.numApps = 3 +*.receiver.app[*].typename = "UdpSink" +*.receiver.app[0].localPort = 1000 +*.receiver.app[1].localPort = 1001 +*.receiver.app[2].localPort = 1002 + +*.scenarioManager.script = xmldoc("scenario.xml") + +*.visualizer.statisticVisualizer.displayStatistics = true +*.visualizer.statisticVisualizer.groupBy = "networkNode" +*.visualizer.statisticVisualizer.signalName = "packetSent" +*.visualizer.statisticVisualizer.sourceFilter = "*.server.app[*]" +*.visualizer.statisticVisualizer.statisticExpression = "count" +*.visualizer.statisticVisualizer.figure = {type: "barChart", valueFormat: "%.0f", title: "packets"} + +%#-------------------------------------------------------------------------------------------------------------- +%contains: test.out +barChart: none +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: diff --git a/tests/module/lib/BarChartProbe.cc b/tests/module/lib/BarChartProbe.cc new file mode 100644 index 00000000000..12cd3f7dcfb --- /dev/null +++ b/tests/module/lib/BarChartProbe.cc @@ -0,0 +1,51 @@ +#include + +#include "inet/common/INETDefs.h" + +#ifdef INET_WITH_VISUALIZATIONCOMMON + +#include "inet/common/figures/BarChartFigure.h" + +namespace inet { + +/** + * Walks the canvas at the end of the simulation and reports the items of the first + * bar chart figure it finds: how many there are and what they are labelled. That is + * what the statistic visualizer actually produced, as opposed to the traffic that + * fed it, which a result file would show even with no visualizer at all. + */ +class BarChartProbe : public cSimpleModule +{ + protected: + virtual void finish() override; + static BarChartFigure *findBarChartFigure(cFigure *figure); +}; + +Define_Module(BarChartProbe); + +BarChartFigure *BarChartProbe::findBarChartFigure(cFigure *figure) +{ + if (auto barChartFigure = dynamic_cast(figure)) + return barChartFigure; + for (int i = 0; i < figure->getNumFigures(); i++) + if (auto found = findBarChartFigure(figure->getFigure(i))) + return found; + return nullptr; +} + +void BarChartProbe::finish() +{ + // std::cout, because a test runs Cmdenv in express mode, where EV is suppressed + auto figure = findBarChartFigure(getSimulation()->getSystemModule()->getCanvas()->getRootFigure()); + if (figure == nullptr) { + std::cout << "barChart: none" << std::endl; + return; + } + std::cout << "barChart items=" << figure->getNumItems() << std::endl; + for (int i = 0; i < figure->getNumItems(); i++) + std::cout << "barChart item " << i << " label=" << figure->getItemLabel(i) << std::endl; +} + +} // namespace inet + +#endif // INET_WITH_VISUALIZATIONCOMMON diff --git a/tests/module/lib/BarChartProbe.ned b/tests/module/lib/BarChartProbe.ned new file mode 100644 index 00000000000..1fda57ad391 --- /dev/null +++ b/tests/module/lib/BarChartProbe.ned @@ -0,0 +1,11 @@ +package inet.test.moduletest.lib; + +// +// Reports what the bar chart figure of a statistic visualizer ends up displaying, +// so that a test can assert on the visualization itself rather than on the traffic +// that happens to feed it. +// +simple BarChartProbe +{ + @class(BarChartProbe); +} diff --git a/tests/module/lib/Makefile b/tests/module/lib/Makefile index ea977aae135..adf70a2d896 100644 --- a/tests/module/lib/Makefile +++ b/tests/module/lib/Makefile @@ -29,6 +29,7 @@ O = $(PROJECT_OUTPUT_DIR)/$(CONFIGNAME)/$(PROJECTRELATIVE_PATH) # Object files for local .cc, .msg and .sm files OBJS = \ + $O/BarChartProbe.o \ $O/EthTestApp.o \ $O/IGMPTester.o \ $O/MeterTestApp.o \ From bfa9693fd3281810c773960e6a06c74a29f41615 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:46 +0200 Subject: [PATCH 7/8] examples: add an example for splitting and grouping statistics Three UDP streams from the server to the receiver, with a bar chart above the receiver showing a per stream quantity: one bar per sink app (groupBy = "networkNode", with count or throughput), or one bar per named flow demultiplexed from a single signal (splitBy = "flow"). A further config displays the throughput of one stream on a gauge instead, to show that the figure is a configuration choice rather than a display mode. --- .../statisticbars/StatisticBarsExample.ned | 51 +++++++ examples/visualizer/statisticbars/omnetpp.ini | 130 ++++++++++++++++ tests/fingerprint/examples.csv | 4 + tests/fingerprint/store.json | 144 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 examples/visualizer/statisticbars/StatisticBarsExample.ned create mode 100644 examples/visualizer/statisticbars/omnetpp.ini diff --git a/examples/visualizer/statisticbars/StatisticBarsExample.ned b/examples/visualizer/statisticbars/StatisticBarsExample.ned new file mode 100644 index 00000000000..25ba200d644 --- /dev/null +++ b/examples/visualizer/statisticbars/StatisticBarsExample.ned @@ -0,0 +1,51 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.examples.visualizer.statisticbars; + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.StandardHost; +import inet.visualizer.contract.IIntegratedVisualizer; + +// +// Demonstrates displaying several values of a statistic at once with the +// ~StatisticVisualizer, with several UDP traffic streams flowing from the server +// to the receiver. A bar chart above the receiver shows a per-stream quantity; +// the ini file selects what, and what displays it: +// +// - `Packets` - number of packets received per stream (one bar per sink app, +// `groupBy = "networkNode"`, `statisticExpression = "count"`). +// - `Throughput` - throughput per stream (same, `statisticExpression = "throughput"`). +// - `Flow` - throughput per named flow, demultiplexed from a single signal +// by the packet's flow tag (`splitBy = "flow"`). +// - `Gauge` - throughput of a single stream on a gauge instead of a bar +// chart, to show that the figure is a configuration choice. +// +network StatisticBarsExample +{ + submodules: + visualizer: like IIntegratedVisualizer { + @display("p=100,80;is=s"); + } + configurator: Ipv4NetworkConfigurator { + @display("p=100,160;is=s"); + } + server: StandardHost { + @display("p=200,350"); + } + switch: EthernetSwitch { + @display("p=450,350"); + } + receiver: StandardHost { + @display("p=700,350"); + } + connections: + server.ethg++ <--> Eth100M <--> switch.ethg++; + switch.ethg++ <--> Eth100M <--> receiver.ethg++; +} diff --git a/examples/visualizer/statisticbars/omnetpp.ini b/examples/visualizer/statisticbars/omnetpp.ini new file mode 100644 index 00000000000..3b2aebb70db --- /dev/null +++ b/examples/visualizer/statisticbars/omnetpp.ini @@ -0,0 +1,130 @@ +[General] +network = StatisticBarsExample +sim-time-limit = 10s +*.visualizer.statisticVisualizer.displayStatistics = true +# The Packets/Throughput/Flow configs give the bar chart figure a fixed maxValue; +# the matching *Autoscale configs leave it unset, so each bar chart scales to its +# own current maximum instead. + +# ========================================================================== +# Grouping by network node: three UDP streams (fast/medium/slow) from the server +# to three separate sink apps on the receiver. groupBy="networkNode" displays each +# matching source module as one item, all in a single bar chart above the receiver; +# the value is the statisticExpression (count or throughput) of that module's +# packetReceived signal. +# ========================================================================== +[Config Streams] +abstract = true +*.server.numApps = 3 +*.server.app[*].typename = "UdpBasicApp" +*.server.app[*].destAddresses = "receiver" +*.server.app[*].messageLength = 1000B +*.server.app[0].destPort = 1000 +*.server.app[1].destPort = 1001 +*.server.app[2].destPort = 1002 +*.server.app[0].sendInterval = 10ms # fast stream +*.server.app[1].sendInterval = 25ms # medium stream +*.server.app[2].sendInterval = 60ms # slow stream +*.server.app[*].startTime = uniform(0s, 10ms) + +*.receiver.numApps = 3 +*.receiver.app[*].typename = "UdpSink" +*.receiver.app[0].localPort = 1000 +*.receiver.app[1].localPort = 1001 +*.receiver.app[2].localPort = 1002 + +*.visualizer.statisticVisualizer.groupBy = "networkNode" +*.visualizer.statisticVisualizer.signalName = "packetReceived" +*.visualizer.statisticVisualizer.sourceFilter = "*.receiver.app[*]" + +[Config Packets] +extends = Streams +description = "Number of packets received per stream" +*.visualizer.statisticVisualizer.statisticExpression = "count" +*.visualizer.statisticVisualizer.figure = {type: "barChart", maxValue: 1000, valueFormat: "%.0f", barColor: "skyblue", title: "packets received"} + +[Config Throughput] +extends = Streams +description = "Throughput per stream" +*.visualizer.statisticVisualizer.statisticExpression = "throughput" +*.visualizer.statisticVisualizer.statisticUnit = "bps" +*.visualizer.statisticVisualizer.unit = "Mbps" +*.visualizer.statisticVisualizer.figure = {type: "barChart", maxValue: 1, valueFormat: "%.2f", barColor: "skyblue", title: "throughput [Mbps]"} + +# ========================================================================== +# Splitting by flow: the same three streams all go to ONE sink on the receiver, +# but each is tagged with a flow name (fast/medium/slow) by a FlowMeasurementStarter. +# A single FlowMeasurementRecorder at the receiver emits packetFlowMeasured for +# all three flows; splitBy="flow" demultiplexes that one signal by the packet's +# flow tag (demuxFlow) into one throughput statistic per flow, and groupBy="source" +# displays all of them on the one bar chart of that source. +# ========================================================================== +[Config Flow] +description = "Throughput per flow (demultiplexed by the packet's flow tag)" +*.server.numApps = 3 +*.server.app[*].typename = "UdpApp" +*.server.app[*].source.packetLength = 1000B +*.server.app[0].source.productionInterval = 10ms # fast flow +*.server.app[1].source.productionInterval = 25ms # medium flow +*.server.app[2].source.productionInterval = 60ms # slow flow +*.server.app[*].io.destAddress = "receiver" +*.server.app[*].io.destPort = 5000 +*.server.app[0].measurementStarter.typename = "FlowMeasurementStarter" +*.server.app[0].measurementStarter.flowName = "fast" +*.server.app[1].measurementStarter.typename = "FlowMeasurementStarter" +*.server.app[1].measurementStarter.flowName = "medium" +*.server.app[2].measurementStarter.typename = "FlowMeasurementStarter" +*.server.app[2].measurementStarter.flowName = "slow" + +*.receiver.numApps = 1 +*.receiver.app[0].typename = "UdpApp" +*.receiver.app[0].source.typename = "" # receive only +*.receiver.app[0].io.localPort = 5000 +*.receiver.app[0].io.destAddress = "" # receive only (unused) +*.receiver.app[0].io.destPort = 5000 # receive only (unused) +*.receiver.app[0].measurementRecorder.typename = "FlowMeasurementRecorder" +*.receiver.app[0].measurementRecorder.flowName = "fast or medium or slow" + +*.visualizer.statisticVisualizer.splitBy = "flow" +*.visualizer.statisticVisualizer.groupBy = "source" +*.visualizer.statisticVisualizer.signalName = "packetFlowMeasured" +*.visualizer.statisticVisualizer.sourceFilter = "*.receiver.app[0].measurementRecorder" +*.visualizer.statisticVisualizer.statisticExpression = "throughput(packetLength(demuxFlow))" +*.visualizer.statisticVisualizer.statisticUnit = "bps" +*.visualizer.statisticVisualizer.unit = "Mbps" +*.visualizer.statisticVisualizer.figure = {type: "barChart", maxValue: 1, valueFormat: "%.2f", barColor: "gold", title: "throughput [Mbps]"} + +# ========================================================================== +# The visualizer provides the values, the figure decides how they are displayed: +# this config displays the throughput of a single stream on a gauge instead, +# without grouping several of them onto one figure. +# ========================================================================== +[Config Gauge] +extends = Streams +description = "Throughput of one stream on a gauge (any indicator figure will do)" +*.visualizer.statisticVisualizer.groupBy = "none" +*.visualizer.statisticVisualizer.sourceFilter = "*.receiver.app[0]" +*.visualizer.statisticVisualizer.statisticExpression = "throughput" +*.visualizer.statisticVisualizer.statisticUnit = "bps" +*.visualizer.statisticVisualizer.unit = "Mbps" +*.visualizer.statisticVisualizer.figure = {type: "gauge", size: [60, 60], minValue: 0, maxValue: 1, tickSize: 0.2, label: "throughput [Mbps]"} + +# ========================================================================== +# Autoscale variants: same as the configs above, but with maxValue unset, so +# each bar chart scales to its own current maximum (the tallest bar always +# fills the height) instead of a fixed reference. +# ========================================================================== +[Config PacketsAutoscale] +extends = Packets +description = "Number of packets received per stream (autoscaled)" +*.visualizer.statisticVisualizer.figure = {type: "barChart", valueFormat: "%.0f", barColor: "skyblue", title: "packets received"} + +[Config ThroughputAutoscale] +extends = Throughput +description = "Throughput per stream (autoscaled)" +*.visualizer.statisticVisualizer.figure = {type: "barChart", valueFormat: "%.2f", barColor: "skyblue", title: "throughput [Mbps]"} + +[Config FlowAutoscale] +extends = Flow +description = "Throughput per flow (autoscaled)" +*.visualizer.statisticVisualizer.figure = {type: "barChart", valueFormat: "%.2f", barColor: "gold", title: "throughput [Mbps]"} diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 283915ab865..090fdf58989 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -542,6 +542,10 @@ /examples/timing/, -f omnetpp.ini -c General -r 0, 0.1s, 164c-e87d/tplx;2b60-28b2/~tNl;3ad0-14cf/~tND;7af0-949a/tyf, PASS, Ipv4 +/examples/visualizer/statisticbars/, -f omnetpp.ini -c Packets -r 0, 10s, adc0-d762/tplx;fb6f-4f84/~tNl;bb53-de23/~tND;6e17-9a3c/tyf, PASS, EthernetMac Ipv4 +/examples/visualizer/statisticbars/, -f omnetpp.ini -c Flow -r 0, 10s, c8a5-c66d/tplx;fcf4-b191/~tNl;d443-d671/~tND;1d81-6e55/tyf, PASS, EthernetMac Ipv4 +/examples/visualizer/statisticbars/, -f omnetpp.ini -c Gauge -r 0, 10s, adc0-d762/tplx;fb6f-4f84/~tNl;bb53-de23/~tND;a7b4-3e59/tyf, PASS, EthernetMac Ipv4 + /examples/voip/, -f omnetpp.ini -c GoodChannel -r 0, 200s, ca5a-018c/tplx;8bed-195e/~tNl;7ec2-f44e/tyf, PASS, Ipv4 /examples/voip/, -f omnetpp.ini -c BadChannel -r 0, 200s, 2641-c26d/tplx;be78-aee4/~tNl;c458-d925/tyf, PASS, Ipv4 /examples/voip/, -f omnetpp.ini -c PeriodicChannel -r 0, 200s, aa4d-f551/tplx;2ba1-3f1a/~tNl;ff77-ad0f/tyf, PASS, Ipv4 diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index 77f4652f9f8..c40c42e6c06 100644 --- a/tests/fingerprint/store.json +++ b/tests/fingerprint/store.json @@ -38027,6 +38027,150 @@ "timestamp": 1681992800.7212515, "itervars": "$repetition==0" }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Packets", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "tplx", + "fingerprint": "adc0-d762", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Packets", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "~tNl", + "fingerprint": "fb6f-4f84", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Packets", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "~tND", + "fingerprint": "bb53-de23", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Packets", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "tyf", + "fingerprint": "6e17-9a3c", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Flow", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "tplx", + "fingerprint": "c8a5-c66d", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Flow", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "~tNl", + "fingerprint": "fcf4-b191", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Flow", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "~tND", + "fingerprint": "d443-d671", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Flow", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "tyf", + "fingerprint": "1d81-6e55", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Gauge", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "tplx", + "fingerprint": "adc0-d762", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Gauge", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "~tNl", + "fingerprint": "fb6f-4f84", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Gauge", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "~tND", + "fingerprint": "bb53-de23", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, + { + "working_directory": "examples/visualizer/statisticbars", + "ini_file": "omnetpp.ini", + "config": "Gauge", + "run_number": 0, + "sim_time_limit": "10s", + "test_result": "PASS", + "ingredients": "tyf", + "fingerprint": "a7b4-3e59", + "timestamp": 1788000000.0, + "itervars": "$repetition==0" + }, { "working_directory": "showcases/general/diffserv", "ini_file": "omnetpp.ini", From 17f1b14dc51587286605805694de976afe3293b9 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 1 Sep 2026 17:41:47 +0200 Subject: [PATCH 8/8] visualizer: add the IEEE 802.11 per-station data rate visualizer Shows the data rate an access point is using towards each of its associated stations as a bar chart above the node, so that rate diversity across stations is visible while the simulation runs rather than only in the results afterwards. It is a configuration of the generic ~StatisticCanvasVisualizer rather than new visualizer code: it subscribes to the rate control's datarateChanged signal, which tags each value with the receiving station, displays one bar per receiver, and configures a bar chart figure with the scale, colors and label format that suit a data rate. Pointing signalName at the coordination function's datarateSelected instead makes it cover fixed and per-receiver configured rates too; the NED documentation gives that configuration. Being a ~StatisticCanvasVisualizer, it goes wherever one does: it is selected as the type of the integrated visualizer's statistic visualizer, and so needs no submodule of its own. --- WHATSNEW | 8 +++ .../Ieee80211RateCanvasVisualizer.ned | 72 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/inet/visualizer/canvas/linklayer/Ieee80211RateCanvasVisualizer.ned diff --git a/WHATSNEW b/WHATSNEW index e3682b1c043..fff54e9bf32 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -107,6 +107,14 @@ Notable backward compatible changes are the following: A model that configures such a figure through the propertyName template and lists several units sees the figure follow the same unit as the label. +6. IEEE 802.11 per-peer data rate visualizer + + The new ~Ieee80211RateCanvasVisualizer displays the data rate a node is using + towards each of its peers as a bar chart above the node. It is a configuration + of ~StatisticCanvasVisualizer rather than new visualizer code, and is selected + as the type of the statistic visualizer of the integrated visualizer. + + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/visualizer/canvas/linklayer/Ieee80211RateCanvasVisualizer.ned b/src/inet/visualizer/canvas/linklayer/Ieee80211RateCanvasVisualizer.ned new file mode 100644 index 00000000000..8c5f0632b59 --- /dev/null +++ b/src/inet/visualizer/canvas/linklayer/Ieee80211RateCanvasVisualizer.ned @@ -0,0 +1,72 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.visualizer.canvas.linklayer; + +import inet.visualizer.canvas.common.StatisticCanvasVisualizer; + +// +// Displays a small per-peer data rate bar chart above each IEEE 802.11 node that +// runs an adaptive rate control (~AarfRateControl, ~OnoeRateControl). Nothing in +// it is specific to an access point; an access point is simply the node that +// usually has several peers to show. It is a configuration of the generic +// ~StatisticCanvasVisualizer rather than new visualizer code: it subscribes to +// the rate control's `datarateChanged` signal, which tags each value with the +// receiver as a named details object, displays one bar per receiver, and +// configures a bar chart figure with the scale, colors and label format that +// suit a data rate. +// +// Being a ~StatisticCanvasVisualizer, it goes wherever one does: select it as +// the type of the statistic visualizer of the integrated visualizer. +// +//
+// *.visualizer.*.statisticVisualizer.typename = "Ieee80211RateCanvasVisualizer"
+// *.visualizer.*.statisticVisualizer.displayRates = true
+// 
+// +// In its default configuration it is driven by the rate control's signal, so it +// only shows rates where an adaptive rate control is configured. To visualize +// interface-wide fixed rates or per-receiver configured rates +// (`dataFrameBitratePerReceiver`) instead, point it at the coordination +// function's `datarateSelected` signal, which reports the rate actually selected +// for every transmitted frame and is likewise tagged per receiver: +// +//
+// *.visualizer.*.statisticVisualizer.signalName = "datarateSelected"
+// *.visualizer.*.statisticVisualizer.sourceFilter = "*.accessPoint.wlan[*].mac.dcf"
+// 
+// +// In that mode a station's bar appears when the access point first sends it a +// data frame, rather than as soon as its rate is configured. +// +// @see ~StatisticCanvasVisualizer, ~StatisticVisualizerBase, ~AarfRateControl +// +simple Ieee80211RateCanvasVisualizer extends StatisticCanvasVisualizer +{ + parameters: + bool displayRates = default(false); // Display the per-peer data rate bar chart above the matching nodes + displayStatistics = default(this.displayRates); + + string nodeFilter = default("*"); // Which nodes' rate controls are shown, as one path component under the network; set `sourceFilter` instead to match nodes deeper in the hierarchy + sourceFilter = default("*." + this.nodeFilter + ".**.rateControl"); // Match the rate control modules under the selected nodes + + double maxRate @unit(bps) = default(54Mbps); // Data rate mapped to a full-height bar and to the fast end of the color gradient + + // Fixed rate visualizer configuration of the generic statistic visualizer: + signalName = default("datarateChanged"); + splitBy = default("details"); // One statistic per receiver, as tagged on the signal + groupBy = default("source"); // All receivers of one network interface on one bar chart + statisticUnit = default("bps"); + unit = default("Mbps"); + figure = default({type: "barChart", + maxValue: this.maxRate / 1Mbps, // the values are displayed in Mbps + barColor: "red orange yellow lime green", // slow (first) to fast (last) rate gradient + valueFormat: "%.0f", // rate rounded to the nearest Mbps + title: "rate [Mbps]"}); + zIndex = default(2); +} +