Loading services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java +119 −24 Original line number Diff line number Diff line Loading @@ -16,14 +16,28 @@ package com.android.server.stats.pull; import static android.net.NetworkStats.METERED_YES; import static android.net.NetworkTemplate.MATCH_MOBILE; import static android.provider.Settings.Global.NETSTATS_UID_BUCKET_DURATION; import static com.android.server.stats.Flags.useNetworkStatsQuerySummary; import static com.android.server.stats.pull.netstats.NetworkStatsUtils.fromPublicNetworkStats; import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MICROSECONDS; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.StatsManager; import android.app.usage.NetworkStatsManager; import android.content.Context; import android.net.NetworkStats; import android.net.NetworkTemplate; import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; import android.os.Trace; import android.provider.Settings; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseIntArray; Loading @@ -32,6 +46,7 @@ import android.util.StatsEvent; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; import com.android.server.selinux.RateLimiter; import com.android.server.stats.pull.netstats.NetworkStatsAccumulator; import java.time.Duration; import java.util.List; Loading @@ -45,6 +60,8 @@ class AggregatedMobileDataStatsPuller { private static final boolean DEBUG = false; private static final long NETSTATS_UID_DEFAULT_BUCKET_DURATION_MS = HOURS.toMillis(2); private static class UidProcState { private final int mUid; Loading @@ -57,8 +74,12 @@ class AggregatedMobileDataStatsPuller { @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof UidProcState key)) return false; if (this == o) { return true; } if (!(o instanceof UidProcState key)) { return false; } return mUid == key.mUid && mState == key.mState; } Loading @@ -76,7 +97,6 @@ class AggregatedMobileDataStatsPuller { public int getState() { return mState; } } private static class MobileDataStats { Loading Loading @@ -128,7 +148,6 @@ class AggregatedMobileDataStatsPuller { // No reason to keep more dimensions than 3000. The 3000 is the hard top for the statsd metrics // dimensions guardrail. It also will keep the result binder transaction size capped to // approximately 220kB for 3000 atoms private static final int UID_STATS_MAX_SIZE = 3000; private final SparseIntArray mUidPreviousState; Loading @@ -141,17 +160,54 @@ class AggregatedMobileDataStatsPuller { private final RateLimiter mRateLimiter; AggregatedMobileDataStatsPuller(@NonNull NetworkStatsManager networkStatsManager) { if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { private final Context mContext; private final NetworkStatsAccumulator mStatsAccumulator; /** * Polling NetworkStats is a heavy operation and it should be done sparingly. Atom pulls may * happen in bursts, but these should be infrequent. The poll rate limit ensures that data is * sufficiently fresh (i.e. not stale) while reducing system load during atom pull bursts. */ private static final long NETSTATS_POLL_RATE_LIMIT_MS = 15000; private long mLastNetworkStatsPollTime = -NETSTATS_POLL_RATE_LIMIT_MS; AggregatedMobileDataStatsPuller(@NonNull Context context) { final boolean traceEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER); if (traceEnabled) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-Init"); } mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(1)); mContext = context; mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(2)); mUidStats = new ArrayMap<>(); mUidPreviousState = new SparseIntArray(); mNetworkStatsManager = networkStatsManager; final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime(); final long currentTimeMillis = MICROSECONDS.toMillis(SystemClock.currentTimeMicro()); final long bootTimeMillis = currentTimeMillis - elapsedMillisSinceBoot; final long bucketDurationMillis = Settings.Global.getLong( mContext.getContentResolver(), NETSTATS_UID_BUCKET_DURATION, NETSTATS_UID_DEFAULT_BUCKET_DURATION_MS); mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class); if (useNetworkStatsQuerySummary()) { NetworkTemplate template = new NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build(); mStatsAccumulator = new NetworkStatsAccumulator( template, false, bucketDurationMillis, bootTimeMillis - bucketDurationMillis); } else { mStatsAccumulator = null; } HandlerThread mMobileDataStatsHandlerThread = new HandlerThread("MobileDataStatsHandler"); mMobileDataStatsHandlerThread.start(); Loading @@ -163,7 +219,7 @@ class AggregatedMobileDataStatsPuller { updateNetworkStats(mNetworkStatsManager); }); } if (DEBUG) { if (traceEnabled) { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } Loading Loading @@ -191,14 +247,14 @@ class AggregatedMobileDataStatsPuller { @GuardedBy("mLock") private MobileDataStats getUidStatsForPreviousStateLocked(int uid) { final int previousState = mUidPreviousState.get(uid, ActivityManager.PROCESS_STATE_UNKNOWN); final UidProcState statsKey = new UidProcState(uid, previousState); if (mUidStats.containsKey(statsKey)) { return mUidStats.get(statsKey); } else { if (DEBUG && previousState == ActivityManager.PROCESS_STATE_UNKNOWN) { Slog.d(TAG, "getUidStatsForPreviousStateLocked() no prev state info for uid " + uid + ". Tracking stats with ActivityManager.PROCESS_STATE_UNKNOWN"); } final UidProcState statsKey = new UidProcState(uid, previousState); if (mUidStats.containsKey(statsKey)) { return mUidStats.get(statsKey); } if (mUidStats.size() < UID_STATS_MAX_SIZE) { MobileDataStats stats = new MobileDataStats(); Loading @@ -215,11 +271,13 @@ class AggregatedMobileDataStatsPuller { // noteUidProcessStateImpl can be called back to back several times while // the updateNetworkStats loops over several stats for multiple uids // and during the first call in a batch of proc state change event it can // contain info for uid with unknown previous state yet which can happen due to a few // contain info for uid with unknown previous state yet which can happen due to // a few // reasons: // - app was just started // - app was started before the ActivityManagerService // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN // as result stats would be created with state == // ActivityManager.PROCESS_STATE_UNKNOWN if (mNetworkStatsManager != null) { updateNetworkStats(mNetworkStatsManager); } else { Loading @@ -230,12 +288,44 @@ class AggregatedMobileDataStatsPuller { } } private NetworkStats getMobileUidStats(NetworkStatsManager networkStatsManager) { if (useNetworkStatsQuerySummary()) { // networkStatsManager.querySummary provides reduced data resolution // and higher latency for fresh data compared to networkStatsManager.getMobileUidStats. // On the other hand getMobileUidStats provides realtime data in the memory // with higher performance cost. // Assumption is the tradeoff is acceptable in regards to existing // atom MOBILE_BYTES_TRANSFER use case final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime(); if (elapsedMillisSinceBoot - mLastNetworkStatsPollTime >= NETSTATS_POLL_RATE_LIMIT_MS) { mLastNetworkStatsPollTime = elapsedMillisSinceBoot; Slog.d(TAG, "getMobileUidStats() forceUpdate"); networkStatsManager.forceUpdate(); } final long currentTimeMillis = MICROSECONDS.toMillis(SystemClock.currentTimeMicro()); return mStatsAccumulator.queryStats( currentTimeMillis, (aTemplate, aIncludeTags, aStartTime, aEndTime) -> { final android.app.usage.NetworkStats queryNonTaggedStats = networkStatsManager.querySummary(aTemplate, aStartTime, aEndTime); final NetworkStats nonTaggedStats = fromPublicNetworkStats(queryNonTaggedStats); queryNonTaggedStats.close(); return nonTaggedStats; }); } else { return networkStatsManager.getMobileUidStats(); } } private void updateNetworkStats(NetworkStatsManager networkStatsManager) { if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { final boolean traceEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER); if (traceEnabled) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); } final NetworkStats latestStats = networkStatsManager.getMobileUidStats(); final NetworkStats latestStats = getMobileUidStats(networkStatsManager); if (isEmpty(latestStats)) { if (DEBUG) { Slog.w(TAG, "getMobileUidStats() failed"); Loading @@ -243,6 +333,10 @@ class AggregatedMobileDataStatsPuller { } return; } if (DEBUG) { Slog.d(TAG, "latestStats: \n" + latestStats.toString()); } NetworkStats delta = latestStats.subtract(mLastMobileUidStats); mLastMobileUidStats = latestStats; Loading @@ -251,18 +345,22 @@ class AggregatedMobileDataStatsPuller { } else if (DEBUG) { Slog.w(TAG, "updateNetworkStats() no delta"); } if (DEBUG) { if (traceEnabled) { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } private void updateNetworkStatsDelta(NetworkStats delta) { if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { final boolean traceEnabled = DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER); if (traceEnabled) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStatsDelta"); } synchronized (mLock) { for (NetworkStats.Entry entry : delta) { if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) { if (DEBUG) { Slog.d(TAG, entry.toString()); } MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); if (stats != null) { stats.addTxBytes(entry.getTxBytes()); Loading @@ -273,16 +371,13 @@ class AggregatedMobileDataStatsPuller { } } } if (DEBUG) { if (traceEnabled) { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } @GuardedBy("mLock") private int pullDataBytesTransferLocked(List<StatsEvent> pulledData) { if (DEBUG) { Slog.d(TAG, "pullDataBytesTransferLocked() start"); } for (Map.Entry<UidProcState, MobileDataStats> uidStats : mUidStats.entrySet()) { if (!uidStats.getValue().isEmpty()) { MobileDataStats stats = uidStats.getValue(); Loading services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +1 −3 Original line number Diff line number Diff line Loading @@ -1071,9 +1071,7 @@ public class StatsPullAtomService extends SystemService { + ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER); } if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) { mAggregatedMobileDataStatsPuller = new AggregatedMobileDataStatsPuller( mContext.getSystemService(NetworkStatsManager.class)); mAggregatedMobileDataStatsPuller = new AggregatedMobileDataStatsPuller(mContext); } } Loading services/core/java/com/android/server/stats/stats_flags.aconfig +9 −1 Original line number Diff line number Diff line Loading @@ -46,3 +46,11 @@ flag { bug: "365731097" is_fixed_read_only: true } flag { name: "use_network_stats_query_summary" namespace: "statsd" description: "Use NetworkStats querySummary API for AggregatedMobileDataStatsPuller" bug: "398327159" is_fixed_read_only: true } Loading
services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java +119 −24 Original line number Diff line number Diff line Loading @@ -16,14 +16,28 @@ package com.android.server.stats.pull; import static android.net.NetworkStats.METERED_YES; import static android.net.NetworkTemplate.MATCH_MOBILE; import static android.provider.Settings.Global.NETSTATS_UID_BUCKET_DURATION; import static com.android.server.stats.Flags.useNetworkStatsQuerySummary; import static com.android.server.stats.pull.netstats.NetworkStatsUtils.fromPublicNetworkStats; import static java.util.concurrent.TimeUnit.HOURS; import static java.util.concurrent.TimeUnit.MICROSECONDS; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.StatsManager; import android.app.usage.NetworkStatsManager; import android.content.Context; import android.net.NetworkStats; import android.net.NetworkTemplate; import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; import android.os.Trace; import android.provider.Settings; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseIntArray; Loading @@ -32,6 +46,7 @@ import android.util.StatsEvent; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; import com.android.server.selinux.RateLimiter; import com.android.server.stats.pull.netstats.NetworkStatsAccumulator; import java.time.Duration; import java.util.List; Loading @@ -45,6 +60,8 @@ class AggregatedMobileDataStatsPuller { private static final boolean DEBUG = false; private static final long NETSTATS_UID_DEFAULT_BUCKET_DURATION_MS = HOURS.toMillis(2); private static class UidProcState { private final int mUid; Loading @@ -57,8 +74,12 @@ class AggregatedMobileDataStatsPuller { @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof UidProcState key)) return false; if (this == o) { return true; } if (!(o instanceof UidProcState key)) { return false; } return mUid == key.mUid && mState == key.mState; } Loading @@ -76,7 +97,6 @@ class AggregatedMobileDataStatsPuller { public int getState() { return mState; } } private static class MobileDataStats { Loading Loading @@ -128,7 +148,6 @@ class AggregatedMobileDataStatsPuller { // No reason to keep more dimensions than 3000. The 3000 is the hard top for the statsd metrics // dimensions guardrail. It also will keep the result binder transaction size capped to // approximately 220kB for 3000 atoms private static final int UID_STATS_MAX_SIZE = 3000; private final SparseIntArray mUidPreviousState; Loading @@ -141,17 +160,54 @@ class AggregatedMobileDataStatsPuller { private final RateLimiter mRateLimiter; AggregatedMobileDataStatsPuller(@NonNull NetworkStatsManager networkStatsManager) { if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { private final Context mContext; private final NetworkStatsAccumulator mStatsAccumulator; /** * Polling NetworkStats is a heavy operation and it should be done sparingly. Atom pulls may * happen in bursts, but these should be infrequent. The poll rate limit ensures that data is * sufficiently fresh (i.e. not stale) while reducing system load during atom pull bursts. */ private static final long NETSTATS_POLL_RATE_LIMIT_MS = 15000; private long mLastNetworkStatsPollTime = -NETSTATS_POLL_RATE_LIMIT_MS; AggregatedMobileDataStatsPuller(@NonNull Context context) { final boolean traceEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER); if (traceEnabled) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-Init"); } mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(1)); mContext = context; mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(2)); mUidStats = new ArrayMap<>(); mUidPreviousState = new SparseIntArray(); mNetworkStatsManager = networkStatsManager; final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime(); final long currentTimeMillis = MICROSECONDS.toMillis(SystemClock.currentTimeMicro()); final long bootTimeMillis = currentTimeMillis - elapsedMillisSinceBoot; final long bucketDurationMillis = Settings.Global.getLong( mContext.getContentResolver(), NETSTATS_UID_BUCKET_DURATION, NETSTATS_UID_DEFAULT_BUCKET_DURATION_MS); mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class); if (useNetworkStatsQuerySummary()) { NetworkTemplate template = new NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build(); mStatsAccumulator = new NetworkStatsAccumulator( template, false, bucketDurationMillis, bootTimeMillis - bucketDurationMillis); } else { mStatsAccumulator = null; } HandlerThread mMobileDataStatsHandlerThread = new HandlerThread("MobileDataStatsHandler"); mMobileDataStatsHandlerThread.start(); Loading @@ -163,7 +219,7 @@ class AggregatedMobileDataStatsPuller { updateNetworkStats(mNetworkStatsManager); }); } if (DEBUG) { if (traceEnabled) { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } Loading Loading @@ -191,14 +247,14 @@ class AggregatedMobileDataStatsPuller { @GuardedBy("mLock") private MobileDataStats getUidStatsForPreviousStateLocked(int uid) { final int previousState = mUidPreviousState.get(uid, ActivityManager.PROCESS_STATE_UNKNOWN); final UidProcState statsKey = new UidProcState(uid, previousState); if (mUidStats.containsKey(statsKey)) { return mUidStats.get(statsKey); } else { if (DEBUG && previousState == ActivityManager.PROCESS_STATE_UNKNOWN) { Slog.d(TAG, "getUidStatsForPreviousStateLocked() no prev state info for uid " + uid + ". Tracking stats with ActivityManager.PROCESS_STATE_UNKNOWN"); } final UidProcState statsKey = new UidProcState(uid, previousState); if (mUidStats.containsKey(statsKey)) { return mUidStats.get(statsKey); } if (mUidStats.size() < UID_STATS_MAX_SIZE) { MobileDataStats stats = new MobileDataStats(); Loading @@ -215,11 +271,13 @@ class AggregatedMobileDataStatsPuller { // noteUidProcessStateImpl can be called back to back several times while // the updateNetworkStats loops over several stats for multiple uids // and during the first call in a batch of proc state change event it can // contain info for uid with unknown previous state yet which can happen due to a few // contain info for uid with unknown previous state yet which can happen due to // a few // reasons: // - app was just started // - app was started before the ActivityManagerService // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN // as result stats would be created with state == // ActivityManager.PROCESS_STATE_UNKNOWN if (mNetworkStatsManager != null) { updateNetworkStats(mNetworkStatsManager); } else { Loading @@ -230,12 +288,44 @@ class AggregatedMobileDataStatsPuller { } } private NetworkStats getMobileUidStats(NetworkStatsManager networkStatsManager) { if (useNetworkStatsQuerySummary()) { // networkStatsManager.querySummary provides reduced data resolution // and higher latency for fresh data compared to networkStatsManager.getMobileUidStats. // On the other hand getMobileUidStats provides realtime data in the memory // with higher performance cost. // Assumption is the tradeoff is acceptable in regards to existing // atom MOBILE_BYTES_TRANSFER use case final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime(); if (elapsedMillisSinceBoot - mLastNetworkStatsPollTime >= NETSTATS_POLL_RATE_LIMIT_MS) { mLastNetworkStatsPollTime = elapsedMillisSinceBoot; Slog.d(TAG, "getMobileUidStats() forceUpdate"); networkStatsManager.forceUpdate(); } final long currentTimeMillis = MICROSECONDS.toMillis(SystemClock.currentTimeMicro()); return mStatsAccumulator.queryStats( currentTimeMillis, (aTemplate, aIncludeTags, aStartTime, aEndTime) -> { final android.app.usage.NetworkStats queryNonTaggedStats = networkStatsManager.querySummary(aTemplate, aStartTime, aEndTime); final NetworkStats nonTaggedStats = fromPublicNetworkStats(queryNonTaggedStats); queryNonTaggedStats.close(); return nonTaggedStats; }); } else { return networkStatsManager.getMobileUidStats(); } } private void updateNetworkStats(NetworkStatsManager networkStatsManager) { if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { final boolean traceEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER); if (traceEnabled) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); } final NetworkStats latestStats = networkStatsManager.getMobileUidStats(); final NetworkStats latestStats = getMobileUidStats(networkStatsManager); if (isEmpty(latestStats)) { if (DEBUG) { Slog.w(TAG, "getMobileUidStats() failed"); Loading @@ -243,6 +333,10 @@ class AggregatedMobileDataStatsPuller { } return; } if (DEBUG) { Slog.d(TAG, "latestStats: \n" + latestStats.toString()); } NetworkStats delta = latestStats.subtract(mLastMobileUidStats); mLastMobileUidStats = latestStats; Loading @@ -251,18 +345,22 @@ class AggregatedMobileDataStatsPuller { } else if (DEBUG) { Slog.w(TAG, "updateNetworkStats() no delta"); } if (DEBUG) { if (traceEnabled) { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } private void updateNetworkStatsDelta(NetworkStats delta) { if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { final boolean traceEnabled = DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER); if (traceEnabled) { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStatsDelta"); } synchronized (mLock) { for (NetworkStats.Entry entry : delta) { if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) { if (DEBUG) { Slog.d(TAG, entry.toString()); } MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); if (stats != null) { stats.addTxBytes(entry.getTxBytes()); Loading @@ -273,16 +371,13 @@ class AggregatedMobileDataStatsPuller { } } } if (DEBUG) { if (traceEnabled) { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } @GuardedBy("mLock") private int pullDataBytesTransferLocked(List<StatsEvent> pulledData) { if (DEBUG) { Slog.d(TAG, "pullDataBytesTransferLocked() start"); } for (Map.Entry<UidProcState, MobileDataStats> uidStats : mUidStats.entrySet()) { if (!uidStats.getValue().isEmpty()) { MobileDataStats stats = uidStats.getValue(); Loading
services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +1 −3 Original line number Diff line number Diff line Loading @@ -1071,9 +1071,7 @@ public class StatsPullAtomService extends SystemService { + ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER); } if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) { mAggregatedMobileDataStatsPuller = new AggregatedMobileDataStatsPuller( mContext.getSystemService(NetworkStatsManager.class)); mAggregatedMobileDataStatsPuller = new AggregatedMobileDataStatsPuller(mContext); } } Loading
services/core/java/com/android/server/stats/stats_flags.aconfig +9 −1 Original line number Diff line number Diff line Loading @@ -46,3 +46,11 @@ flag { bug: "365731097" is_fixed_read_only: true } flag { name: "use_network_stats_query_summary" namespace: "statsd" description: "Use NetworkStats querySummary API for AggregatedMobileDataStatsPuller" bug: "398327159" is_fixed_read_only: true }