Loading core/java/android/app/jank/JankDataProcessor.java 0 → 100644 +351 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.app.jank.StateTracker.StateData; import android.util.Log; import android.util.Pools.SimplePool; import android.view.SurfaceControl.JankData; import androidx.annotation.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * This class is responsible for associating frames received from SurfaceFlinger to active widget * states and logging those states back to the platform. * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public class JankDataProcessor { private static final int MAX_IN_MEMORY_STATS = 25; private static final int LOG_BATCH_FREQUENCY = 50; private int mCurrentBatchCount = 0; private StateTracker mStateTracker = null; private ArrayList<StateData> mPendingStates = new ArrayList<>(); private SimplePool<PendingJankStat> mPendingJankStatsPool = new SimplePool<>(MAX_IN_MEMORY_STATS); private HashMap<String, PendingJankStat> mPendingJankStats = new HashMap<>(); public JankDataProcessor(@NonNull StateTracker stateTracker) { mStateTracker = stateTracker; } /** * Called once per batch of JankData. * @param jankData data received from SurfaceFlinger to be processed * @param activityName name of the activity that is tracking jank metrics. * @param appUid the uid of the app. */ public void processJankData(List<JankData> jankData, String activityName, int appUid) { mCurrentBatchCount++; // add all the previous and active states to the pending states list. mStateTracker.retrieveAllStates(mPendingStates); // TODO b/376332122 Look to see if this logic can be optimized. for (int i = 0; i < jankData.size(); i++) { JankData frame = jankData.get(i); // for each frame we need to check if the state was active during that time. for (int j = 0; j < mPendingStates.size(); j++) { StateData pendingState = mPendingStates.get(j); // This state was active during the frame if (frame.frameVsyncId >= pendingState.mVsyncIdStart && frame.frameVsyncId <= pendingState.mVsyncIdEnd) { recordFrameCount(frame, pendingState, activityName, appUid); pendingState.mProcessed = true; } } } // At this point we have attributed all frames to a state. if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) { logMetricCounts(); } // return the StatData object back to the pool to be reused. jankDataProcessingComplete(); } /** * Returns the aggregate map of different pending jank stats. */ @VisibleForTesting public HashMap<String, PendingJankStat> getPendingJankStats() { return mPendingJankStats; } private void jankDataProcessingComplete() { mStateTracker.stateProcessingComplete(); mPendingStates.clear(); } /** * Determine if frame is Janky and add to existing memory counter or create a new one. */ private void recordFrameCount(JankData frameData, StateData stateData, String activityName, int appUid) { // Check if we have an existing Jank state PendingJankStat jankStats = mPendingJankStats.get(stateData.mStateDataKey); if (jankStats == null) { // Check if we have space for another pending stat if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) { return; } jankStats = mPendingJankStatsPool.acquire(); if (jankStats == null) { jankStats = new PendingJankStat(); } jankStats.clearStats(); jankStats.mActivityName = activityName; jankStats.mUid = appUid; mPendingJankStats.put(stateData.mStateDataKey, jankStats); } // This state has already been accounted for if (jankStats.processedVsyncId == frameData.frameVsyncId) return; jankStats.mTotalFrames += 1; if (frameData.jankType == JankData.JANK_APPLICATION) { jankStats.mJankyFrames += 1; } jankStats.recordFrameOverrun(frameData.actualAppFrameTimeNs); jankStats.processedVsyncId = frameData.frameVsyncId; } /** * When called will log pending Jank stats currently stored in memory to the platform. Will not * clear any pending widget states. */ public void logMetricCounts() { //TODO b/374607503 when api changes are in add enum mapping for category and state. try { mPendingJankStats.values().forEach(stat -> { FrameworkStatsLog.write(FrameworkStatsLog.JANK_FRAME_COUNT_BY_WIDGET, /*app uid*/ stat.getUid(), /*activity name*/ stat.getActivityName(), /*widget id*/ stat.getWidgetId(), /*refresh rate*/ stat.getRefreshRate(), /*widget category*/ 0, /*widget state*/ 0, /*total frames*/ stat.getTotalFrames(), /*janky frames*/ stat.getJankyFrames(), /*histogram*/ stat.mFrameOverrunBuckets); Log.d(stat.mActivityName, stat.toString()); // return the pending stat to the pool it will be reset the next time its // used. mPendingJankStatsPool.release(stat); } ); // All stats have been recorded and added back to the pool for reuse, clear the pending // stats. mPendingJankStats.clear(); mCurrentBatchCount = 0; } catch (Exception exception) { // TODO b/374608358 handle logging exceptions. } } public static final class PendingJankStat { private static final int NANOS_PER_MS = 1000000; public long processedVsyncId = -1; // UID of the app private int mUid; // The name of the activity that is currently collecting frame metrics. private String mActivityName; // The id that has been set for the widget. private String mWidgetId; // A general category that the widget applies to. private String mWidgetCategory; // The states that the UI elements can report private String mWidgetState; // The number of frames reported during this state. private long mTotalFrames; // Total number of frames determined to be janky during the reported state. private long mJankyFrames; private int mRefreshRate; private static final int[] sFrameOverrunHistogramBounds = { Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000 }; private final int[] mFrameOverrunBuckets = new int[sFrameOverrunHistogramBounds.length]; // Histogram of frame duration overruns encoded in predetermined buckets. public PendingJankStat() { } public long getProcessedVsyncId() { return processedVsyncId; } public void setProcessedVsyncId(long processedVsyncId) { this.processedVsyncId = processedVsyncId; } public int getUid() { return mUid; } public void setUid(int uid) { mUid = uid; } public String getActivityName() { return mActivityName; } public void setActivityName(String activityName) { mActivityName = activityName; } public String getWidgetId() { return mWidgetId; } public void setWidgetId(String widgetId) { mWidgetId = widgetId; } public String getWidgetCategory() { return mWidgetCategory; } public void setWidgetCategory(String widgetCategory) { mWidgetCategory = widgetCategory; } public String getWidgetState() { return mWidgetState; } public void setWidgetState(String widgetState) { mWidgetState = widgetState; } public long getTotalFrames() { return mTotalFrames; } public void setTotalFrames(long totalFrames) { mTotalFrames = totalFrames; } public long getJankyFrames() { return mJankyFrames; } public void setJankyFrames(long jankyFrames) { mJankyFrames = jankyFrames; } public int[] getFrameOverrunBuckets() { return mFrameOverrunBuckets; } public int getRefreshRate() { return mRefreshRate; } public void setRefreshRate(int refreshRate) { mRefreshRate = refreshRate; } /** * Will convert the frame time from ns to ms and record how long the frame took to render. */ public void recordFrameOverrun(long frameTimeNano) { try { // TODO b/375650163 calculate frame overrun from refresh rate. int frameTimeMillis = (int) frameTimeNano / NANOS_PER_MS; mFrameOverrunBuckets[indexForFrameOverrun(frameTimeMillis)]++; } catch (IndexOutOfBoundsException exception) { // TODO b/375650163 figure out how to handle this if it happens. } } /** * resets all fields in the object back to defaults. */ public void clearStats() { this.mUid = -1; this.mActivityName = ""; this.processedVsyncId = -1; this.mJankyFrames = 0; this.mTotalFrames = 0; this.mWidgetCategory = ""; this.mWidgetState = ""; this.mRefreshRate = 0; clearHistogram(); } private void clearHistogram() { for (int i = 0; i < mFrameOverrunBuckets.length; i++) { mFrameOverrunBuckets[i] = 0; } } // This takes the overrun time and returns what bucket it belongs to in the histogram. private int indexForFrameOverrun(int overrunTime) { if (overrunTime < 20) { if (overrunTime >= -20) { return (overrunTime + 20) / 2 + 12; } if (overrunTime >= -30) { return (overrunTime + 30) / 5 + 10; } if (overrunTime >= -100) { return (overrunTime + 100) / 10 + 3; } if (overrunTime >= -200) { return (overrunTime + 200) / 50 + 1; } return 0; } if (overrunTime < 30) { return (overrunTime - 20) / 5 + 32; } if (overrunTime < 100) { return (overrunTime - 30) / 10 + 34; } if (overrunTime < 200) { return (overrunTime - 50) / 100 + 41; } if (overrunTime < 1000) { return (overrunTime - 200) / 100 + 43; } return sFrameOverrunHistogramBounds.length - 1; } } } core/java/android/app/jank/StateTracker.java +0 −2 Original line number Diff line number Diff line Loading @@ -36,7 +36,6 @@ import java.util.concurrent.ConcurrentHashMap; * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) @VisibleForTesting public class StateTracker { // Used to synchronize access to mPreviousStates. Loading Loading @@ -188,7 +187,6 @@ public class StateTracker { /** * @hide */ @VisibleForTesting public static class StateData { // Concatenated string of widget category, widget state and widget id. Loading tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java 0 → 100644 +279 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank.tests; import static org.junit.Assert.assertEquals; import android.app.jank.Flags; import android.app.jank.JankDataProcessor; import android.app.jank.StateTracker; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.Choreographer; import android.view.SurfaceControl; import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ActivityScenario; import androidx.test.runner.AndroidJUnit4; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; @RunWith(AndroidJUnit4.class) public class JankDataProcessorTest { private Choreographer mChoreographer; private StateTracker mStateTracker; private JankDataProcessor mJankDataProcessor; private static final int NANOS_PER_MS = 1_000_000; private static String sActivityName; private static ActivityScenario<EmptyActivity> sEmptyActivityActivityScenario; private static final int APP_ID = 25; @BeforeClass public static void classSetup() { sEmptyActivityActivityScenario = ActivityScenario.launch(EmptyActivity.class); sActivityName = sEmptyActivityActivityScenario.toString(); } @AfterClass public static void classTearDown() { sEmptyActivityActivityScenario.close(); } @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Before @UiThreadTest public void setup() { mChoreographer = Choreographer.getInstance(); mStateTracker = new StateTracker(mChoreographer); mJankDataProcessor = new JankDataProcessor(mStateTracker); } @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_multipleFramesAndStates_attributesTotalFramesCorrectly() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); long totalFramesAttributed = getTotalFramesCounted(); // Each state is active for each frame that is passed in, there are two states being tested // which is why jankData.size is multiplied by 2. assertEquals(jankData.size() * 2, totalFramesAttributed); } /** * Each JankData frame has an associated vsyncid, only frames that have vsyncids between the * StatData start and end vsyncids should be counted. This test confirms that if JankData * does not share any frames with the states then no jank stats are added. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_outOfRangeVsyncId_skipOutOfRangeVsyncIds() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_outOfRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); assertEquals(0, mJankDataProcessor.getPendingJankStats().size()); } /** * It's expected to see many duplicate widget states, if a user is scrolling then * pauses and resumes scrolling again, we may get three widget states two of which are the same. * State 1: {Scroll,WidgetId,Scrolling} State 2: {Scroll,WidgetId,None} * State 3: {Scroll,WidgetId,Scrolling} * These duplicate states should coalesce into only one Jank stat. This test confirms that * behavior. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_duplicateStates_confirmDuplicatesCoalesce() { // getMockStateData will return 10 states 5 of which are set to none and 5 of which are // scrolling. mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(getMockJankData_vsyncId_inRange(), sActivityName, APP_ID); // Confirm the duplicate states are coalesced down to 2 stats 1 for the scrolling state // another for the none state. assertEquals(2, mJankDataProcessor.getPendingJankStats().size()); } @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_inRangeVsyncIds_confirmOnlyInRangeFramesCounted() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); int inRangeFrameCount = jankData.size(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); // Two states are active for each frame which is why inRangeFrameCount is multiplied by 2. assertEquals(inRangeFrameCount * 2, getTotalFramesCounted()); } @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_inRangeVsyncIds_confirmHistogramCountMatchesFrameCount() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); long totalFrames = getTotalFramesCounted(); long histogramFrames = getHistogramFrameCount(); assertEquals(totalFrames, histogramFrames); } // TODO b/375005277 add tests that cover logging and releasing resources back to pool. private long getTotalFramesCounted() { return mJankDataProcessor.getPendingJankStats().values() .stream().mapToLong(stat -> stat.getTotalFrames()).sum(); } private long getHistogramFrameCount() { long totalHistogramFrames = 0; for (JankDataProcessor.PendingJankStat stats : mJankDataProcessor.getPendingJankStats().values()) { int[] overrunHistogram = stats.getFrameOverrunBuckets(); for (int i = 0; i < overrunHistogram.length; i++) { totalHistogramFrames += overrunHistogram[i]; } } return totalHistogramFrames; } /** * Out of range data will have a mVsyncIdStart and mVsyncIdEnd values set to below 25. */ private List<StateTracker.StateData> getMockStateData_vsyncId_outOfRange() { ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); StateTracker.StateData newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = 20; newStateData.mStateDataKey = "Test1_OutBand"; newStateData.mVsyncIdStart = 1; newStateData.mWidgetState = "scrolling"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = 24; newStateData.mStateDataKey = "Test1_InBand"; newStateData.mVsyncIdStart = 20; newStateData.mWidgetState = "Idle"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = 20; newStateData.mStateDataKey = "Test1_OutBand"; newStateData.mVsyncIdStart = 12; newStateData.mWidgetState = "Idle"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); return stateData; } /** * This method returns two unique states, one state is set to scrolling the other is set * to none. Both states will have the same startvsyncid to ensure each state is counted the same * number of times. This keeps logic in asserts easier to reason about. Both states will have * a startVsyncId between 25 and 35. */ private List<StateTracker.StateData> getMockStateData_vsyncId_inRange() { ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); for (int i = 0; i < 10; i++) { StateTracker.StateData newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = Long.MAX_VALUE; newStateData.mStateDataKey = "Test1_" + (i % 2 == 0 ? "scrolling" : "none"); // Divide i by two to ensure both the scrolling and none states get the same vsyncid // This makes asserts in tests easier to reason about as each state should be counted // the same number of times. newStateData.mVsyncIdStart = 25 + (i / 2); newStateData.mWidgetState = i % 2 == 0 ? "scrolling" : "none"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); } return stateData; } /** * In range data will have a frameVsyncId value between 25 and 35. */ private List<SurfaceControl.JankData> getMockJankData_vsyncId_inRange() { ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); for (int i = 0; i < 10; i++) { mockData.add(new SurfaceControl.JankData( /*frameVsyncId*/25 + i, SurfaceControl.JankData.JANK_NONE, NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i))); } return mockData; } /** * Out of range data will have frameVsyncId values below 25. */ private List<SurfaceControl.JankData> getMockJankData_vsyncId_outOfRange() { ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); for (int i = 0; i < 10; i++) { mockData.add(new SurfaceControl.JankData( /*frameVsyncId*/i, SurfaceControl.JankData.JANK_NONE, NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i))); } return mockData; } } Loading
core/java/android/app/jank/JankDataProcessor.java 0 → 100644 +351 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.app.jank.StateTracker.StateData; import android.util.Log; import android.util.Pools.SimplePool; import android.view.SurfaceControl.JankData; import androidx.annotation.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * This class is responsible for associating frames received from SurfaceFlinger to active widget * states and logging those states back to the platform. * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public class JankDataProcessor { private static final int MAX_IN_MEMORY_STATS = 25; private static final int LOG_BATCH_FREQUENCY = 50; private int mCurrentBatchCount = 0; private StateTracker mStateTracker = null; private ArrayList<StateData> mPendingStates = new ArrayList<>(); private SimplePool<PendingJankStat> mPendingJankStatsPool = new SimplePool<>(MAX_IN_MEMORY_STATS); private HashMap<String, PendingJankStat> mPendingJankStats = new HashMap<>(); public JankDataProcessor(@NonNull StateTracker stateTracker) { mStateTracker = stateTracker; } /** * Called once per batch of JankData. * @param jankData data received from SurfaceFlinger to be processed * @param activityName name of the activity that is tracking jank metrics. * @param appUid the uid of the app. */ public void processJankData(List<JankData> jankData, String activityName, int appUid) { mCurrentBatchCount++; // add all the previous and active states to the pending states list. mStateTracker.retrieveAllStates(mPendingStates); // TODO b/376332122 Look to see if this logic can be optimized. for (int i = 0; i < jankData.size(); i++) { JankData frame = jankData.get(i); // for each frame we need to check if the state was active during that time. for (int j = 0; j < mPendingStates.size(); j++) { StateData pendingState = mPendingStates.get(j); // This state was active during the frame if (frame.frameVsyncId >= pendingState.mVsyncIdStart && frame.frameVsyncId <= pendingState.mVsyncIdEnd) { recordFrameCount(frame, pendingState, activityName, appUid); pendingState.mProcessed = true; } } } // At this point we have attributed all frames to a state. if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) { logMetricCounts(); } // return the StatData object back to the pool to be reused. jankDataProcessingComplete(); } /** * Returns the aggregate map of different pending jank stats. */ @VisibleForTesting public HashMap<String, PendingJankStat> getPendingJankStats() { return mPendingJankStats; } private void jankDataProcessingComplete() { mStateTracker.stateProcessingComplete(); mPendingStates.clear(); } /** * Determine if frame is Janky and add to existing memory counter or create a new one. */ private void recordFrameCount(JankData frameData, StateData stateData, String activityName, int appUid) { // Check if we have an existing Jank state PendingJankStat jankStats = mPendingJankStats.get(stateData.mStateDataKey); if (jankStats == null) { // Check if we have space for another pending stat if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) { return; } jankStats = mPendingJankStatsPool.acquire(); if (jankStats == null) { jankStats = new PendingJankStat(); } jankStats.clearStats(); jankStats.mActivityName = activityName; jankStats.mUid = appUid; mPendingJankStats.put(stateData.mStateDataKey, jankStats); } // This state has already been accounted for if (jankStats.processedVsyncId == frameData.frameVsyncId) return; jankStats.mTotalFrames += 1; if (frameData.jankType == JankData.JANK_APPLICATION) { jankStats.mJankyFrames += 1; } jankStats.recordFrameOverrun(frameData.actualAppFrameTimeNs); jankStats.processedVsyncId = frameData.frameVsyncId; } /** * When called will log pending Jank stats currently stored in memory to the platform. Will not * clear any pending widget states. */ public void logMetricCounts() { //TODO b/374607503 when api changes are in add enum mapping for category and state. try { mPendingJankStats.values().forEach(stat -> { FrameworkStatsLog.write(FrameworkStatsLog.JANK_FRAME_COUNT_BY_WIDGET, /*app uid*/ stat.getUid(), /*activity name*/ stat.getActivityName(), /*widget id*/ stat.getWidgetId(), /*refresh rate*/ stat.getRefreshRate(), /*widget category*/ 0, /*widget state*/ 0, /*total frames*/ stat.getTotalFrames(), /*janky frames*/ stat.getJankyFrames(), /*histogram*/ stat.mFrameOverrunBuckets); Log.d(stat.mActivityName, stat.toString()); // return the pending stat to the pool it will be reset the next time its // used. mPendingJankStatsPool.release(stat); } ); // All stats have been recorded and added back to the pool for reuse, clear the pending // stats. mPendingJankStats.clear(); mCurrentBatchCount = 0; } catch (Exception exception) { // TODO b/374608358 handle logging exceptions. } } public static final class PendingJankStat { private static final int NANOS_PER_MS = 1000000; public long processedVsyncId = -1; // UID of the app private int mUid; // The name of the activity that is currently collecting frame metrics. private String mActivityName; // The id that has been set for the widget. private String mWidgetId; // A general category that the widget applies to. private String mWidgetCategory; // The states that the UI elements can report private String mWidgetState; // The number of frames reported during this state. private long mTotalFrames; // Total number of frames determined to be janky during the reported state. private long mJankyFrames; private int mRefreshRate; private static final int[] sFrameOverrunHistogramBounds = { Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000 }; private final int[] mFrameOverrunBuckets = new int[sFrameOverrunHistogramBounds.length]; // Histogram of frame duration overruns encoded in predetermined buckets. public PendingJankStat() { } public long getProcessedVsyncId() { return processedVsyncId; } public void setProcessedVsyncId(long processedVsyncId) { this.processedVsyncId = processedVsyncId; } public int getUid() { return mUid; } public void setUid(int uid) { mUid = uid; } public String getActivityName() { return mActivityName; } public void setActivityName(String activityName) { mActivityName = activityName; } public String getWidgetId() { return mWidgetId; } public void setWidgetId(String widgetId) { mWidgetId = widgetId; } public String getWidgetCategory() { return mWidgetCategory; } public void setWidgetCategory(String widgetCategory) { mWidgetCategory = widgetCategory; } public String getWidgetState() { return mWidgetState; } public void setWidgetState(String widgetState) { mWidgetState = widgetState; } public long getTotalFrames() { return mTotalFrames; } public void setTotalFrames(long totalFrames) { mTotalFrames = totalFrames; } public long getJankyFrames() { return mJankyFrames; } public void setJankyFrames(long jankyFrames) { mJankyFrames = jankyFrames; } public int[] getFrameOverrunBuckets() { return mFrameOverrunBuckets; } public int getRefreshRate() { return mRefreshRate; } public void setRefreshRate(int refreshRate) { mRefreshRate = refreshRate; } /** * Will convert the frame time from ns to ms and record how long the frame took to render. */ public void recordFrameOverrun(long frameTimeNano) { try { // TODO b/375650163 calculate frame overrun from refresh rate. int frameTimeMillis = (int) frameTimeNano / NANOS_PER_MS; mFrameOverrunBuckets[indexForFrameOverrun(frameTimeMillis)]++; } catch (IndexOutOfBoundsException exception) { // TODO b/375650163 figure out how to handle this if it happens. } } /** * resets all fields in the object back to defaults. */ public void clearStats() { this.mUid = -1; this.mActivityName = ""; this.processedVsyncId = -1; this.mJankyFrames = 0; this.mTotalFrames = 0; this.mWidgetCategory = ""; this.mWidgetState = ""; this.mRefreshRate = 0; clearHistogram(); } private void clearHistogram() { for (int i = 0; i < mFrameOverrunBuckets.length; i++) { mFrameOverrunBuckets[i] = 0; } } // This takes the overrun time and returns what bucket it belongs to in the histogram. private int indexForFrameOverrun(int overrunTime) { if (overrunTime < 20) { if (overrunTime >= -20) { return (overrunTime + 20) / 2 + 12; } if (overrunTime >= -30) { return (overrunTime + 30) / 5 + 10; } if (overrunTime >= -100) { return (overrunTime + 100) / 10 + 3; } if (overrunTime >= -200) { return (overrunTime + 200) / 50 + 1; } return 0; } if (overrunTime < 30) { return (overrunTime - 20) / 5 + 32; } if (overrunTime < 100) { return (overrunTime - 30) / 10 + 34; } if (overrunTime < 200) { return (overrunTime - 50) / 100 + 41; } if (overrunTime < 1000) { return (overrunTime - 200) / 100 + 43; } return sFrameOverrunHistogramBounds.length - 1; } } }
core/java/android/app/jank/StateTracker.java +0 −2 Original line number Diff line number Diff line Loading @@ -36,7 +36,6 @@ import java.util.concurrent.ConcurrentHashMap; * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) @VisibleForTesting public class StateTracker { // Used to synchronize access to mPreviousStates. Loading Loading @@ -188,7 +187,6 @@ public class StateTracker { /** * @hide */ @VisibleForTesting public static class StateData { // Concatenated string of widget category, widget state and widget id. Loading
tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java 0 → 100644 +279 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank.tests; import static org.junit.Assert.assertEquals; import android.app.jank.Flags; import android.app.jank.JankDataProcessor; import android.app.jank.StateTracker; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.Choreographer; import android.view.SurfaceControl; import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ActivityScenario; import androidx.test.runner.AndroidJUnit4; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; @RunWith(AndroidJUnit4.class) public class JankDataProcessorTest { private Choreographer mChoreographer; private StateTracker mStateTracker; private JankDataProcessor mJankDataProcessor; private static final int NANOS_PER_MS = 1_000_000; private static String sActivityName; private static ActivityScenario<EmptyActivity> sEmptyActivityActivityScenario; private static final int APP_ID = 25; @BeforeClass public static void classSetup() { sEmptyActivityActivityScenario = ActivityScenario.launch(EmptyActivity.class); sActivityName = sEmptyActivityActivityScenario.toString(); } @AfterClass public static void classTearDown() { sEmptyActivityActivityScenario.close(); } @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Before @UiThreadTest public void setup() { mChoreographer = Choreographer.getInstance(); mStateTracker = new StateTracker(mChoreographer); mJankDataProcessor = new JankDataProcessor(mStateTracker); } @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_multipleFramesAndStates_attributesTotalFramesCorrectly() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); long totalFramesAttributed = getTotalFramesCounted(); // Each state is active for each frame that is passed in, there are two states being tested // which is why jankData.size is multiplied by 2. assertEquals(jankData.size() * 2, totalFramesAttributed); } /** * Each JankData frame has an associated vsyncid, only frames that have vsyncids between the * StatData start and end vsyncids should be counted. This test confirms that if JankData * does not share any frames with the states then no jank stats are added. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_outOfRangeVsyncId_skipOutOfRangeVsyncIds() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_outOfRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); assertEquals(0, mJankDataProcessor.getPendingJankStats().size()); } /** * It's expected to see many duplicate widget states, if a user is scrolling then * pauses and resumes scrolling again, we may get three widget states two of which are the same. * State 1: {Scroll,WidgetId,Scrolling} State 2: {Scroll,WidgetId,None} * State 3: {Scroll,WidgetId,Scrolling} * These duplicate states should coalesce into only one Jank stat. This test confirms that * behavior. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_duplicateStates_confirmDuplicatesCoalesce() { // getMockStateData will return 10 states 5 of which are set to none and 5 of which are // scrolling. mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(getMockJankData_vsyncId_inRange(), sActivityName, APP_ID); // Confirm the duplicate states are coalesced down to 2 stats 1 for the scrolling state // another for the none state. assertEquals(2, mJankDataProcessor.getPendingJankStats().size()); } @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_inRangeVsyncIds_confirmOnlyInRangeFramesCounted() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); int inRangeFrameCount = jankData.size(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); // Two states are active for each frame which is why inRangeFrameCount is multiplied by 2. assertEquals(inRangeFrameCount * 2, getTotalFramesCounted()); } @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void processJankData_inRangeVsyncIds_confirmHistogramCountMatchesFrameCount() { List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); long totalFrames = getTotalFramesCounted(); long histogramFrames = getHistogramFrameCount(); assertEquals(totalFrames, histogramFrames); } // TODO b/375005277 add tests that cover logging and releasing resources back to pool. private long getTotalFramesCounted() { return mJankDataProcessor.getPendingJankStats().values() .stream().mapToLong(stat -> stat.getTotalFrames()).sum(); } private long getHistogramFrameCount() { long totalHistogramFrames = 0; for (JankDataProcessor.PendingJankStat stats : mJankDataProcessor.getPendingJankStats().values()) { int[] overrunHistogram = stats.getFrameOverrunBuckets(); for (int i = 0; i < overrunHistogram.length; i++) { totalHistogramFrames += overrunHistogram[i]; } } return totalHistogramFrames; } /** * Out of range data will have a mVsyncIdStart and mVsyncIdEnd values set to below 25. */ private List<StateTracker.StateData> getMockStateData_vsyncId_outOfRange() { ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); StateTracker.StateData newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = 20; newStateData.mStateDataKey = "Test1_OutBand"; newStateData.mVsyncIdStart = 1; newStateData.mWidgetState = "scrolling"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = 24; newStateData.mStateDataKey = "Test1_InBand"; newStateData.mVsyncIdStart = 20; newStateData.mWidgetState = "Idle"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = 20; newStateData.mStateDataKey = "Test1_OutBand"; newStateData.mVsyncIdStart = 12; newStateData.mWidgetState = "Idle"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); return stateData; } /** * This method returns two unique states, one state is set to scrolling the other is set * to none. Both states will have the same startvsyncid to ensure each state is counted the same * number of times. This keeps logic in asserts easier to reason about. Both states will have * a startVsyncId between 25 and 35. */ private List<StateTracker.StateData> getMockStateData_vsyncId_inRange() { ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); for (int i = 0; i < 10; i++) { StateTracker.StateData newStateData = new StateTracker.StateData(); newStateData.mVsyncIdEnd = Long.MAX_VALUE; newStateData.mStateDataKey = "Test1_" + (i % 2 == 0 ? "scrolling" : "none"); // Divide i by two to ensure both the scrolling and none states get the same vsyncid // This makes asserts in tests easier to reason about as each state should be counted // the same number of times. newStateData.mVsyncIdStart = 25 + (i / 2); newStateData.mWidgetState = i % 2 == 0 ? "scrolling" : "none"; newStateData.mWidgetId = "widgetId"; newStateData.mWidgetCategory = "Scroll"; stateData.add(newStateData); } return stateData; } /** * In range data will have a frameVsyncId value between 25 and 35. */ private List<SurfaceControl.JankData> getMockJankData_vsyncId_inRange() { ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); for (int i = 0; i < 10; i++) { mockData.add(new SurfaceControl.JankData( /*frameVsyncId*/25 + i, SurfaceControl.JankData.JANK_NONE, NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i))); } return mockData; } /** * Out of range data will have frameVsyncId values below 25. */ private List<SurfaceControl.JankData> getMockJankData_vsyncId_outOfRange() { ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); for (int i = 0; i < 10; i++) { mockData.add(new SurfaceControl.JankData( /*frameVsyncId*/i, SurfaceControl.JankData.JANK_NONE, NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i), NANOS_PER_MS * ((long) i))); } return mockData; } }