Browse Source

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
shortcuts
Daniel Grunwald 16 years ago
parent
commit
2ed12a18e0
  1. 60
      src/AddIns/Misc/Profiler/Controller/Data/Linq/AllCalls.cs
  2. 69
      src/AddIns/Misc/Profiler/Controller/Data/Linq/AllFunctions.cs
  3. 12
      src/AddIns/Misc/Profiler/Controller/Data/Linq/ExpressionSqlWriter.cs
  4. 107
      src/AddIns/Misc/Profiler/Controller/Data/Linq/Filter.cs
  5. 62
      src/AddIns/Misc/Profiler/Controller/Data/Linq/Limit.cs
  6. 70
      src/AddIns/Misc/Profiler/Controller/Data/Linq/MergeByName.cs
  7. 119
      src/AddIns/Misc/Profiler/Controller/Data/Linq/OptimizeQueryExpressionVisitor.cs
  8. 422
      src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs
  9. 129
      src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryNode.cs
  10. 33
      src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs
  11. 105
      src/AddIns/Misc/Profiler/Controller/Data/Linq/Sort.cs
  12. 48
      src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs
  13. 10
      src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs
  14. 41
      src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataSQLiteProvider.cs
  15. 41
      src/AddIns/Misc/Profiler/Controller/Data/SQLiteCallTreeNode.cs
  16. 8
      src/AddIns/Misc/Profiler/Controller/Profiler.Controller.csproj
  17. 30
      src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs

60
src/AddIns/Misc/Profiler/Controller/Data/Linq/AllCalls.cs

@ -0,0 +1,60 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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
{
/// <summary>
/// Query AST node representing the whole 'FunctionData' table.
/// This is the source of all queries.
/// Produces SELECT .. FROM .. in SQL.
/// </summary>
sealed class AllCalls : QueryNode
{
public static readonly AllCalls Instance = new AllCalls();
private AllCalls() : base(null)
{
}
protected override Expression VisitChildren(Func<Expression, Expression> 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;
}
}
}

69
src/AddIns/Misc/Profiler/Controller/Data/Linq/AllFunctions.cs

@ -0,0 +1,69 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.Linq.Expressions;
using System.Text;
namespace ICSharpCode.Profiler.Controller.Data.Linq
{
/// <summary>
/// Query AST node representing the whole 'FunctionData' table.
/// This is the source of all queries.
/// Produces SELECT .. FROM .. in SQL.
/// </summary>
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<Expression, Expression> 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;
}
}
}

12
src/AddIns/Misc/Profiler/Controller/Data/Linq/ExpressionSqlWriter.cs

@ -16,16 +16,18 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{ {
readonly TextWriter w; readonly TextWriter w;
readonly ParameterExpression callTreeNodeParameter; readonly ParameterExpression callTreeNodeParameter;
readonly SqlQueryContext context;
readonly CallTreeNodeSqlNameSet nameSet; readonly CallTreeNodeSqlNameSet nameSet;
public ExpressionSqlWriter(TextWriter w, CallTreeNodeSqlNameSet nameSet, ParameterExpression callTreeNodeParameter) public ExpressionSqlWriter(TextWriter w, SqlQueryContext context, ParameterExpression callTreeNodeParameter)
{ {
if (w == null) if (w == null)
throw new ArgumentNullException("w"); throw new ArgumentNullException("w");
if (nameSet == null) if (context == null)
throw new ArgumentNullException("nameSet"); throw new ArgumentNullException("context");
this.w = w; this.w = w;
this.nameSet = nameSet; this.context = context;
this.nameSet = context.CurrentNameSet;
this.callTreeNodeParameter = callTreeNodeParameter; this.callTreeNodeParameter = callTreeNodeParameter;
} }
@ -107,7 +109,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
void WriteMemberAccess(MemberExpression me) void WriteMemberAccess(MemberExpression me)
{ {
if (me.Expression == callTreeNodeParameter) { 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"); throw new InvalidOperationException("SingleCall references are invalid here");
if (me.Member == SingleCall.IDField) { if (me.Member == SingleCall.IDField) {
w.Write("id"); w.Write("id");

107
src/AddIns/Misc/Profiler/Controller/Data/Linq/Filter.cs

@ -0,0 +1,107 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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
{
/// <summary>
/// Query node that filters the input using conditions. Produces WHERE or HAVING in SQL.
/// </summary>
sealed class Filter : QueryNode
{
/// <summary>
/// List of conditions. The operator between these is AND.
/// </summary>
public readonly ReadOnlyCollection<LambdaExpression> 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<Expression, Expression> 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());
}
}
}

62
src/AddIns/Misc/Profiler/Controller/Data/Linq/Limit.cs

@ -0,0 +1,62 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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
{
/// <summary>
/// Query node that limits the amount of data selected. Produces LIMIT in SQL.
/// </summary>
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<Expression, Expression> 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;
}
}
}

70
src/AddIns/Misc/Profiler/Controller/Data/Linq/MergeByName.cs

@ -0,0 +1,70 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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
{
/// <summary>
/// Query node that represents the 'group by nameid and merge' operation. Produces SELECT ... GROUP BY .. in SQL.
/// </summary>
sealed class MergeByName : QueryNode
{
public MergeByName(QueryNode target)
: base(target)
{
Debug.Assert(target != null);
}
protected override Expression VisitChildren(Func<Expression, Expression> 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;
}
}
}

119
src/AddIns/Misc/Profiler/Controller/Data/Linq/OptimizeQueryExpressionVisitor.cs

@ -50,6 +50,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return ReorderFilter(filter); return ReorderFilter(filter);
} }
static readonly MemberInfo[] SafeMembersForMoveIntoMergeByName = { KnownMembers.CallTreeNode_NameMapping };
/// <summary> /// <summary>
/// Tries to combine nested filters; /// Tries to combine nested filters;
/// move 'MergeByName' nodes out of filter, if possible /// move 'MergeByName' nodes out of filter, if possible
@ -63,7 +65,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
} else if (filter.Target is MergeByName) { } else if (filter.Target is MergeByName) {
// x.MergeByName().Filter(<criteria>) -> x.Filter(x, <criteria>).MergeByName() for some safe criterias // x.MergeByName().Filter(<criteria>) -> x.Filter(x, <criteria>).MergeByName() for some safe criterias
QueryNode innerTarget = filter.Target.Target; 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) { if (conditionsToMoveIntoFilter.Length != 0) {
MergeByName newTarget = new MergeByName(ReorderFilter(new Filter(innerTarget, conditionsToMoveIntoFilter))); MergeByName newTarget = new MergeByName(ReorderFilter(new Filter(innerTarget, conditionsToMoveIntoFilter)));
var conditionsKeptOutsideFilter = filter.Conditions.Except(conditionsToMoveIntoFilter).ToArray(); 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);
}
}
/// <summary> /// <summary>
/// Optimizes the filter; but does not try to combine nested filter (etc.) /// Optimizes the filter; but does not try to combine nested filter (etc.)
/// </summary> /// </summary>
@ -145,8 +115,59 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
// x.MergeByName().MergeByName() -> x.MergeByName() // x.MergeByName().MergeByName() -> x.MergeByName()
return target; 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<LambdaExpression> newConditions = new List<LambdaExpression>();
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); return new MergeByName(target);
} }
static object GetConstantValue(Expression expr)
{
return ((ConstantExpression)expr).Value;
}
#endregion #endregion
protected override Expression VisitMethodCall(MethodCallExpression node) protected override Expression VisitMethodCall(MethodCallExpression node)
@ -162,4 +183,36 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return base.VisitMethodCall(node); 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);
}
}
} }

422
src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs

@ -1,422 +0,0 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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
{
/// <summary>
/// The SingleCall class is never instanciated; it only used to represent database rows
/// inside expression trees.
/// </summary>
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");
}
/// <summary>
/// Base class for nodes in the Query AST.
/// </summary>
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<CallTreeNode>);
}
protected abstract override Expression VisitChildren(Func<Expression, Expression> visitor);
/// <summary>
/// SQL construction documentation see SQLiteQueryProvider documentation.
/// </summary>
public abstract SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context);
/// <summary>
/// Wraps the current SQL statement into an inner select, allowing to continue with "WHERE" queries
/// even after ORDER BY or LIMIT.
/// </summary>
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;
}
/// <summary>
/// Helper function that builds the text 'expression AS newName'
/// </summary>
protected static string SqlAs(string expression, string newName)
{
if (expression == newName)
return newName;
else
return expression + " AS " + newName;
}
public IQueryable<CallTreeNode> 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<CallTreeNode> result = provider.RunSQLNodeList(b.ToString());
w.Stop();
if (options.HasLoggers) {
options.WriteLogLine("Query returned " + result.Count + " rows in " + w.Elapsed);
}
return result.AsQueryable();
}
}
/// <summary>
/// Query AST node representing the whole 'FunctionData' table.
/// This is the source of all queries.
/// Produces SELECT .. FROM .. in SQL.
/// </summary>
sealed class AllCalls : QueryNode
{
public static readonly AllCalls Instance = new AllCalls();
private AllCalls() : base(null)
{
}
protected override Expression VisitChildren(Func<Expression, Expression> 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;
}
}
/// <summary>
/// Query node that represents the 'group by nameid and merge' operation. Produces SELECT ... GROUP BY .. in SQL.
/// </summary>
sealed class MergeByName : QueryNode
{
public MergeByName(QueryNode target)
: base(target)
{
Debug.Assert(target != null);
}
protected override Expression VisitChildren(Func<Expression, Expression> 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;
}
}
/// <summary>
/// Query node that filters the input using conditions. Produces WHERE or HAVING in SQL.
/// </summary>
sealed class Filter : QueryNode
{
/// <summary>
/// List of conditions. The operator between these is AND.
/// </summary>
public readonly ReadOnlyCollection<LambdaExpression> 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<Expression, Expression> 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());
}
}
/// <summary>
/// Query node that limits the amount of data selected. Produces LIMIT in SQL.
/// </summary>
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<Expression, Expression> 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<SortArgument> arguments;
public Sort(QueryNode target, IList<SortArgument> args)
: base(target)
{
this.arguments = new ReadOnlyCollection<SortArgument>(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<Expression, Expression> 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() + ")";
}
}
}

129
src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryNode.cs

@ -0,0 +1,129 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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
{
/// <summary>
/// The SingleCall class is never instanciated; it only used to represent database rows
/// inside expression trees.
/// </summary>
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");
}
/// <summary>
/// Base class for nodes in the Query AST.
/// </summary>
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<CallTreeNode>);
}
protected abstract override Expression VisitChildren(Func<Expression, Expression> visitor);
/// <summary>
/// SQL construction documentation see SQLiteQueryProvider documentation.
/// </summary>
public abstract SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context);
/// <summary>
/// Wraps the current SQL statement into an inner select, allowing to continue with "WHERE" queries
/// even after ORDER BY or LIMIT.
/// </summary>
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);
}
/// <summary>
/// Helper function that builds the text 'expression AS newName'
/// </summary>
protected static string SqlAs(string expression, string newName)
{
if (expression == newName)
return newName;
else
return expression + " AS " + newName;
}
public IQueryable<CallTreeNode> 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<CallTreeNode> 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();
}
}
}

33
src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; 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). Note: Skip/Take combinations are not combined (Skip is not supported).
input.OrderBy(x => x.SomeField) -> input.Sort({ [x.SomeField, ascending] }) 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] }) 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: Translation rules for expression importer:
Any valid expressions (as defined in 'valid expressions in QueryAst nodes') are copied over directly. 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#*") c.IsThread -> Glob(c.NameMapping.Name, "Thread#*")
s.StartsWith(constantString, StringComparison.Ordinal) -> Glob(s, constantString + "*"); s.StartsWith(constantString, StringComparison.Ordinal) -> Glob(s, constantString + "*");
s.StartsWith(constantString, StringComparison.OrdinalIgnoreCase) -> Like(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: Optimization of QueryAst:
The OptimizeQueryExpressionVisitor is performing these optimizations: 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 x.MergeByName().Filter(criteria) -> x.Filter(criteria).MergeByName() for some safe criterias
Criterias are safe if they access no CallTreeNode properties except for NameMapping Criterias are safe if they access no CallTreeNode properties except for NameMapping
x.MergeByName().MergeByName() -> x.MergeByName() 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: 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. 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; 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) if (sqliteProvider == null)
throw new ArgumentNullException("sqliteProvider"); throw new ArgumentNullException("sqliteProvider");
this.sqliteProvider = sqliteProvider; this.sqliteProvider = sqliteProvider;
this.startDataSetID = startDataSetID; this.startDataSetID = startDataSetID;
this.endDataSetID = endDataSetID;
} }
// Implement GetMapping and ProcessorFrequency so that SQLiteQueryProvider can be used in place of // 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; } get { return sqliteProvider.ProcessorFrequency; }
} }
public IList<CallTreeNode> RunSQLNodeList(string command) public IList<CallTreeNode> RunSQLNodeList(string command, bool hasIdList)
{ {
return sqliteProvider.RunSQLNodeList(this, command); return sqliteProvider.RunSQLNodeList(this, command, hasIdList);
} }
/// <summary> /// <summary>
@ -164,6 +172,15 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return sqliteProvider.RunSQLIDList(command); 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<CallTreeNode> CreateQuery(QueryNode query) public IQueryable<CallTreeNode> CreateQuery(QueryNode query)
{ {
return new Query<CallTreeNode>(this, query); return new Query<CallTreeNode>(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) { } else if (node.Method == KnownMembers.Queryable_WithQueryLog && node.Arguments[1].NodeType == ExpressionType.Constant) {
options.AddLogger((TextWriter)(((ConstantExpression)node.Arguments[1]).Value)); options.AddLogger((TextWriter)(((ConstantExpression)node.Arguments[1]).Value));
return Visit(node.Arguments[0]); 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) { } else if (node.Method == KnownMembers.QueryableOfCallTreeNode_Take && node.Arguments[1].NodeType == ExpressionType.Constant) {
ConstantExpression ce = (ConstantExpression)node.Arguments[1]; ConstantExpression ce = (ConstantExpression)node.Arguments[1];
if (ce.Type == typeof(int)) { if (ce.Type == typeof(int)) {

105
src/AddIns/Misc/Profiler/Controller/Data/Linq/Sort.cs

@ -0,0 +1,105 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <owner name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using System;
using System.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<SortArgument> arguments;
public Sort(QueryNode target, IList<SortArgument> args)
: base(target)
{
this.arguments = new ReadOnlyCollection<SortArgument>(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<Expression, Expression> 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() + ")";
}
}
}

48
src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs

@ -14,6 +14,31 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{ {
public readonly int StartDataSetID; public readonly int StartDataSetID;
public CallTreeNodeSqlNameSet CurrentNameSet { get; private set; }
/// <summary>
/// The type of the table currently being accessed (the current FROM clause).
/// Is 'None' when reading from an inner query.
/// </summary>
public SqlTableType CurrentTable { get; private set; }
/// <summary>
/// Passed down the query tree to signalize that the ID list is required.
/// </summary>
public bool RequireIDList;
/// <summary>
/// Passed up the query tree to signalize whether an ID list is present.
/// </summary>
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) public SqlQueryContext(SQLiteQueryProvider provider)
{ {
this.StartDataSetID = provider.startDataSetID; this.StartDataSetID = provider.startDataSetID;
@ -25,8 +50,18 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{ {
return "v" + (++uniqueVariableIndex).ToString(CultureInfo.InvariantCulture); return "v" + (++uniqueVariableIndex).ToString(CultureInfo.InvariantCulture);
} }
}
public CallTreeNodeSqlNameSet CurrentNameSet;
enum SqlTableType
{
/// <summary>
/// No direct table
/// </summary>
None,
/// <summary>
/// The FunctionData table
/// </summary>
Calls
} }
sealed class CallTreeNodeSqlNameSet sealed class CallTreeNodeSqlNameSet
@ -38,15 +73,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
public readonly string HasChildren; public readonly string HasChildren;
public readonly string ActiveCallCount; public readonly string ActiveCallCount;
/// <summary> public CallTreeNodeSqlNameSet(SqlQueryContext c)
/// Gets whether this nameset represents non-merged calls.
/// </summary>
public readonly bool IsCalls;
public CallTreeNodeSqlNameSet(SqlQueryContext c, bool isCalls)
{ {
this.IsCalls = isCalls;
string prefix = c.GenerateUniqueVariableName(); string prefix = c.GenerateUniqueVariableName();
ID = prefix + "ID"; ID = prefix + "ID";
NameID = prefix + "NameID"; NameID = prefix + "NameID";

10
src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs

@ -60,12 +60,20 @@ namespace ICSharpCode.Profiler.Controller.Data
/// </summary> /// </summary>
public abstract string GetProperty(string name); public abstract string GetProperty(string name);
/// <summary>
/// Returns the list of all calls in a specified range of datasets.
/// </summary>
public virtual IQueryable<CallTreeNode> GetAllCalls(int startIndex, int endIndex)
{
return GetRoot(startIndex, endIndex).Descendants;
}
/// <summary> /// <summary>
/// Returns the list of all functions called in a specified range of datasets. /// Returns the list of all functions called in a specified range of datasets.
/// </summary> /// </summary>
public virtual IQueryable<CallTreeNode> GetFunctions(int startIndex, int endIndex) public virtual IQueryable<CallTreeNode> GetFunctions(int startIndex, int endIndex)
{ {
return GetRoot(startIndex, endIndex).Descendants.Where(c => !c.IsThread).MergeByName(); return GetAllCalls(startIndex, endIndex).Where(c => !c.IsThread).MergeByName();
} }
} }
} }

41
src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataSQLiteProvider.cs

@ -191,7 +191,7 @@ namespace ICSharpCode.Profiler.Controller.Data
endIndex = help; endIndex = help;
} }
SQLiteQueryProvider queryProvider = new SQLiteQueryProvider(this, startIndex); SQLiteQueryProvider queryProvider = new SQLiteQueryProvider(this, startIndex, endIndex);
Expression<Func<SingleCall, bool>> filterLambda = c => c.ParentID == -1; Expression<Func<SingleCall, bool>> filterLambda = c => c.ParentID == -1;
return queryProvider.CreateQuery(new Filter(AllCalls.Instance, DataSetFilter(startIndex, endIndex), filterLambda)).Merge(); return queryProvider.CreateQuery(new Filter(AllCalls.Instance, DataSetFilter(startIndex, endIndex), filterLambda)).Merge();
} }
@ -263,19 +263,17 @@ namespace ICSharpCode.Profiler.Controller.Data
} }
/// <inheritdoc/> /// <inheritdoc/>
public override IQueryable<CallTreeNode> GetFunctions(int startIndex, int endIndex) public override IQueryable<CallTreeNode> 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); throw new ArgumentOutOfRangeException("startIndex", startIndex, "Value must be between 0 and " + endIndex);
if (endIndex < startIndex || endIndex >= this.DataSets.Count) if (endIndex < startIndex || endIndex >= this.DataSets.Count)
throw new ArgumentOutOfRangeException("endIndex", endIndex, "Value must be between " + startIndex + " and " + (this.DataSets.Count - 1)); 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)))); var query = queryProvider.CreateQuery(new Filter(AllCalls.Instance, DataSetFilter(startIndex, endIndex)));
return from c in query return query.Where(c => c.NameMapping.Id != 0);
where c.NameMapping.Id != 0 && !c.IsThread
select c;
} }
Expression<Func<SingleCall, bool>> DataSetFilter(int startIndex, int endIndex) Expression<Func<SingleCall, bool>> DataSetFilter(int startIndex, int endIndex)
@ -283,7 +281,7 @@ namespace ICSharpCode.Profiler.Controller.Data
return c => startIndex <= c.DataSetID && c.DataSetID <= endIndex; return c => startIndex <= c.DataSetID && c.DataSetID <= endIndex;
} }
internal IList<CallTreeNode> RunSQLNodeList(SQLiteQueryProvider queryProvider, string command) internal IList<CallTreeNode> RunSQLNodeList(SQLiteQueryProvider queryProvider, string command, bool hasIdList)
{ {
List<CallTreeNode> result = new List<CallTreeNode>(); List<CallTreeNode> result = new List<CallTreeNode>();
@ -293,18 +291,21 @@ namespace ICSharpCode.Profiler.Controller.Data
using (SQLiteDataReader reader = cmd.ExecuteReader()) { using (SQLiteDataReader reader = cmd.ExecuteReader()) {
while (reader.Read()) { while (reader.Read()) {
SQLiteCallTreeNode node = new SQLiteCallTreeNode(reader.GetInt32(1), null, queryProvider); SQLiteCallTreeNode node = new SQLiteCallTreeNode(reader.GetInt32(0), null, queryProvider);
node.callCount = reader.GetInt32(3); node.callCount = reader.GetInt32(2);
node.cpuCyclesSpent = reader.GetInt64(2); node.cpuCyclesSpent = reader.GetInt64(1);
object ids = reader.GetValue(0); if (hasIdList) {
if (ids is long) { object ids = reader.GetValue(5);
node.ids = new List<int> { (int)(long)ids }; if (ids is long) {
} else { node.IdList = new int[] { (int)(long)ids };
node.ids = reader.GetString(0).Split(',').Select(s => int.Parse(s)).ToList(); } else {
node.ids.Sort(); int[] idList = ids.ToString().Split(',').Select(s => int.Parse(s)).ToArray();
Array.Sort(idList);
node.IdList = idList;
}
} }
node.hasChildren = reader.GetBoolean(4); node.hasChildren = reader.GetBoolean(3);
node.activeCallCount = reader.GetInt32(5); node.activeCallCount = reader.GetInt32(4);
result.Add(node); result.Add(node);
} }
} }

41
src/AddIns/Misc/Profiler/Controller/Data/SQLiteCallTreeNode.cs

@ -26,7 +26,6 @@ namespace ICSharpCode.Profiler.Controller.Data
internal long cpuCyclesSpent; internal long cpuCyclesSpent;
CallTreeNode parent; CallTreeNode parent;
SQLiteQueryProvider provider; SQLiteQueryProvider provider;
internal List<int> ids = new List<int>();
internal bool hasChildren; internal bool hasChildren;
internal int activeCallCount; internal int activeCallCount;
@ -39,6 +38,26 @@ namespace ICSharpCode.Profiler.Controller.Data
this.parent = parent; this.parent = parent;
this.provider = provider; this.provider = provider;
} }
volatile int[] ids;
/// <summary>
/// Gets/Sets the ID list.
/// For function nodes, the usually long ID list is delay-loaded.
/// </summary>
internal int[] IdList {
get {
int[] tmp = this.ids;
if (tmp == null) {
tmp = provider.LoadIDListForFunction(nameId);
this.ids = tmp;
}
return tmp;
}
set {
this.ids = value;
}
}
/// <summary> /// <summary>
/// Gets a reference to the name, return type and parameter list of the method. /// 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) if (!hasChildren)
return EmptyQueryable; return EmptyQueryable;
Expression<Func<SingleCall, bool>> filterLambda = c => this.ids.Contains(c.ParentID); List<int> ids = this.IdList.ToList();
Expression<Func<SingleCall, bool>> filterLambda = c => ids.Contains(c.ParentID);
return provider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, filterLambda))); return provider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, filterLambda)));
} }
} }
@ -135,10 +155,11 @@ namespace ICSharpCode.Profiler.Controller.Data
public override CallTreeNode Merge(IEnumerable<CallTreeNode> nodes) public override CallTreeNode Merge(IEnumerable<CallTreeNode> nodes)
{ {
SQLiteCallTreeNode mergedNode = new SQLiteCallTreeNode(0, null, this.provider); SQLiteCallTreeNode mergedNode = new SQLiteCallTreeNode(0, null, this.provider);
List<int> mergedIds = new List<int>();
bool initialised = false; bool initialised = false;
foreach (SQLiteCallTreeNode node in nodes) { foreach (SQLiteCallTreeNode node in nodes) {
mergedNode.ids.AddRange(node.ids); mergedIds.AddRange(node.IdList);
mergedNode.callCount += node.callCount; mergedNode.callCount += node.callCount;
mergedNode.cpuCyclesSpent += node.cpuCyclesSpent; mergedNode.cpuCyclesSpent += node.cpuCyclesSpent;
mergedNode.activeCallCount += node.activeCallCount; mergedNode.activeCallCount += node.activeCallCount;
@ -149,6 +170,8 @@ namespace ICSharpCode.Profiler.Controller.Data
mergedNode.nameId = 0; mergedNode.nameId = 0;
initialised = true; initialised = true;
} }
mergedIds.Sort();
mergedNode.IdList = mergedIds.ToArray();
return mergedNode; return mergedNode;
} }
@ -162,7 +185,7 @@ namespace ICSharpCode.Profiler.Controller.Data
List<int> parentIDList = provider.RunSQLIDList( List<int> parentIDList = provider.RunSQLIDList(
"SELECT parentid FROM FunctionData " "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<Func<SingleCall, bool>> filterLambda = c => parentIDList.Contains(c.ID); Expression<Func<SingleCall, bool>> filterLambda = c => parentIDList.Contains(c.ID);
return provider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, filterLambda))); return provider.CreateQuery(new MergeByName(new Filter(AllCalls.Instance, filterLambda)));
@ -174,11 +197,13 @@ namespace ICSharpCode.Profiler.Controller.Data
SQLiteCallTreeNode node = other as SQLiteCallTreeNode; SQLiteCallTreeNode node = other as SQLiteCallTreeNode;
if (node != null) { 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; return false;
for (int i = 0; i < this.ids.Count; i++) { for (int i = 0; i < a.Length; i++) {
if (node.ids[i] != this.ids[i]) if (a[i] != b[i])
return false; return false;
} }
@ -195,7 +220,7 @@ namespace ICSharpCode.Profiler.Controller.Data
int hash = 0; int hash = 0;
unchecked { unchecked {
foreach (int i in this.ids) { foreach (int i in this.IdList) {
hash = hash * hashPrime + i; hash = hash * hashPrime + i;
} }
} }

8
src/AddIns/Misc/Profiler/Controller/Profiler.Controller.csproj

@ -95,10 +95,16 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Analysis\IProfilingDataComparer.cs" /> <Compile Include="Analysis\IProfilingDataComparer.cs" />
<Compile Include="Data\Linq\AllCalls.cs" />
<Compile Include="Data\Linq\AllFunctions.cs" />
<Compile Include="Data\Linq\ExpressionSqlWriter.cs" /> <Compile Include="Data\Linq\ExpressionSqlWriter.cs" />
<Compile Include="Data\Linq\Filter.cs" />
<Compile Include="Data\Linq\KnownMembers.cs" /> <Compile Include="Data\Linq\KnownMembers.cs" />
<Compile Include="Data\Linq\Limit.cs" />
<Compile Include="Data\Linq\MergeByName.cs" />
<Compile Include="Data\Linq\OptimizeQueryExpressionVisitor.cs" /> <Compile Include="Data\Linq\OptimizeQueryExpressionVisitor.cs" />
<Compile Include="Data\Linq\QueryAst.cs" /> <Compile Include="Data\Linq\QueryNode.cs" />
<Compile Include="Data\Linq\Sort.cs" />
<Compile Include="Data\Linq\SQLiteQueryProvider.cs" /> <Compile Include="Data\Linq\SQLiteQueryProvider.cs" />
<Compile Include="Data\Linq\SqlQueryContext.cs" /> <Compile Include="Data\Linq\SqlQueryContext.cs" />
<Compile Include="Data\PerformanceCounterDescriptor.cs" /> <Compile Include="Data\PerformanceCounterDescriptor.cs" />

30
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); 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] [Test]
public void TestSupportedOrderByOnRootChildren() public void TestSupportedOrderByOnRootChildren()
{ {

Loading…
Cancel
Save