diff options
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.java | 573 |
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); + } +} |