diff options
author | Ryan Campbell <ryanjcampbell@google.com> | 2017-07-18 16:53:58 -0700 |
---|---|---|
committer | Ryan Campbell <ryanjcampbell@google.com> | 2017-07-20 17:52:23 -0700 |
commit | 1b06ae8e8fa9ac62c8751d141aa5fe5e7945205d (patch) | |
tree | 76004e9eab68c701aa4bfcd7b0ca211755283851 | |
parent | 2b532bee21a2b3318435ae55928a769e6f2a3df7 (diff) | |
download | dashboard-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
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); + } +} |