using System.Globalization; using AIStudio.Chat; using AngouriMath; using AngouriMath.Core.Exceptions; namespace AIStudio.Tools.Mathematics; /// /// Runs the computations of the Mathematics tools, one at a time and within a time budget. /// /// /// AngouriMath parses and simplifies recursively. About 500 nested parentheses overflow the stack of /// a thread pool thread, which ends the whole process, so every computation runs on a thread of its /// own with a stack of 64 MB. That holds the deepest nesting an expression within /// MathLimits.MAX_EXPRESSION_LENGTH can have.

/// Only one computation runs at a time, because AI Studio also runs on a Raspberry Pi. AngouriMath /// stops at the cancellation token of its thread wherever it checks it, and that token ends with the /// budget or with the request. Some computations never check it, such as an exact power with /// millions of digits; MathGuards refuses those which can be told beforehand. One which still runs /// past its budget keeps the slot until it ends: the call gives up and says so, and the next /// computations wait for the slot instead of taking another core.

/// The settings of AngouriMath belong to the flow which sets them. Each computation reads /// expressions explicitly and solves without Newton's method, see AngouriMathBehaviorTests. ///
/// The logger. public sealed class MathComputationService(ILogger logger) { private const int STACK_SIZE_BYTES = 64 * 1024 * 1024; private const int MAX_MESSAGE_LENGTH = 400; /// /// How long a computation may take, and how long it waits for the slot before that. /// public static readonly TimeSpan DEFAULT_TIME_BUDGET = TimeSpan.FromSeconds(10); /// /// How long a call waits beyond the budget for a computation to stop at its cancellation token. /// private static readonly TimeSpan STOP_GRACE = TimeSpan.FromSeconds(1); private readonly SemaphoreSlim slot = new(1, 1); /// /// How long a computation may take. Tests shorten it. /// internal TimeSpan TimeBudget { get; init; } = DEFAULT_TIME_BUDGET; /// /// Runs a computation on a thread of its own, once the slot is free. /// /// /// Parse the expressions inside the computation, too: the parser needs the stack of its thread. /// An exception of AngouriMath comes back as a MathComputationException with its message. /// /// The computation, which gets the token of its budget. /// The cancellation token of the request. /// The type of the result. /// The result of the computation. /// The computation was refused, failed, or did not finish within its budget. public async Task ComputeAsync(Func computation, CancellationToken token = default) { if (!await this.slot.WaitAsync(this.TimeBudget, token)) throw new MathComputationException($"Another computation is still running, so this one could not start within {this.BudgetInSeconds} seconds."); // The thread disposes the budget and frees the slot once its computation ended. Should the thread not // even start, the slot is freed below, and the budget is left to the garbage collector: var budget = CancellationTokenSource.CreateLinkedTokenSource(token); var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var thread = new Thread(() => this.Run(computation, budget, completion, token), STACK_SIZE_BYTES) { IsBackground = true, Name = "Mathematics", }; try { budget.CancelAfter(this.TimeBudget); thread.Start(); } catch { this.slot.Release(); throw; } try { return await completion.Task.WaitAsync(this.TimeBudget + STOP_GRACE, token); } catch (TimeoutException) { logger.LogWarning("A computation of the Mathematics tools ran past its time budget of {Seconds} seconds without stopping. It keeps the computing slot until it ends.", this.BudgetInSeconds); throw new MathComputationException($"The computation did not finish within {this.BudgetInSeconds} seconds and could not be stopped. It keeps running until it ends, and other computations wait for it meanwhile."); } } private string BudgetInSeconds => this.TimeBudget.TotalSeconds.ToString(CultureInfo.InvariantCulture); private void Run(Func computation, CancellationTokenSource budget, TaskCompletionSource completion, CancellationToken token) { try { MathS.Multithreading.SetLocalCancellationToken(budget.Token); using var explicitParsing = MathS.Settings.ExplicitParsingOnly.Set(true); using var withoutNewton = MathS.Settings.AllowNewton.Set(false); completion.TrySetResult(computation(budget.Token)); } catch (OperationCanceledException) when (token.IsCancellationRequested) { completion.TrySetCanceled(token); } catch (OperationCanceledException) when (budget.IsCancellationRequested) { completion.TrySetException(new MathComputationException($"The computation did not finish within {this.BudgetInSeconds} seconds.")); } catch (AngouriMathBaseException exception) { logger.LogInformation("AngouriMath could not compute an expression: {Message}", exception.Message); completion.TrySetException(new MathComputationException($"The tool could not compute this:{exception.Message.Shorten(MAX_MESSAGE_LENGTH)}")); } catch (Exception exception) { completion.TrySetException(exception); } finally { budget.Dispose(); this.slot.Release(); } } }