Skip to content

Commit a16f23d

Browse files
committed
DO NOT MERGE, this is just a prototype as a base for discussion.
Progress towards #2525. The basic idea is to replace a lot of scattered scoring logic (which might become more complex) with encoding enough information in the rank cache. The information is encoded in a sort key that can be sorted descendingly. The sort key is a tuple (serialized as string into the database) of fixed precision decimals. Each tuple is using 9 decimals and left padded with `0`s, so it's sortable via normal DB query operations. If sorted ascending (e.g. penalty time), then we subtract from a very large number. For example, with ICPC scoring, the top 2 teams from NWERC 2024 gets these sort keys: ``` 00000000000000000000013.000000000,999999999999999999998725.000000000,999999999999999999999711.000000000 00000000000000000000011.000000000,999999999999999999998918.000000000,999999999999999999999701.000000000 ``` With this mechanism, we should be able to implement other planned scoring methods (in particular partial scoring, optimization problems) without too much complication on the business logic side. We do use `bcmath` with fixed precision to not run into numerical precision issues with sequencing floating point operations. This is not important today, but will become important once we deall with non-integers (e.g. for optimization problems). It also caches some computation and makes scoreboard computation more efficient today.
1 parent 84e4903 commit a16f23d

19 files changed

+445
-357
lines changed

.github/jobs/baseinstall.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-root}
1515

1616
set -euxo pipefail
1717

18+
sudo apt update && sudo apt install php-bcmath -y
19+
sudo apt install php${phpversion}-bcmath -y
20+
21+
echo
22+
echo "DEBUG <========================================================================="
23+
echo
24+
apt search bcmath
25+
echo "DEBUG <========================================================================="
26+
echo
27+
28+
1829
if [ -z "$phpversion" ]; then
1930
phpversion=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";')
2031
fi

.github/workflows/codeql-analysis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ jobs:
3636
with:
3737
languages: ${{ matrix.language }}
3838

39+
- name: Install bcmath
40+
run: sudo apt update; sudo apt install php-bcmath -y
41+
3942
- name: Install composer files
4043
if: ${{ contains(env.COMPILED, matrix.language) }}
4144
run: |

doc/manual/install-domserver.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ GNU/Linux, or one of its derivative distributions like Ubuntu::
3737

3838
sudo apt install libcgroup-dev make acl zip unzip pv mariadb-server apache2 \
3939
php php-fpm php-gd php-cli php-intl php-mbstring php-mysql \
40-
php-curl php-json php-xml php-zip composer ntp python3-yaml
40+
php-curl php-json php-xml php-zip composer ntp python3-yaml php-bcmath
4141

4242
The following command can be used on Fedora, and related distributions like
4343
Red Hat Enterprise Linux and Rocky Linux (before V9)::
4444

4545
sudo dnf install libcgroup-devel make acl zip unzip pv mariadb-server httpd \
4646
php-gd php-cli php-intl php-mbstring php-mysqlnd php-fpm \
47-
php-xml php-zip composer chronyd python3-pyyaml
47+
php-xml php-zip composer chronyd python3-pyyaml php-bcmath
4848

4949
`nginx` can be used as an alternate web server.
5050

webapp/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
],
4141
"require": {
4242
"php": "^8.1.0",
43+
"ext-bcmath": "*",
4344
"ext-ctype": "*",
4445
"ext-curl": "*",
4546
"ext-fileinfo": "*",

webapp/composer.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20250309122806 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Add sort keys to rankcache, allowing us to support different scoring functions efficiently and elegantly';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// this up() migration is auto-generated, please modify it to your needs
23+
$this->addSql('ALTER TABLE rankcache ADD sort_key_public TEXT DEFAULT \'\' NOT NULL COMMENT \'Opaque sort key for public audience.\', ADD sort_key_restricted TEXT DEFAULT \'\' NOT NULL COMMENT \'Opaque sort key for restricted audience.\'');
24+
$this->addSql('CREATE INDEX sortKeyPublic ON rankcache (sort_key_public)');
25+
$this->addSql('CREATE INDEX sortKeyRestricted ON rankcache (sort_key_restricted)');
26+
}
27+
28+
public function down(Schema $schema): void
29+
{
30+
// this down() migration is auto-generated, please modify it to your needs
31+
$this->addSql('DROP INDEX sortKeyPublic ON rankcache');
32+
$this->addSql('DROP INDEX sortKeyRestricted ON rankcache');
33+
$this->addSql('ALTER TABLE rankcache DROP sort_key_public, DROP sort_key_restricted');
34+
}
35+
36+
public function isTransactional(): bool
37+
{
38+
return false;
39+
}
40+
}

webapp/src/Controller/Jury/ImportExportController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ protected function getResultsHtml(
412412
$filter = new Filter();
413413
$filter->categories = $categoryIds;
414414
$scoreboard = $this->scoreboardService->getScoreboard($contest, true, $filter);
415-
$teams = $scoreboard->getTeams();
415+
$teams = $scoreboard->getTeamsInDescendingOrder();
416416

417417
$teamNames = [];
418418
foreach ($teams as $team) {

webapp/src/DataFixtures/Test/RejudgingFirstToSolveFixture.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public function load(ObjectManager $manager): void
2222

2323
$manager->persist($team2);
2424

25+
$contest = $manager->getRepository(Contest::class)->findOneBy(['shortname' => 'demo']);
2526
$submissionData = [
2627
// team, submittime, result]
27-
[$team1, '2021-01-01 12:34:56', 'correct'],
28-
[$team2, '2021-01-01 12:33:56', 'wrong-answer'],
28+
[$team1, $contest->getStarttime() + 400, 'correct'],
29+
[$team2, $contest->getStarttime() + 300, 'wrong-answer'],
2930
];
3031

31-
$contest = $manager->getRepository(Contest::class)->findOneBy(['shortname' => 'demo']);
3232
$language = $manager->getRepository(Language::class)->find('cpp');
3333
$problem = $contest->getProblems()->filter(fn(ContestProblem $problem) => $problem->getShortname() === 'A')->first();
3434

@@ -39,11 +39,11 @@ public function load(ObjectManager $manager): void
3939
->setContestProblem($problem)
4040
->setLanguage($language)
4141
->setValid(true)
42-
->setSubmittime(Utils::toEpochFloat($submissionItem[1]));
42+
->setSubmittime($submissionItem[1]);
4343
$judging = (new Judging())
4444
->setContest($contest)
45-
->setStarttime(Utils::toEpochFloat($submissionItem[1]))
46-
->setEndtime(Utils::toEpochFloat($submissionItem[1]) + 5)
45+
->setStarttime($submissionItem[1])
46+
->setEndtime($submissionItem[1] + 5)
4747
->setValid(true)
4848
->setResult($submissionItem[2]);
4949
$judging->setSubmission($submission);

webapp/src/Entity/RankCache.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php declare(strict_types=1);
22
namespace App\Entity;
33

4+
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
45
use Doctrine\ORM\Mapping as ORM;
56

67
/**
@@ -18,6 +19,8 @@
1819
#[ORM\Index(columns: ['cid', 'points_public', 'totaltime_public', 'totalruntime_public'], name: 'order_public')]
1920
#[ORM\Index(columns: ['cid'], name: 'cid')]
2021
#[ORM\Index(columns: ['teamid'], name: 'teamid')]
22+
#[ORM\Index(columns: ['sort_key_public'], name: 'sortKeyPublic')]
23+
#[ORM\Index(columns: ['sort_key_restricted'], name: 'sortKeyRestricted')]
2124
class RankCache
2225
{
2326
#[ORM\Column(options: [
@@ -62,6 +65,20 @@ class RankCache
6265
#[ORM\JoinColumn(name: 'teamid', referencedColumnName: 'teamid', onDelete: 'CASCADE')]
6366
private Team $team;
6467

68+
#[ORM\Column(
69+
type: 'text',
70+
length: AbstractMySQLPlatform::LENGTH_LIMIT_TEXT,
71+
options: ['comment' => 'Opaque sort key for public audience.', 'default' => '']
72+
)]
73+
private string $sortKeyPublic = '';
74+
75+
#[ORM\Column(
76+
type: 'text',
77+
length: AbstractMySQLPlatform::LENGTH_LIMIT_TEXT,
78+
options: ['comment' => 'Opaque sort key for restricted audience.', 'default' => '']
79+
)]
80+
private string $sortKeyRestricted = '';
81+
6582
public function setPointsRestricted(int $pointsRestricted): RankCache
6683
{
6784
$this->points_restricted = $pointsRestricted;
@@ -149,4 +166,26 @@ public function getTeam(): Team
149166
{
150167
return $this->team;
151168
}
169+
170+
public function setSortKeyPublic(string $sortKeyPublic): RankCache
171+
{
172+
$this->sortKeyPublic = $sortKeyPublic;
173+
return $this;
174+
}
175+
176+
public function getSortKeyPublic(): string
177+
{
178+
return $this->sortKeyPublic;
179+
}
180+
181+
public function setSortKeyRestricted(string $sortKeyRestricted): RankCache
182+
{
183+
$this->sortKeyRestricted = $sortKeyRestricted;
184+
return $this;
185+
}
186+
187+
public function getSortKeyRestricted(): string
188+
{
189+
return $this->sortKeyRestricted;
190+
}
152191
}

webapp/src/Service/AwardService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void
1616
{
1717
$group_winners = $problem_winners = $problem_shortname = [];
1818
$groups = [];
19-
foreach ($scoreboard->getTeams() as $team) {
19+
foreach ($scoreboard->getTeamsInDescendingOrder() as $team) {
2020
$teamid = $team->getExternalid();
2121
if ($scoreboard->isBestInCategory($team)) {
2222
$catId = $team->getCategory()->getExternalid();

0 commit comments

Comments
 (0)