summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Campbell <ryanjcampbell@google.com>2017-07-18 16:53:58 -0700
committerRyan Campbell <ryanjcampbell@google.com>2017-07-20 17:52:23 -0700
commit1b06ae8e8fa9ac62c8751d141aa5fe5e7945205d (patch)
tree76004e9eab68c701aa4bfcd7b0ca211755283851
parent2b532bee21a2b3318435ae55928a769e6f2a3df7 (diff)
downloaddashboard-1b06ae8e8fa9ac62c8751d141aa5fe5e7945205d.tar.gz
Process profiling data in a push task queue.
Instead of querying for profiling data every time a summary should be displayed, we can optimize by creating daily summaries. Multiple summaries must be created so that we can still filter/query. This solution creates an entry for each device build flavor and device branch in the run. E.g. if a test runs against master/device-userdebug, then a summary entry will be updated for master/ALL, master/device-userdebug, ALL/device-userdebug, ALL/ALL. Test: local with VtsHalLightV2_0TargetProfiling Bug: 63775075 Change-Id: Ica1865c7151159d40b3e1bb993374cb5990d09db
-rw-r--r--src/main/java/com/android/vts/entity/ProfilingPointEntity.java137
-rw-r--r--src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java51
-rw-r--r--src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java296
-rw-r--r--src/main/java/com/android/vts/servlet/VtsProfilingStatsJobServlet.java266
-rw-r--r--src/main/java/com/android/vts/util/DatastoreHelper.java13
-rw-r--r--src/main/java/com/android/vts/util/PerformanceUtil.java37
-rw-r--r--src/main/java/com/android/vts/util/StatSummary.java62
-rw-r--r--src/main/webapp/WEB-INF/queue.xml23
-rw-r--r--src/main/webapp/WEB-INF/web.xml10
-rw-r--r--src/test/java/com/android/vts/servlet/VtsProfilingStatsJobServletTest.java383
10 files changed, 1231 insertions, 47 deletions
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointEntity.java
new file mode 100644
index 0000000..cfe4983
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/ProfilingPointEntity.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed 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 com.android.vts.entity;
+
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingType;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing a profiling point. */
+public class ProfilingPointEntity implements DashboardEntity {
+ protected static final Logger logger = Logger.getLogger(ProfilingPointEntity.class.getName());
+ protected static final String DELIMITER = "#";
+
+ public static final String KIND = "ProfilingPoint";
+
+ // Property keys
+ public static final String TEST_NAME = "testName";
+ public static final String PROFILING_POINT_NAME = "profilingPointName";
+ public static final String TYPE = "type";
+ public static final String REGRESSION_MODE = "regressionMode";
+ public static final String X_LABEL = "xLabel";
+ public static final String Y_LABEL = "yLabel";
+
+ public final Key key;
+
+ public final String testName;
+ public final String profilingPointName;
+ public final VtsProfilingType type;
+ public final VtsProfilingRegressionMode regressionMode;
+ public final String xLabel;
+ public final String yLabel;
+
+ /**
+ * Create a ProfilingPointEntity object.
+ *
+ * @param testName The name of test containing the profiling point.
+ * @param profilingPointName The name of the profiling point.
+ * @param type The (number) type of the profiling point data.
+ * @param regressionMode The (number) mode to use for detecting regression.
+ * @param xLabel The x axis label.
+ * @param yLabel The y axis label.
+ */
+ public ProfilingPointEntity(
+ String testName,
+ String profilingPointName,
+ int type,
+ int regressionMode,
+ String xLabel,
+ String yLabel) {
+ this.key = createKey(testName, profilingPointName);
+ this.testName = testName;
+ this.profilingPointName = profilingPointName;
+ this.type = VtsProfilingType.valueOf(type);
+ this.regressionMode = VtsProfilingRegressionMode.valueOf(regressionMode);
+ this.xLabel = xLabel;
+ this.yLabel = yLabel;
+ }
+
+ /**
+ * Create a key for a ProfilingPointEntity.
+ *
+ * @param testName The name of test containing the profiling point.
+ * @param profilingPointName The name of the profiling point.
+ * @return a Key object for the ProfilingEntity in the database.
+ */
+ public static Key createKey(String testName, String profilingPointName) {
+ return KeyFactory.createKey(KIND, testName + DELIMITER + profilingPointName);
+ }
+
+ @Override
+ public Entity toEntity() {
+ Entity profilingPoint = new Entity(key);
+ profilingPoint.setIndexedProperty(TEST_NAME, this.testName);
+ profilingPoint.setIndexedProperty(PROFILING_POINT_NAME, this.profilingPointName);
+ profilingPoint.setUnindexedProperty(TYPE, this.type.getNumber());
+ profilingPoint.setUnindexedProperty(REGRESSION_MODE, this.regressionMode.getNumber());
+ profilingPoint.setUnindexedProperty(X_LABEL, this.xLabel);
+ profilingPoint.setUnindexedProperty(Y_LABEL, this.yLabel);
+
+ return profilingPoint;
+ }
+
+ /**
+ * Convert an Entity object to a ProfilingPointEntity.
+ *
+ * @param e The entity to process.
+ * @return ProfilingPointEntity object with the properties from e, or null if incompatible.
+ */
+ @SuppressWarnings("unchecked")
+ public static ProfilingPointEntity fromEntity(Entity e) {
+ if (!e.getKind().equals(KIND)
+ || e.getKey().getName() == null
+ || !e.hasProperty(TEST_NAME)
+ || !e.hasProperty(PROFILING_POINT_NAME)
+ || !e.hasProperty(TYPE)
+ || !e.hasProperty(REGRESSION_MODE)
+ || !e.hasProperty(X_LABEL)
+ || !e.hasProperty(Y_LABEL)) {
+ logger.log(
+ Level.WARNING, "Missing profiling point attributes in entity: " + e.toString());
+ return null;
+ }
+ try {
+ String testName = (String) e.getProperty(TEST_NAME);
+ String profilingPointName = (String) e.getProperty(PROFILING_POINT_NAME);
+ int type = (int) (long) e.getProperty(TYPE);
+ int regressionMode = (int) (long) e.getProperty(REGRESSION_MODE);
+ String xLabel = (String) e.getProperty(X_LABEL);
+ String yLabel = (String) e.getProperty(Y_LABEL);
+
+ return new ProfilingPointEntity(
+ testName, profilingPointName, type, regressionMode, xLabel, yLabel);
+ } catch (ClassCastException exception) {
+ // Invalid cast
+ logger.log(Level.WARNING, "Error parsing profiling point entity.", exception);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
index fdbbb29..165644f 100644
--- a/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
+++ b/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
@@ -21,11 +21,10 @@ import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
import com.android.vts.proto.VtsReportMessage.VtsProfilingType;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
import com.google.protobuf.ByteString;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -45,7 +44,7 @@ public class ProfilingPointRunEntity implements DashboardEntity {
public static final String Y_LABEL = "yLabel";
public static final String OPTIONS = "options";
- private final Key parentKey;
+ public final Key key;
public final String name;
public final VtsProfilingType type;
@@ -69,10 +68,17 @@ public class ProfilingPointRunEntity implements DashboardEntity {
* @param yLabel The y axis label.
* @param options The list of key=value options for the profiling point run.
*/
- public ProfilingPointRunEntity(Key parentKey, String name, int type, int regressionMode,
- List<String> labels, List<Long> values, String xLabel, String yLabel,
+ public ProfilingPointRunEntity(
+ Key parentKey,
+ String name,
+ int type,
+ int regressionMode,
+ List<String> labels,
+ List<Long> values,
+ String xLabel,
+ String yLabel,
List<String> options) {
- this.parentKey = parentKey;
+ this.key = KeyFactory.createKey(parentKey, KIND, name);
this.name = name;
this.type = VtsProfilingType.valueOf(type);
this.regressionMode = VtsProfilingRegressionMode.valueOf(regressionMode);
@@ -85,7 +91,7 @@ public class ProfilingPointRunEntity implements DashboardEntity {
@Override
public Entity toEntity() {
- Entity profilingRun = new Entity(KIND, this.name, this.parentKey);
+ Entity profilingRun = new Entity(this.key);
profilingRun.setUnindexedProperty(TYPE, this.type.getNumber());
profilingRun.setUnindexedProperty(REGRESSION_MODE, this.regressionMode.getNumber());
if (this.labels != null) {
@@ -109,9 +115,13 @@ public class ProfilingPointRunEntity implements DashboardEntity {
*/
@SuppressWarnings("unchecked")
public static ProfilingPointRunEntity fromEntity(Entity e) {
- if (!e.getKind().equals(KIND) || e.getKey().getName() == null || !e.hasProperty(TYPE)
- || !e.hasProperty(REGRESSION_MODE) || !e.hasProperty(VALUES)
- || !e.hasProperty(X_LABEL) || !e.hasProperty(Y_LABEL)) {
+ if (!e.getKind().equals(KIND)
+ || e.getKey().getName() == null
+ || !e.hasProperty(TYPE)
+ || !e.hasProperty(REGRESSION_MODE)
+ || !e.hasProperty(VALUES)
+ || !e.hasProperty(X_LABEL)
+ || !e.hasProperty(Y_LABEL)) {
logger.log(
Level.WARNING, "Missing profiling point attributes in entity: " + e.toString());
return null;
@@ -150,9 +160,11 @@ public class ProfilingPointRunEntity implements DashboardEntity {
*/
public static ProfilingPointRunEntity fromProfilingReport(
Key parentKey, ProfilingReportMessage profilingReport) {
- if (!profilingReport.hasName() || !profilingReport.hasType()
+ if (!profilingReport.hasName()
+ || !profilingReport.hasType()
|| profilingReport.getType() == VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE
- || !profilingReport.hasRegressionMode() || !profilingReport.hasXAxisLabel()
+ || !profilingReport.hasRegressionMode()
+ || !profilingReport.hasXAxisLabel()
|| !profilingReport.hasYAxisLabel()) {
return null; // invalid profiling report;
}
@@ -165,7 +177,8 @@ public class ProfilingPointRunEntity implements DashboardEntity {
List<String> labels = null;
switch (type) {
case VTS_PROFILING_TYPE_TIMESTAMP:
- if (!profilingReport.hasStartTimestamp() || !profilingReport.hasEndTimestamp()
+ if (!profilingReport.hasStartTimestamp()
+ || !profilingReport.hasEndTimestamp()
|| profilingReport.getEndTimestamp()
< profilingReport.getStartTimestamp()) {
return null; // missing timestamp
@@ -198,7 +211,15 @@ public class ProfilingPointRunEntity implements DashboardEntity {
options.add(optionBytes.toStringUtf8());
}
}
- return new ProfilingPointRunEntity(parentKey, name, type.getNumber(),
- regressionMode.getNumber(), labels, values, xLabel, yLabel, options);
+ return new ProfilingPointRunEntity(
+ parentKey,
+ name,
+ type.getNumber(),
+ regressionMode.getNumber(),
+ labels,
+ values,
+ xLabel,
+ yLabel,
+ options);
}
}
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java
new file mode 100644
index 0000000..e5f46a9
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed 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 com.android.vts.entity;
+
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.android.vts.util.StatSummary;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing a profiling point summary. */
+public class ProfilingPointSummaryEntity implements DashboardEntity {
+ protected static final Logger logger =
+ Logger.getLogger(ProfilingPointSummaryEntity.class.getName());
+ protected static final String DELIMITER = "#";
+
+ public static final String KIND = "ProfilingPointSummary";
+ public static final String ALL = "ALL";
+
+ // Property keys
+ public static final String START_TIME = "startTime";
+ public static final String MEAN = "mean";
+ public static final String SUMSQ = "sumSq";
+ public static final String MIN = "min";
+ public static final String MAX = "max";
+ public static final String LABELS = "labels";
+ public static final String LABEL_MEANS = "labelMeans";
+ public static final String LABEL_SUMSQS = "labelSumSqs";
+ public static final String LABEL_MINS = "labelMins";
+ public static final String LABEL_MAXES = "labelMaxes";
+ public static final String LABEL_COUNTS = "labelCounts";
+ public static final String COUNT = "count";
+ public static final String BRANCH = "branch";
+ public static final String BUILD_FLAVOR = "buildFlavor";
+ public static final String SERIES = "series";
+
+ private final Key key;
+
+ public final StatSummary globalStats;
+ public final List<String> labels;
+ public final Map<String, StatSummary> labelStats;
+ public final String branch;
+ public final String buildFlavor;
+ public final String series;
+ public final long startTime;
+
+ /**
+ * Create a ProfilingPointSummaryEntity object.
+ *
+ * @param parentKey The Key object for the parent TestRunEntity in the database.
+ * @param globalStats The StatSummary object recording global statistics about the profiling
+ * point.
+ * @param labels The list of data labels.
+ * @param labelStats The map from data label to StatSummary object for the label.
+ * @param branch The branch.
+ * @param buildFlavor The device build flavor.
+ * @param series The string describing the profiling point series (e.g. binder or passthrough).
+ * @param startTime The timestamp indicating the beginning of the summary.
+ */
+ public ProfilingPointSummaryEntity(
+ Key parentKey,
+ StatSummary globalStats,
+ List<String> labels,
+ Map<String, StatSummary> labelStats,
+ String branch,
+ String buildFlavor,
+ String series,
+ long startTime) {
+ this.globalStats = globalStats;
+ this.labels = labels;
+ this.labelStats = labelStats;
+ this.buildFlavor = buildFlavor == null ? ALL : buildFlavor;
+ this.branch = branch == null ? ALL : branch;
+ this.series = series == null ? "" : series;
+ this.startTime = startTime;
+ this.key = createKey(parentKey, this.branch, this.buildFlavor, this.series, this.startTime);
+ }
+
+ /**
+ * Create a new ProfilingPointSummaryEntity object.
+ *
+ * @param parentKey The Key object for the parent TestRunEntity in the database.
+ * @param branch The branch.
+ * @param buildFlavor The buildFlavor name.
+ * @param series The string describing the profiling point series (e.g. binder or passthrough).
+ * @param startTime The timestamp indicating the beginning of the summary.
+ */
+ public ProfilingPointSummaryEntity(
+ Key parentKey, String branch, String buildFlavor, String series, long startTime) {
+ this(
+ parentKey,
+ new StatSummary(null, VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE),
+ new ArrayList<>(),
+ new HashMap<>(),
+ branch,
+ buildFlavor,
+ series,
+ startTime);
+ }
+
+ /**
+ * Create a key for a ProfilingPointSummaryEntity.
+ *
+ * @param parentKey The Key object for the parent TestRunEntity in the database.
+ * @param branch The branch.
+ * @param buildFlavor The device build flavor.
+ * @param series The string describing the profiling point series (e.g. binder or passthrough).
+ * @param startTime The timestamp indicating the beginning of the summary.
+ * @return a Key object for the ProfilingPointSummaryEntity in the database.
+ */
+ public static Key createKey(
+ Key parentKey, String branch, String buildFlavor, String series, long startTime) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(branch);
+ sb.append(DELIMITER);
+ sb.append(buildFlavor);
+ sb.append(DELIMITER);
+ sb.append(series);
+ sb.append(DELIMITER);
+ sb.append(startTime);
+ return KeyFactory.createKey(parentKey, KIND, sb.toString());
+ }
+
+ /**
+ * Updates the profiling summary with the data from a new profiling report.
+ *
+ * @param profilingRun The profiling point run entity object containing profiling data.
+ */
+ public void update(ProfilingPointRunEntity profilingRun) {
+ if (profilingRun.labels != null
+ && profilingRun.labels.size() == profilingRun.values.size()) {
+ for (int i = 0; i < profilingRun.labels.size(); i++) {
+ String label = profilingRun.labels.get(i);
+ if (!this.labelStats.containsKey(label)) {
+ StatSummary summary = new StatSummary(label, profilingRun.regressionMode);
+ this.labelStats.put(label, summary);
+ }
+ StatSummary summary = this.labelStats.get(label);
+ summary.updateStats(profilingRun.values.get(i));
+ }
+ this.labels.clear();
+ this.labels.addAll(profilingRun.labels);
+ }
+ for (long value : profilingRun.values) {
+ this.globalStats.updateStats(value);
+ }
+ }
+
+ @Override
+ public Entity toEntity() {
+ Entity profilingSummary;
+ profilingSummary = new Entity(this.key);
+ profilingSummary.setUnindexedProperty(MEAN, this.globalStats.getMean());
+ profilingSummary.setUnindexedProperty(SUMSQ, this.globalStats.getSumSq());
+ profilingSummary.setUnindexedProperty(MIN, this.globalStats.getMin());
+ profilingSummary.setUnindexedProperty(MAX, this.globalStats.getMax());
+ profilingSummary.setUnindexedProperty(COUNT, this.globalStats.getCount());
+ profilingSummary.setIndexedProperty(START_TIME, this.startTime);
+ profilingSummary.setIndexedProperty(BRANCH, this.branch);
+ profilingSummary.setIndexedProperty(BUILD_FLAVOR, this.buildFlavor);
+ profilingSummary.setIndexedProperty(SERIES, this.series);
+ if (this.labels.size() != 0) {
+ List<Double> labelMeans = new ArrayList<>();
+ List<Double> labelSumsqs = new ArrayList<>();
+ List<Double> labelMins = new ArrayList<>();
+ List<Double> labelMaxes = new ArrayList<>();
+ List<Long> labelCounts = new ArrayList<>();
+ for (String label : this.labels) {
+ if (!this.labelStats.containsKey(label)) continue;
+ StatSummary labelStat = this.labelStats.get(label);
+ labelMeans.add(labelStat.getMean());
+ labelSumsqs.add(labelStat.getSumSq());
+ labelMins.add(labelStat.getMin());
+ labelMaxes.add(labelStat.getMax());
+ labelCounts.add(new Long(labelStat.getCount()));
+ }
+ profilingSummary.setUnindexedProperty(LABELS, this.labels);
+ profilingSummary.setUnindexedProperty(LABEL_MEANS, labelMeans);
+ profilingSummary.setUnindexedProperty(LABEL_SUMSQS, labelSumsqs);
+ profilingSummary.setUnindexedProperty(LABEL_MINS, labelMins);
+ profilingSummary.setUnindexedProperty(LABEL_MAXES, labelMaxes);
+ profilingSummary.setUnindexedProperty(LABEL_COUNTS, labelCounts);
+ }
+
+ return profilingSummary;
+ }
+
+ /**
+ * Convert an Entity object to a ProfilingPointSummaryEntity.
+ *
+ * @param e The entity to process.
+ * @return ProfilingPointSummaryEntity object with the properties from e, or null if
+ * incompatible.
+ */
+ @SuppressWarnings("unchecked")
+ public static ProfilingPointSummaryEntity fromEntity(Entity e) {
+ if (!e.getKind().equals(KIND)
+ || !e.hasProperty(MEAN)
+ || !e.hasProperty(SUMSQ)
+ || !e.hasProperty(MIN)
+ || !e.hasProperty(MAX)
+ || !e.hasProperty(COUNT)
+ || !e.hasProperty(START_TIME)
+ || !e.hasProperty(BRANCH)
+ || !e.hasProperty(BUILD_FLAVOR)
+ || !e.hasProperty(SERIES)) {
+ logger.log(
+ Level.WARNING, "Missing profiling point attributes in entity: " + e.toString());
+ return null;
+ }
+ try {
+ Key parentKey = e.getParent();
+ double mean = (double) e.getProperty(MEAN);
+ double sumsq = (double) e.getProperty(SUMSQ);
+ double min = (double) e.getProperty(MIN);
+ double max = (double) e.getProperty(MAX);
+ int count = (int) (long) e.getProperty(COUNT);
+ StatSummary globalStats =
+ new StatSummary(
+ null,
+ min,
+ max,
+ mean,
+ sumsq,
+ count,
+ VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE);
+ Map<String, StatSummary> labelStats = new HashMap<>();
+ List<String> labels = new ArrayList<>();
+ if (e.hasProperty(LABELS)) {
+ labels = (List<String>) e.getProperty(LABELS);
+ List<Double> labelMeans = (List<Double>) e.getProperty(LABEL_MEANS);
+ List<Double> labelSumsqs = (List<Double>) e.getProperty(LABEL_SUMSQS);
+ List<Double> labelMins = (List<Double>) e.getProperty(LABEL_MINS);
+ List<Double> labelMaxes = (List<Double>) e.getProperty(LABEL_MAXES);
+ List<Long> labelCounts = (List<Long>) e.getProperty(LABEL_COUNTS);
+ if (labels.size() != labelMeans.size()
+ || labels.size() != labelSumsqs.size()
+ || labels.size() != labelMins.size()
+ || labels.size() != labelMaxes.size()
+ || labels.size() != labelCounts.size()) {
+ logger.log(Level.WARNING, "Jagged label information for entity: " + e.getKey());
+ return null;
+ }
+ for (int i = 0; i < labels.size(); ++i) {
+ StatSummary labelStat =
+ new StatSummary(
+ labels.get(i),
+ labelMins.get(i),
+ labelMaxes.get(i),
+ labelMeans.get(i),
+ labelSumsqs.get(i),
+ labelCounts.get(i).intValue(),
+ VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE);
+ labelStats.put(labels.get(i), labelStat);
+ }
+ }
+ String branch = (String) e.getProperty(BRANCH);
+ String buildFlavor = (String) e.getProperty(BUILD_FLAVOR);
+ String series = (String) e.getProperty(SERIES);
+ long startTime = (long) e.getProperty(START_TIME);
+ return new ProfilingPointSummaryEntity(
+ parentKey,
+ globalStats,
+ labels,
+ labelStats,
+ branch,
+ buildFlavor,
+ series,
+ startTime);
+ } catch (ClassCastException exception) {
+ // Invalid cast
+ logger.log(Level.WARNING, "Error parsing profiling point summary entity.", exception);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/android/vts/servlet/VtsProfilingStatsJobServlet.java b/src/main/java/com/android/vts/servlet/VtsProfilingStatsJobServlet.java
new file mode 100644
index 0000000..e7c45f3
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/VtsProfilingStatsJobServlet.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed 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 com.android.vts.servlet;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.ProfilingPointSummaryEntity;
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.PerformanceUtil;
+import com.android.vts.util.TaskQueueHelper;
+import com.google.appengine.api.datastore.DatastoreFailureException;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.DatastoreTimeoutException;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Transaction;
+import com.google.appengine.api.taskqueue.Queue;
+import com.google.appengine.api.taskqueue.QueueFactory;
+import com.google.appengine.api.taskqueue.TaskOptions;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Represents the notifications service which is automatically called on a fixed schedule. */
+public class VtsProfilingStatsJobServlet extends HttpServlet {
+ protected static final Logger logger =
+ Logger.getLogger(VtsProfilingStatsJobServlet.class.getName());
+ private static final String HIDL_HAL_OPTION = "hidl_hal_mode";
+ private static final String[] splitKeysArray = new String[] {HIDL_HAL_OPTION};
+ private static final Set<String> splitKeySet = new HashSet<>(Arrays.asList(splitKeysArray));
+
+ public static final String PROFILING_STATS_JOB_URL = "/task/vts_profiling_stats_job";
+ public static final String PROFILING_POINT_KEY = "profilingPointKey";
+ public static final String QUEUE = "profilingStatsQueue";
+ public static final ZoneId PT_ZONE = ZoneId.of("America/Los_Angeles");
+
+ /**
+ * Round the date down to the start of the day (PST).
+ *
+ * @param time The time in microseconds.
+ * @return
+ */
+ public static long getCanonicalTime(long time) {
+ long timeMillis = TimeUnit.MICROSECONDS.toMillis(time);
+ ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timeMillis), PT_ZONE);
+ return TimeUnit.SECONDS.toMicros(
+ zdt.withHour(0).withMinute(0).withSecond(0).toEpochSecond());
+ }
+
+ /**
+ * Add tasks to process profiling run data
+ *
+ * @param profilingPointKeys The list of keys of the profiling point runs whose data process.
+ */
+ public static void addTasks(List<Key> profilingPointKeys) {
+ Queue queue = QueueFactory.getQueue(QUEUE);
+ List<TaskOptions> tasks = new ArrayList<>();
+ for (Key key : profilingPointKeys) {
+ String keyString = KeyFactory.keyToString(key);
+ tasks.add(
+ TaskOptions.Builder.withUrl(PROFILING_STATS_JOB_URL)
+ .param(PROFILING_POINT_KEY, keyString)
+ .method(TaskOptions.Method.POST));
+ }
+ TaskQueueHelper.addToQueue(queue, tasks);
+ }
+
+ /**
+ * Update the profiling summaries with the information from a profiling point run.
+ *
+ * @param testKey The key to the TestEntity whose profiling data to analyze.
+ * @param profilingPointRun The profiling data to analyze.
+ * @param devices The list of devices used in the profiling run.
+ * @param time The canonical timestamp of the summary to update.
+ * @return true if the update succeeds, false otherwise.
+ */
+ public static boolean updateSummaries(
+ Key testKey,
+ ProfilingPointRunEntity profilingPointRun,
+ List<DeviceInfoEntity> devices,
+ long time) {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ Transaction tx = datastore.beginTransaction();
+ try {
+ List<Entity> puts = new ArrayList<>();
+
+ ProfilingPointEntity profilingPoint =
+ new ProfilingPointEntity(
+ testKey.getName(),
+ profilingPointRun.name,
+ profilingPointRun.type.getNumber(),
+ profilingPointRun.regressionMode.getNumber(),
+ profilingPointRun.xLabel,
+ profilingPointRun.yLabel);
+ puts.add(profilingPoint.toEntity());
+
+ String option = PerformanceUtil.getOptionAlias(profilingPointRun, splitKeySet);
+
+ Set<String> branches = new HashSet<>();
+ Set<String> deviceNames = new HashSet<>();
+
+ branches.add(ProfilingPointSummaryEntity.ALL);
+ deviceNames.add(ProfilingPointSummaryEntity.ALL);
+
+ for (DeviceInfoEntity d : devices) {
+ branches.add(d.branch);
+ deviceNames.add(d.buildFlavor);
+ }
+
+ List<Key> summaryGets = new ArrayList<>();
+ for (String branch : branches) {
+ for (String device : deviceNames) {
+ summaryGets.add(
+ ProfilingPointSummaryEntity.createKey(
+ profilingPoint.key, branch, device, option, time));
+ }
+ }
+
+ Map<Key, Entity> summaries = datastore.get(tx, summaryGets);
+ Map<String, Map<String, ProfilingPointSummaryEntity>> summaryMap = new HashMap<>();
+ for (Key key : summaries.keySet()) {
+ Entity e = summaries.get(key);
+ ProfilingPointSummaryEntity profilingPointSummary =
+ ProfilingPointSummaryEntity.fromEntity(e);
+ if (profilingPointSummary == null) {
+ logger.log(Level.WARNING, "Invalid profiling point summary: " + e.getKey());
+ continue;
+ }
+ if (!summaryMap.containsKey(profilingPointSummary.branch)) {
+ summaryMap.put(profilingPointSummary.branch, new HashMap<>());
+ }
+ Map<String, ProfilingPointSummaryEntity> deviceMap =
+ summaryMap.get(profilingPointSummary.branch);
+ deviceMap.put(profilingPointSummary.buildFlavor, profilingPointSummary);
+ }
+
+ Set<ProfilingPointSummaryEntity> modifiedEntities = new HashSet<>();
+
+ for (String branch : branches) {
+ if (!summaryMap.containsKey(branch)) {
+ summaryMap.put(branch, new HashMap<>());
+ }
+ Map<String, ProfilingPointSummaryEntity> deviceMap = summaryMap.get(branch);
+
+ for (String device : deviceNames) {
+ ProfilingPointSummaryEntity summary;
+ if (deviceMap.containsKey(device)) {
+ summary = deviceMap.get(device);
+ } else {
+ summary =
+ new ProfilingPointSummaryEntity(
+ profilingPoint.key, branch, device, option, time);
+ deviceMap.put(device, summary);
+ }
+ summary.update(profilingPointRun);
+ modifiedEntities.add(summary);
+ }
+ }
+
+ for (ProfilingPointSummaryEntity profilingPointSummary : modifiedEntities) {
+ puts.add(profilingPointSummary.toEntity());
+ }
+ datastore.put(tx, puts);
+ tx.commit();
+ } catch (ConcurrentModificationException
+ | DatastoreFailureException
+ | DatastoreTimeoutException e) {
+ return false;
+ } finally {
+ if (tx.isActive()) {
+ tx.rollback();
+ logger.log(
+ Level.WARNING,
+ "Profiling stats job transaction still active: " + profilingPointRun.key);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ String profilingPointKeyString = request.getParameter(PROFILING_POINT_KEY);
+
+ Key profilingPointRunKey;
+ try {
+ profilingPointRunKey = KeyFactory.stringToKey(profilingPointKeyString);
+ } catch (IllegalArgumentException e) {
+ logger.log(Level.WARNING, "Invalid key specified: " + profilingPointKeyString);
+ return;
+ }
+ Key testKey = profilingPointRunKey.getParent().getParent();
+
+ ProfilingPointRunEntity profilingPointRun = null;
+ try {
+ Entity profilingPointRunEntity = datastore.get(profilingPointRunKey);
+ profilingPointRun = ProfilingPointRunEntity.fromEntity(profilingPointRunEntity);
+ } catch (EntityNotFoundException e) {
+ // no run found
+ }
+ if (profilingPointRun == null) {
+ return;
+ }
+
+ Query deviceQuery =
+ new Query(DeviceInfoEntity.KIND).setAncestor(profilingPointRunKey.getParent());
+
+ List<DeviceInfoEntity> devices = new ArrayList<>();
+ for (Entity e : datastore.prepare(deviceQuery).asIterable()) {
+ DeviceInfoEntity deviceInfoEntity = DeviceInfoEntity.fromEntity(e);
+ if (e == null) continue;
+ devices.add(deviceInfoEntity);
+ }
+
+ long canonicalTime = getCanonicalTime(profilingPointRunKey.getParent().getId());
+ int retryCount = 0;
+ while (retryCount++ <= DatastoreHelper.MAX_WRITE_RETRIES) {
+ boolean result = updateSummaries(testKey, profilingPointRun, devices, canonicalTime);
+ if (!result) {
+ logger.log(
+ Level.WARNING, "Retrying profiling stats update: " + profilingPointRunKey);
+ continue;
+ }
+ break;
+ }
+ if (retryCount > DatastoreHelper.MAX_WRITE_RETRIES) {
+ logger.log(Level.SEVERE, "Could not update profiling stats: " + profilingPointRunKey);
+ }
+ }
+}
diff --git a/src/main/java/com/android/vts/util/DatastoreHelper.java b/src/main/java/com/android/vts/util/DatastoreHelper.java
index e38555b..d4a849a 100644
--- a/src/main/java/com/android/vts/util/DatastoreHelper.java
+++ b/src/main/java/com/android/vts/util/DatastoreHelper.java
@@ -32,6 +32,7 @@ import com.android.vts.proto.VtsReportMessage.TestPlanReportMessage;
import com.android.vts.proto.VtsReportMessage.TestReportMessage;
import com.android.vts.servlet.VtsAlertJobServlet;
import com.android.vts.servlet.VtsCoverageAlertJobServlet;
+import com.android.vts.servlet.VtsProfilingStatsJobServlet;
import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
@@ -206,7 +207,7 @@ public class DatastoreHelper {
Key testRunKey =
KeyFactory.createKey(
- testEntity.key, TestRunEntity.KIND, report.getStartTimestamp());
+ testEntity.key, TestRunEntity.KIND, report.getStartTimestamp());
long passCount = 0;
long failCount = 0;
@@ -214,6 +215,7 @@ public class DatastoreHelper {
long totalLineCount = 0;
List<TestCaseRunEntity> testCases = new ArrayList<>();
+ List<Key> profilingPointKeys = new ArrayList<>();
// Process test cases
for (TestCaseReportMessage testCase : report.getTestCaseList()) {
@@ -250,6 +252,7 @@ public class DatastoreHelper {
logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey);
}
puts.add(profilingEntity.toEntity());
+ profilingPointKeys.add(profilingEntity.key);
testEntity.setHasProfilingData(true);
}
@@ -322,6 +325,7 @@ public class DatastoreHelper {
logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey);
}
puts.add(profilingEntity.toEntity());
+ profilingPointKeys.add(profilingEntity.key);
testEntity.setHasProfilingData(true);
}
@@ -372,6 +376,9 @@ public class DatastoreHelper {
if (testRunEntity.hasCoverage) {
VtsCoverageAlertJobServlet.addTask(testRunKey);
}
+ if (profilingPointKeys.size() > 0) {
+ VtsProfilingStatsJobServlet.addTasks(profilingPointKeys);
+ }
}
break;
} catch (ConcurrentModificationException
@@ -380,9 +387,7 @@ public class DatastoreHelper {
puts.remove(test);
logger.log(Level.WARNING, "Retrying test run insert: " + test.getKey());
if (retries++ >= MAX_WRITE_RETRIES) {
- logger.log(
- Level.SEVERE,
- "Exceeded maximum test run retries: " + test.getKey());
+ logger.log(Level.SEVERE, "Exceeded maximum test run retries: " + test.getKey());
throw e;
}
} finally {
diff --git a/src/main/java/com/android/vts/util/PerformanceUtil.java b/src/main/java/com/android/vts/util/PerformanceUtil.java
index 236dfa2..f32f2ac 100644
--- a/src/main/java/com/android/vts/util/PerformanceUtil.java
+++ b/src/main/java/com/android/vts/util/PerformanceUtil.java
@@ -311,22 +311,35 @@ public class PerformanceUtil {
public static String getOptionAlias(
ProfilingPointRunEntity profilingRun, Set<String> optionKeys) {
String name = "";
- List<String> nameSuffixes = new ArrayList<>();
if (profilingRun.options != null) {
- for (String optionString : profilingRun.options) {
- String[] optionParts = optionString.split(OPTION_DELIMITER);
- if (optionParts.length != 2) {
- continue;
- }
- if (optionKeys.contains(optionParts[0].trim().toLowerCase())) {
- nameSuffixes.add(optionParts[1].trim().toLowerCase());
- }
+ name = getOptionAlias(profilingRun.options, optionKeys);
+ }
+ return name;
+ }
+
+ /**
+ * Generates a string of the values in optionsList which have matches in the profiling entity.
+ *
+ * @param optionList The list of key=value option pair strings.
+ * @param optionKeys A list of keys to match against the optionsList key value pairs.
+ * @return The values in optionsList whose key match a key in optionKeys.
+ */
+ public static String getOptionAlias(List<String> optionList, Set<String> optionKeys) {
+ String name = "";
+ List<String> nameSuffixes = new ArrayList<>();
+ for (String optionString : optionList) {
+ String[] optionParts = optionString.split(OPTION_DELIMITER);
+ if (optionParts.length != 2) {
+ continue;
}
- if (nameSuffixes.size() > 0) {
- StringUtils.join(nameSuffixes, NAME_DELIMITER);
- name += StringUtils.join(nameSuffixes, NAME_DELIMITER);
+ if (optionKeys.contains(optionParts[0].trim().toLowerCase())) {
+ nameSuffixes.add(optionParts[1].trim().toLowerCase());
}
}
+ if (nameSuffixes.size() > 0) {
+ StringUtils.join(nameSuffixes, NAME_DELIMITER);
+ name += StringUtils.join(nameSuffixes, NAME_DELIMITER);
+ }
return name;
}
}
diff --git a/src/main/java/com/android/vts/util/StatSummary.java b/src/main/java/com/android/vts/util/StatSummary.java
index 04db7c4..d3afe31 100644
--- a/src/main/java/com/android/vts/util/StatSummary.java
+++ b/src/main/java/com/android/vts/util/StatSummary.java
@@ -24,28 +24,51 @@ public class StatSummary {
private double min;
private double max;
private double mean;
- private double var;
+ private double sumSq;
private int n;
private VtsProfilingRegressionMode regression_mode;
/**
+ * Create a statistical summary.
+ *
+ * <p>Sets the label, min, max, mean, sum of squared error, n, and mode as provided.
+ *
+ * @param label The (String) label to assign to the summary.
+ * @param min The minimum observed value.
+ * @param max The maximum observed value.
+ * @param mean The average observed value.
+ * @param sumSq The sum of squared error.
+ * @param n The number of values observed.
+ * @param mode The VtsProfilingRegressionMode to use when analyzing performance.
+ */
+ public StatSummary(
+ String label,
+ double min,
+ double max,
+ double mean,
+ double sumSq,
+ int n,
+ VtsProfilingRegressionMode mode) {
+ this.label = label;
+ this.min = min;
+ this.max = max;
+ this.mean = mean;
+ this.sumSq = sumSq;
+ this.n = n;
+ this.regression_mode = mode;
+ }
+
+ /**
* Initializes the statistical summary.
*
* <p>Sets the label as provided. Initializes the mean, variance, and n (number of values seen)
- * to
- * 0.
+ * to 0.
*
* @param label The (String) label to assign to the summary.
* @param mode The VtsProfilingRegressionMode to use when analyzing performance.
*/
public StatSummary(String label, VtsProfilingRegressionMode mode) {
- this.label = label;
- this.min = Double.MAX_VALUE;
- this.max = Double.MIN_VALUE;
- this.mean = 0;
- this.var = 0;
- this.n = 0;
- this.regression_mode = mode;
+ this(label, Double.MAX_VALUE, Double.MIN_VALUE, 0, 0, 0, mode);
}
/**
@@ -57,11 +80,9 @@ public class StatSummary {
n += 1;
double oldMean = mean;
mean = oldMean + (value - oldMean) / n;
- var = var + (value - mean) * (value - oldMean);
- if (value < min)
- min = value;
- if (value > max)
- max = value;
+ sumSq = sumSq + (value - mean) * (value - oldMean);
+ if (value < min) min = value;
+ if (value > max) max = value;
}
/**
@@ -106,12 +127,21 @@ public class StatSummary {
}
/**
+ * Gets the calculated sum of squared error of the stream.
+ *
+ * @return The sum of squared error.
+ */
+ public double getSumSq() {
+ return sumSq;
+ }
+
+ /**
* Gets the calculated standard deviation of the stream.
*
* @return The standard deviation.
*/
public double getStd() {
- return Math.sqrt(var / (n - 1));
+ return Math.sqrt(sumSq / (n - 1));
}
/**
diff --git a/src/main/webapp/WEB-INF/queue.xml b/src/main/webapp/WEB-INF/queue.xml
new file mode 100644
index 0000000..0fccfeb
--- /dev/null
+++ b/src/main/webapp/WEB-INF/queue.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+ Licensed 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.
+-->
+<queue-entries>
+ <queue>
+ <name>profilingStatsQueue</name>
+ <rate>200/s</rate>
+ <bucket-size>40</bucket-size>
+ </queue>
+</queue-entries> \ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index 0dfec12..853d316 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -106,6 +106,11 @@ Copyright 2016 Google Inc. All Rights Reserved.
</servlet>
<servlet>
+ <servlet-name>vts_profiling_stats_job</servlet-name>
+ <servlet-class>com.android.vts.servlet.VtsProfilingStatsJobServlet</servlet-class>
+</servlet>
+
+<servlet>
<servlet-name>vts_coverage_job</servlet-name>
<servlet-class>com.android.vts.servlet.VtsCoverageAlertJobServlet</servlet-class>
</servlet>
@@ -206,6 +211,11 @@ Copyright 2016 Google Inc. All Rights Reserved.
</servlet-mapping>
<servlet-mapping>
+ <servlet-name>vts_profiling_stats_job</servlet-name>
+ <url-pattern>/task/vts_profiling_stats_job/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
<servlet-name>vts_performance_job</servlet-name>
<url-pattern>/cron/vts_performance_job/*</url-pattern>
</servlet-mapping>
diff --git a/src/test/java/com/android/vts/servlet/VtsProfilingStatsJobServletTest.java b/src/test/java/com/android/vts/servlet/VtsProfilingStatsJobServletTest.java
new file mode 100644
index 0000000..062d7e4
--- /dev/null
+++ b/src/test/java/com/android/vts/servlet/VtsProfilingStatsJobServletTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed 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 com.android.vts.servlet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.ProfilingPointSummaryEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage;
+import com.android.vts.util.StatSummary;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.taskqueue.dev.LocalTaskQueue;
+import com.google.appengine.api.taskqueue.dev.QueueStateInfo;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.math3.stat.descriptive.moment.Mean;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class VtsProfilingStatsJobServletTest {
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ new LocalDatastoreServiceTestConfig(),
+ new LocalTaskQueueTestConfig()
+ .setQueueXmlPath("src/main/webapp/WEB-INF/queue.xml"));
+ private static final double THRESHOLD = 1e-10;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ private static void createProfilingRun() {
+ Date d = new Date();
+ long time = TimeUnit.MILLISECONDS.toMicros(d.getTime());
+ long canonicalTime = VtsProfilingStatsJobServlet.getCanonicalTime(time);
+ String test = "test";
+ String profilingPointName = "profilingPoint";
+ String xLabel = "xLabel";
+ String yLabel = "yLabel";
+ VtsReportMessage.VtsProfilingType type =
+ VtsReportMessage.VtsProfilingType.VTS_PROFILING_TYPE_UNLABELED_VECTOR;
+ VtsReportMessage.VtsProfilingRegressionMode mode =
+ VtsReportMessage.VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+
+ Key testKey = KeyFactory.createKey(TestEntity.KIND, test);
+ Key testRunKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, time);
+ Long[] valueArray = new Long[] {1l, 2l, 3l, 4l, 5l};
+ StatSummary stats =
+ new StatSummary(
+ "expected",
+ VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE);
+ for (long value : valueArray) {
+ stats.updateStats(value);
+ }
+ Mean mean = new Mean();
+ List<Long> values = Arrays.asList(valueArray);
+ ProfilingPointRunEntity profilingPointRunEntity =
+ new ProfilingPointRunEntity(
+ testRunKey,
+ profilingPointName,
+ type.getNumber(),
+ mode.getNumber(),
+ null,
+ values,
+ xLabel,
+ yLabel,
+ null);
+
+ String branch = "master";
+ String product = "product";
+ String flavor = "flavor";
+ String id = "12345";
+ String bitness = "64";
+ String abiName = "abi";
+ DeviceInfoEntity device =
+ new DeviceInfoEntity(testRunKey, branch, product, flavor, id, bitness, abiName);
+ }
+
+ /**
+ * Test that tasks are correctly scheduled on the queue.
+ *
+ * @throws InterruptedException
+ */
+ @Test
+ public void testTasksScheduled() throws InterruptedException {
+ String[] testNames = new String[] {"test1", "test2", "test3"};
+ List<Key> testKeys = new ArrayList();
+ Set<Key> testKeySet = new HashSet<>();
+ String kind = "TEST";
+ for (String testName : testNames) {
+ Key key = KeyFactory.createKey(kind, testName);
+ testKeys.add(key);
+ testKeySet.add(key);
+ }
+ VtsProfilingStatsJobServlet.addTasks(testKeys);
+ Thread.sleep(1000); // wait one second (tasks are scheduled asychronously), must wait.
+ LocalTaskQueue taskQueue = LocalTaskQueueTestConfig.getLocalTaskQueue();
+ QueueStateInfo qsi = taskQueue.getQueueStateInfo().get(VtsProfilingStatsJobServlet.QUEUE);
+ assertNotNull(qsi);
+ assertEquals(testNames.length, qsi.getTaskInfo().size());
+
+ int i = 0;
+ for (QueueStateInfo.TaskStateInfo taskStateInfo : qsi.getTaskInfo()) {
+ assertEquals(
+ VtsProfilingStatsJobServlet.PROFILING_STATS_JOB_URL, taskStateInfo.getUrl());
+ assertEquals("POST", taskStateInfo.getMethod());
+ String body = taskStateInfo.getBody();
+ String[] parts = body.split("=");
+ assertEquals(2, parts.length);
+ assertEquals(VtsProfilingStatsJobServlet.PROFILING_POINT_KEY, parts[0]);
+ String keyString = parts[1];
+ Key profilingPointRunKey;
+ try {
+ profilingPointRunKey = KeyFactory.stringToKey(keyString);
+ } catch (IllegalArgumentException e) {
+ fail();
+ return;
+ }
+ assertTrue(testKeys.contains(profilingPointRunKey));
+ }
+ }
+
+ /** Test that canonical time is correctly derived from a timestamp in the middle of the day. */
+ @Test
+ public void testCanonicalTimeMidday() {
+ int year = 2017;
+ Month month = Month.MAY;
+ int day = 28;
+ int hour = 14;
+ int minute = 30;
+ LocalDateTime now = LocalDateTime.of(year, month.getValue(), day, hour, minute);
+ ZonedDateTime zdt = ZonedDateTime.of(now, VtsProfilingStatsJobServlet.PT_ZONE);
+ long time = TimeUnit.SECONDS.toMicros(zdt.toEpochSecond());
+ long canonicalTime = VtsProfilingStatsJobServlet.getCanonicalTime(time);
+ long canonicalTimeSec = TimeUnit.MICROSECONDS.toSeconds(canonicalTime);
+ ZonedDateTime canonical =
+ ZonedDateTime.ofInstant(
+ Instant.ofEpochSecond(canonicalTimeSec),
+ VtsProfilingStatsJobServlet.PT_ZONE);
+ assertEquals(month, canonical.getMonth());
+ assertEquals(day, canonical.getDayOfMonth());
+ assertEquals(0, canonical.getHour());
+ assertEquals(0, canonical.getMinute());
+ }
+
+ /** Test that canonical time is correctly derived at the boundary of two days (midnight). */
+ @Test
+ public void testCanonicalTimeMidnight() {
+ int year = 2017;
+ Month month = Month.MAY;
+ int day = 28;
+ int hour = 0;
+ int minute = 0;
+ LocalDateTime now = LocalDateTime.of(year, month.getValue(), day, hour, minute);
+ ZonedDateTime zdt = ZonedDateTime.of(now, VtsProfilingStatsJobServlet.PT_ZONE);
+ long time = TimeUnit.SECONDS.toMicros(zdt.toEpochSecond());
+ long canonicalTime = VtsProfilingStatsJobServlet.getCanonicalTime(time);
+ long canonicalTimeSec = TimeUnit.MICROSECONDS.toSeconds(canonicalTime);
+ ZonedDateTime canonical =
+ ZonedDateTime.ofInstant(
+ Instant.ofEpochSecond(canonicalTimeSec),
+ VtsProfilingStatsJobServlet.PT_ZONE);
+ assertEquals(zdt, canonical);
+ }
+
+ /** Test that new summaries are created with a clean database. */
+ @Test
+ public void testNewSummary() {
+ Date d = new Date();
+ long time = TimeUnit.MILLISECONDS.toMicros(d.getTime());
+ String test = "test";
+
+ Key testKey = KeyFactory.createKey(TestEntity.KIND, test);
+ Key testRunKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, time);
+ Long[] valueArray = new Long[] {1l, 2l, 3l, 4l, 5l};
+ StatSummary expected =
+ new StatSummary(
+ "expected",
+ VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE);
+ for (long value : valueArray) {
+ expected.updateStats(value);
+ }
+ Mean mean = new Mean();
+ List<Long> values = Arrays.asList(valueArray);
+ ProfilingPointRunEntity profilingPointRunEntity =
+ new ProfilingPointRunEntity(
+ testRunKey,
+ "profilingPoint",
+ VtsReportMessage.VtsProfilingType.VTS_PROFILING_TYPE_UNLABELED_VECTOR_VALUE,
+ VtsReportMessage.VtsProfilingRegressionMode
+ .VTS_REGRESSION_MODE_INCREASING_VALUE,
+ null,
+ values,
+ "xLabel",
+ "yLabel",
+ null);
+
+ DeviceInfoEntity device =
+ new DeviceInfoEntity(
+ testRunKey, "master", "product", "flavor", "12345", "64", "abi");
+
+ List<DeviceInfoEntity> devices = new ArrayList<>();
+ devices.add(device);
+
+ boolean result =
+ VtsProfilingStatsJobServlet.updateSummaries(
+ testKey, profilingPointRunEntity, devices, time);
+ assertTrue(result);
+
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ // Check profiling point entity
+ Key profilingPointKey = ProfilingPointEntity.createKey(test, profilingPointRunEntity.name);
+ ProfilingPointEntity profilingPointEntity = null;
+ try {
+ Entity profilingPoint = datastore.get(profilingPointKey);
+ profilingPointEntity = ProfilingPointEntity.fromEntity(profilingPoint);
+ } catch (EntityNotFoundException exception) {
+ fail();
+ }
+ assertNotNull(profilingPointEntity);
+ assertEquals(profilingPointRunEntity.name, profilingPointEntity.profilingPointName);
+ assertEquals(profilingPointRunEntity.xLabel, profilingPointEntity.xLabel);
+ assertEquals(profilingPointRunEntity.yLabel, profilingPointEntity.yLabel);
+ assertEquals(profilingPointRunEntity.type, profilingPointEntity.type);
+ assertEquals(profilingPointRunEntity.regressionMode, profilingPointEntity.regressionMode);
+
+ // Check all summary entities
+ Query q = new Query(ProfilingPointSummaryEntity.KIND).setAncestor(profilingPointKey);
+ for (Entity e : datastore.prepare(q).asIterable()) {
+ ProfilingPointSummaryEntity pps = ProfilingPointSummaryEntity.fromEntity(e);
+ assertNotNull(pps);
+ assertTrue(
+ pps.branch.equals(device.branch)
+ || pps.branch.equals(ProfilingPointSummaryEntity.ALL));
+ assertTrue(
+ pps.buildFlavor.equals(ProfilingPointSummaryEntity.ALL)
+ || pps.buildFlavor.equals(device.buildFlavor));
+ assertEquals(expected.getCount(), pps.globalStats.getCount());
+ assertEquals(expected.getMax(), pps.globalStats.getMax(), THRESHOLD);
+ assertEquals(expected.getMin(), pps.globalStats.getMin(), THRESHOLD);
+ assertEquals(expected.getMean(), pps.globalStats.getMean(), THRESHOLD);
+ assertEquals(expected.getSumSq(), pps.globalStats.getSumSq(), THRESHOLD);
+ }
+ }
+
+ /** Test that existing summaries are updated correctly when a job pushes new profiling data. */
+ @Test
+ public void testUpdateSummary() {
+ Date d = new Date();
+ long time = TimeUnit.MILLISECONDS.toMicros(d.getTime());
+ String test = "test2";
+
+ Key testKey = KeyFactory.createKey(TestEntity.KIND, test);
+ Key testRunKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, time);
+ Long[] valueArray = new Long[] {0l};
+ List<Long> values = Arrays.asList(valueArray);
+
+ // Create a new profiling point run
+ ProfilingPointRunEntity profilingPointRunEntity =
+ new ProfilingPointRunEntity(
+ testRunKey,
+ "profilingPoint2",
+ VtsReportMessage.VtsProfilingType.VTS_PROFILING_TYPE_UNLABELED_VECTOR_VALUE,
+ VtsReportMessage.VtsProfilingRegressionMode
+ .VTS_REGRESSION_MODE_INCREASING_VALUE,
+ null,
+ values,
+ "xLabel",
+ "yLabel",
+ null);
+
+ // Create a device for the run
+ String series = "";
+ DeviceInfoEntity device =
+ new DeviceInfoEntity(
+ testRunKey, "master", "product", "flavor", "12345", "64", "abi");
+
+ List<DeviceInfoEntity> devices = new ArrayList<>();
+ devices.add(device);
+
+ // Create the existing stats
+ Key profilingPointKey = ProfilingPointEntity.createKey(test, profilingPointRunEntity.name);
+ StatSummary expected =
+ new StatSummary(
+ "label",
+ 0,
+ 10,
+ 5,
+ 100,
+ 10,
+ VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE);
+ ProfilingPointSummaryEntity summary =
+ new ProfilingPointSummaryEntity(
+ profilingPointKey,
+ expected,
+ new ArrayList<>(),
+ new HashMap<>(),
+ device.branch,
+ device.buildFlavor,
+ series,
+ time);
+
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ datastore.put(summary.toEntity());
+
+ // Update the summaries in the database
+ boolean result =
+ VtsProfilingStatsJobServlet.updateSummaries(
+ testKey, profilingPointRunEntity, devices, time);
+ assertTrue(result);
+
+ // Calculate the expected stats with the values from the new run
+ for (long value : values) expected.updateStats(value);
+
+ // Get the summary and check the values match what is expected
+ Key summaryKey =
+ ProfilingPointSummaryEntity.createKey(
+ profilingPointKey, device.branch, device.buildFlavor, series, time);
+ ProfilingPointSummaryEntity pps = null;
+ try {
+ Entity e = datastore.get(summaryKey);
+ pps = ProfilingPointSummaryEntity.fromEntity(e);
+ } catch (EntityNotFoundException e) {
+ fail();
+ }
+ assertNotNull(pps);
+ assertTrue(pps.branch.equals(device.branch));
+ assertTrue(pps.buildFlavor.equals(device.buildFlavor));
+ assertEquals(expected.getCount(), pps.globalStats.getCount());
+ assertEquals(expected.getMax(), pps.globalStats.getMax(), THRESHOLD);
+ assertEquals(expected.getMin(), pps.globalStats.getMin(), THRESHOLD);
+ assertEquals(expected.getMean(), pps.globalStats.getMean(), THRESHOLD);
+ assertEquals(expected.getSumSq(), pps.globalStats.getSumSq(), THRESHOLD);
+ }
+}