visualizer: display several series of a statistic with any indicator figure, and the IEEE 802.11 per-station 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.
| // 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.
| } | ||
| 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.
|
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:
|
An indicator figure displayed either one value, or a fixed number of series identified by index. A quantity that exists per peer, per source or per flow fits neither: the set of series only becomes known while the simulation is running, and the series have names rather than indices. Add getSeriesIndex() to ~IIndicatorFigure, which resolves a series label to an index and, in figures that support a changing set of series, creates the series. The default implementation maps the empty label to the first series, so single series figures are unaffected. Add BarChartFigure, registered as the "barChart" figure type. It displays one bar per series, the bar height representing 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. Bars are laid out in sorted label order, so the chart does not reshuffle as series appear. 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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… figure ~StatisticVisualizerBase displayed the last value of a statistic per signal source. A quantity that exists per peer, per source or per flow forces a choice between an aggregate that hides the distribution and one visualizer instance per series with no visual relationship between them, while the recording side already handles it with demux(). Add the seriesBy parameter, which determines how the values of the signal are grouped into the series of a single visualization: - "details": one series per distinct details object emitted with the value, the live counterpart of the demux() result filter - "sources": one series per matching signal source, the visualizations being per network node - "flow": one series per packet flow of a single source, demultiplexing its signal by the flow tag (statisticExpression contains demuxFlow()) In the latter two the values come from the result recorders built from statisticExpression, so a series can display a count or a throughput rather than the raw value of the signal. How the series are displayed is not for the visualizer to decide: that is what the figure is for, and each kind of figure comes with its own set of parameters. So ~StatisticCanvasVisualizer takes the attributes of the figure the same way an @figure property gives them, either from a figure template property along its module path (the propertyName parameter, which already existed), or from the new figure object parameter: *.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. A visualizer displaying series without either defaults to a bar chart. 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 chart gaining series. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three UDP streams from the server to the receiver, with a bar chart above the receiver showing a per stream quantity: one series per sink app (seriesBy = "sources", with count or throughput), or one series per named flow demultiplexed from a single signal (seriesBy = "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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 now tags each value with the receiving station, displays one series 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. Wired into ~IntegratedCanvasVisualizer and ~IntegratedMultiCanvasVisualizer, and inert by default: displayRates is false, so nothing is drawn unless it is switched on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 |
Lets
StatisticVisualizerdisplay several values of a statistic at once, and lets any indicator figure display them; adds an 802.11 per-station data rate visualizer configured from it.Why.
StatisticVisualizercould 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 series with no visual relationship between them — while the recording side already handles this withdemux(), visible only after the run.Reworked after review (thanks — the point was that a visualizer must not carry the parameters of every kind of figure it might use). The split is now: the visualizer produces the data (named series of values), the figure decides how they are displayed, and each figure keeps its own parameters as figure attributes.
seriesBydetermines how the values of the signal are grouped into the series of one visualization:"details"— one series per distinct details object emitted with the value. The live counterpart of thedemux()result filter."sources"— one series per matching source module, the visualizations being per network node."flow"— one series per packet flow, demultiplexing one source's signal by the flow tag.In the latter two the values come from result recorders built from
statisticExpression, so a series can show a count or a throughput rather than the raw value.The figure is given as the attributes of an
@figureproperty, either in a figure template property along the module path (propertyName, which already existed and was the mechanism the review pointed at) or in a newfigureobject parameter:Both funnel into
cCanvas::createFigure()+cFigure::parse(), so there is one attribute vocabulary and no second dialect. 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 defaults to, and so takes precedence.The missing piece for charts driven by a live demux was on the figure side:
IIndicatorFigurehad a fixed series count and integer-indexed series only.getSeriesIndex(label, createIfMissing)resolves a label to a series index and creates it if the figure supports a changing set of series; the default implementation maps the empty label to series 0, so existing figures are unaffected.BarChartFigure(Register_Figure("barChart")) is the first such figure, and owns all its appearance parameters.Also here: figure types are now resolved through the
Register_Figure()registry as well as the legacyinet::<Type>Figureclass name; an indicator figure gets the value in the display unit (the value the text label would show) rather than the raw one; and the size reserved for a figure among a node's annotations is updated when it changes, as it does in a chart gaining series.Ieee80211RateVisualizershows the data rate an access point is using towards each associated station. It is a NED configuration of the above —datarateChanged(tagged with the receiving station by the base branch),seriesBy = "details", and a bar chart figure with the unit, scale, gradient and label format that suit a data rate. PointingsignalNameatdatarateSelectedextends it to fixed and per-receiver configured rates; the NED documentation gives that configuration. Wired intoIntegratedCanvasVisualizerandIntegratedMultiCanvasVisualizer, inert by default (displayRates = false).Based on
topic/gy/ieee80211-per-station-rate-control— please review/merge that first.Test
Builds at every commit.
examples/visualizer/statisticbars(new) covers all of it:PacketsandThroughput(sources),Flow(flow),Gauge(a gauge instead of a bar chart), plus autoscaled variants. Each was run in Qtenv and checked on the canvas; theFlowconfig shows one throughput bar per flow (fast 0.80, medium 0.32, slow 0.16 Mbps). The 802.11 visualizer was run both with per-receiver configured rates (54/18/6 Mbps, one bar per station) and withAarfRateControl. The plain text label and the figure template path were re-checked for regressions.