blob: 4f920699f4d69b922dca88376ffc0fcd47984fc9 [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.tinkerpop.gremlin.process.traversal.util;
import org.apache.commons.lang3.StringUtils;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.ProfileStep;
import org.javatuples.Pair;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Default implementation for {@link TraversalMetrics} that aggregates {@link ImmutableMetrics} instances from a
* {@link Traversal}.
*
* @author Bob Briody (http://bobbriody.com)
* @author Marko A. Rodriguez (http://markorodriguez.com)
* @author Stephen Mallette (http://stephen.genoprime.com)
*/
public final class DefaultTraversalMetrics implements TraversalMetrics, Serializable {
/**
* toString() specific headers
*/
private static final String[] HEADERS = {"Step", "Count", "Traversers", "Time (ms)", "% Dur"};
/**
* {@link ImmutableMetrics} indexed by their step identifier.
*/
private final Map<String, ImmutableMetrics> stepIndexedMetrics = new HashMap<>();
/**
* A computed value representing the total time spent on all steps.
*/
private long totalStepDuration;
/**
* {@link ImmutableMetrics} indexed by their step position.
*/
private Map<Integer, ImmutableMetrics> positionIndexedMetrics = new HashMap<>();
/**
* Determines if final metrics have been computed
*/
private volatile boolean finalized = false;
public DefaultTraversalMetrics() {
}
/**
* This is only a convenient constructor needed for GraphSON deserialization.
*/
public DefaultTraversalMetrics(final long totalStepDurationNs, final List<MutableMetrics> orderedMetrics) {
totalStepDuration = totalStepDurationNs;
int ix = 0;
for (final MutableMetrics metric : orderedMetrics) {
stepIndexedMetrics.put(metric.getId(), metric.getImmutableClone());
positionIndexedMetrics.put(ix++, metric.getImmutableClone());
}
}
@Override
public long getDuration(final TimeUnit unit) {
return unit.convert(this.totalStepDuration, MutableMetrics.SOURCE_UNIT);
}
@Override
public Metrics getMetrics(final int index) {
return this.positionIndexedMetrics.get(index);
}
@Override
public Metrics getMetrics(final String id) {
return this.stepIndexedMetrics.get(id);
}
@Override
public Collection<ImmutableMetrics> getMetrics() {
return positionIndexedMetrics.entrySet().stream().sorted(Map.Entry.comparingByKey()).
collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(oldValue, newValue) -> oldValue, LinkedHashMap::new)).values();
}
/**
* The metrics have been computed and can no longer be modified.
*/
public boolean isFinalized() {
return finalized;
}
@Override
public String toString() {
// Build a pretty table of metrics data.
// Append headers
final StringBuilder sb = new StringBuilder("Traversal Metrics\n")
.append(String.format("%-50s %21s %11s %15s %8s", HEADERS));
sb.append("\n=============================================================================================================");
appendMetrics(this.positionIndexedMetrics.values(), sb, 0);
// Append total duration
sb.append(String.format("%n%50s %21s %11s %15.3f %8s",
">TOTAL", "-", "-", getDuration(TimeUnit.MICROSECONDS) / 1000.0, "-"));
return sb.toString();
}
/**
* Extracts metrics from the provided {@code traversal} and computes metrics. Calling this method finalizes the
* metrics such that their values can no longer be modified.
*/
public synchronized void setMetrics(final Traversal.Admin traversal, final boolean onGraphComputer) {
if (finalized) throw new IllegalStateException("Metrics have been finalized and cannot be modified");
finalized = true;
handleNestedTraversals(traversal, null, onGraphComputer);
addTopLevelMetrics(traversal, onGraphComputer);
}
private void addTopLevelMetrics(final Traversal.Admin traversal, final boolean onGraphComputer) {
this.totalStepDuration = 0;
final List<ProfileStep> profileSteps = TraversalHelper.getStepsOfClass(ProfileStep.class, traversal);
final List<Pair<Integer, MutableMetrics>> tempMetrics = new ArrayList<>(profileSteps.size());
for (int ii = 0; ii < profileSteps.size(); ii++) {
// The index is necessary to ensure that step order is preserved after a merge.
final ProfileStep step = profileSteps.get(ii);
final MutableMetrics stepMetrics = onGraphComputer ? traversal.getSideEffects().get(step.getId()) : step.getMetrics();
this.totalStepDuration += stepMetrics.getDuration(MutableMetrics.SOURCE_UNIT);
tempMetrics.add(Pair.with(ii, stepMetrics.clone()));
}
tempMetrics.forEach(m -> {
final double dur = m.getValue1().getDuration(TimeUnit.NANOSECONDS) * 100.d / this.totalStepDuration;
m.getValue1().setAnnotation(PERCENT_DURATION_KEY, dur);
});
tempMetrics.forEach(p -> {
this.stepIndexedMetrics.put(p.getValue1().getId(), p.getValue1().getImmutableClone());
this.positionIndexedMetrics.put(p.getValue0(), p.getValue1().getImmutableClone());
});
}
private void handleNestedTraversals(final Traversal.Admin traversal, final MutableMetrics parentMetrics, final boolean onGraphComputer) {
long prevDur = 0;
for (int i = 0; i < traversal.getSteps().size(); i++) {
final Step step = (Step) traversal.getSteps().get(i);
if (!(step instanceof ProfileStep))
continue;
final MutableMetrics metrics = onGraphComputer ?
traversal.getSideEffects().get(step.getId()) :
((ProfileStep) step).getMetrics();
if (null != metrics) { // this happens when a particular branch never received a .next() call (the metrics were never initialized)
if (!onGraphComputer) {
// subtract upstream duration.
final long durBeforeAdjustment = metrics.getDuration(TimeUnit.NANOSECONDS);
// adjust duration
metrics.setDuration(metrics.getDuration(TimeUnit.NANOSECONDS) - prevDur, TimeUnit.NANOSECONDS);
prevDur = durBeforeAdjustment;
}
if (parentMetrics != null) {
parentMetrics.addNested(metrics);
}
if (step.getPreviousStep() instanceof TraversalParent) {
for (Traversal.Admin<?, ?> t : ((TraversalParent) step.getPreviousStep()).getLocalChildren()) {
handleNestedTraversals(t, metrics, onGraphComputer);
}
for (Traversal.Admin<?, ?> t : ((TraversalParent) step.getPreviousStep()).getGlobalChildren()) {
handleNestedTraversals(t, metrics, onGraphComputer);
}
}
}
}
}
private void appendMetrics(final Collection<? extends Metrics> metrics, final StringBuilder sb, final int indent) {
// Append each StepMetric's row. indexToLabelMap values are ordered by index.
for (Metrics m : metrics) {
final StringBuilder metricName = new StringBuilder();
// Handle indentation
for (int ii = 0; ii < indent; ii++) {
metricName.append(" ");
}
metricName.append(m.getName());
// Abbreviate if necessary
final StringBuilder rowName = new StringBuilder(StringUtils.abbreviate(metricName.toString(), 50));
// Grab the values
final Long itemCount = m.getCount(ELEMENT_COUNT_ID);
final Long traverserCount = m.getCount(TRAVERSER_COUNT_ID);
final Double percentDur = (Double) m.getAnnotation(PERCENT_DURATION_KEY);
// Build the row string
sb.append(String.format("%n%-50s", rowName.toString()));
if (itemCount != null) {
sb.append(String.format(" %21d", itemCount));
} else {
sb.append(String.format(" %21s", ""));
}
if (traverserCount != null) {
sb.append(String.format(" %11d", traverserCount));
} else {
sb.append(String.format(" %11s", ""));
}
sb.append(String.format(" %15.3f", m.getDuration(TimeUnit.MICROSECONDS) / 1000.0));
if (percentDur != null) {
sb.append(String.format(" %8.2f", percentDur));
}
// process any annotations
final Map<String,Object> annotations = m.getAnnotations();
if (!annotations.isEmpty()) {
// ignore the PERCENT_DURATION_KEY as that is a TinkerPop annotation that is displayed by default
annotations.entrySet().stream().filter(kv -> !kv.getKey().equals(PERCENT_DURATION_KEY)).forEach(kv -> {
final String prefix = " \\_";
final String separator = "=";
final String k = prefix + StringUtils.abbreviate(kv.getKey(), 30);
final int valueIndentLen = separator.length() + k.length() + indent;
final int leftover = 110 - valueIndentLen;
final String[] splitValues = splitOnSize(kv.getValue().toString(), leftover);
for (int ix = 0; ix < splitValues.length; ix++) {
// the first lines gets the annotation prefix. the rest are indented to the separator
if (ix == 0) {
sb.append(String.format("%n%s", k + separator + splitValues[ix]));
} else {
sb.append(String.format("%n%s", padLeft(splitValues[ix], valueIndentLen - 1)));
}
}
});
}
appendMetrics(m.getNested(), sb, indent + 1);
}
}
private static String[] splitOnSize(final String text, final int size) {
final String[] ret = new String[(text.length() + size - 1) / size];
int counter = 0;
for (int start = 0; start < text.length(); start += size) {
ret[counter] = text.substring(start, Math.min(text.length(), start + size));
counter++;
}
return ret;
}
private static String padLeft(final String text, final int amountToPad) {
// not sure why this method needed to exist. stupid string format stuff and commons utilities wouldn't
// work for some reason in the context this method was used above.
final StringBuilder newText = new StringBuilder();
for (int ix = 0; ix < amountToPad; ix++) {
newText.append(" ");
}
newText.append(text);
return newText.toString();
}
}