diff --git a/WHATSNEW b/WHATSNEW index 2acaff561e1..fff54e9bf32 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -29,6 +29,30 @@ 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. + +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 @@ -57,6 +81,40 @@ 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. + +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. + +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/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/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 + diff --git a/src/inet/common/figures/FigureRecorder.cc b/src/inet/common/figures/FigureRecorder.cc index ac372d69f4d..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 > 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); diff --git a/src/inet/visualizer/base/StatisticVisualizerBase.cc b/src/inet/visualizer/base/StatisticVisualizerBase.cc index 8ebcb090248..acbd9f4f1dd 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,35 @@ 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); + 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"); + subscribedSignal = registerSignal(signalName); subscribe(); } } @@ -95,7 +123,7 @@ void StatisticVisualizerBase::handleParameterChange(const char *name) void StatisticVisualizerBase::subscribe() { - visualizationSubjectModule->subscribe(registerSignal(signalName), this); + visualizationSubjectModule->subscribe(subscribedSignal, this); } void StatisticVisualizerBase::unsubscribe() @@ -103,7 +131,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 +177,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 +197,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 +205,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 +266,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 +282,135 @@ 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; + 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) +{ + 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()); + // 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) { + 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]; + if (item.recorder == nullptr) + statisticVisualization->itemsVersion++; + 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..e0abf5f80cb 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,22 @@ 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 + }; + + // 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; @@ -34,12 +54,29 @@ 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 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: - LastValueRecorder *recorder = nullptr; - const int moduleId = -1; + 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 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; @@ -65,9 +102,12 @@ 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; + GroupMode groupMode = GROUP_NONE; StringFormat format; std::vector units; cFigure::Font font; @@ -78,7 +118,8 @@ class INET_API StatisticVisualizerBase : public VisualizerBase, public cListener double placementPriority; //@} - std::map, const StatisticVisualization *> statisticVisualizations; + std::map statisticVisualizations; // module id -> visualization + mutable std::set registeredSourceIds; // signal sources whose result recorders are already attached protected: virtual void initialize(int stage) override; @@ -91,28 +132,64 @@ 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 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 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. + 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 && 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 && 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); } + 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..f77bcd9bf90 100644 --- a/src/inet/visualizer/base/StatisticVisualizerBase.ned +++ b/src/inet/visualizer/base/StatisticVisualizerBase.ned @@ -48,6 +48,31 @@ 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() + // 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 769967f6c6e..161a63a52a8 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc @@ -8,8 +8,8 @@ #include "inet/visualizer/canvas/common/StatisticCanvasVisualizer.h" #include "inet/common/ModuleAccess.h" +#include "inet/common/figures/BarChartFigure.h" #include "inet/common/figures/BoxedLabelFigure.h" -#include "inet/common/figures/IIndicatorFigure.h" namespace inet { @@ -17,9 +17,27 @@ namespace visualizer { Define_Module(StatisticCanvasVisualizer); -StatisticCanvasVisualizer::StatisticCanvasVisualization::StatisticCanvasVisualization(NetworkNodeCanvasVisualization *networkNodeVisualization, cFigure *figure, int moduleId, simsignal_t signal, const char *unit) : +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, int networkNodeId, cFigure *figure, int moduleId, simsignal_t signal, const char *unit) : StatisticVisualization(moduleId, signal, unit), networkNodeVisualization(networkNodeVisualization), + networkNodeId(networkNodeId), figure(figure) { } @@ -40,12 +58,92 @@ 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); + // 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; +} + +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 *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 && 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); boxedLabelFigure->setText(""); @@ -56,70 +154,135 @@ 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 (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()); } - 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(); + 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(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 && groupMode == GROUP_NONE) + return; // a single value is refreshed when its signal is received + if (groupMode == GROUP_NETWORK_NODE) + refreshSourceItemValues(); + else if (splitMode == SPLIT_FLOW) + refreshFlowItemValues(); + for (auto& it : statisticVisualizations) + refreshFigure(static_cast(it.second)); +} + +void StatisticCanvasVisualizer::refreshFigure(StatisticCanvasVisualization *statisticCanvasVisualization) const +{ auto figure = statisticCanvasVisualization->figure; - if (auto indicatorFigure = dynamic_cast(figure)) - indicatorFigure->setValue(0, simTime(), statisticVisualization->recorder->getLastValue()); + auto& items = statisticCanvasVisualization->items; + if (auto indicatorFigure = dynamic_cast(figure)) { + 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); + 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 df94830ffb1..4579af2fafb 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" @@ -23,10 +24,13 @@ 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); + int displayedItemsVersion = -1; // the version of the item set the figure was last given 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(); }; @@ -36,11 +40,36 @@ class INET_API StatisticCanvasVisualizer : public StatisticVisualizerBase protected: virtual void initialize(int stage) override; + virtual void refreshDisplay() const 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; + // 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(); + // 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 *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 4297fc42b05..cd0c5b34a30 100644 --- a/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned +++ b/src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.ned @@ -18,8 +18,12 @@ 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. 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 // @@ -27,7 +31,16 @@ 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(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, 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 string propertyIndex = default(""); // Optional property index of a figure template along the module path of the visualizer @class(StatisticCanvasVisualizer); } 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); +} + 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 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", 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 \