package com.linkedin.thirdeye.dashboard.views.tabular; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import com.google.common.collect.Multimap; import com.linkedin.thirdeye.api.TimeSpec; import com.linkedin.thirdeye.client.MetricExpression; import com.linkedin.thirdeye.client.ThirdEyeCacheRegistry; import com.linkedin.thirdeye.client.cache.QueryCache; import com.linkedin.thirdeye.client.comparison.Row; import com.linkedin.thirdeye.client.comparison.Row.Metric; import com.linkedin.thirdeye.client.comparison.TimeOnTimeComparisonHandler; import com.linkedin.thirdeye.client.comparison.TimeOnTimeComparisonRequest; import com.linkedin.thirdeye.client.comparison.TimeOnTimeComparisonResponse; import com.linkedin.thirdeye.dashboard.views.GenericResponse; import com.linkedin.thirdeye.dashboard.views.GenericResponse.ResponseSchema; import com.linkedin.thirdeye.dashboard.views.TimeBucket; import com.linkedin.thirdeye.dashboard.views.ViewHandler; import com.linkedin.thirdeye.dashboard.views.heatmap.HeatMapCell; import com.linkedin.thirdeye.datalayer.dto.DatasetConfigDTO; import com.linkedin.thirdeye.util.ThirdEyeUtils; public class TabularViewHandler implements ViewHandler<TabularViewRequest, TabularViewResponse> { private static ThirdEyeCacheRegistry CACHE_REGISTRY = ThirdEyeCacheRegistry.getInstance(); private final Comparator<Row> rowComparator = new Comparator<Row>() { @Override public int compare(Row row1, Row row2) { long millisSinceEpoch1 = row1.getCurrentStart().getMillis(); long millisSinceEpoch2 = row2.getCurrentStart().getMillis(); return (millisSinceEpoch1 < millisSinceEpoch2) ? -1 : (millisSinceEpoch1 == millisSinceEpoch2) ? 0 : 1; } }; private final QueryCache queryCache; public TabularViewHandler(QueryCache queryCache) { this.queryCache = queryCache; } private TimeOnTimeComparisonRequest generateTimeOnTimeComparisonRequest(TabularViewRequest request) throws Exception { TimeOnTimeComparisonRequest comparisonRequest = new TimeOnTimeComparisonRequest(); String collection = request.getCollection(); DateTime baselineStart = request.getBaselineStart(); DateTime baselineEnd = request.getBaselineEnd(); DateTime currentStart = request.getCurrentStart(); DateTime currentEnd = request.getCurrentEnd(); DatasetConfigDTO datasetConfig = CACHE_REGISTRY.getDatasetConfigCache().get(collection); TimeSpec timespec = ThirdEyeUtils.getTimeSpecFromDatasetConfig(datasetConfig); if (!request.getTimeGranularity().getUnit().equals(TimeUnit.DAYS) || !StringUtils.isBlank(timespec.getFormat())) { comparisonRequest.setEndDateInclusive(true); } Multimap<String, String> filters = request.getFilters(); List<MetricExpression> metricExpressions = request.getMetricExpressions(); comparisonRequest.setCollectionName(collection); comparisonRequest.setBaselineStart(baselineStart); comparisonRequest.setBaselineEnd(baselineEnd); comparisonRequest.setCurrentStart(currentStart); comparisonRequest.setCurrentEnd(currentEnd); comparisonRequest.setFilterSet(filters); comparisonRequest.setMetricExpressions(metricExpressions); comparisonRequest.setAggregationTimeGranularity(request.getTimeGranularity()); return comparisonRequest; } private List<TimeBucket> getTimeBuckets(TimeOnTimeComparisonResponse response) { List<TimeBucket> timeBuckets = new ArrayList<>(); int numRows = response.getNumRows(); for (int i = 0; i < numRows; i++) { Row row = response.getRow(i); TimeBucket bucket = TimeBucket.fromRow(row); timeBuckets.add(bucket); } Collections.sort(timeBuckets); return timeBuckets; } private List<Row> getRowsSortedByTime(TimeOnTimeComparisonResponse response) { SortedSet<Row> rows = new TreeSet<>(rowComparator); int numRows = response.getNumRows(); for (int i = 0; i < numRows; i++) { Row row = response.getRow(i); rows.add(row); } return new ArrayList<Row>(rows); } @Override public TabularViewResponse process(TabularViewRequest request) throws Exception { // query 1 for everything from baseline start to baseline end // query 2 for everything from current start to current end // for each dimension group by top 100 // query 1 for everything from baseline start to baseline end // query for everything from current start to current end TimeOnTimeComparisonRequest comparisonRequest = generateTimeOnTimeComparisonRequest(request); TimeOnTimeComparisonHandler handler = new TimeOnTimeComparisonHandler(queryCache); TimeOnTimeComparisonResponse response = handler.handle(comparisonRequest); List<TimeBucket> timeBuckets = getTimeBuckets(response); List<Row> rows = getRowsSortedByTime(response); TabularViewResponse tabularViewResponse = new TabularViewResponse(); Map<String, String> summary = new HashMap<>(); summary.put("baselineStart", Long.toString(comparisonRequest.getBaselineStart().getMillis())); summary.put("baselineEnd", Long.toString(comparisonRequest.getBaselineEnd().getMillis())); summary.put("currentStart", Long.toString(comparisonRequest.getCurrentStart().getMillis())); summary.put("currentEnd", Long.toString(comparisonRequest.getCurrentEnd().getMillis())); tabularViewResponse.setSummary(summary); List<String> expressionNames = new ArrayList<>(); for (MetricExpression expression : request.getMetricExpressions()) { expressionNames.add(expression.getExpressionName()); } tabularViewResponse.setMetrics(expressionNames); tabularViewResponse.setTimeBuckets(timeBuckets); String[] columns = new String[] { "baselineValue", "currentValue", "ratio", "cumulativeBaselineValue", "cumulativeCurrentValue", "cumulativeRatio" }; // maintain same order in response Map<String, GenericResponse> data = new LinkedHashMap<>(); for (String metric : expressionNames) { ResponseSchema schema = new ResponseSchema(); for (int i = 0; i < columns.length; i++) { String column = columns[i]; schema.add(column, i); } GenericResponse rowData = new GenericResponse(); rowData.setSchema(schema); List<String[]> genericResponseData = new ArrayList<>(); rowData.setResponseData(genericResponseData); data.put(metric, rowData); } tabularViewResponse.setData(data); Map<String, Double[]> runningTotalMap = new HashMap<>(); for (Row row : rows) { for (Metric metric : row.getMetrics()) { String metricName = metric.getMetricName(); if (!expressionNames.contains(metricName)) { continue; } Double baselineValue = metric.getBaselineValue(); Double currentValue = metric.getCurrentValue(); String baselineValueStr = HeatMapCell.format(baselineValue); String currentValueStr = HeatMapCell.format(currentValue); double ratio = ((currentValue - baselineValue) * 100) / baselineValue; if (Double.isNaN(ratio)) { ratio = 0; } else if (Double.isInfinite(ratio)) { ratio = 100; } String ratioStr = HeatMapCell.format(ratio); Double cumulativeBaselineValue; Double cumulativeCurrentValue; if (runningTotalMap.containsKey(metricName)) { Double[] totalValues = runningTotalMap.get(metricName); cumulativeBaselineValue = totalValues[0] + baselineValue; cumulativeCurrentValue = totalValues[1] + currentValue; } else { cumulativeBaselineValue = baselineValue; cumulativeCurrentValue = currentValue; } Double[] runningTotalPerMetric = new Double[] { cumulativeBaselineValue, cumulativeCurrentValue }; runningTotalMap.put(metricName, runningTotalPerMetric); String cumulativeBaselineValueStr = HeatMapCell.format(cumulativeBaselineValue); String cumulativeCurrentValueStr = HeatMapCell.format(cumulativeCurrentValue); double cumulativeRatio = ((cumulativeCurrentValue - cumulativeBaselineValue) * 100) / cumulativeBaselineValue; if (Double.isNaN(ratio)) { cumulativeRatio = 0; } else if (Double.isInfinite(cumulativeRatio)) { cumulativeRatio = 100; } String cumulativeRatioStr = HeatMapCell.format(cumulativeRatio); String[] columnData = { baselineValueStr, currentValueStr, ratioStr, cumulativeBaselineValueStr, cumulativeCurrentValueStr, cumulativeRatioStr }; GenericResponse rowData = data.get(metric.getMetricName()); rowData.getResponseData().add(columnData); } } return tabularViewResponse; } public void addColumnData(Map<String, GenericResponse> data, String metric, String[] columnData, String[] columns) { GenericResponse rowData; if (data.containsKey(metric)) { rowData = data.get(metric); rowData.getResponseData().add(columnData); } else { ResponseSchema schema = new ResponseSchema(); for (int i = 0; i < columns.length; i++) { String column = columns[i]; schema.add(column, i); } rowData = new GenericResponse(); List<String[]> genericResponseData = new ArrayList<>(); genericResponseData.add(columnData); rowData.setResponseData(genericResponseData); rowData.setSchema(schema); data.put(metric, rowData); } } }