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. 46
      src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs
  13. 10
      src/AddIns/Misc/Profiler/Controller/Data/ProfilingDataProvider.cs
  14. 37
      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 @@ @@ -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 @@ @@ -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 @@ -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 @@ -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");

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

@ -0,0 +1,107 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ -50,6 +50,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return ReorderFilter(filter);
}
static readonly MemberInfo[] SafeMembersForMoveIntoMergeByName = { KnownMembers.CallTreeNode_NameMapping };
/// <summary>
/// Tries to combine nested filters;
/// move 'MergeByName' nodes out of filter, if possible
@ -63,7 +65,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -63,7 +65,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
} else if (filter.Target is MergeByName) {
// x.MergeByName().Filter(<criteria>) -> x.Filter(x, <criteria>).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 @@ -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>
/// Optimizes the filter; but does not try to combine nested filter (etc.)
/// </summary>
@ -145,8 +115,59 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -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<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);
}
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 @@ -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);
}
}
}

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

@ -1,422 +0,0 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -151,9 +159,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
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>
@ -164,6 +172,15 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -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<CallTreeNode> CreateQuery(QueryNode query)
{
return new Query<CallTreeNode>(this, query);
@ -288,6 +305,10 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -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)) {

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

@ -0,0 +1,105 @@ @@ -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() + ")";
}
}
}

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

@ -14,6 +14,31 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -14,6 +14,31 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{
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)
{
this.StartDataSetID = provider.startDataSetID;
@ -25,8 +50,18 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -25,8 +50,18 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
{
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
@ -38,15 +73,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq @@ -38,15 +73,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
public readonly string HasChildren;
public readonly string ActiveCallCount;
/// <summary>
/// Gets whether this nameset represents non-merged calls.
/// </summary>
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";

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

@ -60,12 +60,20 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -60,12 +60,20 @@ namespace ICSharpCode.Profiler.Controller.Data
/// </summary>
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>
/// Returns the list of all functions called in a specified range of datasets.
/// </summary>
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();
}
}
}

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

@ -191,7 +191,7 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -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<Func<SingleCall, bool>> 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 @@ -263,19 +263,17 @@ namespace ICSharpCode.Profiler.Controller.Data
}
/// <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);
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<Func<SingleCall, bool>> DataSetFilter(int startIndex, int endIndex)
@ -283,7 +281,7 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -283,7 +281,7 @@ namespace ICSharpCode.Profiler.Controller.Data
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>();
@ -293,18 +291,21 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -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);
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.ids = new List<int> { (int)(long)ids };
node.IdList = new int[] { (int)(long)ids };
} else {
node.ids = reader.GetString(0).Split(',').Select(s => int.Parse(s)).ToList();
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.activeCallCount = reader.GetInt32(5);
}
node.hasChildren = reader.GetBoolean(3);
node.activeCallCount = reader.GetInt32(4);
result.Add(node);
}
}

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

@ -26,7 +26,6 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -26,7 +26,6 @@ namespace ICSharpCode.Profiler.Controller.Data
internal long cpuCyclesSpent;
CallTreeNode parent;
SQLiteQueryProvider provider;
internal List<int> ids = new List<int>();
internal bool hasChildren;
internal int activeCallCount;
@ -40,6 +39,26 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -40,6 +39,26 @@ namespace ICSharpCode.Profiler.Controller.Data
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>
/// Gets a reference to the name, return type and parameter list of the method.
/// </summary>
@ -94,7 +113,8 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -94,7 +113,8 @@ namespace ICSharpCode.Profiler.Controller.Data
if (!hasChildren)
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)));
}
}
@ -135,10 +155,11 @@ namespace ICSharpCode.Profiler.Controller.Data @@ -135,10 +155,11 @@ namespace ICSharpCode.Profiler.Controller.Data
public override CallTreeNode Merge(IEnumerable<CallTreeNode> nodes)
{
SQLiteCallTreeNode mergedNode = new SQLiteCallTreeNode(0, null, this.provider);
List<int> mergedIds = new List<int>();
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 @@ -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 @@ -162,7 +185,7 @@ namespace ICSharpCode.Profiler.Controller.Data
List<int> 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<Func<SingleCall, bool>> 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 @@ -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 @@ -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;
}
}

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

@ -95,10 +95,16 @@ @@ -95,10 +95,16 @@
</ItemGroup>
<ItemGroup>
<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\Filter.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\QueryAst.cs" />
<Compile Include="Data\Linq\QueryNode.cs" />
<Compile Include="Data\Linq\Sort.cs" />
<Compile Include="Data\Linq\SQLiteQueryProvider.cs" />
<Compile Include="Data\Linq\SqlQueryContext.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 @@ -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()
{

Loading…
Cancel
Save