blob: ebd529452b4524d53c85ed9e35d1e96ce0b11fd7 [file] [log] [blame]
/*
* Copyright (c) 2016 Google Inc. All Rights Reserved.
*
* 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.job;
import com.android.vts.entity.TestEntity;
import com.android.vts.util.EmailHelper;
import com.android.vts.util.PerformanceSummary;
import com.android.vts.util.PerformanceUtil;
import com.android.vts.util.ProfilingPointSummary;
import com.android.vts.util.StatSummary;
import com.android.vts.util.TaskQueueHelper;
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.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
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.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
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 VtsPerformanceJobServlet extends BaseJobServlet {
protected static final Logger logger =
Logger.getLogger(VtsPerformanceJobServlet.class.getName());
private static final String PERFORMANCE_JOB_URL = "/cron/vts_performance_job";
private static final String MEAN = "Mean";
private static final String MAX = "Max";
private static final String MIN = "Min";
private static final String MIN_DELTA = "ΔMin (%)";
private static final String MAX_DELTA = "ΔMax (%)";
private static final String HIGHER_IS_BETTER =
"Note: Higher values are better. Maximum is the best-case performance.";
private static final String LOWER_IS_BETTER =
"Note: Lower values are better. Minimum is the best-case performance.";
private static final String STD = "Std";
private static final String SUBJECT_PREFIX = "Daily Performance Digest: ";
private static final String LAST_WEEK = "Last Week";
private static final String LABEL_STYLE = "font-family: arial";
private static final String SUBTEXT_STYLE = "font-family: arial; font-size: 12px";
private static final String TABLE_STYLE =
"width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;";
private static final String SECTION_LABEL_STYLE =
"border: 1px solid black; border-bottom: none; background-color: lightgray;";
private static final String COL_LABEL_STYLE =
"border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;";
private static final String HEADER_COL_STYLE =
"border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;";
private static final String INNER_CELL_STYLE =
"border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;";
private static final String OUTER_CELL_STYLE =
"border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;";
private static final DecimalFormat FORMATTER;
/** Initialize the decimal formatter. */
static {
FORMATTER = new DecimalFormat("#.##");
FORMATTER.setRoundingMode(RoundingMode.HALF_UP);
}
/**
* Generates an HTML summary of the performance changes for the profiling results in the
* specified table.
*
* <p>Retrieves the past 24 hours of profiling data and compares it to the 24 hours that
* preceded it. Creates a table representation of the mean and standard deviation for each
* profiling point. When performance degrades, the cell is shaded red.
*
* @param testName The name of the test whose profiling data to summarize.
* @param perfSummaries List of PerformanceSummary objects for each profiling run (in reverse
* chronological order).
* @param labels List of string labels for use as the column headers.
* @returns An HTML string containing labeled table summaries.
*/
public static String getPerformanceSummary(
String testName, List<PerformanceSummary> perfSummaries, List<String> labels) {
if (perfSummaries.size() == 0) return "";
PerformanceSummary now = perfSummaries.get(0);
String tableHTML = "<p style='" + LABEL_STYLE + "'><b>";
tableHTML += testName + "</b></p>";
for (String profilingPoint : now.getProfilingPointNames()) {
ProfilingPointSummary summary = now.getProfilingPointSummary(profilingPoint);
tableHTML += "<table cellpadding='2' style='" + TABLE_STYLE + "'>";
// Format header rows
String[] headerRows = new String[] {profilingPoint, summary.yLabel};
int colspan = labels.size() * 4;
for (String content : headerRows) {
tableHTML += "<tr><td colspan='" + colspan + "'>" + content + "</td></tr>";
}
// Format section labels
tableHTML += "<tr>";
for (int i = 0; i < labels.size(); i++) {
String content = labels.get(i);
tableHTML += "<th style='" + SECTION_LABEL_STYLE + "' ";
if (i == 0) tableHTML += "colspan='1'";
else if (i == 1) tableHTML += "colspan='3'";
else tableHTML += "colspan='4'";
tableHTML += ">" + content + "</th>";
}
tableHTML += "</tr>";
String deltaString;
String bestCaseString;
String subtext;
switch (now.getProfilingPointSummary(profilingPoint).getRegressionMode()) {
case VTS_REGRESSION_MODE_DECREASING:
deltaString = MAX_DELTA;
bestCaseString = MAX;
subtext = HIGHER_IS_BETTER;
break;
default:
deltaString = MIN_DELTA;
bestCaseString = MIN;
subtext = LOWER_IS_BETTER;
break;
}
// Format column labels
tableHTML += "<tr>";
for (int i = 0; i < labels.size(); i++) {
if (i > 1) {
tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + deltaString + "</th>";
}
if (i == 0) {
tableHTML += "<th style='" + COL_LABEL_STYLE + "'>";
tableHTML += summary.xLabel + "</th>";
} else if (i > 0) {
tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + bestCaseString + "</th>";
tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + MEAN + "</th>";
tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + STD + "</th>";
}
}
tableHTML += "</tr>";
// Populate data cells
for (StatSummary stats : summary) {
String label = stats.getLabel();
tableHTML += "<tr><td style='" + HEADER_COL_STYLE + "'>" + label;
tableHTML += "</td><td style='" + INNER_CELL_STYLE + "'>";
tableHTML += FORMATTER.format(stats.getBestCase()) + "</td>";
tableHTML += "<td style='" + INNER_CELL_STYLE + "'>";
tableHTML += FORMATTER.format(stats.getMean()) + "</td>";
tableHTML += "<td style='" + OUTER_CELL_STYLE + "'>";
if (stats.getCount() < 2) {
tableHTML += " - </td>";
} else {
tableHTML += FORMATTER.format(stats.getStd()) + "</td>";
}
for (int i = 1; i < perfSummaries.size(); i++) {
PerformanceSummary oldPerfSummary = perfSummaries.get(i);
if (oldPerfSummary.hasProfilingPoint(profilingPoint)) {
StatSummary baseline =
oldPerfSummary
.getProfilingPointSummary(profilingPoint)
.getStatSummary(label);
tableHTML +=
PerformanceUtil.getBestCasePerformanceComparisonHTML(
baseline,
stats,
"",
"",
INNER_CELL_STYLE,
OUTER_CELL_STYLE);
} else tableHTML += "<td></td><td></td><td></td><td></td>";
}
tableHTML += "</tr>";
}
tableHTML += "</table>";
tableHTML += "<i style='" + SUBTEXT_STYLE + "'>" + subtext + "</i><br><br>";
}
return tableHTML;
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Query q = new Query(TestEntity.KIND).setKeysOnly();
List<TaskOptions> tasks = new ArrayList<>();
for (Entity test : datastore.prepare(q).asIterable()) {
if (test.getKey().getName() == null) {
continue;
}
TaskOptions task =
TaskOptions.Builder.withUrl(PERFORMANCE_JOB_URL)
.param("testKey", KeyFactory.keyToString(test.getKey()))
.method(TaskOptions.Method.POST);
tasks.add(task);
}
TaskQueueHelper.addToQueue(queue, tasks);
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String testKeyString = request.getParameter("testKey");
Key testKey;
try {
testKey = KeyFactory.stringToKey(testKeyString);
} catch (IllegalArgumentException e) {
logger.log(Level.WARNING, "Invalid key specified: " + testKeyString);
return;
}
long nowMicro = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
// Add today to the list of time intervals to analyze
List<PerformanceSummary> summaries = new ArrayList<>();
PerformanceSummary today =
new PerformanceSummary(nowMicro - TimeUnit.DAYS.toMicros(1), nowMicro);
summaries.add(today);
// Add yesterday as a baseline time interval for analysis
long oneDayAgo = nowMicro - TimeUnit.DAYS.toMicros(1);
PerformanceSummary yesterday =
new PerformanceSummary(oneDayAgo - TimeUnit.DAYS.toMicros(1), oneDayAgo);
summaries.add(yesterday);
// Add last week as a baseline time interval for analysis
long oneWeek = TimeUnit.DAYS.toMicros(7);
long oneWeekAgo = nowMicro - oneWeek;
String spanString = "<span class='date-label'>";
String label =
spanString + TimeUnit.MICROSECONDS.toMillis(oneWeekAgo - oneWeek) + "</span>";
label += " - " + spanString + TimeUnit.MICROSECONDS.toMillis(oneWeekAgo) + "</span>";
PerformanceSummary lastWeek =
new PerformanceSummary(oneWeekAgo - oneWeek, oneWeekAgo, label);
summaries.add(lastWeek);
PerformanceUtil.updatePerformanceSummary(
testKey.getName(), oneWeekAgo - oneWeek, nowMicro, null, summaries);
List<PerformanceSummary> nonEmptySummaries = new ArrayList<>();
List<String> labels = new ArrayList<>();
labels.add("");
for (PerformanceSummary perfSummary : summaries) {
if (perfSummary.size() == 0) continue;
nonEmptySummaries.add(perfSummary);
labels.add(perfSummary.label);
}
String body = getPerformanceSummary(testKey.getName(), nonEmptySummaries, labels);
if (body == null || body.equals("")) {
return;
}
List<String> emails = EmailHelper.getSubscriberEmails(testKey);
if (emails.size() == 0) {
return;
}
String subject = SUBJECT_PREFIX + testKey.getName();
EmailHelper.send(emails, subject, body);
}
}