package com.linkedin.thirdeye.client.comparison;
import static com.linkedin.thirdeye.client.ResponseParserUtils.OTHER;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Range;
import com.linkedin.thirdeye.api.TimeGranularity;
import com.linkedin.thirdeye.client.MetricFunction;
import com.linkedin.thirdeye.client.ResponseParserUtils;
import com.linkedin.thirdeye.client.ThirdEyeResponse;
import com.linkedin.thirdeye.client.ThirdEyeResponseRow;
import com.linkedin.thirdeye.client.comparison.Row.Builder;
import com.linkedin.thirdeye.client.comparison.Row.Metric;
import com.linkedin.thirdeye.util.ThirdEyeUtils;
public class TimeOnTimeResponseParser {
private final ThirdEyeResponse baselineResponse;
private final ThirdEyeResponse currentResponse;
private final List<Range<DateTime>> baselineRanges;
private final List<Range<DateTime>> currentRanges;
private final TimeGranularity aggTimeGranularity;
private final List<String> groupByDimensions;
public static final Logger LOGGER = LoggerFactory.getLogger(TimeOnTimeResponseParser.class);
private Map<String, ThirdEyeResponseRow> baselineResponseMap;
private Map<String, ThirdEyeResponseRow> currentResponseMap;
private List<MetricFunction> metricFunctions;
private int numMetrics;
int numTimeBuckets;
private List<Row> rows;
Map<String, Double> metricThresholds = new HashMap<>();
public TimeOnTimeResponseParser(ThirdEyeResponse baselineResponse, ThirdEyeResponse currentResponse, List<Range<DateTime>> baselineRanges,
List<Range<DateTime>> currentRanges, TimeGranularity timeGranularity, List<String> groupByDimensions) {
this.baselineResponse = baselineResponse;
this.currentResponse = currentResponse;
this.baselineRanges = baselineRanges;
this.currentRanges = currentRanges;
this.aggTimeGranularity = timeGranularity;
this.groupByDimensions = groupByDimensions;
metricFunctions = baselineResponse.getMetricFunctions();
metricThresholds = ThirdEyeUtils.getMetricThresholdsMap(metricFunctions);
}
public List<Row> parseResponse() {
if (baselineResponse == null || currentResponse == null) {
return Collections.emptyList();
}
boolean hasGroupByDimensions = false;
if (groupByDimensions != null && groupByDimensions.size() > 0) {
hasGroupByDimensions = true;
}
boolean hasGroupByTime = false;
if (aggTimeGranularity != null) {
hasGroupByTime = true;
}
metricFunctions = baselineResponse.getMetricFunctions();
numMetrics = metricFunctions.size();
numTimeBuckets = Math.min(currentRanges.size(), baselineRanges.size());
if (currentRanges.size() != baselineRanges.size()) {
LOGGER.info("Current and baseline time series have different length, which could be induced by DST.");
}
rows = new ArrayList<>();
if (hasGroupByTime) {
if (hasGroupByDimensions) { // contributor view
parseGroupByTimeDimensionResponse();
} else { // tabular view
parseGroupByTimeResponse();
}
} else {
if (hasGroupByDimensions) { // heatmap
parseGroupByDimensionResponse();
} else {
throw new UnsupportedOperationException("Response cannot have neither group by time nor group by dimension");
}
}
return rows;
}
private void parseGroupByDimensionResponse() {
baselineResponseMap = ResponseParserUtils.createResponseMapByDimension(baselineResponse);
currentResponseMap = ResponseParserUtils.createResponseMapByDimension(currentResponse);
List<Double> baselineMetricSums = ResponseParserUtils.getMetricSums(baselineResponse);
List<Double> currentMetricSums = ResponseParserUtils.getMetricSums(currentResponse);
// group by dimension name
String dimensionName = baselineResponse.getGroupKeyColumns().get(0);
// group by dimension values
Set<String> dimensionValues = new HashSet<>();
dimensionValues.addAll(baselineResponseMap.keySet());
dimensionValues.addAll(currentResponseMap.keySet());
// Construct OTHER row
Row.Builder otherBuilder = new Row.Builder();
otherBuilder.setBaselineStart(baselineRanges.get(0).lowerEndpoint());
otherBuilder.setBaselineEnd(baselineRanges.get(0).upperEndpoint());
otherBuilder.setCurrentStart(currentRanges.get(0).lowerEndpoint());
otherBuilder.setCurrentEnd(currentRanges.get(0).upperEndpoint());
otherBuilder.setDimensionName(dimensionName);
otherBuilder.setDimensionValue(OTHER);
Double[] otherBaseline = new Double[numMetrics];
Arrays.fill(otherBaseline, 0.0);
Double[] otherCurrent = new Double[numMetrics];
Arrays.fill(otherCurrent, 0.0);
boolean includeOther = false;
// for every dimension value, we check if the row we constructed passes metric threshold
// if it does, we add it to the rows
// else, include it in the OTHER row
for (String dimensionValue : dimensionValues) {
Row.Builder builder = new Row.Builder();
builder.setBaselineStart(baselineRanges.get(0).lowerEndpoint());
builder.setBaselineEnd(baselineRanges.get(0).upperEndpoint());
builder.setCurrentStart(currentRanges.get(0).lowerEndpoint());
builder.setCurrentEnd(currentRanges.get(0).upperEndpoint());
builder.setDimensionName(dimensionName);
builder.setDimensionValue(dimensionValue);
ThirdEyeResponseRow baselineRow = baselineResponseMap.get(dimensionValue);
ThirdEyeResponseRow currentRow = currentResponseMap.get(dimensionValue);
addMetric(baselineRow, currentRow, builder);
Row row = builder.build();
boolean passedThreshold = checkMetricSums(row, baselineMetricSums, currentMetricSums);
// if any non-OTHER metric passes threshold, include it
if (passedThreshold && !dimensionValue.equalsIgnoreCase(OTHER)) {
rows.add(row);
} else { // else add it to OTHER
includeOther = true;
List<Metric> metrics = row.getMetrics();
for (int i = 0; i < numMetrics; i++) {
Metric metric = metrics.get(i);
otherBaseline[i] += metric.getBaselineValue();
otherCurrent[i] += metric.getCurrentValue();
}
}
}
if (includeOther) {
for (int i = 0; i < numMetrics; i++) {
otherBuilder.addMetric(metricFunctions.get(i).getMetricName(), otherBaseline[i], otherCurrent[i]);
}
Row row = otherBuilder.build();
if (isValidMetric(row, Arrays.asList(otherBaseline), Arrays.asList(otherCurrent))) {
rows.add(row);
}
}
}
private void parseGroupByTimeResponse() {
baselineResponseMap = ResponseParserUtils.createResponseMapByTime(baselineResponse);
currentResponseMap = ResponseParserUtils.createResponseMapByTime(currentResponse);
for (int timeBucketId = 0; timeBucketId < numTimeBuckets; timeBucketId++) {
Range<DateTime> baselineTimeRange = baselineRanges.get(timeBucketId);
ThirdEyeResponseRow baselineRow = baselineResponseMap.get(String.valueOf(timeBucketId));
Range<DateTime> currentTimeRange = currentRanges.get(timeBucketId);
ThirdEyeResponseRow currentRow = currentResponseMap.get(String.valueOf(timeBucketId));
Row.Builder builder = new Row.Builder();
builder.setBaselineStart(baselineTimeRange.lowerEndpoint());
builder.setBaselineEnd(baselineTimeRange.upperEndpoint());
builder.setCurrentStart(currentTimeRange.lowerEndpoint());
builder.setCurrentEnd(currentTimeRange.upperEndpoint());
addMetric(baselineRow, currentRow, builder);
Row row = builder.build();
rows.add(row);
}
}
private void parseGroupByTimeDimensionResponse() {
baselineResponseMap = ResponseParserUtils.createResponseMapByTimeAndDimension(baselineResponse);
currentResponseMap = ResponseParserUtils.createResponseMapByTimeAndDimension(currentResponse);
Map<Integer, List<Double>> baselineMetricSums = ResponseParserUtils.getMetricSumsByTime(baselineResponse);
Map<Integer, List<Double>> currentMetricSums = ResponseParserUtils.getMetricSumsByTime(currentResponse);
// group by time and dimension values
Set<String> timeDimensionValues = new HashSet<>();
timeDimensionValues.addAll(baselineResponseMap.keySet());
timeDimensionValues.addAll(currentResponseMap.keySet());
Set<String> dimensionValues = new HashSet<>();
for (String timeDimensionValue : timeDimensionValues) {
String dimensionValue = ResponseParserUtils.extractFirstDimensionValue(timeDimensionValue);
dimensionValues.add(dimensionValue);
}
// group by dimension name
String dimensionName = baselineResponse.getGroupKeyColumns().get(1);
// other row
List<Row.Builder> otherBuilders = new ArrayList<>();
List<Double[]> otherBaselineMetrics = new ArrayList<>();
List<Double[]> otherCurrentMetrics = new ArrayList<>();
boolean includeOther = false;
// constructing an OTHER rows, 1 for each time bucket
for (int timeBucketId = 0; timeBucketId < numTimeBuckets; timeBucketId++) {
Range<DateTime> baselineTimeRange = baselineRanges.get(timeBucketId);
Range<DateTime> currentTimeRange = currentRanges.get(timeBucketId);
Row.Builder builder = new Row.Builder();
builder.setBaselineStart(baselineTimeRange.lowerEndpoint());
builder.setBaselineEnd(baselineTimeRange.upperEndpoint());
builder.setCurrentStart(currentTimeRange.lowerEndpoint());
builder.setCurrentEnd(currentTimeRange.upperEndpoint());
builder.setDimensionName(dimensionName);
builder.setDimensionValue(OTHER);
otherBuilders.add(builder);
Double[] otherBaseline = new Double[numMetrics];
Arrays.fill(otherBaseline, 0.0);
Double[] otherCurrent = new Double[numMetrics];
Arrays.fill(otherCurrent, 0.0);
otherBaselineMetrics.add(otherBaseline);
otherCurrentMetrics.add(otherCurrent);
}
// for every comparison row we construct, we check if any of its time buckets passes metric
// threshold
// if it does, we add it to the rows as is
// else, we add the metric values to the OTHER row
for (String dimensionValue : dimensionValues) {
List<Row> thresholdRows = new ArrayList<>();
for (int timeBucketId = 0; timeBucketId < numTimeBuckets; timeBucketId++) {
Range<DateTime> baselineTimeRange = baselineRanges.get(timeBucketId);
Range<DateTime> currentTimeRange = currentRanges.get(timeBucketId);
// compute the time|dimension key
String baselineTimeDimensionValue = ResponseParserUtils.computeTimeDimensionValue(timeBucketId, dimensionValue);
String currentTimeDimensionValue = baselineTimeDimensionValue;
ThirdEyeResponseRow baselineRow = baselineResponseMap.get(baselineTimeDimensionValue);
ThirdEyeResponseRow currentRow = currentResponseMap.get(currentTimeDimensionValue);
Row.Builder builder = new Row.Builder();
builder.setBaselineStart(baselineTimeRange.lowerEndpoint());
builder.setBaselineEnd(baselineTimeRange.upperEndpoint());
builder.setCurrentStart(currentTimeRange.lowerEndpoint());
builder.setCurrentEnd(currentTimeRange.upperEndpoint());
builder.setDimensionName(dimensionName);
builder.setDimensionValue(dimensionValue);
addMetric(baselineRow, currentRow, builder);
Row row = builder.build();
thresholdRows.add(row);
}
// check if rows pass threshold
boolean passedThreshold = false;
for (int timeBucketId = 0; timeBucketId < numTimeBuckets; timeBucketId++) {
if (checkMetricSums(thresholdRows.get(timeBucketId), baselineMetricSums.get(timeBucketId), currentMetricSums.get(timeBucketId))) {
passedThreshold = true;
break;
}
}
if (passedThreshold) { // if any of the cells of a contributor row passes threshold, add all
// those cells
rows.addAll(thresholdRows);
} else { // else that row of cells goes into OTHER
includeOther = true;
for (int timeBucketId = 0; timeBucketId < numTimeBuckets; timeBucketId++) {
Row row = thresholdRows.get(timeBucketId);
List<Metric> metrics = row.getMetrics();
for (int i = 0; i < metrics.size(); i++) {
Metric metricToAdd = metrics.get(i);
otherBaselineMetrics.get(timeBucketId)[i] += metricToAdd.getBaselineValue();
otherCurrentMetrics.get(timeBucketId)[i] += metricToAdd.getCurrentValue();
}
}
}
}
// create other row using the other baseline and current sums
if (includeOther) {
for (int timeBucketId = 0; timeBucketId < numTimeBuckets; timeBucketId++) {
Builder otherBuilder = otherBuilders.get(timeBucketId);
Double[] otherBaseline = otherBaselineMetrics.get(timeBucketId);
Double[] otherCurrent = otherCurrentMetrics.get(timeBucketId);
for (int i = 0; i < numMetrics; i++) {
otherBuilder.addMetric(metricFunctions.get(i).getMetricName(), otherBaseline[i], otherCurrent[i]);
}
Row row = otherBuilder.build();
if (isValidMetric(row, Arrays.asList(otherBaseline), Arrays.asList(otherCurrent))) {
rows.add(row);
}
}
}
}
private void addMetric(ThirdEyeResponseRow baselineRow, ThirdEyeResponseRow currentRow, Builder builder) {
List<MetricFunction> metricFunctions = baselineResponse.getMetricFunctions();
for (int i = 0; i < metricFunctions.size(); i++) {
MetricFunction metricFunction = metricFunctions.get(i);
double baselineValue = 0;
if (baselineRow != null) {
baselineValue = baselineRow.getMetrics().get(i);
}
double currentValue = 0;
if (currentRow != null) {
currentValue = currentRow.getMetrics().get(i);
}
builder.addMetric(metricFunction.getMetricName(), baselineValue, currentValue);
}
}
private boolean checkMetricSums(Row row, List<Double> baselineMetricSums, List<Double> currentMetricSums) {
return isValidMetric(row, baselineMetricSums, currentMetricSums);
}
boolean isValidMetric(Row row, List<Double> baselineMetricSums, List<Double> currentMetricSums) {
List<Metric> metrics = row.getMetrics();
for (int i = 0; i < metrics.size(); i++) {
Metric metric = metrics.get(i);
double baselineSum = 0;
if (baselineMetricSums != null) {
baselineSum = baselineMetricSums.get(i);
}
double currentSum = 0;
if (currentMetricSums != null) {
currentSum = currentMetricSums.get(i);
}
Double thresholdFraction = metricThresholds.get(metric.getMetricName());
if (metric.getBaselineValue() > thresholdFraction * baselineSum || metric.getCurrentValue() > thresholdFraction * currentSum) {
return true;
}
}
return false;
}
}