blob: 8a0da1f5bb75a2b07c12eb79ba9ae665028b460d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.sandbox.search;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import org.apache.lucene.search.Query;
/**
* This class tracks the dependency tree for queries (scoring and rewriting) and generates {@link
* QueryProfilerBreakdown} for each node in the tree.
*/
class QueryProfilerTree {
private final ArrayList<QueryProfilerBreakdown> breakdowns;
/** Maps the Query to it's list of children. This is basically the dependency tree */
private final ArrayList<ArrayList<Integer>> tree;
/** A list of the original queries, keyed by index position */
private final ArrayList<Query> queries;
/** A list of top-level "roots". Each root can have its own tree of profiles */
private final ArrayList<Integer> roots;
/** A temporary stack used to record where we are in the dependency tree. */
private final Deque<Integer> stack;
private int currentToken = 0;
/** Rewrite time */
private long rewriteTime;
private long rewriteScratch;
public QueryProfilerTree() {
breakdowns = new ArrayList<>(10);
stack = new ArrayDeque<>(10);
tree = new ArrayList<>(10);
queries = new ArrayList<>(10);
roots = new ArrayList<>(10);
}
/**
* Returns a {@link QueryProfilerBreakdown} for a scoring query. Scoring queries (e.g. those that
* are past the rewrite phase and are now being wrapped by createWeight() ) follow a recursive
* progression. We can track the dependency tree by a simple stack
*
* <p>The only hiccup is that the first scoring query will be identical to the last rewritten
* query, so we need to take special care to fix that
*
* @param query The scoring query we wish to profile
* @return A ProfileBreakdown for this query
*/
public QueryProfilerBreakdown getProfileBreakdown(Query query) {
int token = currentToken;
boolean stackEmpty = stack.isEmpty();
// If the stack is empty, we are a new root query
if (stackEmpty) {
// We couldn't find a rewritten query to attach to, so just add it as a
// top-level root. This is just a precaution: it really shouldn't happen.
// We would only get here if a top-level query that never rewrites for some reason.
roots.add(token);
// Increment the token since we are adding a new node, but notably, do not
// updateParent() because this was added as a root
currentToken += 1;
stack.add(token);
return addDependencyNode(query, token);
}
updateParent(token);
// Increment the token since we are adding a new node
currentToken += 1;
stack.add(token);
return addDependencyNode(query, token);
}
/**
* Helper method to add a new node to the dependency tree.
*
* <p>Initializes a new list in the dependency tree, saves the query and generates a new {@link
* QueryProfilerBreakdown} to track the timings of this query.
*
* @param query The query to profile
* @param token The assigned token for this query
* @return A {@link QueryProfilerBreakdown} to profile this query
*/
private QueryProfilerBreakdown addDependencyNode(Query query, int token) {
// Add a new slot in the dependency tree
tree.add(new ArrayList<>(5));
// Save our query for lookup later
queries.add(query);
QueryProfilerBreakdown breakdown = createProfileBreakdown();
breakdowns.add(token, breakdown);
return breakdown;
}
private QueryProfilerBreakdown createProfileBreakdown() {
return new QueryProfilerBreakdown();
}
/** Removes the last (e.g. most recent) value on the stack */
public void pollLast() {
stack.pollLast();
}
/**
* After the query has been run and profiled, we need to merge the flat timing map with the
* dependency graph to build a data structure that mirrors the original query tree
*
* @return a hierarchical representation of the profiled query tree
*/
public List<QueryProfilerResult> getTree() {
ArrayList<QueryProfilerResult> results = new ArrayList<>(roots.size());
for (Integer root : roots) {
results.add(doGetTree(root));
}
return results;
}
/**
* Recursive helper to finalize a node in the dependency tree
*
* @param token The node we are currently finalizing
* @return A hierarchical representation of the tree inclusive of children at this level
*/
private QueryProfilerResult doGetTree(int token) {
Query query = queries.get(token);
QueryProfilerBreakdown breakdown = breakdowns.get(token);
List<Integer> children = tree.get(token);
List<QueryProfilerResult> childrenProfileResults = Collections.emptyList();
if (children != null) {
childrenProfileResults = new ArrayList<>(children.size());
for (Integer child : children) {
QueryProfilerResult childNode = doGetTree(child);
childrenProfileResults.add(childNode);
}
}
// TODO this would be better done bottom-up instead of top-down to avoid
// calculating the same times over and over...but worth the effort?
String type = getTypeFromQuery(query);
String description = getDescriptionFromQuery(query);
return new QueryProfilerResult(
type,
description,
breakdown.toBreakdownMap(),
breakdown.toTotalTime(),
childrenProfileResults);
}
private String getTypeFromQuery(Query query) {
// Anonymous classes won't have a name,
// we need to get the super class
if (query.getClass().getSimpleName().isEmpty()) {
return query.getClass().getSuperclass().getSimpleName();
}
return query.getClass().getSimpleName();
}
private String getDescriptionFromQuery(Query query) {
return query.toString();
}
/**
* Internal helper to add a child to the current parent node
*
* @param childToken The child to add to the current parent
*/
private void updateParent(int childToken) {
Integer parent = stack.peekLast();
ArrayList<Integer> parentNode = tree.get(parent);
parentNode.add(childToken);
tree.set(parent, parentNode);
}
/** Begin timing a query for a specific Timing context */
public void startRewriteTime() {
assert rewriteScratch == 0;
rewriteScratch = System.nanoTime();
}
/**
* Halt the timing process and add the elapsed rewriting time. startRewriteTime() must be called
* for a particular context prior to calling stopAndAddRewriteTime(), otherwise the elapsed time
* will be negative and nonsensical
*
* @return The elapsed time
*/
public long stopAndAddRewriteTime() {
long time = Math.max(1, System.nanoTime() - rewriteScratch);
rewriteTime += time;
rewriteScratch = 0;
return time;
}
public long getRewriteTime() {
return rewriteTime;
}
}