From 1a48aa3bef607fbff1a855de131662b2415d0431 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sun, 12 Jun 2022 20:29:39 -0300 Subject: [PATCH 1/3] working cut --- mathics/builtin/base.py | 35 ++++++++++++- mathics/builtin/datentime.py | 54 +++++++++++--------- mathics/builtin/numbers/calculus.py | 21 +++++++- mathics/builtin/procedural.py | 1 + mathics/core/evaluation.py | 76 +++++++++-------------------- mathics/core/expression.py | 6 ++- mathics/core/rules.py | 2 + test/builtin/test_datentime.py | 70 ++++++++++++++++++++------ 8 files changed, 170 insertions(+), 95 deletions(-) diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 6e42083b7..cbbd8a22e 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -9,6 +9,7 @@ import typing from typing import Any, Iterable, cast +from datetime import datetime, timedelta from mathics.builtin.exceptions import ( BoxConstructError, @@ -17,6 +18,7 @@ from mathics.core.attributes import no_attributes from mathics.core.convert import from_sympy from mathics.core.definitions import Definition +from mathics.core.interrupt import TimeoutInterrupt from mathics.core.list import ListExpression from mathics.core.parser.util import SystemDefinitions, PyMathicsDefinitions from mathics.core.rules import Rule, BuiltinRule, Pattern @@ -42,6 +44,9 @@ from mathics.core.attributes import protected, read_protected +from multiprocessing import Process, Queue + + def get_option(options, name, evaluation, pop=False, evaluate=True): # we do not care whether an option X is given as System`X, # Global`X, or with any prefix from $ContextPath for that @@ -658,6 +663,27 @@ def apply(self, expr, evaluation) -> Symbol: return +def call_sympy_function(queue, sympy_fn, *sympy_args): + queue.put(from_sympy(sympy_fn(*sympy_args))) + + +def call_with_timeout(evaluation, external_function, *args): + t, start_time = evaluation.timeout_queue[-1] + curr_time = datetime.now().timestamp() + remaining = t + start_time - curr_time + if remaining < 0: + raise TimeoutInterrupt + queue = Queue() + process = Process(target=external_function, args=(queue, *args)) + process.start() + process.join(remaining) + if process.is_alive(): + process.terminate() + evaluation.timeout + raise TimeoutInterrupt + return queue.get() + + class SympyFunction(SympyObject): def apply(self, z, evaluation): # @@ -669,7 +695,14 @@ def apply(self, z, evaluation): args = z.numerify(evaluation).get_sequence() sympy_args = [a.to_sympy() for a in args] sympy_fn = getattr(sympy, self.sympy_name) - return from_sympy(sympy_fn(*sympy_args)) + + if evaluation.timeout_queue: + result = call_with_timeout( + evaluation, call_sympy_function, sympy_fn, *sympy_args + ) + else: + result = from_sympy(sympy_fn(*sympy_args)) + return result def get_constant(self, precision, evaluation, have_mpmath=False): try: diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 4892ac118..5d9c2d274 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -18,7 +18,7 @@ from mathics.builtin.base import Builtin, Predefined from mathics.core.atoms import Integer, Real, String, from_python from mathics.core.attributes import hold_all, no_attributes, protected, read_protected -from mathics.core.evaluation import TimeoutInterrupt, run_with_timeout_and_stack +from mathics.core.interrupt import TimeoutInterrupt from mathics.core.element import ImmutableValueMixin from mathics.core.expression import Expression, to_expression from mathics.core.list import ListExpression, to_mathics_list @@ -1046,8 +1046,16 @@ def apply(self, n, evaluation): "Pause", "numnm", Expression(SymbolPause, from_python(n)) ) return - - time.sleep(sleeptime) + if evaluation.timeout_queue: + while sleeptime > 0 and not evaluation.timeout: + # Fixme: look for a criteria to set the + # granularity + time.sleep(0.01) + sleeptime = sleeptime - 0.01 + if evaluation.timeout: + raise TimeoutInterrupt + else: + time.sleep(sleeptime) return SymbolNull @@ -1088,7 +1096,7 @@ def evaluate(self, evaluation): return Expression(SymbolDateObject.evaluate(evaluation)) -if sys.platform != "win32" and not hasattr(sys, "pyston_version_info"): +if True: # sys.platform != "win32" and not hasattr(sys, "pyston_version_info"): class TimeConstrained(Builtin): r""" @@ -1105,24 +1113,19 @@ class TimeConstrained(Builtin): the evaluation continues after the timeout. However, at the end of the evaluation, the function will return '$Aborted' and the results will not affect the state of the \Mathics kernel. - """ - - # FIXME: these tests sometimes cause SEGVs which probably means - # that TimeConstraint has bugs. + >> TimeConstrained[Pause[1]; x^2 , .1] + = $Aborted - # Consider testing via unit tests. - # >> TimeConstrained[Integrate[Sin[x]^1000000,x],1] - # = $Aborted + >> TimeConstrained[Pause[1]; x^2, .1, sqx] + = sqx - # >> TimeConstrained[Integrate[Sin[x]^1000000,x], 1, Integrate[Cos[x],x]] - # = Sin[x] + >> s = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] + : Number of seconds a is not a positive machine-sized number or Infinity. + = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] - # >> s=TimeConstrained[Integrate[Sin[x] ^ 3, x], a] - # : Number of seconds a is not a positive machine-sized number or Infinity. - # = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] - - # >> a=1; s - # = Cos[x] (-5 + Cos[2 x]) / 6 + >> a=1.; s + = Cos[x] (-3 + Cos[x] ^ 2) / 3 + """ attributes = hold_all | protected messages = { @@ -1144,12 +1147,15 @@ def apply_3(self, expr, t, failexpr, evaluation): try: t = float(t.to_python()) evaluation.timeout_queue.append((t, datetime.now().timestamp())) - request = lambda: expr.evaluate(evaluation) - res = run_with_timeout_and_stack(request, t, evaluation) + res = expr.evaluate(evaluation) except TimeoutInterrupt: - evaluation.timeout_queue.pop() - return failexpr.evaluate(evaluation) - except: + last = evaluation.timeout_queue.pop() + if last == True: + return failexpr.evaluate(evaluation) + # The timeout was not set here. Reraise the + # TimeoutInterrupt exception. + raise TimeoutInterrupt + except Exception as e: evaluation.timeout_queue.pop() raise evaluation.timeout_queue.pop() diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 6e7d953b0..79e2a81eb 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -16,7 +16,12 @@ series_times_series, series_derivative, ) -from mathics.builtin.base import Builtin, PostfixOperator, SympyFunction +from mathics.builtin.base import ( + Builtin, + PostfixOperator, + SympyFunction, + call_with_timeout, +) from mathics.builtin.scoping import dynamic_scoping from mathics.core.atoms import ( @@ -42,6 +47,7 @@ from mathics.core.convert import sympy_symbol_prefix, SympyExpression, from_sympy from mathics.core.evaluation import Evaluation from mathics.core.evaluators import apply_N +from mathics.core.interrupt import TimeoutInterrupt from mathics.core.expression import Expression, to_expression from mathics.core.list import ListExpression, to_mathics_list @@ -496,6 +502,10 @@ def to_sympy(self, expr, **kwargs): return +def call_sympy_integrate(queue, f_sympy, *vars): + queue.put(sympy.integrate(f_sympy, *vars)) + + class Integrate(SympyFunction): r"""
@@ -669,8 +679,15 @@ def apply(self, f, xs, evaluation, options): else: vars.append((x, a, b)) try: - sympy_result = sympy.integrate(f_sympy, *vars) + if evaluation.timeout_queue: + sympy_result = call_with_timeout( + evaluation, call_sympy_integrate, f_sympy, *vars + ) + else: + sympy_result = sympy.integrate(f_sympy, *vars) pass + except TimeoutInterrupt: + raise except sympy.PolynomialError: return except ValueError: diff --git a/mathics/builtin/procedural.py b/mathics/builtin/procedural.py index 572bd76eb..df31566da 100644 --- a/mathics/builtin/procedural.py +++ b/mathics/builtin/procedural.py @@ -195,6 +195,7 @@ def apply(self, expr, evaluation): for expr in items: prev_result = result + result = expr.evaluate(evaluation) # `expr1; expr2;` returns `Null` but assigns `expr2` to `Out[n]`. diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 6e3a1b503..451b00b64 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -7,6 +7,7 @@ import os import sys from threading import Thread, stack_size as set_thread_stack_size +from datetime import datetime, timedelta from typing import Tuple @@ -94,53 +95,6 @@ def set_python_recursion_limit(n) -> None: raise OverflowError -def run_with_timeout_and_stack(request, timeout, evaluation): - """ - interrupts evaluation after a given time period. Provides a suitable stack environment. - """ - - # only use set_thread_stack_size if max recursion depth was changed via the environment variable - # MATHICS_MAX_RECURSION_DEPTH. if it is set, we always use a thread, even if timeout is None, in - # order to be able to set the thread stack size. - - if MAX_RECURSION_DEPTH > settings.DEFAULT_MAX_RECURSION_DEPTH: - set_thread_stack_size(python_stack_size(MAX_RECURSION_DEPTH)) - elif timeout is None: - return request() - - queue = Queue(maxsize=1) # stores the result or exception - thread = Thread(target=_thread_target, args=(request, queue)) - thread.start() - - # Thead join(timeout) can leave zombie threads (we are the parent) - # when a time out occurs, but the thread hasn't terminated. See - # https://docs.python.org/3/library/multiprocessing.shared_memory.html - # for a detailed discussion of this. - # - # To reduce this problem, we make use of specific properties of - # the Mathics evaluator: if we set "evaluation.timeout", the - # next call to "Expression.evaluate" in the thread will finish it - # immediately. - # - # However this still will not terminate long-running processes - # in Sympy or or libraries called by Mathics that might hang or run - # for a long time. - thread.join(timeout) - if thread.is_alive(): - evaluation.timeout = True - while thread.is_alive(): - pass - evaluation.timeout = False - evaluation.stopped = False - raise TimeoutInterrupt() - - success, result = queue.get() - if success: - return result - else: - raise result[0].with_traceback(result[1], result[2]) - - class Out(KeyComparable): def __init__(self) -> None: self.is_message = False @@ -233,7 +187,6 @@ def __init__( definitions = Definitions() self.definitions = definitions self.recursion_depth = 0 - self.timeout = False self.timeout_queue = [] self.stopped = False self.out = [] @@ -298,7 +251,6 @@ def evaluate(self, query, timeout=None, format=None): self.start_time = time.time() self.recursion_depth = 0 - self.timeout = False self.stopped = False self.exc_result = self.SymbolNull self.last_eval = None @@ -308,7 +260,6 @@ def evaluate(self, query, timeout=None, format=None): line_no = self.definitions.get_line_no() line_no += 1 self.definitions.set_line_no(line_no) - history_length = self.definitions.get_history_length() result = None @@ -349,9 +300,11 @@ def evaluate(): self.exec_result = self.SymbolNull return None + if timeout: + self.timeout_queue.append((timeout, datetime.now().timestamp())) try: try: - result = run_with_timeout_and_stack(evaluate, timeout, self) + result = evaluate() except KeyboardInterrupt: if self.catch_interrupt: self.exc_result = SymbolAborted @@ -387,7 +340,6 @@ def evaluate(): self.exc_result = Expression(SymbolHold, Expression(SymbolContinue)) except TimeoutInterrupt: self.stopped = False - self.timeout = True self.message("General", "timeout") self.exc_result = SymbolAborted except AbortInterrupt: # , error: @@ -405,6 +357,8 @@ def evaluate(): finally: self.stop() + if timeout: + self.timeout_queue.pop() history_length = self.definitions.get_history_length() line = line_no - history_length @@ -573,6 +527,24 @@ def check_stopped(self) -> None: if self.stopped: raise TimeoutInterrupt + @property + def timeout(self) -> bool: + curr_time = datetime.now().timestamp() + timeout_levels = len(self.timeout_queue) + for i in range(timeout_levels): + entry = self.timeout_queue[-i - 1] + if entry == True: + return True + t, start = entry + if curr_time - start > t: + self.timeout_queue[-i - 1] = True + return True + return False + + @timeout.setter + def timeout(self, value: bool): + raise ValueError("you cannot set timeout") + def inc_recursion_depth(self) -> None: self.check_stopped() limit = self.definitions.get_config_value( diff --git a/mathics/core/expression.py b/mathics/core/expression.py index a7dc13fc9..a9567e8b1 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -51,6 +51,10 @@ SymbolBlank, SymbolSequence, ) +from mathics.core.interrupt import ( + TimeoutInterrupt, +) + from mathics.core.atoms import String # from mathics.core.util import timeit @@ -440,7 +444,7 @@ def evaluate( Evaluation is a recusive:``rewrite_apply_eval_step()`` may call us. """ if evaluation.timeout: - return + raise TimeoutInterrupt expr = self reevaluate = True diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 44433b23e..8c6c839bd 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -2,6 +2,7 @@ # cython: language_level=3 # -*- coding: utf-8 -*- +from mathics.core.interrupt import TimeoutInterrupt from mathics.core.element import KeyComparable from mathics.core.expression import Expression from mathics.core.symbols import strip_context @@ -208,6 +209,7 @@ def do_replace(self, expression, vars, options, evaluation): vars_noctx = dict(((strip_context(s), vars[s]) for s in vars)) if self.pass_expression: vars_noctx["expression"] = expression + if options: return self.function(evaluation=evaluation, options=options, **vars_noctx) else: diff --git a/test/builtin/test_datentime.py b/test/builtin/test_datentime.py index 9430a923f..9631461d2 100644 --- a/test/builtin/test_datentime.py +++ b/test/builtin/test_datentime.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from test.helper import check_evaluation, evaluate +from mathics.core.symbols import Symbol import pytest import sys @@ -7,26 +8,65 @@ import time -@pytest.mark.skipif( - sys.platform in ("win32",) or hasattr(sys, "pyston_version_info"), - reason="TimeConstrained needs to be rewritten", -) -def test_timeremaining(): - str_expr = "TimeConstrained[1+2; TimeRemaining[], 0.9]" - result = evaluate(str_expr) - assert result is None or 0 < result.to_python() < 9 - - -@pytest.mark.skip(reason="TimeConstrained needs to be rewritten") -def test_timeconstrained1(): - # +def test_timeconstrained_assignment_1(): + # This test str_expr1 = "a=1.; TimeConstrained[Do[Pause[.1];a=a+1,{1000}],1]" result = evaluate(str_expr1) str_expected = "$Aborted" expected = evaluate(str_expected) assert result == expected time.sleep(1) - assert evaluate("a").to_python() == 10 + # if all the operations where instantaneous, then the + # value of ``a`` should be 10. However, in macOS, ``a`` + # just reach 3... + result = evaluate("a").to_python() + assert result <= 10 + + +def test_timeconstrained_assignment_2(): + # This test checks if the assignment is really aborted + # if the RHS exceeds the wall time. + str_expr1 = "a=1.; TimeConstrained[a=(Pause[.2];2.),.1]" + result = evaluate(str_expr1) + str_expected = "$Aborted" + expected = evaluate(str_expected) + assert result == expected + time.sleep(0.2) + assert evaluate("a").to_python() == 1.0 + + +def test_timeconstrained_assignment_3(): + # This test checks if the assignment is really aborted + # if the RHS exceeds the wall time. + str_expr1 = ( + "a=1.;TimeConstrained[TimeConstrained[a=(Pause[.1];2.), .3, a=-2], .1,a=-3]" + ) + result = evaluate(str_expr1) + str_expected = "-3" + expected = evaluate(str_expected) + assert result == expected + time.sleep(0.3) + assert evaluate("a").to_python() == -3 + + +def test_timeconstrained_sympy(): + # This test tries to run a large and onerous calculus that runs + # in sympy (outside the control of Mathics). + # If the behaviour is the right one, the evaluation + # is interrupted before it saturates memory and raise a SIGEV + # exception. + str_expr = "TimeConstrained[Integrate[Sin[x]^1000000, x], 0.9]" + result = evaluate(str_expr) + assert result is None or result == Symbol("$Aborted") + + +def test_timeremaining(): + str_expr = "TimeConstrained[1+2;Pause[2]; TimeRemaining[], 10.]" + result = evaluate(str_expr) + assert result is None or 0 < result.to_python() < 10.0, ( + result, + "must belong to the interval [0, 10]", + ) def test_datelist(): @@ -55,7 +95,7 @@ def test_datelist(): check_evaluation(str_expr, str_expected) -def test_datestring(): +def test_datestring2(): for str_expr, str_expected in ( ## Check Leading 0s # ( From bc26f13c179619d72625cee0b43e65526ae4abe7 Mon Sep 17 00:00:00 2001 From: mmatera Date: Mon, 13 Jun 2022 07:39:51 -0300 Subject: [PATCH 2/3] fixing the time in TimeConstrained test --- mathics/builtin/datentime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 5d9c2d274..51761ae16 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -1123,7 +1123,7 @@ class TimeConstrained(Builtin): : Number of seconds a is not a positive machine-sized number or Infinity. = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] - >> a=1.; s + >> a=10.; s = Cos[x] (-3 + Cos[x] ^ 2) / 3 """ From e26c2905d64f60a0d8692dfbd29a35728e72bfb4 Mon Sep 17 00:00:00 2001 From: mmatera Date: Mon, 13 Jun 2022 08:03:02 -0300 Subject: [PATCH 3/3] removing condition arround TimeConstrained --- mathics/builtin/datentime.py | 104 +++++++++++++++++------------------ 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index 51761ae16..391e63b15 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -1096,70 +1096,68 @@ def evaluate(self, evaluation): return Expression(SymbolDateObject.evaluate(evaluation)) -if True: # sys.platform != "win32" and not hasattr(sys, "pyston_version_info"): - - class TimeConstrained(Builtin): - r""" -
-
'TimeConstrained[$expr$, $t$]' -
'evaluates $expr$, stopping after $t$ seconds.' +class TimeConstrained(Builtin): + r""" +
+
'TimeConstrained[$expr$, $t$]' +
'evaluates $expr$, stopping after $t$ seconds.' -
'TimeConstrained[$expr$, $t$, $failexpr$]' -
'returns $failexpr$ if the time constraint is not met.' -
+
'TimeConstrained[$expr$, $t$, $failexpr$]' +
'returns $failexpr$ if the time constraint is not met.' +
- Possible issues: for certain time-consuming functions (like simplify) - which are based on sympy or other libraries, it is possible that - the evaluation continues after the timeout. However, at the end of the evaluation, the function will return '$Aborted' and the results will not affect - the state of the \Mathics kernel. + Possible issues: for certain time-consuming functions (like simplify) + which are based on sympy or other libraries, it is possible that + the evaluation continues after the timeout. However, at the end of the evaluation, the function will return '$Aborted' and the results will not affect + the state of the \Mathics kernel. - >> TimeConstrained[Pause[1]; x^2 , .1] - = $Aborted + >> TimeConstrained[Pause[1]; x^2 , .1] + = $Aborted - >> TimeConstrained[Pause[1]; x^2, .1, sqx] - = sqx + >> TimeConstrained[Pause[1]; x^2, .1, sqx] + = sqx - >> s = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] - : Number of seconds a is not a positive machine-sized number or Infinity. - = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] + >> s = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] + : Number of seconds a is not a positive machine-sized number or Infinity. + = TimeConstrained[Integrate[Sin[x] ^ 3, x], a] - >> a=10.; s - = Cos[x] (-3 + Cos[x] ^ 2) / 3 - """ + >> a=10.; s + = Cos[x] (-3 + Cos[x] ^ 2) / 3 + """ - attributes = hold_all | protected - messages = { - "timc": "Number of seconds `1` is not a positive machine-sized number or Infinity.", - } + attributes = hold_all | protected + messages = { + "timc": "Number of seconds `1` is not a positive machine-sized number or Infinity.", + } - summary_text = "run a command for at most a specified time" + summary_text = "run a command for at most a specified time" - def apply_2(self, expr, t, evaluation): - "TimeConstrained[expr_, t_]" - return self.apply_3(expr, t, SymbolAborted, evaluation) + def apply_2(self, expr, t, evaluation): + "TimeConstrained[expr_, t_]" + return self.apply_3(expr, t, SymbolAborted, evaluation) - def apply_3(self, expr, t, failexpr, evaluation): - "TimeConstrained[expr_, t_, failexpr_]" - t = t.evaluate(evaluation) - if not t.is_numeric(evaluation): - evaluation.message("TimeConstrained", "timc", t) - return - try: - t = float(t.to_python()) - evaluation.timeout_queue.append((t, datetime.now().timestamp())) - res = expr.evaluate(evaluation) - except TimeoutInterrupt: - last = evaluation.timeout_queue.pop() - if last == True: - return failexpr.evaluate(evaluation) - # The timeout was not set here. Reraise the - # TimeoutInterrupt exception. - raise TimeoutInterrupt - except Exception as e: - evaluation.timeout_queue.pop() - raise + def apply_3(self, expr, t, failexpr, evaluation): + "TimeConstrained[expr_, t_, failexpr_]" + t = t.evaluate(evaluation) + if not t.is_numeric(evaluation): + evaluation.message("TimeConstrained", "timc", t) + return + try: + t = float(t.to_python()) + evaluation.timeout_queue.append((t, datetime.now().timestamp())) + res = expr.evaluate(evaluation) + except TimeoutInterrupt: + last = evaluation.timeout_queue.pop() + if last == True: + return failexpr.evaluate(evaluation) + # The timeout was not set here. Reraise the + # TimeoutInterrupt exception. + raise TimeoutInterrupt + except Exception as e: evaluation.timeout_queue.pop() - return res + raise + evaluation.timeout_queue.pop() + return res class TimeZone(Predefined):