org.chromium.latency.walt.AccelerometerFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.chromium.latency.walt.AccelerometerFragment.java

Source

/*
 * 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 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.support.v4.app.Fragment;
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 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 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;

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("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("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];
        }
    }
}