This is an automated email from the ASF dual-hosted git repository.

nju_yaho pushed a commit to tag ebay-3.1.0-release-20200701
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit 99d3c9be4e3de0dcf7f40e9747d7a84667af2f9a
Author: Zhong, Yanghong <nju_y...@apache.org>
AuthorDate: Thu May 28 13:51:22 2020 +0800

    KYLIN-4532 Introduce cube planner optimization benefit score
---
 .../org/apache/kylin/common/KylinConfigBase.java   |   6 +-
 .../kylin/common/persistence/ResourceStore.java    |   1 +
 .../java/org/apache/kylin/cube/CubeInstance.java   |  22 ++
 .../java/org/apache/kylin/cube/CubeManager.java    |  50 ++++
 .../java/org/apache/kylin/cube/CubeUpdate.java     |  18 ++
 .../cube/cuboid/algorithm/CuboidRecommender.java   |   2 +-
 .../cube/cuboid/algorithm/CuboidStatsUtil.java     |  82 +++++-
 .../cube/cuboid/algorithm/OptimizationBenefit.java | 289 +++++++++++++++++++++
 .../cube/cuboid/algorithm/RecommendResult.java     |  58 +++++
 .../engine/mr/common/CuboidRecommenderUtil.java    |  34 ++-
 .../kylin/rest/controller/CubeController.java      |  75 ++++--
 .../java/org/apache/kylin/rest/msg/Message.java    |   5 +
 .../rest/response/CuboidRecommendResponse.java     |  42 +++
 .../org/apache/kylin/rest/service/CubeService.java |   3 +-
 webapp/app/js/services/cubes.js                    |   2 +-
 15 files changed, 658 insertions(+), 31 deletions(-)

diff --git 
a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java 
b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
index 93cdcf9..416800b 100644
--- a/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
+++ b/core-common/src/main/java/org/apache/kylin/common/KylinConfigBase.java
@@ -840,7 +840,7 @@ public abstract class KylinConfigBase implements 
Serializable {
         return 
Double.parseDouble(getOptional("kylin.cube.cubeplanner.bpus-min-benefit-ratio", 
"0.01"));
     }
 
-    public int getCubePlannerAgreedyAlgorithmAutoThreshold() {
+    public int getCubePlannerGreedyAlgorithmAutoThreshold() {
         return 
Integer.parseInt(getOptional("kylin.cube.cubeplanner.algorithm-threshold-greedy",
 "8"));
     }
 
@@ -848,6 +848,10 @@ public abstract class KylinConfigBase implements 
Serializable {
         return 
Integer.parseInt(getOptional("kylin.cube.cubeplanner.algorithm-threshold-genetic",
 "23"));
     }
 
+    public double getCubePlannerOptimizationSpaceBenefitRatio() {
+        return 
Double.parseDouble(getOptional("kylin.cube.cubeplanner.optimization-space-benefit-ratio",
 "0.1"));
+    }
+
     /**
      * get assigned server array, which a empty string array in default
      * @return
diff --git 
a/core-common/src/main/java/org/apache/kylin/common/persistence/ResourceStore.java
 
b/core-common/src/main/java/org/apache/kylin/common/persistence/ResourceStore.java
index 453fd01..b933ae3 100644
--- 
a/core-common/src/main/java/org/apache/kylin/common/persistence/ResourceStore.java
+++ 
b/core-common/src/main/java/org/apache/kylin/common/persistence/ResourceStore.java
@@ -80,6 +80,7 @@ abstract public class ResourceStore {
     public static final String DRAFT_RESOURCE_ROOT = "/draft";
     public static final String USER_ROOT = "/user";
     public static final String EXT_SNAPSHOT_RESOURCE_ROOT = 
"/ext_table_snapshot";
+    public static final String CUBE_SCORE_ROOT = "/cube_score";
 
     public static final String METASTORE_UUID_TAG = "/UUID";
 
diff --git a/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java 
b/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java
index 454816d..0d3f39d 100644
--- a/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java
+++ b/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java
@@ -130,6 +130,12 @@ public class CubeInstance extends RootPersistentEntity 
implements IRealization,
     @JsonProperty("snapshots")
     private Map<String, String> snapshots = Maps.newHashMap();
 
+    @JsonProperty("score")
+    private double score = -1;
+
+    @JsonProperty("score_hint")
+    private String scoreHint;
+    
     // cuboid scheduler lazy built
     transient private CuboidScheduler cuboidScheduler;
 
@@ -735,6 +741,22 @@ public class CubeInstance extends RootPersistentEntity 
implements IRealization,
         getSnapshots().put(table, snapshotResPath);
     }
 
+    public void setScore(double score) {
+        this.score = score;
+    }
+
+    public double getScore() {
+        return score;
+    }
+
+    public String getScoreHint() {
+        return scoreHint;
+    }
+
+    public void setScoreHint(String scoreHint) {
+        this.scoreHint = scoreHint;
+    }
+
     public static CubeInstance getCopyOf(CubeInstance other) {
         CubeInstance ret = new CubeInstance();
         ret.setName(other.getName());
diff --git a/core-cube/src/main/java/org/apache/kylin/cube/CubeManager.java 
b/core-cube/src/main/java/org/apache/kylin/cube/CubeManager.java
index 6e9452d..a61a409 100755
--- a/core-cube/src/main/java/org/apache/kylin/cube/CubeManager.java
+++ b/core-cube/src/main/java/org/apache/kylin/cube/CubeManager.java
@@ -45,6 +45,7 @@ import org.apache.kylin.common.util.Dictionary;
 import org.apache.kylin.common.util.Pair;
 import org.apache.kylin.common.util.RandomUtil;
 import org.apache.kylin.cube.cuboid.Cuboid;
+import org.apache.kylin.cube.cuboid.algorithm.OptimizationBenefit;
 import org.apache.kylin.cube.model.CubeDesc;
 import org.apache.kylin.cube.model.CubeDescTiretreeGlobalDomainDictUtil;
 import org.apache.kylin.cube.model.SnapshotTableDesc;
@@ -305,6 +306,24 @@ public class CubeManager implements IRealizationProvider {
         }
     }
 
+    private void updateCubeScore(CubeInstance cube, double score, String 
scoreHint) throws IOException {
+        if (cube.getScore() == score) {
+            if (cube.getScoreHint() == null) {
+                if (scoreHint == null)
+                    return;
+            } else if (cube.getScoreHint().equals(scoreHint)) {
+                return;
+            }
+        }
+        try (AutoLock lock = cubeMapLock.lockForWrite()) {
+            cube = cube.latestCopyForWrite(); // get a latest copy
+            CubeUpdate update = new CubeUpdate(cube);
+            update.setScore(score);
+            update.setScoreHint(scoreHint);
+            updateCube(update);
+        }
+    }
+
     public CubeInstance updateCubeStatus(CubeInstance cube, 
RealizationStatusEnum newStatus) throws IOException {
         try (AutoLock lock = cubeMapLock.lockForWrite()) {
             cube = cube.latestCopyForWrite(); // get a latest copy
@@ -454,6 +473,14 @@ public class CubeManager implements IRealizationProvider {
         if (update.getCuboidLastOptimized() >= 0) {
             cube.setCuboidLastOptimized(update.getCuboidLastOptimized());
         }
+
+        if (update.getScore() >= -1) {
+            cube.setScore(update.getScore());
+        }
+
+        if (update.getScoreHint() != null) {
+            cube.setScoreHint(update.getScoreHint());
+        }
     }
 
     private void processToUpdateSegments(CubeUpdate update, 
Segments<CubeSegment> newSegs) {
@@ -586,6 +613,29 @@ public class CubeManager implements IRealizationProvider {
         return snapshotResPath;
     }
 
+    public OptimizationBenefit loadCubeOptimizationBenefit(final CubeInstance 
cubeInstance) throws IOException {
+        logger.info("Going to load optimization benefit for cube {}", 
cubeInstance.getName());
+        return OptimizationBenefit.loadFromStore(getStore(), cubeInstance);
+    }
+
+    public void saveCubeOptimizationBenefit(CubeInstance cubeInstance, 
OptimizationBenefit benefit) throws IOException {
+        saveCubeOptimizationBenefit(cubeInstance, benefit, true);
+    }
+
+    public void saveCubeOptimizationBenefit(CubeInstance cubeInstance, 
OptimizationBenefit benefit, boolean ifUpdate)
+            throws IOException {
+        logger.info("Going to update optimization benefit for cube {}", 
cubeInstance.getName());
+        OptimizationBenefit.saveToStore(getStore(), cubeInstance, benefit);
+        if (ifUpdate) {
+            updateCubeScore(cubeInstance, benefit.getScore(), 
benefit.getScoreHint());
+        }
+    }
+
+    private void removeCubeScoreStats(CubeInstance cubeInstance) throws 
IOException {
+        logger.info("Going to remove optimization benefit for cube {}", 
cubeInstance.getName());
+        OptimizationBenefit.removeFromStore(getStore(), cubeInstance);
+    }
+
     @VisibleForTesting
     /*private*/ String generateStorageLocation() {
         String namePrefix = config.getHBaseTableNamePrefix();
diff --git a/core-cube/src/main/java/org/apache/kylin/cube/CubeUpdate.java 
b/core-cube/src/main/java/org/apache/kylin/cube/CubeUpdate.java
index 33a3251..28c9bca 100644
--- a/core-cube/src/main/java/org/apache/kylin/cube/CubeUpdate.java
+++ b/core-cube/src/main/java/org/apache/kylin/cube/CubeUpdate.java
@@ -39,6 +39,8 @@ public class CubeUpdate {
     private Map<String, String> updateTableSnapshotPath = null;
     private long createTimeUTC = -1;
     private long cuboidLastOptimized = -1;
+    private double score = -2;
+    private String scoreHint;
 
     public CubeUpdate(CubeInstance cubeInstance) {
         setCubeInstance(cubeInstance);
@@ -151,4 +153,20 @@ public class CubeUpdate {
     public void setCuboidLastOptimized(long cuboidLastOptimized) {
         this.cuboidLastOptimized = cuboidLastOptimized;
     }
+    
+    public double getScore() {
+        return score;
+    }
+
+    public void setScore(double score) {
+        this.score = score;
+    }
+
+    public String getScoreHint() {
+        return scoreHint;
+    }
+
+    public void setScoreHint(String scoreHint) {
+        this.scoreHint = scoreHint;
+    }
 }
diff --git 
a/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidRecommender.java
 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidRecommender.java
index dfea903..4fbdc21 100644
--- 
a/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidRecommender.java
+++ 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidRecommender.java
@@ -118,7 +118,7 @@ public class CuboidRecommender {
      */
     public Map<Long, Long> getRecommendCuboidList(CuboidStats cuboidStats, 
KylinConfig kylinConf,
             boolean ifForceRecommend) {
-        long threshold1 = 1L << 
kylinConf.getCubePlannerAgreedyAlgorithmAutoThreshold() - 1;
+        long threshold1 = 1L << 
kylinConf.getCubePlannerGreedyAlgorithmAutoThreshold() - 1;
         long threshold2 = 1L << 
kylinConf.getCubePlannerGeneticAlgorithmAutoThreshold() - 1;
         if (threshold1 >= threshold2) {
             logger.error("Invalid Cube Planner Algorithm configuration");
diff --git 
a/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidStatsUtil.java
 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidStatsUtil.java
index 88b3db1..8beeb75 100644
--- 
a/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidStatsUtil.java
+++ 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/CuboidStatsUtil.java
@@ -28,12 +28,17 @@ import java.util.SortedSet;
 import java.util.TreeSet;
 
 import org.apache.kylin.common.util.Pair;
-
+import org.apache.kylin.cube.cuboid.CuboidScheduler;
 import org.apache.kylin.shaded.com.google.common.collect.Lists;
 import org.apache.kylin.shaded.com.google.common.collect.Maps;
+import org.apache.kylin.shaded.com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class CuboidStatsUtil {
 
+    private static final Logger logger = 
LoggerFactory.getLogger(CuboidStatsUtil.class);
+    
     private CuboidStatsUtil() {
         throw new IllegalStateException("Class CuboidStatsUtil is an utility 
class !");
     }
@@ -348,6 +353,81 @@ public class CuboidStatsUtil {
         return (cuboidToCheck & parentCuboid) == cuboidToCheck;
     }
 
+    /**
+     * The formula for calculating the benefit of cube optimization according 
to recommended cuboids is as follows:
+     * query benefit + space benefit
+     * | query benefit = (rollupBenefit - rollupCost) / rollupInputCount
+     * | space benefit = (curTotalSize - recomTotalSize) / spaceLimit
+     * @return the benefit of cube optimization according to recommended 
cuboids
+     */
+    public static OptimizationBenefit calculateOptimizationBenefit(Map<Long, 
Long> statistics,
+            Map<Long, Double> cuboidsSize, CuboidScheduler curCuboidScheduler, 
CuboidScheduler recomCuboidScheduler,
+            Map<Long, Double> cuboidHitProbabilityMap, double spaceLimit, 
double spaceBenefitRatio) {
+        if (cuboidHitProbabilityMap == null || 
cuboidHitProbabilityMap.isEmpty()) {
+            return OptimizationBenefit.ZERO;
+        }
+
+        Set<Long> currentCuboids = curCuboidScheduler.getAllCuboidIds();
+        Set<Long> recommendCuboids = recomCuboidScheduler.getAllCuboidIds();
+
+        double rollupBenefit = 0.0;
+        {// compute the benefits of added cuboids in recommendCuboids
+            Set<Long> recommendCuboidsOnly = Sets.newHashSet(recommendCuboids);
+            recommendCuboidsOnly.removeAll(currentCuboids);
+            logger.info("Added cuboids {}", recommendCuboidsOnly);
+            for (Long cuboid : recommendCuboidsOnly) {
+                Long pCuboid = curCuboidScheduler.findBestMatchCuboid(cuboid);
+                long rollupCount = statistics.get(pCuboid) - 
statistics.get(cuboid);
+                rollupBenefit += rollupCount * 
cuboidHitProbabilityMap.get(cuboid);
+            }
+        }
+
+        double rollupCost = 0.0;
+        {// compute the cost of removed cuboids in currentCuboids
+            Set<Long> currentCuboidsOnly = Sets.newHashSet(currentCuboids);
+            currentCuboidsOnly.removeAll(recommendCuboids);
+            logger.info("Removed cuboids {}", currentCuboidsOnly);
+            for (Long cuboid : currentCuboidsOnly) {
+                Long pCuboid = 
recomCuboidScheduler.findBestMatchCuboid(cuboid);
+                long rollupCount = statistics.get(pCuboid) - 
statistics.get(cuboid);
+                rollupCost += rollupCount * 
cuboidHitProbabilityMap.get(cuboid);
+            }
+        }
+
+        double rollupInputCount = 0.0;
+        {// compute the total rollup input count
+            for (Long cuboid : currentCuboids) {
+                Long rowCount = statistics.get(cuboid);
+                rollupInputCount += rowCount * 
cuboidHitProbabilityMap.get(cuboid);
+            }
+
+            Set<Long> recommendCuboidsOnly = Sets.newHashSet(recommendCuboids);
+            recommendCuboidsOnly.removeAll(currentCuboids);
+            for (Long cuboid : recommendCuboidsOnly) {
+                Long pCuboid = curCuboidScheduler.findBestMatchCuboid(cuboid);
+                Long rowCount = statistics.get(pCuboid);
+                rollupInputCount += rowCount * 
cuboidHitProbabilityMap.get(cuboid);
+            }
+        }
+
+        double curTotalSize = 0.0;
+        {// compute the total size of current cuboids
+            for (Long cuboid : currentCuboids) {
+                curTotalSize += cuboidsSize.get(cuboid);
+            }
+        }
+
+        double recomTotalSize = 0.0;
+        {// compute the total size of recommended cuboids
+            for (Long cuboid : recommendCuboids) {
+                recomTotalSize += cuboidsSize.get(cuboid);
+            }
+        }
+
+        return new OptimizationBenefit(rollupBenefit, rollupCost, 
rollupInputCount, curTotalSize, recomTotalSize,
+                spaceLimit, spaceBenefitRatio);
+    }
+
     private static double calculateRollupRatio(Pair<Long, Long> rollupStats) {
         double rollupInputCount = (double) rollupStats.getFirst() + 
rollupStats.getSecond();
         return rollupInputCount == 0 ? 0 : 1.0 * rollupStats.getFirst() / 
rollupInputCount;
diff --git 
a/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/OptimizationBenefit.java
 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/OptimizationBenefit.java
new file mode 100644
index 0000000..aa6ce01
--- /dev/null
+++ 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/OptimizationBenefit.java
@@ -0,0 +1,289 @@
+/*
+ * 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.kylin.cube.cuboid.algorithm;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Objects;
+
+import org.apache.kylin.common.persistence.JsonSerializer;
+import org.apache.kylin.common.persistence.ResourceStore;
+import org.apache.kylin.common.persistence.RootPersistentEntity;
+import org.apache.kylin.common.persistence.Serializer;
+import org.apache.kylin.cube.CubeInstance;
+import org.apache.kylin.metadata.MetadataConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@SuppressWarnings("serial")
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, 
getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = 
JsonAutoDetect.Visibility.NONE, setterVisibility = 
JsonAutoDetect.Visibility.NONE)
+public class OptimizationBenefit extends RootPersistentEntity {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(OptimizationBenefit.class);
+
+    public static final String SCORE_TYPE = "opt_benefit";
+
+    public static final Serializer<OptimizationBenefit> CUBE_SCORE_SERIALIZER 
= new JsonSerializer<>(
+            OptimizationBenefit.class);
+
+    public static OptimizationBenefit loadFromStore(ResourceStore store, 
CubeInstance cubeInstance) throws IOException {
+        return store.getResource(getScorePath(cubeInstance), 
CUBE_SCORE_SERIALIZER);
+    }
+
+    public static void saveToStore(ResourceStore store, CubeInstance 
cubeInstance, OptimizationBenefit obj)
+            throws IOException {
+        store.putBigResource(getScorePath(cubeInstance), obj, 
System.currentTimeMillis(), CUBE_SCORE_SERIALIZER);
+    }
+
+    public static void removeFromStore(ResourceStore store, CubeInstance 
cubeInstance) throws IOException {
+        store.deleteResource(getScorePath(cubeInstance));
+    }
+
+    private static String getScorePath(CubeInstance cubeInstance) {
+        return ResourceStore.CUBE_SCORE_ROOT + "/" + SCORE_TYPE + "/" + 
cubeInstance.getName()
+                + MetadataConstants.FILE_SURFIX;
+    }
+
+    public static final OptimizationBenefit ZERO = new OptimizationBenefit();
+
+    // queryBenefit = (rollupBenefit - rollupCost) / totalRollupInputCount
+    @JsonProperty("query_benefit")
+    private double queryBenefit;
+
+    @JsonProperty("rollup_benefit")
+    private double rollupBenefit;
+
+    @JsonProperty("rollup_cost")
+    private double rollupCost;
+
+    @JsonProperty("rollup_input_count")
+    private double rollupInputCount;
+
+    // spaceBenefit = (curTotalSize - recomTotalSize) / spaceLimit
+    @JsonProperty("space_benefit")
+    private double spaceBenefit;
+
+    @JsonProperty("recommend_total_size")
+    private double recomTotalSize;
+
+    @JsonProperty("current_total_size")
+    private double curTotalSize;
+
+    @JsonProperty("space_limit_size")
+    private double spaceLimit;
+
+    // total benefit = query benefit + k * space benefit;
+    @JsonProperty("space_benefit_ratio")
+    private double k;
+
+    @JsonProperty("total_benefit")
+    private double totalBenefit;
+
+    public OptimizationBenefit() {
+        this.rollupBenefit = 0;
+        this.rollupCost = 0;
+        this.rollupInputCount = 0;
+        this.queryBenefit = 0;
+
+        this.recomTotalSize = 0;
+        this.curTotalSize = 0;
+        this.spaceLimit = 0;
+        this.spaceBenefit = 0;
+
+        this.k = 0;
+        this.totalBenefit = 0;
+    }
+
+    public OptimizationBenefit(double rollupBenefit, double rollupCost, double 
rollupInputCount, double curTotalSize,
+            double recomTotalSize, double spaceLimit) {
+        this(rollupBenefit, rollupCost, rollupInputCount, curTotalSize, 
recomTotalSize, spaceLimit, 0.1);
+    }
+
+    public OptimizationBenefit(double rollupBenefit, double rollupCost, double 
rollupInputCount, double curTotalSize,
+            double recomTotalSize, double spaceLimit, double 
spaceBenefitRatio) {
+        updateRandomUuid();
+
+        this.rollupBenefit = rollupBenefit;
+        this.rollupCost = rollupCost;
+        this.rollupInputCount = rollupInputCount;
+        this.queryBenefit = rollupInputCount == 0 ? 0 : (rollupBenefit - 
rollupCost) / rollupInputCount;
+
+        this.curTotalSize = curTotalSize;
+        this.recomTotalSize = recomTotalSize;
+        this.spaceLimit = spaceLimit;
+
+        this.spaceBenefit = spaceLimit == 0 ? 0.0 : (curTotalSize - 
recomTotalSize) / spaceLimit;
+
+        this.k = spaceBenefitRatio;
+        this.totalBenefit = queryBenefit + k * spaceBenefit;
+    }
+
+    public double getTotalBenefit() {
+        return totalBenefit;
+    }
+
+    public void setTotalBenefit(double totalBenefit) {
+        this.totalBenefit = totalBenefit;
+    }
+
+    public double getK() {
+        return k;
+    }
+
+    public void setK(double k) {
+        this.k = k;
+    }
+
+    public double getSpaceLimit() {
+        return spaceLimit;
+    }
+
+    public void setSpaceLimit(double spaceLimit) {
+        this.spaceLimit = spaceLimit;
+    }
+
+    public double getCurTotalSize() {
+        return curTotalSize;
+    }
+
+    public void setCurTotalSize(double curTotalSize) {
+        this.curTotalSize = curTotalSize;
+    }
+
+    public double getSpaceBenefit() {
+        return spaceBenefit;
+    }
+
+    public void setSpaceBenefit(double spaceBenefit) {
+        this.spaceBenefit = spaceBenefit;
+    }
+
+    public double getRecomTotalSize() {
+        return recomTotalSize;
+    }
+
+    public void setRecomTotalSize(double recomTotalSize) {
+        this.recomTotalSize = recomTotalSize;
+    }
+
+    public double getRollupInputCount() {
+        return rollupInputCount;
+    }
+
+    public void setRollupInputCount(double rollupInputCount) {
+        this.rollupInputCount = rollupInputCount;
+    }
+
+    public double getRollupBenefit() {
+        return rollupBenefit;
+    }
+
+    public void setRollupBenefit(double rollupBenefit) {
+        this.rollupBenefit = rollupBenefit;
+    }
+
+    public double getRollupCost() {
+        return rollupCost;
+    }
+
+    public void setRollupCost(double rollupCost) {
+        this.rollupCost = rollupCost;
+    }
+
+    public double getQueryBenefit() {
+        return queryBenefit;
+    }
+
+    public void setQueryBenefit(double queryBenefit) {
+        this.queryBenefit = queryBenefit;
+    }
+
+    @JsonProperty("score")
+    public double getScore() {
+        if (isZero()) {
+            return -1;
+        }
+        return (int) (100 * (totalBenefit <= 0 ? 1 : 1 / (1 + totalBenefit)));
+    }
+
+    public String getScoreHint() {
+        if (isZero()) {
+            return "";
+        }
+        String queryDesc = queryBenefit >= 0 ? "speed up " : "slow down ";
+        String spaceDesc = spaceBenefit >= 0 ? "reduce " : "increase ";
+
+        String queryRatio = String.format(Locale.ROOT, "%.0f", 
Math.abs(queryBenefit) * 100);
+        String spaceRatio = String.format(Locale.ROOT, "%.0f", 
Math.abs(spaceBenefit) * 100);
+        String spaceSize = String.format(Locale.ROOT, "%.0f", 
Math.abs(curTotalSize - recomTotalSize));
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("Benefit you will get after optimization: \n");
+        sb.append("- query latency: 
").append(queryDesc).append(queryRatio).append("%\n");
+        sb.append("- storage usage: 
").append(spaceDesc).append(spaceRatio).append("% with size ").append(spaceSize)
+                .append(" MB\n");
+        return sb.toString();
+    }
+
+    public void printDetails() {
+        logger.info("benefit = queryBenefitRatio + spaceBenefitRatio = {}", 
totalBenefit);
+        logger.info("                    queryBenefit: {}", queryBenefit);
+        logger.info("                    spaceBenefit: {}", spaceBenefit);
+        logger.info("                   rollupBenefit: {}", rollupBenefit);
+        logger.info("                      rollupCost: {}", rollupCost);
+        logger.info("                rollupInputCount: {}", rollupInputCount);
+        logger.info("                    curTotalSize: {}", curTotalSize);
+        logger.info("                  recomTotalSize: {}", recomTotalSize);
+        logger.info("                      spaceLimit: {}", spaceLimit);
+        logger.info("               spaceBenefitRatio: {}", k);
+    }
+
+    public boolean isZero() {
+        return this.equals(ZERO);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        if (!super.equals(o))
+            return false;
+        OptimizationBenefit that = (OptimizationBenefit) o;
+        return Double.compare(that.queryBenefit, queryBenefit) == 0
+                && Double.compare(that.rollupBenefit, rollupBenefit) == 0
+                && Double.compare(that.rollupCost, rollupCost) == 0
+                && Double.compare(that.rollupInputCount, rollupInputCount) == 0
+                && Double.compare(that.spaceBenefit, spaceBenefit) == 0
+                && Double.compare(that.recomTotalSize, recomTotalSize) == 0
+                && Double.compare(that.curTotalSize, curTotalSize) == 0
+                && Double.compare(that.spaceLimit, spaceLimit) == 0 && 
Double.compare(that.k, k) == 0
+                && Double.compare(that.totalBenefit, totalBenefit) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), queryBenefit, rollupBenefit, 
rollupCost, rollupInputCount, spaceBenefit,
+                recomTotalSize, curTotalSize, spaceLimit, k, totalBenefit);
+    }
+}
diff --git 
a/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/RecommendResult.java
 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/RecommendResult.java
new file mode 100644
index 0000000..a491b69f
--- /dev/null
+++ 
b/core-cube/src/main/java/org/apache/kylin/cube/cuboid/algorithm/RecommendResult.java
@@ -0,0 +1,58 @@
+/*
+ * 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.kylin.cube.cuboid.algorithm;
+
+import java.util.Map;
+
+public class RecommendResult {
+
+    public static class Builder {
+        private Map<Long, Long> recommendCuboids;
+        private OptimizationBenefit optBenefit;
+
+        public Builder(Map<Long, Long> recommendCuboids) {
+            this.recommendCuboids = recommendCuboids;
+        }
+
+        public Builder setOptimizationBenefitModel(OptimizationBenefit 
optBenefit) {
+            this.optBenefit = optBenefit;
+            return this;
+        }
+
+        public RecommendResult build() {
+            return new RecommendResult(recommendCuboids, optBenefit);
+        }
+    }
+
+    private Map<Long, Long> recommendCuboids;
+    private OptimizationBenefit optBenefit;
+
+    public RecommendResult(Map<Long, Long> recommendCuboids, 
OptimizationBenefit optBenefit) {
+        this.recommendCuboids = recommendCuboids;
+        this.optBenefit = optBenefit;
+    }
+
+    public Map<Long, Long> getRecommendCuboids() {
+        return recommendCuboids;
+    }
+
+    public OptimizationBenefit getOptimizationBenefit() {
+        return optBenefit;
+    }
+}
diff --git 
a/engine-mr/src/main/java/org/apache/kylin/engine/mr/common/CuboidRecommenderUtil.java
 
b/engine-mr/src/main/java/org/apache/kylin/engine/mr/common/CuboidRecommenderUtil.java
index 6d9b748..ab958aa 100644
--- 
a/engine-mr/src/main/java/org/apache/kylin/engine/mr/common/CuboidRecommenderUtil.java
+++ 
b/engine-mr/src/main/java/org/apache/kylin/engine/mr/common/CuboidRecommenderUtil.java
@@ -27,8 +27,13 @@ import org.apache.kylin.common.util.Pair;
 import org.apache.kylin.cube.CubeInstance;
 import org.apache.kylin.cube.CubeSegment;
 import org.apache.kylin.cube.cuboid.CuboidScheduler;
+import org.apache.kylin.cube.cuboid.TreeCuboidScheduler;
 import org.apache.kylin.cube.cuboid.algorithm.CuboidRecommender;
 import org.apache.kylin.cube.cuboid.algorithm.CuboidStats;
+import org.apache.kylin.cube.cuboid.algorithm.CuboidStatsUtil;
+import org.apache.kylin.cube.cuboid.algorithm.OptimizationBenefit;
+import org.apache.kylin.cube.cuboid.algorithm.RecommendResult;
+import org.apache.kylin.shaded.com.google.common.collect.Lists;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -67,7 +72,7 @@ public class CuboidRecommenderUtil {
     }
 
     /** Trigger cube planner phase two for optimization */
-    public static Map<Long, Long> getRecommendCuboidList(CubeInstance cube, 
Map<Long, Long> hitFrequencyMap,
+    public static RecommendResult getRecommendCuboidList(CubeInstance cube, 
Map<Long, Long> hitFrequencyMap,
             Map<Long, Map<Long, Pair<Long, Long>>> rollingUpCountSourceMap) 
throws IOException {
 
         CuboidScheduler cuboidScheduler = cube.getCuboidScheduler();
@@ -84,6 +89,7 @@ public class CuboidRecommenderUtil {
         String key = cube.getName();
         double queryUncertaintyRatio = 
config.getCubePlannerQueryUncertaintyRatio();
         double bpusMinBenefitRatio = 
config.getCubePlannerBPUSMinBenefitRatio();
+        double expansionRatio = config.getCubePlannerExpansionRateThreshold();
         CuboidStats cuboidStats = new CuboidStats.Builder(key, baseCuboid, 
statsPair.getFirst(),
                 statsPair.getSecond()) {
             @Override
@@ -100,7 +106,31 @@ public class CuboidRecommenderUtil {
                 .setHitFrequencyMap(hitFrequencyMap) //
                 .setRollingUpCountSourceMap(rollingUpCountSourceMap) //
                 .build();
-        return 
CuboidRecommender.getInstance().getRecommendCuboidList(cuboidStats, config);
+        Map<Long, Long> recomCuboidStatsMap = 
CuboidRecommender.getInstance().getRecommendCuboidList(cuboidStats,
+                config);
+
+        OptimizationBenefit optBftModel;
+        if (hitFrequencyMap != null && !hitFrequencyMap.isEmpty()) {
+            Map<Long, Long> allStats = cuboidStats.getStatistics();
+            Map<Long, Double> allSizes = 
CuboidStatsReaderUtil.readCuboidSizeFromCube(allStats, cube);
+
+            CuboidScheduler recomCuboidScheduler = new 
TreeCuboidScheduler(cube.getDescriptor(),
+                    Lists.newArrayList(recomCuboidStatsMap.keySet()),
+                    new 
TreeCuboidScheduler.CuboidCostComparator(recomCuboidStatsMap));
+
+            Map<Long, Double> cuboidHitProbabilityMap = 
CuboidStatsUtil.calculateCuboidHitProbability(allStats.keySet(),
+                    hitFrequencyMap, baseCuboid, queryUncertaintyRatio);
+
+            double spaceLimit = expansionRatio * allSizes.get(baseCuboid);
+            double spaceBenefitRatio = 
config.getCubePlannerOptimizationSpaceBenefitRatio();
+
+            optBftModel = 
CuboidStatsUtil.calculateOptimizationBenefit(allStats, allSizes, 
cuboidScheduler,
+                    recomCuboidScheduler, cuboidHitProbabilityMap, spaceLimit, 
spaceBenefitRatio);
+            logger.info("The benefit for optimizing cube {} is {}", cube, 
optBftModel.getTotalBenefit());
+        } else {
+            optBftModel = OptimizationBenefit.ZERO;
+        }
+        return new 
RecommendResult.Builder(recomCuboidStatsMap).setOptimizationBenefitModel(optBftModel).build();
     }
 
     /** For future segment level recommend */
diff --git 
a/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
 
b/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
index d237002..e937920 100644
--- 
a/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
+++ 
b/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
@@ -39,6 +39,8 @@ import org.apache.kylin.cube.CubeManager;
 import org.apache.kylin.cube.CubeSegment;
 import org.apache.kylin.cube.cuboid.CuboidScheduler;
 import org.apache.kylin.cube.cuboid.TreeCuboidScheduler;
+import org.apache.kylin.cube.cuboid.algorithm.OptimizationBenefit;
+import org.apache.kylin.cube.cuboid.algorithm.RecommendResult;
 import org.apache.kylin.cube.model.CubeBuildTypeEnum;
 import org.apache.kylin.cube.model.CubeDesc;
 import org.apache.kylin.cube.model.CubeJoinedFlatTableDesc;
@@ -71,6 +73,7 @@ import org.apache.kylin.rest.request.JobBuildRequest2;
 import org.apache.kylin.rest.request.JobOptimizeRequest;
 import org.apache.kylin.rest.request.LookupSnapshotBuildRequest;
 import org.apache.kylin.rest.response.CubeInstanceResponse;
+import org.apache.kylin.rest.response.CuboidRecommendResponse;
 import org.apache.kylin.rest.response.CuboidTreeResponse;
 import org.apache.kylin.rest.response.EnvelopeResponse;
 import org.apache.kylin.rest.response.GeneralResponse;
@@ -862,26 +865,34 @@ public class CubeController extends BasicController {
         checkCubeExists(cubeName);
         CubeInstance cube = cubeService.getCubeManager().getCube(cubeName);
 
-        Map<Long, Long> cuboidList = getRecommendCuboidList(cube);
-        List<Set<String>> dimensionSetList = Lists.newLinkedList();
+        Message msg = MsgPicker.getMsg();
+        String errorMsg = String.format(Locale.ROOT, 
msg.getFAIL_RECOMMEND_CUBOID(), cubeName);
+
+        RecommendResult recommRet = getRecommendCuboidList(cube);
+        if (recommRet == null) {
+            logger.warn(errorMsg);
+            throw new InternalErrorException(errorMsg);
+        }
 
+        Map<Long, Long> cuboidList = recommRet.getRecommendCuboids();
         if (cuboidList == null || cuboidList.isEmpty()) {
-            logger.info("Cannot get recommended cuboid list for cube " + 
cubeName);
-        } else {
-            if (cuboidList.size() < top) {
-                logger.info("Require " + top + " recommended cuboids, but only 
" + cuboidList.size() + " is found.");
-            }
-            Iterator<Long> cuboidIterator = cuboidList.keySet().iterator();
-            RowKeyColDesc[] rowKeyColDescList = 
cube.getDescriptor().getRowkey().getRowKeyColumns();
-
-            while (top-- > 0 && cuboidIterator.hasNext()) {
-                Set<String> dimensionSet = Sets.newHashSet();
-                dimensionSetList.add(dimensionSet);
-                long cuboid = cuboidIterator.next();
-                for (int i = 0; i < rowKeyColDescList.length; i++) {
-                    if ((cuboid & (1L << rowKeyColDescList[i].getBitIndex())) 
> 0) {
-                        dimensionSet.add(rowKeyColDescList[i].getColumn());
-                    }
+            logger.warn(errorMsg);
+            throw new InternalErrorException(errorMsg);
+        }
+        if (cuboidList.size() < top) {
+            logger.info("Only recommend " + cuboidList.size() + " cuboids less 
than topn " + top);
+        }
+        Iterator<Long> cuboidIterator = cuboidList.keySet().iterator();
+        RowKeyColDesc[] rowKeyColDescList = 
cube.getDescriptor().getRowkey().getRowKeyColumns();
+
+        List<Set<String>> dimensionSetList = Lists.newLinkedList();
+        while (top-- > 0 && cuboidIterator.hasNext()) {
+            Set<String> dimensionSet = Sets.newHashSet();
+            dimensionSetList.add(dimensionSet);
+            long cuboid = cuboidIterator.next();
+            for (int i = 0; i < rowKeyColDescList.length; i++) {
+                if ((cuboid & (1L << rowKeyColDescList[i].getBitIndex())) > 0) 
{
+                    dimensionSet.add(rowKeyColDescList[i].getColumn());
                 }
             }
         }
@@ -923,13 +934,27 @@ public class CubeController extends BasicController {
 
     @RequestMapping(value = "/{cubeName}/cuboids/recommend", method = 
RequestMethod.GET)
     @ResponseBody
-    public CuboidTreeResponse getRecommendCuboids(@PathVariable String 
cubeName) throws IOException {
+    public CuboidRecommendResponse getRecommendCuboids(@PathVariable String 
cubeName) throws IOException {
         checkCubeExists(cubeName);
         CubeInstance cube = cubeService.getCubeManager().getCube(cubeName);
-        Map<Long, Long> recommendCuboidStatsMap = getRecommendCuboidList(cube);
+
+        Message msg = MsgPicker.getMsg();
+        String errorMsg = String.format(Locale.ROOT, 
msg.getFAIL_RECOMMEND_CUBOID(), cubeName);
+
+        RecommendResult recommRet = getRecommendCuboidList(cube);
+        if (recommRet == null) {
+            logger.warn(errorMsg);
+            return new CuboidRecommendResponse(new CuboidTreeResponse(), 
OptimizationBenefit.ZERO);
+        }
+        Map<Long, Long> recommendCuboidStatsMap = 
recommRet.getRecommendCuboids();
         if (recommendCuboidStatsMap == null || 
recommendCuboidStatsMap.isEmpty()) {
-            return new CuboidTreeResponse();
+            logger.warn(errorMsg);
+            return new CuboidRecommendResponse(new CuboidTreeResponse(), 
OptimizationBenefit.ZERO);
         }
+
+        //update cube score
+        cubeService.getCubeManager().saveCubeOptimizationBenefit(cube, 
recommRet.getOptimizationBenefit());
+
         CuboidScheduler cuboidScheduler = new 
TreeCuboidScheduler(cube.getDescriptor(),
                 Lists.newArrayList(recommendCuboidStatsMap.keySet()),
                 new 
TreeCuboidScheduler.CuboidCostComparator(recommendCuboidStatsMap));
@@ -940,11 +965,13 @@ public class CubeController extends BasicController {
         Map<Long, Long> queryMatchMap = 
cubeService.getCuboidQueryMatchCount(cubeName);
 
         Set<Long> currentCuboidSet = 
cube.getCuboidScheduler().getAllCuboidIds();
-        return cubeService.getCuboidTreeResponse(cuboidScheduler, 
recommendCuboidStatsMap, displayHitFrequencyMap,
-                queryMatchMap, currentCuboidSet);
+
+        CuboidTreeResponse cuboidTree = 
cubeService.getCuboidTreeResponse(cuboidScheduler, recommendCuboidStatsMap,
+                displayHitFrequencyMap, queryMatchMap, currentCuboidSet);
+        return new CuboidRecommendResponse(cuboidTree, 
recommRet.getOptimizationBenefit());
     }
 
-    private Map<Long, Long> getRecommendCuboidList(CubeInstance cube) throws 
IOException {
+    private RecommendResult getRecommendCuboidList(CubeInstance cube) throws 
IOException {
         // Get cuboid source info
         Map<Long, Long> optimizeHitFrequencyMap = 
getSourceCuboidHitFrequency(cube.getName());
         Map<Long, Map<Long, Pair<Long, Long>>> rollingUpCountSourceMap = 
cubeService
diff --git a/server-base/src/main/java/org/apache/kylin/rest/msg/Message.java 
b/server-base/src/main/java/org/apache/kylin/rest/msg/Message.java
index ea23761..a06d897 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/msg/Message.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/msg/Message.java
@@ -173,6 +173,11 @@ public class Message {
         return "Rebuild snapshot of hive view '%s' is not supported, please 
refresh segment of the cube";
     }
 
+    // Cube Planner
+    public String getFAIL_RECOMMEND_CUBOID() {
+        return "Cannot get recommend cuboid list for cube '%s'.";
+    }
+    
     // Model
     public String getINVALID_MODEL_DEFINITION() {
         return "The data model definition is invalid.";
diff --git 
a/server-base/src/main/java/org/apache/kylin/rest/response/CuboidRecommendResponse.java
 
b/server-base/src/main/java/org/apache/kylin/rest/response/CuboidRecommendResponse.java
new file mode 100644
index 0000000..58bbd8c
--- /dev/null
+++ 
b/server-base/src/main/java/org/apache/kylin/rest/response/CuboidRecommendResponse.java
@@ -0,0 +1,42 @@
+/*
+ * 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.kylin.rest.response;
+
+import java.io.Serializable;
+
+import org.apache.kylin.cube.cuboid.algorithm.OptimizationBenefit;
+
+public class CuboidRecommendResponse implements Serializable {
+
+    private CuboidTreeResponse cuboidTree;
+    private OptimizationBenefit optBenefit;
+
+    public CuboidRecommendResponse(CuboidTreeResponse cuboidTree, 
OptimizationBenefit optBenefit) {
+        this.cuboidTree = cuboidTree;
+        this.optBenefit = optBenefit;
+    }
+
+    public CuboidTreeResponse getCuboidTree() {
+        return cuboidTree;
+    }
+
+    public OptimizationBenefit getOptBenefit() {
+        return optBenefit;
+    }
+}
diff --git 
a/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java 
b/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java
index f11f31a..f332b4c 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java
@@ -43,6 +43,7 @@ import org.apache.kylin.cube.CubeUpdate;
 import org.apache.kylin.cube.cuboid.Cuboid;
 import org.apache.kylin.cube.cuboid.CuboidCLI;
 import org.apache.kylin.cube.cuboid.CuboidScheduler;
+import org.apache.kylin.cube.cuboid.algorithm.RecommendResult;
 import org.apache.kylin.cube.model.CubeDesc;
 import org.apache.kylin.engine.EngineFactory;
 import org.apache.kylin.engine.mr.CubingJob;
@@ -1019,7 +1020,7 @@ public class CubeService extends BasicService implements 
InitializingBean {
     }
 
     /** cube planner services */
-    public Map<Long, Long> getRecommendCuboidStatistics(CubeInstance cube, 
Map<Long, Long> hitFrequencyMap,
+    public RecommendResult getRecommendCuboidStatistics(CubeInstance cube, 
Map<Long, Long> hitFrequencyMap,
             Map<Long, Map<Long, Pair<Long, Long>>> rollingUpCountSourceMap) 
throws IOException {
         aclEvaluate.checkProjectAdminPermission(cube.getProject());
         return CuboidRecommenderUtil.getRecommendCuboidList(cube, 
hitFrequencyMap, rollingUpCountSourceMap);
diff --git a/webapp/app/js/services/cubes.js b/webapp/app/js/services/cubes.js
index 522d043..13de034 100644
--- a/webapp/app/js/services/cubes.js
+++ b/webapp/app/js/services/cubes.js
@@ -74,7 +74,7 @@ KylinApp.factory('CubeService', ['$resource', function 
($resource, config) {
       isArray: false,
       interceptor: {
         response: function(response) {
-          return transformCuboidsResponse(response.data);
+          return transformCuboidsResponse(response.data.cuboidTree);
         }
       }
     },

Reply via email to