Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions apps/ied_simulator/qml/BrowserReportPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ Rectangle {
}
Repeater {
model: root.hasLastReport && root.latestReport.values
? root.latestReport.values.slice(0, 256) : []
? root.latestReport.values : []
delegate: DataRow { received: true }
}
Repeater {
Expand All @@ -439,6 +439,15 @@ Rectangle {
color: theme.muted
font.pixelSize: 9
}
Label {
visible: root.hasLastReport &&
root.latestReport.valueTextTruncated === true
Layout.fillWidth: true
leftPadding: 9
text: "Some values are preview-truncated; the report payload is preserved by MMS runtime."
color: theme.muted
font.pixelSize: 9
}
Label {
visible: root.hasLastReport ? !root.latestReport.values || root.latestReport.values.length === 0
: root.configuredMembers.length === 0
Expand All @@ -455,7 +464,7 @@ Rectangle {
font.pixelSize: 10
}
Label {
visible: root.hasLastReport ? root.latestReport.values.length > 256
visible: root.hasLastReport ? root.latestReport.valuesTruncated === true
: root.configuredMembers.length > 256
Layout.fillWidth: true
leftPadding: 10
Expand Down
99 changes: 99 additions & 0 deletions apps/ied_simulator/src/BrowserReportMemberProjection.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "MmsEngineeringEvidence.hpp"
#include "ariec61850/mms/data_codec.hpp"
#include "ariec61850/mms/reporting.hpp"

#include <QString>
#include <QStringList>
#include <QVariantList>
#include <QVariantMap>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <utility>

// Bounded, read-only projection of already-decoded MMS report members.
// Report runtime keeps full authoritative frame. The GUI only needs a preview.
namespace arstack::iedsim {
namespace mms = ar::iec61850::mms;

struct BrowserReportMemberProjection final {
QStringList values;
QVariantList evidence;
bool rowsTruncated{};
bool textTruncated{};
bool evidenceTruncated{};
};

inline constexpr int kBrowserReportVisibleMemberLimit = 256;
inline constexpr int kBrowserReportTextLimit = 1024;

[[nodiscard]] inline BrowserReportMemberProjection projectBrowserReportMembers(
const std::vector<mms::MmsReportValue>& members,
int& remainingTypedEvidence,
const int requestedVisibleMembers = kBrowserReportVisibleMemberLimit) {
BrowserReportMemberProjection projection;
const int visibleLimit = std::clamp(requestedVisibleMembers, 0, kBrowserReportVisibleMemberLimit);
const auto visibleCount = std::min(members.size(), static_cast<std::size_t>(visibleLimit));
projection.rowsTruncated = members.size() > visibleCount;
projection.values.reserve(static_cast<qsizetype>(visibleCount));

for (std::size_t ordinal = 0; ordinal < visibleCount; ++ordinal) {
const auto& member = members[ordinal];
QString reference = QString::fromStdString(member.data_reference);
if (reference.isEmpty() && member.member)
reference = QString::fromStdString(member.member->user_reference);
if (reference.isEmpty() && member.member)
reference = QString::fromStdString(member.member->mms_reference);

QString displayReference = reference;
if (displayReference.isEmpty())
displayReference = QStringLiteral("member[%1]").arg(
static_cast<qulonglong>(member.data_set_index));
QString displayValue = QStringLiteral("<failed>");
if (member.value)
displayValue = QString::fromStdString(mms::MmsDataCodec::to_display_string(*member.value));
Comment on lines +58 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound formatting before materializing the full value

When a retained member contains a large string, byte sequence, array, or structure, to_display_string first materializes the complete recursively formatted std::string, and fromStdString then copies all of it into a QString; the 1,024-character limit is applied only afterward. Because buildSubscriptionUi repeats this for up to 128 retained reports on every 100-ms poll, a history of large valid values can still cause substantial allocation churn and delay report refreshes despite the advertised bound. Use a length-aware formatter that stops appending at the preview limit rather than truncating after full conversion.

AGENTS.md reference: AGENTS.md:L133-L140

Useful? React with 👍 / 👎.

QString reasons;
for (const auto& reason : member.reason_for_inclusion.names) {
if (!reasons.isEmpty()) reasons += QStringLiteral(", ");
reasons += QString::fromStdString(reason);
}
QString text = QStringLiteral("%1 = %2%3").arg(
displayReference, displayValue,
reasons.isEmpty() ? QString{} : QStringLiteral(" · ") + reasons);
if (text.size() > kBrowserReportTextLimit) {
projection.textTruncated = true;
text = text.left(kBrowserReportTextLimit) + QStringLiteral(" … [preview truncated]");
}
projection.values.push_back(std::move(text));

// Only read successful typed q/t values, and only when the retained
// presentation row matches the member ordinal (never DataSet position).
if (!member.value || reference.isEmpty()) continue;
if (remainingTypedEvidence > 0) {
const auto decoded = decodeEngineeringEvidence(reference, *member.value);
if (!decoded.isEmpty()) {
projection.evidence.push_back(QVariantMap{
{QStringLiteral("index"), static_cast<int>(ordinal)},
{QStringLiteral("evidence"), decoded}});
--remainingTypedEvidence;
}
} else {
// Do not decode (allocate rows) once the shared budget is empty.
const auto separator = std::max(
reference.lastIndexOf(QLatin1Char('$')),
reference.lastIndexOf(QLatin1Char('.')));
const QString leaf = reference.mid(separator + 1);
const auto kind = member.value->kind();
if ((leaf == QStringLiteral("q") && kind == mms::MmsDataKind::bit_string) ||
(leaf == QStringLiteral("t") && kind == mms::MmsDataKind::utc_time))
projection.evidenceTruncated = true;
}
}
return projection;
}
} // namespace arstack::iedsim
72 changes: 13 additions & 59 deletions apps/ied_simulator/src/MmsReportController.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "MmsReportController.hpp"
#include "MmsEngineeringEvidence.hpp"
#include "BrowserReportMemberProjection.hpp"

#include "ariec61850/mms/data_codec.hpp"
#include "ariec61850/mms/dynamic_data_set.hpp"
Expand Down Expand Up @@ -28,7 +28,6 @@ namespace mms = ar::iec61850::mms;
namespace {
constexpr int kMaximumVisibleReports = 128;
constexpr int kMaximumTypedReportEvidence = 96;
constexpr int kMaximumVisibleReportMembers = 256;

QString boolText(const std::optional<bool> value) {
if (!value.has_value()) return QStringLiteral("—");
Expand Down Expand Up @@ -436,21 +435,6 @@ struct ReportUi final {
bool cleanupRequired{};
};

QString reportValueText(const mms::MmsReportValue& item) {
QString value = QStringLiteral("<failed>");
if (item.value) value = QString::fromStdString(mms::MmsDataCodec::to_display_string(*item.value));
QString reference = QString::fromStdString(item.data_reference);
if (reference.isEmpty() && item.member) reference = QString::fromStdString(item.member->user_reference);
if (reference.isEmpty()) reference = QStringLiteral("member[%1]").arg(static_cast<qulonglong>(item.data_set_index));
QString reasons;
for (const auto& reason : item.reason_for_inclusion.names) {
if (!reasons.isEmpty()) reasons += QStringLiteral(", ");
reasons += QString::fromStdString(reason);
}
return QStringLiteral("%1 = %2%3")
.arg(reference, value, reasons.isEmpty() ? QString{} : QStringLiteral(" · ") + reasons);
}

ReportUi buildSubscriptionUi(
const mms::MmsReportSubscriptionSnapshot& subscription,
const bool active) {
Expand Down Expand Up @@ -480,48 +464,18 @@ ReportUi buildSubscriptionUi(
map.insert(QStringLiteral("overflow"), frame.header.buffer_overflow.value_or(false));
map.insert(QStringLiteral("decoderMode"), QString::fromStdString(frame.decoder_mode));
map.insert(QStringLiteral("valueCount"), static_cast<int>(frame.values.size()));
QStringList values;
QVariantList typedEvidence;
bool typedEvidenceTruncated = false;
int memberIndex = 0;
for (const auto& item : frame.values) {
values.push_back(reportValueText(item));
if (item.value && memberIndex < kMaximumVisibleReportMembers) {
QString memberRef = QString::fromStdString(item.data_reference);
if (memberRef.isEmpty() && item.member)
memberRef = QString::fromStdString(item.member->user_reference);
if (memberRef.isEmpty() && item.member)
memberRef = QString::fromStdString(item.member->mms_reference);
if (typedEvidenceBudget > 0) {
const auto evidence = arstack::iedsim::decodeEngineeringEvidence(
memberRef, *item.value);
if (!evidence.isEmpty()) {
typedEvidence.push_back(QVariantMap{
{QStringLiteral("index"), memberIndex},
{QStringLiteral("evidence"), evidence}});
--typedEvidenceBudget;
}
} else {
// Mark truncation only for real q/t typed members, not
// unrelated BIT STRING or UTC-valued attributes.
const auto kind = item.value->kind();
const auto separator = std::max(
memberRef.lastIndexOf(QLatin1Char('$')),
memberRef.lastIndexOf(QLatin1Char('.')));
const QString leaf = memberRef.mid(separator + 1);
if ((leaf == QStringLiteral("q") &&
kind == mms::MmsDataKind::bit_string) ||
(leaf == QStringLiteral("t") &&
kind == mms::MmsDataKind::utc_time))
typedEvidenceTruncated = true;
}
}
++memberIndex;
}
map.insert(QStringLiteral("values"), values);
map.insert(QStringLiteral("valueEvidence"), typedEvidence);
map.insert(QStringLiteral("valueEvidenceTruncated"), typedEvidenceTruncated);
map.insert(QStringLiteral("valuesSummary"), values.mid(0, 3).join(QStringLiteral(" | ")));
// A frame can include far more DataSet members than the 256 GUI
// rows. Project only retained rows, preserving actual total count
// and a shared typed-evidence budget across recent frames.
const auto projected = arstack::iedsim::projectBrowserReportMembers(
frame.values, typedEvidenceBudget);
map.insert(QStringLiteral("values"), projected.values);
map.insert(QStringLiteral("valueEvidence"), projected.evidence);
map.insert(QStringLiteral("valueEvidenceTruncated"), projected.evidenceTruncated);
map.insert(QStringLiteral("valuesTruncated"), projected.rowsTruncated);
map.insert(QStringLiteral("valueTextTruncated"), projected.textTruncated);
map.insert(QStringLiteral("valuesSummary"),
projected.values.mid(0, 3).join(QStringLiteral(" | ")));
map.insert(QStringLiteral("duplicateCount"), static_cast<qulonglong>(streamIt->state.duplicate_count));
map.insert(QStringLiteral("gapCount"), static_cast<qulonglong>(streamIt->state.gap_count));
map.insert(QStringLiteral("resetCount"), static_cast<qulonglong>(streamIt->state.reset_count));
Expand Down
43 changes: 43 additions & 0 deletions apps/ied_simulator/src/ied_browser_large_model_qa.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "MmsLiveTreeModel.hpp"
#include "MmsEngineeringEvidence.hpp"
#include "BrowserReportMemberProjection.hpp"

#include <QCoreApplication>
#include <QElapsedTimer>
Expand Down Expand Up @@ -311,6 +312,48 @@ int main(int argc, char** argv) {
return 25;
}

// Project 300 decoded report members but allocate only 256 GUI rows.
// A shared one-item evidence budget prevents a q/t storm from allocating
// a detailed map per member or per retained report frame.
std::vector<ar::iec61850::mms::MmsReportValue> largeFrame(300);
for (std::size_t index = 0; index < largeFrame.size(); ++index) {
largeFrame[index].data_set_index = index;
largeFrame[index].data_reference = "LD4/LLN19.DO99.stVal";
largeFrame[index].value = ar::iec61850::mms::MmsDataValue::boolean(true);
}
largeFrame[0].data_reference = "LD4/LLN19.DO99.q";
largeFrame[0].value = ar::iec61850::mms::MmsDataValue::bit_string(3U, bits);
largeFrame[1].data_reference = "LD4/LLN19.DO99.t";
largeFrame[1].value = ar::iec61850::mms::MmsDataValue::utc_time(utc);
largeFrame[2].value.reset(); // An unsuccessful member has no typed evidence
largeFrame[3].data_reference = "LD4/LLN19.DO99.q";
largeFrame[3].value = ar::iec61850::mms::MmsDataValue::boolean(false); // Wrong kind
largeFrame[4].data_reference = std::string(2048, 'X'); // Bounded preview
int typedBudget = 1;
const auto projected = arstack::iedsim::projectBrowserReportMembers(
largeFrame, typedBudget);
if (projected.values.size() != 256 ||
projected.evidence.size() != 1 ||
projected.evidence.constFirst().toMap().value(QStringLiteral("index")).toInt() != 0 ||
typedBudget != 0 ||
!projected.rowsTruncated || !projected.textTruncated ||
!projected.evidenceTruncated ||
!projected.values.at(2).contains(QStringLiteral("<failed>")) ||
projected.values.at(4).size() > 1100) {
std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_report_projection\n";
return 27;
}
int smallBudget = 2;
const auto smallProjection = arstack::iedsim::projectBrowserReportMembers(
largeFrame, smallBudget, 3);
if (smallProjection.values.size() != 3 ||
smallProjection.evidence.size() != 2 ||
smallBudget != 0 || !smallProjection.rowsTruncated ||
smallProjection.evidenceTruncated) {
std::cerr << "IED_BROWSER_LARGE_MODEL_FAIL bounded_report_small_window\n";
return 28;
}

const bool passes = applyMs <= 5000 && filterMs <= 1500 && selectMs <= 500;
std::cout << "IED_BROWSER_LARGE_MODEL_" << (passes ? "PASS" : "FAIL")
<< " nodes=" << model.totalNodeCount()
Expand Down
21 changes: 19 additions & 2 deletions apps/ied_simulator/test_browser_convergence.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@
report_controller = (workspace_path.parent.parent / "src" / "MmsReportController.cpp").read_text(encoding="utf-8")
for token in [
'kMaximumTypedReportEvidence = 96',
'kMaximumVisibleReportMembers = 256',
'decodeEngineeringEvidence(',
'projectBrowserReportMembers(',
'QStringLiteral("valueEvidence")',
'QStringLiteral("valueEvidenceTruncated")',
]:
Expand All @@ -103,6 +102,22 @@
"const QString leaf = mmsItem.mid(leafSeparator + 1);" not in typed_decoder):
raise SystemExit("BROWSER_REPORT_TYPED_EVIDENCE_FAIL invalid shared decoder source")

projection_header = (workspace_path.parent.parent / "src" / "BrowserReportMemberProjection.hpp").read_text(encoding="utf-8")
for token in [
'kBrowserReportVisibleMemberLimit = 256',
'kBrowserReportTextLimit = 1024',
'std::min(members.size(),',
'projection.rowsTruncated = members.size() > visibleCount',
'if (!member.value || reference.isEmpty()) continue',
'decodeEngineeringEvidence(',
]:
if token not in projection_header:
raise SystemExit("BROWSER_REPORT_BOUNDED_PROJECTION_FAIL missing=" + token)
if 'for (const auto& item : frame.values)' in report_controller:
raise SystemExit("BROWSER_REPORT_BOUNDED_PROJECTION_FAIL full-frame QML string conversion")
if 'decodeEngineeringEvidence(' in report_controller:
raise SystemExit("BROWSER_REPORT_BOUNDED_PROJECTION_FAIL duplicate inline decoding")

report_required = [
'objectName: "browserReportUnifiedInspector"',
'Control Block Attributes',
Expand All @@ -122,6 +137,8 @@
'root.selectedReportMemberIndex = dataRow.index',
'onLatestReportIdentityChanged: selectedReportMemberIndex = -1',
'BrowserEngineeringEvidence {',
'root.latestReport.valuesTruncated === true',
'root.latestReport.valueTextTruncated === true',
'String(frame.reportId) === wanted',
'Offline values are not fabricated.',
]
Expand Down
Loading