Browse Source

Expression caching

git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4745 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61
shortcuts
David Srbecký 16 years ago
parent
commit
4162f7ab31
  1. 1
      src/AddIns/Misc/Debugger/Debugger.Core/Project/Debugger.Core.csproj
  2. 3
      src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Control/Process-StateControl.cs
  3. 9
      src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Control/Process.cs
  4. 248
      src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Expressions/ExpressionEvaluator.cs
  5. 8
      src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Internal/ManagedCallback.cs
  6. 46
      src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Util/CallbackOnDispose.cs

1
src/AddIns/Misc/Debugger/Debugger.Core/Project/Debugger.Core.csproj

@ -266,6 +266,7 @@
<Compile Include="Src\Mono.Cecil\Mono.Cecil.Signatures\Var.cs" /> <Compile Include="Src\Mono.Cecil\Mono.Cecil.Signatures\Var.cs" />
<Compile Include="Src\Mono.Cecil\Mono.Cecil\MethodCallingConvention.cs" /> <Compile Include="Src\Mono.Cecil\Mono.Cecil\MethodCallingConvention.cs" />
<Compile Include="Src\Mono.Cecil\Mono.Cecil\ReflectionException.cs" /> <Compile Include="Src\Mono.Cecil\Mono.Cecil\ReflectionException.cs" />
<Compile Include="Src\Util\CallbackOnDispose.cs" />
<Compile Include="Src\Util\HighPrecisionTimer.cs" /> <Compile Include="Src\Util\HighPrecisionTimer.cs" />
<Compile Include="Src\Values\ArrayDimension.cs" /> <Compile Include="Src\Values\ArrayDimension.cs" />
<Compile Include="Src\Values\ArrayDimensions.cs" /> <Compile Include="Src\Values\ArrayDimensions.cs" />

3
src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Control/Process-StateControl.cs

@ -5,6 +5,7 @@
// <version>$Revision$</version> // <version>$Revision$</version>
// </file> // </file>
using ICSharpCode.NRefactory.Ast;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -16,6 +17,7 @@ namespace Debugger
{ {
internal bool TerminateCommandIssued = false; internal bool TerminateCommandIssued = false;
internal Queue<Breakpoint> BreakpointHitEventQueue = new Queue<Breakpoint>(); internal Queue<Breakpoint> BreakpointHitEventQueue = new Queue<Breakpoint>();
internal Dictionary<INode, Value> CachedExpressions = new Dictionary<INode, Value>();
#region Events #region Events
@ -99,6 +101,7 @@ namespace Debugger
if (action == DebuggeeStateAction.Clear) { if (action == DebuggeeStateAction.Clear) {
if (debuggeeState == null) throw new DebuggerException("Debugee state already cleared"); if (debuggeeState == null) throw new DebuggerException("Debugee state already cleared");
debuggeeState = null; debuggeeState = null;
this.CachedExpressions.Clear();
} }
} }

9
src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Control/Process.cs

@ -185,13 +185,16 @@ namespace Debugger
} }
} }
public void TraceMessage(string message, params object[] args) public void TraceVerboseMessage(string message, params object[] args)
{ {
TraceMessage(string.Format(message, args)); if (this.Options.Verbose)
TraceMessage(message, args);
} }
public void TraceMessage(string message) public void TraceMessage(string message, params object[] args)
{ {
if (args.Length > 0)
message = string.Format(message, args);
System.Diagnostics.Debug.WriteLine("Debugger:" + message); System.Diagnostics.Debug.WriteLine("Debugger:" + message);
debugger.OnDebuggerTraceMessage(new MessageEventArgs(this, message)); debugger.OnDebuggerTraceMessage(new MessageEventArgs(this, message));
} }

248
src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Expressions/ExpressionEvaluator.cs

@ -8,6 +8,7 @@ using ICSharpCode.NRefactory.PrettyPrinter;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Text; using System.Text;
using Debugger.MetaData; using Debugger.MetaData;
using ICSharpCode.NRefactory; using ICSharpCode.NRefactory;
@ -18,7 +19,7 @@ namespace Debugger
{ {
public class ExpressionEvaluator: NotImplementedAstVisitor public class ExpressionEvaluator: NotImplementedAstVisitor
{ {
/// <summary> Evaluate given expression. If you expression tree already, use overloads of this method.</summary> /// <summary> Evaluate given expression. If you have expression tree already, use overloads of this method.</summary>
/// <returns> Returned value or null for statements </returns> /// <returns> Returned value or null for statements </returns>
public static Value Evaluate(string code, SupportedLanguage language, StackFrame context) public static Value Evaluate(string code, SupportedLanguage language, StackFrame context)
{ {
@ -45,29 +46,12 @@ namespace Debugger
} }
} }
static Dictionary<AppDomain, Dictionary<string, Value>> expressionCache = new Dictionary<AppDomain, Dictionary<string, Value>>();
public static Value Evaluate(INode code, StackFrame context) public static Value Evaluate(INode code, StackFrame context)
{ {
if (context == null) throw new ArgumentNullException("context"); if (context == null) throw new ArgumentNullException("context");
if (context.IsInvalid) throw new DebuggerException("The context is no longer valid"); if (context.IsInvalid) throw new DebuggerException("The context is no longer valid");
string codeAsText = code.PrettyPrint();
// Get value from cache if possible
if (expressionCache.ContainsKey(context.AppDomain) &&
expressionCache[context.AppDomain].ContainsKey(codeAsText)) {
Value cached = expressionCache[context.AppDomain][codeAsText];
if (!cached.IsInvalid) {
if (context.Process.Options.Verbose) {
context.Process.TraceMessage(string.Format("Cached: {0}", codeAsText));
}
return cached;
}
}
Value result; Value result;
DateTime start = Debugger.Util.HighPrecisionTimer.Now;
try { try {
result = (Value)code.AcceptVisitor(new ExpressionEvaluator(context), null); result = (Value)code.AcceptVisitor(new ExpressionEvaluator(context), null);
} catch (GetValueException) { } catch (GetValueException) {
@ -75,18 +59,6 @@ namespace Debugger
} catch (NotImplementedException e) { } catch (NotImplementedException e) {
throw new GetValueException(code, "Language feature not implemented: " + e.Message); throw new GetValueException(code, "Language feature not implemented: " + e.Message);
} }
DateTime end = Debugger.Util.HighPrecisionTimer.Now;
// Store value in cache
if (!expressionCache.ContainsKey(context.AppDomain)) {
expressionCache[context.AppDomain] = new Dictionary<string, Value>();
// TODO
}
// expressionCache[context.AppDomain][codeAsText] = result;
if (context.Process.Options.Verbose) {
context.Process.TraceMessage(string.Format("Evaluated: {0} ({1} ms)", code, (end - start).TotalMilliseconds));
}
return result; return result;
} }
@ -129,6 +101,34 @@ namespace Debugger
} }
} }
void AddToCache(INode expression, Value value)
{
// Expressions are cleared then the process is resumed
context.Process.CachedExpressions[expression] = value;
}
bool TryGetCached(INode expression, out Value cached)
{
Value val;
if (context.Process.CachedExpressions.TryGetValue(expression, out val)) {
if (val == null || !val.IsInvalid) {
// context.Process.TraceMessage("Is cached: {0}", expression.PrettyPrint());
cached = val;
return true;
}
}
cached = null;
return false;
}
Value EvalAndPermRef(INode expression)
{
Value val = (Value)expression.AcceptVisitor(this, null);
if (val != null)
val = val.GetPermanentReference();
AddToCache(expression, val);
return val;
}
StackFrame context; StackFrame context;
@ -141,8 +141,61 @@ namespace Debugger
this.context = context; this.context = context;
} }
IDisposable LogEval(INode expression)
{
Stopwatch watch = new Stopwatch();
watch.Start();
return new CallbackOnDispose(delegate {
if (!context.Process.CachedExpressions.ContainsKey(expression))
throw new DebuggerException("Result not added to cache");
watch.Stop();
context.Process.TraceMessage("Evaluated: {0} in {1} ms total", expression.PrettyPrint(), watch.ElapsedMilliseconds);
});
}
public override object VisitEmptyStatement(EmptyStatement emptyStatement, object data)
{
using(LogEval(emptyStatement)) {
return null;
}
}
public override object VisitExpressionStatement(ExpressionStatement expressionStatement, object data)
{
using(LogEval(expressionStatement)) {
EvalAndPermRef(expressionStatement.Expression);
return null;
}
}
public override object VisitParenthesizedExpression(ParenthesizedExpression parenthesizedExpression, object data)
{
Value cached;
if (TryGetCached(parenthesizedExpression, out cached)) return cached;
using(LogEval(parenthesizedExpression)) {
Value res = EvalAndPermRef(parenthesizedExpression.Expression);
AddToCache(parenthesizedExpression, res);
return res;
}
}
public override object VisitBlockStatement(BlockStatement blockStatement, object data)
{
using(LogEval(blockStatement)) {
foreach(INode statement in blockStatement.Children) {
EvalAndPermRef(statement);
}
return null;
}
}
/// <remarks> We have to put that in cache as well otherwise expaning (a = b).Prop will reevalute </remarks>
public override object VisitAssignmentExpression(AssignmentExpression assignmentExpression, object data) public override object VisitAssignmentExpression(AssignmentExpression assignmentExpression, object data)
{ {
Value cached;
if (TryGetCached(assignmentExpression, out cached)) return cached;
using(LogEval(assignmentExpression)) {
BinaryOperatorType op; BinaryOperatorType op;
switch (assignmentExpression.Op) { switch (assignmentExpression.Op) {
case AssignmentOperatorType.Assign: op = BinaryOperatorType.None; break; case AssignmentOperatorType.Assign: op = BinaryOperatorType.None; break;
@ -164,7 +217,7 @@ namespace Debugger
Value right; Value right;
if (op == BinaryOperatorType.None) { if (op == BinaryOperatorType.None) {
right = (Value)assignmentExpression.Right.AcceptVisitor(this, null); right = EvalAndPermRef(assignmentExpression.Right);
} else { } else {
BinaryOperatorExpression binOpExpr = new BinaryOperatorExpression(); BinaryOperatorExpression binOpExpr = new BinaryOperatorExpression();
binOpExpr.Left = assignmentExpression.Left; binOpExpr.Left = assignmentExpression.Left;
@ -174,74 +227,71 @@ namespace Debugger
} }
right = right.GetPermanentReference(); right = right.GetPermanentReference();
Value left = ((Value)assignmentExpression.Left.AcceptVisitor(this, null)); Value left = EvalAndPermRef(assignmentExpression.Left);
if (!left.IsReference && left.Type.FullName != right.Type.FullName) { if (!left.IsReference && left.Type.FullName != right.Type.FullName) {
throw new GetValueException(string.Format("Type {0} expected, {1} seen", left.Type.FullName, right.Type.FullName)); throw new GetValueException(string.Format("Type {0} expected, {1} seen", left.Type.FullName, right.Type.FullName));
} }
left.SetValue(right); left.SetValue(right);
return right;
}
public override object VisitBlockStatement(BlockStatement blockStatement, object data)
{
foreach(INode statement in blockStatement.Children) {
statement.AcceptVisitor(this, null);
}
return null;
}
public override object VisitEmptyStatement(EmptyStatement emptyStatement, object data) AddToCache(assignmentExpression, right);
{ return right;
return null;
} }
public override object VisitExpressionStatement(ExpressionStatement expressionStatement, object data)
{
expressionStatement.Expression.AcceptVisitor(this, null);
return null;
} }
public override object VisitIdentifierExpression(IdentifierExpression identifierExpression, object data) public override object VisitIdentifierExpression(IdentifierExpression identifierExpression, object data)
{ {
Value cached;
if (TryGetCached(identifierExpression, out cached)) return cached;
using(LogEval(identifierExpression)) {
string identifier = identifierExpression.Identifier; string identifier = identifierExpression.Identifier;
Value result = null;
if (identifier == "__exception") { if (identifier == "__exception") {
if (context.Thread.CurrentException != null) { if (context.Thread.CurrentException != null) {
return context.Thread.CurrentException.Value; result = context.Thread.CurrentException.Value;
} else { } else {
throw new GetValueException("No current exception"); throw new GetValueException("No current exception");
} }
} }
Value arg = context.GetArgumentValue(identifier); result = result ?? context.GetArgumentValue(identifier);
if (arg != null) return arg;
Value local = context.GetLocalVariableValue(identifier); result = result ?? context.GetLocalVariableValue(identifier);
if (local != null) return local;
if (result == null) {
if (!context.MethodInfo.IsStatic) { if (!context.MethodInfo.IsStatic) {
Value member = context.GetThisValue().GetMemberValue(identifier); // Can be null
if (member != null) return member; result = context.GetThisValue().GetMemberValue(identifier);
} else { } else {
MemberInfo memberInfo = context.MethodInfo.DeclaringType.GetMember(identifier); MemberInfo memberInfo = context.MethodInfo.DeclaringType.GetMember(identifier);
if (memberInfo != null && memberInfo.IsStatic) { if (memberInfo != null && memberInfo.IsStatic) {
return Value.GetMemberValue(null, memberInfo, null); result = Value.GetMemberValue(null, memberInfo, null);
}
} }
} }
if (result == null)
throw new GetValueException("Identifier \"" + identifier + "\" not found in this context"); throw new GetValueException("Identifier \"" + identifier + "\" not found in this context");
AddToCache(identifierExpression, result);
return result;
}
} }
public override object VisitIndexerExpression(IndexerExpression indexerExpression, object data) public override object VisitIndexerExpression(IndexerExpression indexerExpression, object data)
{ {
Value cached;
if (TryGetCached(indexerExpression, out cached)) return cached;
using(LogEval(indexerExpression)) {
List<Value> indexes = new List<Value>(); List<Value> indexes = new List<Value>();
foreach(Expression indexExpr in indexerExpression.Indexes) { foreach(Expression indexExpr in indexerExpression.Indexes) {
Value indexValue = ((Value)indexExpr.AcceptVisitor(this, null)).GetPermanentReference(); Value indexValue = EvalAndPermRef(indexExpr);
indexes.Add(indexValue); indexes.Add(indexValue);
} }
Value target = (Value)indexerExpression.TargetObject.AcceptVisitor(this, null); Value target = EvalAndPermRef(indexerExpression.TargetObject);
if (target.Type.IsArray) { if (target.Type.IsArray) {
List<int> intIndexes = new List<int>(); List<int> intIndexes = new List<int>();
@ -263,16 +313,23 @@ namespace Debugger
PropertyInfo pi = target.Type.GetProperty("Item"); PropertyInfo pi = target.Type.GetProperty("Item");
if (pi == null) throw new GetValueException("The object does not have an indexer property"); if (pi == null) throw new GetValueException("The object does not have an indexer property");
return target.GetPropertyValue(pi, indexes.ToArray()); Value result = target.GetPropertyValue(pi, indexes.ToArray());
AddToCache(indexerExpression, result);
return result;
}
} }
public override object VisitInvocationExpression(InvocationExpression invocationExpression, object data) public override object VisitInvocationExpression(InvocationExpression invocationExpression, object data)
{ {
Value cached;
if (TryGetCached(invocationExpression, out cached)) return cached;
using(LogEval(invocationExpression)) {
Value target; Value target;
string methodName; string methodName;
MemberReferenceExpression memberRef = invocationExpression.TargetObject as MemberReferenceExpression; MemberReferenceExpression memberRef = invocationExpression.TargetObject as MemberReferenceExpression;
if (memberRef != null) { if (memberRef != null) {
target = ((Value)memberRef.TargetObject.AcceptVisitor(this, null)).GetPermanentReference(); target = EvalAndPermRef(memberRef.TargetObject);
methodName = memberRef.MemberName; methodName = memberRef.MemberName;
} else { } else {
IdentifierExpression ident = invocationExpression.TargetObject as IdentifierExpression; IdentifierExpression ident = invocationExpression.TargetObject as IdentifierExpression;
@ -285,44 +342,64 @@ namespace Debugger
} }
List<Value> args = new List<Value>(); List<Value> args = new List<Value>();
foreach(Expression expr in invocationExpression.Arguments) { foreach(Expression expr in invocationExpression.Arguments) {
args.Add(((Value)expr.AcceptVisitor(this, null)).GetPermanentReference()); args.Add(EvalAndPermRef(expr));
} }
MethodInfo method = target.Type.GetMember(methodName, BindingFlags.Method | BindingFlags.IncludeSuperType) as MethodInfo; MethodInfo method = target.Type.GetMember(methodName, BindingFlags.Method | BindingFlags.IncludeSuperType) as MethodInfo;
if (method == null) { if (method == null) {
throw new GetValueException("Method " + methodName + " not found"); throw new GetValueException("Method " + methodName + " not found");
} }
return target.InvokeMethod(method, args.ToArray()); Value result = target.InvokeMethod(method, args.ToArray());
AddToCache(invocationExpression, result);
return result;
}
} }
public override object VisitMemberReferenceExpression(MemberReferenceExpression memberReferenceExpression, object data) public override object VisitMemberReferenceExpression(MemberReferenceExpression memberReferenceExpression, object data)
{ {
Value target = (Value)memberReferenceExpression.TargetObject.AcceptVisitor(this, null); Value cached;
if (TryGetCached(memberReferenceExpression, out cached)) return cached;
using(LogEval(memberReferenceExpression)) {
Value target = EvalAndPermRef(memberReferenceExpression.TargetObject);
Value member = target.GetMemberValue(memberReferenceExpression.MemberName); Value member = target.GetMemberValue(memberReferenceExpression.MemberName);
if (member != null) { if (member == null)
return member;
} else {
throw new GetValueException("Member \"" + memberReferenceExpression.MemberName + "\" not found"); throw new GetValueException("Member \"" + memberReferenceExpression.MemberName + "\" not found");
}
}
public override object VisitParenthesizedExpression(ParenthesizedExpression parenthesizedExpression, object data) AddToCache(memberReferenceExpression, member);
{ return member;
return parenthesizedExpression.Expression.AcceptVisitor(this, null); }
} }
public override object VisitPrimitiveExpression(PrimitiveExpression primitiveExpression, object data) public override object VisitPrimitiveExpression(PrimitiveExpression primitiveExpression, object data)
{ {
return Eval.CreateValue(context.AppDomain, primitiveExpression.Value); Value cached;
if (TryGetCached(primitiveExpression, out cached)) return cached;
using(LogEval(primitiveExpression)){
Value result = Eval.CreateValue(context.AppDomain, primitiveExpression.Value);
AddToCache(primitiveExpression, result);
return result;
}
} }
public override object VisitThisReferenceExpression(ThisReferenceExpression thisReferenceExpression, object data) public override object VisitThisReferenceExpression(ThisReferenceExpression thisReferenceExpression, object data)
{ {
return context.GetThisValue(); Value cached;
if (TryGetCached(thisReferenceExpression, out cached)) return cached;
using(LogEval(thisReferenceExpression)) {
Value result = context.GetThisValue();
AddToCache(thisReferenceExpression, result);
return result;
}
} }
public override object VisitUnaryOperatorExpression(UnaryOperatorExpression unaryOperatorExpression, object data) public override object VisitUnaryOperatorExpression(UnaryOperatorExpression unaryOperatorExpression, object data)
{ {
Value value = ((Value)unaryOperatorExpression.Expression.AcceptVisitor(this, null)); Value cached;
if (TryGetCached(unaryOperatorExpression, out cached)) return cached;
using(LogEval(unaryOperatorExpression)) {
Value value = EvalAndPermRef(unaryOperatorExpression.Expression);
UnaryOperatorType op = unaryOperatorExpression.Op; UnaryOperatorType op = unaryOperatorExpression.Op;
if (op == UnaryOperatorType.Dereference) { if (op == UnaryOperatorType.Dereference) {
@ -379,18 +456,29 @@ namespace Debugger
if (result == null) throw new GetValueException("Unsuppored unary expression " + op); if (result == null) throw new GetValueException("Unsuppored unary expression " + op);
return Eval.CreateValue(context.AppDomain, result); Value res = Eval.CreateValue(context.AppDomain, result);
AddToCache(unaryOperatorExpression, res);
return res;
}
} }
public override object VisitBinaryOperatorExpression(BinaryOperatorExpression binaryOperatorExpression, object data) public override object VisitBinaryOperatorExpression(BinaryOperatorExpression binaryOperatorExpression, object data)
{ {
Value left = ((Value)binaryOperatorExpression.Left.AcceptVisitor(this, null)).GetPermanentReference(); Value cached;
Value right = ((Value)binaryOperatorExpression.Right.AcceptVisitor(this, null)).GetPermanentReference(); if (TryGetCached(binaryOperatorExpression, out cached)) return cached;
using(LogEval(binaryOperatorExpression)) {
Value left = EvalAndPermRef(binaryOperatorExpression.Left);
Value right = EvalAndPermRef(binaryOperatorExpression.Right);
object result = VisitBinaryOperatorExpressionInternal(left, right, binaryOperatorExpression.Op); object result = VisitBinaryOperatorExpressionInternal(left, right, binaryOperatorExpression.Op);
// Conver long to int if possible // Conver long to int if possible
if (result is long && int.MinValue <= (long)result && (long)result <= int.MaxValue) result = (int)(long)result; if (result is long && int.MinValue <= (long)result && (long)result <= int.MaxValue) result = (int)(long)result;
return Eval.CreateValue(context.AppDomain, result); Value res = Eval.CreateValue(context.AppDomain, result);
AddToCache(binaryOperatorExpression, res);
return res;
}
} }
public object VisitBinaryOperatorExpressionInternal(Value leftValue, Value rightValue, BinaryOperatorType op) public object VisitBinaryOperatorExpressionInternal(Value leftValue, Value rightValue, BinaryOperatorType op)

8
src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Internal/ManagedCallback.cs

@ -240,14 +240,18 @@ namespace Debugger
public void EvalException(ICorDebugAppDomain pAppDomain, ICorDebugThread pThread, ICorDebugEval corEval) public void EvalException(ICorDebugAppDomain pAppDomain, ICorDebugThread pThread, ICorDebugEval corEval)
{ {
EnterCallback(PausedReason.EvalComplete, "EvalException", pThread); Eval eval = process.ActiveEvals[corEval];
EnterCallback(PausedReason.EvalComplete, "EvalException: " + eval.Description, pThread);
HandleEvalComplete(pAppDomain, pThread, corEval, true); HandleEvalComplete(pAppDomain, pThread, corEval, true);
} }
public void EvalComplete(ICorDebugAppDomain pAppDomain, ICorDebugThread pThread, ICorDebugEval corEval) public void EvalComplete(ICorDebugAppDomain pAppDomain, ICorDebugThread pThread, ICorDebugEval corEval)
{ {
EnterCallback(PausedReason.EvalComplete, "EvalComplete", pThread); Eval eval = process.ActiveEvals[corEval];
EnterCallback(PausedReason.EvalComplete, "EvalComplete: " + eval.Description, pThread);
HandleEvalComplete(pAppDomain, pThread, corEval, false); HandleEvalComplete(pAppDomain, pThread, corEval, false);
} }

46
src/AddIns/Misc/Debugger/Debugger.Core/Project/Src/Util/CallbackOnDispose.cs

@ -0,0 +1,46 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Diagnostics;
using System.Threading;
namespace Debugger
{
/// <summary>
/// Invokes a callback when this class is disposed.
/// </summary>
sealed class CallbackOnDispose : IDisposable
{
Action callback;
public CallbackOnDispose(Action callback)
{
if (callback == null)
throw new ArgumentNullException("callback");
this.callback = callback;
}
public void Dispose()
{
Action action = Interlocked.Exchange(ref callback, null);
if (action != null) {
action();
#if DEBUG
GC.SuppressFinalize(this);
#endif
}
}
#if DEBUG
~CallbackOnDispose()
{
Debug.Fail("CallbackOnDispose was finalized without being disposed.");
}
#endif
}
}
Loading…
Cancel
Save