diff options
Diffstat (limited to 'android/WALT/app/src/main/java/org/chromium/latency/walt/AccelerometerFragment.java')
-rw-r--r-- | android/WALT/app/src/main/java/org/chromium/latency/walt/AccelerometerFragment.java | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/android/WALT/app/src/main/java/org/chromium/latency/walt/AccelerometerFragment.java b/android/WALT/app/src/main/java/org/chromium/latency/walt/AccelerometerFragment.java new file mode 100644 index 0000000..4f39547 --- /dev/null +++ b/android/WALT/app/src/main/java/org/chromium/latency/walt/AccelerometerFragment.java @@ -0,0 +1,379 @@ +/* + * 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<AccelerometerEvent> 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<Entry> getEntriesFromString(final String latencyString) { + List<Entry> 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<Entry> smoothEntries(List<Entry> entries, int windowSize) { + List<Entry> 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<Entry> phoneEntries, List<Entry> 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<Entry> phoneEntries = new ArrayList<>(); + List<Entry> waltEntries = getEntriesFromString(latencyString); + List<Entry> 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<Entry> phoneEntriesShifted = new ArrayList<>(); + for (Entry e : phoneEntries) { + phoneEntriesShifted.add(new Entry((float) (e.getX() - bestShift), e.getY())); + } + + drawLatencyChart(phoneEntriesShifted, waltSmoothEntries); + } + + private void drawLatencyChart(List<Entry> phoneEntriesShifted, List<Entry> 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]; + } + } +} |