Loading AconfigFlags.bp +42 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,7 @@ aconfig_declarations_group { "android.view.inputmethod.flags-aconfig-java", "android.webkit.flags-aconfig-java", "android.widget.flags-aconfig-java", "android.xr.flags-aconfig-java", "art_exported_aconfig_flags_lib", "backstage_power_flags_lib", "backup_flags_lib", Loading Loading @@ -109,6 +110,7 @@ aconfig_declarations_group { "hwui_flags_java_lib", "interaction_jank_monitor_flags_lib", "libcore_exported_aconfig_flags_lib", "libcore_readonly_aconfig_flags_lib", "libgui_flags_java_lib", "power_flags_lib", "sdk_sandbox_flags_lib", Loading Loading @@ -168,6 +170,26 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } // See b/368409430 - This is for libcore flags to be generated with // force-read-only mode, so access to the flags does not involve I/O, // which could break Isolated Processes with I/O permission disabled. // The issue will be addressed once new Aconfig storage API is landed // and the readonly version will be removed. aconfig_declarations { name: "libcore-readonly-aconfig-flags", package: "com.android.libcore.readonly", container: "system", srcs: ["libcore-readonly.aconfig"], } // Core Libraries / libcore java_aconfig_library { name: "libcore_readonly_aconfig_flags_lib", aconfig_declarations: "libcore-readonly-aconfig-flags", mode: "force-read-only", defaults: ["framework-minus-apex-aconfig-java-defaults"], } // Telecom java_aconfig_library { name: "telecom_flags_core_java_lib", Loading Loading @@ -791,6 +813,12 @@ java_aconfig_library { ], } cc_aconfig_library { name: "android.permission.flags-aconfig-cc", aconfig_declarations: "android.permission.flags-aconfig", host_supported: true, } // SQLite aconfig_declarations { name: "android.database.sqlite-aconfig", Loading Loading @@ -893,6 +921,20 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } // XR aconfig_declarations { name: "android.xr.flags-aconfig", package: "android.xr", container: "system", srcs: ["core/java/android/content/pm/xr.aconfig"], } java_aconfig_library { name: "android.xr.flags-aconfig-java", aconfig_declarations: "android.xr.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } // android.app aconfig_declarations { name: "android.app.flags-aconfig", Loading MULTIUSER_OWNERS +0 −2 Original line number Diff line number Diff line Loading @@ -3,7 +3,5 @@ annabauza@google.com bookatz@google.com nykkumar@google.com olilan@google.com omakoto@google.com tetianameronyk@google.com tyk@google.com yamasani@google.com OWNERS +8 −10 Original line number Diff line number Diff line Loading @@ -7,8 +7,6 @@ dsandler@google.com #{LAST_RESORT_SUGGESTION} hackbod@android.com #{LAST_RESORT_SUGGESTION} hackbod@google.com #{LAST_RESORT_SUGGESTION} jjaggi@google.com #{LAST_RESORT_SUGGESTION} jsharkey@android.com #{LAST_RESORT_SUGGESTION} jsharkey@google.com #{LAST_RESORT_SUGGESTION} lorenzo@google.com #{LAST_RESORT_SUGGESTION} michaelwr@google.com #{LAST_RESORT_SUGGESTION} nandana@google.com #{LAST_RESORT_SUGGESTION} Loading @@ -33,19 +31,19 @@ per-file **.bp,**.mk =joeo@google.com, lamontjones@google.com per-file TestProtoLibraries.bp = file:platform/platform_testing:/libraries/health/OWNERS per-file TestProtoLibraries.bp = file:platform/tools/tradefederation:/OWNERS per-file INPUT_OWNERS = file:/INPUT_OWNERS per-file ZYGOTE_OWNERS = file:/ZYGOTE_OWNERS per-file SQLITE_OWNERS = file:/SQLITE_OWNERS per-file *ravenwood* = file:ravenwood/OWNERS per-file *Ravenwood* = file:ravenwood/OWNERS per-file INPUT_OWNERS = file:/INPUT_OWNERS per-file ZYGOTE_OWNERS = file:/ZYGOTE_OWNERS per-file SQLITE_OWNERS = file:/SQLITE_OWNERS per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS per-file WEAR_OWNERS = file:/WEAR_OWNERS per-file ACTIVITY_MANAGER_OWNERS = file:/ACTIVITY_MANAGER_OWNERS per-file BATTERY_STATS_OWNERS = file:/BATTERY_STATS_OWNERS per-file OOM_ADJUSTER_OWNERS = file:/OOM_ADJUSTER_OWNERS per-file MULTIUSER_OWNERS = file:/MULTIUSER_OWNERS per-file BROADCASTS_OWNERS = file:/BROADCASTS_OWNERS per-file ADPF_OWNERS = file:/ADPF_OWNERS per-file GAME_MANAGER_OWNERS = file:/GAME_MANAGER_OWNERS apct-tests/perftests/core/src/android/os/MessageQueuePerfTest.java 0 → 100644 +213 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 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.os; import android.app.Activity; import android.app.Instrumentation; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.perftests.utils.Stats; import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.Random; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Performance tests for {@link MessageQueue}. */ @RunWith(AndroidJUnit4.class) @LargeTest public class MessageQueuePerfTest { static final String TAG = "MessageQueuePerfTest"; private static final int PER_THREAD_MESSAGE_COUNT = 1000; private static final int THREAD_COUNT = 8; private static final int TOTAL_MESSAGE_COUNT = PER_THREAD_MESSAGE_COUNT * THREAD_COUNT; static Object sLock = new Object(); private ArrayList<Long> mResults; @Before public void setUp() { } @After public void tearDown() { } class EnqueueThread extends Thread { CountDownLatch mStartLatch; CountDownLatch mEndLatch; Handler mHandler; int mMessageStartIdx; Message[] mMessages; long[] mDelays; EnqueueThread(CountDownLatch startLatch, CountDownLatch endLatch, Handler handler, int startIdx, Message[] messages, long[] delays) { super(); mStartLatch = startLatch; mEndLatch = endLatch; mHandler = handler; mMessageStartIdx = startIdx; mMessages = messages; mDelays = delays; } @Override public void run() { Log.d(TAG, "Enqueue thread started at message index " + mMessageStartIdx); try { mStartLatch.await(); } catch (InterruptedException e) { } long now = SystemClock.uptimeMillis(); long startTimeNS = SystemClock.elapsedRealtimeNanos(); for (int i = mMessageStartIdx; i < (mMessageStartIdx + PER_THREAD_MESSAGE_COUNT); i++) { if (mDelays[i] == 0) { mHandler.sendMessageAtFrontOfQueue(mMessages[i]); } else { mHandler.sendMessageAtTime(mMessages[i], now + mDelays[i]); } } long endTimeNS = SystemClock.elapsedRealtimeNanos(); synchronized (sLock) { mResults.add(endTimeNS - startTimeNS); } mEndLatch.countDown(); } } class TestHandler extends Handler { TestHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { } } void reportPerf(String prefix, int threadCount, int perThreadMessageCount) { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Stats stats = new Stats(mResults); Log.d(TAG, "Reporting perf now"); Bundle status = new Bundle(); status.putLong(prefix + "_median_ns", stats.getMedian()); status.putLong(prefix + "_mean_ns", (long) stats.getMean()); status.putLong(prefix + "_min_ns", stats.getMin()); status.putLong(prefix + "_max_ns", stats.getMax()); status.putLong(prefix + "_stddev_ns", (long) stats.getStandardDeviation()); status.putLong(prefix + "_nr_threads", threadCount); status.putLong(prefix + "_msgs_per_thread", perThreadMessageCount); instrumentation.sendStatus(Activity.RESULT_OK, status); } HandlerThread mHandlerThread; private void fillMessagesArray(Message[] messages) { for (int i = 0; i < messages.length; i++) { messages[i] = mHandlerThread.getThreadHandler().obtainMessage(i); } } private void startTestAndWaitOnThreads(CountDownLatch threadStartLatch, CountDownLatch threadEndLatch) { try { threadStartLatch.countDown(); Log.e(TAG, "Test threads started"); threadEndLatch.await(); } catch (InterruptedException ignored) { } Log.e(TAG, "Test threads ended, quitting handler thread"); } @Test public void benchmarkEnqueueAtFrontOfQueue() { CountDownLatch threadStartLatch = new CountDownLatch(1); CountDownLatch threadEndLatch = new CountDownLatch(THREAD_COUNT); mHandlerThread = new HandlerThread("MessageQueuePerfTest"); mHandlerThread.start(); Message[] messages = new Message[TOTAL_MESSAGE_COUNT]; fillMessagesArray(messages); long[] delays = new long[TOTAL_MESSAGE_COUNT]; mResults = new ArrayList<>(); TestHandler handler = new TestHandler(mHandlerThread.getLooper()); for (int i = 0; i < THREAD_COUNT; i++) { EnqueueThread thread = new EnqueueThread(threadStartLatch, threadEndLatch, handler, i * PER_THREAD_MESSAGE_COUNT, messages, delays); thread.start(); } startTestAndWaitOnThreads(threadStartLatch, threadEndLatch); mHandlerThread.quitSafely(); reportPerf("enqueueAtFront", THREAD_COUNT, PER_THREAD_MESSAGE_COUNT); } /** * Fill array with random delays, for benchmarkEnqueueDelayed */ public long[] fillDelayArray() { long[] delays = new long[TOTAL_MESSAGE_COUNT]; Random rand = new Random(0xDEADBEEF); for (int i = 0; i < TOTAL_MESSAGE_COUNT; i++) { delays[i] = Math.abs(rand.nextLong() % 5000); } return delays; } @Test public void benchmarkEnqueueDelayed() { CountDownLatch threadStartLatch = new CountDownLatch(1); CountDownLatch threadEndLatch = new CountDownLatch(THREAD_COUNT); mHandlerThread = new HandlerThread("MessageQueuePerfTest"); mHandlerThread.start(); Message[] messages = new Message[TOTAL_MESSAGE_COUNT]; fillMessagesArray(messages); long[] delays = fillDelayArray(); mResults = new ArrayList<>(); TestHandler handler = new TestHandler(mHandlerThread.getLooper()); for (int i = 0; i < THREAD_COUNT; i++) { EnqueueThread thread = new EnqueueThread(threadStartLatch, threadEndLatch, handler, i * PER_THREAD_MESSAGE_COUNT, messages, delays); thread.start(); } startTestAndWaitOnThreads(threadStartLatch, threadEndLatch); mHandlerThread.quitSafely(); reportPerf("enqueueDelayed", THREAD_COUNT, PER_THREAD_MESSAGE_COUNT); } } apex/jobscheduler/service/aconfig/app_idle.aconfig +9 −0 Original line number Diff line number Diff line Loading @@ -12,3 +12,12 @@ flag { } } flag { name: "screen_time_bypass" namespace: "backstage_power" description: "Bypass the screen time check for bucket evaluation" bug: "374114769" metadata { purpose: PURPOSE_BUGFIX } } Loading
AconfigFlags.bp +42 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,7 @@ aconfig_declarations_group { "android.view.inputmethod.flags-aconfig-java", "android.webkit.flags-aconfig-java", "android.widget.flags-aconfig-java", "android.xr.flags-aconfig-java", "art_exported_aconfig_flags_lib", "backstage_power_flags_lib", "backup_flags_lib", Loading Loading @@ -109,6 +110,7 @@ aconfig_declarations_group { "hwui_flags_java_lib", "interaction_jank_monitor_flags_lib", "libcore_exported_aconfig_flags_lib", "libcore_readonly_aconfig_flags_lib", "libgui_flags_java_lib", "power_flags_lib", "sdk_sandbox_flags_lib", Loading Loading @@ -168,6 +170,26 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } // See b/368409430 - This is for libcore flags to be generated with // force-read-only mode, so access to the flags does not involve I/O, // which could break Isolated Processes with I/O permission disabled. // The issue will be addressed once new Aconfig storage API is landed // and the readonly version will be removed. aconfig_declarations { name: "libcore-readonly-aconfig-flags", package: "com.android.libcore.readonly", container: "system", srcs: ["libcore-readonly.aconfig"], } // Core Libraries / libcore java_aconfig_library { name: "libcore_readonly_aconfig_flags_lib", aconfig_declarations: "libcore-readonly-aconfig-flags", mode: "force-read-only", defaults: ["framework-minus-apex-aconfig-java-defaults"], } // Telecom java_aconfig_library { name: "telecom_flags_core_java_lib", Loading Loading @@ -791,6 +813,12 @@ java_aconfig_library { ], } cc_aconfig_library { name: "android.permission.flags-aconfig-cc", aconfig_declarations: "android.permission.flags-aconfig", host_supported: true, } // SQLite aconfig_declarations { name: "android.database.sqlite-aconfig", Loading Loading @@ -893,6 +921,20 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } // XR aconfig_declarations { name: "android.xr.flags-aconfig", package: "android.xr", container: "system", srcs: ["core/java/android/content/pm/xr.aconfig"], } java_aconfig_library { name: "android.xr.flags-aconfig-java", aconfig_declarations: "android.xr.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } // android.app aconfig_declarations { name: "android.app.flags-aconfig", Loading
MULTIUSER_OWNERS +0 −2 Original line number Diff line number Diff line Loading @@ -3,7 +3,5 @@ annabauza@google.com bookatz@google.com nykkumar@google.com olilan@google.com omakoto@google.com tetianameronyk@google.com tyk@google.com yamasani@google.com
OWNERS +8 −10 Original line number Diff line number Diff line Loading @@ -7,8 +7,6 @@ dsandler@google.com #{LAST_RESORT_SUGGESTION} hackbod@android.com #{LAST_RESORT_SUGGESTION} hackbod@google.com #{LAST_RESORT_SUGGESTION} jjaggi@google.com #{LAST_RESORT_SUGGESTION} jsharkey@android.com #{LAST_RESORT_SUGGESTION} jsharkey@google.com #{LAST_RESORT_SUGGESTION} lorenzo@google.com #{LAST_RESORT_SUGGESTION} michaelwr@google.com #{LAST_RESORT_SUGGESTION} nandana@google.com #{LAST_RESORT_SUGGESTION} Loading @@ -33,19 +31,19 @@ per-file **.bp,**.mk =joeo@google.com, lamontjones@google.com per-file TestProtoLibraries.bp = file:platform/platform_testing:/libraries/health/OWNERS per-file TestProtoLibraries.bp = file:platform/tools/tradefederation:/OWNERS per-file INPUT_OWNERS = file:/INPUT_OWNERS per-file ZYGOTE_OWNERS = file:/ZYGOTE_OWNERS per-file SQLITE_OWNERS = file:/SQLITE_OWNERS per-file *ravenwood* = file:ravenwood/OWNERS per-file *Ravenwood* = file:ravenwood/OWNERS per-file INPUT_OWNERS = file:/INPUT_OWNERS per-file ZYGOTE_OWNERS = file:/ZYGOTE_OWNERS per-file SQLITE_OWNERS = file:/SQLITE_OWNERS per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS per-file WEAR_OWNERS = file:/WEAR_OWNERS per-file ACTIVITY_MANAGER_OWNERS = file:/ACTIVITY_MANAGER_OWNERS per-file BATTERY_STATS_OWNERS = file:/BATTERY_STATS_OWNERS per-file OOM_ADJUSTER_OWNERS = file:/OOM_ADJUSTER_OWNERS per-file MULTIUSER_OWNERS = file:/MULTIUSER_OWNERS per-file BROADCASTS_OWNERS = file:/BROADCASTS_OWNERS per-file ADPF_OWNERS = file:/ADPF_OWNERS per-file GAME_MANAGER_OWNERS = file:/GAME_MANAGER_OWNERS
apct-tests/perftests/core/src/android/os/MessageQueuePerfTest.java 0 → 100644 +213 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 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.os; import android.app.Activity; import android.app.Instrumentation; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.perftests.utils.Stats; import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.Random; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Performance tests for {@link MessageQueue}. */ @RunWith(AndroidJUnit4.class) @LargeTest public class MessageQueuePerfTest { static final String TAG = "MessageQueuePerfTest"; private static final int PER_THREAD_MESSAGE_COUNT = 1000; private static final int THREAD_COUNT = 8; private static final int TOTAL_MESSAGE_COUNT = PER_THREAD_MESSAGE_COUNT * THREAD_COUNT; static Object sLock = new Object(); private ArrayList<Long> mResults; @Before public void setUp() { } @After public void tearDown() { } class EnqueueThread extends Thread { CountDownLatch mStartLatch; CountDownLatch mEndLatch; Handler mHandler; int mMessageStartIdx; Message[] mMessages; long[] mDelays; EnqueueThread(CountDownLatch startLatch, CountDownLatch endLatch, Handler handler, int startIdx, Message[] messages, long[] delays) { super(); mStartLatch = startLatch; mEndLatch = endLatch; mHandler = handler; mMessageStartIdx = startIdx; mMessages = messages; mDelays = delays; } @Override public void run() { Log.d(TAG, "Enqueue thread started at message index " + mMessageStartIdx); try { mStartLatch.await(); } catch (InterruptedException e) { } long now = SystemClock.uptimeMillis(); long startTimeNS = SystemClock.elapsedRealtimeNanos(); for (int i = mMessageStartIdx; i < (mMessageStartIdx + PER_THREAD_MESSAGE_COUNT); i++) { if (mDelays[i] == 0) { mHandler.sendMessageAtFrontOfQueue(mMessages[i]); } else { mHandler.sendMessageAtTime(mMessages[i], now + mDelays[i]); } } long endTimeNS = SystemClock.elapsedRealtimeNanos(); synchronized (sLock) { mResults.add(endTimeNS - startTimeNS); } mEndLatch.countDown(); } } class TestHandler extends Handler { TestHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { } } void reportPerf(String prefix, int threadCount, int perThreadMessageCount) { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Stats stats = new Stats(mResults); Log.d(TAG, "Reporting perf now"); Bundle status = new Bundle(); status.putLong(prefix + "_median_ns", stats.getMedian()); status.putLong(prefix + "_mean_ns", (long) stats.getMean()); status.putLong(prefix + "_min_ns", stats.getMin()); status.putLong(prefix + "_max_ns", stats.getMax()); status.putLong(prefix + "_stddev_ns", (long) stats.getStandardDeviation()); status.putLong(prefix + "_nr_threads", threadCount); status.putLong(prefix + "_msgs_per_thread", perThreadMessageCount); instrumentation.sendStatus(Activity.RESULT_OK, status); } HandlerThread mHandlerThread; private void fillMessagesArray(Message[] messages) { for (int i = 0; i < messages.length; i++) { messages[i] = mHandlerThread.getThreadHandler().obtainMessage(i); } } private void startTestAndWaitOnThreads(CountDownLatch threadStartLatch, CountDownLatch threadEndLatch) { try { threadStartLatch.countDown(); Log.e(TAG, "Test threads started"); threadEndLatch.await(); } catch (InterruptedException ignored) { } Log.e(TAG, "Test threads ended, quitting handler thread"); } @Test public void benchmarkEnqueueAtFrontOfQueue() { CountDownLatch threadStartLatch = new CountDownLatch(1); CountDownLatch threadEndLatch = new CountDownLatch(THREAD_COUNT); mHandlerThread = new HandlerThread("MessageQueuePerfTest"); mHandlerThread.start(); Message[] messages = new Message[TOTAL_MESSAGE_COUNT]; fillMessagesArray(messages); long[] delays = new long[TOTAL_MESSAGE_COUNT]; mResults = new ArrayList<>(); TestHandler handler = new TestHandler(mHandlerThread.getLooper()); for (int i = 0; i < THREAD_COUNT; i++) { EnqueueThread thread = new EnqueueThread(threadStartLatch, threadEndLatch, handler, i * PER_THREAD_MESSAGE_COUNT, messages, delays); thread.start(); } startTestAndWaitOnThreads(threadStartLatch, threadEndLatch); mHandlerThread.quitSafely(); reportPerf("enqueueAtFront", THREAD_COUNT, PER_THREAD_MESSAGE_COUNT); } /** * Fill array with random delays, for benchmarkEnqueueDelayed */ public long[] fillDelayArray() { long[] delays = new long[TOTAL_MESSAGE_COUNT]; Random rand = new Random(0xDEADBEEF); for (int i = 0; i < TOTAL_MESSAGE_COUNT; i++) { delays[i] = Math.abs(rand.nextLong() % 5000); } return delays; } @Test public void benchmarkEnqueueDelayed() { CountDownLatch threadStartLatch = new CountDownLatch(1); CountDownLatch threadEndLatch = new CountDownLatch(THREAD_COUNT); mHandlerThread = new HandlerThread("MessageQueuePerfTest"); mHandlerThread.start(); Message[] messages = new Message[TOTAL_MESSAGE_COUNT]; fillMessagesArray(messages); long[] delays = fillDelayArray(); mResults = new ArrayList<>(); TestHandler handler = new TestHandler(mHandlerThread.getLooper()); for (int i = 0; i < THREAD_COUNT; i++) { EnqueueThread thread = new EnqueueThread(threadStartLatch, threadEndLatch, handler, i * PER_THREAD_MESSAGE_COUNT, messages, delays); thread.start(); } startTestAndWaitOnThreads(threadStartLatch, threadEndLatch); mHandlerThread.quitSafely(); reportPerf("enqueueDelayed", THREAD_COUNT, PER_THREAD_MESSAGE_COUNT); } }
apex/jobscheduler/service/aconfig/app_idle.aconfig +9 −0 Original line number Diff line number Diff line Loading @@ -12,3 +12,12 @@ flag { } } flag { name: "screen_time_bypass" namespace: "backstage_power" description: "Bypass the screen time check for bucket evaluation" bug: "374114769" metadata { purpose: PURPOSE_BUGFIX } }