aboutsummaryrefslogtreecommitdiff
path: root/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java')
-rw-r--r--android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java573
1 files changed, 573 insertions, 0 deletions
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java
new file mode 100644
index 0000000..cfe6a53
--- /dev/null
+++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/ScreenResponseFragment.java
@@ -0,0 +1,573 @@
+/*
+ * 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);
+ }
+}