From 2ed12a18e029480f0340a04b22b3e92d1fbecea6 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Thu, 1 Oct 2009 12:03:19 +0000 Subject: [PATCH] Optimize AllCalls.MergeByName() -> AllFunctions. Delay load ID-list in functions view. The "Top 20" view now opens more than twice as fast. git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@5036 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../Profiler/Controller/Data/Linq/AllCalls.cs | 60 +++ .../Controller/Data/Linq/AllFunctions.cs | 69 +++ .../Data/Linq/ExpressionSqlWriter.cs | 12 +- .../Profiler/Controller/Data/Linq/Filter.cs | 107 +++++ .../Profiler/Controller/Data/Linq/Limit.cs | 62 +++ .../Controller/Data/Linq/MergeByName.cs | 70 +++ .../Linq/OptimizeQueryExpressionVisitor.cs | 119 +++-- .../Profiler/Controller/Data/Linq/QueryAst.cs | 422 ------------------ .../Controller/Data/Linq/QueryNode.cs | 129 ++++++ .../Data/Linq/SQLiteQueryProvider.cs | 33 +- .../Profiler/Controller/Data/Linq/Sort.cs | 105 +++++ .../Controller/Data/Linq/SqlQueryContext.cs | 48 +- .../Controller/Data/ProfilingDataProvider.cs | 10 +- .../Data/ProfilingDataSQLiteProvider.cs | 41 +- .../Controller/Data/SQLiteCallTreeNode.cs | 41 +- .../Controller/Profiler.Controller.csproj | 8 +- .../Controller/Data/LinqTests.cs | 30 ++ 17 files changed, 860 insertions(+), 506 deletions(-) create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/AllCalls.cs create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/AllFunctions.cs create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/Filter.cs create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/Limit.cs create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/MergeByName.cs delete mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryNode.cs create mode 100644 src/AddIns/Misc/Profiler/Controller/Data/Linq/Sort.cs 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() {