Skip to content
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

Cache run results #127

Merged
merged 18 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/sinol_make/commands/gen/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import argparse
import glob
import os
import hashlib
import yaml

import multiprocessing as mp

from sinol_make import util
from sinol_make.commands.gen import gen_util
from sinol_make.commands.gen.structs import OutputGenerationArguments
from sinol_make.helpers import parsers, package_util, compile
from sinol_make.structs.gen_structs import OutputGenerationArguments
from sinol_make.helpers import parsers, package_util
from sinol_make.interfaces.BaseCommand import BaseCommand


Expand Down
2 changes: 1 addition & 1 deletion src/sinol_make/commands/inwer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Dict, List

from sinol_make import util
from sinol_make.commands.inwer.structs import TestResult, InwerExecution, VerificationResult, TableData
from sinol_make.structs.inwer_structs import TestResult, InwerExecution, VerificationResult, TableData
from sinol_make.helpers import package_util, compile, printer, paths
from sinol_make.helpers.parsers import add_compilation_arguments
from sinol_make.interfaces.BaseCommand import BaseCommand
Expand Down
71 changes: 51 additions & 20 deletions src/sinol_make/commands/run/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
from typing import Dict

from sinol_make import contest_types, oiejq
from sinol_make.commands.run.structs import ExecutionResult, ResultChange, ValidationResult, ExecutionData, \
PointsChange, PrintData
from sinol_make.structs.run_structs import ExecutionData, PrintData
from sinol_make.structs.cache_structs import CacheTest, CacheFile
from sinol_make.helpers.parsers import add_compilation_arguments
from sinol_make.interfaces.BaseCommand import BaseCommand
from sinol_make.interfaces.Errors import CompilationError, CheckerOutputException, UnknownContestType
from sinol_make.helpers import compile, compiler, package_util, printer, paths
from sinol_make.structs.status_structs import Status
from sinol_make.helpers import compile, compiler, package_util, printer, paths, cache
from sinol_make.structs.status_structs import Status, ResultChange, PointsChange, ValidationResult, ExecutionResult
import sinol_make.util as util
import yaml, os, collections, sys, re, math, dictdiffer
import multiprocessing as mp
Expand Down Expand Up @@ -308,17 +308,17 @@ def get_groups(self, tests):
return sorted(list(set([self.get_group(test) for test in tests])))


def compile_solutions(self, solutions):
def compile_solutions(self, solutions, is_checker=False):
os.makedirs(paths.get_compilation_log_path(), exist_ok=True)
os.makedirs(paths.get_executables_path(), exist_ok=True)
print("Compiling %d solutions..." % len(solutions))
args = [(solution, True) for solution in solutions]
args = [(solution, True, is_checker) for solution in solutions]
with mp.Pool(self.cpus) as pool:
compilation_results = pool.starmap(self.compile, args)
return compilation_results


def compile(self, solution, use_extras = False):
def compile(self, solution, use_extras = False, is_checker = False):
compile_log_file = paths.get_compilation_log_path("%s.compile_log" % package_util.get_file_name(solution))
source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution))
output = paths.get_executables_path(package_util.get_executable(solution))
Expand All @@ -339,7 +339,7 @@ def compile(self, solution, use_extras = False):
try:
with open(compile_log_file, "w") as compile_log:
compile.compile(source_file, output, self.compilers, compile_log, self.args.weak_compilation_flags,
extra_compilation_args, extra_compilation_files)
extra_compilation_args, extra_compilation_files, is_checker=is_checker)
print(util.info("Compilation of file %s was successful."
% package_util.get_file_name(solution)))
return True
Expand Down Expand Up @@ -400,7 +400,6 @@ def check_output(self, name, input_file, output_file_path, output, answer_file_p
output_file.write("\n".join(output) + "\n")
return self.check_output_checker(name, input_file, output_file_path, answer_file_path)


def execute_oiejq(self, command, name, result_file_path, input_file_path, output_file_path, answer_file_path,
time_limit, memory_limit, hard_time_limit):
env = os.environ.copy()
Expand Down Expand Up @@ -611,17 +610,29 @@ def run_solutions(self, compiled_commands, names, solutions):
"""

executions = []
all_cache_files: Dict[str, CacheFile] = {}
all_results = collections.defaultdict(
lambda: collections.defaultdict(lambda: collections.defaultdict(map)))

for (name, executable, result) in compiled_commands:
lang = package_util.get_file_lang(name)
solution_cache = cache.get_cache_file(os.path.join(os.getcwd(), "prog", name))
all_cache_files[name] = solution_cache

if result:
for test in self.tests:
executions.append((name, executable, test,
package_util.get_time_limit(test, self.config, lang, self.ID, self.args),
package_util.get_memory_limit(test, self.config, lang, self.ID, self.args),
self.timetool_path))
all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING)
test_time_limit = package_util.get_time_limit(test, self.config, lang, self.ID, self.args)
test_memory_limit = package_util.get_memory_limit(test, self.config, lang, self.ID, self.args)

test_result: CacheTest = solution_cache.tests.get(self.test_md5sums[os.path.basename(test)], None)
if test_result is not None and test_result.time_limit == test_time_limit and \
test_result.memory_limit == test_memory_limit and \
test_result.time_tool == self.timetool_name:
all_results[name][self.get_group(test)][test] = test_result.result
else:
executions.append((name, executable, test, test_time_limit, test_memory_limit,
self.timetool_path))
all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING)
os.makedirs(paths.get_executions_path(name), exist_ok=True)
else:
for test in self.tests:
Expand Down Expand Up @@ -651,6 +662,17 @@ def run_solutions(self, compiled_commands, names, solutions):
result.Points = contest_points
all_results[name][self.get_group(test)][test] = result
print_data.i = i

# We store the result in dictionary to write it to cache files later.
lang = package_util.get_file_lang(name)
test_time_limit = package_util.get_time_limit(test, self.config, lang, self.ID, self.args)
test_memory_limit = package_util.get_memory_limit(test, self.config, lang, self.ID, self.args)
all_cache_files[name].tests[self.test_md5sums[os.path.basename(test)]] = CacheTest(
time_limit=test_time_limit,
memory_limit=test_memory_limit,
time_tool=self.timetool_name,
result=result
)
pool.terminate()
except KeyboardInterrupt:
keyboard_interrupt = True
Expand All @@ -664,6 +686,10 @@ def run_solutions(self, compiled_commands, names, solutions):
names, executions, self.groups, self.scores, self.tests, self.possible_score,
self.cpus, self.args.hide_memory, self.config, self.contest, self.args)[0]))

# Write cache files.
for solution, cache_data in all_cache_files.items():
cache_data.save(os.path.join(os.getcwd(), "prog", solution))

if keyboard_interrupt:
util.exit_with_error("Stopped due to keyboard interrupt.")

Expand Down Expand Up @@ -1127,6 +1153,14 @@ def check_errors(self, results: Dict[str, Dict[str, Dict[str, ExecutionResult]]]
if error_msg != "":
util.exit_with_error(error_msg)

def compile_checker(self):
checker_basename = os.path.basename(self.checker)
self.checker_executable = paths.get_executables_path(checker_basename + ".e")

checker_compilation = self.compile_solutions([self.checker], is_checker=True)
if not checker_compilation[0]:
util.exit_with_error('Checker compilation failed.')

def run(self, args):
util.exit_if_not_package()

Expand All @@ -1152,24 +1186,21 @@ def run(self, args):
title = self.config["title"]
print("Task: %s (tag: %s)" % (title, self.ID))
self.cpus = args.cpus or mp.cpu_count()
cache.save_to_cache_extra_compilation_files(self.config.get("extra_compilation_files", []), self.ID)

checker = package_util.get_files_matching_pattern(self.ID, f'{self.ID}chk.*')
if len(checker) != 0:
print(util.info("Checker found: %s" % os.path.basename(checker[0])))
self.checker = checker[0]
checker_basename = os.path.basename(self.checker)
self.checker_executable = paths.get_executables_path(checker_basename + ".e")

checker_compilation = self.compile_solutions([self.checker])
if not checker_compilation[0]:
util.exit_with_error('Checker compilation failed.')
self.compile_checker()
else:
self.checker = None

lib = package_util.get_files_matching_pattern(self.ID, f'{self.ID}lib.*')
self.has_lib = len(lib) != 0

self.tests = package_util.get_tests(self.ID, self.args.tests)
self.test_md5sums = {os.path.basename(test): util.get_file_md5(test) for test in self.tests}
self.check_are_any_tests_to_run()
self.set_scores()
self.failed_compilations = []
Expand Down
77 changes: 0 additions & 77 deletions src/sinol_make/commands/run/structs.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/sinol_make/contest_types/default.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from math import ceil
from typing import List

from sinol_make.commands.run.structs import ExecutionResult
from sinol_make.structs.status_structs import ExecutionResult


class DefaultContest:
Expand Down
2 changes: 1 addition & 1 deletion src/sinol_make/contest_types/oi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sinol_make.commands.run.structs import ExecutionResult
from sinol_make.structs.status_structs import ExecutionResult
from sinol_make.contest_types.default import DefaultContest


Expand Down
102 changes: 102 additions & 0 deletions src/sinol_make/helpers/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import os
import yaml
from typing import Union

from sinol_make import util
from sinol_make.structs.cache_structs import CacheFile
from sinol_make.helpers import paths, package_util


def get_cache_file(solution_path: str) -> CacheFile:
"""
Returns content of cache file for given solution
:param solution_path: Path to solution
:return: Content of cache file
"""
os.makedirs(paths.get_cache_path("md5sums"), exist_ok=True)
cache_file_path = paths.get_cache_path("md5sums", os.path.basename(solution_path))
try:
with open(cache_file_path, 'r') as cache_file:
data = yaml.load(cache_file, Loader=yaml.FullLoader)
if not isinstance(data, dict):
print(util.warning(f"Cache file for program {os.path.basename(solution_path)} is corrupted."))
os.unlink(cache_file_path)
return CacheFile()
try:
return CacheFile.from_dict(data)
except ValueError as exc:
print(util.error(f"An error occured while parsing cache file for solution {os.path.basename(solution_path)}."))
util.exit_with_error(str(exc))
except FileNotFoundError:
return CacheFile()
except (yaml.YAMLError, TypeError):
print(util.warning(f"Cache file for program {os.path.basename(solution_path)} is corrupted."))
os.unlink(cache_file_path)
return CacheFile()


def check_compiled(file_path: str) -> Union[str, None]:
"""
Check if a file is compiled
:param file_path: Path to the file
:return: executable path if compiled, None otherwise
"""
md5sum = util.get_file_md5(file_path)
try:
info = get_cache_file(file_path)
if info.md5sum == md5sum:
exe_path = info.executable_path
if os.path.exists(exe_path):
return exe_path
return None
except FileNotFoundError:
return None


def save_compiled(file_path: str, exe_path: str, is_checker: bool = False):
"""
Save the compiled executable path to cache in `.cache/md5sums/<basename of file_path>`,
which contains the md5sum of the file and the path to the executable.
:param file_path: Path to the file
:param exe_path: Path to the compiled executable
:param is_checker: Whether the compiled file is a checker. If True, all cached tests are removed.
"""
info = get_cache_file(file_path)
info.executable_path = exe_path
info.md5sum = util.get_file_md5(file_path)
info.save(file_path)

if is_checker:
for solution in os.listdir(paths.get_cache_path('md5sums')):
info = get_cache_file(solution)
info.tests = {}
info.save(solution)


def save_to_cache_extra_compilation_files(extra_compilation_files, task_id):
"""
Checks if extra compilation files have changed and saves them to cache.
If they have, removes all cached solutions that use them.
:param extra_compilation_files: List of extra compilation files
:param task_id: Task id
"""
solutions_re = package_util.get_solutions_re(task_id)
for file in extra_compilation_files:
file_path = os.path.join(os.getcwd(), "prog", file)
if not os.path.exists(file_path):
continue
md5sum = util.get_file_md5(file_path)
lang = package_util.get_file_lang(file)
if lang == 'h':
lang = 'cpp'
info = get_cache_file(file_path)

if info.md5sum != md5sum:
for solution in os.listdir(paths.get_cache_path('md5sums')):
# Remove only files in the same language and matching the solution regex
if package_util.get_file_lang(solution) == lang and \
solutions_re.match(solution) is not None:
os.unlink(paths.get_cache_path('md5sums', solution))

info.md5sum = md5sum
info.save(file_path)
Loading