diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-12-02 00:27:50 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-12-02 00:27:50 +0000 |
commit | 5c74f573ba7440b95b762608ced4a4db36134411 (patch) | |
tree | a23151903ff517dd777b28486fff1546ff6efad9 | |
parent | 74155c9cc3035d0cee2a79b464a9fca3d9047f78 (diff) | |
parent | 1eee21ee5c1c3dafbadc0879565d26e4ea2f14bd (diff) | |
download | Car-android13-d4-release.tar.gz |
Snap for 9358956 from 1eee21ee5c1c3dafbadc0879565d26e4ea2f14bd to tm-d4-releaseandroid-13.0.0_r48android-13.0.0_r47android-13.0.0_r46android13-d4-s2-releaseandroid13-d4-s1-releaseandroid13-d4-release
Change-Id: Id31cafee7e919fd778727275d76368bd2f438c30
29 files changed, 1034 insertions, 304 deletions
diff --git a/car-evs-helper-lib/Android.bp b/car-evs-helper-lib/Android.bp new file mode 100644 index 0000000000..f383376a71 --- /dev/null +++ b/car-evs-helper-lib/Android.bp @@ -0,0 +1,37 @@ +// Copyright (C) 2022 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. + +// This project contains libraries that are used internally, mostly by +// CarService and CarServiceHelperService. +// +// They're not meant to be used by other system apps and hence are not +// supported. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_library { + name: "car-evs-helper-lib", + srcs: [ + "src/**/*.java", + ], + libs: [ + "android.car", + ], + platform_apis: true, + required : [ + "libcarevsglrenderer_jni", + ], +} diff --git a/car-evs-helper-lib/AndroidManifest.xml b/car-evs-helper-lib/AndroidManifest.xml new file mode 100644 index 0000000000..ba31871a3c --- /dev/null +++ b/car-evs-helper-lib/AndroidManifest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.car.internal.evs" > +</manifest> diff --git a/car-evs-helper-lib/README.md b/car-evs-helper-lib/README.md new file mode 100644 index 0000000000..416a8a8cdd --- /dev/null +++ b/car-evs-helper-lib/README.md @@ -0,0 +1,82 @@ +<!-- + Copyright (C) 2022 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 + --> +# car-evs-helper-lib +This directory contains two modules that are used by other apps to process +CarEvsBufferDescriptor and render its contents to the display with EGL. +* `car-evs-helper-lib:` This library contains `CarEvsGLSurfaceView` and + `CarEvsBufferRenderer` classes. +* `libcarevsglrenderer_jni`: This is a JNI library `CarEvsBufferRenderer` uses + to render the contents of `CarEvsBufferDescriptor` with EGL. +## How to use +Please follow below instructions to delegate a `CarEvsBufferDescriptor` rendering +to this library. A reference implementation is also available at +`packages/services/Car/tests/CarEvsCameraPreviewApp`. +1. Make the application refer to `car-evs-helper-lib` and + `libcarevsglrenderer_jni` libraries by adding below lines to `Android.bp`. +``` +static_libs: ["car-evs-helper-lib"], +jni_libs: ["libcarevsglrenderer_jni"], +``` +2. Implement `CarEvsGLSurfaceView.Callback` interface. For example, +``` +/** + * This method is called by the renderer to fetch a new frame to draw. + */ +@Override +public CarEvsBufferDescriptor getNewFrame() { + synchronized(mLock) { + // Return a buffer to render. + return mBufferToRender; + } +} +/** + * This method is called by the renderer when it is done with a passed + * CarEvsBufferDescriptor object. + */ +@Override +public void returnBuffer(CarEvsBufferDescriptor buffer) { + // Return a buffer to CarEvsService. + try { + mEvsManager.returnFrameBuffer(buffer); + } catch (Exception e) { + ... + } + ... +} +``` +3. Create `CarEvsGLSurfaceView` with the application context, + `CarEvsGLSurfaceView.Callback` object, and, optionally, a desired in-plane + rotation angle. +``` +private CarEvsGLSurfaceView mView; +@Override +protected void onCreate(Bundle savedInstanceState) { + ... + mView = CarEvsGLSurfaceView(getAppliation(), this, /* angleInDegree= */ 0); + ... +} +``` +4. Start a video stream and update wheneven new frame buffer arrives. For + example, +``` +private final CarEvsManager.CarEvsStreamCallback mStreamHandler = + new CarEvsManager.CarEvsStreamCallback() { + ... + @Override + public void onNewFrame(CarEvsBufferDescriptor buffer) { + synchronized(mLock) { + mBufferToRender = buffer; + } + } +} +``` diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/jni/Android.bp b/car-evs-helper-lib/jni/Android.bp index d06620ac5b..702a23ee14 100644 --- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/jni/Android.bp +++ b/car-evs-helper-lib/jni/Android.bp @@ -23,9 +23,7 @@ cc_library_shared { sdk_version: "current", - srcs: [ - "CarEvsCameraPreviewRenderer.cpp", - ], + srcs: ["./**/*.cpp"], header_libs: ["jni_headers"], @@ -43,7 +41,7 @@ cc_library_shared { }, cflags: [ - "-DLOG_TAG=\"CarEvsCameraRendererJNI\"", + "-DLOG_TAG=\"CarEvsBufferRendererJNI\"", "-DGL_GLEXT_PROTOTYPES", "-DEGL_EGLEXT_PROTOTYPES", "-Wall", diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/jni/CarEvsCameraPreviewRenderer.cpp b/car-evs-helper-lib/jni/CarEvsBufferRenderer.cpp index 91ca40bd8e..654703ff13 100644 --- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/jni/CarEvsCameraPreviewRenderer.cpp +++ b/car-evs-helper-lib/jni/CarEvsBufferRenderer.cpp @@ -20,19 +20,18 @@ #include <GLES2/gl2ext.h> #include <GLES3/gl3.h> #include <GLES3/gl3ext.h> - #include <android/hardware_buffer_jni.h> + #include <jni.h> namespace { -const char kClassName[] = "com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer"; +const char kClassName[] = "com/android/car/internal/evs/GLES20CarEvsBufferRenderer"; EGLImageKHR gKHRImage = EGL_NO_IMAGE_KHR; -jboolean nativeUpdateTexture( - JNIEnv* env, jobject /*thiz*/, jobject hardwareBufferObj, jint textureId) { - +jboolean nativeUpdateTexture(JNIEnv* env, jobject /*thiz*/, jobject hardwareBufferObj, + jint textureId) { EGLDisplay eglCurrentDisplay = eglGetCurrentDisplay(); if (gKHRImage != EGL_NO_IMAGE_KHR) { // Release a previous EGL image @@ -49,11 +48,8 @@ jboolean nativeUpdateTexture( // Create EGL image from a native hardware buffer EGLClientBuffer eglBuffer = eglGetNativeClientBufferANDROID(nativeBuffer); EGLint eglImageAttributes[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; - gKHRImage = eglCreateImageKHR(eglCurrentDisplay, - EGL_NO_CONTEXT, - EGL_NATIVE_BUFFER_ANDROID, - eglBuffer, - eglImageAttributes); + gKHRImage = eglCreateImageKHR(eglCurrentDisplay, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, + eglBuffer, eglImageAttributes); if (gKHRImage == EGL_NO_IMAGE_KHR) { return JNI_FALSE; } @@ -74,7 +70,7 @@ jboolean nativeUpdateTexture( return JNI_TRUE; } -} // namespace unnamed +} // namespace JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { JNIEnv* env; @@ -85,7 +81,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { // Registers native methods static const JNINativeMethod methods[] = { {"nUpdateTexture", "(Landroid/hardware/HardwareBuffer;I)Z", - reinterpret_cast<void*>(nativeUpdateTexture)}, + reinterpret_cast<void*>(nativeUpdateTexture)}, }; jclass clazz = env->FindClass(kClassName); diff --git a/car-evs-helper-lib/src/com/android/car/internal/evs/CarEvsGLSurfaceView.java b/car-evs-helper-lib/src/com/android/car/internal/evs/CarEvsGLSurfaceView.java new file mode 100644 index 0000000000..c798ee39a9 --- /dev/null +++ b/car-evs-helper-lib/src/com/android/car/internal/evs/CarEvsGLSurfaceView.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 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 com.android.car.internal.evs; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.car.evs.CarEvsBufferDescriptor; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.view.MotionEvent; + +import com.android.internal.util.Preconditions; +import com.android.car.internal.evs.GLES20CarEvsBufferRenderer; + +/** + * GPU-backed SurfaceView to render a hardware buffer described by {@link CarEvsBufferDescriptor}. + */ +public final class CarEvsGLSurfaceView extends GLSurfaceView { + private static final String TAG = CarEvsGLSurfaceView.class.getSimpleName(); + private static final int DEFAULT_IN_PLANE_ROTATION_ANGLE = 0; + + private final GLES20CarEvsBufferRenderer mRenderer; + + /** An interface to pull and return {@code CarEvsBufferDescriptor} object to render. */ + public interface BufferCallback { + /** + * Requests a new {@link CarEvsBufferDescriptor} to draw. + * + * This method may return a {@code null} if no new frame has been prepared since the last + * frame was drawn. + * + * @return {@link CarEvsBufferDescriptor} object to process. + */ + @Nullable CarEvsBufferDescriptor onBufferRequested(); + + /** + * Notifies that the buffer is processed. + * + * @param buffer {@link CarEvsBufferDescriptor} object we are done with. + */ + void onBufferProcessed(@NonNull CarEvsBufferDescriptor buffer); + } + + private CarEvsGLSurfaceView(Context context, BufferCallback callback, int angleInDegree) { + super(context); + setEGLContextClientVersion(2); + + mRenderer = new GLES20CarEvsBufferRenderer(context, callback, angleInDegree); + setRenderer(mRenderer); + + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + + /** + * Returns all buffers held by the renderer. + */ + public void reset() { + mRenderer.clearBuffer(); + } + + /** + * Creates a {@link CarEvsGLSurfaceView} object with the default in-plane rotation angle. + * + * @param context Current appliation context. + * @param callback {@link CarEvsGLSurfaceView.BufferCallback} object. + * + */ + public static CarEvsGLSurfaceView create(Context context, BufferCallback callback) { + return create(context, callback, DEFAULT_IN_PLANE_ROTATION_ANGLE); + } + + /** + * Creates a {@link CarEvsGLSurfaceView} object with a given in-plane rotation angle in degree. + * + * @param context Current appliation context. + * @param callback {@link CarEvsGLSurfaceView.BufferCallback} object. + * @param angleInDegree In-plane counter-clockwise rotation angle in degree. + */ + public static CarEvsGLSurfaceView create(Context context, BufferCallback callback, + int angleInDegree) { + + Preconditions.checkArgument(context != null, "Context cannot be null."); + Preconditions.checkArgument(callback != null, "BufferCallback cannot be null."); + + return new CarEvsGLSurfaceView(context, callback, angleInDegree); + } +} diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java b/car-evs-helper-lib/src/com/android/car/internal/evs/GLES20CarEvsBufferRenderer.java index f04d15ba84..8de69f1e6e 100644 --- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java +++ b/car-evs-helper-lib/src/com/android/car/internal/evs/GLES20CarEvsBufferRenderer.java @@ -14,37 +14,36 @@ * limitations under the License. */ -package com.google.android.car.evs; +package com.android.car.internal.evs; import static android.opengl.GLU.gluErrorString; +import android.annotation.NonNull; import android.car.evs.CarEvsBufferDescriptor; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; import android.hardware.HardwareBuffer; import android.opengl.GLES20; import android.opengl.GLSurfaceView; -import android.opengl.GLUtils; import android.util.Log; -import androidx.annotation.GuardedBy; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; -import java.util.Random; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; /** - * GLES20 SurfaceView Renderer + * GLES20 SurfaceView Renderer for CarEvsBufferDescriptor. */ -public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Renderer { - private static final String TAG = GLES20CarEvsCameraPreviewRenderer.class.getSimpleName(); +public final class GLES20CarEvsBufferRenderer implements GLSurfaceView.Renderer { + + private static final String TAG = GLES20CarEvsBufferRenderer.class.getSimpleName() + .replace("GLES20", ""); + private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static final int FLOAT_SIZE_BYTES = 4; private static final float[] sVertCarPosData = { @@ -66,49 +65,58 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re 0.0f, 0.0f, 0.0f, 1.0f }; private final String mVertexShader = - "#version 300 es \n" + - "layout(location = 0) in vec4 pos; \n" + - "layout(location = 1) in vec2 tex; \n" + - "uniform mat4 cameraMat; \n" + - "out vec2 uv; \n" + - "void main() \n" + - "{ \n" + - " gl_Position = cameraMat * pos; \n" + - " uv = tex; \n" + - "} \n"; + "attribute vec4 pos; \n" + + "attribute vec2 tex; \n" + + "uniform mat4 cameraMat; \n" + + "varying vec2 uv; \n" + + "void main() \n" + + "{ \n" + + " gl_Position = cameraMat * pos; \n" + + " uv = tex; \n" + + "} \n"; private final String mFragmentShader = - "#version 300 es \n" + - "precision mediump float; \n" + - "uniform sampler2D tex; \n" + - "in vec2 uv; \n" + - "out vec4 color; \n" + - "void main() \n" + - "{ \n" + - " vec4 texel = texture(tex, uv); \n" + - " color = texel; \n" + - "} \n"; + "precision mediump float; \n" + + "uniform sampler2D tex; \n" + + "varying vec2 uv; \n" + + "void main() \n" + + "{ \n" + + " gl_FragColor = texture2D(tex, uv); \n" + + "} \n"; private final Object mLock = new Object(); + private final CarEvsGLSurfaceView.BufferCallback mCallback; + private final Context mContext; + private final FloatBuffer mVertCarPos; + private final FloatBuffer mVertCarTex; - private CarEvsCameraPreviewActivity mActivity; + private int mProgram; + private int mTextureId; + private int mWidth; + private int mHeight; + // Native method to update the texture with a received frame buffer @GuardedBy("mLock") - private CarEvsBufferDescriptor mBufferInUse = null; + private CarEvsBufferDescriptor mBufferInUse; + + /** Load jni on initialization. */ + static { + System.loadLibrary("carevsglrenderer_jni"); + } + + public GLES20CarEvsBufferRenderer(@NonNull Context context, + @NonNull CarEvsGLSurfaceView.BufferCallback callback, int angleInDegree) { - public GLES20CarEvsCameraPreviewRenderer(Context context, - CarEvsCameraPreviewActivity activity) { + Preconditions.checkArgument(context != null, "Context cannot be null."); + Preconditions.checkArgument(callback != null, "Callback cannot be null."); mContext = context; - mActivity = activity; + mCallback = callback; mVertCarPos = ByteBuffer.allocateDirect(sVertCarPosData.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()).asFloatBuffer(); mVertCarPos.put(sVertCarPosData).position(0); - // Rotates the matrix in counter-clockwise - int angleInDegree = mContext.getResources().getInteger( - R.integer.config_evsRearviewCameraInPlaneRotationAngle); double angleInRadian = Math.toRadians(angleInDegree); float[] rotated = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}; float sin = (float)Math.sin(angleInRadian); @@ -140,7 +148,7 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re } // bufferToReturn is not null here. - mActivity.returnBuffer(bufferToReturn); + mCallback.onBufferProcessed(bufferToReturn); } @Override @@ -149,7 +157,8 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re CarEvsBufferDescriptor bufferToRender = null; CarEvsBufferDescriptor bufferToReturn = null; - CarEvsBufferDescriptor newFrame = mActivity.getNewFrame(); + CarEvsBufferDescriptor newFrame = mCallback.onBufferRequested(); + synchronized (mLock) { if (newFrame != null) { // If a new frame has not been delivered yet, we're using a previous frame. @@ -161,8 +170,15 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re bufferToRender = mBufferInUse; } + if (bufferToRender == null) { + if (DBG) { + Log.d(TAG, "No buffer to draw."); + } + return; + } + if (bufferToReturn != null) { - mActivity.returnBuffer(bufferToReturn); + mCallback.onBufferProcessed(bufferToReturn); } // Specify a shader program to use @@ -175,22 +191,13 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re } GLES20.glUniformMatrix4fv(matrix, 1, false, sIdentityMatrix, 0); - if (bufferToRender == null) { - // Show the default screen - drawDefaultScreen(); - } else { - // Retrieve a hardware buffer from a descriptor and update the texture - HardwareBuffer buffer = bufferToRender.getHardwareBuffer(); - if (buffer == null) { - Log.e(TAG, "HardwareBuffer is invalid."); - drawDefaultScreen(); - } else { - // Update the texture with a given hardware buffer - if (!nUpdateTexture(buffer, mTextureId)) { - throw new RuntimeException( - "Failed to update the texture with the preview frame"); - } - } + // Retrieve a hardware buffer from a descriptor and update the texture + HardwareBuffer buffer = bufferToRender.getHardwareBuffer(); + + // Update the texture with a given hardware buffer + if (!nUpdateTexture(buffer, mTextureId)) { + throw new RuntimeException( + "Failed to update the texture with the preview frame"); } GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); @@ -239,7 +246,8 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re mHeight = height; } - @Override public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { + @Override + public void onSurfaceCreated(GL10 glUnused, EGLConfig config) { // Use the GLES20 class's static methods instead of a passed GL10 interface. mProgram = buildShaderProgram(mVertexShader, mFragmentShader); if (mProgram == 0) { @@ -270,34 +278,6 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re GLES20.GL_CLAMP_TO_EDGE); } - public void setTextLocation(float x, float y) { - mTextX = x; - mTextY = y; - } - - private void drawDefaultScreen() { - Drawable drawable = mContext.getResources().getDrawable(R.drawable.rearview); - Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_4444); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, mWidth, mHeight); - drawable.draw(canvas); - - Paint fontColor = new Paint(); - fontColor.setTextSize(Math.min(mWidth, mHeight) * 0.10f); - fontColor.setAntiAlias(true); - - // Pick a font color randomly - fontColor.setColor((0xFF << 24) + (mRandom.nextInt(0xFF) << 16) - + (mRandom.nextInt(0xFF) << 8) + mRandom.nextInt(0xFF)); - - // Set a location of the text relative to the surface size - canvas.drawText("The rearview is not available.", mTextX, mTextY, fontColor); - - // Draw - GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level = */ 0, bitmap, /* border = */ 0); - bitmap.recycle(); - } - private int loadShader(int shaderType, String source) { int shader = GLES20.glCreateShader(shaderType); if (shader == 0) { @@ -342,6 +322,10 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re checkGlError("glAttachShader"); GLES20.glAttachShader(program, fragmentShader); checkGlError("glAttachShader"); + + GLES20.glBindAttribLocation(program, 0, "pos"); + GLES20.glBindAttribLocation(program, 1, "tex"); + GLES20.glLinkProgram(program); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); @@ -363,21 +347,5 @@ public final class GLES20CarEvsCameraPreviewRenderer implements GLSurfaceView.Re } } - private Context mContext; - private int mProgram; - private int mTextureId; - private FloatBuffer mVertCarPos; - private FloatBuffer mVertCarTex; - private int mWidth; - private int mHeight; - private float mTextX; - private float mTextY; - private Random mRandom = new Random(); - - static { - System.loadLibrary("carevsglrenderer_jni"); - } - - // Native method to update the texture with a received frame buffer private native boolean nUpdateTexture(HardwareBuffer buffer, int textureId); } diff --git a/car-lib/src/android/car/telemetry/telemetry.proto b/car-lib/src/android/car/telemetry/telemetry.proto index de1dac1b5b..e384e83960 100644 --- a/car-lib/src/android/car/telemetry/telemetry.proto +++ b/car-lib/src/android/car/telemetry/telemetry.proto @@ -84,6 +84,7 @@ message StatsPublisher { // Collects all the app start events with the initial used RSS/CACHE/SWAP memory. APP_START_MEMORY_STATE_CAPTURED = 1; // Collects memory state of processes in 5-minute buckets (1 memory measurement per bucket). + // Consider using PROCESS_MEMORY_SNAPSHOT instead for smaller data size. PROCESS_MEMORY_STATE = 2; // Collects activity foreground/background transition events. ACTIVITY_FOREGROUND_STATE_CHANGED = 3; @@ -95,6 +96,10 @@ message StatsPublisher { ANR_OCCURRED = 6; // Collects "wtf"-level log events. WTF_OCCURRED = 7; + // Collects memory snapshot of processes in 5-minute buckets (1 memory measurement per bucket). + // It differs from PROCESS_MEMORY_STATE in that the snapshot can be used for leaked memory + // detection by tracking anon RSS + swap usage. + PROCESS_MEMORY_SNAPSHOT = 8; } // Required. diff --git a/car-maps-placeholder/res/values-ms/strings.xml b/car-maps-placeholder/res/values-ms/strings.xml index 6b68645abc..09a2c32753 100644 --- a/car-maps-placeholder/res/values-ms/strings.xml +++ b/car-maps-placeholder/res/values-ms/strings.xml @@ -16,6 +16,6 @@ limitations under the License. <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="app_name" msgid="6575346965016311017">"Peta"</string> + <string name="app_name" msgid="6575346965016311017">"Maps"</string> <string name="error_text" msgid="5575174711944349180">"Tiada aplikasi peta dipasang. Sila hubungi pengeluar kereta anda."</string> </resources> diff --git a/cpp/evs/manager/aidl/include/VirtualCamera.h b/cpp/evs/manager/aidl/include/VirtualCamera.h index 04e64aa5e3..20072158ff 100644 --- a/cpp/evs/manager/aidl/include/VirtualCamera.h +++ b/cpp/evs/manager/aidl/include/VirtualCamera.h @@ -27,6 +27,7 @@ #include <aidl/android/hardware/automotive/evs/IEvsDisplay.h> #include <aidl/android/hardware/automotive/evs/ParameterRange.h> #include <aidl/android/hardware/automotive/evs/Stream.h> +#include <android-base/thread_annotations.h> #include <condition_variable> #include <deque> @@ -78,7 +79,10 @@ public: virtual ~VirtualCamera(); unsigned getAllowedBuffers() { return mFramesAllowed; }; - bool isStreaming() { return mStreamState == RUNNING; } + bool isStreaming() { + std::lock_guard lock(mMutex); + return mStreamState == RUNNING; + } std::vector<std::shared_ptr<HalCamera>> getHalCameras(); void setDescriptor(aidlevs::CameraDesc* desc) { mDesc = desc; } @@ -87,7 +91,7 @@ public: bool deliverFrame(const aidlevs::BufferDesc& bufDesc); // Dump current status to a given file descriptor - std::string toString(const char* indent = "") const; + std::string toString(const char* indent = "") const NO_THREAD_SAFETY_ANALYSIS; private: void shutdown(); @@ -104,7 +108,7 @@ private: STOPPED, RUNNING, STOPPING, - } mStreamState = STOPPED; + } mStreamState GUARDED_BY(mMutex) = STOPPED; std::unordered_map<std::string, std::deque<aidlevs::BufferDesc>> mFramesHeld; std::thread mCaptureThread; diff --git a/cpp/evs/manager/aidl/src/VirtualCamera.cpp b/cpp/evs/manager/aidl/src/VirtualCamera.cpp index e1b2968a17..7aa6dd1816 100644 --- a/cpp/evs/manager/aidl/src/VirtualCamera.cpp +++ b/cpp/evs/manager/aidl/src/VirtualCamera.cpp @@ -463,8 +463,16 @@ ScopedAStatus VirtualCamera::startVideoStream(const std::shared_ptr<IEvsCameraSt constexpr auto kFrameTimeout = 5s; // timeout in seconds. int64_t lastFrameTimestamp = -1; EvsResult status = EvsResult::OK; - while (mStreamState == RUNNING) { + while (true) { std::unique_lock lock(mMutex); + ::android::base::ScopedLockAssertion assume_lock(mMutex); + + if (mStreamState != RUNNING) { + // A video stream is stopped while a capture thread is acquiring + // a lock. + LOG(DEBUG) << "Requested to stop capturing frames"; + break; + } unsigned count = 0; for (auto&& [key, hwCamera] : mHalCamera) { @@ -485,7 +493,7 @@ ScopedAStatus VirtualCamera::startVideoStream(const std::shared_ptr<IEvsCameraSt break; } - if (!mFramesReadySignal.wait_for(lock, kFrameTimeout, [this]() { + if (!mFramesReadySignal.wait_for(lock, kFrameTimeout, [this]() REQUIRES(mMutex) { // Stops waiting if // 1) we've requested to stop capturing // new frames @@ -498,35 +506,43 @@ ScopedAStatus VirtualCamera::startVideoStream(const std::shared_ptr<IEvsCameraSt LOG(DEBUG) << "Timer for a new frame expires"; status = EvsResult::UNDERLYING_SERVICE_ERROR; break; - } else if (mStreamState == RUNNING) { - // Fetch frames and forward to the client - if (!mFramesHeld.empty() && mStream) { - // Pass this buffer through to our client - std::vector<BufferDesc> frames; - frames.resize(count); - unsigned i = 0; - for (auto&& [key, hwCamera] : mHalCamera) { - std::shared_ptr<HalCamera> pHwCamera = hwCamera.lock(); - if (!pHwCamera || mFramesHeld[key].empty()) { - continue; - } - - // Duplicate the latest buffer and forward it to the - // active clients - auto frame = Utils::dupBufferDesc(mFramesHeld[key].back(), - /* doDup= */ true); - if (frame.timestamp > lastFrameTimestamp) { - lastFrameTimestamp = frame.timestamp; - } - frames[i++] = std::move(frame); - } - - if (!mStream->deliverFrame(frames).isOk()) { - LOG(WARNING) << "Failed to forward frames"; - } + } + + if (mStreamState != RUNNING || !mStream) { + // A video stream is stopped while a capture thread is waiting + // for a new frame or we have lost a client. + LOG(DEBUG) << "Requested to stop capturing frames or lost a client"; + break; + } + + // Fetch frames and forward to the client + if (mFramesHeld.empty()) { + // We do not have any frame to forward. + continue; + } + + // Pass this buffer through to our client + std::vector<BufferDesc> frames; + frames.resize(count); + unsigned i = 0; + for (auto&& [key, hwCamera] : mHalCamera) { + std::shared_ptr<HalCamera> pHwCamera = hwCamera.lock(); + if (!pHwCamera || mFramesHeld[key].empty()) { + continue; } - } else if (mStreamState != RUNNING) { - LOG(DEBUG) << "Requested to stop capturing frames"; + + // Duplicate the latest buffer and forward it to the + // active clients + auto frame = Utils::dupBufferDesc(mFramesHeld[key].back(), + /* doDup= */ true); + if (frame.timestamp > lastFrameTimestamp) { + lastFrameTimestamp = frame.timestamp; + } + frames[i++] = std::move(frame); + } + + if (!mStream->deliverFrame(frames).isOk()) { + LOG(WARNING) << "Failed to forward frames"; } } @@ -554,6 +570,10 @@ ScopedAStatus VirtualCamera::startVideoStream(const std::shared_ptr<IEvsCameraSt ScopedAStatus VirtualCamera::stopVideoStream() { { std::lock_guard lock(mMutex); + if (mStreamState != RUNNING) { + // No action is required. + return ScopedAStatus::ok(); + } if (!mStream || mStreamState != RUNNING) { // Safely ignore a request to stop video stream @@ -570,7 +590,7 @@ ScopedAStatus VirtualCamera::stopVideoStream() { EvsEventDesc event{ .aType = EvsEventType::STREAM_STOPPED, }; - if (!mStream->notify(std::move(event)).isOk()) { + if (mStream && !mStream->notify(std::move(event)).isOk()) { LOG(WARNING) << "Error delivering end of stream event"; } @@ -579,20 +599,20 @@ ScopedAStatus VirtualCamera::stopVideoStream() { // Note, however, that there still might be frames already queued that client will see // after returning from the client side of this call. mStreamState = STOPPED; + } - // Give the underlying hardware camera the heads up that it might be time to stop - for (auto&& [_, hwCamera] : mHalCamera) { - auto pHwCamera = hwCamera.lock(); - if (pHwCamera) { - pHwCamera->clientStreamEnding(this); - } + // Give the underlying hardware camera the heads up that it might be time to stop + for (auto&& [_, hwCamera] : mHalCamera) { + auto pHwCamera = hwCamera.lock(); + if (pHwCamera) { + pHwCamera->clientStreamEnding(this); } - - // Signal a condition to unblock a capture thread and then join - mSourceCameras.clear(); - mFramesReadySignal.notify_all(); } + // Signal a condition to unblock a capture thread and then join + mSourceCameras.clear(); + mFramesReadySignal.notify_all(); + if (mCaptureThread.joinable()) { mCaptureThread.join(); } @@ -740,18 +760,23 @@ bool VirtualCamera::deliverFrame(const BufferDesc& bufDesc) { bool VirtualCamera::notify(const EvsEventDesc& event) { switch (event.aType) { - case EvsEventType::STREAM_STOPPED: - if (mStreamState != STOPPING) { + case EvsEventType::STREAM_STOPPED: { + { + std::lock_guard lock(mMutex); + if (mStreamState != RUNNING) { + // We're not actively consuming a video stream or already in + // a process to stop a video stream. + return true; + } + // Warn if we got an unexpected stream termination LOG(WARNING) << "Stream unexpectedly stopped, current status " << mStreamState; - - // Clean up the resource and forward an event to the client - stopVideoStream(); - - // This event is handled properly. - return true; } - break; + + // Clean up the resource and forward an event to the client + stopVideoStream(); + return true; + } // v1.0 client will ignore all other events. case EvsEventType::PARAMETER_CHANGED: diff --git a/packages/ScriptExecutor/src/BundleWrapper.cpp b/packages/ScriptExecutor/src/BundleWrapper.cpp index 01c4d24c78..799f37ca1f 100644 --- a/packages/ScriptExecutor/src/BundleWrapper.cpp +++ b/packages/ScriptExecutor/src/BundleWrapper.cpp @@ -107,6 +107,37 @@ Result<void> BundleWrapper::putString(const char* key, const char* value) { return {}; // ok result } +Result<void> BundleWrapper::putBooleanArray(const char* key, + const std::vector<unsigned char>& value) { + ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key)); + if (keyStringRef == nullptr) { + return Error() << "Failed to create a string for a key=" << key << " due to OOM error"; + } + + jmethodID putBooleanArrayMethod = + mJNIEnv->GetMethodID(mBundleClass, "putBooleanArray", "(Ljava/lang/String;[Z)V"); + + ScopedLocalRef<jbooleanArray> arrayRef(mJNIEnv, mJNIEnv->NewBooleanArray(value.size())); + mJNIEnv->SetBooleanArrayRegion(arrayRef.get(), 0, value.size(), &value.at(0)); + mJNIEnv->CallVoidMethod(mBundle, putBooleanArrayMethod, keyStringRef.get(), arrayRef.get()); + return {}; // ok result +} + +Result<void> BundleWrapper::putDoubleArray(const char* key, const std::vector<double>& value) { + ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key)); + if (keyStringRef == nullptr) { + return Error() << "Failed to create a string for a key=" << key << " due to OOM error"; + } + + jmethodID putDoubleArrayMethod = + mJNIEnv->GetMethodID(mBundleClass, "putDoubleArray", "(Ljava/lang/String;[D)V"); + + ScopedLocalRef<jdoubleArray> arrayRef(mJNIEnv, mJNIEnv->NewDoubleArray(value.size())); + mJNIEnv->SetDoubleArrayRegion(arrayRef.get(), 0, value.size(), &value[0]); + mJNIEnv->CallVoidMethod(mBundle, putDoubleArrayMethod, keyStringRef.get(), arrayRef.get()); + return {}; // ok result +} + Result<void> BundleWrapper::putLongArray(const char* key, const std::vector<int64_t>& value) { ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key)); if (keyStringRef == nullptr) { diff --git a/packages/ScriptExecutor/src/BundleWrapper.h b/packages/ScriptExecutor/src/BundleWrapper.h index 094f1dd33c..e7473d8e80 100644 --- a/packages/ScriptExecutor/src/BundleWrapper.h +++ b/packages/ScriptExecutor/src/BundleWrapper.h @@ -45,6 +45,9 @@ public: ::android::base::Result<void> putLong(const char* key, int64_t value); ::android::base::Result<void> putDouble(const char* key, double value); ::android::base::Result<void> putString(const char* key, const char* value); + ::android::base::Result<void> putBooleanArray(const char* key, + const std::vector<unsigned char>& value); + ::android::base::Result<void> putDoubleArray(const char* key, const std::vector<double>& value); ::android::base::Result<void> putLongArray(const char* key, const std::vector<int64_t>& value); ::android::base::Result<void> putStringArray(const char* key, const std::vector<std::string>& value); diff --git a/packages/ScriptExecutor/src/JniUtils.cpp b/packages/ScriptExecutor/src/JniUtils.cpp index b33bfc24a0..119debd6ec 100644 --- a/packages/ScriptExecutor/src/JniUtils.cpp +++ b/packages/ScriptExecutor/src/JniUtils.cpp @@ -55,12 +55,11 @@ void pushBundleToLuaTable(JNIEnv* env, lua_State* lua, jobject bundle) { ScopedLocalRef<jclass> longClass(env, env->FindClass("java/lang/Long")); ScopedLocalRef<jclass> numberClass(env, env->FindClass("java/lang/Number")); ScopedLocalRef<jclass> stringClass(env, env->FindClass("java/lang/String")); + ScopedLocalRef<jclass> booleanArrayClass(env, env->FindClass("[Z")); ScopedLocalRef<jclass> intArrayClass(env, env->FindClass("[I")); ScopedLocalRef<jclass> longArrayClass(env, env->FindClass("[J")); ScopedLocalRef<jclass> doubleArrayClass(env, env->FindClass("[D")); ScopedLocalRef<jclass> stringArrayClass(env, env->FindClass("[Ljava/lang/String;")); - // TODO(b/188816922): Handle more types such as float and integer arrays, - // and perhaps nested Bundles. jmethodID getMethod = env->GetMethodID(persistableBundleClass.get(), "get", "(Ljava/lang/String;)Ljava/lang/Object;"); @@ -100,6 +99,23 @@ void pushBundleToLuaTable(JNIEnv* env, lua_State* lua, jobject bundle) { env->GetStringUTFChars(static_cast<jstring>(value.get()), nullptr); lua_pushstring(lua, rawStringValue); env->ReleaseStringUTFChars(static_cast<jstring>(value.get()), rawStringValue); + } else if (env->IsInstanceOf(value.get(), booleanArrayClass.get())) { + jbooleanArray booleanArray = static_cast<jbooleanArray>(value.get()); + const auto kLength = env->GetArrayLength(booleanArray); + // Arrays are represented as a table of sequential elements in Lua. + // We are creating a nested table to represent this array. We specify number of elements + // in the Java array to preallocate memory accordingly. + lua_createtable(lua, kLength, 0); + jboolean* rawBooleanArray = env->GetBooleanArrayElements(booleanArray, nullptr); + // Fills in the table at stack idx -2 with key value pairs, where key is a + // Lua index and value is an integer from the long array at that index + for (int i = 0; i < kLength; i++) { + lua_pushboolean(lua, rawBooleanArray[i]); + lua_rawseti(lua, /* idx= */ -2, + i + 1); // lua index starts from 1 + } + // JNI_ABORT is used because we do not need to copy back elements. + env->ReleaseBooleanArrayElements(booleanArray, rawBooleanArray, JNI_ABORT); } else if (env->IsInstanceOf(value.get(), intArrayClass.get())) { jintArray intArray = static_cast<jintArray>(value.get()); const auto kLength = env->GetArrayLength(intArray); @@ -264,6 +280,8 @@ Result<void> convertLuaTableToBundle(JNIEnv* env, lua_State* lua, BundleWrapper* "is unrecoverable."; } + std::vector<unsigned char> boolArray; + std::vector<double> doubleArray; std::vector<int64_t> longArray; std::vector<std::string> stringArray; int originalLuaType = LUA_TNIL; @@ -289,13 +307,15 @@ Result<void> convertLuaTableToBundle(JNIEnv* env, lua_State* lua, BundleWrapper* "unrecoverable."; } switch (currentType) { + case LUA_TBOOLEAN: + boolArray.push_back( + static_cast<unsigned char>(lua_toboolean(lua, /* index = */ -1))); + break; case LUA_TNUMBER: - if (!lua_isinteger(lua, /* index = */ -1)) { - return Error() << "Returned value for key=" << key - << " contains a floating number array, which is not " - "supported yet."; - } else { + if (lua_isinteger(lua, /* index = */ -1)) { longArray.push_back(lua_tointeger(lua, /* index = */ -1)); + } else { + doubleArray.push_back(lua_tonumber(lua, /* index = */ -1)); } break; case LUA_TSTRING: @@ -313,8 +333,15 @@ Result<void> convertLuaTableToBundle(JNIEnv* env, lua_State* lua, BundleWrapper* lua_pop(lua, 1); } switch (originalLuaType) { + case LUA_TBOOLEAN: + bundleInsertionResult = bundleWrapper->putBooleanArray(key, boolArray); + break; case LUA_TNUMBER: - bundleInsertionResult = bundleWrapper->putLongArray(key, longArray); + if (longArray.size() > 0) { + bundleInsertionResult = bundleWrapper->putLongArray(key, longArray); + } else { + bundleInsertionResult = bundleWrapper->putDoubleArray(key, doubleArray); + } break; case LUA_TSTRING: bundleInsertionResult = bundleWrapper->putStringArray(key, stringArray); diff --git a/packages/ScriptExecutor/tests/functional/src/com/android/car/scriptexecutortest/functional/ScriptExecutorFunctionalTest.java b/packages/ScriptExecutor/tests/functional/src/com/android/car/scriptexecutortest/functional/ScriptExecutorFunctionalTest.java index e0760ef9b3..8ae1f27a8f 100644 --- a/packages/ScriptExecutor/tests/functional/src/com/android/car/scriptexecutortest/functional/ScriptExecutorFunctionalTest.java +++ b/packages/ScriptExecutor/tests/functional/src/com/android/car/scriptexecutortest/functional/ScriptExecutorFunctionalTest.java @@ -284,16 +284,22 @@ public final class ScriptExecutorFunctionalTest { String script = "function arrays(data, state)\n" + " result = {}\n" + + " result.boolean_array = state.boolean_array\n" + + " result.double_array = state.double_array\n" + " result.int_array = state.int_array\n" + " result.long_array = state.long_array\n" + " result.string_array = state.string_array\n" + " on_success(result)\n" + "end\n"; PersistableBundle previousState = new PersistableBundle(); + boolean[] boolean_array = new boolean[] {true, true, false}; + double[] double_array = new double[] {1.5, 2.222}; int[] int_array = new int[] {1, 2}; long[] int_array_in_long = new long[] {1, 2}; long[] long_array = new long[] {1, 2, 3}; String[] string_array = new String[] {"one", "two", "three"}; + previousState.putBooleanArray("boolean_array", boolean_array); + previousState.putDoubleArray("double_array", double_array); previousState.putIntArray("int_array", int_array); previousState.putLongArray("long_array", long_array); previousState.putStringArray("string_array", string_array); @@ -301,9 +307,12 @@ public final class ScriptExecutorFunctionalTest { runScriptAndWaitForResponse(script, "arrays", mEmptyPublishedData, previousState); // Verify that keys are preserved but the values are modified as expected. - assertThat(mListener.mInterimResult.size()).isEqualTo(3); + assertThat(mListener.mInterimResult.size()).isEqualTo(5); // Lua has only one lua_Integer. Here Java long is used to represent it when data is // transferred from Lua to CarTelemetryService. + assertThat(mListener.mInterimResult.getBooleanArray("boolean_array")) + .isEqualTo(boolean_array); + assertThat(mListener.mInterimResult.getDoubleArray("double_array")).isEqualTo(double_array); assertThat(mListener.mInterimResult.getLongArray("int_array")).isEqualTo(int_array_in_long); assertThat(mListener.mInterimResult.getLongArray("long_array")).isEqualTo(long_array); assertThat(mListener.mInterimResult.getStringArray("string_array")).isEqualTo(string_array); @@ -886,52 +895,6 @@ public final class ScriptExecutorFunctionalTest { } @Test - public void invokeScript_returnedFloatingArraysNotSupported() - throws RemoteException, InterruptedException { - // Verifies that we do not support return values that contain floating number arrays. - String script = - "function floating_point_arrays(data, state)\n" - + " array = {}\n" - + " array[0] = 1.1\n" - + " array[1] = 1.2\n" - + " result = {data = array}\n" - + " on_success(result)\n" - + "end\n"; - - runScriptAndWaitForResponse( - script, "floating_point_arrays", mEmptyPublishedData, mEmptyIterimResult); - - // Verify that the expected error is received. - assertThat(mListener.mErrorType) - .isEqualTo(IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR); - assertThat(mListener.mMessage) - .contains("a floating number array, which is not supported yet"); - } - - @Test - public void invokeScript_returnedBooleanArraysNotSupported() - throws RemoteException, InterruptedException { - // Verifies that we do not yet support return values that contain boolean arrays. - String script = - "function array_of_booleans(data, state)\n" - + " array = {}\n" - + " array[0] = false\n" - + " array[1] = true\n" - + " result = {data = array}\n" - + " on_success(result)\n" - + "end\n"; - - runScriptAndWaitForResponse( - script, "array_of_booleans", mEmptyPublishedData, mEmptyIterimResult); - - // Verify that the expected error is received. - assertThat(mListener.mErrorType) - .isEqualTo(IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR); - assertThat(mListener.mMessage) - .contains("is an array with values of type=boolean, which is not supported yet"); - } - - @Test public void invokeScript_onMetricsReport_returnsReport() throws Exception { String returnFinalResultScript = "function script_completes(data, state)\n" diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTest.java b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTest.java index be10d69719..3e4f33eb1d 100644 --- a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTest.java +++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTest.java @@ -38,6 +38,7 @@ public final class JniUtilsTest { private static final String INT_KEY = "int_key"; private static final String STRING_KEY = "string_key"; private static final String NUMBER_KEY = "number_key"; + private static final String BOOLEAN_ARRAY_KEY = "boolean_array_key"; private static final String INT_ARRAY_KEY = "int_array_key"; private static final String LONG_ARRAY_KEY = "long_array_key"; private static final String DOUBLE_ARRAY_KEY = "double_array_key"; @@ -48,6 +49,7 @@ public final class JniUtilsTest { private static final int INT_VALUE = 10; private static final int INT_VALUE_2 = 20; private static final String STRING_VALUE = "test"; + private static final boolean[] BOOLEAN_ARRAY_VALUE = new boolean[]{true, false, true}; private static final int[] INT_ARRAY_VALUE = new int[]{1, 2, 3}; private static final long[] LONG_ARRAY_VALUE = new long[]{1, 2, 3, 4}; private static final double[] DOUBLE_ARRAY_VALUE = new double[]{1.1d, 2.2d, 3.3d, 4.4d}; @@ -97,6 +99,9 @@ public final class JniUtilsTest { private native boolean nativeHasIntValue(long luaEnginePtr, String key, int value); + private native boolean nativeHasBooleanArrayValue( + long luaEnginePtr, String key, boolean[] value); + private native boolean nativeHasDoubleValue(long luaEnginePtr, String key, double value); private native boolean nativeHasIntArrayValue(long luaEnginePtr, String key, int[] value); @@ -163,6 +168,7 @@ public final class JniUtilsTest { @Test public void pushBundleToLuaTable_arrays() { PersistableBundle bundle = new PersistableBundle(); + bundle.putBooleanArray(BOOLEAN_ARRAY_KEY, BOOLEAN_ARRAY_VALUE); bundle.putIntArray(INT_ARRAY_KEY, INT_ARRAY_VALUE); bundle.putLongArray(LONG_ARRAY_KEY, LONG_ARRAY_VALUE); bundle.putDoubleArray(DOUBLE_ARRAY_KEY, DOUBLE_ARRAY_VALUE); @@ -174,6 +180,8 @@ public final class JniUtilsTest { // Check contents of Lua table. // Java int and long arrays both end up being arrays of Lua's Integer type, // which is interpreted as a 8-byte int type. + assertThat(nativeHasBooleanArrayValue( + mLuaEnginePtr, BOOLEAN_ARRAY_KEY, BOOLEAN_ARRAY_VALUE)).isTrue(); assertThat(nativeHasIntArrayValue(mLuaEnginePtr, INT_ARRAY_KEY, INT_ARRAY_VALUE)).isTrue(); assertThat(nativeHasLongArrayValue(mLuaEnginePtr, LONG_ARRAY_KEY, LONG_ARRAY_VALUE)) .isTrue(); diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTestHelper.cpp b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTestHelper.cpp index ce3be6660a..5597e50d1a 100644 --- a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTestHelper.cpp +++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutortest/unit/JniUtilsTestHelper.cpp @@ -72,6 +72,44 @@ bool hasValidNumberArray(JNIEnv* env, jobject object, jlong luaEnginePtr, jstrin return result; } +template <typename T> +bool hasValidBooleanArray(JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, + T rawInputArray, const int arrayLength) { + const char* rawKey = env->GetStringUTFChars(key, nullptr); + scriptexecutor::LuaEngine* engine = + reinterpret_cast<scriptexecutor::LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)); + // Assumes the table is on top of the stack. + auto* luaState = engine->getLuaState(); + lua_pushstring(luaState, rawKey); + env->ReleaseStringUTFChars(key, rawKey); + lua_gettable(luaState, -2); + bool result = false; + if (!lua_istable(luaState, -1)) { + result = false; + } else { + // First, compare the input and Lua array sizes. + const auto kActualLength = lua_rawlen(luaState, -1); + if (arrayLength != kActualLength) { + // No need to compare further if number of elements in the two arrays are not equal. + result = false; + } else { + // Do element by element comparison. + bool is_equal = true; + for (int i = 0; i < arrayLength; ++i) { + lua_rawgeti(luaState, -1, i + 1); + is_equal = lua_isboolean(luaState, /* idx = */ -1) && + lua_toboolean(luaState, /* idx = */ -1) == + static_cast<bool>(rawInputArray[i]); + lua_pop(luaState, 1); + if (!is_equal) break; + } + result = is_equal; + } + } + lua_pop(luaState, 1); + return result; +} + extern "C" { #include "lua.h" @@ -198,6 +236,16 @@ Java_com_android_car_scriptexecutortest_unit_JniUtilsTest_nativeHasStringValue( } JNIEXPORT bool JNICALL +Java_com_android_car_scriptexecutortest_unit_JniUtilsTest_nativeHasBooleanArrayValue( + JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jbooleanArray value) { + jboolean* rawInputArray = env->GetBooleanArrayElements(value, nullptr); + const auto kInputLength = env->GetArrayLength(value); + bool result = hasValidBooleanArray(env, object, luaEnginePtr, key, rawInputArray, kInputLength); + env->ReleaseBooleanArrayElements(value, rawInputArray, JNI_ABORT); + return result; +} + +JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutortest_unit_JniUtilsTest_nativeHasIntArrayValue( JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jintArray value) { jint* rawInputArray = env->GetIntArrayElements(value, nullptr); diff --git a/service/proto/android/car/telemetry/atoms.proto b/service/proto/android/car/telemetry/atoms.proto index 4aad7166f9..cd504f1c1d 100644 --- a/service/proto/android/car/telemetry/atoms.proto +++ b/service/proto/android/car/telemetry/atoms.proto @@ -37,6 +37,7 @@ message Atom { oneof pulled { ProcessMemoryState process_memory_state = 10013; ProcessCpuTime process_cpu_time = 10035; + ProcessMemorySnapshot process_memory_snapshot = 10064; } } @@ -167,6 +168,19 @@ message WTFOccurred { optional ErrorSource error_source = 5; } +message ProcessMemorySnapshot { + optional int32 uid = 1; + optional string process_name = 2; + optional int32 pid = 3; + optional int32 oom_score_adj = 4; + optional int32 rss_in_kilobytes = 5; + optional int32 anon_rss_in_kilobytes = 6; + optional int32 swap_in_kilobytes = 7; + optional int32 anon_rss_and_swap_in_kilobytes = 8; + optional int32 gpu_memory_kb = 9; + optional bool has_foreground_services = 10; +} + message CarPowerStateChanged { enum State { WAIT_FOR_VHAL = 0; diff --git a/service/src/com/android/car/telemetry/publisher/StatsPublisher.java b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java index 328ef2c0f2..2897f2c2e8 100644 --- a/service/src/com/android/car/telemetry/publisher/StatsPublisher.java +++ b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java @@ -21,6 +21,7 @@ import static com.android.car.telemetry.AtomsProto.Atom.ANR_OCCURRED_FIELD_NUMBE import static com.android.car.telemetry.AtomsProto.Atom.APP_CRASH_OCCURRED_FIELD_NUMBER; import static com.android.car.telemetry.AtomsProto.Atom.APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER; import static com.android.car.telemetry.AtomsProto.Atom.PROCESS_CPU_TIME_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.Atom.PROCESS_MEMORY_SNAPSHOT_FIELD_NUMBER; import static com.android.car.telemetry.AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER; import static com.android.car.telemetry.AtomsProto.Atom.WTF_OCCURRED_FIELD_NUMBER; import static com.android.car.telemetry.CarTelemetryService.DEBUG; @@ -41,6 +42,7 @@ import android.util.LongSparseArray; import com.android.car.CarLog; import com.android.car.telemetry.AtomsProto.ProcessCpuTime; +import com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot; import com.android.car.telemetry.AtomsProto.ProcessMemoryState; import com.android.car.telemetry.ResultStore; import com.android.car.telemetry.StatsLogProto; @@ -98,6 +100,10 @@ public class StatsPublisher extends AbstractPublisher { static final long WTF_OCCURRED_ATOM_MATCHER_ID = 13; @VisibleForTesting static final long WTF_OCCURRED_EVENT_METRIC_ID = 14; + @VisibleForTesting + static final long PROCESS_MEMORY_SNAPSHOT_ATOM_MATCHER_ID = 15; + @VisibleForTesting + static final long PROCESS_MEMORY_SNAPSHOT_GAUGE_METRIC_ID = 16; // TODO(b/202115033): Flatten the load spike by pulling reports for each MetricsConfigs // using separate periodical timers. @@ -135,6 +141,30 @@ public class StatsPublisher extends AbstractPublisher { .setField(ProcessCpuTime.SYSTEM_TIME_MILLIS_FIELD_NUMBER)) .build(); + @VisibleForTesting + static final StatsdConfigProto.FieldMatcher PROCESS_MEMORY_SNAPSHOT_FIELDS_MATCHER = + StatsdConfigProto.FieldMatcher.newBuilder() + .setField( + PROCESS_MEMORY_SNAPSHOT_FIELD_NUMBER) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.PID_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.OOM_SCORE_ADJ_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.RSS_IN_KILOBYTES_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.ANON_RSS_IN_KILOBYTES_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.SWAP_IN_KILOBYTES_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot + .ANON_RSS_AND_SWAP_IN_KILOBYTES_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.GPU_MEMORY_KB_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.HAS_FOREGROUND_SERVICES_FIELD_NUMBER)) + .build(); + private final StatsManagerProxy mStatsManager; private final ResultStore mResultStore; private final Handler mTelemetryHandler; @@ -192,12 +222,10 @@ public class StatsPublisher extends AbstractPublisher { if (!mIsPullingReports) { if (DEBUG) { - Slogf.d(CarLog.TAG_TELEMETRY, "Stats report will be pulled in " - + PULL_REPORTS_PERIOD.toMinutes() + " minutes."); + Slogf.d(CarLog.TAG_TELEMETRY, "Triggering pull stats reports"); } mIsPullingReports = true; - mTelemetryHandler.postDelayed( - mPullReportsPeriodically, PULL_REPORTS_PERIOD.toMillis()); + mTelemetryHandler.post(mPullReportsPeriodically); } } @@ -247,12 +275,17 @@ public class StatsPublisher extends AbstractPublisher { case WTF_OCCURRED: metricId = WTF_OCCURRED_EVENT_METRIC_ID; break; + case PROCESS_MEMORY_SNAPSHOT: + metricId = PROCESS_MEMORY_SNAPSHOT_GAUGE_METRIC_ID; + break; default: return; } if (!metricBundles.containsKey(metricId)) { Slogf.w(CarLog.TAG_TELEMETRY, - "No reports for metric id " + metricId + " for config " + configKey); + "No reports for metric id " + metricId + " (" + + subscriber.getPublisherParam().getStats().getSystemMetric() + + ") for config " + configKey); return; } PersistableBundle bundle = metricBundles.get(metricId); @@ -551,6 +584,8 @@ public class StatsPublisher extends AbstractPublisher { return buildAnrOccurredStatsdConfig(builder); case WTF_OCCURRED: return buildWtfOccurredStatsdConfig(builder); + case PROCESS_MEMORY_SNAPSHOT: + return buildProcessMemorySnapshotStatsdConfig(builder); default: throw new IllegalArgumentException("Unsupported metric " + metric.name()); } @@ -702,6 +737,39 @@ public class StatsPublisher extends AbstractPublisher { .build(); } + @NonNull + private static StatsdConfig buildProcessMemorySnapshotStatsdConfig( + @NonNull StatsdConfig.Builder builder) { + return builder + .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder() + // The id must be unique within StatsdConfig/matchers + .setId(PROCESS_MEMORY_SNAPSHOT_ATOM_MATCHER_ID) + .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder() + .setAtomId(PROCESS_MEMORY_SNAPSHOT_FIELD_NUMBER))) + .addGaugeMetric(StatsdConfigProto.GaugeMetric.newBuilder() + // The id must be unique within StatsdConfig/metrics + .setId(PROCESS_MEMORY_SNAPSHOT_GAUGE_METRIC_ID) + .setWhat(PROCESS_MEMORY_SNAPSHOT_ATOM_MATCHER_ID) + .setDimensionsInWhat(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(PROCESS_MEMORY_SNAPSHOT_FIELD_NUMBER) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.UID_FIELD_NUMBER)) + .addChild(StatsdConfigProto.FieldMatcher.newBuilder() + .setField(ProcessMemorySnapshot.PROCESS_NAME_FIELD_NUMBER)) + ) + .setGaugeFieldsFilter(StatsdConfigProto.FieldFilter.newBuilder() + .setFields(PROCESS_MEMORY_SNAPSHOT_FIELDS_MATCHER) + ) // setGaugeFieldsFilter + .setSamplingType( + StatsdConfigProto.GaugeMetric.SamplingType.RANDOM_ONE_SAMPLE) + .setBucket(StatsdConfigProto.TimeUnit.FIVE_MINUTES) + ) + .addPullAtomPackages(StatsdConfigProto.PullAtomPackages.newBuilder() + .setAtomId(PROCESS_MEMORY_SNAPSHOT_FIELD_NUMBER) + .addPackages("AID_SYSTEM")) + .build(); + } + @Override protected void handleSessionStateChange(SessionAnnotation annotation) {} } diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java index c9227f5006..85e0eaecdc 100644 --- a/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java +++ b/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java @@ -44,7 +44,8 @@ public class AtomListConverter { // Map of pulled atom cases to corresponding atom converter. private static Map<Atom.PulledCase, AbstractAtomConverter<?>> sPulledCaseConverters = Map.of( Atom.PulledCase.PROCESS_MEMORY_STATE, new ProcessMemoryStateConverter(), - Atom.PulledCase.PROCESS_CPU_TIME, new ProcessCpuTimeConverter()); + Atom.PulledCase.PROCESS_CPU_TIME, new ProcessCpuTimeConverter(), + Atom.PulledCase.PROCESS_MEMORY_SNAPSHOT, new ProcessMemorySnapshotConverter()); /** * Converts a list of atoms to separate the atoms fields values into arrays to be put into the diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemorySnapshotConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemorySnapshotConverter.java new file mode 100644 index 0000000000..42845e87e1 --- /dev/null +++ b/service/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemorySnapshotConverter.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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 com.android.car.telemetry.publisher.statsconverters; + +import android.annotation.NonNull; +import android.util.SparseArray; + +import com.android.car.telemetry.AtomsProto.Atom; +import com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot; +import com.android.internal.util.Preconditions; + +/** + * Atom data converter for atoms of type {@link ProcessMemorySnapshot}. + */ +public class ProcessMemorySnapshotConverter extends AbstractAtomConverter<ProcessMemorySnapshot> { + + private static final SparseArray<AtomFieldAccessor<ProcessMemorySnapshot, ?>> + sAtomFieldAccessorMap = new SparseArray<>(); + static { + sAtomFieldAccessorMap.append(1, new AtomFieldAccessor<>( + "uid", + a -> a.hasUid(), + a -> a.getUid() + )); + sAtomFieldAccessorMap.append(2, new AtomFieldAccessor<>( + "process_name", + a -> a.hasProcessName(), + a -> a.getProcessName() + )); + sAtomFieldAccessorMap.append(3, new AtomFieldAccessor<>( + "pid", + a -> a.hasPid(), + a -> a.getPid() + )); + sAtomFieldAccessorMap.append(4, new AtomFieldAccessor<>( + "oom_score_adj", + a -> a.hasOomScoreAdj(), + a -> a.getOomScoreAdj() + )); + sAtomFieldAccessorMap.append(5, new AtomFieldAccessor<>( + "rss_in_kilobytes", + a -> a.hasRssInKilobytes(), + a -> a.getRssInKilobytes() + )); + sAtomFieldAccessorMap.append(6, new AtomFieldAccessor<>( + "anon_rss_in_kilobytes", + a -> a.hasAnonRssInKilobytes(), + a -> a.getAnonRssInKilobytes() + )); + sAtomFieldAccessorMap.append(7, new AtomFieldAccessor<>( + "swap_in_kilobytes", + a -> a.hasSwapInKilobytes(), + a -> a.getSwapInKilobytes() + )); + sAtomFieldAccessorMap.append(8, new AtomFieldAccessor<>( + "anon_rss_and_swap_in_kilobytes", + a -> a.hasAnonRssAndSwapInKilobytes(), + a -> a.getAnonRssAndSwapInKilobytes() + )); + sAtomFieldAccessorMap.append(9, new AtomFieldAccessor<>( + "gpu_memory_kb", + a -> a.hasGpuMemoryKb(), + a -> a.getGpuMemoryKb() + )); + sAtomFieldAccessorMap.append(10, new AtomFieldAccessor<>( + "has_foreground_services", + a -> a.hasHasForegroundServices(), + a -> a.getHasForegroundServices() + )); + } + + ProcessMemorySnapshotConverter() { + super(); + } + + @NonNull + @Override + SparseArray<AtomFieldAccessor<ProcessMemorySnapshot, ?>> getAtomFieldAccessorMap() { + return sAtomFieldAccessorMap; + } + + @NonNull + @Override + ProcessMemorySnapshot getAtomData(@NonNull Atom atom) { + Preconditions.checkArgument( + atom.hasProcessMemorySnapshot(), "Atom doesn't contain ProcessMemorySnapshot"); + return atom.getProcessMemorySnapshot(); + } + + @NonNull + @Override + String getAtomDataClassName() { + return ProcessMemorySnapshot.class.getSimpleName(); + } +} diff --git a/tests/CarEvsCameraPreviewApp/Android.bp b/tests/CarEvsCameraPreviewApp/Android.bp index fea6aafeac..a570123ae8 100644 --- a/tests/CarEvsCameraPreviewApp/Android.bp +++ b/tests/CarEvsCameraPreviewApp/Android.bp @@ -43,11 +43,14 @@ android_app { }, libs: [ + "android.car", "android.car-system-stubs", - "androidx.annotation_annotation", ], - static_libs: ["androidx.annotation_annotation"], + static_libs: [ + "androidx.annotation_annotation", + "car-evs-helper-lib", + ], // To make this app be able to re-installed use_embedded_native_libs: true, diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraGLSurfaceView.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraGLSurfaceView.java deleted file mode 100644 index 0383b74996..0000000000 --- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraGLSurfaceView.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2021 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 com.google.android.car.evs; - -import android.content.Context; -import android.opengl.GLSurfaceView; -import android.view.MotionEvent; - -/** - * GPU-backed SurfaceView to render a hardware buffer described by CarEvsBufferDescriptor. - */ -public final class CarEvsCameraGLSurfaceView extends GLSurfaceView { - private static final String TAG = CarEvsCameraGLSurfaceView.class.getSimpleName(); - - private final GLES20CarEvsCameraPreviewRenderer mRenderer; - - public CarEvsCameraGLSurfaceView(Context context, CarEvsCameraPreviewActivity activity) { - super(context); - setEGLContextClientVersion(2); - - mRenderer = new GLES20CarEvsCameraPreviewRenderer(context, activity); - setRenderer(mRenderer); - - setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); - } - - @Override - public boolean onTouchEvent(MotionEvent e) { - float x = e.getX(); - float y = e.getY(); - - // Update a location of a text to tell the rearview is not available. - mRenderer.setTextLocation(x, y); - - return true; - } - - public void clearBuffer() { - mRenderer.clearBuffer(); - } -} diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java index 01c0667213..4bdcf03b36 100644 --- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java +++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java @@ -44,11 +44,16 @@ import android.widget.LinearLayout; import androidx.annotation.GuardedBy; +import com.android.car.internal.evs.CarEvsGLSurfaceView; +import com.android.car.internal.evs.GLES20CarEvsBufferRenderer; + import java.util.ArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -public class CarEvsCameraPreviewActivity extends Activity { +public class CarEvsCameraPreviewActivity extends Activity + implements CarEvsGLSurfaceView.BufferCallback { + private static final String TAG = CarEvsCameraPreviewActivity.class.getSimpleName(); /** * ActivityManagerService encodes the reason for a request to close system dialogs with this @@ -97,7 +102,7 @@ public class CarEvsCameraPreviewActivity extends Activity { private final ExecutorService mCallbackExecutor = Executors.newFixedThreadPool(1); /** GL backed surface view to render the camera preview */ - private CarEvsCameraGLSurfaceView mEvsView; + private CarEvsGLSurfaceView mEvsView; private ViewGroup mRootView; private LinearLayout mPreviewContainer; @@ -142,7 +147,7 @@ public class CarEvsCameraPreviewActivity extends Activity { if (mStreamState == STREAM_STATE_INVISIBLE) { // When the activity becomes invisible (e.g. goes background), we immediately // returns received frame buffers instead of stopping a video stream. - returnBufferLocked(buffer); + doneWithBufferLocked(buffer); } else { // Enqueues a new frame and posts a rendering job mBufferQueue.add(buffer); @@ -255,7 +260,8 @@ public class CarEvsCameraPreviewActivity extends Activity { Car.createCar(getApplicationContext(), /* handler = */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener); - mEvsView = new CarEvsCameraGLSurfaceView(getApplication(), this); + mEvsView = CarEvsGLSurfaceView.create(getApplication(), this, getApplicationContext() + .getResources().getInteger(R.integer.config_evsRearviewCameraInPlaneRotationAngle)); mRootView = (ViewGroup) LayoutInflater.from(this).inflate( R.layout.evs_preview_activity, /* root= */ null); mPreviewContainer = mRootView.findViewById(R.id.evs_preview_container); @@ -439,8 +445,8 @@ public class CarEvsCameraPreviewActivity extends Activity { return state; } - /** Get a new frame */ - CarEvsBufferDescriptor getNewFrame() { + @Override + public CarEvsBufferDescriptor onBufferRequested() { synchronized (mLock) { if (mBufferQueue.isEmpty()) { return null; @@ -455,15 +461,15 @@ public class CarEvsCameraPreviewActivity extends Activity { } } - /** Request to return a buffer we're done with */ - void returnBuffer(CarEvsBufferDescriptor buffer) { + @Override + public void onBufferProcessed(CarEvsBufferDescriptor buffer) { synchronized (mLock) { - returnBufferLocked(buffer); + doneWithBufferLocked(buffer); } } @GuardedBy("mLock") - private void returnBufferLocked(CarEvsBufferDescriptor buffer) { + private void doneWithBufferLocked(CarEvsBufferDescriptor buffer) { try { mEvsManager.returnFrameBuffer(buffer); } catch (Exception e) { diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml index 2a5d05ffeb..8340a55b6f 100644 --- a/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml +++ b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml @@ -364,6 +364,31 @@ android:layout_height="wrap_content" android:text="@string/get_report"/> </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/process_memory_snapshot_config"/> + <Button + android:id="@+id/send_on_process_memory_snapshot_config" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/add_metrics_config"/> + <Button + android:id="@+id/remove_on_process_memory_snapshot_config" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/remove_metrics_config"/> + <Button + android:id="@+id/get_on_process_memory_snapshot_report" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/get_report"/> + </LinearLayout> </LinearLayout> <!-- @+id/metrics_config_buttons --> <TextView diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml index 841e2ff534..054a784164 100644 --- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml +++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml @@ -387,7 +387,7 @@ <!-- CarTelemetryService Test --> <string name="gear_change_config" translatable="false">on_gear_change:</string> - <string name="process_memory_config" translatable="false">process_memory:</string> + <string name="process_memory_config" translatable="false">process_memory_state:</string> <string name="app_start_memory_state_captured_config" translatable="false">app_start_memory_state_captured:</string> <string name="activity_foreground_state_changed_config" translatable="false">activity_foreground_state_changed:</string> <string name="process_cpu_time_config" translatable="false">process_cpu_time:</string> @@ -397,6 +397,7 @@ <string name="wifi_netstats_config" translatable="false">wifi_netstats_top_consumers:</string> <string name="stats_and_connectivity_config" translatable="false">stats and connectivity:</string> <string name="memory_config" translatable="false">memory:</string> + <string name="process_memory_snapshot_config" translatable="false">process_memory_snapshot:</string> <string name="add_metrics_config" translatable="false">Add MetricsConfig</string> <string name="remove_metrics_config" translatable="false">Remove MetricsConfig</string> <string name="download_data" translatable="false">Download Data</string> diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java index 812e5bdc83..40a965f5e7 100644 --- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java +++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java @@ -21,6 +21,7 @@ import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.A import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.APP_CRASH_OCCURRED; import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.APP_START_MEMORY_STATE_CAPTURED; import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.PROCESS_CPU_TIME; +import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.PROCESS_MEMORY_SNAPSHOT; import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.PROCESS_MEMORY_STATE; import static android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.WTF_OCCURRED; @@ -465,6 +466,32 @@ public class CarTelemetryTestFragment extends Fragment { private static final String MEMORY_CONFIG_NAME = METRICS_CONFIG_MEMORY_V1.getName(); + /** ProcessMemorySnapshot section. */ + private static final String LUA_SCRIPT_ON_PROCESS_MEMORY_SNAPSHOT = new StringBuilder() + .append("function onProcessMemorySnapshot(published_data, state)\n") + .append(" on_script_finished(published_data)\n") + .append("end\n") + .toString(); + private static final TelemetryProto.Publisher PROCESS_MEMORY_SNAPSHOT_PUBLISHER = + TelemetryProto.Publisher.newBuilder() + .setStats( + TelemetryProto.StatsPublisher.newBuilder() + .setSystemMetric(PROCESS_MEMORY_SNAPSHOT)) + .build(); + private static final TelemetryProto.MetricsConfig METRICS_CONFIG_PROCESS_MEMORY_SNAPSHOT_V1 = + TelemetryProto.MetricsConfig.newBuilder() + .setName("process_memory_snapshot_metrics_config") + .setVersion(1) + .setScript(LUA_SCRIPT_ON_PROCESS_MEMORY_SNAPSHOT) + .addSubscribers( + TelemetryProto.Subscriber.newBuilder() + .setHandler("onProcessMemorySnapshot") + .setPublisher(PROCESS_MEMORY_SNAPSHOT_PUBLISHER) + .setPriority(SCRIPT_EXECUTION_PRIORITY_HIGH)) + .build(); + private static final String PROCESS_MEMORY_SNAPSHOT_CONFIG_NAME = + METRICS_CONFIG_PROCESS_MEMORY_SNAPSHOT_V1.getName(); + private final Executor mExecutor = Executors.newSingleThreadExecutor(); private boolean mReceiveReportNotification = false; @@ -592,6 +619,13 @@ public class CarTelemetryTestFragment extends Fragment { .setOnClickListener(this::onRemoveMemoryConfigBtnClick); view.findViewById(R.id.get_memory_report) .setOnClickListener(this::onGetMemoryReportBtnClick); + /** StatsPublisher process_memory_snapshot */ + view.findViewById(R.id.send_on_process_memory_snapshot_config) + .setOnClickListener(this::onSendProcessMemorySnapshotConfigBtnClick); + view.findViewById(R.id.remove_on_process_memory_snapshot_config) + .setOnClickListener(this::onRemoveProcessMemorySnapshotConfigBtnClick); + view.findViewById(R.id.get_on_process_memory_snapshot_report) + .setOnClickListener(this::onGetProcessMemorySnapshotReportBtnClick); /** Print mem info button */ view.findViewById(R.id.print_mem_info_btn).setOnClickListener(this::onPrintMemInfoBtnClick); return view; @@ -982,6 +1016,24 @@ public class CarTelemetryTestFragment extends Fragment { mCarTelemetryManager.getFinishedReport(MEMORY_CONFIG_NAME, mExecutor, mListener); } + private void onSendProcessMemorySnapshotConfigBtnClick(View view) { + mCarTelemetryManager.addMetricsConfig( + PROCESS_MEMORY_SNAPSHOT_CONFIG_NAME, + METRICS_CONFIG_PROCESS_MEMORY_SNAPSHOT_V1.toByteArray(), + mExecutor, + mAddMetricsConfigCallback); + } + + private void onRemoveProcessMemorySnapshotConfigBtnClick(View view) { + showOutput("Removing MetricsConfig that listens for PROCESS_MEMORY_SNAPSHOT..."); + mCarTelemetryManager.removeMetricsConfig(PROCESS_MEMORY_SNAPSHOT_CONFIG_NAME); + } + + private void onGetProcessMemorySnapshotReportBtnClick(View view) { + mCarTelemetryManager.getFinishedReport( + PROCESS_MEMORY_SNAPSHOT_CONFIG_NAME, mExecutor, mListener); + } + /** Gets a MemoryInfo object for the device's current memory status. */ private ActivityManager.MemoryInfo getAvailableMemory() { ActivityManager activityManager = getActivity().getSystemService(ActivityManager.class); diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java index 59e27e043c..d3858e4a54 100644 --- a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java +++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java @@ -387,8 +387,7 @@ public class StatsPublisherTest { assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1); Message msg = mFakeHandlerWrapper.getQueuedMessages().get(0); - long expectedPullPeriodMillis = 10 * 60 * 1000; // 10 minutes - assertThatMessageIsScheduledWithGivenDelay(msg, expectedPullPeriodMillis); + assertThatMessageIsScheduledWithGivenDelay(msg, 0); } @Test diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemorySnapshotConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemorySnapshotConverterTest.java new file mode 100644 index 0000000000..715830ef35 --- /dev/null +++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemorySnapshotConverterTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2022 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 com.android.car.telemetry.publisher.statsconverters; + +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.ANON_RSS_AND_SWAP_IN_KILOBYTES_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.ANON_RSS_IN_KILOBYTES_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.GPU_MEMORY_KB_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.HAS_FOREGROUND_SERVICES_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.OOM_SCORE_ADJ_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.PID_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.PROCESS_NAME_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.RSS_IN_KILOBYTES_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.SWAP_IN_KILOBYTES_FIELD_NUMBER; +import static com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot.UID_FIELD_NUMBER; +import static com.android.car.telemetry.publisher.Constants.STATS_BUNDLE_KEY_PREFIX; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.os.PersistableBundle; +import android.util.SparseArray; + +import com.android.car.telemetry.AtomsProto.Atom; +import com.android.car.telemetry.AtomsProto.ProcessMemorySnapshot; +import com.android.car.telemetry.StatsLogProto.DimensionsValue; +import com.android.car.telemetry.publisher.HashUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@RunWith(JUnit4.class) +public class ProcessMemorySnapshotConverterTest { + private static final Atom ATOM_A = + Atom.newBuilder() + .setProcessMemorySnapshot(ProcessMemorySnapshot.newBuilder() + .setPid(88) + .setOomScoreAdj(100) + .setRssInKilobytes(1234) + .setAnonRssInKilobytes(99) + .setSwapInKilobytes(101) + .setAnonRssAndSwapInKilobytes(200) + .setGpuMemoryKb(0) + .setHasForegroundServices(true)) + .build(); + + private static final Atom ATOM_B = + Atom.newBuilder() + .setProcessMemorySnapshot(ProcessMemorySnapshot.newBuilder() + .setPid(99) + .setOomScoreAdj(200) + .setRssInKilobytes(6666) + .setAnonRssInKilobytes(100) + .setSwapInKilobytes(100) + .setAnonRssAndSwapInKilobytes(200) + .setGpuMemoryKb(1) + .setHasForegroundServices(false)) + .build(); + + private static final Atom ATOM_MISMATCH = + Atom.newBuilder() + .setProcessMemorySnapshot(ProcessMemorySnapshot.newBuilder() + // Some fields are not set, creating mismatch with above atoms + .setSwapInKilobytes(333)) + .build(); + + private static final List<Integer> DIM_FIELDS_IDS = Arrays.asList(1, 2); + private static final Long HASH_1 = HashUtils.murmur2Hash64("process.name.1"); + private static final Long HASH_2 = HashUtils.murmur2Hash64("process.name.2"); + private static final Map<Long, String> HASH_STR_MAP = Map.of( + HASH_1, "process.name.1", + HASH_2, "process.name.2"); + + private static final List<DimensionsValue> DV_PAIR_A = + Arrays.asList( + DimensionsValue.newBuilder().setValueInt(1000).build(), + DimensionsValue.newBuilder().setValueStrHash(HASH_1).build()); + + private static final List<DimensionsValue> DV_PAIR_B = + Arrays.asList( + DimensionsValue.newBuilder().setValueInt(2000).build(), + DimensionsValue.newBuilder().setValueStrHash(HASH_2).build()); + + private static final List<DimensionsValue> DV_PAIR_MALFORMED = + Arrays.asList( + DimensionsValue.newBuilder().setValueInt(3000).build(), + // Wrong format since leaf level dimension value should set value, not field + DimensionsValue.newBuilder().setField(3).build()); + + // Subject of the test. + private ProcessMemorySnapshotConverter mConverter = new ProcessMemorySnapshotConverter(); + + @Test + public void testConvertAtomsListWithDimensionValues_putsCorrectDataToPersistableBundle() + throws StatsConversionException { + List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B); + List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B); + SparseArray<AtomFieldAccessor<ProcessMemorySnapshot, ?>> accessorMap = + mConverter.getAtomFieldAccessorMap(); + + PersistableBundle bundle = mConverter.convert(atomsList, DIM_FIELDS_IDS, + dimensionsValuesList, HASH_STR_MAP); + + assertThat(bundle.size()).isEqualTo(10); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get(UID_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(1000, 2000).inOrder(); + assertThat(Arrays.asList(bundle.getStringArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + PROCESS_NAME_FIELD_NUMBER).getFieldName()))) + .containsExactly("process.name.1", "process.name.2").inOrder(); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get(PID_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(88, 99); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + OOM_SCORE_ADJ_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(100, 200).inOrder(); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + RSS_IN_KILOBYTES_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(1234, 6666).inOrder(); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + ANON_RSS_IN_KILOBYTES_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(99, 100).inOrder(); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + SWAP_IN_KILOBYTES_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(101, 100).inOrder(); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + ANON_RSS_AND_SWAP_IN_KILOBYTES_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(200, 200).inOrder(); + assertThat(bundle.getIntArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + GPU_MEMORY_KB_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(0, 1).inOrder(); + assertThat(bundle.getBooleanArray( + STATS_BUNDLE_KEY_PREFIX + accessorMap.get( + HAS_FOREGROUND_SERVICES_FIELD_NUMBER).getFieldName())) + .asList().containsExactly(true, false).inOrder(); + } + + @Test + public void testAtomSetFieldInconsistency_throwsException() { + List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_MISMATCH); + List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B); + + assertThrows( + StatsConversionException.class, + () -> mConverter.convert( + atomsList, + DIM_FIELDS_IDS, + dimensionsValuesList, + HASH_STR_MAP)); + } + + @Test + public void testMalformedDimensionValue_throwsException() { + List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B); + List<List<DimensionsValue>> dimensionsValuesList = + Arrays.asList(DV_PAIR_A, DV_PAIR_MALFORMED); + + assertThrows( + StatsConversionException.class, + () -> mConverter.convert( + atomsList, + DIM_FIELDS_IDS, + dimensionsValuesList, + HASH_STR_MAP)); + } +} |