diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/AllCalls.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/AllCalls.cs
new file mode 100644
index 0000000000..3437cf9eb1
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/AllCalls.cs
@@ -0,0 +1,60 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ ///
+ /// Query AST node representing the whole 'FunctionData' table.
+ /// This is the source of all queries.
+ /// Produces SELECT .. FROM .. in SQL.
+ ///
+ sealed class AllCalls : QueryNode
+ {
+ public static readonly AllCalls Instance = new AllCalls();
+
+ private AllCalls() : base(null)
+ {
+ }
+
+ protected override Expression VisitChildren(Func visitor)
+ {
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return "AllCalls";
+ }
+
+ public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
+ {
+ CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context);
+ context.SetCurrent(newNames, SqlTableType.Calls, true);
+
+ b.AppendLine("SELECT "
+ + SqlAs("nameid", newNames.NameID) + ", "
+ + SqlAs("timespent", newNames.CpuCyclesSpent) + ", " // TODO : change to CpuCyclesSpent
+ + SqlAs("callcount", newNames.CallCount) + ", "
+ + SqlAs("(id != endid)", newNames.HasChildren) + ", "
+ + SqlAs("((datasetid = " + context.StartDataSetID + ") AND isActiveAtStart)", newNames.ActiveCallCount) + ", "
+ + SqlAs("id", newNames.ID));
+ b.AppendLine("FROM FunctionData");
+ return SqlStatementKind.Select;
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/AllFunctions.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/AllFunctions.cs
new file mode 100644
index 0000000000..e86eca9dd6
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/AllFunctions.cs
@@ -0,0 +1,69 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Linq.Expressions;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ ///
+ /// Query AST node representing the whole 'FunctionData' table.
+ /// This is the source of all queries.
+ /// Produces SELECT .. FROM .. in SQL.
+ ///
+ sealed class AllFunctions : QueryNode
+ {
+ public readonly int StartDataSet, EndDataSet;
+
+ public AllFunctions() : base(null)
+ {
+ this.StartDataSet = -1;
+ this.EndDataSet = -1;
+ }
+
+ public AllFunctions(int startDataSet, int endDataSet) : base(null)
+ {
+ this.StartDataSet = startDataSet;
+ this.EndDataSet = endDataSet;
+ }
+
+ protected override Expression VisitChildren(Func visitor)
+ {
+ return this;
+ }
+
+ public override string ToString()
+ {
+ if (StartDataSet < 0)
+ return "AllFunctions";
+ else
+ return "AllFunctions(" + StartDataSet + ", " + EndDataSet + ")";
+ }
+
+ public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
+ {
+ if (context.RequireIDList)
+ throw new NotSupportedException();
+
+ CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context);
+ context.SetCurrent(newNames, SqlTableType.None, false);
+
+ b.AppendLine("SELECT "
+ + SqlAs("nameid", newNames.NameID) + ", "
+ + SqlAs("SUM(timespent)", newNames.CpuCyclesSpent) + ", "
+ + SqlAs("SUM(callcount)", newNames.CallCount) + ", "
+ + SqlAs("MAX(id != endid)", newNames.HasChildren) + ", "
+ + SqlAs("SUM((datasetid = " + context.StartDataSetID + ") AND isActiveAtStart)", newNames.ActiveCallCount));
+ b.AppendLine("FROM FunctionData");
+ if (StartDataSet >= 0)
+ b.AppendLine("WHERE datasetid BETWEEN " + StartDataSet + " AND " + EndDataSet);
+ b.AppendLine("GROUP BY nameid");
+ return SqlStatementKind.SelectGroupBy;
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/ExpressionSqlWriter.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/ExpressionSqlWriter.cs
index b70f6c3f0a..6d8c61cd1c 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/Linq/ExpressionSqlWriter.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/ExpressionSqlWriter.cs
@@ -16,16 +16,18 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{
readonly TextWriter w;
readonly ParameterExpression callTreeNodeParameter;
+ readonly SqlQueryContext context;
readonly CallTreeNodeSqlNameSet nameSet;
- public ExpressionSqlWriter(TextWriter w, CallTreeNodeSqlNameSet nameSet, ParameterExpression callTreeNodeParameter)
+ public ExpressionSqlWriter(TextWriter w, SqlQueryContext context, ParameterExpression callTreeNodeParameter)
{
if (w == null)
throw new ArgumentNullException("w");
- if (nameSet == null)
- throw new ArgumentNullException("nameSet");
+ if (context == null)
+ throw new ArgumentNullException("context");
this.w = w;
- this.nameSet = nameSet;
+ this.context = context;
+ this.nameSet = context.CurrentNameSet;
this.callTreeNodeParameter = callTreeNodeParameter;
}
@@ -107,7 +109,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
void WriteMemberAccess(MemberExpression me)
{
if (me.Expression == callTreeNodeParameter) {
- if (me.Member.DeclaringType == typeof(SingleCall) && !nameSet.IsCalls)
+ if (me.Member.DeclaringType == typeof(SingleCall) && context.CurrentTable != SqlTableType.Calls)
throw new InvalidOperationException("SingleCall references are invalid here");
if (me.Member == SingleCall.IDField) {
w.Write("id");
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/Filter.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/Filter.cs
new file mode 100644
index 0000000000..a1f7d96e9f
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/Filter.cs
@@ -0,0 +1,107 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ ///
+ /// Query node that filters the input using conditions. Produces WHERE or HAVING in SQL.
+ ///
+ sealed class Filter : QueryNode
+ {
+ ///
+ /// List of conditions. The operator between these is AND.
+ ///
+ public readonly ReadOnlyCollection Conditions;
+
+ public Filter(QueryNode target, params LambdaExpression[] conditions)
+ : base(target)
+ {
+ Debug.Assert(target != null);
+ foreach (LambdaExpression l in conditions) {
+ Debug.Assert(l.Parameters.Count == 1);
+ }
+ this.Conditions = Array.AsReadOnly(conditions);
+ }
+
+ protected override Expression VisitChildren(Func visitor)
+ {
+ QueryNode newTarget = (QueryNode)visitor(Target);
+ LambdaExpression[] newConditions = new LambdaExpression[Conditions.Count];
+ bool unchanged = (newTarget == Target);
+ for (int i = 0; i < newConditions.Length; i++) {
+ newConditions[i] = (LambdaExpression)visitor(Conditions[i]);
+ unchanged &= newConditions[i] == Conditions[i];
+ }
+ if (unchanged)
+ return this;
+ else
+ return new Filter(newTarget, newConditions);
+ }
+
+ public override string ToString()
+ {
+ StringBuilder b = new StringBuilder();
+ b.Append(Target.ToString());
+ b.Append(".Filter(");
+ for (int i = 0; i < Conditions.Count; i++) {
+ if (i > 0)
+ b.Append(" && ");
+ b.Append(Conditions[i].ToString());
+ }
+ b.Append(')');
+ return b.ToString();
+ }
+
+ public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
+ {
+ SqlStatementKind kind = Target.BuildSql(b, context);
+ SqlStatementKind resultKind;
+ switch (kind) {
+ case SqlStatementKind.Select:
+ b.Append(" WHERE ");
+ resultKind = SqlStatementKind.SelectWhere;
+ break;
+ case SqlStatementKind.SelectGroupBy:
+ b.Append(" HAVING ");
+ resultKind = SqlStatementKind.SelectGroupByHaving;
+ break;
+ default:
+ WrapSqlIntoNestedStatement(b, context);
+ b.Append(" WHERE ");
+ resultKind = SqlStatementKind.SelectWhere;
+ break;
+ }
+ for (int i = 0; i < Conditions.Count; i++) {
+ if (i > 0)
+ b.Append(" AND ");
+ BuildSqlForCondition(b, context, Conditions[i]);
+ }
+ b.AppendLine();
+ return resultKind;
+ }
+
+ static void BuildSqlForCondition(StringBuilder b, SqlQueryContext context, LambdaExpression condition)
+ {
+ Debug.Assert(condition.Parameters.Count == 1);
+ StringWriter w = new StringWriter(CultureInfo.InvariantCulture);
+ ExpressionSqlWriter writer = new ExpressionSqlWriter(w, context, condition.Parameters[0]);
+ writer.Write(condition.Body);
+ b.Append(w.ToString());
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/Limit.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/Limit.cs
new file mode 100644
index 0000000000..25bb3d9533
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/Limit.cs
@@ -0,0 +1,62 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ ///
+ /// Query node that limits the amount of data selected. Produces LIMIT in SQL.
+ ///
+ sealed class Limit : QueryNode
+ {
+ public int Start { get; private set; }
+ public int Length { get; private set; }
+
+ public Limit(QueryNode target, int start, int length)
+ : base(target)
+ {
+ this.Start = start;
+ this.Length = length;
+ }
+
+ protected override Expression VisitChildren(Func visitor)
+ {
+ QueryNode newTarget = (QueryNode)visitor(Target);
+ if (newTarget == Target)
+ return this;
+ else
+ return new Limit(newTarget, Start, Length);
+ }
+
+ public override string ToString()
+ {
+ return Target + ".Limit(" + Start + "," + Length + ")";
+ }
+
+ public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
+ {
+ SqlStatementKind kind = Target.BuildSql(b, context);
+ if (kind == SqlStatementKind.SelectLimit)
+ WrapSqlIntoNestedStatement(b, context);
+
+ b.Append(" LIMIT " + Length);
+ b.AppendLine(" OFFSET " + Start);
+
+ return SqlStatementKind.SelectLimit;
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/MergeByName.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/MergeByName.cs
new file mode 100644
index 0000000000..125672b15b
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/MergeByName.cs
@@ -0,0 +1,70 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ ///
+ /// Query node that represents the 'group by nameid and merge' operation. Produces SELECT ... GROUP BY .. in SQL.
+ ///
+ sealed class MergeByName : QueryNode
+ {
+ public MergeByName(QueryNode target)
+ : base(target)
+ {
+ Debug.Assert(target != null);
+ }
+
+ protected override Expression VisitChildren(Func visitor)
+ {
+ QueryNode newTarget = (QueryNode)visitor(Target);
+ if (newTarget == Target)
+ return this;
+ else
+ return new MergeByName(newTarget);
+ }
+
+ public override string ToString()
+ {
+ return Target + ".MergeByName()";
+ }
+
+ public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
+ {
+ Target.BuildSql(b, context);
+
+ CallTreeNodeSqlNameSet oldNames = context.CurrentNameSet;
+ CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context);
+
+ string query = "SELECT "
+ + SqlAs(oldNames.NameID, newNames.NameID) + ", "
+ + SqlAs("SUM(" + oldNames.CpuCyclesSpent + ")", newNames.CpuCyclesSpent) + ", "
+ + SqlAs("SUM(" + oldNames.CallCount + ")", newNames.CallCount) + ", "
+ + SqlAs("MAX(" + oldNames.HasChildren + ")", newNames.HasChildren) + ", "
+ + SqlAs("SUM(" + oldNames.ActiveCallCount + ")", newNames.ActiveCallCount);
+ if (context.HasIDList) {
+ query += ", " + SqlAs("GROUP_CONCAT(" + oldNames.ID + ")", newNames.ID);
+ }
+ query += Environment.NewLine + "FROM (" + Environment.NewLine;
+ b.Insert(0, query);
+ b.AppendLine(") GROUP BY " + oldNames.NameID);
+ context.SetCurrent(newNames, SqlTableType.None, context.HasIDList);
+
+ return SqlStatementKind.SelectGroupBy;
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/OptimizeQueryExpressionVisitor.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/OptimizeQueryExpressionVisitor.cs
index cd1cef7419..24443b98b9 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/Linq/OptimizeQueryExpressionVisitor.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/OptimizeQueryExpressionVisitor.cs
@@ -50,6 +50,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return ReorderFilter(filter);
}
+ static readonly MemberInfo[] SafeMembersForMoveIntoMergeByName = { KnownMembers.CallTreeNode_NameMapping };
+
///
/// Tries to combine nested filters;
/// move 'MergeByName' nodes out of filter, if possible
@@ -63,7 +65,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
} else if (filter.Target is MergeByName) {
// x.MergeByName().Filter() -> x.Filter(x, ).MergeByName() for some safe criterias
QueryNode innerTarget = filter.Target.Target;
- var conditionsToMoveIntoFilter = filter.Conditions.Where(c => IsConditionSafeForMoveIntoMergeByName.Test(c)).ToArray();
+ var conditionsToMoveIntoFilter = filter.Conditions.Where(c => IsConditionSafeVisitor.Test(c, SafeMembersForMoveIntoMergeByName)).ToArray();
if (conditionsToMoveIntoFilter.Length != 0) {
MergeByName newTarget = new MergeByName(ReorderFilter(new Filter(innerTarget, conditionsToMoveIntoFilter)));
var conditionsKeptOutsideFilter = filter.Conditions.Except(conditionsToMoveIntoFilter).ToArray();
@@ -79,38 +81,6 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
}
}
- sealed class IsConditionSafeForMoveIntoMergeByName : System.Linq.Expressions.ExpressionVisitor
- {
- public static bool Test(Expression ex)
- {
- var visitor = new IsConditionSafeForMoveIntoMergeByName();
- visitor.Visit(ex);
- return visitor.IsSafe;
- }
-
- static readonly MemberInfo[] SafeMembers = {
- KnownMembers.CallTreeNode_NameMapping
- };
-
- bool IsSafe = true;
-
- protected override Expression VisitMember(MemberExpression node)
- {
- if (node.Expression.NodeType == ExpressionType.Parameter && !SafeMembers.Contains(node.Member))
- IsSafe = false;
- return base.VisitMember(node);
- }
-
- protected override Expression VisitMethodCall(MethodCallExpression node)
- {
- if (node.Object != null) {
- if (node.Object.NodeType == ExpressionType.Parameter && !SafeMembers.Contains(node.Method))
- IsSafe = false;
- }
- return base.VisitMethodCall(node);
- }
- }
-
///
/// Optimizes the filter; but does not try to combine nested filter (etc.)
///
@@ -145,8 +115,59 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
// x.MergeByName().MergeByName() -> x.MergeByName()
return target;
}
+ if (target == AllCalls.Instance) {
+ // AllCalls.MergeByName() -> AllFunctions
+ return new AllFunctions();
+ }
+ if (target is Filter && target.Target == AllCalls.Instance) {
+ // AllCalls.Filter(criteria).MergeByName() -> AllFunctions.Filter(criteria)
+ // If criteria accesses no CallTreeNode properties except for NameMapping.
+ // Criteria of the form 'start <= c.DataSetID && c.DataSetID <= end' will be converted into AllFunctions(start,end)
+ List newConditions = new List();
+ bool allIsSafe = true;
+ int startDataSetID = -1;
+ int endDataSetID = -1;
+ foreach (LambdaExpression condition in ((Filter)target).Conditions) {
+ if (IsConditionSafeVisitor.Test(condition, SafeMembersForMoveIntoMergeByName)) {
+ newConditions.Add(condition);
+ } else if (condition.Body.NodeType == ExpressionType.AndAlso && startDataSetID < 0) {
+ // try match 'constant <= c.DataSetID && c.DataSetID <= constant', but only if we
+ // haven't found it already (startDataSetID is still -1)
+ BinaryExpression bin = (BinaryExpression)condition.Body;
+ if (bin.Left.NodeType == ExpressionType.LessThanOrEqual && bin.Right.NodeType == ExpressionType.LessThanOrEqual) {
+ BinaryExpression left = (BinaryExpression)bin.Left;
+ BinaryExpression right = (BinaryExpression)bin.Right;
+ if (left.Left.NodeType == ExpressionType.Constant && left.Right.NodeType == ExpressionType.MemberAccess
+ && right.Left.NodeType == ExpressionType.MemberAccess && right.Right.NodeType == ExpressionType.Constant
+ && ((MemberExpression)left.Right).Member == SingleCall.DataSetIdField
+ && ((MemberExpression)right.Left).Member == SingleCall.DataSetIdField)
+ {
+ startDataSetID = (int)GetConstantValue(left.Left);
+ endDataSetID = (int)GetConstantValue(right.Right);
+ } else {
+ allIsSafe = false;
+ }
+ } else {
+ allIsSafe = false;
+ }
+ } else {
+ allIsSafe = false;
+ }
+ }
+ if (allIsSafe) {
+ if (newConditions.Count > 0)
+ return new Filter(new AllFunctions(startDataSetID, endDataSetID), newConditions.ToArray());
+ else
+ return new AllFunctions(startDataSetID, endDataSetID);
+ }
+ }
return new MergeByName(target);
}
+
+ static object GetConstantValue(Expression expr)
+ {
+ return ((ConstantExpression)expr).Value;
+ }
#endregion
protected override Expression VisitMethodCall(MethodCallExpression node)
@@ -162,4 +183,36 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return base.VisitMethodCall(node);
}
}
+
+
+ sealed class IsConditionSafeVisitor : System.Linq.Expressions.ExpressionVisitor
+ {
+ public static bool Test(Expression ex, params MemberInfo[] safeMembers)
+ {
+ var visitor = new IsConditionSafeVisitor();
+ visitor.SafeMembers = safeMembers;
+ visitor.Visit(ex);
+ return visitor.IsSafe;
+ }
+
+ MemberInfo[] SafeMembers;
+
+ bool IsSafe = true;
+
+ protected override Expression VisitMember(MemberExpression node)
+ {
+ if (node.Expression.NodeType == ExpressionType.Parameter && !SafeMembers.Contains(node.Member))
+ IsSafe = false;
+ return base.VisitMember(node);
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression node)
+ {
+ if (node.Object != null) {
+ if (node.Object.NodeType == ExpressionType.Parameter && !SafeMembers.Contains(node.Method))
+ IsSafe = false;
+ }
+ return base.VisitMethodCall(node);
+ }
+ }
}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs
deleted file mode 100644
index 9625fc21b6..0000000000
--- a/src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs
+++ /dev/null
@@ -1,422 +0,0 @@
-//
-//
-//
-//
-// $Revision$
-//
-
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Text;
-
-namespace ICSharpCode.Profiler.Controller.Data.Linq
-{
- ///
- /// The SingleCall class is never instanciated; it only used to represent database rows
- /// inside expression trees.
- ///
- abstract class SingleCall
- {
- // Warning CS0649: Field is never assigned to, and will always have its default value 0
- #pragma warning disable 649
- public int ID;
- public int ParentID;
- public int DataSetID;
- #pragma warning restore 649
-
- public static readonly FieldInfo IDField = typeof(SingleCall).GetField("ID");
- public static readonly FieldInfo ParentIDField = typeof(SingleCall).GetField("ParentID");
- public static readonly FieldInfo DataSetIdField = typeof(SingleCall).GetField("DataSetID");
- }
-
- ///
- /// Base class for nodes in the Query AST.
- ///
- abstract class QueryNode : Expression
- {
- public enum SqlStatementKind
- {
- Select,
- SelectWhere,
- SelectGroupBy,
- SelectGroupByHaving,
- SelectOrderBy,
- SelectLimit
- }
-
- public readonly QueryNode Target;
-
- protected QueryNode(QueryNode target)
- {
- this.Target = target;
- }
-
- protected override ExpressionType NodeTypeImpl()
- {
- return ExpressionType.Extension;
- }
-
- protected override Type TypeImpl()
- {
- return typeof(IQueryable);
- }
-
- protected abstract override Expression VisitChildren(Func visitor);
-
- ///
- /// SQL construction documentation see SQLiteQueryProvider documentation.
- ///
- public abstract SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context);
-
- ///
- /// Wraps the current SQL statement into an inner select, allowing to continue with "WHERE" queries
- /// even after ORDER BY or LIMIT.
- ///
- protected static void WrapSqlIntoNestedStatement(StringBuilder b, SqlQueryContext context)
- {
- CallTreeNodeSqlNameSet oldNames = context.CurrentNameSet;
- CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context, false);
- b.Insert(0, "SELECT " + SqlAs(oldNames.ID, newNames.ID) + ", "
- + SqlAs(oldNames.NameID, newNames.NameID) + ", "
- + SqlAs(oldNames.CpuCyclesSpent, newNames.CpuCyclesSpent) + ", "
- + SqlAs(oldNames.CallCount, newNames.CallCount) + ", "
- + SqlAs(oldNames.HasChildren, newNames.HasChildren) + ", "
- + SqlAs(oldNames.ActiveCallCount, newNames.ActiveCallCount)
- + Environment.NewLine + "FROM (" + Environment.NewLine);
- b.AppendLine(")");
- context.CurrentNameSet = newNames;
- }
-
- ///
- /// Helper function that builds the text 'expression AS newName'
- ///
- protected static string SqlAs(string expression, string newName)
- {
- if (expression == newName)
- return newName;
- else
- return expression + " AS " + newName;
- }
-
- public IQueryable Execute(SQLiteQueryProvider provider, QueryExecutionOptions options)
- {
- StringBuilder b = new StringBuilder();
- BuildSql(b, new SqlQueryContext(provider));
- if (options.HasLoggers)
- options.WriteLogLine(b.ToString());
- Stopwatch w = Stopwatch.StartNew();
- IList result = provider.RunSQLNodeList(b.ToString());
- w.Stop();
- if (options.HasLoggers) {
- options.WriteLogLine("Query returned " + result.Count + " rows in " + w.Elapsed);
- }
- return result.AsQueryable();
- }
- }
-
- ///
- /// Query AST node representing the whole 'FunctionData' table.
- /// This is the source of all queries.
- /// Produces SELECT .. FROM .. in SQL.
- ///
- sealed class AllCalls : QueryNode
- {
- public static readonly AllCalls Instance = new AllCalls();
-
- private AllCalls() : base(null)
- {
- }
-
- protected override Expression VisitChildren(Func visitor)
- {
- return this;
- }
-
- public override string ToString()
- {
- return "AllCalls";
- }
-
- public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
- {
- CallTreeNodeSqlNameSet newNames = context.CurrentNameSet = new CallTreeNodeSqlNameSet(context, true);
-
- b.AppendLine("SELECT "
- + SqlAs("id", newNames.ID) + ", "
- + SqlAs("nameid", newNames.NameID) + ", "
- + SqlAs("timespent", newNames.CpuCyclesSpent) + ", " // TODO : change to CpuCyclesSpent
- + SqlAs("callcount", newNames.CallCount) + ", "
- + SqlAs("(id != endid)", newNames.HasChildren) + ", "
- + SqlAs("((datasetid = " + context.StartDataSetID + ") AND isActiveAtStart)", newNames.ActiveCallCount));
- b.AppendLine("FROM FunctionData");
- return SqlStatementKind.Select;
- }
- }
-
- ///
- /// Query node that represents the 'group by nameid and merge' operation. Produces SELECT ... GROUP BY .. in SQL.
- ///
- sealed class MergeByName : QueryNode
- {
- public MergeByName(QueryNode target)
- : base(target)
- {
- Debug.Assert(target != null);
- }
-
- protected override Expression VisitChildren(Func visitor)
- {
- QueryNode newTarget = (QueryNode)visitor(Target);
- if (newTarget == Target)
- return this;
- else
- return new MergeByName(newTarget);
- }
-
- public override string ToString()
- {
- return Target + ".MergeByName()";
- }
-
- public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
- {
- Target.BuildSql(b, context);
-
- CallTreeNodeSqlNameSet oldNames = context.CurrentNameSet;
- CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context, false);
- b.Insert(0, "SELECT "
- + SqlAs("GROUP_CONCAT(" + oldNames.ID + ")", newNames.ID) + ", "
- + SqlAs(oldNames.NameID, newNames.NameID) + ", "
- + SqlAs("SUM(" + oldNames.CpuCyclesSpent + ")", newNames.CpuCyclesSpent) + ", "
- + SqlAs("SUM(" + oldNames.CallCount + ")", newNames.CallCount) + ", "
- + SqlAs("MAX(" + oldNames.HasChildren + ")", newNames.HasChildren) + ", "
- + SqlAs("SUM(" + oldNames.ActiveCallCount + ")", newNames.ActiveCallCount)
- + Environment.NewLine + "FROM (" + Environment.NewLine);
- b.AppendLine(") GROUP BY " + oldNames.NameID);
- context.CurrentNameSet = newNames;
-
- return SqlStatementKind.SelectGroupBy;
- }
- }
-
- ///
- /// Query node that filters the input using conditions. Produces WHERE or HAVING in SQL.
- ///
- sealed class Filter : QueryNode
- {
- ///
- /// List of conditions. The operator between these is AND.
- ///
- public readonly ReadOnlyCollection Conditions;
-
- public Filter(QueryNode target, params LambdaExpression[] conditions)
- : base(target)
- {
- Debug.Assert(target != null);
- foreach (LambdaExpression l in conditions) {
- Debug.Assert(l.Parameters.Count == 1);
- }
- this.Conditions = Array.AsReadOnly(conditions);
- }
-
- protected override Expression VisitChildren(Func visitor)
- {
- QueryNode newTarget = (QueryNode)visitor(Target);
- LambdaExpression[] newConditions = new LambdaExpression[Conditions.Count];
- bool unchanged = (newTarget == Target);
- for (int i = 0; i < newConditions.Length; i++) {
- newConditions[i] = (LambdaExpression)visitor(Conditions[i]);
- unchanged &= newConditions[i] == Conditions[i];
- }
- if (unchanged)
- return this;
- else
- return new Filter(newTarget, newConditions);
- }
-
- public override string ToString()
- {
- StringBuilder b = new StringBuilder();
- b.Append(Target.ToString());
- b.Append(".Filter(");
- for (int i = 0; i < Conditions.Count; i++) {
- if (i > 0)
- b.Append(" && ");
- b.Append(Conditions[i].ToString());
- }
- b.Append(')');
- return b.ToString();
- }
-
- public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
- {
- SqlStatementKind kind = Target.BuildSql(b, context);
- SqlStatementKind resultKind;
- switch (kind) {
- case SqlStatementKind.Select:
- b.Append(" WHERE ");
- resultKind = SqlStatementKind.SelectWhere;
- break;
- case SqlStatementKind.SelectGroupBy:
- b.Append(" HAVING ");
- resultKind = SqlStatementKind.SelectGroupByHaving;
- break;
- default:
- WrapSqlIntoNestedStatement(b, context);
- b.Append(" WHERE ");
- resultKind = SqlStatementKind.SelectWhere;
- break;
- }
- for (int i = 0; i < Conditions.Count; i++) {
- if (i > 0)
- b.Append(" AND ");
- BuildSqlForCondition(b, context, Conditions[i]);
- }
- b.AppendLine();
- return resultKind;
- }
-
- static void BuildSqlForCondition(StringBuilder b, SqlQueryContext context, LambdaExpression condition)
- {
- Debug.Assert(condition.Parameters.Count == 1);
- StringWriter w = new StringWriter(CultureInfo.InvariantCulture);
- ExpressionSqlWriter writer = new ExpressionSqlWriter(w, context.CurrentNameSet, condition.Parameters[0]);
- writer.Write(condition.Body);
- b.Append(w.ToString());
- }
- }
-
- ///
- /// Query node that limits the amount of data selected. Produces LIMIT in SQL.
- ///
- sealed class Limit : QueryNode
- {
- public int Start { get; private set; }
- public int Length { get; private set; }
-
- public Limit(QueryNode target, int start, int length)
- : base(target)
- {
- this.Start = start;
- this.Length = length;
- }
-
- protected override Expression VisitChildren(Func visitor)
- {
- QueryNode newTarget = (QueryNode)visitor(Target);
- if (newTarget == Target)
- return this;
- else
- return new Limit(newTarget, Start, Length);
- }
-
- public override string ToString()
- {
- return Target + ".Limit(" + Start + "," + Length + ")";
- }
-
- public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
- {
- SqlStatementKind kind = Target.BuildSql(b, context);
- if (kind == SqlStatementKind.SelectLimit)
- WrapSqlIntoNestedStatement(b, context);
-
- b.Append(" LIMIT " + Length);
- b.AppendLine(" OFFSET " + Start);
-
- return SqlStatementKind.SelectLimit;
- }
- }
-
- class SortArgument
- {
- readonly LambdaExpression arg;
- readonly bool desc;
-
- public SortArgument(LambdaExpression arg, bool desc)
- {
- this.arg = arg;
- this.desc = desc;
- }
-
- public bool Descending {
- get { return desc; }
- }
- public LambdaExpression Argument {
- get { return arg; }
- }
-
- public override string ToString()
- {
- if (Descending)
- return Argument.ToString() + " DESC";
- else
- return Argument.ToString();
- }
- }
-
- sealed class Sort : QueryNode
- {
- ReadOnlyCollection arguments;
-
- public Sort(QueryNode target, IList args)
- : base(target)
- {
- this.arguments = new ReadOnlyCollection(args);
- }
-
- public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
- {
- SqlStatementKind kind = Target.BuildSql(b, context);
- if (kind == SqlStatementKind.SelectOrderBy)
- WrapSqlIntoNestedStatement(b, context);
-
- b.Append(" ORDER BY ");
-
- for (int i = 0; i < arguments.Count; i++) {
- StringWriter w = new StringWriter();
- ExpressionSqlWriter writer = new ExpressionSqlWriter(w, context.CurrentNameSet, arguments[i].Argument.Parameters[0]);
- writer.Write(arguments[i].Argument.Body);
-
- if (i == 0)
- b.Append(w.ToString());
- else
- b.Append(", " + w.ToString());
-
- if (arguments[i].Descending)
- b.Append(" DESC");
- else
- b.Append(" ASC");
- }
-
- return SqlStatementKind.SelectOrderBy;
- }
-
- protected override Expression VisitChildren(Func visitor)
- {
- QueryNode newTarget = (QueryNode)visitor(Target);
- if (newTarget == Target)
- return this;
- else
- return new Sort(newTarget, arguments);
- }
-
- public override string ToString()
- {
- StringBuilder builder = new StringBuilder();
- for (int i = 0; i < arguments.Count; i++) {
- if (i > 0)
- builder.Append(", ");
- builder.Append(arguments[i].ToString());
- }
- return Target + ".Sort(" + builder.ToString() + ")";
- }
- }
-}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryNode.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryNode.cs
new file mode 100644
index 0000000000..13503f544a
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryNode.cs
@@ -0,0 +1,129 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ ///
+ /// The SingleCall class is never instanciated; it only used to represent database rows
+ /// inside expression trees.
+ ///
+ abstract class SingleCall
+ {
+ // Warning CS0649: Field is never assigned to, and will always have its default value 0
+ #pragma warning disable 649
+ public int ID;
+ public int ParentID;
+ public int DataSetID;
+ #pragma warning restore 649
+
+ public static readonly FieldInfo IDField = typeof(SingleCall).GetField("ID");
+ public static readonly FieldInfo ParentIDField = typeof(SingleCall).GetField("ParentID");
+ public static readonly FieldInfo DataSetIdField = typeof(SingleCall).GetField("DataSetID");
+ }
+
+ ///
+ /// Base class for nodes in the Query AST.
+ ///
+ abstract class QueryNode : Expression
+ {
+ public enum SqlStatementKind
+ {
+ Select,
+ SelectWhere,
+ SelectGroupBy,
+ SelectGroupByHaving,
+ SelectOrderBy,
+ SelectLimit
+ }
+
+ public readonly QueryNode Target;
+
+ protected QueryNode(QueryNode target)
+ {
+ this.Target = target;
+ }
+
+ protected override ExpressionType NodeTypeImpl()
+ {
+ return ExpressionType.Extension;
+ }
+
+ protected override Type TypeImpl()
+ {
+ return typeof(IQueryable);
+ }
+
+ protected abstract override Expression VisitChildren(Func visitor);
+
+ ///
+ /// SQL construction documentation see SQLiteQueryProvider documentation.
+ ///
+ public abstract SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context);
+
+ ///
+ /// Wraps the current SQL statement into an inner select, allowing to continue with "WHERE" queries
+ /// even after ORDER BY or LIMIT.
+ ///
+ protected static void WrapSqlIntoNestedStatement(StringBuilder b, SqlQueryContext context)
+ {
+ CallTreeNodeSqlNameSet oldNames = context.CurrentNameSet;
+ CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context);
+
+ string query = "SELECT "
+ + SqlAs(oldNames.NameID, newNames.NameID) + ", "
+ + SqlAs(oldNames.CpuCyclesSpent, newNames.CpuCyclesSpent) + ", "
+ + SqlAs(oldNames.CallCount, newNames.CallCount) + ", "
+ + SqlAs(oldNames.HasChildren, newNames.HasChildren) + ", "
+ + SqlAs(oldNames.ActiveCallCount, newNames.ActiveCallCount);
+ if (context.HasIDList) {
+ query += ", " + SqlAs(oldNames.ID, newNames.ID);
+ }
+ query += Environment.NewLine + "FROM (" + Environment.NewLine;
+ b.Insert(0, query);
+ b.AppendLine(")");
+ context.SetCurrent(newNames, SqlTableType.None, context.HasIDList);
+ }
+
+ ///
+ /// Helper function that builds the text 'expression AS newName'
+ ///
+ protected static string SqlAs(string expression, string newName)
+ {
+ if (expression == newName)
+ return newName;
+ else
+ return expression + " AS " + newName;
+ }
+
+ public IQueryable Execute(SQLiteQueryProvider provider, QueryExecutionOptions options)
+ {
+ StringBuilder b = new StringBuilder();
+ SqlQueryContext context = new SqlQueryContext(provider);
+ BuildSql(b, context);
+ if (options.HasLoggers)
+ options.WriteLogLine(b.ToString());
+ Stopwatch w = Stopwatch.StartNew();
+ IList result = provider.RunSQLNodeList(b.ToString(), context.HasIDList);
+ w.Stop();
+ if (options.HasLoggers) {
+ options.WriteLogLine("Query returned " + result.Count + " rows in " + w.Elapsed);
+ }
+ return result.AsQueryable();
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs
index 6c6f677581..3751acd2a4 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
@@ -90,9 +91,10 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
Note: Skip/Take combinations are not combined (Skip is not supported).
input.OrderBy(x => x.SomeField) -> input.Sort({ [x.SomeField, ascending] })
- Note: ThenBy is not supported.
-
input.OrderByDescending(x => x.SomeField) -> input.Sort({ [x.SomeField, descending] })
+ Note: OrderBy is not converted into Sort if followed by a ThenBy.
+
+ input.MergeByName() -> new MergeByName(input)
Translation rules for expression importer:
Any valid expressions (as defined in 'valid expressions in QueryAst nodes') are copied over directly.
@@ -101,6 +103,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
c.IsThread -> Glob(c.NameMapping.Name, "Thread#*")
s.StartsWith(constantString, StringComparison.Ordinal) -> Glob(s, constantString + "*");
s.StartsWith(constantString, StringComparison.OrdinalIgnoreCase) -> Like(s, constantString + "%");
+ s.EndsWith(constantString, StringComparison.Ordinal) -> Glob(s, "*" + constantString);
+ s.EndsWith(constantString, StringComparison.OrdinalIgnoreCase) -> Like(s, "%" + constantString);
Optimization of QueryAst:
The OptimizeQueryExpressionVisitor is performing these optimizations:
@@ -108,6 +112,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
x.MergeByName().Filter(criteria) -> x.Filter(criteria).MergeByName() for some safe criterias
Criterias are safe if they access no CallTreeNode properties except for NameMapping
x.MergeByName().MergeByName() -> x.MergeByName()
+ AllCalls.Filter(criteria).MergeByName() -> AllFunctions.Filter(criteria)
+ If criteria accesses no CallTreeNode properties except for NameMapping.
+ Criteria of the form 'start <= c.DataSetID && c.DataSetID <= end' will be converted into AllFunctions(start,end)
SQL string building and execution:
It must be possible to create SQL for every combination of QueryNodes, even if they do strange things like merging multiple times.
@@ -130,14 +137,15 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
*/
readonly ProfilingDataSQLiteProvider sqliteProvider;
- internal readonly int startDataSetID;
+ internal readonly int startDataSetID, endDataSetID;
- public SQLiteQueryProvider(ProfilingDataSQLiteProvider sqliteProvider, int startDataSetID)
+ public SQLiteQueryProvider(ProfilingDataSQLiteProvider sqliteProvider, int startDataSetID, int endDataSetID)
{
if (sqliteProvider == null)
throw new ArgumentNullException("sqliteProvider");
this.sqliteProvider = sqliteProvider;
this.startDataSetID = startDataSetID;
+ this.endDataSetID = endDataSetID;
}
// Implement GetMapping and ProcessorFrequency so that SQLiteQueryProvider can be used in place of
@@ -151,9 +159,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
get { return sqliteProvider.ProcessorFrequency; }
}
- public IList RunSQLNodeList(string command)
+ public IList RunSQLNodeList(string command, bool hasIdList)
{
- return sqliteProvider.RunSQLNodeList(this, command);
+ return sqliteProvider.RunSQLNodeList(this, command, hasIdList);
}
///
@@ -164,6 +172,15 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return sqliteProvider.RunSQLIDList(command);
}
+ public int[] LoadIDListForFunction(int nameid)
+ {
+ string command = string.Format(
+ CultureInfo.InvariantCulture,
+ "SELECT id FROM FunctionData WHERE (nameid = {0}) AND (datasetid BETWEEN {1} AND {2});",
+ nameid, startDataSetID, endDataSetID);
+ return sqliteProvider.RunSQLIDList(command).ToArray();
+ }
+
public IQueryable CreateQuery(QueryNode query)
{
return new Query(this, query);
@@ -288,6 +305,10 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
} else if (node.Method == KnownMembers.Queryable_WithQueryLog && node.Arguments[1].NodeType == ExpressionType.Constant) {
options.AddLogger((TextWriter)(((ConstantExpression)node.Arguments[1]).Value));
return Visit(node.Arguments[0]);
+ } else if (node.Method == KnownMembers.Queryable_MergeByName) {
+ QueryNode target = Visit(node.Arguments[0]) as QueryNode;
+ if (target != null)
+ return new MergeByName(target);
} else if (node.Method == KnownMembers.QueryableOfCallTreeNode_Take && node.Arguments[1].NodeType == ExpressionType.Constant) {
ConstantExpression ce = (ConstantExpression)node.Arguments[1];
if (ce.Type == typeof(int)) {
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/Sort.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/Sort.cs
new file mode 100644
index 0000000000..25b7e097bc
--- /dev/null
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/Sort.cs
@@ -0,0 +1,105 @@
+//
+//
+//
+//
+// $Revision$
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+
+namespace ICSharpCode.Profiler.Controller.Data.Linq
+{
+ class SortArgument
+ {
+ readonly LambdaExpression arg;
+ readonly bool desc;
+
+ public SortArgument(LambdaExpression arg, bool desc)
+ {
+ this.arg = arg;
+ this.desc = desc;
+ }
+
+ public bool Descending {
+ get { return desc; }
+ }
+ public LambdaExpression Argument {
+ get { return arg; }
+ }
+
+ public override string ToString()
+ {
+ if (Descending)
+ return Argument.ToString() + " DESC";
+ else
+ return Argument.ToString();
+ }
+ }
+
+ sealed class Sort : QueryNode
+ {
+ ReadOnlyCollection arguments;
+
+ public Sort(QueryNode target, IList args)
+ : base(target)
+ {
+ this.arguments = new ReadOnlyCollection(args);
+ }
+
+ public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context)
+ {
+ SqlStatementKind kind = Target.BuildSql(b, context);
+ if (kind == SqlStatementKind.SelectOrderBy)
+ WrapSqlIntoNestedStatement(b, context);
+
+ b.Append(" ORDER BY ");
+
+ for (int i = 0; i < arguments.Count; i++) {
+ StringWriter w = new StringWriter();
+ ExpressionSqlWriter writer = new ExpressionSqlWriter(w, context, arguments[i].Argument.Parameters[0]);
+ writer.Write(arguments[i].Argument.Body);
+
+ if (i == 0)
+ b.Append(w.ToString());
+ else
+ b.Append(", " + w.ToString());
+
+ if (arguments[i].Descending)
+ b.Append(" DESC");
+ else
+ b.Append(" ASC");
+ }
+
+ return SqlStatementKind.SelectOrderBy;
+ }
+
+ protected override Expression VisitChildren(Func visitor)
+ {
+ QueryNode newTarget = (QueryNode)visitor(Target);
+ if (newTarget == Target)
+ return this;
+ else
+ return new Sort(newTarget, arguments);
+ }
+
+ public override string ToString()
+ {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < arguments.Count; i++) {
+ if (i > 0)
+ builder.Append(", ");
+ builder.Append(arguments[i].ToString());
+ }
+ return Target + ".Sort(" + builder.ToString() + ")";
+ }
+ }
+}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs b/src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs
index 92f5abb070..cbc78fed63 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs
@@ -14,6 +14,31 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{
public readonly int StartDataSetID;
+ public CallTreeNodeSqlNameSet CurrentNameSet { get; private set; }
+
+ ///
+ /// The type of the table currently being accessed (the current FROM clause).
+ /// Is 'None' when reading from an inner query.
+ ///
+ public SqlTableType CurrentTable { get; private set; }
+
+ ///
+ /// Passed down the query tree to signalize that the ID list is required.
+ ///
+ public bool RequireIDList;
+
+ ///
+ /// Passed up the query tree to signalize whether an ID list is present.
+ ///
+ public bool HasIDList { get; private set; }
+
+ public void SetCurrent(CallTreeNodeSqlNameSet nameSet, SqlTableType table, bool hasIDList)
+ {
+ this.CurrentNameSet = nameSet;
+ this.CurrentTable = table;
+ this.HasIDList = hasIDList;
+ }
+
public SqlQueryContext(SQLiteQueryProvider provider)
{
this.StartDataSetID = provider.startDataSetID;
@@ -25,8 +50,18 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{
return "v" + (++uniqueVariableIndex).ToString(CultureInfo.InvariantCulture);
}
-
- public CallTreeNodeSqlNameSet CurrentNameSet;
+ }
+
+ enum SqlTableType
+ {
+ ///
+ /// No direct table
+ ///
+ None,
+ ///
+ /// The FunctionData table
+ ///
+ Calls
}
sealed class CallTreeNodeSqlNameSet
@@ -38,15 +73,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
public readonly string HasChildren;
public readonly string ActiveCallCount;
- ///
- /// Gets whether this nameset represents non-merged calls.
- ///
- public readonly bool IsCalls;
-
- public CallTreeNodeSqlNameSet(SqlQueryContext c, bool isCalls)
+ public CallTreeNodeSqlNameSet(SqlQueryContext c)
{
- this.IsCalls = isCalls;
-
string prefix = c.GenerateUniqueVariableName();
ID = prefix + "ID";
NameID = prefix + "NameID";
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs b/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs
index 04f5e774a8..bdca6fd303 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs
@@ -60,12 +60,20 @@ namespace ICSharpCode.Profiler.Controller.Data
///
public abstract string GetProperty(string name);
+ ///
+ /// Returns the list of all calls in a specified range of datasets.
+ ///
+ public virtual IQueryable GetAllCalls(int startIndex, int endIndex)
+ {
+ return GetRoot(startIndex, endIndex).Descendants;
+ }
+
///
/// Returns the list of all functions called in a specified range of datasets.
///
public virtual IQueryable GetFunctions(int startIndex, int endIndex)
{
- return GetRoot(startIndex, endIndex).Descendants.Where(c => !c.IsThread).MergeByName();
+ return GetAllCalls(startIndex, endIndex).Where(c => !c.IsThread).MergeByName();
}
}
}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataSQLiteProvider.cs b/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataSQLiteProvider.cs
index 60e49df261..5ca59b542a 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataSQLiteProvider.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataSQLiteProvider.cs
@@ -191,7 +191,7 @@ namespace ICSharpCode.Profiler.Controller.Data
endIndex = help;
}
- SQLiteQueryProvider queryProvider = new SQLiteQueryProvider(this, startIndex);
+ SQLiteQueryProvider queryProvider = new SQLiteQueryProvider(this, startIndex, endIndex);
Expression> filterLambda = c => c.ParentID == -1;
return queryProvider.CreateQuery(new Filter(AllCalls.Instance, DataSetFilter(startIndex, endIndex), filterLambda)).Merge();
}
@@ -263,19 +263,17 @@ namespace ICSharpCode.Profiler.Controller.Data
}
///
- public override IQueryable GetFunctions(int startIndex, int endIndex)
+ public override IQueryable GetAllCalls(int startIndex, int endIndex)
{
- if (startIndex < 0 || startIndex >= this.dataSets.Count)
+ if (startIndex < 0 || startIndex >= this.DataSets.Count)
throw new ArgumentOutOfRangeException("startIndex", startIndex, "Value must be between 0 and " + endIndex);
if (endIndex < startIndex || endIndex >= this.DataSets.Count)
throw new ArgumentOutOfRangeException("endIndex", endIndex, "Value must be between " + startIndex + " and " + (this.DataSets.Count - 1));
- SQLiteQueryProvider queryProvider = new SQLiteQueryProvider(this, startIndex);
+ SQLiteQueryProvider queryProvider = new SQLiteQueryProvider(this, startIndex, endIndex);
- var query = queryProvider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, DataSetFilter(startIndex, endIndex))));
- return from c in query
- where c.NameMapping.Id != 0 && !c.IsThread
- select c;
+ var query = queryProvider.CreateQuery(new Filter(AllCalls.Instance, DataSetFilter(startIndex, endIndex)));
+ return query.Where(c => c.NameMapping.Id != 0);
}
Expression> DataSetFilter(int startIndex, int endIndex)
@@ -283,7 +281,7 @@ namespace ICSharpCode.Profiler.Controller.Data
return c => startIndex <= c.DataSetID && c.DataSetID <= endIndex;
}
- internal IList RunSQLNodeList(SQLiteQueryProvider queryProvider, string command)
+ internal IList RunSQLNodeList(SQLiteQueryProvider queryProvider, string command, bool hasIdList)
{
List result = new List();
@@ -293,18 +291,21 @@ namespace ICSharpCode.Profiler.Controller.Data
using (SQLiteDataReader reader = cmd.ExecuteReader()) {
while (reader.Read()) {
- SQLiteCallTreeNode node = new SQLiteCallTreeNode(reader.GetInt32(1), null, queryProvider);
- node.callCount = reader.GetInt32(3);
- node.cpuCyclesSpent = reader.GetInt64(2);
- object ids = reader.GetValue(0);
- if (ids is long) {
- node.ids = new List { (int)(long)ids };
- } else {
- node.ids = reader.GetString(0).Split(',').Select(s => int.Parse(s)).ToList();
- node.ids.Sort();
+ SQLiteCallTreeNode node = new SQLiteCallTreeNode(reader.GetInt32(0), null, queryProvider);
+ node.callCount = reader.GetInt32(2);
+ node.cpuCyclesSpent = reader.GetInt64(1);
+ if (hasIdList) {
+ object ids = reader.GetValue(5);
+ if (ids is long) {
+ node.IdList = new int[] { (int)(long)ids };
+ } else {
+ int[] idList = ids.ToString().Split(',').Select(s => int.Parse(s)).ToArray();
+ Array.Sort(idList);
+ node.IdList = idList;
+ }
}
- node.hasChildren = reader.GetBoolean(4);
- node.activeCallCount = reader.GetInt32(5);
+ node.hasChildren = reader.GetBoolean(3);
+ node.activeCallCount = reader.GetInt32(4);
result.Add(node);
}
}
diff --git a/src/AddIns/Misc/Profiler/Controller/Data/SQLiteCallTreeNode.cs b/src/AddIns/Misc/Profiler/Controller/Data/SQLiteCallTreeNode.cs
index b17b345c94..e9ef15a5e9 100644
--- a/src/AddIns/Misc/Profiler/Controller/Data/SQLiteCallTreeNode.cs
+++ b/src/AddIns/Misc/Profiler/Controller/Data/SQLiteCallTreeNode.cs
@@ -26,7 +26,6 @@ namespace ICSharpCode.Profiler.Controller.Data
internal long cpuCyclesSpent;
CallTreeNode parent;
SQLiteQueryProvider provider;
- internal List ids = new List();
internal bool hasChildren;
internal int activeCallCount;
@@ -39,6 +38,26 @@ namespace ICSharpCode.Profiler.Controller.Data
this.parent = parent;
this.provider = provider;
}
+
+ volatile int[] ids;
+
+ ///
+ /// Gets/Sets the ID list.
+ /// For function nodes, the usually long ID list is delay-loaded.
+ ///
+ internal int[] IdList {
+ get {
+ int[] tmp = this.ids;
+ if (tmp == null) {
+ tmp = provider.LoadIDListForFunction(nameId);
+ this.ids = tmp;
+ }
+ return tmp;
+ }
+ set {
+ this.ids = value;
+ }
+ }
///
/// Gets a reference to the name, return type and parameter list of the method.
@@ -94,7 +113,8 @@ namespace ICSharpCode.Profiler.Controller.Data
if (!hasChildren)
return EmptyQueryable;
- Expression> filterLambda = c => this.ids.Contains(c.ParentID);
+ List ids = this.IdList.ToList();
+ Expression> filterLambda = c => ids.Contains(c.ParentID);
return provider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, filterLambda)));
}
}
@@ -135,10 +155,11 @@ namespace ICSharpCode.Profiler.Controller.Data
public override CallTreeNode Merge(IEnumerable nodes)
{
SQLiteCallTreeNode mergedNode = new SQLiteCallTreeNode(0, null, this.provider);
+ List mergedIds = new List();
bool initialised = false;
foreach (SQLiteCallTreeNode node in nodes) {
- mergedNode.ids.AddRange(node.ids);
+ mergedIds.AddRange(node.IdList);
mergedNode.callCount += node.callCount;
mergedNode.cpuCyclesSpent += node.cpuCyclesSpent;
mergedNode.activeCallCount += node.activeCallCount;
@@ -149,6 +170,8 @@ namespace ICSharpCode.Profiler.Controller.Data
mergedNode.nameId = 0;
initialised = true;
}
+ mergedIds.Sort();
+ mergedNode.IdList = mergedIds.ToArray();
return mergedNode;
}
@@ -162,7 +185,7 @@ namespace ICSharpCode.Profiler.Controller.Data
List parentIDList = provider.RunSQLIDList(
"SELECT parentid FROM FunctionData "
- + "WHERE id IN(" + string.Join(",", this.ids.Select(s => s.ToString()).ToArray()) + @")");
+ + "WHERE id IN(" + string.Join(",", this.IdList.Select(s => s.ToString()).ToArray()) + @")");
Expression> filterLambda = c => parentIDList.Contains(c.ID);
return provider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, filterLambda)));
@@ -174,11 +197,13 @@ namespace ICSharpCode.Profiler.Controller.Data
SQLiteCallTreeNode node = other as SQLiteCallTreeNode;
if (node != null) {
- if (node.ids.Count != this.ids.Count)
+ int[] a = this.IdList;
+ int[] b = node.IdList;
+ if (a.Length != b.Length)
return false;
- for (int i = 0; i < this.ids.Count; i++) {
- if (node.ids[i] != this.ids[i])
+ for (int i = 0; i < a.Length; i++) {
+ if (a[i] != b[i])
return false;
}
@@ -195,7 +220,7 @@ namespace ICSharpCode.Profiler.Controller.Data
int hash = 0;
unchecked {
- foreach (int i in this.ids) {
+ foreach (int i in this.IdList) {
hash = hash * hashPrime + i;
}
}
diff --git a/src/AddIns/Misc/Profiler/Controller/Profiler.Controller.csproj b/src/AddIns/Misc/Profiler/Controller/Profiler.Controller.csproj
index d2635c8763..cd72c83d7d 100644
--- a/src/AddIns/Misc/Profiler/Controller/Profiler.Controller.csproj
+++ b/src/AddIns/Misc/Profiler/Controller/Profiler.Controller.csproj
@@ -95,10 +95,16 @@
+
+
+
+
+
-
+
+
diff --git a/src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs b/src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs
index db49c10814..defc61669b 100644
--- a/src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs
+++ b/src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs
@@ -144,6 +144,36 @@ namespace Profiler.Tests.Controller.Data
Assert.AreEqual(350 * k, functions[1].CpuCyclesSpent);
}
+
+ [Test]
+ public void TestFunctionsChildren()
+ {
+ CallTreeNode[] functions = provider.GetFunctions(1, 2).OrderBy(f => f.Name).WithQueryLog(Console.Out).ToArray();
+ CallTreeNode[] children = functions[1].Children.ToArray();
+
+ Assert.AreEqual(1, children.Length);
+ Assert.AreEqual("m1", children[0].Name);
+ Assert.IsFalse(children[0].HasChildren);
+ Assert.AreEqual(5, children[0].CallCount);
+ Assert.AreEqual(1 * k, children[0].CpuCyclesSpent);
+ }
+
+ [Test]
+ public void TestFunctionsQuery()
+ {
+ var query = provider.GetFunctions(0, 1);
+ Assert.AreEqual("AllFunctions(0, 1).Filter(c => (c.NameMapping.Id != 0) && c => Not(GlobImpl(c.NameMapping.Name, \"Thread#*\")))",
+ SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
+ }
+
+ [Test]
+ public void TestAllCallsMergedToFunctions()
+ {
+ var query = provider.GetAllCalls(0, 1).MergeByName();
+ Assert.AreEqual("AllFunctions(0, 1).Filter(c => (c.NameMapping.Id != 0))",
+ SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
+ }
+
[Test]
public void TestSupportedOrderByOnRootChildren()
{