package com.linkedin.thirdeye.dashboard.views.contributor; 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.Set; import java.util.SortedSet; import java.util.TreeMap; 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.Lists; 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.Utils; import com.linkedin.thirdeye.dashboard.views.GenericResponse; import com.linkedin.thirdeye.dashboard.views.GenericResponse.Info; 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.datalayer.dto.DatasetConfigDTO; import com.linkedin.thirdeye.util.ThirdEyeUtils; public class ContributorViewHandler implements ViewHandler<ContributorViewRequest, ContributorViewResponse> { private static final 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 ContributorViewHandler(QueryCache queryCache) { this.queryCache = queryCache; } private TimeOnTimeComparisonRequest generateTimeOnTimeComparisonRequest( ContributorViewRequest 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<String> dimensionsToGroupBy = request.getGroupByDimensions(); if (dimensionsToGroupBy == null || dimensionsToGroupBy.isEmpty()) { List<String> allDimensions = Utils.getDimensionsToGroupBy(collection, filters); dimensionsToGroupBy = Lists.newArrayList(allDimensions.get(0)); } 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()); comparisonRequest.setGroupByDimensions(dimensionsToGroupBy); return comparisonRequest; } private List<TimeBucket> getTimeBuckets(TimeOnTimeComparisonResponse response) { Set<TimeBucket> timeBuckets = new TreeSet<>(); int numRows = response.getNumRows(); for (int i = 0; i < numRows; i++) { Row row = response.getRow(i); TimeBucket bucket = TimeBucket.fromRow(row); timeBuckets.add(bucket); } return new ArrayList<TimeBucket>(timeBuckets); } private Map<String, SortedSet<Row>> getRowsSortedByTime(TimeOnTimeComparisonResponse response) { Map<String, SortedSet<Row>> result = new HashMap<>(); int numRows = response.getNumRows(); for (int i = 0; i < numRows; i++) { Row row = response.getRow(i); String dimensionName = row.getDimensionName(); String dimensionValue = row.getDimensionValue(); String rowGroupKey = dimensionName + "." + dimensionValue; if (result.containsKey(rowGroupKey)) { result.get(rowGroupKey).add(row); } else { SortedSet<Row> rows = new TreeSet<>(rowComparator); rows.add(row); result.put(rowGroupKey, rows); } } return result; } @Override public ContributorViewResponse process(ContributorViewRequest request) throws Exception { TimeOnTimeComparisonRequest comparisonRequest = generateTimeOnTimeComparisonRequest(request); TimeOnTimeComparisonHandler handler = new TimeOnTimeComparisonHandler(queryCache); TimeOnTimeComparisonResponse response = handler.handle(comparisonRequest); List<String> metricNames = new ArrayList<>(response.getMetrics()); List<String> expressionNames = new ArrayList<>(); for (MetricExpression expression : request.getMetricExpressions()) { expressionNames.add(expression.getExpressionName()); } List<String> dimensions = new ArrayList<>(response.getDimensions()); List<TimeBucket> timeBuckets = getTimeBuckets(response); Map<String, SortedSet<Row>> rows = getRowsSortedByTime(response); ContributorViewResponse contributorViewResponse = new ContributorViewResponse(); contributorViewResponse.setMetrics(expressionNames); contributorViewResponse.setDimensions(dimensions); contributorViewResponse.setTimeBuckets(timeBuckets); GenericResponse genericResponse = new GenericResponse(); Map<String, Double[]> runningTotalMap = new HashMap<>(); // one view per <metric,dimensionName> combination Map<String, ContributionViewTableBuilder> contributionViewTableMap = new LinkedHashMap<>(); Map<String, List<String>> dimensionValuesMap = new HashMap<>(); for (Map.Entry<String, SortedSet<Row>> entry : rows.entrySet()) { for (Row row : entry.getValue()) { String dimensionName = row.getDimensionName(); String dimensionValue = row.getDimensionValue(); for (Metric metric : row.getMetrics()) { String metricName = metric.getMetricName(); if (!expressionNames.contains(metricName)) { continue; } Double baselineValue = metric.getBaselineValue(); Double currentValue = metric.getCurrentValue(); Double cumulativeBaselineValue; Double cumulativeCurrentValue; String metricDimensionNameString = metricName + "." + dimensionName; ContributionViewTableBuilder contributionViewTable = contributionViewTableMap.get(metricDimensionNameString); if (contributionViewTable == null) { contributionViewTable = new ContributionViewTableBuilder(metricName, dimensionName); contributionViewTableMap.put(metricDimensionNameString, contributionViewTable); } String rowKey = metricName + "." + dimensionName + "." + dimensionValue; if (runningTotalMap.containsKey(rowKey)) { Double[] totalValues = runningTotalMap.get(rowKey); cumulativeBaselineValue = totalValues[0] + baselineValue; cumulativeCurrentValue = totalValues[1] + currentValue; } else { cumulativeBaselineValue = baselineValue; cumulativeCurrentValue = currentValue; } TimeBucket timeBucket = TimeBucket.fromRow(row); contributionViewTable.addEntry(dimensionValue, timeBucket, baselineValue, currentValue, cumulativeBaselineValue, cumulativeCurrentValue); List<String> dimensionValues = dimensionValuesMap.get(dimensionName); if (dimensionValues == null) { dimensionValues = new ArrayList<>(); dimensionValuesMap.put(dimensionName, dimensionValues); } if (!dimensionValues.contains(dimensionValue)) { dimensionValues.add(dimensionValue); } Double[] runningTotalPerMetric = new Double[] { cumulativeBaselineValue, cumulativeCurrentValue }; runningTotalMap.put(rowKey, runningTotalPerMetric); } } } Map<String, List<Integer>> keyToRowIdListMapping = new TreeMap<>(); List<String[]> rowData = new ArrayList<>(); // for each metric, dimension pair compute the total value for each dimension. This will be used // to sort the dimension values Map<String, Map<String, Map<String, Double>>> baselineTotalMapPerDimensionValue = new HashMap<>(); Map<String, Map<String, Map<String, Double>>> currentTotalMapPerDimensionValue = new HashMap<>(); for (String metricDimensionNameString : contributionViewTableMap.keySet()) { ContributionViewTableBuilder contributionViewTable = contributionViewTableMap.get(metricDimensionNameString); ContributionViewTable table = contributionViewTable.build(); List<ContributionCell> cells = table.getCells(); for (ContributionCell cell : cells) { String metricName = table.getMetricName(); String dimName = table.getDimensionName(); String dimValue = cell.getDimensionValue(); String key = metricName + "|" + dimName + "|" + dimValue; List<Integer> rowIdList = keyToRowIdListMapping.get(key); if (rowIdList == null) { rowIdList = new ArrayList<>(); keyToRowIdListMapping.put(key, rowIdList); } rowIdList.add(rowData.size()); rowData.add(cell.toArray()); // update baseline updateTotalForDimensionValue(baselineTotalMapPerDimensionValue, metricName, dimName, dimValue, cell.getBaselineValue()); // update current updateTotalForDimensionValue(currentTotalMapPerDimensionValue, metricName, dimName, dimValue, cell.getCurrentValue()); } } genericResponse.setResponseData(rowData); genericResponse.setSchema(new ResponseSchema(ContributionCell.columns())); genericResponse.setKeyToRowIdMapping(keyToRowIdListMapping); Info summary = new Info(); genericResponse.setSummary(summary); for (String dimensionName : dimensionValuesMap.keySet()) { List<String> dimensionValues = dimensionValuesMap.get(dimensionName); sort(expressionNames, dimensionName, dimensionValues, baselineTotalMapPerDimensionValue, currentTotalMapPerDimensionValue); } contributorViewResponse.setDimensionValuesMap(dimensionValuesMap); contributorViewResponse.setResponseData(genericResponse); contributorViewResponse.setCurrentTotalMapPerDimensionValue(currentTotalMapPerDimensionValue); contributorViewResponse.setBaselineTotalMapPerDimensionValue(baselineTotalMapPerDimensionValue); return contributorViewResponse; } /** * sort the values based on their values * @param dimensionValues * @param baselineTotalMapPerDimensionValue * @param currentTotalMapPerDimensionValue */ private void sort(List<String> metricNames, String dimensionName, List<String> dimensionValues, final Map<String, Map<String, Map<String, Double>>> baselineTotalMapPerDimensionValue, final Map<String, Map<String, Map<String, Double>>> currentTotalMapPerDimensionValue) { final String metricName = metricNames.get(0); // sort using current values for now final Map<String, Double> baselineValuesMap = baselineTotalMapPerDimensionValue.get(metricName).get(dimensionName); final Map<String, Double> currentValuesMap = currentTotalMapPerDimensionValue.get(metricName).get(dimensionName); Comparator<? super String> comparator = new Comparator<String>() { @Override public int compare(String o1, String o2) { double d1 = 0; if (currentValuesMap.get(o1) != null) { d1 += currentValuesMap.get(o1); } if (baselineValuesMap.get(o1) != null) { d1 += baselineValuesMap.get(o1); } double d2 = 0; if (currentValuesMap.get(o2) != null) { d2 += currentValuesMap.get(o2); } if (baselineValuesMap.get(o2) != null) { d2 += baselineValuesMap.get(o2); } return Double.compare(d1, d2) * -1; } }; Collections.sort(dimensionValues, comparator); } private void updateTotalForDimensionValue(Map<String, Map<String, Map<String, Double>>> map, String metricName, String dimName, String dimValue, double value) { if (!map.containsKey(metricName)) { map.put(metricName, new HashMap<String, Map<String, Double>>()); } if (!map.get(metricName).containsKey(dimName)) { map.get(metricName).put(dimName, new HashMap<String, Double>()); } if (!map.get(metricName).get(dimName).containsKey(dimValue)) { map.get(metricName).get(dimName).put(dimValue, 0d); ; } double currentSum = map.get(metricName).get(dimName).get(dimValue); map.get(metricName).get(dimName).put(dimValue, currentSum + value); } }