// Copyright 2008 Google Inc.
//
// Licensed 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 com.google.android.stardroid.util;
import com.google.android.stardroid.base.Preconditions;
import com.google.android.stardroid.base.Provider;
import com.google.android.stardroid.base.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
/**
* A path through a {@link StopWatchTree} used to easily perform nested timings.
* Each time {@link #push} is called this {@link StopWatchTreePath} descends to
* the appropriate child of underlying {@link StopWatchTree}, creating a new
* node if necessary. Conversely, {@link #pop} stops the timer at the current
* level of the {@link StopWatchTree} and updates this {@link StopWatchTreePath}
* to point at the node's parent.
* <p>
* NOTE: clients should not call methods on the {@link StopWatchTree} directly.
* <p>
* The general usage pattern is:
*
* <code><pre>
* StopWatchTreePath path = new StopWatchTreePath(new StopWatchTree()).start();
*
* path.push("foo");
* <i>do some foo work<i>
* path.pop();
*
* path.push("bar").push("soap");
* <i> do some soap bar work</i>
* path.popAndRemove().popAndRemove();
*
* path.push("foo");
* <i>do more foo work<i>
* path.popAndRemove();
*
* TimingTree result = path.getTimings();
*
* <i>even more work<i>
*
* path.stop();
* path.getTimings();
* </pre></code>
*
* @author Brent Bryan
*/
public class StopWatchTreePath {
private final StopWatchTree tree;
private final Stack<StopWatchTreeNode> stack = new Stack<StopWatchTreeNode>();
private final HashMap<StopWatchTreeNode, List<StopWatchTreeNode>> removedNodes =
new HashMap<StopWatchTreeNode, List<StopWatchTreeNode>>();
/**
* Constructs a new {@link StopWatchTreePath} using a default implementation
* of a {@link StopWatchTree}. Sets the root node as the
* {@link StopWatchTreePath}'s current node.
*/
public StopWatchTreePath() {
this(StopWatchTree.getProvider());
}
/**
* Constructs a new {@link StopWatchTreePath} using the given
* {@link StopWatchTree} as the underlying hierarchical timer representation.
* Sets the root node as the {@link StopWatchTreePath}'s current node.
*/
public StopWatchTreePath(Provider<StopWatchTree> treeProvider) {
this.tree = treeProvider.get().start();
stack.push(tree.getRoot());
}
/**
* Resets and restarts the underlying {@link StopWatchTree}. Sets the current
* node to be the root of the tree. All children and timings will be lost.
*/
public StopWatchTreePath reset() {
removedNodes.clear();
tree.reset().start();
stack.removeAllElements();
stack.push(tree.getRoot());
return this;
}
/**
* Descends the underlying {@link StopWatchTree} to a child of the current
* node with the given name, creating a new child if necessary. Starts the
* timer on the child node, and sets the child as the current node of this
* {@link StopWatchTreePath}.
*
* @throws RuntimeException if the underlying {@link StopWatchTree} is not
* running
* @return A reference to this object for chaining
*/
public StopWatchTreePath push(String name) {
Preconditions.check(tree.isRunning());
stack.push(stack.peek().getChild(name)).getStopWatch().start();
return this;
}
/**
* Stops the timer in the current node and sets the parent of the current node
* as the current node in this {@link StopWatchTreePath}. This operation does
* not cause the value of the current node to be recorded or erased. As such,
* users can {@link #push} down into the current node again at a later time
* and continue timing from where they left off.
*
* @throws RuntimeException if the underlying {@link StopWatchTree} is not
* running, or if the current node is the root node
* @return A reference to this object for chaining
*/
public StopWatchTreePath pop() {
Preconditions.check(tree.isRunning() && stack.size() > 1);
stack.pop().getStopWatch().stop();
return this;
}
/**
* Returns the current {@link StopWatchTreeNode}.
*/
public StopWatchTreeNode getCurrentNode() {
return stack.peek();
}
/**
* Records the value of the timer in the current node, removes the current
* node from the underlying {@link StopWatchTree}, and sets the parent of the
* current node as the current node in this {@link StopWatchTreePath}.
*
*
* @throws RuntimeException if the underlying {@link StopWatchTree} is not
* running, or if the current node is the root node
* @return A reference to this object for chaining
*/
public StopWatchTreePath popAndRemove() {
Preconditions.check(tree.isRunning() && stack.size() > 1);
StopWatchTreeNode node = stack.pop().stop();
saveRemovedNode(stack.peek(), node);
stack.peek().removeChild(node.getName());
return this;
}
/**
* Returns the {@link TimingTree} describing the current state of the
* underlying {@link StopWatchTree}. See
* {@link StopWatchTree#getCurrentTiming()}
*/
// We have to reimplement this method so that we can include all the removed
// nodes, as well as those currently in the tree.
public TimingTree getCurrentTiming() {
return new TimingTree(createTimingForNode(0, tree.getRoot()));
}
private TimingTreeNode createTimingForNode(int level, StopWatchTreeNode node) {
long elapsedTime = node.getStopWatch().getElapsedTime();
List<TimingTreeNode> children = new ArrayList<TimingTreeNode>();
for (StopWatchTreeNode child : node.getChildren()) {
children.add(createTimingForNode(level + 1, child));
}
if (removedNodes.containsKey(node)) {
for (StopWatchTreeNode child : removedNodes.get(node)) {
children.add(createTimingForNode(level + 1, child));
}
}
return new TimingTreeNode(level, node.getName(), elapsedTime, children);
}
@VisibleForTesting
void saveRemovedNode(StopWatchTreeNode parent, StopWatchTreeNode node) {
List<StopWatchTreeNode> nodes = removedNodes.get(parent);
if (nodes == null) {
nodes = new ArrayList<StopWatchTreeNode>();
removedNodes.put(parent, nodes);
}
nodes.add(node);
}
}