One网络模拟器探索之六:Report类的扩展
一、 report包介绍
report包的功能在前面的文章中已经简单介绍过了,这里侧重于如何扩展report中数据收集的功能。
对于仿真来说结果非常重要,而清晰的展示所需要的结果更是难上加难,虽然默认的report包中已经包含大部分需要统计的信息,但仍然不能够保证所有用户的需要,但ONE的设计者已经考虑到这种情况,所以其设计充分包含了可扩展性。用户可以根据自己的需要进行扩展,只需要继承自Report类,并且在配置文件中增加自定义Report类即可。
二、report成员函数
report类的成员函数主要分为辅助函数(例如,计算一个给定列表的中位数、平均数等)和done以及format等在仿真结束时需要调用的函数(其中done函数是在仿真结束时将需要永久保存下来的数据写到磁盘文件中,format是字符串格式化函数,从而让所有数字型输出具有统一的格式,方便数据分析软件或者程序读取);
三、 仿真预热
report类成员中除了上述可用函数外,还有判断以及处理warmup的函数。
warmup即是预热,启动仿真运行过程,但不会记录此过程中产生的数据,直到warmup规定的时间结束后,report才开始收集数据。这样做的主要意义在于,有些路由策略需要利用历史数据或者先验知识,这时候设定预热时间后,预热时间内的所有数据可以供路由模块收集后作为路由转发决策的依据之一,但这段时间的路由算法效果不会计入到仿真结果中。
四、 report.done()调用时机
此函数功能在前面已经简单介绍,这里主要说明其调用时机。
从源码包中找到ONE程序的入口函数位于core.DTNSim.java文件,其中调用了DTNSimUI类,此类由DTNSimTextUI和DTNSimGUI两个类实现,二者决定仿真过程是运行在命令行中还是在图形界面中,而DTNSimUI中有一个成员函数为done(),此函数的实现为
/** * Runs maintenance jobs that are needed before exiting. */ public void done() { for (Report r : this.reports) { r.done(); } }
而此函数的调用代码位于DTNSimUI的runSim()函数中,此函数运行的即是仿真的整个过程,当此函数实行结束,则整个仿真过程结束。
protected void runSim() { double simTime = SimClock.getTime(); double endTime = scen.getEndTime(); print("Running simulation '" + scen.getName() + "'"); startTime = System.currentTimeMillis(); lastUpdateRt = startTime; while (simTime < endTime && !simCancelled) { try { world.update(); } catch (AssertionError e) { e.printStackTrace(); done(); return; } simTime = SimClock.getTime(); this.update(false); } double duration = (System.currentTimeMillis() - startTime) / 1000.0; simDone = true; done(); this.update(true); // force final UI update print("Simulation done in " + String.format("%.2f", duration) + "s"); }
其中的while循环为仿真过程中所有事件(包含节点位置更新、连接更新、仿真世界时间更新等操作)的调用处。在此while循环结束后(也就是仿真过程结束之后),调用done函数将所有while循环过程中收集到的数据,进行处理后保存到磁盘文件中。
五、仿真过程中的数据收集时机
仿真过程中收集到的数据是在report.done()中实现处理为需要的格式并且保存到磁盘文件中,本部分详细解释report类是如何在仿真过程中收集到这些数据的。
ONE中定义了很多接口,每个接口都由不同的类来实现。虽然接口实现类不同,不能够使用同一个类对象引用进行调用,但是通过接口引用则是理所当然的。这里可以简单认为继承是一系列类的纵向关系,使用父类引用就能够调用到不同子类覆盖的函数,而接口则可以实现不同类的横向调用。
仿真过程中有几类动作是比较重要的,例如,节点间连接的建立和断开、消息的开始传输、正在传输和传输完成等。通常情况下,数据的收集也都是在这几个动作发生的时间点进行收集的,例如,需要保存消息传输路径时,只需要在消息开始传输或者传输完成的时刻,记录下当前节点地址或者编号即可;再如,获取每条消息从生成到传输到目的地的时延,只需要在消息传输完成的时候,获取仿真世界的当前时间,然后减去消息生成时间(消息内部保存),即可得到。
ONE中定义的接口很多,都在core包中,常用的有MessageListener、ConnectionListener、ApplicationListener、UpdateListener、MovementListener等,每个接口中都定义了多个需要实现的函数。
六、 简单实例
本部分以节点缓存平均利用率功能为例对ONE的report包进行扩展。这里首先需要确定节点缓存利用率的决定因素,即所有统计量的最终来源都是仿真过程中可以直接获取到的信息,通过各种公式将原始数据加工成为所需要看到的信息。例如,这里节点缓存利用率=缓存中消息大小之和/缓存大小,缓存大小可以通过DTNHost直接获取到,稍微麻烦一点的就是缓存中的所有节点的大小(当然也不是非常麻烦),但是从这里可以看到可用缓存大小的变化与消息的动作息息相关,当消息从缓存中丢弃时(messageDeleted),可用缓存需要加上所丢弃消息的大小;当节点接收到消息时(messageTransferred),对于接收节点的可用缓存需要减去所接收消息的大小,而发送节点的可用缓存则需要加上所发送消息的大小;当消息生成时,源节点可用缓存需要减去此消息的大小。
明确了节点缓存变化的时机之后,就需要针对每一个动作(或者说接口函数)进行信息更新。这里BufferAvgUtility类需要继承自Report类,并且实现MessageListener中的接口函数。具体实现如下所示:
package report; import core.DTNHost; import core.Message; import core.MessageListener; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * * @author roardragon * @since 2013-05-11 * @version 1.0 * */ public class BufferAvgUtilityReport extends Report implements MessageListener{ public static final String Header="#the average buffer utility for each node during the whole simulation"; public static final String ColumnNames="hostid bufferSize bufferUsedAvg bufferUtilityRatioAvg"; protected Map<DTNHost,Double> avgUtility; protected double prevBufferUsage; protected double bufferUsage; public BufferAvgUtilityReport(){ init(); } @Override protected void init(){ super.init(); avgUtility=new HashMap<DTNHost,Double>(); prevBufferUsage=0; bufferUsage=0; } @Override public void done(){ if(avgUtility.isEmpty()){ write("#There is no message transferred"); } else{ String resultMsg = Header+"\n"+ColumnNames+'\n'; Set<Map.Entry<DTNHost,Double>>set=avgUtility.entrySet(); for(Iterator<Map.Entry<DTNHost,Double>> it=set.iterator();it.hasNext();){ Map.Entry<DTNHost,Double> entry=(Map.Entry<DTNHost,Double>)it.next(); resultMsg+=String.format("%4d",entry.getKey().getAddress())+" " +String.format("%10.2f", this.getBufferTotal(entry.getKey()))+" " +format(this.getBufferUsed(entry.getKey()))+" "//在程序退出前一刻的已使用缓存 +format(entry.getValue())+"\n"; } write(resultMsg); } super.done(); } protected void updateHostBufferUtility(DTNHost host){ prevBufferUsage=avgUtility.containsKey(host)?avgUtility.get(host):0; bufferUsage=host.getBufferOccupancy(); avgUtility.put(host, (0.5*(bufferUsage+prevBufferUsage))); } @Override public void newMessage(Message m) { updateHostBufferUtility(m.getFrom()); } @Override public void messageTransferStarted(Message m, DTNHost from, DTNHost to) { } @Override public void messageDeleted(Message m, DTNHost where, boolean dropped) { if(dropped){ updateHostBufferUtility(where); } } @Override public void messageTransferAborted(Message m, DTNHost from, DTNHost to) { } @Override public void messageTransferred(Message m, DTNHost from, DTNHost to, boolean firstDelivery) { //??if the function is called after the message transferred from the fromhost to tohost??? updateHostBufferUtility(from); updateHostBufferUtility(to); } protected double getBufferUsed(DTNHost host){ return host.getRouter().getBufferSize()-host.getRouter().getFreeBufferSize(); } protected double getBufferTotal(DTNHost host){ return host.getRouter().getBufferSize(); } }
七、配置文件
完成了上面的工作之后,直接运行ONE是不能够产生预期效果的,还需要在配置文件中进行单独的配置,即
#在原来基础上加一 Report.nrofReports = prevValue+1 #此行copy自上一行,并且将id+1,等号右边修改为BufferAvgUtility Report.report[id] = BufferAvgUtility
如果自己实现的report子类,用到了配置文件中的参数,也需要在运行前添加好。
所有上述工作都完成之后就会在仿真结束后,在report.dir指定的目录中找到包含BufferAvgUtility字符串的文件名了,打开之后就是done函数写入的内容了。