2020面向对象设计与构造 第二单元 评测系统开发
面向对象设计与构造 第二单元 评测系统开发
由于第二单元涉及了定时投放,电梯运行模拟,服务时间限制等,笔者决定这次不再依赖其他dalao,自行迭代开发了三次作业的评测系统,在此篇博客记录一下具体实现。
笔者的评测系统秉承不需要对源程序作出任何修改的原则,直接将程序封装为jar包即可。
一、定时投放 + 评测系统(Java)
笔者的定时投放与评测都是基于Java实现,因此建在了一个项目中,设置了两个入口。不得不说,评测机的迭代开发也需要很OO才行。这里主要谈一下第七次作业的评测系统实现。
由于调度本身具有不确定性的特点,笔者决定直接维护电梯的对象来全流程模拟调度所对应的电梯运行状态。
下图是笔者第七次作业评测系统的UML类图。
1. Schedule接口及其实现类
接口Schedule
共有三个实现类,用于存放电梯运行的输出内容:
Arrive
:电梯elevatorName
到达了楼层floor
。Person
:乘客id
在楼层floor
,进入或者离开电梯elevatorName
,使用布尔值type
表示进入与离开。Door
:电梯elevatorName
的门在楼层floor
打开或关闭,使用布尔值type
表示开门与关门。
2. Request接口及其实现类
接口Request
可以类比作业的官方包:
PersonRequest
:与作业完全一致,包含人员编号id
,出发楼层fromFloor
,目标楼层toFloor
。Elevator
:将在下面介绍。
3. RequestList类与定时投放
该类负责处理带有时间戳的输入请求。
(1)输入文件
从文本文件testdata.txt
中读取带有时间戳的请求信息,样例如下。
[1.0]121-FROM--3-TO-18
[2.3]X2-ADD-ELEVATOR-B
[1.2]274-FROM-2-TO--1
[1.5]531-FROM-3-TO-10
[3.0]407-FROM-6-TO-5
(2)字符串处理
这一部分就可以用上第一单元刚学过的正则表达式的知识了,使用split(String)
、Pattern.compile(String)
等熟悉的函数,将时间戳与请求解析出来,转换为对应的数据类型,存入list
中。同时评测机模拟的几部电梯也在该过程创建。
(3)请求排序
考虑到第五次作业的强测输入中,出现了时间戳递减的现象,为了方便本地复现强测的结果,需要对时间戳进行排序。
这里笔者学习了一下Java中lambda表达式的使用方法。
public void sort() {
list.sort((pair1, pair2) -> (int) (pair1.getEle1() - pair2.getEle1()));
}
(4)定时输出模拟请求投放
使用Thread.sleep(long)
方法,可以基本做到零误差输出请求。
4. ScheduleList类
该类负责处理带有时间戳的输出调度。
(1)输入
从文本文件testdata.txt
中读取请求,从标准输入中读取调度结果。
(2)字符串处理
- 使用正则表达式将调度字符串进行拆分,划分至接口
Schedule
对应的实现类对象中。 - 匹配请求的过程中,可以识别出如下几类
WRONG_ANSWER
错误:- 输出格式不正确,出现概率基本为零,但由于输出函数存在线程安全问题,故需要考虑。
- 输出了不存在的电梯id,概率基本为零。
- 输出了不存在的楼层,根据电梯调度算法,有一定概率出现此Bug。
- 输出了超出
int
范围的乘客id,概率基本为零。
(3)调度排序
由于可能存在输出时间乱序的情况,需要对调度进行排序,但是笔者最后的实现忘记使用。
5. Elevator类
该类模拟电梯运行,只在此介绍成员变量。
elevatorName
:电梯的id。type
:电梯的类型。floorMap
:用于统计电梯可以服务的楼层,这里笔者经过研读指导书才知,尽管B类、C类电梯不能服务15层以上的楼层,但是可以运行到这些楼层,即输出ARRIVE-16-B
不能算作错误。floor
:电梯当前所在的楼层,检查服务楼层错误等。load
:当前电梯内部的人数,检查超载问题。past
:用于记录上一次电梯到达、开门、关门的时间,检查输出时间间隔是否小于了规定的移动与服务时间。doorState
:电梯门的状态,可以检查是否开两次门或在开门时移动等错误。inside
:维护电梯内的乘客信息。moveTime
、openTime
、closeTime
:时间限制。maxLoad
:最大载客量。availableTime
:标记电梯请求到来时间,初始电梯该值为0,用于检查是否存在提前调度未请求电梯的情况。runBeforeCall
:初始为false,用于标记是否存在电梯提前被调度的情况。
6. Check类
该类负责按照流程检查电梯调度结果,可以访问电梯的五类检查方法。
requestList
:RequestList
对象。schduleList
:ScheduleList
对象。timeMap
:每个乘客发出请求的时间,用于检查是否有“预测未来”的情况,并统计乘客等待时间。考虑到有些调度,乘客到达目的地后可能再次进入电梯去兜风,因此每次乘客到达目的地后更新乘客等待时间为离开电梯时间。outside
:存放电梯外每个楼层的乘客,乘客进入电梯、离开电梯时均需要维护outside
状态。toMap
:存放每个乘客的目标楼层,最后与outside
进行对比,若有不同,说明存在乘客没有到达目的地的情况。elevatorMap
:所有可用电梯。
7. CheckMessage类
该类所有方法为静态方法,用于输出发现的错误或者正确答案。
在发现错误时,除了两种特殊错误外,评测机会直接System.exit(0)
退出,并且附加电梯调度的所有输出。
在判定结果正确时,会输出性能值,并且附加电梯调度的所有输出。
(1)成员
list
:存放电梯调度程序没有经过任何修改的标准输出。beforeRequest
:布尔值,标记是否发现在请求前就进入电梯的乘客。performance
:性能值,在第七次作业加入,统计电梯运行时间与乘客总等待时间的和。
(2)错误输出方法
笔者对照指导书,一共统计了27种可能出现的错误,其中的25种WRONG_ANSWER
类错误由该Java评测机负责检查。
wrongFormat
:输出格式错误,除了最初手误会产生该类Bug外,几乎不可能出现。floorLimitExceeded
:输出中出现了不存在的楼层,如21
、0
,该类Bug比较常见,多是电梯运行算法有误所致。idNotExist
:输出了不存在的乘客id,出现概率基本为零。elevatorNotExist
:输出了不存在的电梯id,在第六次作业可能出现,概率极低。realTimeLimitExceeded
:运行超时,超出了该Java评测机管辖的范畴。cpuTimeLimitExceeded
:CPU超时,超出了该Java评测机管辖的范畴。inWhenIsClosed
:乘客在门关闭时进入电梯。outWhenIsClosed
:乘客在门关闭时离开电梯。moveTooFast
:电梯运行过快,笔者第五次作业开始时存在这个Bug。serveTooFast
:开关门过快。skipFloor
:电梯跳过了部分楼层,或者连续在同一层“ARRIVE”。moveWhenIsOpen
:电梯门开放时移动。openWhenIsOpen
:电梯门开放时再次开门。closeWhenIsClosed
:电梯门关闭时再次关门。openAtWrongFloor
:电梯在错误楼层开门,如电梯停在3楼,开了2楼的门。closeAtWrongFloor
:电梯在错误楼层关门。serveAtWrongFloor
:电梯在不可服务的楼层开门,第七次作业添加。inBeforeRequest
:乘客在发出请求前进入电梯,由于评测的输入投放与电梯程序存在时间误差,所以这类错误一般都并不是程序问题。笔者采取了激活beforeRequest
标记,输出提示语句的方式,不退出评测程序。runBeforeRequest
:电梯在激活之前运行,也存在时间误差问题。激活电梯类runBeforeRequest
标记来保证只输出一次提示,同样不退出评测程序。enterAtWrongFloor
:乘客进入电梯时的楼层与电梯所在楼层不一致。leaveAtWrongFloor
:乘客离开电梯时的楼层与电梯所在楼层不一致。notOut
:乘客已经进入电梯,不处于外部队列。notIn
:乘客没有在电梯内。overload
:电梯超载,第六次作业添加,第七次作业互测时出现频率最高的Bug——A
类电梯没有修改载客量(没想到这个地方居然很多人会遗漏)。stillWrongFloor
:电梯运行结束,仍有乘客没有到达目的楼层,第六次互测发现了该Bug。stillIn
:电梯运行结束,仍有乘客在电梯里,本质与上相同,但上面更倾向于发现乘客最终所在楼层错误的Bug。notCloseAtLast
:电梯运行结束,最后没有关门。accepted
:电梯运行结束且调度正确,输出Accepted Schedule!
,并附加性能值。printOutput
:输出调度结果,在发现与时间误差无关Bug,或结果正确时调用。
二、运行脚本 + 调度时间监测(Python)
笔者主要使用了Python的subprocess
模块,用来实现进程的生成,管道的连接。
1. 封装
笔者将投放系统包装为Input.jar
,评测机包装为Check.jar
,作业包装为Homework.jar
。
2. subprocess.Popen类
subprocess.Popen
类用于在一个新进程中执行一个子程序。
- 使用
subprocess.Popen
构造方法,启动java投放与电梯系统,用subprocess.PIPE
作为管道连接两者的标准输出与标准输入。 - 调用电梯调度子进程的
communicate()
方法,与该Python进程进行交互,设置timeout为200。- 该方法将返回一个元组,包括子进程标准输出与标准异常的字节流。
- 若子进程运行时间超出timeout限制,会抛出
TimeoutExpired
异常,在异常处理时输出超时信息即可。
- 需要对子进程的标准输出与标准异常字节流按照UTF-8格式解码,才能够正常显示结果。
- 创建评测进程同理,就不再赘述。
更多的操作可以参考链接subprocess — Subprocess Management、python:subprocess模块。
3. time模块监视程序真实运行时间
使用time.time()
方法获取当前执行时间,分别在交互前与交互后调用,可以得到基本准确的程序运行时间,在第五次和第六次作业中用来作为性能的参考指标。
4. 部分代码
import subprocess
import time
Tmax = 200
child1 = subprocess.Popen("java -jar Input.jar", stdout=subprocess.PIPE)
try:
command = "java -jar Homework.jar"
child2 = subprocess.Popen(command, stdin=child1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
start_time = time.time()
lists = child2.communicate(timeout=Tmax)
out_list = lists[0]
err_list = lists[1]
end_time = time.time()
# ......
except Exception:
# ......
finally:
# ......
5. 运行
笔者在外部的shell脚本中添加了210秒的熔断,严格限制运行时间,避免Python程序出现意外情况不能正常结束。
timeout 210 python runElevator.py > result.txt
三、CPU时间监测(VisualVM)
笔者经过很长时间的搜索,也未能找到可以精确获取进程使用CPU时间的方法。除了JProfiler外,笔者经过dalao的推荐,使用VisualVM来对电梯调度各个线程的CPU情况进行监视,常见的轮询问题都会暴露出来。
链接:VisualVM
- 进入后,可以看到如下界面,可以从左侧栏选中要监视的作业Jar包,使用线程插件可以实现监视CPU时间的目的。
- 运行评测脚本,获取
Homework.jar
的实时情况,进入Threads一栏,可以监视每个线程的CPU情况,笔者为了方便区分各个线程,给每个线程进行了命名。除了输入线程外,用户程序内所创建的线程不应当出现绿色Running状态,否则证明存在CPU轮询的行为。(由于VisualVM的监视粒度不是非常细,一些算法运行时间的对比并无法精准捕获)- 通过这种方法解决了第七次作业中,困扰笔者数天的CPU时间不稳定问题,之前一直误判为算法复杂度过高。
- 在互测中,笔者监视了两轮同房间内成员的CPU使用情况,发现有程序的CPU时间达到了5秒左右,但是多次运行并没有出现超出10秒限制的情况。
VisualVM性能分析功能非常强大,除监视外,还包括快照、转储等,常见操作可以参考链接:JVisualVM 简介、工具:VisualVM。
IDEA本身也有VisualVM Launcher插件,但是在这一单元,由于需要精准把控请求投放时间,所以在IDEA内监视的效果并不理想。
四、版本历程
第五次作业刚刚开发评测机,存在很多Bug,在此匿名感谢各位使用笔者评测系统的dalao们,帮助笔者完成维护,同时也感谢几位dalao贡献的随机数据生成以及多程序测试脚本,让评测系统的功能更加丰富。
附上版本记录与一些评测运行的结果显示。
- 版本记录:
Version Log:
v1.0: 最初版本,支持单部电梯运行评测。
v1.1: 解决了Check.jar对应输入文件路径错误的问题。
v1.2: 解决了格式错误或楼层错误时,输出程序结果不完整的问题。
v1.3: 解决了运行未达到时限报告超时信息的问题。
v1.4: 增加显示程序真实运行时间。
v1.4.4: 修复了时间计算的浮点误差Bug。
v1.5: 经过与数据生成系统的整合,实现自动生成多组数据评测。
v2.0: 更改为支持不确定数量电梯的运行评测。
v2.1: 增加显示程序在运行时限内发生的异常或错误。
v2.2: 经过与数据生成系统的整合,实现自动生成多组数据评测。
v3.0: 更改为支持可动态增加电梯以及不同电梯类型的运行评测。
v3.1: 经过与数据生成系统整合,实现自动生成多组数据评测。
- 单人评测:
-------Real Time Used-----------
Your program used 1.6911s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 2.3780s.
Your real outputs are:
[ 0.0710]OPEN-1-B
[ 0.4860]IN-666-1-B
[ 0.4860]CLOSE-1-B
[ 0.9880]ARRIVE-2-B
[ 0.9880]OPEN-2-B
[ 0.9880]OUT-666-2-B
[ 1.3900]CLOSE-2-B
- 多人评测:
Alterego
-------Real Time Used-----------
Real time limit exceeded.
-------Auto Check Result--------
Real time limit exceeded.
Archer
-------Real Time Used-----------
Your program used 58.5679s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 787.8840s.
Assassin
-------Real Time Used-----------
Your program used 72.7500s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 893.2930s.
Berserker
-------Real Time Used-----------
Your program used 74.6148s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 981.2520s.
Caster
-------Real Time Used-----------
Your program used 67.9409s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 925.2090s.
Lancer
-------Real Time Used-----------
Your program used 64.1071s.
-------Auto Check Result--------
Elevator A was overloaded: [30.3110]IN-23-15-A
Your real outputs are:
[ 2.1800]ARRIVE-2-B
[ 2.2810]ARRIVE-2-C
[ 2.6830]ARRIVE-3-B
......
Rider
-------Real Time Used-----------
Your program used 69.6644s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 810.9880s.
Saber
-------Real Time Used-----------
Your program used 69.6151s.
-------Auto Check Result--------
Accepted Schedule! Total running time and waiting time is 1025.3150s.
五、评测系统的不足之处
由于笔者在开发此评测系统之前没有太多Python的基础,因此该评测系统存在诸多缺陷,或待提高的地方。
- 评测效率较低,没有设置多线程或多进程运行。
- Java部分方法复杂度过高,依然需要多培养面向对象的编程习惯。
- 没有发现捕获RE、CPU_TLE的办法,且本地一些时间相关结果偶与课程官网评测机不一致。
- 未进行图形界面的包装,大数据统计等,方便用户查看各项结果会让评测系统锦上添花。
六、写在最后
OO的旅途已经过半了,回望半个学期的磨练,传言中6系最难的OO,对我们每个人来说都应有着或浅或深的意义。
-
OO可能是压在我们肩上的一块石头,为了追赶DDL而应付,作业有效便万岁。
-
OO可能是驱使我们竞争本能的一块鲜肉,为了性能上的遥遥领先,熬夜优化架构,思索算法。
-
OO可能是一条没有尽头的赛道,为了磨练心性,我们努力地奔跑着,提升自己。
-
OO可能是引领我们走向更宽广道路的一盏明灯,为了追寻更多的知识,无限拓展自己的技术栈,享受编程的乐趣。
OO其实更像是一面镜子,照出了我们内心对待求知的态度。
当笔者看见自己的评测系统成功运行,发现自己或他人的Bug时,那种喜悦是发自内心的、单纯来自于兴趣的。
但笔者也不得不承认,目前自己还没有从“分数”走出,每次互测开始时,都要先看看房内的代码是不是写得清爽,长得像不像A Room
;每次强测公布结果前,都要感受那几分钟紧张的心跳。
OO只能再陪伴我们剩余的半个学期,但真正的路还很长。
谨以此来督促尚不成熟的自己。