/* * 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 org.chromium.latency.walt; import static org.chromium.latency.walt.Utils.argmax; import static org.chromium.latency.walt.Utils.interp; import static org.chromium.latency.walt.Utils.max; import static org.chromium.latency.walt.Utils.mean; import static org.chromium.latency.walt.Utils.min; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.text.method.ScrollingMovementMethod; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.fragment.app.Fragment; import com.github.mikephil.charting.charts.ScatterChart; import com.github.mikephil.charting.components.Description; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.ScatterData; import com.github.mikephil.charting.data.ScatterDataSet; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class AccelerometerFragment extends Fragment implements View.OnClickListener, SensorEventListener { private static final int MAX_TEST_LENGTH_MS = 10000; private SimpleLogger logger; private WaltDevice waltDevice; private TextView logTextView; private View startButton; private ScatterChart latencyChart; private View latencyChartLayout; private StringBuilder accelerometerData; private List phoneAccelerometerData = new ArrayList<>(); private Handler handler = new Handler(); private SensorManager sensorManager; private Sensor accelerometer; private double realTimeOffsetMs; private boolean isTestRunning = false; Runnable finishAccelerometer = new Runnable() { @Override public void run() { isTestRunning = false; waltDevice.stopListener(); waltDevice.clearTriggerHandler(); calculateAndDrawLatencyChart(accelerometerData.toString()); startButton.setEnabled(true); accelerometerData = new StringBuilder(); LogUploader.uploadIfAutoEnabled(getContext()); } }; private BroadcastReceiver logReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String msg = intent.getStringExtra("message"); AccelerometerFragment.this.appendLogText(msg); } }; private WaltDevice.TriggerHandler triggerHandler = new WaltDevice.TriggerHandler() { @Override public void onReceive(WaltDevice.TriggerMessage tmsg) { logger.log("ERROR: Accelerometer trigger got a trigger message, " + "this should never happen."); } @Override public void onReceiveRaw(String s) { if (s.trim().equals("end")) { // Remove the delayed callback and run it now handler.removeCallbacks(finishAccelerometer); handler.post(finishAccelerometer); } else { accelerometerData.append(s); } } }; Runnable startAccelerometer = new Runnable() { @Override public void run() { waltDevice.setTriggerHandler(triggerHandler); try { waltDevice.command(WaltDevice.CMD_ACCELEROMETER); } catch (IOException e) { logger.log("Error sending command CMD_ACCELEROMETER: " + e.getMessage()); startButton.setEnabled(true); return; } logger.log("=== Accelerometer Test ===\n"); isTestRunning = true; handler.postDelayed(finishAccelerometer, MAX_TEST_LENGTH_MS); } }; public AccelerometerFragment() { // Required empty public constructor } static List getEntriesFromString(final String latencyString) { List entries = new ArrayList<>(); // "o" marks the start of the accelerometer data int startIndex = latencyString.indexOf("o") + 1; String[] brightnessStrings = latencyString.substring(startIndex).trim().split("\n"); for (String str : brightnessStrings) { String[] arr = str.split(" "); final float timestampMs = Integer.parseInt(arr[0]) / 1000f; final float value = Integer.parseInt(arr[1]); entries.add(new Entry(timestampMs, value)); } return entries; } static List smoothEntries(List entries, int windowSize) { List smoothEntries = new ArrayList<>(); for (int i = windowSize; i < entries.size() - windowSize; i++) { final float time = entries.get(i).getX(); float avg = 0; for (int j = i - windowSize; j <= i + windowSize; j++) { avg += entries.get(j).getY() / (2 * windowSize + 1); } smoothEntries.add(new Entry(time, avg)); } return smoothEntries; } static double[] findShifts(List phoneEntries, List waltEntries) { double[] phoneTimes = new double[phoneEntries.size()]; double[] phoneValues = new double[phoneEntries.size()]; double[] waltTimes = new double[waltEntries.size()]; double[] waltValues = new double[waltEntries.size()]; for (int i = 0; i < phoneTimes.length; i++) { phoneTimes[i] = phoneEntries.get(i).getX(); phoneValues[i] = phoneEntries.get(i).getY(); } for (int i = 0; i < waltTimes.length; i++) { waltTimes[i] = waltEntries.get(i).getX(); waltValues[i] = waltEntries.get(i).getY(); } double[] shiftCorrelations = new double[401]; for (int i = 0; i < shiftCorrelations.length; i++) { double shift = i / 10.; final double[] shiftedPhoneTimes = new double[phoneTimes.length]; for (int j = 0; j < phoneTimes.length; j++) { shiftedPhoneTimes[j] = phoneTimes[j] - shift; } final double[] interpolatedValues = interp(shiftedPhoneTimes, waltTimes, waltValues); double sum = 0; for (int j = 0; j < shiftedPhoneTimes.length; j++) { // Calculate square dot product of phone and walt values sum += Math.pow(phoneValues[j] * interpolatedValues[j], 2); } shiftCorrelations[i] = sum; } return shiftCorrelations; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { logger = SimpleLogger.getInstance(getContext()); waltDevice = WaltDevice.getInstance(getContext()); // Inflate the layout for this fragment final View view = inflater.inflate(R.layout.fragment_accelerometer, container, false); logTextView = (TextView) view.findViewById(R.id.txt_log); startButton = view.findViewById(R.id.button_start); latencyChart = (ScatterChart) view.findViewById(R.id.latency_chart); latencyChartLayout = view.findViewById(R.id.latency_chart_layout); logTextView.setMovementMethod(new ScrollingMovementMethod()); view.findViewById(R.id.button_close_chart).setOnClickListener(this); sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE); accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (accelerometer == null) { logger.log("ERROR! Accelerometer sensor not found"); } return view; } @Override public void onResume() { super.onResume(); logTextView.setText(logger.getLogText()); logger.registerReceiver(logReceiver); startButton.setOnClickListener(this); sensorManager.registerListener( AccelerometerFragment.this, accelerometer, SensorManager.SENSOR_DELAY_FASTEST); } @Override public void onPause() { logger.unregisterReceiver(logReceiver); sensorManager.unregisterListener(AccelerometerFragment.this, accelerometer); super.onPause(); } public void appendLogText(String msg) { logTextView.append(msg + "\n"); } void startMeasurement() { logger.log("Starting accelerometer latency measurement"); try { accelerometerData = new StringBuilder(); phoneAccelerometerData.clear(); waltDevice.syncClock(); waltDevice.startListener(); realTimeOffsetMs = SystemClock.elapsedRealtimeNanos() / 1e6 - waltDevice.clock.micros() / 1e3; } catch (IOException e) { logger.log("Error syncing clocks: " + e.getMessage()); startButton.setEnabled(true); return; } Toast.makeText(getContext(), "Start shaking the phone and WALT!", Toast.LENGTH_LONG).show(); handler.postDelayed(startAccelerometer, 500); } /** * Handler for all the button clicks on this screen. */ @Override public void onClick(View v) { if (v.getId() == R.id.button_start) { latencyChartLayout.setVisibility(View.GONE); startButton.setEnabled(false); startMeasurement(); return; } if (v.getId() == R.id.button_close_chart) { latencyChartLayout.setVisibility(View.GONE); } } private void calculateAndDrawLatencyChart(final String latencyString) { List phoneEntries = new ArrayList<>(); List waltEntries = getEntriesFromString(latencyString); List waltSmoothEntries = smoothEntries(waltEntries, 4); for (AccelerometerEvent e : phoneAccelerometerData) { phoneEntries.add(new Entry(e.callbackTimeMs, e.value)); } while (phoneEntries.get(0).getX() < waltSmoothEntries.get(0).getX()) { // This event is earlier than any walt event, so discard it phoneEntries.remove(0); } while (phoneEntries.get(phoneEntries.size() - 1).getX() > waltSmoothEntries.get(waltSmoothEntries.size() - 1).getX()) { // This event is later than any walt event, so discard it phoneEntries.remove(phoneEntries.size() - 1); } // Adjust waltEntries so min and max is the same as phoneEntries float phoneMean = mean(phoneEntries); float phoneMax = max(phoneEntries); float phoneMin = min(phoneEntries); float waltMin = min(waltSmoothEntries); float phoneRange = phoneMax - phoneMin; float waltRange = max(waltSmoothEntries) - waltMin; for (Entry e : waltSmoothEntries) { e.setY((e.getY() - waltMin) * (phoneRange / waltRange) + phoneMin - phoneMean); } // Adjust phoneEntries so mean=0 for (Entry e : phoneEntries) { e.setY(e.getY() - phoneMean); } double[] shifts = findShifts(phoneEntries, waltSmoothEntries); double bestShift = argmax(shifts) / 10d; logger.log(String.format(Locale.US, "Accelerometer latency: %.1fms", bestShift)); double[] deltasKernelToCallback = new double[phoneAccelerometerData.size()]; for (int i = 0; i < deltasKernelToCallback.length; i++) { deltasKernelToCallback[i] = phoneAccelerometerData.get(i).callbackTimeMs - phoneAccelerometerData.get(i).kernelTimeMs; } logger.log(String.format( Locale.US, "Mean kernel-to-callback latency: %.1fms", mean(deltasKernelToCallback))); List phoneEntriesShifted = new ArrayList<>(); for (Entry e : phoneEntries) { phoneEntriesShifted.add(new Entry((float) (e.getX() - bestShift), e.getY())); } drawLatencyChart(phoneEntriesShifted, waltSmoothEntries); } private void drawLatencyChart(List phoneEntriesShifted, List waltEntries) { final ScatterDataSet dataSetWalt = new ScatterDataSet(waltEntries, "WALT Events"); dataSetWalt.setColor(Color.BLUE); dataSetWalt.setScatterShape(ScatterChart.ScatterShape.CIRCLE); dataSetWalt.setScatterShapeSize(8f); final ScatterDataSet dataSetPhoneShifted = new ScatterDataSet(phoneEntriesShifted, "Phone Events Shifted"); dataSetPhoneShifted.setColor(Color.RED); dataSetPhoneShifted.setScatterShapeSize(10f); dataSetPhoneShifted.setScatterShape(ScatterChart.ScatterShape.X); final ScatterData scatterData = new ScatterData(dataSetWalt, dataSetPhoneShifted); final Description desc = new Description(); desc.setText(""); desc.setTextSize(12f); latencyChart.setDescription(desc); latencyChart.setData(scatterData); latencyChart.invalidate(); latencyChartLayout.setVisibility(View.VISIBLE); } @Override public void onSensorChanged(SensorEvent event) { if (isTestRunning) { phoneAccelerometerData.add(new AccelerometerEvent(event)); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } private class AccelerometerEvent { float callbackTimeMs; float kernelTimeMs; float value; AccelerometerEvent(SensorEvent event) { callbackTimeMs = waltDevice.clock.micros() / 1e3f; kernelTimeMs = (float) (event.timestamp / 1e6f - realTimeOffsetMs); value = event.values[2]; } } }