一、背景
在文章AADL 端到端流延迟分析示例项目 Latency-case-study 简述的 “第八章 进行系统的端到端流延迟分析” 中,遇到了这样的一个问题:对分布式系统的端到端流延迟进行分析时,没有生成流延迟分析报告,并且错误日志提示,出现内部错误“空指针异常”。
上述文章给出的解决方案是使用旧版本的OSATE,但是这并未真正地解决问题。将OSATE项目的源代码下载,进行调试和分析,结果如下:
在涉及到总线的端到端延迟分析过程中,只将“在end to end flow中涉及的连接” 添加到指定数据结构connectionsToContributors。如果一个连接绑定到了某总线,却未在某个end to end flow中声明,那么该连接便不会添加到connectionsToContributors中,由此导致connectionsToContributors缺少关于该连接的数据,导致空指针异常。
本文在最后给出了修改后的代码,以下为详细内容:
二、问题回顾
对于Latency-case-study项目,在生成integration.software_distributed实例之后,启用流延迟分析,发现并未有任何报告被生成:
错误日志(Error Log)显示如下:
java.lang.NullPointerException: Cannot invoke "org.osate.analysis.flows.model.LatencyContributor.addSubContributor(org.osate.analysis.flows.model.LatencyContributor)" because "latencyContributor" is null
at org.osate.analysis.flows.FlowLatencyAnalysisSwitch.fillInQueuingTimes(FlowLatencyAnalysisSwitch.java:1465)
at org.osate.analysis.flows.FlowLatencyAnalysisSwitch.invokeOnSOM(FlowLatencyAnalysisSwitch.java:1052)
at org.osate.analysis.flows.handlers.CheckFlowLatency.analyzeInstanceModel(CheckFlowLatency.java:158)
at org.osate.ui.handlers.AbstractInstanceOrDeclarativeModelReadOnlyHandler.analyzeInstanceModelInMode(AbstractInstanceOrDeclarativeModelReadOnlyHandler.java:126)
at org.osate.ui.handlers.AbstractInstanceOrDeclarativeModelReadOnlyHandler.doAaxlAction(AbstractInstanceOrDeclarativeModelReadOnlyHandler.java:101)
at org.osate.ui.handlers.AbstractInstanceOrDeclarativeModelModifyHandler.processAaxlAction(AbstractInstanceOrDeclarativeModelModifyHandler.java:58)
at org.osate.ui.handlers.AbstractAaxlHandler.actionBody(AbstractAaxlHandler.java:181)
at org.osate.ui.handlers.AaxlReadOnlyHandlerAsJob$ActionAsJob.runInWorkspace(AaxlReadOnlyHandlerAsJob.java:115)
at org.eclipse.core.internal.resources.InternalWorkspaceJob.run(InternalWorkspaceJob.java:43)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
可以看到,内部错误的主要原因是因为“ "latencyContributor" is null ”
三、问题详细分析
为什么会产生空指针异常,接下来将自上而下地对问题进行分析:
在整个分析过程中,对于连接延迟的计算,涉及到了一个Map对象connectionsToContributors,其定义如下:(FlowLatencyAnalysisSwitch.java : line 111)
/*
* Map from (bus component, connection instance) -> latency contributor. We need this because the queuing latency is computed at the end of process
* after all the transmission times are computed. So we add the queuing latency to this latency contributor for the connection instance
* bound to the given bus. This may contain entries that in the end are not interesting because virtual buses can be bound to
* other virtual buses that are eventually bound to real bus. We chases down all those layers.
*/
private final Map<Pair<ComponentInstance, ConnectionInstance>, LatencyContributor> connectionsToContributors = new HashMap<>();
在流延迟分析过程中,会将端到端流中的元素按照类型依次添加到对应的Map中(其中一个Map用来存储关于连接的组件,即上面提到的connectionsToContributors),代码如下:
(FlowLatencyAnalysisSwitch.java : mapFlowElementInstance() : line 187)
public LatencyReportEntry analyzeLatency(EndToEndFlowInstance etef, SystemOperationMode som,boolean asynchronousSystem) {
LatencyReportEntry entry = new LatencyReportEntry(etef, som, asynchronousSystem,
report.isMajorFrameDelay());
for (FlowElementInstance fei : etef.getFlowElements()) {
mapFlowElementInstance(etef, fei, entry);
}
// Issue 1148 moved this somewhere else
// entry.finalizeReportEntry();
return entry;
}
上述代码中,会遍历所有的端到端流etef(end to end flow),将每个端到端流fei(Flow Element Instance)作为参数传递给函数mapFlowElementInstance(etef, fei, entry),而该函数会间接调用processSamplingAndQueuingTimes(),从而根据端到端流的组成元素,向Map connectionsToContributors 中添加元素(只有在端到端流中声明的元素才会被添加),代码如下:(FlowLatencyAnalysisSwitch.java : processSamplingAndQueuingTimes():line 896)
//通过下面函数获取总线的周期,对于周期性总线和非周期性总线,延迟的计算方式不同。
double period = hasPeriod.contains(cc) ? PropertyUtils.getScaled(
TimingProperties::getPeriod, boundBusOrRequiredClassifier, TimeUnits.MS).orElse(0.0) : 0.0;
// 如果是周期性总线,增加采样延迟,将排队延迟设为0
if (period > 0) {
// add sampling latency due to the protocol or bus being periodic
LatencyContributor samplingLatencyContributor = new LatencyContributorComponent(
boundBusOrRequiredClassifier, report.isMajorFrameDelay());
samplingLatencyContributor.setBestCaseMethod(LatencyContributorMethod.SAMPLED_PROTOCOL);
samplingLatencyContributor.setWorstCaseMethod(LatencyContributorMethod.SAMPLED_PROTOCOL);
samplingLatencyContributor.setSamplingPeriod(period);
latencyContributor.addSubContributor(samplingLatencyContributor);
// add queuing latency: always zero in this case
LatencyContributor queuingLatencyContributor = new LatencyContributorComponent(
boundBusOrRequiredClassifier, report.isMajorFrameDelay());
queuingLatencyContributor.setBestCaseMethod(LatencyContributorMethod.QUEUED);
queuingLatencyContributor.setWorstCaseMethod(LatencyContributorMethod.QUEUED);
queuingLatencyContributor.setMinimum(0.0);
queuingLatencyContributor.setMaximum(0.0);
latencyContributor.addSubContributor(queuingLatencyContributor);
}
//如果是非周期性总线,需要在后续过程中计算排队延迟,将其存储于Map中
else {
/*
* Issue 1148
*
* if "boundBus" is really a bound component instance, and not a required component classifier,
* then we remember the bus as asynchronous. Later in fillInQueuingTimes() we go through this list,
* and then find all the connection instances bound to this bus. For each connection,
* we compute the sum of the max transmission times of the OTHER connections bound to the bus. This
* we set as the worse case queuing time. (Best case is 0.)
*
* We also remember the bus--connection pair that needs the queuing latency by storing its latency contributor.
*/
if (bindingConnection != null) {
final ComponentInstance boundBus = (ComponentInstance) boundBusOrRequiredClassifier;
/* Set the bus order and then add it to the ordered set */
if (!busOrder.containsKey(boundBus)) {
busOrder.put(boundBus, nextBusId++);
asyncBuses.add(boundBus);
}
connectionsToContributors.put(new Pair<>(boundBus, bindingConnection), latencyContributor);
}
根据上述代码:对于周期性总线和非周期性总线,其延迟的计算方式不同。
对于周期性总线,计算它的采样延迟,范围为[0, period]。
对于非周期性总线,其最小延迟为0,最大延迟按照绑定到该总线的其他连接的数据传输时间之和进行计算。
因此,对于端到端流延迟分析函数的调用代码,涉及到了一个名为 fillInQueuingTimes()的函数,其源码如下:(FlowLatencyAnalysisSwitch.java : fillInQueuingTimes():line 1412)
private void fillInQueuingTimes(final SystemInstance system) {
// Nothing to do if there are no asynchronous buses
if (!asyncBuses.isEmpty()) {
// Get all the connections bound to a bus and group them together by the bus they are bound to
final Map<ComponentInstance, Set<ConnectionInstance>> sortedConnections = sortBoundConnections(system);
/*
* Go through the list of all the asynchronous buses
*/
for (final NamedElement ne : asyncBuses) {
// only proceed if it is a bus instance and not a classifier (from Required_Virtual_Bus_Class)
if (ne instanceof ComponentInstance) {
final ComponentInstance bus = (ComponentInstance) ne;
// Get all the connections bound to that bus
final Set<ConnectionInstance> boundConnections = sortedConnections.getOrDefault(bus,Collections.emptySet());
// Get all the transmission times and compute the total
double totalTime = 0.0;
final Map<ConnectionInstance, Double> transmissionTimes = new HashMap<>();
for (final ConnectionInstance ci : boundConnections) {
final Double time = computedMaxTransmissionLatencies
.getOrDefault(new Pair<ComponentInstance, ConnectionInstance>(bus, ci), 0.0);
transmissionTimes.put(ci, time);
totalTime += time;
}
/*
* Go through the list of connections again, and subtract the time associated
* with the current connection to find the max waiting time for each connection.
* (That each for each connection ci, we will have the sum of all the times
* for the _other_ connections bound to same bus. This gives us the max
* time that connection ci may have to wait to use the bus.)
*/
for (final ConnectionInstance ci : boundConnections) {
final Double ciTime = transmissionTimes.get(ci);
final double maxWaitingTime = totalTime - ciTime;
// Finally we can stick this into the latency contributor
final LatencyContributor latencyContributor = connectionsToContributors.get(new Pair<>(bus, ci));
final LatencyContributor queuingLatencyContributor = new LatencyContributorComponent(bus,report.isMajorFrameDelay());
queuingLatencyContributor.setBestCaseMethod(LatencyContributorMethod.QUEUED);
queuingLatencyContributor.setWorstCaseMethod(LatencyContributorMethod.QUEUED);
queuingLatencyContributor.setMinimum(0.0);
if (report.isDisableQueuingLatency()) {
// Hide the queuing time
queuingLatencyContributor.setMaximum(0.0);
queuingLatencyContributor.reportInfo("Ignoring queuing time of " + maxWaitingTime + "ms");
} else {
// Report the queuing time
queuingLatencyContributor.setMaximum(maxWaitingTime);
}
latencyContributor.addSubContributor(queuingLatencyContributor);
// add the sampling latency
LatencyContributor samplingLatencyContributor = new LatencyContributorComponent(
bus, report.isMajorFrameDelay());
samplingLatencyContributor.setBestCaseMethod(LatencyContributorMethod.SAMPLED_PROTOCOL);
samplingLatencyContributor.setWorstCaseMethod(LatencyContributorMethod.SAMPLED_PROTOCOL);
samplingLatencyContributor.setSamplingPeriod(0.0);
latencyContributor.addSubContributor(samplingLatencyContributor);
}
}
}
}
}
在上述代码中,首先会获取一条总线上绑定的所有连接。
final Set<ConnectionInstance> boundConnections = sortedConnections.getOrDefault(bus,Collections.emptySet());
然后依次遍历每条连接,在Map connectionsToContributors中定位到这个连接并向其中添加延迟贡献。
final LatencyContributor latencyContributor = connectionsToContributors.get(new Pair<>(bus, ci));
......
latencyContributor.addSubContributor(queuingLatencyContributor);
异常出现的原因:如果一个连接没有被包含于某个端到端流,但是却被绑定到总线上,那么Map connectionsToContributors将不会添加此连接,故而在遍历绑定到总线的连接时,会出现空指针异常。
四、解决方案
修复此问题的方法有2种:
1. 完整地建模,声明系统中存在的所有端到端流,以保证不会有连接被遗漏。
2. 设定总线周期,避免排队延迟情况的出现(即绕过排队延迟计算)。
五、修复模型并测试
两种方案的代码如下:(在 integration.aadl 的 integration.software_distributed 部分)
system implementation integration.software_distributed extends integration.software_generic
subcomponents
s1_cpu : processor latency_cs::platform::generic_cpu;
s2_cpu : processor latency_cs::platform::generic_cpu;
p_cpu : processor latency_cs::platform::generic_cpu;
a_cpu : processor latency_cs::platform::generic_cpu;
s_p_bus: bus latency_cs::platform::generic_bus;
p_a_bus : bus latency_cs::platform::generic_bus;
connections
b0 : bus access s1_cpu.net <-> s_p_bus;
b1 : bus access s2_cpu.net <-> s_p_bus;
b2 : bus access p_cpu.net <-> s_p_bus;
b3 : bus access p_cpu.net <-> p_a_bus;
b4 : bus access a_cpu.net <-> p_a_bus;
-- 为解决流延迟分析过程中出现的空指针异常而增添的代码--------------
-- 方法1.完整地建模,声明系统中存在的所有端到端流(在flows中声明)
flows
etef2 : end to end flow s1.sensor_source -> c0 -> p.sink0;
etef3 : end to end flow s2.sensor_source -> c1 -> p.sink1;
-- ---------------------------------------------------------------
properties
-- 为解决流延迟分析过程中出现的空指针异常而增添的代码----------
-- 方法2.设定总线周期(在properties中设定)
-- Period => 5ms applies to s_p_bus, p_a_bus;
-- -----------------------------------------------------------
actual_processor_binding => (reference (s1_cpu)) applies to s1;
actual_processor_binding => (reference (s2_cpu)) applies to s2;
actual_processor_binding => (reference (p_cpu)) applies to p;
actual_processor_binding => (reference (a_cpu)) applies to a;
actual_connection_binding => (reference (s_p_bus)) applies to c0;
actual_connection_binding => (reference (s_p_bus)) applies to c1;
actual_connection_binding => (reference (p_a_bus)) applies to c2;
-- protocol that applies to the connections
required_virtual_bus_class => (classifier (latency_cs::platform::generic_protocol)) applies to c0, c1, c2;
end integration.software_distributed;
1. 完整地建模,声明系统中存在的所有端到端流,实例化后进行分析:
成功生成流延迟分析报告:
报告内容如下所示:
2. 设定总线周期,避免排队延迟情况的出现
成功生成流延迟分析报告:
报告内容如下所示:
六、相关链接
OSATE 开发者文档,展示了如何部署环境并拉取OSATE源码,如下:
Setting up an OSATE development environment — OSATE 2.13.0 documentation
OSATE 项目的源码也可以直接在github下载:
osate/osate2: Open Source AADL2 Tool Environment (github.com)
latency-case-study项目的源码也可以直接在github下载:
examples/latency-case-study at master · osate/examples (github.com)
修改后的完整的项目代码可在此处下载。
【免费】latency-case-study项目修改版(资源-CSDN文库)
如有不当或错误之处,恳请您的指正,谢谢!!!