blob: cfe6a5360440f879f60af602778e7000441e7793 [file] [log] [blame]
/*
* Copyright (C) 2015 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 org.chromium.latency.walt;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.method.ScrollingMovementMethod;
import android.view.Choreographer;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Description;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static org.chromium.latency.walt.Utils.getBooleanPreference;
import static org.chromium.latency.walt.Utils.getIntPreference;
/**
* Measurement of screen response time when switching between black and white.
*/
public class ScreenResponseFragment extends Fragment implements View.OnClickListener {
private static final int CURVE_TIMEOUT = 1000; // milliseconds
private static final int CURVE_BLINK_TIME = 250; // milliseconds
private static final int W2B_INDEX = 0;
private static final int B2W_INDEX = 1;
private SimpleLogger logger;
private TraceLogger traceLogger = null;
private WaltDevice waltDevice;
private Handler handler = new Handler();
private TextView blackBox;
private View startButton;
private View stopButton;
private Spinner spinner;
private LineChart brightnessChart;
private HistogramChart latencyChart;
private View brightnessChartLayout;
private View buttonBarView;
private FastPathSurfaceView fastSurfaceView;
private int timesToBlink;
private boolean shouldShowLatencyChart = false;
private boolean isTestRunning = false;
private boolean enableFullScreen = false;
private boolean isFastPathGraphics = false;
int initiatedBlinks = 0;
int detectedBlinks = 0;
boolean isBoxWhite = false;
long lastFrameStartTime;
long lastFrameCallbackTime;
long lastSetBackgroundTime;
ArrayList<Double> deltas_w2b = new ArrayList<>();
ArrayList<Double> deltas_b2w = new ArrayList<>();
ArrayList<Double> deltas = new ArrayList<>();
private static final int color_gray = Color.argb(0xFF, 0xBB, 0xBB, 0xBB);
private StringBuilder brightnessCurveData;
private BroadcastReceiver logReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!isTestRunning) {
String msg = intent.getStringExtra("message");
blackBox.append(msg + "\n");
}
}
};
public ScreenResponseFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
timesToBlink = getIntPreference(getContext(), R.string.preference_screen_blinks, 20);
shouldShowLatencyChart = getBooleanPreference(getContext(), R.string.preference_show_blink_histogram, true);
enableFullScreen = getBooleanPreference(getContext(), R.string.preference_screen_fullscreen, true);
if (getBooleanPreference(getContext(), R.string.preference_systrace, true)) {
traceLogger = TraceLogger.getInstance();
}
waltDevice = WaltDevice.getInstance(getContext());
logger = SimpleLogger.getInstance(getContext());
// Inflate the layout for this fragment
final View view = inflater.inflate(R.layout.fragment_screen_response, container, false);
stopButton = view.findViewById(R.id.button_stop_screen_response);
startButton = view.findViewById(R.id.button_start_screen_response);
blackBox = (TextView) view.findViewById(R.id.txt_black_box_screen);
fastSurfaceView = (FastPathSurfaceView) view.findViewById(R.id.fast_path_surface);
spinner = (Spinner) view.findViewById(R.id.spinner_screen_response);
buttonBarView = view.findViewById(R.id.button_bar);
ArrayAdapter<CharSequence> modeAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.screen_response_mode_array, android.R.layout.simple_spinner_item);
modeAdapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item);
spinner.setAdapter(modeAdapter);
stopButton.setEnabled(false);
blackBox.setMovementMethod(new ScrollingMovementMethod());
brightnessChartLayout = view.findViewById(R.id.brightness_chart_layout);
view.findViewById(R.id.button_close_chart).setOnClickListener(this);
brightnessChart = (LineChart) view.findViewById(R.id.chart);
latencyChart = (HistogramChart) view.findViewById(R.id.latency_chart);
if (getBooleanPreference(getContext(), R.string.preference_auto_increase_brightness, true)) {
increaseScreenBrightness();
}
return view;
}
@Override
public void onResume() {
super.onResume();
logger.registerReceiver(logReceiver);
// Register this fragment class as the listener for some button clicks
startButton.setOnClickListener(this);
stopButton.setOnClickListener(this);
}
@Override
public void onPause() {
logger.unregisterReceiver(logReceiver);
super.onPause();
}
void startBlinkLatency() {
setFullScreen(enableFullScreen);
deltas.clear();
deltas_b2w.clear();
deltas_w2b.clear();
if (shouldShowLatencyChart) {
latencyChart.clearData();
latencyChart.setVisibility(View.VISIBLE);
latencyChart.setLabel(W2B_INDEX, "White-to-black");
latencyChart.setLabel(B2W_INDEX, "Black-to-white");
}
initiatedBlinks = 0;
detectedBlinks = 0;
if (isFastPathGraphics) {
blackBox.setVisibility(View.GONE);
fastSurfaceView.setVisibility(View.VISIBLE);
fastSurfaceView.setRectColor(Color.WHITE);
} else {
blackBox.setText("");
blackBox.setBackgroundColor(Color.WHITE);
}
isBoxWhite = true;
handler.postDelayed(startBlinking, enableFullScreen ? 800 : 300);
}
Runnable startBlinking = new Runnable() {
@Override
public void run() {
try {
// Check for PWM
WaltDevice.TriggerMessage tmsg = waltDevice.readTriggerMessage(WaltDevice.CMD_SEND_LAST_SCREEN);
logger.log("Blink count was: " + tmsg.count);
waltDevice.softReset();
waltDevice.syncClock(); // Note, sync also sends CMD_RESET (but not simpleSync).
waltDevice.command(WaltDevice.CMD_AUTO_SCREEN_ON);
waltDevice.startListener();
} catch (IOException e) {
logger.log("Error: " + e.getMessage());
}
// Register a callback for triggers
waltDevice.setTriggerHandler(triggerHandler);
// post doBlink runnable
handler.postDelayed(doBlinkRunnable, 100);
}
};
Runnable doBlinkRunnable = new Runnable() {
@Override
public void run() {
if (!isTestRunning) return;
logger.log("======\ndoBlink.run(), initiatedBlinks = " + initiatedBlinks + " detectedBlinks = " + detectedBlinks);
// Check if we saw some transitions without blinking, this would usually mean
// the screen has PWM enabled, warn and ask the user to turn it off.
if (initiatedBlinks == 0 && detectedBlinks > 1) {
logger.log("Unexpected blinks detected, probably PWM, turn it off");
isTestRunning = false;
stopButton.setEnabled(false);
startButton.setEnabled(true);
showPwmDialog();
return;
}
if (initiatedBlinks >= timesToBlink) {
isTestRunning = false;
finishAndShowStats();
return;
}
// * 2 flip the screen, save time as last flip time (last flip direction?)
isBoxWhite = !isBoxWhite;
int nextColor = isBoxWhite ? Color.WHITE : Color.BLACK;
initiatedBlinks++;
if (traceLogger != null) {
traceLogger.log(RemoteClockInfo.microTime(), RemoteClockInfo.microTime() + 1000,
"Request-to-" + (isBoxWhite ? "white" : "black"),
"Application has called setBackgroundColor at start of bar");
}
if (isFastPathGraphics) {
fastSurfaceView.setRectColor(nextColor);
} else {
blackBox.setBackgroundColor(nextColor);
}
lastSetBackgroundTime = waltDevice.clock.micros();
// Set up a callback to run on next frame render to collect the timestamp
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
// frameTimeNanos is he time in nanoseconds when the frame started being
// rendered, in the nanoTime() timebase.
lastFrameStartTime = frameTimeNanos / 1000 - waltDevice.clock.baseTime;
lastFrameCallbackTime = System.nanoTime() / 1000 - waltDevice.clock.baseTime;
}
});
// Repost doBlink to some far away time to blink again even if nothing arrives from
// Teensy. This callback will almost always get cancelled by onIncomingTimestamp()
handler.postDelayed(doBlinkRunnable, 550 + (long) (Math.random()*100));
}
};
private void showPwmDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Detected extra blinks, please set your brightness to max")
.setTitle("Unexpected Blinks")
.setPositiveButton("OK", null);
AlertDialog dialog = builder.create();
dialog.show();
}
private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() {
@Override
public void onReceive(WaltDevice.TriggerMessage tmsg) {
// Remove the far away doBlink callback
handler.removeCallbacks(doBlinkRunnable);
detectedBlinks++;
logger.log("blink counts " + initiatedBlinks + " " + detectedBlinks);
if (initiatedBlinks == 0) {
if (detectedBlinks < 5) {
logger.log("got incoming but initiatedBlinks = 0");
return;
} else {
logger.log("Looks like PWM is used for this screen, turn auto brightness off and set it to max brightness");
showPwmDialog();
return;
}
}
final long startTimeMicros = lastFrameStartTime + waltDevice.clock.baseTime;
final long finishTimeMicros = tmsg.t + waltDevice.clock.baseTime;
if (traceLogger != null) {
traceLogger.log(startTimeMicros, finishTimeMicros,
isBoxWhite ? "Black-to-white" : "White-to-black",
"Bar starts at beginning of frame and ends when photosensor detects blink");
}
double dt = (tmsg.t - lastFrameStartTime) / 1000.;
deltas.add(dt);
if (isBoxWhite) { // Current color is the color we transitioned to
deltas_b2w.add(dt);
} else {
deltas_w2b.add(dt);
}
if (shouldShowLatencyChart) latencyChart.addEntry(isBoxWhite ? B2W_INDEX : W2B_INDEX, dt);
// Other times can be important, logging them to allow more detailed analysis
logger.log(String.format(Locale.US,
"Times [ms]: setBG:%.3f callback:%.3f physical:%.3f black2white:%d",
(lastSetBackgroundTime - lastFrameStartTime) / 1000.0,
(lastFrameCallbackTime - lastFrameStartTime) / 1000.0,
dt,
isBoxWhite ? 1 : 0
));
if (traceLogger != null) {
traceLogger.log(lastFrameCallbackTime + waltDevice.clock.baseTime,
lastFrameCallbackTime + waltDevice.clock.baseTime + 1000,
isBoxWhite ? "FrameCallback Black-to-white" : "FrameCallback White-to-black",
"FrameCallback was called at start of bar");
}
// Schedule another blink soon-ish
handler.postDelayed(doBlinkRunnable, 40 + (long) (Math.random()*20));
}
};
void finishAndShowStats() {
setFullScreen(false);
// Stop the USB listener
waltDevice.stopListener();
// Unregister trigger handler
waltDevice.clearTriggerHandler();
waltDevice.sendAndFlush(WaltDevice.CMD_AUTO_SCREEN_OFF);
waltDevice.checkDrift();
// Show deltas and the median
/* // Debug printouts
logger.log("deltas = array(" + deltas.toString() + ")");
logger.log("deltas_w2b = array(" + deltas_w2b.toString() + ")");
logger.log("deltas_b2w = array(" + deltas_b2w.toString() + ")");
*/
double median_b2w = Utils.median(deltas_b2w);
double median_w2b = Utils.median(deltas_w2b);
logger.log(String.format(Locale.US,
"\n-------------------------------\n" +
"Median screen response latencies (N=%d):\n" +
"Black to white: %.1f ms (N=%d)\n" +
"White to black: %.1f ms (N=%d)\n" +
"Average: %.1f ms\n" +
"-------------------------------\n",
deltas.size(),
median_b2w, deltas_b2w.size(),
median_w2b, deltas_w2b.size(),
(median_b2w + median_w2b) / 2
));
if (traceLogger != null) traceLogger.flush(getContext());
fastSurfaceView.setVisibility(View.GONE);
blackBox.setVisibility(View.VISIBLE);
blackBox.setText(logger.getLogText());
blackBox.setMovementMethod(new ScrollingMovementMethod());
blackBox.setBackgroundColor(color_gray);
stopButton.setEnabled(false);
startButton.setEnabled(true);
if (shouldShowLatencyChart) {
latencyChart.setLabel(W2B_INDEX, String.format(Locale.US, "White-to-black m=%.1f ms", median_w2b));
latencyChart.setLabel(B2W_INDEX, String.format(Locale.US, "Black-to-white m=%.1f ms", median_b2w));
}
LogUploader.uploadIfAutoEnabled(getContext());
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.button_stop_screen_response) {
isTestRunning = false;
handler.removeCallbacks(doBlinkRunnable);
handler.removeCallbacks(startBlinking);
finishAndShowStats();
return;
}
if (v.getId() == R.id.button_start_screen_response) {
brightnessChartLayout.setVisibility(View.GONE);
latencyChart.setVisibility(View.GONE);
if (!waltDevice.isConnected()) {
logger.log("Error starting test: WALT is not connected");
return;
}
isTestRunning = true;
startButton.setEnabled(false);
blackBox.setBackgroundColor(Color.BLACK);
blackBox.setText("");
isFastPathGraphics = false;
final int spinnerPosition = spinner.getSelectedItemPosition();
if (spinnerPosition == 0) {
logger.log("Starting screen response measurement");
stopButton.setEnabled(true);
startBlinkLatency();
} else if (spinnerPosition == 1) {
logger.log("Starting screen brightness curve measurement");
startBrightnessCurve();
} else if (spinnerPosition == 2) {
logger.log("Starting fast-path screen response measurement");
isFastPathGraphics = true;
startBlinkLatency();
} else {
logger.log("ERROR: Spinner position is out of range");
}
return;
}
if (v.getId() == R.id.button_close_chart) {
brightnessChartLayout.setVisibility(View.GONE);
return;
}
}
private WaltDevice.TriggerHandler brightnessTriggerHandler = new WaltDevice.TriggerHandler() {
@Override
public void onReceive(WaltDevice.TriggerMessage tmsg) {
logger.log("ERROR: Brightness curve trigger got a trigger message, " +
"this should never happen."
);
}
@Override
public void onReceiveRaw(String s) {
brightnessCurveData.append(s);
if (s.trim().equals("end")) {
// Remove the delayed callback and run it now
handler.removeCallbacks(finishBrightnessCurve);
handler.post(finishBrightnessCurve);
}
}
};
void startBrightnessCurve() {
try {
brightnessCurveData = new StringBuilder();
waltDevice.syncClock();
waltDevice.startListener();
} catch (IOException e) {
logger.log("Error starting test: " + e.getMessage());
isTestRunning = false;
startButton.setEnabled(true);
return;
}
setFullScreen(enableFullScreen);
blackBox.setText("");
blackBox.setBackgroundColor(Color.BLACK);
handler.postDelayed(startBrightness, enableFullScreen ? 1000 : CURVE_BLINK_TIME);
}
Runnable startBrightness = new Runnable() {
@Override
public void run() {
waltDevice.setTriggerHandler(brightnessTriggerHandler);
long tStart = waltDevice.clock.micros();
try {
waltDevice.command(WaltDevice.CMD_BRIGHTNESS_CURVE);
} catch (IOException e) {
logger.log("Error sending command CMD_BRIGHTNESS_CURVE: " + e.getMessage());
isTestRunning = false;
startButton.setEnabled(true);
return;
}
blackBox.setBackgroundColor(Color.WHITE);
logger.log("=== Screen brightness curve: ===\nt_start: " + tStart);
handler.postDelayed(finishBrightnessCurve, CURVE_TIMEOUT);
// Schedule the screen to flip back to black in CURVE_BLINK_TIME ms
handler.postDelayed(new Runnable() {
@Override
public void run() {
long tBack = waltDevice.clock.micros();
blackBox.setBackgroundColor(Color.BLACK);
logger.log("t_back: " + tBack);
}
}, CURVE_BLINK_TIME);
}
};
Runnable finishBrightnessCurve = new Runnable() {
@Override
public void run() {
waltDevice.stopListener();
waltDevice.clearTriggerHandler();
// TODO: Add option to save this data into a separate file rather than the main log.
logger.log(brightnessCurveData.toString());
logger.log("=== End of screen brightness data ===");
blackBox.setText(logger.getLogText());
blackBox.setMovementMethod(new ScrollingMovementMethod());
blackBox.setBackgroundColor(color_gray);
isTestRunning = false;
startButton.setEnabled(true);
setFullScreen(false);
drawBrightnessChart();
LogUploader.uploadIfAutoEnabled(getContext());
}
};
private void drawBrightnessChart() {
final String brightnessCurveString = brightnessCurveData.toString();
List<Entry> entries = new ArrayList<>();
// "u" marks the start of the brightness curve data
int startIndex = brightnessCurveString.indexOf("u") + 1;
int endIndex = brightnessCurveString.indexOf("end");
if (endIndex == -1) endIndex = brightnessCurveString.length();
String[] brightnessStrings =
brightnessCurveString.substring(startIndex, endIndex).trim().split("\n");
for (String str : brightnessStrings) {
String[] arr = str.split(" ");
final float timestampMs = Integer.parseInt(arr[0]) / 1000f;
final float brightness = Integer.parseInt(arr[1]);
entries.add(new Entry(timestampMs, brightness));
}
LineDataSet dataSet = new LineDataSet(entries, "Brightness");
dataSet.setColor(Color.BLACK);
dataSet.setValueTextColor(Color.BLACK);
dataSet.setCircleColor(Color.BLACK);
dataSet.setCircleRadius(1.5f);
dataSet.setCircleColorHole(Color.DKGRAY);
LineData lineData = new LineData(dataSet);
brightnessChart.setData(lineData);
final Description desc = new Description();
desc.setText("Screen Brightness [digital level 0-1023] vs. Time [ms]");
desc.setTextSize(12f);
brightnessChart.setDescription(desc);
brightnessChart.getLegend().setEnabled(false);
brightnessChart.invalidate();
brightnessChartLayout.setVisibility(View.VISIBLE);
}
private void increaseScreenBrightness() {
final WindowManager.LayoutParams layoutParams = getActivity().getWindow().getAttributes();
layoutParams.screenBrightness = 1f;
getActivity().getWindow().setAttributes(layoutParams);
}
private void setFullScreen(boolean enable) {
final AppCompatActivity activity = (AppCompatActivity) getActivity();
final ActionBar actionBar = activity != null ? activity.getSupportActionBar() : null;
int newVisibility = 0;
if (enable) {
if (actionBar != null) actionBar.hide();
buttonBarView.setVisibility(View.GONE);
newVisibility |= View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
} else {
if (actionBar != null) actionBar.show();
buttonBarView.setVisibility(View.VISIBLE);
}
if (activity != null) activity.getWindow().getDecorView().setSystemUiVisibility(newVisibility);
}
}