NewBusLoadAnalysis.java
/**
* Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
* All Rights Reserved.
*
* NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
* KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
* OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
* MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
* SPDX-License-Identifier: EPL-2.0
*
* Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
*
* This program includes and/or can make use of certain third party source code, object code, documentation and other
* files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
* configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
* conditions contained in any such Third Party Software or separate license file distributed with such Third Party
* Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
* aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
* censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
*/
package org.osate.analysis.resource.budgets.busload;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.emf.ecore.EObject;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.Element;
import org.osate.aadl2.NamedElement;
import org.osate.aadl2.contrib.aadlproject.DataRateUnits;
import org.osate.aadl2.contrib.aadlproject.SizeUnits;
import org.osate.aadl2.contrib.aadlproject.TimeUnits;
import org.osate.aadl2.contrib.communication.CommunicationProperties;
import org.osate.aadl2.contrib.communication.RateSpec.RateUnit_FieldType;
import org.osate.aadl2.contrib.memory.MemoryProperties;
import org.osate.aadl2.contrib.timing.TimingProperties;
import org.osate.aadl2.instance.ComponentInstance;
import org.osate.aadl2.instance.ConnectionInstance;
import org.osate.aadl2.instance.ConnectionInstanceEnd;
import org.osate.aadl2.instance.FeatureInstance;
import org.osate.aadl2.instance.InstanceObject;
import org.osate.aadl2.instance.SystemInstance;
import org.osate.aadl2.instance.SystemOperationMode;
import org.osate.aadl2.modelsupport.modeltraversal.SOMIterator;
import org.osate.aadl2.util.Aadl2Util;
import org.osate.analysis.model.AnalysisElement;
import org.osate.analysis.model.util.AnalysisModelTraversal;
import org.osate.analysis.model.util.AnalysisModelTraversal.Nothing;
import org.osate.analysis.resource.budgets.internal.models.busload.Broadcast;
import org.osate.analysis.resource.budgets.internal.models.busload.Bus;
import org.osate.analysis.resource.budgets.internal.models.busload.BusLoadModel;
import org.osate.analysis.resource.budgets.internal.models.busload.BusOrVirtualBus;
import org.osate.analysis.resource.budgets.internal.models.busload.Connection;
import org.osate.analysis.resource.budgets.internal.models.busload.VirtualBus;
import org.osate.analysis.resource.budgets.internal.models.busload.util.BusloadSwitch;
import org.osate.contribution.sei.sei.DataRate;
import org.osate.contribution.sei.sei.MessageRate;
import org.osate.contribution.sei.sei.Sei;
import org.osate.pluginsupport.properties.PropertyUtils;
import org.osate.result.AnalysisResult;
import org.osate.result.Result;
import org.osate.result.ResultType;
import org.osate.result.util.ResultUtil;
import org.osate.ui.dialogs.Dialog;
/**
* Class for performing "bus load" analysis on a system. Basically it makes sure all the connections and buses
* have enough actual capacity to carry the data loads bound to them.
*
* <p>The format for the returned {@code AnalysisResult} object is as follows:
*
* <p>For the {@code AnalysisResult} object itself:
* <ul>
* <li>analysis = "Bus Load"
* <li>modelElement = {@code SystemInstance} being analyzed
* <li>resultType = SUCCESS
* <li>message = "Bus load analysis of ..."
* <li>diagnostics = empty list
* <li>parameters = empty list
* <li>results = one {@code Result} for each system operation mode
* <ul>
* <li>modelElement = {@code SystemOperationMode} instance object
* <li>resultType = SUCCESS
* <li>message = "" if the SOM is {@code null} or the empty SOM, otherwise "(xxx, ..., yyy)"
* <li>values = empty list
* <li>diagnostics = empty list
* <li>subResults = one {@code Result} for each {@code ComponentInstance} with category of {@code Bus}
* <ul>
* <li>modelElement = {@code ComponentInstance} instance object
* <li>resultType = SUCCESS
* <li>message = The component's name from {@link ComponentInstance#getName()}
* <li>values[0] = The capacity of the bus in KB/s as specified by the SEI::BandwidthCapacity property (RealValue)
* <li>values[1] = The budget of the bus in KB/s as specified by the SEI::BandwidthBudget property (RealValue)
* <li>values[2] = The required budget of the bus in KB/s (the sum of the budgets of all the bound buses and connections) (RealValue)
* <li>values[3] = The actual usage of the bus in KB/s (the sum of the actual usages of all the bound buses and connections) (RealValue)
* <li>values[4] = The number of virtual buses bound to this bus (IntegerValue)
* <li>values[5] = The number of connections bound to this bus (IntegerValue)
* <li>values[6] = The number of broadcast sources bound to this bus (IntegerValue)
* <li>values[7] = The overhead of the bus in bytes as computed by analysis (IntegerValue)
* <li>diagnostics = Diagnostics associated with this bus.
* <li>subResults indexes 0 through (values[4] -1) refer to {@code Result} objects for virtual buses.
* <ul>
* <li>subResults objects for virtual buses are the same as for buses
* </ul>
* <li>subResults indexes values[4] through values[4] + values[5] - 1) refer to {@code Result} objects for connections.
* <ul>
* <li>modelElement = {@code ConnectionInstance} instance object
* <li>resultType = SUCCESS
* <li>message = The connection's name from {@link ConnectionInstance#geName()}
* <li>values[0] = The budget of the connection in KB/s as specified by the SEI::BandwidthBudget property (RealValue)
* <li>values[1] = The actual usage of the bus in KB/s as computed by the multiplying the connection's data size by the connection's message rate. This takes into
* account any messaging overhead by the bus hierarchy the connection is bound to. (RealValue)
* <li>diagnostics = Diagnostics associated with this connection
* <li>subResults = empty list
* </ul>
* <li>subResults indexes values[4] + values[5] through values[4] + values[5] + values[6] - 1 refer to {@code Result} objects for broadcast sources.
* <ul>
* <li>modelElement = {@code ConnectionInstanceEnd} instance object of the feature that is the source of the broadcast
* <li>resultType = SUCCESS
* <li>message = "Broadcast from " + the feature's path in the model as returned by {@link ConnectionInstanceEnd#getInstanceObjectPath()}
* <li>values[0] = The budget of the broadcast source in KB/s (RealValue)
* <li>values[1] = The actual usage of the bus in KB/s (RealValue)
* <li>diagnostics = Diagnostics associated with this broadcast source
* <li>subResults = The {@code Result} objects for the connections that are part of the broadcast (share the same source)
* <ul>
* <li>Same as above
* </ul>
* </ul>
* </ul>
* </ul>
* </ul>
*
* @since 4.0
*/
public final class NewBusLoadAnalysis {
public NewBusLoadAnalysis() {
super();
}
/*
* For now we just have an invoke() method that runs over all the SOMs in the model.
* That is all I need for the JUnit tests.
* Add more methods as they are needed rather than try to guess and just leave
* a bunch of useless junky methods in there.
*/
/**
* Analyze the given system instance, taking all the system operation modes into account.
*
* @param monitor The progress monitor to use, or {@code null} if one is not needed.
* @param systemInstance The system instance to analyze.
* @return The results in a {@code AnalysisResult} object.
*/
public AnalysisResult invoke(final IProgressMonitor monitor, final SystemInstance systemInstance) {
final IProgressMonitor pm = monitor == null ? new NullProgressMonitor() : monitor;
return analyzeBody(pm, systemInstance);
}
private AnalysisResult analyzeBody(final IProgressMonitor monitor, final Element obj) {
if (obj instanceof InstanceObject) {
final SystemInstance root = ((InstanceObject) obj).getSystemInstance();
final AnalysisResult analysisResult = ResultUtil.createAnalysisResult("Bus Load", root);
analysisResult.setResultType(ResultType.SUCCESS);
analysisResult.setMessage("Bus load analysis of " + root.getFullName());
final SOMIterator soms = new SOMIterator(root);
while (soms.hasNext()) {
final SystemOperationMode som = soms.nextSOM();
final Result somResult = ResultUtil.createResult(
Aadl2Util.isPrintableSOMName(som) ? Aadl2Util.getPrintableSOMMembers(som) : "", som,
ResultType.SUCCESS);
analysisResult.getResults().add(somResult);
final BusLoadModel busLoadModel = BusLoadModelBuilder.buildModel(root, som);
analyzeBusLoadModel(busLoadModel, somResult, monitor);
}
monitor.done();
return analysisResult;
} else {
Dialog.showError("Bound Bus Bandwidth Analysis Error", "Can only check system instances");
return null;
}
}
private void analyzeBusLoadModel(final BusLoadModel busLoadModel, final Result somResult,
final IProgressMonitor monitor) {
final Map<AnalysisElement, Result> results = new HashMap<>();
AnalysisModelTraversal.preOrder(busLoadModel, new BandwidthAndResultPreOrderSwitch(results, somResult),
monitor);
if (!monitor.isCanceled()) {
AnalysisModelTraversal.postOrder(busLoadModel, new CapacityAndBudgetPostOrderSwitch(results), monitor);
}
}
private static final class BandwidthAndResultPreOrderSwitch extends BusloadSwitch<Nothing> {
private final Result somResult;
private final Map<AnalysisElement, Result> results;
public BandwidthAndResultPreOrderSwitch(final Map<AnalysisElement, Result> results, final Result somResult) {
this.results = results;
this.somResult = somResult;
}
private void attachResult(final AnalysisElement analysisElement, final String label, final InstanceObject instanceObject) {
final Result newResult = ResultUtil.createResult(label, instanceObject, ResultType.SUCCESS);
results.get(analysisElement.eContainer()).getSubResults().add(newResult);
results.put(analysisElement, newResult);
}
@Override
public Nothing caseConnection(final Connection connection) {
final ConnectionInstance connectionInstance = connection.getConnectionInstance();
attachResult(connection, connectionInstance.getName(), connectionInstance);
return Nothing.NONE;
}
@Override
public Nothing caseBroadcast(final Broadcast broadcast) {
final ConnectionInstanceEnd cie = broadcast.getSource();
attachResult(broadcast, "Broadcast from " + cie.getInstanceObjectPath(), cie);
return Nothing.NONE;
}
@Override
public Nothing caseBusOrVirtualBus(final BusOrVirtualBus bus) {
final ComponentInstance busInstance = bus.getBusInstance();
attachResult(bus, busInstance.getName(), busInstance);
// Increment the data overhead
final EObject parent = bus.eContainer();
final double parentOverhead = parent instanceof BusLoadModel ? 0.0
: ((BusOrVirtualBus) parent).getDataOverhead();
final double localOverheadKBytesps = PropertyUtils
.getScaled(MemoryProperties::getDataSize, busInstance, SizeUnits.KBYTE).orElse(0.0);
bus.setDataOverhead(parentOverhead + localOverheadKBytesps);
return Nothing.NONE;
}
@Override
public Nothing caseBusLoadModel(final BusLoadModel busLoadModel) {
results.put(busLoadModel, somResult);
return Nothing.NONE;
}
}
private static final class CapacityAndBudgetPostOrderSwitch extends BusloadSwitch<Nothing> {
private final Map<AnalysisElement, Result> results;
public CapacityAndBudgetPostOrderSwitch(final Map<AnalysisElement, Result> results) {
this.results = results;
}
private double getOverhead(final EObject analysisElement) {
if (analysisElement instanceof BusOrVirtualBus) {
return ((BusOrVirtualBus) analysisElement).getDataOverhead();
} else {
return getOverhead(analysisElement.eContainer());
}
}
private Result getResult(final AnalysisElement analysisElement) {
return results.get(analysisElement);
}
@Override
public Nothing caseConnection(final Connection connection) {
final ConnectionInstance connectionInstance = connection.getConnectionInstance();
final double dataOverheadKBytes = getOverhead(connection);
final Result connectionResult = getResult(connection);
final double actual = getConnectionActualKBytesps(connectionInstance.getSource(), dataOverheadKBytes);
connection.setActual(actual);
final double budget = PropertyUtils
.getScaled(Sei::getBandwidthbudget, connectionInstance, DataRateUnits.KBYTESPS)
.orElse(0.0);
connection.setBudget(budget);
ResultUtil.addRealValue(connectionResult, budget);
ResultUtil.addRealValue(connectionResult, actual);
if (budget > 0.0) {
if (actual > budget) {
ResultUtil.addError(connectionResult, connectionInstance, "Connection " + connectionInstance.getName()
+ " -- Actual bandwidth > budget: " + actual + " KB/s > " + budget + " KB/s");
}
} else {
ResultUtil.addWarning(connectionResult, connectionInstance, "Connection " + connectionInstance.getName() + " has no bandwidth budget");
}
return Nothing.NONE;
}
@Override
public Nothing caseBroadcast(final Broadcast broadcast) {
final double dataOverheadKBytes = getOverhead(broadcast);
final Result broadcastResult = getResult(broadcast);
// Compute the actual usage and budget requirements
final double actual = getConnectionActualKBytesps(broadcast.getSource(), dataOverheadKBytes);
broadcast.setActual(actual);
// Use the maximum budget from the connections, warn if they are not equal
double maxBudget = 0.0;
boolean unequal = false;
Connection last = null;
for (final Connection c : broadcast.getConnections()) {
final double current = c.getBudget();
maxBudget = Math.max(maxBudget, current);
if (last != null) {
unequal = last.getBudget() != current;
}
last = c;
}
broadcast.setBudget(maxBudget);
ResultUtil.addRealValue(broadcastResult, maxBudget);
ResultUtil.addRealValue(broadcastResult, actual);
if (unequal) {
for (final Connection c : broadcast.getConnections()) {
ResultUtil.addWarning(broadcastResult, c.getConnectionInstance(), "Connection " + c.getConnectionInstance().getName() + " sharing broadcast source "
+ broadcast.getSource().getInstanceObjectPath() + " has budget " + c.getBudget()
+ " KB/s; using maximum");
}
}
return Nothing.NONE;
}
@Override
public Nothing caseBusOrVirtualBus(final BusOrVirtualBus bus) {
final long myDataOverheadInBytes = (long) (1000.0 * bus.getDataOverhead());
final Result busResult = getResult(bus);
// Compute the actual usage and budget requirements
double actual = 0.0;
double totalBudget = 0.0;
for (final VirtualBus vb : bus.getBoundVirtualBuses()) {
actual += vb.getActual();
totalBudget += vb.getBudget();
}
for (final Connection c : bus.getBoundConnections()) {
actual += c.getActual();
totalBudget += c.getBudget();
}
for (final Broadcast b : bus.getBoundBroadcasts()) {
actual += b.getActual();
totalBudget += b.getBudget();
}
bus.setActual(actual);
final ComponentInstance busInstance = bus.getBusInstance();
final double capacity = PropertyUtils
.getScaled(Sei::getBandwidthcapacity, busInstance, DataRateUnits.KBYTESPS)
.orElse(0.0);
final double budget = PropertyUtils.getScaled(Sei::getBandwidthbudget, busInstance, DataRateUnits.KBYTESPS)
.orElse(0.0);
bus.setBudget(budget);
ResultUtil.addRealValue(busResult, capacity);
ResultUtil.addRealValue(busResult, budget);
ResultUtil.addRealValue(busResult, totalBudget);
ResultUtil.addRealValue(busResult, actual);
ResultUtil.addIntegerValue(busResult, bus.getBoundVirtualBuses().size());
ResultUtil.addIntegerValue(busResult, bus.getBoundConnections().size());
ResultUtil.addIntegerValue(busResult, bus.getBoundBroadcasts().size());
ResultUtil.addIntegerValue(busResult, myDataOverheadInBytes);
if (capacity == 0.0) {
ResultUtil.addWarning(busResult, busInstance, getLabel(bus) + busInstance.getName() + " has no capacity");
} else {
if (actual > capacity) {
ResultUtil.addError(busResult, busInstance, getLabel(bus) + busInstance.getName()
+ " -- Actual bandwidth > capacity: " + actual + " KB/s > " + capacity + " KB/s");
}
}
if (budget == 0.0) {
ResultUtil.addWarning(busResult, busInstance, getLabel(bus) + busInstance.getName()
+ " has no bandwidth budget");
} else {
if (budget > capacity) {
ResultUtil.addError(busResult, busInstance, getLabel(bus) + busInstance.getName()
+ " -- budget > capacity: " + budget + " KB/s > " + capacity + " KB/s");
}
if (totalBudget > budget) {
ResultUtil.addError(busResult, busInstance, getLabel(bus) + busInstance.getName()
+ " -- Required budget > budget: " + totalBudget + " KB/s > " + budget + " KB/s");
}
}
return Nothing.NONE;
}
private static String getLabel(final BusOrVirtualBus bus) {
return bus instanceof Bus ? "Bus " : "Virtual bus ";
}
}
// ==== Adapter classes ===
// ==== Helper methods for the visitor ===
/**
* Calculate bandwidth demand from rate & data size
* @param ci The connection instance to calculate for
* @param dataOverheadKBytes The current data overhead from bound buses expressed in KB/s. This is applied to
* the connections message size.
*/
private static double getConnectionActualKBytesps(final ConnectionInstanceEnd cie,
final double dataOverheadKBytes) {
double actualDataRate = 0;
if (cie instanceof FeatureInstance) {
final FeatureInstance fi = (FeatureInstance) cie;
final double datasize = dataOverheadKBytes
+ PropertyUtils.getScaled(MemoryProperties::getDataSize, fi, SizeUnits.KBYTE).orElseGet(
() -> PropertyUtils.getScaled(MemoryProperties::getSourceDataSize, fi, SizeUnits.KBYTE)
.orElse(0.0));
final double srcRate = getOutgoingMessageRatePerSecond(fi);
actualDataRate = datasize * srcRate;
}
return actualDataRate;
}
private static double getOutgoingMessageRatePerSecond(final FeatureInstance fi) {
return PropertyUtils.getScaled(Sei::getDataRate, fi, DataRate.PERSECOND)
.orElseGet(
() -> PropertyUtils.getScaled(Sei::getMessageRate, fi, MessageRate.PERSECOND).orElseGet(() -> {
// Try to get rate from the OUTPUT_RATE record
final Classifier containingClassifier = fi.getContainingClassifier();
return CommunicationProperties.getOutputRate(fi).map(rateSpec -> {
final double maxDataRate = rateSpec.getValueRange().map(rr -> rr.getMaximum()).orElse(0.0);
return rateSpec.getRateUnit().filter(x -> x == RateUnit_FieldType.PERDISPATCH).map(ignore -> {
final double period = getPeriodInSeconds(
((InstanceObject) fi).getContainingComponentInstance());
return period == 0.0 ? 0.0 : maxDataRate / period;
}).orElseGet(() -> {
if (maxDataRate > 0.0) {
return maxDataRate;
} else {
// unit is PERSECOND, but the max data rate is missing, so try the period from the containing classifier
double period = getPeriodInSeconds(containingClassifier);
return period == 0.0 ? 0.0 : 1.0 / period;
}
});
}).orElseGet(
// Cannot get rate from the FeatureInstance, try the period of the containing classifier
() -> {
double period = getPeriodInSeconds(containingClassifier);
return period == 0.0 ? 0.0 : 1.0 / period;
});
}));
}
private static double getPeriodInSeconds(final NamedElement containingClassifier) {
return PropertyUtils.getScaled(TimingProperties::getPeriod, containingClassifier,
TimeUnits.SEC).orElse(0.0);
}
@SuppressWarnings("unused")
private static void print(final PrintWriter pw, final BusLoadModel busLoadModel) {
final Map<AnalysisElement, String> indents = new HashMap<>();
AnalysisModelTraversal.preOrder(busLoadModel, new BusloadSwitch<Nothing>() {
private String getIndent(final AnalysisElement analysisElement) {
return indents.get(analysisElement.eContainer());
}
@Override
public Nothing caseConnection(final Connection c) {
String prefix = getIndent(c);
pw.println(prefix + "Connection " + c.getConnectionInstance().getName());
prefix = prefix + " ";
pw.println(prefix + "Budget = " + c.getBudget() + " KB/s");
pw.println(prefix + "Actual usage = " + c.getActual() + " KB/s");
indents.put(c, prefix);
return Nothing.NONE;
}
@Override
public Nothing caseBroadcast(final Broadcast b) {
String prefix = getIndent(b);
pw.println(prefix + "Broadcast from " + b.getSource().getName());
prefix = prefix + " ";
pw.println(prefix + "Budget = " + b.getBudget() + " KB/s");
pw.println(prefix + "Actual usage = " + b.getActual() + " KB/s");
indents.put(b, prefix);
return Nothing.NONE;
}
@Override
public Nothing caseBus(final Bus b) {
String prefix = getIndent(b);
pw.println(prefix + "Bus " + b.getBusInstance().getName());
prefix = prefix + " ";
pw.println(prefix + "Budget = " + b.getBudget() + " KB/s");
pw.println(prefix + "Actual usage = " + b.getActual() + " KB/s");
pw.println(prefix + "Data overhead = " + (int) (b.getDataOverhead() * 1000.0) + " bytes");
indents.put(b, prefix);
return Nothing.NONE;
}
@Override
public Nothing caseVirtualBus(final VirtualBus b) {
String prefix = getIndent(b);
pw.println(prefix + "Virtual Bus " + b.getBusInstance().getName());
prefix = prefix + " ";
pw.println(prefix + "Budget = " + b.getBudget() + " KB/s");
pw.println(prefix + "Actual usage = " + b.getActual() + " KB/s");
pw.println(prefix + "Data overhead = " + (int) (b.getDataOverhead() * 1000.0) + " bytes");
indents.put(b, prefix);
return Nothing.NONE;
}
@Override
public Nothing caseBusLoadModel(final BusLoadModel m) {
indents.put(m, "");
return Nothing.NONE;
}
}, null);
pw.flush();
}
}