|
|
|
@ -21,9 +21,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
@@ -21,9 +21,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
|
|
|
|
|
{ |
|
|
|
|
/// <summary>
|
|
|
|
|
/// "LINQ-To-Profiler" QueryProvider implementation for SQLiteCallTreeNode.
|
|
|
|
|
///
|
|
|
|
|
///
|
|
|
|
|
/// Input to a LINQ QueryProvider is a System.Linq.Expressions tree describing the query that should be executed.
|
|
|
|
|
///
|
|
|
|
|
///
|
|
|
|
|
/// Query execution is done as:
|
|
|
|
|
/// 1. Partial evaluation
|
|
|
|
|
/// 2. Translation of expression tree to QueryAst
|
|
|
|
@ -31,85 +31,114 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
@@ -31,85 +31,114 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
|
|
|
|
|
/// 4. Execution of Queries (by converting them to SQL and running it on the DB)
|
|
|
|
|
/// 5. Execution of remaining query using LINQ-to-Objects
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// The base class of all QueryAst nodes is QueryNode. A QueryNode represents a query of type IQueryable{CallTreeNode}.
|
|
|
|
|
/// For QueryNodes that have input, that input must be another QueryNode.
|
|
|
|
|
///
|
|
|
|
|
/// QueryAst nodes:
|
|
|
|
|
/// AllCalls: represents the whole FunctionData table
|
|
|
|
|
/// input.Filter(x => condition1(x) && y => condition2(y)): WHERE clause with multiple conditions
|
|
|
|
|
/// input.MergeByName(): GROUP BY nameid
|
|
|
|
|
///
|
|
|
|
|
/// Valid expressions in QueryAst nodes:
|
|
|
|
|
/// Only a limited set of expressions are valid in conditions and sort descriptors.
|
|
|
|
|
/// These are checked by the SafeExpressionImporter.
|
|
|
|
|
/// - Integer constants
|
|
|
|
|
/// - Binary operators: < <= > >= == !=
|
|
|
|
|
/// - value(List{int}).Contains(validExpr)
|
|
|
|
|
/// - if c is the lambda parameter, then these expressions are valid:
|
|
|
|
|
/// c.NameMapping.ID
|
|
|
|
|
///
|
|
|
|
|
/// Additionally, field references on a lambda parameter of type SingleCall are valid inside
|
|
|
|
|
/// filters that operate directly on "AllCalls" (e.g. AllCalls.Filter()).
|
|
|
|
|
/// In other cases (other filters, sort descriptors), SingleCall usage is invalid.
|
|
|
|
|
/// SingleCall usage cannot be imported using SafeExpressionImporter; but is created directly for
|
|
|
|
|
/// some expressions on SQLiteCallTreeNode (see translation rules below).
|
|
|
|
|
///
|
|
|
|
|
/// Translation rules from CallTreeNode object model to QueryAst:
|
|
|
|
|
/// Properties serving as query roots:
|
|
|
|
|
/// sqliteCallTreeNode.Children = AllCalls.Filter((SingleCall c) -> sqliteCallTreeNode.ids.Contains(c.ParentID))
|
|
|
|
|
/// profilingDataSQLiteProvider.GetFunctions = AllCalls.Filter((SingleCall c) -> @start <= c.DataSetId && c.DataSetId <= @end).MergeByName()
|
|
|
|
|
///
|
|
|
|
|
/// Translation rules for query nodes:
|
|
|
|
|
/// input.Where(x => f(x)) -> input.Filter(x => f'(x)), if f(x) is a safe expression
|
|
|
|
|
/// Note: if the root expression of a filter condition is the '&&' operator, a filter with multiple conditions is created.
|
|
|
|
|
/// input.Where(c => c.CallCount > 10 && c.TimeSpent > 1)
|
|
|
|
|
/// -> input.Filter(c => c.CallCount > 10 && c => c.TimeSpent > 1)
|
|
|
|
|
///
|
|
|
|
|
/// input.Select(x => x) -> input
|
|
|
|
|
/// This rule is necessary to remove degenerate selects so that the parts of the query continuing after the select
|
|
|
|
|
/// can also be represented as QueryNodes.
|
|
|
|
|
///
|
|
|
|
|
/// Translation rules for expression importer:
|
|
|
|
|
/// Any valid expressions (as defined in 'valid expressions in QueryAst nodes') are copied over directly.
|
|
|
|
|
/// Moreover, these expressions are be converted into valid expressions:
|
|
|
|
|
/// c.IsUserCode -> c.NameMapping.ID > 0
|
|
|
|
|
///
|
|
|
|
|
/// Optimization of QueryAst:
|
|
|
|
|
/// The OptimizeQueryExpressionVisitor is performing these optimizations:
|
|
|
|
|
/// x.Filter(y).Filter(z) -> x.Filter(y && z)
|
|
|
|
|
/// x.MergeByName().Filter(criteria) -> x.Filter(x, criteria).MergeByName() for some safe criterias
|
|
|
|
|
/// Criterias are safe if they access no CallTreeNode properties except for NameMapping
|
|
|
|
|
///
|
|
|
|
|
/// 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.
|
|
|
|
|
/// To solve this, we define that every SQL query must have the same set of result fields, which are dynamically named to
|
|
|
|
|
/// ensure unique names even with nested queries. The current set of names is held in the SqlQueryContext.
|
|
|
|
|
/// Indeed, conceptually we build a nested query for every QueryNode.
|
|
|
|
|
///
|
|
|
|
|
/// The query is built inside-out: the innermost nested query is appended first to the StringBuilder, the outer queries will
|
|
|
|
|
/// then insert "SELECT ... FROM (" into the beginning of the StringBuilder and append ") outer query" at the end.
|
|
|
|
|
///
|
|
|
|
|
/// The return value of the QueryNode.BuildSql method contains the kind of SQL statement that is currently in the StringBuilder.
|
|
|
|
|
/// This allows us to simply append clauses in the majority of cases, only rarely the QueryNode.WrapSqlIntoNestedStatement
|
|
|
|
|
/// method will be used to create an outer query.
|
|
|
|
|
/// For example, a Filter will simply append a WHERE to a "SELECT .. FROM .." query. To a "SELECT .. FROM .. GROUP BY .." query,
|
|
|
|
|
/// a Filter will append HAVING. Only in rare cases like filtering after sorting or after limiting the number of elements,
|
|
|
|
|
/// a Filter query node will create a nested query.
|
|
|
|
|
///
|
|
|
|
|
/// Because all constructed SELECT queries always select fields with the same meaning in the same order, executing the query is
|
|
|
|
|
/// a matter of simply filling the SQLiteCallTreeNodes with the query results.
|
|
|
|
|
///
|
|
|
|
|
/// </remarks>
|
|
|
|
|
sealed class SQLiteQueryProvider : QueryProvider |
|
|
|
|
{ |
|
|
|
|
/* |
|
|
|
|
The base class of all QueryAst nodes is QueryNode. A QueryNode represents a query of type IQueryable<CallTreeNode>. |
|
|
|
|
For QueryNodes that have input, that input must be another QueryNode. |
|
|
|
|
|
|
|
|
|
QueryAst nodes: |
|
|
|
|
AllCalls: represents the whole FunctionData table |
|
|
|
|
input.Filter(x => condition1(x) && y => condition2(y)): WHERE clause with multiple conditions |
|
|
|
|
input.MergeByName(): GROUP BY nameid |
|
|
|
|
|
|
|
|
|
Valid expressions in QueryAst nodes: |
|
|
|
|
Only a limited set of expressions are valid in conditions and sort descriptors. |
|
|
|
|
These are checked by the SafeExpressionImporter. |
|
|
|
|
- Integer constants |
|
|
|
|
- Binary operators: < <= > >= == != && || |
|
|
|
|
- value(List<int>).Contains(validExpr) |
|
|
|
|
- if c is the lambda parameter, then these expressions are valid: |
|
|
|
|
c.NameMapping.ID |
|
|
|
|
|
|
|
|
|
Additionally, field references on a lambda parameter of type SingleCall are valid inside |
|
|
|
|
filters that operate directly on "AllCalls" (e.g. AllCalls.Filter()). |
|
|
|
|
In other cases (other filters, sort descriptors), SingleCall usage is invalid. |
|
|
|
|
SingleCall usage cannot be imported using SafeExpressionImporter; but is created directly for |
|
|
|
|
some expressions on SQLiteCallTreeNode (see translation rules below). |
|
|
|
|
|
|
|
|
|
Translation rules from CallTreeNode object model to QueryAst: |
|
|
|
|
Properties serving as query roots: |
|
|
|
|
sqliteCallTreeNode.Children |
|
|
|
|
-> AllCalls.Filter((SingleCall c) => sqliteCallTreeNode.ids.Contains(c.ParentID)) |
|
|
|
|
profilingDataSQLiteProvider.GetFunctions |
|
|
|
|
-> AllCalls.Filter((SingleCall c) => @start <= c.DataSetId && c.DataSetId <= @end).MergeByName() |
|
|
|
|
profilingDataSQLiteProvider.GetRoot |
|
|
|
|
-> AllCalls.Filter((SingleCall c) => @start <= c.DataSetId && c.DataSetId <= @end |
|
|
|
|
&& c => c.ParentID == -1).Merge() |
|
|
|
|
|
|
|
|
|
Translation rules for query nodes: |
|
|
|
|
input.Where(x => f(x)) -> input.Filter(x => f'(x)), if f(x) is a safe expression |
|
|
|
|
Note: If the root expression of a filter condition is the '&&' operator, a filter with multiple conditions is created. |
|
|
|
|
This allows the optimizer to move around the filter conditions independently. |
|
|
|
|
input.Where(c => c.CallCount > 10 && c.TimeSpent > 1) |
|
|
|
|
-> input.Filter(c => c.CallCount > 10 && c => c.TimeSpent > 1) |
|
|
|
|
|
|
|
|
|
input.Select(x => x) -> input |
|
|
|
|
This rule is necessary to remove degenerate selects so that the parts of the query continuing after the select |
|
|
|
|
can also be represented as QueryNodes. |
|
|
|
|
|
|
|
|
|
Translation rules for expression importer: |
|
|
|
|
Any valid expressions (as defined in 'valid expressions in QueryAst nodes') are copied over directly. |
|
|
|
|
Moreover, these expressions are be converted into valid expressions: |
|
|
|
|
c.IsUserCode -> c.NameMapping.ID > 0 |
|
|
|
|
|
|
|
|
|
Optimization of QueryAst: |
|
|
|
|
The OptimizeQueryExpressionVisitor is performing these optimizations: |
|
|
|
|
x.Filter(y).Filter(z) -> x.Filter(y && z) |
|
|
|
|
x.MergeByName().Filter(criteria) -> x.Filter(x, criteria).MergeByName() for some safe criterias |
|
|
|
|
Criterias are safe if they access no CallTreeNode properties except for NameMapping |
|
|
|
|
|
|
|
|
|
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. |
|
|
|
|
To solve this, we define that every SQL query must have the same set of result fields, which are dynamically named to |
|
|
|
|
ensure unique names even with nested queries. The current set of names is held in the SqlQueryContext. |
|
|
|
|
Indeed, conceptually we build a nested query for every QueryNode. |
|
|
|
|
|
|
|
|
|
The query is built inside-out: the innermost nested query is appended first to the StringBuilder, the outer queries will |
|
|
|
|
then insert "SELECT ... FROM (" into the beginning of the StringBuilder and append ") outer query" at the end. |
|
|
|
|
|
|
|
|
|
The return value of the QueryNode.BuildSql method contains the kind of SQL statement that is currently in the StringBuilder. |
|
|
|
|
This allows us to simply append clauses in the majority of cases, only rarely the QueryNode.WrapSqlIntoNestedStatement |
|
|
|
|
method will be used to create an outer query. |
|
|
|
|
For example, a Filter will simply append a WHERE to a "SELECT .. FROM .." query. To a "SELECT .. FROM .. GROUP BY .." query, |
|
|
|
|
a Filter will append HAVING. Only in rare cases like filtering after sorting or after limiting the number of elements, |
|
|
|
|
a Filter query node will create a nested query. |
|
|
|
|
|
|
|
|
|
Because all constructed SELECT queries always select fields with the same meaning in the same order, executing the query is |
|
|
|
|
a matter of simply filling the SQLiteCallTreeNodes with the query results. |
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
readonly ProfilingDataSQLiteProvider sqliteProvider; |
|
|
|
|
internal readonly int startDataSetID; |
|
|
|
|
|
|
|
|
|
public SQLiteQueryProvider(ProfilingDataSQLiteProvider sqliteProvider) |
|
|
|
|
public SQLiteQueryProvider(ProfilingDataSQLiteProvider sqliteProvider, int startDataSetID) |
|
|
|
|
{ |
|
|
|
|
if (sqliteProvider == null) |
|
|
|
|
throw new ArgumentNullException("sqliteProvider"); |
|
|
|
|
this.sqliteProvider = sqliteProvider; |
|
|
|
|
this.startDataSetID = startDataSetID; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Implement GetMapping and ProcessorFrequency so that SQLiteQueryProvider can be used in place of
|
|
|
|
|
// ProfilingDataSQLiteProvider.
|
|
|
|
|
public NameMapping GetMapping(int nameID) |
|
|
|
|
{ |
|
|
|
|
return sqliteProvider.GetMapping(nameID); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public int ProcessorFrequency { |
|
|
|
|
get { return sqliteProvider.ProcessorFrequency; } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
internal IList<CallTreeNode> RunSQL(string command) |
|
|
|
|
{ |
|
|
|
|
return sqliteProvider.RunSQL(this, command); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public IQueryable<CallTreeNode> CreateQuery(QueryNode query) |
|
|
|
|
{ |
|
|
|
|
return new Query<CallTreeNode>(this, query); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public override string GetQueryText(Expression expression) |
|
|
|
@ -133,10 +162,13 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
@@ -133,10 +162,13 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
|
|
|
|
|
// If the whole query was converted, execute it:
|
|
|
|
|
QueryNode query = expression as QueryNode; |
|
|
|
|
if (query != null) |
|
|
|
|
return query.Execute(sqliteProvider); |
|
|
|
|
return query.Execute(this); |
|
|
|
|
|
|
|
|
|
// Query not converted completely: we have to use a LINQ-To-Objects / LINQ-To-Profiler mix
|
|
|
|
|
expression = new ExecuteAllQueriesVisitor(sqliteProvider).Visit(expression); |
|
|
|
|
expression = new ExecuteAllQueriesVisitor(this).Visit(expression); |
|
|
|
|
if (expression.Type.IsValueType) { |
|
|
|
|
expression = Expression.Convert(expression, typeof(object)); |
|
|
|
|
} |
|
|
|
|
var lambdaExpression = Expression.Lambda<Func<object>>(expression); |
|
|
|
|
return lambdaExpression.Compile()(); |
|
|
|
|
} |
|
|
|
@ -246,6 +278,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
@@ -246,6 +278,8 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
|
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
case ExpressionType.AndAlso: |
|
|
|
|
case ExpressionType.OrElse: |
|
|
|
|
case ExpressionType.LessThan: |
|
|
|
|
case ExpressionType.LessThanOrEqual: |
|
|
|
|
case ExpressionType.GreaterThan: |
|
|
|
@ -281,9 +315,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
@@ -281,9 +315,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
|
|
|
|
|
#region ExecuteAllQueriesVisitor
|
|
|
|
|
sealed class ExecuteAllQueriesVisitor : System.Linq.Expressions.ExpressionVisitor |
|
|
|
|
{ |
|
|
|
|
readonly ProfilingDataSQLiteProvider sqliteProvider; |
|
|
|
|
readonly SQLiteQueryProvider sqliteProvider; |
|
|
|
|
|
|
|
|
|
public ExecuteAllQueriesVisitor(ProfilingDataSQLiteProvider sqliteProvider) |
|
|
|
|
public ExecuteAllQueriesVisitor(SQLiteQueryProvider sqliteProvider) |
|
|
|
|
{ |
|
|
|
|
this.sqliteProvider = sqliteProvider; |
|
|
|
|
} |
|
|
|
|