visualizer: display several values of a statistic on one figure, and the IEEE 802.11 per-peer rate visualizer - #1125
Conversation
| double range = maxForScale - minValue; | ||
| double fraction = range > 0 ? (value - minValue) / range : 0; | ||
| if (fraction < 0) fraction = 0; | ||
| if (fraction > 1) fraction = 1; | ||
| double pos = fraction * (barColors.size() - 1); | ||
| int index = (int)std::floor(pos); |
There was a problem hiding this comment.
🔴 Charts with a not-yet-measured bar can read past the end of the color list and crash
A bar whose value is still unknown is fed into the color picker (getBarColor() at src/inet/visualizer/base/StatisticVisualizerBase.cc:344-349) without any check for the "no value yet" case, so the color index becomes garbage and the program can read outside the color list and crash.
Impact: Simulations using the bar chart display can crash or draw corrupt charts as soon as a bar exists before its first measurement.
NaN propagation into the gradient index and into figure geometry
In sources mode every newly registered bar is initialized with values[label] = NaN (src/inet/visualizer/base/StatisticVisualizerBase.cc:378), and refreshGroupedBarValues() keeps NaN until the recorder produces a value; refreshFlowBarValues() and processBarValue() can also store NaN.
In getBarColor(), with value = NaN: fraction = (NaN - minValue)/range = NaN; both clamps if (fraction < 0) and if (fraction > 1) are false for NaN, so pos = NaN, and int index = (int)std::floor(NaN) is undefined behaviour (typically INT_MIN). index >= (int)barColors.size() - 1 is then false, so barColors[index] / barColors[index + 1] index the vector far out of range.
The same NaN also reaches StatisticCanvasVisualizer::refreshChart() (src/inet/visualizer/canvas/common/StatisticCanvasVisualizer.cc:242-248), producing h = NaN and a cRectangleFigure with NaN bounds.
Both places need an explicit NaN check (e.g. skip drawing the bar, or draw a zero-height bar in a neutral color), like the text mode does in DirectiveResolver::resolveDirective which renders "-" for NaN.
| double range = maxForScale - minValue; | |
| double fraction = range > 0 ? (value - minValue) / range : 0; | |
| if (fraction < 0) fraction = 0; | |
| if (fraction > 1) fraction = 1; | |
| double pos = fraction * (barColors.size() - 1); | |
| int index = (int)std::floor(pos); | |
| double fraction = range > 0 ? (value - minValue) / range : 0; | |
| if (std::isnan(fraction)) fraction = 0; | |
| if (fraction < 0) fraction = 0; | |
| if (fraction > 1) fraction = 1; | |
| double pos = fraction * (barColors.size() - 1); | |
| int index = (int)std::floor(pos); | |
| if (index >= (int)barColors.size() - 1) |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Handled by the rework this review predates: the gradient moved into BarChartFigure, whose getBarColor() returns the first color for NaN, and whose layout() maps a NaN value to zero height and hides the bar. No NaN reaches the color index or the figure bounds.
| // attach a result recorder (statisticExpression, e.g. count or throughput) whose value the bar will show | ||
| addResultRecorder(source, signal); | ||
| auto recorder = getResultRecorder(source, signal); | ||
| auto networkNode = getContainingNode(module); | ||
| auto barSetVisualization = getBarSetVisualization(networkNode->getId()); | ||
| if (barSetVisualization == nullptr) { | ||
| barSetVisualization = createBarSetVisualization(networkNode); | ||
| if (barSetVisualization == nullptr) | ||
| return; // bar charts not supported by this concrete visualizer (e.g. osg) |
There was a problem hiding this comment.
🔴 Statistic collectors pile up without limit when a display cannot draw bar charts
A new hidden statistic collector is attached to the same module on every single signal (addResultRecorder() at src/inet/visualizer/base/StatisticVisualizerBase.cc:366) before it is known whether the chart can actually be created, so when charts are unsupported the collectors grow without bound.
Impact: With a display that does not support bar charts, memory use and per-signal processing cost grow continuously until the run slows to a crawl or runs out of memory.
Early return skips the registration bookkeeping
processGroupedBarSource() first checks groupedBarSourceIds and the source filter, then calls addResultRecorder(source, signal). If createBarSetVisualization() returns nullptr (the base-class default, i.e. any visualizer that does not implement bar charts, e.g. StatisticOsgVisualizer), it returns at src/inet/visualizer/base/StatisticVisualizerBase.cc:373 before groupedBarSourceIds.insert(module->getId()) at line 379. Consequently the guard at line 361 never fires, and each subsequent emission of the signal appends yet another recorder chain to the source module.
processFlowBarSource() has exactly the same ordering problem: addResultRecorder() at line 404, early return at line 409, groupedBarSourceIds.insert() at line 412.
A fix is to determine whether a bar set can be created (or mark the source as handled / disable bar mode) before attaching the recorder.
Prompt for agents
In StatisticVisualizerBase::processGroupedBarSource() and processFlowBarSource() (src/inet/visualizer/base/StatisticVisualizerBase.cc), addResultRecorder() is called before the code checks whether createBarSetVisualization() succeeds. When the concrete visualizer does not support bar charts (base implementation returns nullptr), the function returns early without inserting the module id into groupedBarSourceIds, so the duplicate-registration guard never trips and a new recorder chain is attached to the source module on every signal emission, growing memory and per-signal cost without bound. Reorder so the bar set is obtained/created (or the unsupported case detected and permanently remembered, e.g. by disabling bar handling or still inserting into groupedBarSourceIds) before any recorder is attached.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Handled by the rework this review predates: registerSeriesSource() now inserts into seriesSourceIds before it tries to create the visualization, and calls addResultRecorder() only after the nullptr check, so a visualizer that cannot display series registers each source once and attaches nothing.
| } | ||
| std::string label = module->getFullName(); | ||
| barSetVisualization->recorders[label] = recorder; |
There was a problem hiding this comment.
🟡 Bars for same-named modules in one node overwrite each other
Each bar is labelled only with the short module name (module->getFullName() at src/inet/visualizer/base/StatisticVisualizerBase.cc:376), so two identically named modules inside the same node share one bar and only one of them is shown.
Impact: In nodes with several same-named submodules (for example multiple wireless interfaces), some sources silently disappear from the chart.
Label collision in the per-node bar map
In sources mode the bar set is keyed by the network node id, and each bar is keyed by module->getFullName(). For a source filter matching e.g. **.wlan[*].mac, both wlan[0].mac and wlan[1].mac have full name mac, so barSetVisualization->recorders[label] and values[label] (lines 377-378) overwrite the earlier entry; the first module's recorder is dropped from the map and its value never displayed, while the count of bars is lower than the number of matching sources.
A label that is unique within the node (e.g. the path relative to the network node) would avoid the collision.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Real, fixed in 1afadc8. The label is now the path of the source relative to the network node, so pointing sourceFilter at *.switch.eth[*].mac in examples/visualizer/statisticbars gives eth[0].mac and eth[1].mac instead of a single mac bar. Direct submodules such as app[0] keep their label, so the existing configs render the same.
|
The statistic visualizer should be capable of visualizing the value using any instrument figure. Each kind of instrument figure comes with its own set of parameters. We can't let all those parameters go into the statistic visualizer module. For example, we can have bar chart/line chart/histogram chart/gauge/text instruments, each have their own set parameters. Options:
|
6f77762 to
1e5fc68
Compare
|
Reworked along these lines — thanks, the objection was right, and it turned out the mechanism was already in the codebase and this PR was simply bypassing it.
So the two options are not alternatives: both are front ends for the same thing, and both now funnel into
Since the template is the only one of the two that can replace a figure a derived visualizer already defaults to, it takes precedence over the parameter. What was actually missing was on the figure side, not the parameter side: The new |
05bb4f7 to
1be82e6
Compare
|
Pushed 1afadc8 for the third Devin finding, which was real: in The other two findings were made against the pre-rework commit and no longer apply: the NaN handling now sits in |
0248366 to
a91a713
Compare
07641e9 to
e47d2b0
Compare
87e64f2 to
353ab79
Compare
e47d2b0 to
d39a093
Compare
353ab79 to
fa2bc31
Compare
d39a093 to
05d85ae
Compare
fa2bc31 to
42d0174
Compare
05d85ae to
e865b4f
Compare
|
The word series makes me so confused. Is it not grouping statistics instead? Groups have members, some figures are capable of displaying a group of statistics. Grouped by flow name for example. A series is just a sequence of data points, so I don't quite understand why is this word used. |
1b90cac to
0aa0d1e
Compare
83adb44 to
2ab6568
Compare
42d0174 to
f55498c
Compare
2ab6568 to
01064c9
Compare
f55498c to
a93ad8c
Compare
01064c9 to
1542b8b
Compare
a93ad8c to
14725f1
Compare
1542b8b to
0f90a30
Compare
14725f1 to
e5d28a4
Compare
0f90a30 to
496fe51
Compare
FigureRecorder accepted a figure index equal to the number of series the figure has, and then called setValue() one past the end. The check was '>' where the index is zero based, so only an index beyond that was refused. Reject it at initialization instead. A model that used the boundary index by mistake now stops with an error naming the figure and the bound, rather than writing into a series that does not exist.
~IIndicatorFigure called the values it displays series. That word only fits ~PlotFigure, where a series really is a sequence of values over time; for a gauge, a counter or a thermometer the indexed thing is a single value that happens to have an identity of its own. Rename it to item: getNumItems() instead of getNumSeries(), and the index parameter of setValue() to index. Only ~PlotFigure and ~FigureRecorder follow, because the name of a parameter is not part of a signature, so the figures that display a single value are untouched. getNumSeries() stays for one release as a deprecated method, and the default getNumItems() calls it, so an indicator figure implemented outside INET keeps working until its author renames the override. Note that overriding a deprecated method is not a use of its name, so the compiler does not warn at the override itself; the deprecation is visible in the header and at any remaining call site. ~PlotFigure keeps its own getNumSeries(), also deprecated, so that a caller of it does not silently get the interface default of 1 instead of the series count. No behavior change beyond the deprecation.
An indicator figure displayed either one value, or a fixed number of them identified by index. A quantity that exists per peer, per source or per flow fits neither: the set of them only becomes known while the simulation is running, and they carry names rather than indices. Add BarChartFigure, registered as the "barChart" figure type. Its items are bars, and the number of them is not fixed: setNumItems() sets it and setItemLabel() names each, the same way ~PlotFigure::setNumSeries() sets the number of its series and setLineColor() configures each. Bars are displayed in index order, so whoever sets them decides the order. A bar height represents the value over the minValue..maxValue range, or over the autoscaled range of the current values when maxValue is not given. barColor accepts a list of colors interpolated over the same range, so the color of a bar carries the value even where the chart is too small to read. Like the other instrument figures, everything about its appearance is a figure attribute parsed from a property, so none of it has to appear as a parameter of whatever displays the figure.
~StatisticCanvasVisualizer displayed a statistic with a text label, or with an indicator figure taken from a figure template property along its module path (the propertyName parameter). A template cannot be given in an ini file and cannot refer to module parameters, so choosing a gauge instead of a label, or merely changing its scale, meant editing a NED file. Add the figure parameter, which takes the attributes of the figure the same way an @figure property gives them: *.visualizer.statisticVisualizer.figure = {type: "gauge", size: [60, 60], maxValue: 100} The parameter, unlike the template, can be set from an ini file and can refer to module parameters; the template, unlike the parameter, can replace a figure a derived visualizer already defaults to, and therefore takes precedence. Along the way, the figure type is now also looked up among the types registered with Register_Figure(), not only as an inet::<Type>Figure class; an indicator figure is given the value in the display unit (what the text label would display) rather than the raw value; and the size reserved for the figure among the annotations of the network node is updated when it changes, as it does in a counter gaining digits.
~StatisticVisualizerBase displayed the last value of a statistic per signal source. A quantity that exists per peer or per flow forces a choice between an aggregate that hides the distribution and one visualizer instance per value with no visual relationship between them, while the recording side already handles it with demux(). Add the splitBy parameter, which determines whether the values a signal source emits are split into several statistics, and what identifies each of them: - "details": one statistic per distinct details object emitted with the value, the live counterpart of the demux() result filter - "flow": one statistic per packet flow of the source, demultiplexing its signal by the flow tag (statisticExpression contains demuxFlow()), so an item can display a count or a throughput rather than the raw value StatisticVisualization now holds the items it displays instead of a single value. An unsplit statistic has one item whose label is empty, so there is one visualization class and one code path rather than one of each per case. The visualizer owns the label to index mapping -- items are displayed in label order, so an item's index is its position among them -- and the figure is only ever given a number of items, their labels, and their values. How the items are displayed is not for the visualizer to decide: that is what the figure is for. A visualizer displaying several values without a figure configured defaults to a bar chart. The per frame refresh this adds is the first thing that touches a visualization outside a signal receipt from a live source, so it is also the first that can meet a deleted one. A visualization whose module is gone is dropped before its figure is read, and the network node visualization is looked up rather than remembered, because a node visualization is a figure group that is deleted with its node and takes the statistic figure with it.
Splitting puts the values of one signal source on that source's figure. Which statistics share a figure is a separate decision, and the other useful answer is per network node: a quantity that exists once per source, displayed for every matching source of a node on a single figure, e.g. the throughput of each of a host's applications. Add the groupBy parameter, which determines which statistics are displayed together as the items of a single figure: - "none": each statistic is displayed on a figure of its own - "source": the statistics of one signal source, i.e. the ones splitBy split its values into - "networkNode": the matching signal sources of one network node, one item per source. An item is labelled with the path of its source relative to the node (e.g. wlan[0].mac), which is unique by construction where the bare module name is not -- the MACs of several network interfaces all share one name, and would merge into a single item. For a source that is a direct submodule of the node the path is just its name (e.g. app[0]). The values then come from the result recorders built from statisticExpression, so an item can display a count or a throughput rather than the raw value of the signal. The error that rejects an unsupported combination names all three rules the guard enforces. This is the first mode whose items hold the result recorder of a module other than the one the visualization is keyed on, and the first whose items can go away. A recorder is deleted with its source module (cResultListener::unsubscribedFrom), and a network node visualization is a figure group that is deleted with its node, taking the statistic figure with it. So an item whose source is gone is dropped before the values are read, a visualization whose module is gone is dropped before its figure is touched, and the network node visualization is looked up rather than remembered, because the remembered pointer is exactly what goes stale. Because the set of items is no longer only grown, the figure is told to relabel on a version counter rather than on the item count, which a removal and an addition between two refreshes would leave unchanged. Four module tests cover it. The first reads the bar chart itself through a probe -- how many bars, and their labels in index order -- because the visualizer records nothing to a result file, so asserting on the traffic would pass just as well with the visualizer switched off. The second asserts that a figure which cannot display labelled items is rejected, which is also what proves the visualizer ran at all. The third deletes a signal source and the fourth deletes a whole network node, each asserting what is left on the figure afterwards.
Three UDP streams from the server to the receiver, with a bar chart above the receiver showing a per stream quantity: one bar per sink app (groupBy = "networkNode", with count or throughput), or one bar per named flow demultiplexed from a single signal (splitBy = "flow"). A further config displays the throughput of one stream on a gauge instead, to show that the figure is a configuration choice rather than a display mode.
Shows the data rate an access point is using towards each of its associated stations as a bar chart above the node, so that rate diversity across stations is visible while the simulation runs rather than only in the results afterwards. It is a configuration of the generic ~StatisticCanvasVisualizer rather than new visualizer code: it subscribes to the rate control's datarateChanged signal, which tags each value with the receiving station, displays one bar per receiver, and configures a bar chart figure with the scale, colors and label format that suit a data rate. Pointing signalName at the coordination function's datarateSelected instead makes it cover fixed and per-receiver configured rates too; the NED documentation gives that configuration. Being a ~StatisticCanvasVisualizer, it goes wherever one does: it is selected as the type of the integrated visualizer's statistic visualizer, and so needs no submodule of its own.
496fe51 to
17f1b14
Compare
Lets a statistic visualizer display several values of one statistic at once, on a bar chart above the network node, and adds an 802.11 per-peer data rate visualizer configured from it.
Why. A statistic visualizer could show one number per signal source. For a quantity that exists per peer, per source or per flow, that forces a choice between an aggregate that hides the distribution and one visualizer instance per value with no visual relationship between them — while the recording side already handles this with
demux(), visible only after the run.Three names for three things. An earlier version of this branch called all of it a "series". That word was doing three jobs at once, and one parameter fused two independent decisions:
splitBydetermines whether the values a signal source emits are split into several statistics, and what identifies each:"none"— the source emits the values of a single statistic"details"— one statistic per distinct details object emitted with the value; the live counterpart of thedemux()result filter"flow"— one statistic per packet flow of the source (statisticExpressioncontainsdemuxFlow())groupBydetermines which statistics are displayed together as the items of a single figure:"none"— each statistic gets a figure of its own"source"— the statistics of one signal source, i.e. the onessplitBysplit its values into"networkNode"— the matching signal sources of one network node, one item per source, each labelled with its path relative to the node (wlan[0].mac), which is unique where the bare module name is notOnly the combinations the visualizations are keyed for are accepted, and the error names all three rules it enforces. Fully decoupling them — so
splitBy = "details"withgroupBy = "none"gives one text instrument per peer — is a follow-up that will not change this NED contract.The figure is given as the attributes of an
@figureproperty, either in a figure template along the module path (propertyName, which already existed) or in a newfigureobject parameter, which unlike the template can be set from an ini file and can refer to module parameters:Both funnel into
cCanvas::createFigure()+cFigure::parse(), so there is one attribute vocabulary, one set of allowed keys, and no second dialect.BarChartFigure(Register_Figure("barChart")) is the first indicator figure whose item count is not fixed. It followsPlotFigure: the interface carries the getter, the figure carries the setters —setNumItems(n)sizes the chart andsetItemLabel(i, label)names each bar, assetNumSeries(3)andsetLineColor(0…2)do for a plot. Bars are drawn in index order. The visualizer owns the label to index mapping: it holds the items in astd::mapkeyed by label, so an item's index is its position in label order, and the figure is only ever given a count, labels and values. A figure that cannot display labelled items is rejected with a message that says so.IIndicatorFigureis renamed, series to item:getNumItems(), andsetValue(int index, …). "Series" only fitsPlotFigure, where an item really is a sequence of values over time.getNumSeries()stays for one release as a deprecated method thatgetNumItems()forwards to, so a figure implemented outside INET keeps working. Note that overriding a deprecated method is not a use of its name, so the compiler does not warn at the override — the deprecation is visible in the header and at call sites, and WHATSNEW says so rather than promising a warning that does not happen.Ieee80211RateCanvasVisualizershows the data rate a node is using towards each of its peers. It is a NED configuration of the generic visualizer, not new visualizer code, and being aStatisticCanvasVisualizerit needs no submodule of its own:Also here: a bounds-check fix in
FigureRecorder(a figure index equal to the item count was accepted and then wrote one past the end; a negative index passed too), and lifetime handling the per-frame refresh makes necessary — a result recorder is deleted with its source module, and a network node visualization is a figure group deleted with its node that takes the statistic figure with it, so an item whose source is gone and a visualization whose module is gone are both dropped before anything reads them, and the node visualization is looked up rather than remembered. That last part also repairs a pre-existing crash on this path.Test
Builds at every one of the eight commits (release).
Four module tests —
inet_run_module_tests -m release -f StatisticVisualizerItems, all PASS:_1BarChartProbe_2_3_4The visualizer is GUI-only (
initialize()returns early on!hasGUI()), so all four run under Cmdenv's fake GUI, which makeshasGUI()true and drivesrefreshDisplay(). That matters for what the assertions are worth: the visualizer records nothing to a result file, so asserting on received packet counts would pass with the visualizer switched off — checked, it does. Test_1therefore reads the figure, and_2's rejection is what proves the path executed at all. Test_4was written for the lifetime handling and found a segfault at teardown in the first version of it.Fingerprints.
examples/visualizer/statisticbars(new) has rows intests/fingerprint/store.jsonforPackets,FlowandGauge—Flowbeing the onlysplitBy = "flow"coverage andGaugethe only non-bar-chart figure. All three pass throughinet_run_fingerprint_tests. Thetyfingredient runs under fake GUI and hashes canvas figure geometry, so these are genuine visualizer coverage:Gaugeshares itstplx/~tNl/~tNDvalues withPackets(identical traffic) but differs ontyf.Runs. All seven example configs run clean under fake GUI with the visualizer live.
Deliberately not in this PR
The OSG visualizer accepts
splitBy/groupByand renders nothing, and still has the pre-existing form of the lifetime defect fixed here on the canvas side.@statistic(record=figure)cannot target abarChart, because nothing pre-sizes its bars from a property. No test coverssplitBy = "details"orIeee80211RateCanvasVisualizeritself.BarChartFigurerelayouts once persetValue, and its autoscale moves only the maximum. The display-unit conversion differs between the unsplit path (walks the unit list) and the split path (uses the first unit only, and none whenstatisticUnitis unset).