Skip to content

Commit f1ae1d2

Browse files
dudikeleticursoragentandrewlock
authored
[Debugger] Bound Dynamic Instrumentation probe expression evaluation with a time budget (#8806)
## Summary of changes - Adds a cooperative **evaluation time budget** to Dynamic Instrumentation (DI) probe expressions, so checkpointed generated evaluation work stops when expiry is observed; regex additionally receives a hard timeout. - Introduces `EvaluationBudget` (a value type passed by `ref` through compiled evaluation paths), `CompiledExpressionDelegate<T>` (delegate signature now carries `ref EvaluationBudget`), and `EvaluationTimeBudgetExceededException`. - The parser injects lightweight budget checkpoints into the generated expression tree at the points that can dominate runtime: the expression root, generated collection and dump loops, selected string/comparison operations, regex, and `instanceof` type resolution. - A single active-time budget is shared across templates → condition → metric → span decorations → capture expressions. It is paused before deferred capture work and resumed after capture-expression compilation, so intervening processing and compilation do not consume the remaining allowance. - Hand-rolls the enumerable `any`/`all`/`filter` loops (instead of LINQ) so a checkpoint can be placed inside each iteration; uses a real `Regex` timeout for pattern matching. - Removes budget checkpoints from two paths where they add cost without value: the `SafeEquals` binary path and member access, which is already restricted to fields and auto-property backing fields. ## Reason for change - DI probe expressions are authored remotely and compiled to delegates that execute **in the customer's process, on the customer's thread**. A pathological or accidentally expensive expression (large collections, nested filters, complex predicates, catastrophic regex) could add unbounded latency and degrade the host application. - There was no evaluation time budget. This change makes supported expensive paths self-limiting: once expiry is observed at a cooperative checkpoint, evaluation aborts and surfaces an evaluation error; regex also receives a hard timeout. ## Implementation details ### The budget (`EvaluationBudget`) - Mutable value type passed by `ref` through compiled delegates and evaluation helpers. Its state is deliberately copied when persisted between the main and deferred-capture phases and when bridged through type-resolution callbacks; the budget itself does not require a heap allocation. - `Create(maxMs)` records a deadline as `Stopwatch.GetTimestamp() + duration` (with an overflow guard so very large values clamp to `long.MaxValue`). - `ThrowIfExceeded()` is the hot amortized checkpoint used in loops and selected operations: reading the clock on every operation would be too expensive, so it samples the clock once every `OperationsBeforeTimeCheck` (32) calls. - The hot checkpoint is marked `[MethodImpl(AggressiveInlining)]`; the throw/clock helpers are `[NoInlining]` to keep the inlined path tiny. - `ThrowIfExceededImmediately()` samples the clock at expression roots/fallbacks and expensive type-resolution boundaries. - `TimedOut` is sticky: once the deadline is hit, all later checkpoints throw immediately. - `GetRemainingTimeout()` converts the remaining budget into a `TimeSpan` and is handed to `Regex.IsMatch(...)`. Regex can block for a long time inside a single call, so it gets a real hard timeout; a `RegexMatchTimeoutException` is converted into the budget exception (and marks the budget timed out). - `Pause()` stores the remaining stopwatch ticks instead of an absolute deadline; `Resume()` rebases the deadline from that remaining active-evaluation allowance. ```mermaid flowchart TD Start["ThrowIfExceeded() — inlined into the hot path"] --> T{TimedOut?} T -- "yes (sticky)" --> Throw["ThrowTimedOut() [NoInlining]"] T -- no --> Dec["--operationsUntilTimeCheck"] Dec --> C{"> 0 ?"} C -- "yes (31 of 32 calls)" --> Ret["return — no clock read"] C -- "no (every 32nd call)" --> Clock["ThrowIfTimeExceeded() [NoInlining]<br/>reset counter to 32, read Stopwatch"] Clock --> D{"now >= deadline?"} D -- no --> Ret D -- yes --> Mark["MarkTimedOut() + throw EvaluationTimeBudgetExceededException"] ``` ### Threading the budget through compiled expressions - `CompiledExpressionDelegate<T>` adds a trailing `ref EvaluationBudget budget` parameter; `CompiledExpression<T>.BudgetedDelegate` is the compiled instance. - The parser creates an `evaluationBudget` `ref` parameter for the generated lambda and emits ordinary `EvaluationBudget.ThrowIfExceeded(ref evaluationBudget)` checks (`BudgetCheck()`) at strategic spots, with immediate checks at roots and type-resolution boundaries. - **One active-time budget per probe hit:** `ProbeExpressionEvaluator.Evaluate` creates a budget after initial expression compilation and passes the same `ref` to every sub-expression. If deferred capture expressions are present, it pauses and stores the budget on the (`ref struct`) result. `EvaluateCaptureExpressions` compiles those expressions while the budget is paused, then resumes and evaluates them with the same remaining active-time allowance. ```mermaid flowchart TD A["Probe hit → compile/cache initial expressions"] --> B["CreateBudget(): deadline = now + configured max"] B --> C["Templates(..., ref budget)"] C --> D["Condition(..., ref budget)"] D --> E["Metric(..., ref budget)"] E --> F["Span decorations(..., ref budget)"] F --> G["Pause budget and store remaining active time in result"] G --> H["Compile deferred capture expressions while paused"] H --> I["Resume and evaluate captures with remaining active-time budget"] ``` ### Where checkpoints are injected (`BudgetCheck()`) - **Root:** an immediate check at the top of every compiled expression (including the fallback delegate) guarantees even a trivial expression observes an already-exceeded budget. - **Collection loops:** `any` / `all` / `filter` are hand-built loops (`BuildEnumerableLoop`) with checkpoints before enumeration and at the top of each iteration; enumerator disposal uses `Expression.TryFinally`. The bounded capture-filter path checks once per item and passes the same budget into the predicate so nested checkpointed operations share it. - **String operations** that scale with input length: `Substring`, `Contains` / `StartsWith` / `EndsWith`, `IsEmpty` (string and collection length), and string lexicographic comparisons. - **Dumps:** generated collection/dictionary dump loops. - **Regex:** real `Regex.IsMatch` timeout via `GetRemainingTimeout()`. - **`instanceof`:** immediate checks around loaded-assembly scans and type-resolution callbacks. ### Checkpoints intentionally removed - **`SafeEquals`:** equality only dispatches to an allowlisted set of `Equals` implementations that are bounded and fast, so a per-comparison checkpoint added overhead with no protective value. - **Member access:** no per-access checkpoint is needed because the current resolver emits fields or compiler-generated auto-property backing fields, rejects side-effecting getters, and guards static initialization. Removing the wrapping block also eliminated a stray rendering artifact in the expression snapshots, so the affected snapshots revert to their clean pre-budget form (no semantic change to results/errors). ### Notes / risks - **Behavior change:** expressions that exceed the budget now throw `EvaluationTimeBudgetExceededException`, surfaced as an evaluation error. As before, a condition that errors defaults to `true`. - **Config:** `DD_INTERNAL_DYNAMIC_INSTRUMENTATION_MAX_EVALUATION_TIME_MS` defaults to 50 ms and accepts values from 10–1000 ms; invalid or missing values use the default. The configured value is propagated through probe processors into their evaluators. - **Trimming:** `Datadog.Trace.Trimming.xml` gains a `System.Linq.Expressions.TryExpression` entry (auto-generated) because the new enumerable loops use `Expression.TryFinally`. - **Hot path:** ordinary checkpoints are aggressively inlined and sample the clock only every 32 calls; roots and expensive type-resolution boundaries check immediately, and throw/clock helpers remain non-inlined. ## Test coverage - `DebuggerExpressionLanguageTests` covers timeout propagation, immediate root/fallback checks, pause/resume and shared capture-budget behavior, regex timeout handling, and budgeted type resolution; expression snapshots were regenerated for the new loop structure while sanitizing budget plumbing from their rendered form. - `DebuggerSettingsTests` covers the configured range/default behavior, and `ProbeProcessorTests` covers propagation and updates of the configured evaluation limit. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Andrew Lock <andrew.lock@datadoghq.com>
1 parent 90443cf commit f1ae1d2

66 files changed

Lines changed: 1779 additions & 627 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

tracer/missing-nullability-files.csv

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,13 @@ src/Datadog.Trace/DataStreamsMonitoring/Transport/DataStreamsTransportStrategy.c
232232
src/Datadog.Trace/DataStreamsMonitoring/Utils/BinaryPrimitivesHelper.cs
233233
src/Datadog.Trace/Debugger/Configurations/Trie.cs
234234
src/Datadog.Trace/Debugger/Expressions/CaptureInfo.cs
235-
src/Datadog.Trace/Debugger/Expressions/CompiledExpression.cs
236235
src/Datadog.Trace/Debugger/Expressions/Enums.cs
237236
src/Datadog.Trace/Debugger/Expressions/MethodScopeMembers.cs
238237
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Binary.cs
239238
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Collection.cs
240239
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.cs
241240
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Dump.cs
242241
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.General.cs
243-
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.String.cs
244242
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParser.Unary.cs
245243
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionParserHelper.cs
246244
src/Datadog.Trace/Debugger/Expressions/ProbeExpressionsProcessor.cs

tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@
376376
<type fullname="System.Linq.Expressions.NewArrayExpression" />
377377
<type fullname="System.Linq.Expressions.NewExpression" />
378378
<type fullname="System.Linq.Expressions.ParameterExpression" />
379+
<type fullname="System.Linq.Expressions.TryExpression" />
379380
<type fullname="System.Linq.Expressions.TypeBinaryExpression" />
380381
<type fullname="System.Linq.Expressions.UnaryExpression" />
381382
<type fullname="System.Runtime.CompilerServices.CallSite" />

tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,16 @@ supportedConfigurations:
831831
documentation: |-
832832
Configuration key for enabling or disabling Dynamic Instrumentation.
833833
Default value is false (disabled).
834+
DD_DYNAMIC_INSTRUMENTATION_EVALUATION_TIMEOUT_MS:
835+
- implementation: A
836+
scope: managed
837+
type: int
838+
default: '50'
839+
product: Debugger
840+
const_name: EvaluationTimeoutMs
841+
documentation: |-
842+
Configuration key for the maximum elapsed time, in milliseconds, allowed for evaluating probe expressions.
843+
Values from <c>10</c> to <c>1000</c> are accepted. Default value is <c>50</c>.
834844
DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE:
835845
- implementation: B
836846
scope: managed

tracer/src/Datadog.Trace/Debugger/DebuggerSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ internal sealed record DebuggerSettings
2121
public const string DebuggerMetricPrefix = "dynamic.instrumentation.metric.probe";
2222
public const int DefaultMaxDepthToSerialize = 3;
2323
public const int DefaultMaxSerializationTimeInMilliseconds = 200;
24+
public const int DefaultMaxEvaluationTimeInMilliseconds = 50;
2425
public const int DefaultMaxNumberOfItemsInCollectionToCopy = 100;
2526
public const int DefaultMaxNumberOfFieldsToCopy = 20;
2627
public const int DefaultMaxStringLength = 1000;
2728
public const int DefaultMaxProbesPerType = 0;
2829

30+
private const int MinAllowedEvaluationTimeInMilliseconds = 10;
31+
private const int MaxAllowedEvaluationTimeInMilliseconds = 1000;
2932
private const int DefaultUploadBatchSize = 100;
3033
public const int DefaultSymbolBatchSizeInBytes = 1 * 1024 * 1024; // 1 MB
3134
private const int DefaultDiagnosticsIntervalSeconds = 60 * 60; // 1 hour
@@ -55,6 +58,13 @@ public DebuggerSettings(IConfigurationSource? source, IConfigurationTelemetry te
5558
serializationTimeThreshold => serializationTimeThreshold > 0)
5659
.Value;
5760

61+
MaxEvaluationTimeInMilliseconds = config
62+
.WithKeys(ConfigurationKeys.Debugger.EvaluationTimeoutMs)
63+
.AsInt32(
64+
DefaultMaxEvaluationTimeInMilliseconds,
65+
evaluationTimeThreshold => evaluationTimeThreshold is >= MinAllowedEvaluationTimeInMilliseconds and <= MaxAllowedEvaluationTimeInMilliseconds)
66+
.Value;
67+
5868
UploadBatchSize = config
5969
.WithKeys(ConfigurationKeys.Debugger.UploadBatchSize)
6070
.AsInt32(DefaultUploadBatchSize, batchSize => batchSize > 0)
@@ -168,6 +178,8 @@ public DebuggerSettings(IConfigurationSource? source, IConfigurationTelemetry te
168178

169179
public int MaxSerializationTimeInMilliseconds { get; }
170180

181+
public int MaxEvaluationTimeInMilliseconds { get; }
182+
171183
public int MaximumDepthOfMembersToCopy { get; }
172184

173185
public int UploadBatchSize { get; }

tracer/src/Datadog.Trace/Debugger/DynamicInstrumentation.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ private void StartBackgroundProcess()
312312
lineProbes.Add(new NativeLineProbeDefinition(location!.ProbeDefinition.Id, location.Mvid, location.MethodToken, (int)location.BytecodeOffset, location.LineNumber, location.ProbeDefinition.Where.SourceFile));
313313
fetchProbeStatus.Add(new FetchProbeStatus(addedProbe.Id, addedProbe.Version ?? 0));
314314
_lastReportedUnboundProbeErrors.Remove(addedProbe.Id);
315-
ProbeExpressionsProcessor.Instance.AddProbeProcessor(addedProbe);
315+
ProbeExpressionsProcessor.Instance.AddProbeProcessor(addedProbe, _settings.MaxEvaluationTimeInMilliseconds);
316316
SetRateLimit(addedProbe);
317317
break;
318318
case LiveProbeResolveStatus.Unbound:
@@ -351,7 +351,7 @@ private void StartBackgroundProcess()
351351
{
352352
var nativeDefinition = new NativeMethodProbeDefinition(addedProbe.Id, addedProbe.Where.TypeName, addedProbe.Where.MethodName, signature);
353353
methodProbes.Add(nativeDefinition);
354-
ProbeExpressionsProcessor.Instance.AddProbeProcessor(addedProbe);
354+
ProbeExpressionsProcessor.Instance.AddProbeProcessor(addedProbe, _settings.MaxEvaluationTimeInMilliseconds);
355355
SetRateLimit(addedProbe);
356356
}
357357

@@ -692,7 +692,7 @@ private void CheckUnboundProbes(object? sender, AssemblyLoadEventArgs args)
692692
// configured rate would never take effect for that probe.
693693
foreach (var boundProbe in boundProbes)
694694
{
695-
ProbeExpressionsProcessor.Instance.AddProbeProcessor(boundProbe);
695+
ProbeExpressionsProcessor.Instance.AddProbeProcessor(boundProbe, _settings.MaxEvaluationTimeInMilliseconds);
696696
SetRateLimit(boundProbe);
697697
}
698698

tracer/src/Datadog.Trace/Debugger/ExceptionAutoInstrumentation/ExceptionReplayProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public void LogException(Exception ex, IDebuggerSnapshotCreator inSnapshotCreato
253253
shadowStack.Leave(snapshotCreator.TrackedStackFrameNode);
254254
}
255255

256-
public IProbeProcessor UpdateProbeProcessor(ProbeDefinition probe)
256+
public IProbeProcessor UpdateProbeProcessor(ProbeDefinition probe, int maxEvaluationTimeInMilliseconds)
257257
{
258258
return this;
259259
}

tracer/src/Datadog.Trace/Debugger/Expressions/CompiledExpression.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

6-
using System;
6+
#nullable enable
7+
78
using System.Linq.Expressions;
89
using Datadog.Trace.Debugger.Models;
910

@@ -12,23 +13,23 @@ namespace Datadog.Trace.Debugger.Expressions
1213
internal readonly record struct CompiledExpression<T>
1314
{
1415
internal CompiledExpression(
15-
Func<ScopeMember, ScopeMember, ScopeMember, Exception, ScopeMember[], T> @delegate,
16-
Expression parsedExpression,
17-
string rawExpression,
18-
EvaluationError[] errors)
16+
CompiledExpressionDelegate<T>? @delegate,
17+
Expression? parsedExpression,
18+
string? rawExpression,
19+
EvaluationError[]? errors)
1920
{
20-
Delegate = @delegate;
21+
BudgetedDelegate = @delegate;
2122
ParsedExpression = parsedExpression;
2223
RawExpression = rawExpression;
2324
Errors = errors;
2425
}
2526

26-
internal Func<ScopeMember, ScopeMember, ScopeMember, Exception, ScopeMember[], T> Delegate { get; }
27+
internal CompiledExpressionDelegate<T>? BudgetedDelegate { get; }
2728

28-
internal Expression ParsedExpression { get; }
29+
internal Expression? ParsedExpression { get; }
2930

30-
internal string RawExpression { get; }
31+
internal string? RawExpression { get; }
3132

32-
internal EvaluationError[] Errors { get; }
33+
internal EvaluationError[]? Errors { get; }
3334
}
3435
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// <copyright file="CompiledExpressionDelegate.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
using Datadog.Trace.Debugger.Models;
10+
11+
namespace Datadog.Trace.Debugger.Expressions;
12+
13+
internal delegate T CompiledExpressionDelegate<T>(
14+
ScopeMember invocationTarget,
15+
ScopeMember returnValue,
16+
ScopeMember duration,
17+
Exception exception,
18+
ScopeMember[] members,
19+
ref EvaluationBudget budget);
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// <copyright file="EvaluationBudget.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
using System.Diagnostics;
10+
using System.Diagnostics.CodeAnalysis;
11+
using System.Runtime.CompilerServices;
12+
13+
namespace Datadog.Trace.Debugger.Expressions;
14+
15+
internal struct EvaluationBudget
16+
{
17+
private const int OperationsBeforeTimeCheck = 32;
18+
private static readonly double StopwatchTicksPerMillisecond = Stopwatch.Frequency / 1000.0;
19+
20+
// Stores an absolute deadline while running and remaining ticks while paused.
21+
// Reusing one field keeps this hot-path struct compact.
22+
private long _deadlineOrRemainingStopwatchTicks;
23+
private int _operationsUntilTimeCheck;
24+
private EvaluationBudgetState _state;
25+
26+
private EvaluationBudget(long deadlineTimestamp)
27+
{
28+
_deadlineOrRemainingStopwatchTicks = deadlineTimestamp;
29+
_operationsUntilTimeCheck = OperationsBeforeTimeCheck;
30+
_state = EvaluationBudgetState.Running;
31+
}
32+
33+
private enum EvaluationBudgetState : byte
34+
{
35+
Uninitialized,
36+
Running,
37+
Paused,
38+
TimedOut
39+
}
40+
41+
internal readonly bool IsInitialized => _state != EvaluationBudgetState.Uninitialized;
42+
43+
internal readonly bool IsPaused => _state == EvaluationBudgetState.Paused;
44+
45+
internal readonly bool TimedOut => _state == EvaluationBudgetState.TimedOut;
46+
47+
internal static EvaluationBudget Create(int maxEvaluationTimeInMilliseconds)
48+
{
49+
var now = Stopwatch.GetTimestamp();
50+
var duration = ToStopwatchTicks(maxEvaluationTimeInMilliseconds);
51+
var deadline = long.MaxValue - now <= duration ? long.MaxValue : now + duration;
52+
return new EvaluationBudget(deadline);
53+
}
54+
55+
internal static void ThrowIfExceeded(ref EvaluationBudget budget)
56+
{
57+
budget.ThrowIfExceeded();
58+
}
59+
60+
internal static void ThrowIfExceededImmediately(ref EvaluationBudget budget)
61+
{
62+
budget.ThrowIfExceededImmediately();
63+
}
64+
65+
private static long ToStopwatchTicks(int milliseconds)
66+
{
67+
if (milliseconds <= 0)
68+
{
69+
return 0;
70+
}
71+
72+
return (long)(milliseconds * StopwatchTicksPerMillisecond);
73+
}
74+
75+
[DoesNotReturn]
76+
private static void ThrowTimedOut()
77+
{
78+
throw new EvaluationTimeBudgetExceededException();
79+
}
80+
81+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
82+
internal void ThrowIfExceeded()
83+
{
84+
if (_state != EvaluationBudgetState.Running)
85+
{
86+
MarkTimedOutAndThrow();
87+
}
88+
89+
if (--_operationsUntilTimeCheck > 0)
90+
{
91+
return;
92+
}
93+
94+
ThrowIfTimeExceeded();
95+
}
96+
97+
internal void ThrowIfExceededImmediately()
98+
{
99+
ThrowIfTimeExceeded();
100+
}
101+
102+
internal void Pause()
103+
{
104+
if (_state != EvaluationBudgetState.Running)
105+
{
106+
return;
107+
}
108+
109+
_deadlineOrRemainingStopwatchTicks -= Stopwatch.GetTimestamp();
110+
_state = EvaluationBudgetState.Paused;
111+
}
112+
113+
internal void Resume()
114+
{
115+
if (_state != EvaluationBudgetState.Paused)
116+
{
117+
return;
118+
}
119+
120+
var remainingStopwatchTicks = _deadlineOrRemainingStopwatchTicks;
121+
var now = Stopwatch.GetTimestamp();
122+
_deadlineOrRemainingStopwatchTicks =
123+
remainingStopwatchTicks <= 0
124+
? now
125+
: long.MaxValue - now <= remainingStopwatchTicks
126+
? long.MaxValue
127+
: now + remainingStopwatchTicks;
128+
_state = EvaluationBudgetState.Running;
129+
}
130+
131+
internal TimeSpan GetRemainingTimeout()
132+
{
133+
ThrowIfTimeExceeded();
134+
135+
var remainingStopwatchTicks = _deadlineOrRemainingStopwatchTicks - Stopwatch.GetTimestamp();
136+
if (remainingStopwatchTicks <= 0)
137+
{
138+
MarkTimedOutAndThrow();
139+
}
140+
141+
var milliseconds = Math.Max(1, (int)(remainingStopwatchTicks / StopwatchTicksPerMillisecond));
142+
return TimeSpan.FromMilliseconds(milliseconds);
143+
}
144+
145+
internal void MarkTimedOut()
146+
{
147+
_state = EvaluationBudgetState.TimedOut;
148+
}
149+
150+
[MethodImpl(MethodImplOptions.NoInlining)]
151+
private void ThrowIfTimeExceeded()
152+
{
153+
if (_state != EvaluationBudgetState.Running)
154+
{
155+
MarkTimedOutAndThrow();
156+
}
157+
158+
_operationsUntilTimeCheck = OperationsBeforeTimeCheck;
159+
if (Stopwatch.GetTimestamp() >= _deadlineOrRemainingStopwatchTicks)
160+
{
161+
MarkTimedOutAndThrow();
162+
}
163+
}
164+
165+
[DoesNotReturn]
166+
private void MarkTimedOutAndThrow()
167+
{
168+
_state = EvaluationBudgetState.TimedOut;
169+
ThrowTimedOut();
170+
}
171+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// <copyright file="EvaluationTimeBudgetExceededException.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
10+
namespace Datadog.Trace.Debugger.Expressions;
11+
12+
internal sealed class EvaluationTimeBudgetExceededException(Exception? innerException = null)
13+
: Exception(ErrorMessage, innerException)
14+
{
15+
internal const string ErrorMessage = "Expression evaluation timed out";
16+
}

0 commit comments

Comments
 (0)