-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Output vizualizer #2744
base: main
Are you sure you want to change the base?
Output vizualizer #2744
Changes from all commits
ebb0b43
a43209a
d3eec0c
cdc1692
88c0e88
131abb5
5e3406f
3b40afa
d4f4401
101cc03
69d99dc
df61e4a
a956b58
fbe7358
2986727
d79783d
8de3e52
f3b96e1
f6baa3a
76deaaf
aa062d7
2963350
f0dcaec
1636944
1af147a
d88d0c0
e56a83f
3a05304
87ca226
5d5d6c6
52fde24
ac0679c
1edc428
9362b92
812042e
eca9e35
de746eb
70ee0e4
7bcac2b
0a7daca
7190795
c424d21
fa3e2ed
7641e17
5b9c970
031d24a
51d3cc3
d34fce3
d616057
ed5e47c
f9de23c
f7e7e23
28c3904
2b6afcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# Invoke as: | ||
# <output_visualizer_program> answer_file feedback_file | ||
import matplotlib.pyplot as plt | ||
import sys | ||
|
||
my_name = sys.argv[0] | ||
my_input = sys.argv[1] | ||
my_feedback = sys.argv[2] | ||
|
||
with open(my_input, 'r') as f: | ||
lines = f.readlines() | ||
vals = [] | ||
for line in lines: | ||
if 'READ' in line: | ||
vals.append(int(line.split(' ')[-1])) | ||
plt.plot([0,1]) | ||
plt.ylabel('Guesses') | ||
plt.savefig(f"{my_feedback}", format='png') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
name: Boolean switch search | ||
|
||
validation: custom interactive | ||
visualization: default | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is that already spec'ed out? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, this is not something which exists in the spec. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was asking because I think this should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. any string will do at this moment, would be nice to get @eldering his opinion as I just picked something. Will change it to |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -334,7 +334,10 @@ function fetch_executable_internal( | |
$execrunpath = $execbuilddir . '/run'; | ||
$execrunjurypath = $execbuilddir . '/runjury'; | ||
if (!is_dir($execdir) || !file_exists($execdeploypath)) { | ||
system('rm -rf ' . dj_escapeshellarg($execdir) . ' ' . dj_escapeshellarg($execbuilddir)); | ||
system('rm -rf ' . dj_escapeshellarg($execdir) . ' ' . dj_escapeshellarg($execbuilddir), $retval); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unrelated to this PR |
||
if ($retval !== 0) { | ||
logmsg(LOG_WARNING, "Deleting '$execdir' or '$execbuilddir' was unsuccessful."); | ||
} | ||
system('mkdir -p ' . dj_escapeshellarg($execbuilddir), $retval); | ||
if ($retval !== 0) { | ||
error("Could not create directory '$execbuilddir'"); | ||
|
@@ -768,7 +771,7 @@ function fetch_executable_internal( | |
$judgehosts = request('judgehosts', 'GET'); | ||
if ($judgehosts !== null) { | ||
$judgehosts = dj_json_decode($judgehosts); | ||
$judgehost = array_filter($judgehosts, fn($j) => $j['hostname'] === $myhost); | ||
$judgehost = array_values(array_filter($judgehosts, fn($j) => $j['hostname'] === $myhost))[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is part of your other PR |
||
if (!isset($judgehost['enabled']) || !$judgehost['enabled']) { | ||
logmsg(LOG_WARNING, "Judgehost needs to be enabled in web interface."); | ||
} | ||
|
@@ -844,7 +847,9 @@ function fetch_executable_internal( | |
$debug_cmd = implode(' ', array_map('dj_escapeshellarg', | ||
[$runpath, $workdir, $tmpfile])); | ||
system($debug_cmd, $retval); | ||
// FIXME: check retval | ||
if ($retval !== 0) { | ||
error("Running '$runpath' failed."); | ||
} | ||
|
||
request( | ||
sprintf('judgehosts/add-debug-info/%s/%s', urlencode($myhost), | ||
|
@@ -872,6 +877,50 @@ function fetch_executable_internal( | |
continue; | ||
} | ||
|
||
if ($type == 'output_visualization') { | ||
if ($lastWorkdir !== null) { | ||
cleanup_judging($lastWorkdir); | ||
$lastWorkdir = null; | ||
} | ||
foreach ($row as $judgeTask) { | ||
if (isset($judgeTask['output_visualizer_script_id'])) { | ||
// Visualization of output which this host judged requested. | ||
$run_config = dj_json_decode($judgeTask['run_config']); | ||
$tmpfile = tempnam(TMPDIR, 'output_visualization_'); | ||
[$runpath, $error] = fetch_executable_internal( | ||
$workdirpath, | ||
'output_visualization', | ||
$judgeTask['output_visualizer_script_id'], | ||
$run_config['hash'] | ||
); | ||
if (isset($error)) { | ||
// FIXME | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix me 😛 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loosely based on what we do for the debug tasks, I'll see if I can merge the shared parts and possibly fix this one. |
||
continue; | ||
} | ||
|
||
$teamoutput = $workdir . "/testcase" . sprintf('%05d', $judgeTask['testcase_id']) . '/1/program.out'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Getting the output of the first run, so in other words, this does not work for multipass problems. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right - then please add a TODO and consider making the one a local variabled, e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would we want to do in this case? I assume we want to visualize actually the last pass? |
||
$visual_cmd = implode(' ', array_map('dj_escapeshellarg', | ||
[$runpath, $teamoutput, $tmpfile])); | ||
system($visual_cmd, $retval); | ||
if ($retval !== 0) { | ||
error("Running '$runpath' failed."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's rather report an internal error like we do in other cases (and not crash judgedaemons) |
||
} | ||
|
||
request( | ||
sprintf('judgehosts/add-visual/%s/%s', urlencode($myhost), | ||
urlencode((string)$judgeTask['judgetaskid'])), | ||
'POST', | ||
['visual_output' => rest_encode_file($tmpfile, false), | ||
'testcase_id' => $judgeTask['testcase_id']], | ||
false | ||
); | ||
unlink($tmpfile); | ||
|
||
logmsg(LOG_INFO, " ⇡ Uploading visual output of workdir $workdir."); | ||
} | ||
} | ||
continue; | ||
} | ||
$success_file = "$workdir/.uuid_pid"; | ||
$expected_uuid_pid = $row[0]['uuid'] . '_' . (string)getmypid(); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace DoctrineMigrations; | ||
|
||
use Doctrine\DBAL\Schema\Schema; | ||
use Doctrine\Migrations\AbstractMigration; | ||
|
||
/** | ||
* Auto-generated Migration: Please modify to your needs! | ||
*/ | ||
final class Version20241018061817 extends AbstractMigration | ||
{ | ||
public function getDescription(): string | ||
{ | ||
return 'Allow storing visualization of team output.'; | ||
} | ||
|
||
public function up(Schema $schema): void | ||
{ | ||
$this->addSql('ALTER TABLE problem ADD special_output_visualizer VARCHAR(32) DEFAULT NULL COMMENT \'Executable ID (string)\', CHANGE multipass_limit multipass_limit INT UNSIGNED DEFAULT NULL COMMENT \'Optional limit on the number of rounds; defaults to 1 for traditional problems, 2 for multi-pass problems if not specified.\''); | ||
$this->addSql('ALTER TABLE problem ADD CONSTRAINT FK_D7E7CCC819F5352E FOREIGN KEY (special_output_visualizer) REFERENCES executable (execid) ON DELETE SET NULL'); | ||
$this->addSql('CREATE INDEX special_output_visualizer ON problem (special_output_visualizer)'); | ||
$this->addSql('ALTER TABLE judgetask ADD output_visualizer_script_id INT UNSIGNED DEFAULT NULL COMMENT \'Output visualizer script ID\''); | ||
$this->addSql('ALTER TABLE `judgetask` | ||
MODIFY COLUMN `type` ENUM(\'judging_run\', \'generic_task\', \'config_check\', \'debug_info\', \'prefetch\', \'output_visualization\') DEFAULT \'judging_run\' NOT NULL COMMENT \'Type of the judge task.(DC2Type:judge_task_type)\''); | ||
$this->addSql('ALTER TABLE judging ADD visualization TINYINT(1) DEFAULT 0 NOT NULL COMMENT \'Explicitly requested to visualize the output.\''); | ||
$this->addSql('CREATE TABLE visualization (visualization_id INT UNSIGNED AUTO_INCREMENT NOT NULL COMMENT \'Visualization ID\', judgingid INT UNSIGNED DEFAULT NULL COMMENT \'Judging ID\', judgehostid INT UNSIGNED DEFAULT NULL COMMENT \'Judgehost ID\', testcaseid INT UNSIGNED DEFAULT NULL COMMENT \'Testcase ID\', filename VARCHAR(255) NOT NULL COMMENT \'Name of the file where we stored the visualization.\', INDEX IDX_E0936C40E0E4FC3E (judgehostid), INDEX IDX_E0936C40D360BB2B (testcaseid), INDEX judgingid (judgingid), PRIMARY KEY(visualization_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Team output visualization.\' '); | ||
$this->addSql('ALTER TABLE visualization ADD CONSTRAINT FK_E0936C405D5FEA72 FOREIGN KEY (judgingid) REFERENCES judging (judgingid) ON DELETE CASCADE'); | ||
$this->addSql('ALTER TABLE visualization ADD CONSTRAINT FK_E0936C40E0E4FC3E FOREIGN KEY (judgehostid) REFERENCES judgehost (judgehostid) ON DELETE SET NULL'); | ||
$this->addSql('ALTER TABLE visualization ADD CONSTRAINT FK_E0936C40D360BB2B FOREIGN KEY (testcaseid) REFERENCES testcase (testcaseid) ON DELETE CASCADE'); | ||
} | ||
|
||
public function down(Schema $schema): void | ||
{ | ||
$this->addSql('ALTER TABLE visualization DROP FOREIGN KEY FK_E0936C405D5FEA72'); | ||
$this->addSql('ALTER TABLE visualization DROP FOREIGN KEY FK_E0936C40E0E4FC3E'); | ||
$this->addSql('ALTER TABLE visualization DROP FOREIGN KEY FK_E0936C40D360BB2B'); | ||
$this->addSql('DROP TABLE visualization'); | ||
$this->addSql('ALTER TABLE judging DROP visualization'); | ||
$this->addSql('ALTER TABLE `judgetask` | ||
MODIFY COLUMN `type` ENUM(\'judging_run\', \'generic_task\', \'config_check\', \'debug_info\', \'prefetch\') DEFAULT \'judging_run\' NOT NULL COMMENT \'Type of the judge task.(DC2Type:judge_task_type)\''); | ||
$this->addSql('ALTER TABLE judgetask DROP output_visualizer_script_id'); | ||
$this->addSql('ALTER TABLE problem DROP FOREIGN KEY FK_D7E7CCC819F5352E'); | ||
$this->addSql('DROP INDEX special_output_visualizer ON problem'); | ||
$this->addSql('ALTER TABLE problem DROP special_output_visualizer, CHANGE multipass_limit multipass_limit INT UNSIGNED DEFAULT NULL COMMENT \'Optional limit on the number of rounds for multi-pass problems; defaults to 2 if not specified.\''); | ||
} | ||
|
||
public function isTransactional(): bool | ||
{ | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,9 @@ | |
|
||
namespace App\Controller\API; | ||
|
||
use App\Entity\Visualization; | ||
use App\Entity\Testcase; | ||
|
||
use App\DataTransferObject\JudgehostFile; | ||
use App\Doctrine\DBAL\Types\JudgeTaskType; | ||
use App\Entity\Contest; | ||
|
@@ -550,6 +553,58 @@ public function addDebugInfo( | |
$this->em->flush(); | ||
} | ||
|
||
/** | ||
* Add visual team output. | ||
*/ | ||
#[IsGranted('ROLE_JUDGEHOST')] | ||
#[Rest\Post('/add-visual/{hostname}/{judgeTaskId<\d+>}')] | ||
#[OA\Response(response: 200, description: 'When the visual output has been added')] | ||
public function addVisualization( | ||
Request $request, | ||
#[OA\PathParameter(description: 'The hostname of the judgehost that wants to add the debug info')] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this is not |
||
string $hostname, | ||
#[OA\PathParameter(description: 'The ID of the judgetask to add', schema: new OA\Schema(type: 'integer'))] | ||
int $judgeTaskId | ||
): void { | ||
$judgeTask = $this->em->getRepository(JudgeTask::class)->find($judgeTaskId); | ||
if ($judgeTask === null) { | ||
throw new BadRequestHttpException( | ||
'Inconsistent data, no judgetask known with judgetaskid = ' . $judgeTaskId . '.'); | ||
} | ||
|
||
foreach (['visual_output', 'testcase_id'] as $argument) { | ||
if (!$request->request->has($argument)) { | ||
throw new BadRequestHttpException( | ||
sprintf("Argument '%s' is mandatory", $argument)); | ||
} | ||
} | ||
|
||
$judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); | ||
if (!$judgehost) { | ||
throw new BadRequestHttpException("Who are you and why are you sending us any data?"); | ||
} | ||
|
||
$judging = $this->em->getRepository(Judging::class)->find($judgeTask->getJobId()); | ||
if ($judging === null) { | ||
throw new BadRequestHttpException( | ||
'Inconsistent data, no judging known with judgingid = ' . $judgeTask->getJobId() . '.'); | ||
} | ||
if ($tempFilename = tempnam($this->dj->getDomjudgeTmpDir(), "visual-")) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This means it doesn't work with 2 servers in HA mode. We should store this in the DB as a file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is loosely based on what we do with debug packages, so we have the same problem there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I didn't know. Then maybe for now that's fine but we should fix it at some point? |
||
$debug_package = base64_decode($request->request->get('visual_output')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: update name (no longer |
||
file_put_contents($tempFilename, $debug_package); | ||
} | ||
// FIXME: error checking | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another fixme |
||
$testcase = $this->em->getRepository(Testcase::class)->findOneBy(['testcaseid' => $request->request->get('testcase_id')]); | ||
$visualization = new Visualization(); | ||
$visualization | ||
->setJudgehost($judgehost) | ||
->setJudging($judging) | ||
->setTestcase($testcase) | ||
->setFilename($tempFilename); | ||
$this->em->persist($visualization); | ||
$this->em->flush(); | ||
} | ||
|
||
/** | ||
* Add one JudgingRun. When relevant, finalize the judging. | ||
* @throws DBALException | ||
|
@@ -1186,7 +1241,7 @@ public function getFilesAction( | |
return match ($type) { | ||
'source' => $this->getSourceFiles($id), | ||
'testcase' => $this->getTestcaseFiles($id), | ||
'compare', 'compile', 'debug', 'run' => $this->getExecutableFiles($id), | ||
'compare', 'compile', 'debug', 'run', 'output_visualization' => $this->getExecutableFiles($id), | ||
default => throw new BadRequestHttpException('Unknown type requested.'), | ||
}; | ||
} | ||
|
@@ -1478,6 +1533,7 @@ public function getJudgeTasksAction(Request $request): array | |
throw new BadRequestHttpException('Argument \'hostname\' is mandatory'); | ||
} | ||
$hostname = $request->request->get('hostname'); | ||
$hostname = 'Computer'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whoops |
||
|
||
$judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); | ||
if (!$judgehost) { | ||
|
@@ -1649,6 +1705,27 @@ public function getJudgeTasksAction(Request $request): array | |
return $this->serializeJudgeTasks($judgetasks, $judgehost); | ||
} | ||
|
||
// If there is nothing else, get visualization jobs that are assigned to this host. | ||
/** @var JudgeTask[] $judgetasks */ | ||
$judgetasks = $this->em | ||
->createQueryBuilder() | ||
->from(JudgeTask::class, 'jt') | ||
->select('jt') | ||
->andWhere('jt.judgehost = :judgehost OR jt.judgehost IS NULL') | ||
//->andWhere('jt.starttime IS NULL') | ||
->andWhere('jt.valid = 1') | ||
->andWhere('jt.type = :type') | ||
->setParameter('judgehost', $judgehost) | ||
->setParameter('type', JudgeTaskType::OUTPUT_VISUALIZATION) | ||
->addOrderBy('jt.priority') | ||
->addOrderBy('jt.judgetaskid') | ||
->setMaxResults(1) | ||
->getQuery() | ||
->getResult(); | ||
if (!empty($judgetasks)) { | ||
return $this->serializeJudgeTasks($judgetasks, $judgehost); | ||
} | ||
|
||
return []; | ||
} | ||
|
||
|
@@ -1667,8 +1744,16 @@ private function serializeJudgeTasks(array $judgeTasks, Judgehost $judgehost): a | |
$submit_id = $judgeTasks[0]->getSubmission()?->getSubmitid(); | ||
$judgetaskids = []; | ||
foreach ($judgeTasks as $judgeTask) { | ||
if ($judgeTask->getSubmission()?->getSubmitid() == $submit_id) { | ||
$judgetaskids[] = $judgeTask->getJudgetaskid(); | ||
if ($judgeTask->getType() == 'judging_run') { | ||
if ($judgeTask->getSubmission()?->getSubmitid() == $submit_id) { | ||
$judgetaskids[] = $judgeTask->getJudgetaskid(); | ||
} | ||
} else { | ||
// Just pick everything assigned to the judgehost itself or unassigned for now | ||
$assignedJudgehost = $judgeTask->getJudgehost(); | ||
if ($assignedJudgehost === $judgehost || $assignedJudgehost === null) { | ||
$judgetaskids[] = $judgeTask->getJudgetaskid(); | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks if the submission writes bogus output, e.g.
READ domjudge
, how do we handle cases like this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say this is something which is left up to the jury.
I wonder if there is something we can do with the output_validator & the judgemessage for communication. Either an extra exitcode or otherwise the jury has to parse the output in this script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's left to the jury. In this case you are the jury by implementing the visualizer :-)
Since people are going to model after examples we give them, we should make it robust and not crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding to this, I think we should return 42 in the script if we were able to visualize the output, and 43 if not.