PAT/PTA Team Blog


  • 首页

  • 归档

记一次评测机性能调优

发表于 2018-11-15

0x00 起因

首先,这是一篇迟到了非常久的 blog,这个性能优化早在今年 6 月份就已经上线,这篇文章将细致的梳理一下这次调优的过程。

调优的对象就是 PTA 使用的评测机,该代码已经开源,原本是 quark 学长维护的版本,而现在在我们团队的 git 仓库当中,分别是 ljudge 和 lrun 。

首先简单讲一下评测机的部署方案:

  • 每个评测机处于一个 docker 容器当中
  • 每台云主机可以 host 多个 docker 容器
  • 每个评测机都是单线程工作的,占用一个核心
  • 每台云主机根据自身核心数来决定最大承担 judger 容器的个数

judger 的性能问题,是在一次承担考试的时候发现的。我因为观察到评测队列堆积,所以进行了一次扩容,将云主机上的容器个数调整到了最大值。但不幸的是,从监控来看,评测队列的消化速度几乎没有增加。之后也遇到过几次同样的情况,所以最终决定进行一次性能问题的排查。

0x01 排查

准备一些脚本:

entrypoint.sh

1
2
3
4
5
6
7
#!/bin/bash

cd /app/ljudge/examples/a-plus-b
for ((i=1;i<=100;i++));
do
ljudge -u a.c -i 1.in -o 1.out -i 2.in -o 2.out 2>/dev/null >/dev/null
done

在容器当中 judge 一个最简单的 a+b 的程序,循环 100 次,放进容器当中备用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

CMD='docker run --rm --privileged --entrypoint=/app/ljudge/examples/a-plus-b/entrypoint.sh --cpus=1 pintia/ljudge-docker:benchmark'

echo '1 container'
time bash -c "${CMD}"

echo '2 container'
time bash -c "${CMD} & ${CMD}"

echo '3 container'
time bash -c "${CMD} & ${CMD} & ${CMD}"

echo '4 container'
time bash -c "${CMD} & ${CMD} & ${CMD} & ${CMD}"

echo '5 container'
time bash -c "${CMD} & ${CMD} & ${CMD} & ${CMD} & ${CMD}"

echo '6 container'
time bash -c "${CMD} & ${CMD} & ${CMD} & ${CMD} & ${CMD} & ${CMD}"

分别启动 1 ~ 6 个容器,这里的 --entrypoint 就是指定刚才放进容器当中的脚本,测试所需要的时间(脚本写的有点 low 见谅)

在一台 8 核心的机器上,得到了测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

1 container
real 0m39.932s
user 0m0.020s
sys 0m0.012s

2 container
real 0m56.090s
user 0m0.032s
sys 0m0.008s

3 container
real 1m11.888s
user 0m0.016s
sys 0m0.004s

4 container
real 1m30.698s
user 0m0.060s
sys 0m0.020s

5 container
real 1m47.885s
user 0m0.028s
sys 0m0.012s

6 container
real 2m2.968s
user 0m0.048s
sys 0m0.024s

很明显可以看出,在一台核心数足够的机器上启用多个 judger,消耗的时间竟然是线性增加的,也就是说,在单机上通过增加 judger 实例数进行的扩容效果几乎没有,多个 judger 之间势必有资源发生了抢占。

0x02 继续排查

那确认了这种部署模式的问题之后,接下去就需要找是什么东西在多个 judger 之间发生了竞争。排查持续了一天,期间使用了各种工具比如 perf,strace 等等,但都没有确定问题所在。但一个偶然的尝试发现了一些问题。

通过观察,可以发现 lrun 进程频繁的 sleep 在 copy_net_ns 这个地方。确认了一下代码,每一次评测都会创建一个新的 network namespace。所以根据表现,大胆猜测 network namespace creation 应该是一个比较耗时而且无法并发的调用。但 judger 确实需要进行网络隔离,所以在这里我尝试绕过这个行为。

因为 judger 同一个时间只会评测一个程序,也就是说同一个时间至多只会存在一个沙盒,所以我可以事先创建好一个已经被隔离开的 network namespace,而让每次评测是将这个 network namespace 设置到当前沙盒上,且不需要考虑多个沙盒的情况。

带着这个思路简单增加了一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// switch to existed netns for better performance if needed
if ((clone_flags & CLONE_NEWNET) == CLONE_NEWNET && arg.reuse_netns) {
INFO("try to reuse network namespace");
int netns_fd = open(netns::LRUN_NETNS_PATH, O_RDONLY);
if (netns_fd < 0) {
// create reusable net ns
INFO("create reusable network namespace")
int result = system(netns::LRUN_NETNS_CMD);
if (result != 0) {
ERROR("can not create network namespace");
return -3;
} else {
netns_fd = open(netns::LRUN_NETNS_PATH, O_RDONLY);
if (netns_fd < 0) {
ERROR("can not open reusable network namespace");
return -3;
}
}
}

INFO("set network ns to %s", netns::LRUN_NETNS_PATH);
// older glibc does not have setns
if (syscall(SYS_setns, netns_fd, CLONE_NEWNET)) {
ERROR("can not set network namespace");
return -3;
};
close(netns_fd);

clone_flags ^= CLONE_NEWNET;
}

如果没有 reusable network namespace,则创建一个新的,如果有并且可用,就将当前沙盒的 network namespace 设置成它。

0x03 验证

准备脚本:

entrypoint-netns.sh

1
2
3
4
5
6
7
#!/bin/bash

cd /app/ljudge/examples/a-plus-b
for ((i=1;i<=100;i++));
do
ljudge --reuse-netns -u a.c -i 1.in -o 1.out -i 2.in -o 2.out 2>/dev/null >/dev/null
done

在之前的测试脚本当中加入测试优化过的 judger 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash

CMD='docker run --rm --privileged --entrypoint=/app/ljudge/examples/a-plus-b/entrypoint.sh --cpus=1 pintia/ljudge-docker:benchmark'
echo 'No reuse netns, 1 container'
time bash -c "${CMD}"

echo 'No reuse netns, 2 container'
time bash -c "${CMD} & ${CMD}"

echo 'No reuse netns, 3 container'
time bash -c "${CMD} & ${CMD} & ${CMD}"

echo 'No reuse netns, 4 container'
time bash -c "${CMD} & ${CMD} & ${CMD} & ${CMD}"

echo 'No reuse netns, 5 container'
time bash -c "${CMD} & ${CMD} & ${CMD} & ${CMD} & ${CMD}"

echo 'No reuse netns, 6 container'
time bash -c "${CMD} & ${CMD} & ${CMD} & ${CMD} & ${CMD} & ${CMD}"

CMD_NETNS='docker run --rm --privileged --entrypoint=/app/ljudge/examples/a-plus-b/entrypoint-netns.sh --cpus=1 pintia/ljudge-docker:benchmark'

echo 'Reuse netns, 1 container'
time bash -c "${CMD_NETNS}"

echo 'Reuse netns, 2 container'
time bash -c "${CMD_NETNS} & ${CMD_NETNS}"

echo 'Reuse netns, 3 container'
time bash -c "${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS}"

echo 'Reuse netns, 4 container'
time bash -c "${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS}"

echo 'Reuse netns, 5 container'
time bash -c "${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS}"

echo 'Reuse netns, 6 container'
time bash -c "${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS} & ${CMD_NETNS}"

对照测试!得到结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
root@judger-test:~# bash benchmark.sh 
No reuse netns, 1 container
real 0m40.134s
user 0m0.044s
sys 0m0.020s

No reuse netns, 2 container
real 0m56.876s
user 0m0.068s
sys 0m0.044s

No reuse netns, 3 container
real 1m13.356s
user 0m0.032s
sys 0m0.020s

No reuse netns, 4 container
real 1m28.670s
user 0m0.072s
sys 0m0.040s

No reuse netns, 5 container
real 1m44.280s
user 0m0.104s
sys 0m0.068s

No reuse netns, 6 container
real 2m2.403s
user 0m0.104s
sys 0m0.072s

Reuse netns, 1 container
real 0m20.290s
user 0m0.040s
sys 0m0.016s

Reuse netns, 2 container
real 0m20.003s
user 0m0.076s
sys 0m0.036s

Reuse netns, 3 container
real 0m20.299s
user 0m0.100s
sys 0m0.060s

Reuse netns, 4 container
real 0m20.947s
user 0m0.088s
sys 0m0.068s

Reuse netns, 5 container
real 0m21.493s
user 0m0.032s
sys 0m0.020s

Reuse netns, 6 container
real 0m23.164s
user 0m0.108s
sys 0m0.056s

可以看到优化之后,在同一台机器上开核心数以内的 judger 实例,总时长并不会有明显增加,也就是说扩容的目标达到了。

至此这次 judger 的性能优化就算是完成了~

主观题学生互评题目集

发表于 2018-09-10

背景

目前在 PTA 中主观题作为单独的一类题目,学生作答后需要老师自行批改,给出分数与可选的评语,并进入题目集的排名列表中。

互评题目集

互评题目集是近日来新加入的功能,参与作业的学生可以在提交作业后,由系统自动分配互评任务,评价打分后自动计算分数,并自动将需要仲裁的作业提醒老师进行仲裁。

创建互评题目集

互评题目集将会是一个全新的题目集类型,此类型只能加入一道主观题。

  1. 作业截止时间

    系统将在作业截止时间自动进行互评任务分发,之后学生需要进入系统对每个任务进行匿名互评。

  2. 互评截止时间

    到达互评截止时间后,系统会根据互评结果计算每份作业的分数。根据互评结果与老师的设置,系统会自动将分数偏差过大、分数过高或过低、评分人数不足等各种有异常的作业单独标注,等待教师仲裁。

  3. 反馈

    启用反馈后,被评分的学生可以对每一个评价进行 1-5 的打分,反馈结果将影响评分者的评分权重。

  4. 申诉

    启用申诉后,学生可以在系统中向申诉本次作业并填写理由,此份作业将单独列出,教师可以选择回复申诉,或再次进行仲裁修改分数。

  5. 评分规则

    教师可以配置是否允许未提交作业的学生参与互评。

    • “用户组中所有用户参与互评,互评成绩单独计算”

      在此规则下教师需要为互评独立设置分数,学生即使未提交作业,如果完成互评也可以获得相应的分数。

    • ”只有提交了作业的人参与互评,惩罚分从作业得分中扣除“

      在此规则下,未提交作业的学生无法参与互评,即在本题目集无法得到任何分数。此时互评完成不佳的学生(未完成或给分偏差过大导致扣分)将直接从作业中扣除分数。

  6. 每组互评份数

    每份作业需要被几位学生评价。当评价人数不足时(学生数量不足或提交了互评的人数不足),作业会被单独标注并等待教师仲裁。

  7. 未完成互评惩罚

    每一份未完成的互评任务扣分分数。

  8. 仲裁阈值

    分数差:每份作业得到的互评分数差距超过满分的百分之多少后,会自动提示教师进行仲裁。同时超过分数差的给分将会导致互评学生扣分。具体规则将在后续介绍。

    得分上限(下限):某份作业的计算后成绩超过了上限(或低于下限)后,会提示教师进行仲裁人工确认分数。

添加用户组

互评任务将在同一个用户组中进行分配,教师可以添加多个用户组,各个用户组在互评中互相独立,任务不会跨越用户组分配。

为了减少学生可能猜出互评的是哪位学生的作业,建议在添加用户组时不要勾选“查看全体提交列表”权限。

添加题目

互评题目集只能添加一道主观题,加入题目集后教师可以为这道题目设置分数。

题目分数将会展示在学生做作业,进行互评等界面。

学生提交作业

到达题目集开始时间后,学生可以在题目集中完成作业,在到达作业截止时间前,可以继续修改自己的作业,最终互评时以最后一次提交为准。

学生进行互评

作业截止时间后,系统会自动分配互评任务。学生不会分配到自己的作业。

学生将会看到需要进行评价的匿名作业,对每份作业都可以打分并写评语。

教师仲裁

到达互评截止时间后,系统会根据学生的评价结果自动计算各项分数,同时将需要仲裁的作业单独列出。

教师可以进入互评结果页面,查看每位学生的互评情况,并仲裁分数。

如果启用了申诉功能,学生可以向老师发送一份申诉,教师可以选择回复申诉,并根据学生的申诉情况进行仲裁。

一个例子

互评配置

在本例中,我们设置了开启反馈与申诉,评分规则为只有提交了作业的学生才可以参与互评。
每份作业需要被两位学生评价,如果学生没有提交评价,每份作业会被扣 5 分。

我们授权了一个用户组参与互评,一共 5 位学生,分别为 学生 1-5。

我们设置了这道题为 30 分,根据前面的仲裁阈值设置,当两位评价的分数差距超过 6 分(30 分的 20%),计算后得分超过 25.5 分或低于 18 分后,会自动提示老师需要进行仲裁。

学生提交作业

前四位学生都提交了作业,而第五位学生没有提交。(为了演示方便我们让每位学生将自己的编号作为答案提交,实际作业中学生不应该提交任何能被识别出自己身份的作业内容)

在提交列表中,教师可以看到每位学生的各此提交情况。某位学生提交了多次,在这里也会全部列出。

学生进行互评

提交作业阶段结束后,系统会自动对作业进行分发,学生在互评任务界面可以匿名的看到自己需要评价的作业。

点击批改会看到作业的答案,学生可以在这里填写分数与评语,同时还可以申报违规。一旦某份作业被学生申报违规,教师会被提醒对这份作业进行仲裁。

互评截止之前,学生可以多次修改互评的结果,评价结果也会显示在列表中。

互评结束

互评阶段结束后,学生和教师可以分别查看自己的和全部的互评情况。

学生查看互评结果与申诉

学生能够查看自己的作业互评得分,给别人互评的得分或扣分,以及所有给自己的评价(包括分数及评语)。

学生一共收到了两份互评,得分分别为 25 和 20, 分数差为 5 分,不需要仲裁。

教师仲裁与调整

互评阶段结束后,教师可以在互评结果页面看到所有学生的互评情况。其中需要仲裁或未提交的学生会列在最前面,此时没有问题的成绩将会进入提交列表和排名。

仲裁违规

点击进入第一位需要仲裁的学生,会列出得分情况以及等待仲裁的原因,教师可以查看作业后进行仲裁。

这份作业有学生报告违规(违规原因可以在下方查看),并且分数低于仲裁得分下限。

下方的详细列表中会列出这位学生所有给出的互评与收到的互评,包括评语以及汇报违规后的理由,教师可以参考这些信息对作业进行仲裁。

假设教师认为这位学生的确违规,可以仲裁时选中“违规”,并且把分数设置为 0 分,此时学生本次作业就会不得分,并且由于 0 分,另一位进行仲裁的学生会被扣除 15 (学生的给分) - 30 (满分) * 20% (仲裁阈值) / 2 分。即 12 分:

可以看到这位学生原本作业得分 27.5, 但由于仲裁导致超阈值扣分 12 分,最终四舍五入得分为 16 分。

此时教师认为对这位学生的互评扣分过多了,只是未发现违规,希望调整他的分数。可以点击右上角的调整按钮,直接调整他的最终得分。

此时我们直接调整了他的总分为 22 分,可以看到虽然各部分得分还在,但最终成绩尊重了教师的调整结果。

学生的申诉与反馈功能都比较简单,学生可以直接进行操作,教师会获得申诉的提示,因此再次不演示。

FAQ

仲裁与调整

  • 仲裁修改的是学生的作业得分(不包括互评相关分数),仲裁后的成绩仍然要减去这位学生的互评扣分,才是最终得分。同时仲裁将会导致其他为这位学生打分的学生重新计算互评罚扣。
  • 调分直接修改学生的最终得分,不会影响他收到的互评。
  • 仲裁与调整操作互相覆盖,后操作的会覆盖掉之前的操作。

评分规则

  • 用户组中所有用户参与互评,互评成绩单独计算

    • 未提交作业的学生也可以参与互评(会被系统分发任务)
    • 题目集满分:作业分数(题目的分数) + 互评满分
    • 学生互评最多罚扣到互评满分
  • 只有提交了作业的人参与互评,惩罚分从作业得分中扣除

    • 未提交作业的学生不可以参与互评(系统不分发任务)
    • 题目集满分:作业分数
    • 学生互评不设置最多罚扣分数(互评完成的不好,最多可能将整个题目集扣至 0 分)

作业分发范围

如果题目集中被授权了多个用户组,作业只会在当前用户组中分发(也就是说不同用户组不会互相互评作业)

分组作业

如果互评作业需要分组完成,老师可以将所有组长单独加到一个用户组中,授予答题权限(提交答案),所有作业都由组长分给同学完成,再由组长提交作业、进行互评。

注:

  1. 互评功能还在早期阶段,由于流程复杂,可能还有 bug。如果遇到成绩计算错误,作业没有分发等问题,请直接在群中联系我们(学生提交的作业都会保留)。
  2. 目前导出功能还未为互评题目集进行优化,后续我们将专门为互评设计导出功能。
  3. 互评题目集在作业分发、计算分数等多个阶段都需要用户组与题目集的授权关系,建议老师轻易不要删除。

PTA题目集公告升级与登录功能升级

发表于 2018-08-31

新的学期即将到来,为了能更好地服务拼题A 各位用户,我们进行了如下升级,希望大家拼题愉快:

新功能

  • markdown编辑器升级,支持上传pdf/word/xls/ppt文件
  • 题目集公告现在支持markdown了
  • 新设计的登录页面

小改动

  • 题目集测试数据文件限制由20MB提升至80MB
  • 部分编译器版本升级
  • 自测图标调整

功能说明

markdown编辑器文件上传

在题目编辑页和题目集公告页的编辑器中,点击工具栏内文件图标,即可上传文件,目前支持pdf/word/xls/ppt
四种类型文件的上传。

题目集公告

题目集编辑页的公告现在也支持markdown了,各位老师可在公告中发布课程相关文件,便于学生第一时间看到公告。

新登录页

我们为企业用户和我们的网站设置了全新的登录页面,在企业用户的专属域名下,登录页将显示企业logo与企业名称,企业HR与应聘者可在此登录进入企业版。

我们旗下4款产品均可使用统一登录页面登录

OMS 监考系统白名单、代理服务器及其他新功能

发表于 2018-08-27

OMS 监考系统在暑假期间进行了较大幅度的升级,下文将介绍对用户影响较大的更新。

更友好的客户端提示

目前客户端中部分错误提示对用户不够友好,我们对一些常见的错误补充了说明内容,方便用户进行排查以及处理。

支持代理服务器

为了响应部分学校的特殊情况,目前系统架构进行了重写,使得客户端已经可以通过代理服务器访问 PTA 相关系统了。

代理服务器的具体要求是:

  1. 为 HTTP 代理,且支持 HTTPS 穿透;
  2. 不修改数据包内容。

使用方式是在客户端目录下的

1
oms-client-startup.exe.config

以及

1
bin/oms-client.exe.config

两个文件中的

1
<add key="proxy" value="" />

的 value 后填入您需要的代理服务器。一个例子如下:

1
<add key="proxy" value="192.168.1.2:3456" />

期望该功能能为各学校提供更多便利。

白名单

每个学校的环境不一致,在监考时可能会出现误报的情况。我们本次增加了关键词白名单,老师可以根据自身情况添加白名单。目前该功能仍在测试中。

结束后自动关闭客户端

现在考试结束后客户端会自动关闭,以减少机房老师工作量。因此如有考试延时等问题,请务必修改考试配置(修改时因为需要重新载入配置,客户端可能会出现消失再出现的情况,请让学生不要惊慌)。我们也推荐老师在使用时预留额外时间。

附言

目前客户端版本为 V0.11.0,根据升级后反馈还可能进行一次到数次的升级,如果有问题欢迎反馈给我们,也欢迎还没有购买系统的学校联系我们了解详情~

ACM/ICPC 计分方式

发表于 2018-08-02

ACM 国际大学生程序设计竞赛(英语: ACM International Collegiate Programming Contest, ICPC )是由美国电脑协会( ACM )主办的,一项旨在展示大学生创新能力、团队精神和在压力下编写程序、分析和解决问题能力的年度竞赛。经过 30 多年的发展,ACM 国际大学生程序设计竞赛已经发展成为最具影响力的大学生计算机竞赛。

近年来 ACM/ICPC 在中国国内迅速发展,越来越多的大学建立了 ACM/ICPC 的培训体系,专门培养和训练学生并组建队伍参加比赛。因此 PTA 加入了 ACM/ICPC 的计分方式,为希望开展竞赛培训的学校提供一个网上比赛和做题的平台。

一个 ACM/ICPC 题目集

一个题目集在创建的时候可以选择是否是 ACM/ICPC 题目集,创建之后不可以更改。

目前 ACM/ICPC 题目集只允许添加编程题,一般而言比赛也只含编程题。

判题和排名说明

PTA 使用的规则参考了官方规则

判题

ACM/ICPC 比赛中,参赛者每提交一个程序,比赛中都会实时评测,并且结果将反馈给参赛者。
评测的结果可能是:

  • 答案正确:通过了所有测试点
  • 答案错误:第一个未通过的测试点是结果错误
  • 运行超时:第一个未通过的测试点运行时间超出了规定时限
  • 段错误:程序发生段错误,可能是数组越界,堆栈溢出(比如,递归调用层数太多)等情况引起

等等,更多错误的解释可以参考 PTA 首页公告里的 FAQ 。

排名

ACM/ICPC 比赛中题目没有分数,选手按过题数量从高到低进行排名,过题数相同时按总用时从小到大进行排名。当这两个都相同时按最后过题的提交时间排名,不过目前尚未实现。

PTA 上对此规则进行了拓展,题目允许存在分数,最终排名按照分数从高到低排名。要实现标准的 ACM 比赛排名,只需要将每道题的分数设置成一样就行了。

总用时的单位为分钟,计算公式为:

总用时 = 过题时间总和 + 总罚时

其中总罚时 = 失败尝试次数 * 20 分钟

一道题的过题时间指的是从比赛开始到在此题上第一次成功的提交经过的时间。

失败尝试次数指在成功做对一道题之前的提交次数,但是不包括编译错误。

总用时也有另外一种等价的计算方法,可以先计算每道通过的题目的用时和罚时,再一起加起来。

封榜规则

ACM/ICPC 比赛中,为了保留悬念,通常在比赛结束前的某个时刻进行封榜,从这时候到宣布排名结果为止,参赛者将无法在排名榜单上看到别人的提交结果,但是可以看到别人封榜后在某个题上进行了尝试。

一般而言一场比赛时长为 5 个小时,封榜时间为比赛结束前 1 小时。

排名页 UI

一个可能的排名页面如下:

图中分别用红黄绿颜色进行了不同的标注:

红色加号表示通过此题,如图 1 和图 2 所示

加号下方的黑色小字表示通过此题的时间(从比赛开始进行计时),图 1 和图 2 中均表示比赛开始后 20 分钟后过题。

加号后面跟着失败的尝试次数,图 1 没有数字,说明第一次提交就通过了,图 2 加号后的 2 表示有两次失败的尝试。

绿色减号表示提交此题,但是仍然没有通过,如图 3 所示

图 3 中绿色的 -1 表示在此题上有一次失败的尝试,并且仍未通过。

绿色数字存在一种特殊情况,如图 4 所示

提交了但是结果为编译错误,由于编译错误不算失败尝试,所以结果会显示为绿色 0 。

黄色数字仅出现在封榜之后,如图 5 所示

如果在封榜后继续提交封榜前没有过的题,结果将不会显示出来,会将封榜后尝试的次数用黄色加号和数字挂在后面,比如图 5 ,表示封榜前有一次失败,封榜后又提交了两次。

注意点

比赛结束后榜单不会自动刷新,需要点一下重建排名才能把封榜的效果去掉。

使用题目自测保证题目质量

发表于 2018-07-23

在 PTA 中,程序填空题、函数题和编程题是需要被评测机编译(或解释)执行的题目类型,通过对输出数据的对比,判断学生答题正确性。

平时我们在检查题目以及解答老师疑问的过程中,总结出一些容易遇到的错误:

  • 出题老师添加的测试数据有误,学生无法得到某些评测点分数
  • 程序填空题挖空语法错误,导致系统解析空格时出错
  • 函数题忘记添加代码插入点,评测机无法正确评测学生答案
  • 编程题样例输入输出错误

我们在逐步优化的过程中添加了更严格的出题语法检查,预防常见的出题错误,同时加入了在线查看测试数据功能,方便老师随时检查测试数据。

本月我们对程序填空题、函数题和编程题上线了题目自测功能。

什么是题目自测

题目自测能够将出题老师的测试数据(以及编程题的样例数据)与题目标准答案一起提交到评测机执行,比对标准答案结果与实际程序输出结果,并给出测试结果。
这一机制能有效保证测试数据的正确性,防止由于出题老师疏忽或系统问题导致学生做题时无法得到正确的评测结果。

通过自测意味着什么

题目自测保证了这道题目能够正确被系统解析、能够在评测机中执行,同时出题老师的标准答案和测试数据相符合。

除此之外自测不能解决以下问题:

  1. 题目描述不清或有误导致学生无法理解
  2. 测试数据本身较弱,使用不完善的程序也能执行通过

引用他人通过自测的题目

在题库列表以及向题目集中添加题目时,可以在状态列查看题目自测结果,同时只有通过自测的题目才可以进行发布。

出题时查看自测结果

每次题目的添加和更新都会自动执行题目自测功能,老师在一般情况下无需手动执行。

在题目修改界面的右上角会显示当前题目的自测结果,并且在点击后查看更加详细的评测结果(包含运行时间、内存占用、编译提示等信息)。

编程题的样例数据是学生辅助学生正确理解题目的重要信息,为尽量避免学生由于样例数据错误对题目造成误解,除了测试数据外,编程题在进行自测时还检查了了样例数据的正确性。

UPDATE 1: 现有的已公开题目有可能未通过自测,我们将逐步提醒老师并协助进行完善,但之后所有的题目发布操作都需要已经通过自测。
UPDATE 2: 目前在题目创建完成时不会自动执行自测,老师可以在右上角手动点击开始自测或再次进行更新,我们近期将会修复。

glibc 更新导致的段错误排查

发表于 2018-06-27

0x00 起因

姥姥在检查 PAT 练习题的过程当中,发现一道题曾经能过的标准程序现在在两个测试点上会产生段错误。题目链接:英文版

0x01 排查

拿到姥姥的标准程序,是个 C 的程序,在网站上看了看同学们的提交,几乎全是 C++,并且也没有遇到同样的问题,所以总体上讲这个情况影响可能不是特别大。

观察出现问题的测试点,发现是两个数据量最大的 case。那看样子这个问题和 case 大小相关。

尝试手跑命令重现,发现一个奇怪的现象,如果不在沙盒里运行,程序毫无问题,运行结果也没有错。

但是放进沙盒运行就直接抛段错误。

而后我们尝试在沙盒当中获得 segmentation fault 可能产生的 core dump 无果。再尝试在沙盒当中运行 gdb 进行调试,但由于 gdb 本身会使用被沙盒禁止的系统调用,所以不能进行断点调试之类的,所以这次索性将所有系统调用开放,允许程序使用。这时候神奇的事请发生了,原本段错误的程序竟然能够正常运行了。

去除系统调用限制

那问题就简单了,经过几次二分查找之后,我们确定了造成段错误的系统调用限制 sysinfo

0x02 验证

我们可以验证一下不同数据集,同样的程序调用系统调用有什么区别

小数据集

大数据集

没错,在大数据集情况下,这个程序会额外调用一个 sysinfo 系统调用,而它是被沙盒禁用的,所以产生了段错误。

0x03 继续排查

经过几次“print 大法好”调试之后,我们发现 sysinfo 是在 qsort(E, M, sizeof(struct edge), ComparW); 这行代码当中发生的。那 qsort 为什么需要调用 sysinfo 呢?通过搜索我们找到了一个 patch getsysstats: use sysinfo() instead of parsing /proc/meminfo

我们节选一些:

Profiling git’s test suite, Linus noted [1] that a disproportionatelylarge amount of time was spent reading /proc/meminfo. This is done bythe glibc functions get_phys_pages and get_avphys_pages, but they onlyneed the MemTotal and MemFree fields, respectively. That sameinformation can be obtained with a single syscall, sysinfo, instead ofsix: open, fstat, mmap, read, close, munmap. While sysinfo alsoprovides more than necessary, it does a lot less work than what thekernel needs to do to provide the entire /proc/meminfo. Both strace -Tand in-app microbenchmarks shows that the sysinfo() approach isroughly an order of magnitude faster.

意思是说 Linus 发现,glibc 在获取内存相关信息的时候,是直接文件读 /proc/meminfo的,而这些信息在 sysinfo 这个系统调用当中就有,而且性能要好得多,因为文件操作确实很慢。

So it seems that any application that uses qsort on a moderately sizedarray will incur this cost (once), which is obviously proportionatelymore expensive for lots of short-lived processes (such as the git testsuite).

他也提到了,当 qsort 执行在一个较大规模的数组上时,这个情况就会发生一次,这对于 git 测试集来说是非常昂贵的性能开销。

我们可以尝试抓一下这个 sysinfo 在哪里,通过 gdb catch syscall 这个方便的功能,我们抓到了调用栈

我们可以看一下源码,msort.c#164

所以当 size < 1024时,qsort 直接使用 stack。不然就会获取系统内存信息,做一些内存空间的判断,然后 malloc

0x04 后记

引入这个问题,是因为大概在今年 4 月份,我们将 judger 的运行基础镜像从 ubuntu 14.04 升级到了 ubuntu 16.04。而新的 judger 代码已经上线,所以现在的 judger 已经不做 sysinfo 这个系统调用的过滤了。

PAT Tech Team

7 日志
2 标签
GitHub
© 2019 PAT Tech Team
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4