Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0f3702e5 authored by Hung-ying Tyan's avatar Hung-ying Tyan Committed by Android (Google) Code Review
Browse files

Revert "[res] Fix the registered shared lib asset caching"

Revert submission 28533683-shared-lib-asset-caching

Reason for revert: b/356730282 (build break on aosp-24Q3-ts-dev)

Reverted changes: /q/submissionid:28533683-shared-lib-asset-caching

Change-Id: Id4a1121019c7853659fc84777b078b5bad568add
parent dc1d67b4
Loading
Loading
Loading
Loading
+95 −167
Original line number Diff line number Diff line
@@ -30,7 +30,6 @@ import android.content.res.AssetManager;
import android.content.res.CompatResources;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Flags;
import android.content.res.Resources;
import android.content.res.ResourcesImpl;
import android.content.res.ResourcesKey;
@@ -139,22 +138,16 @@ public class ResourcesManager {
    private final ArrayMap<String, SharedLibraryAssets> mSharedLibAssetsMap =
            new ArrayMap<>();

    @VisibleForTesting
    public ArrayMap<String, SharedLibraryAssets> getRegisteredResourcePaths() {
        return mSharedLibAssetsMap;
    }

    /**
     * The internal function to register the resources paths of a package (e.g. a shared library).
     * This will collect the package resources' paths from its ApplicationInfo and add them to all
     * existing and future contexts while the application is running.
     */
    public void registerResourcePaths(@NonNull String uniqueId, @NonNull ApplicationInfo appInfo) {
        if (!Flags.registerResourcePaths()) {
            return;
        }
        SharedLibraryAssets sharedLibAssets = new SharedLibraryAssets(appInfo.sourceDir,
                appInfo.splitSourceDirs, appInfo.sharedLibraryFiles,
                appInfo.resourceDirs, appInfo.overlayPaths);

        final var sharedLibAssets = new SharedLibraryAssets(appInfo);
        synchronized (mLock) {
            if (mSharedLibAssetsMap.containsKey(uniqueId)) {
                Slog.v(TAG, "Package resources' paths for uniqueId: " + uniqueId
@@ -162,37 +155,18 @@ public class ResourcesManager {
                return;
            }
            mSharedLibAssetsMap.put(uniqueId, sharedLibAssets);
            appendLibAssetsLocked(sharedLibAssets);
            Slog.v(TAG, "The following library key has been added: "
                    + sharedLibAssets.getResourcesKey());
        }
    }

    /**
     * Apply the registered library paths to the passed impl object
     * @return the hash code for the current version of the registered paths
     */
    public int updateResourceImplWithRegisteredLibs(@NonNull ResourcesImpl impl) {
        if (!Flags.registerResourcePaths()) {
            return 0;
            appendLibAssetsLocked(sharedLibAssets.getAllAssetPaths());
            Slog.v(TAG, "The following resources' paths have been added: "
                    + Arrays.toString(sharedLibAssets.getAllAssetPaths()));
        }

        final var collector = new PathCollector(null);
        final int size = mSharedLibAssetsMap.size();
        for (int i = 0; i < size; i++) {
            final var libraryKey = mSharedLibAssetsMap.valueAt(i).getResourcesKey();
            collector.appendKey(libraryKey);
        }
        impl.getAssets().addPresetApkKeys(extractApkKeys(collector.collectedKey()));
        return size;
    }

    public static class ApkKey {
    private static class ApkKey {
        public final String path;
        public final boolean sharedLib;
        public final boolean overlay;

        public ApkKey(String path, boolean sharedLib, boolean overlay) {
        ApkKey(String path, boolean sharedLib, boolean overlay) {
            this.path = path;
            this.sharedLib = sharedLib;
            this.overlay = overlay;
@@ -216,12 +190,6 @@ public class ResourcesManager {
            return this.path.equals(other.path) && this.sharedLib == other.sharedLib
                    && this.overlay == other.overlay;
        }

        @Override
        public String toString() {
            return "ApkKey[" + (sharedLib ? "lib" : "app") + (overlay ? ", overlay" : "") + ": "
                    + path + "]";
        }
    }

    /**
@@ -537,10 +505,7 @@ public class ResourcesManager {
        return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap";
    }

    /**
     * Loads the ApkAssets object for the passed key, or picks the one from the cache if available.
     */
    public @NonNull ApkAssets loadApkAssets(@NonNull final ApkKey key) throws IOException {
    private @NonNull ApkAssets loadApkAssets(@NonNull final ApkKey key) throws IOException {
        ApkAssets apkAssets;

        // Optimistically check if this ApkAssets exists somewhere else.
@@ -782,8 +747,8 @@ public class ResourcesManager {
    private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
            @NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) {
        ResourcesImpl impl = findResourcesImplForKeyLocked(key);
        // ResourcesImpl also need to be recreated if its shared library hash is not up-to-date.
        if (impl == null || impl.getAppliedSharedLibsHash() != mSharedLibAssetsMap.size()) {
        // ResourcesImpl also need to be recreated if its shared library count is not up-to-date.
        if (impl == null || impl.getSharedLibCount() != mSharedLibAssetsMap.size()) {
            impl = createResourcesImpl(key, apkSupplier);
            if (impl != null) {
                mResourceImpls.put(key, new WeakReference<>(impl));
@@ -1568,108 +1533,55 @@ public class ResourcesManager {
        }
    }

    /**
     * A utility class to collect resources paths into a ResourcesKey object:
     *  - Separates the libraries and the overlays into different sets as those are loaded in
     *    different ways.
     *  - Allows to start with an existing original key object, and copies all non-path related
     *    properties into the final one.
     *  - Preserves the path order while dropping all duplicates in an efficient manner.
     */
    private static class PathCollector {
        public final ResourcesKey originalKey;
        public final ArrayList<String> orderedLibs = new ArrayList<>();
        public final ArraySet<String> libsSet = new ArraySet<>();
        public final ArrayList<String> orderedOverlays = new ArrayList<>();
        public final ArraySet<String> overlaysSet = new ArraySet<>();

        static void appendNewPath(@NonNull String path,
                @NonNull ArraySet<String> uniquePaths, @NonNull ArrayList<String> orderedPaths) {
            if (uniquePaths.add(path)) {
                orderedPaths.add(path);
            }
        }

        static void appendAllNewPaths(@Nullable String[] paths,
                @NonNull ArraySet<String> uniquePaths, @NonNull ArrayList<String> orderedPaths) {
            if (paths == null) return;
            for (int i = 0, size = paths.length; i < size; i++) {
                appendNewPath(paths[i], uniquePaths, orderedPaths);
            }
        }

        PathCollector(@Nullable ResourcesKey original) {
            originalKey = original;
            if (originalKey != null) {
                appendKey(originalKey);
            }
        }

        public void appendKey(@NonNull ResourcesKey key) {
            appendAllNewPaths(key.mLibDirs, libsSet, orderedLibs);
            appendAllNewPaths(key.mOverlayPaths, overlaysSet, orderedOverlays);
        }

        boolean isSameAsOriginal() {
            if (originalKey == null) {
                return orderedLibs.isEmpty() && orderedOverlays.isEmpty();
            }
            return ((originalKey.mLibDirs == null && orderedLibs.isEmpty())
                        || (originalKey.mLibDirs != null
                            && originalKey.mLibDirs.length == orderedLibs.size()))
                    && ((originalKey.mOverlayPaths == null && orderedOverlays.isEmpty())
                        || (originalKey.mOverlayPaths != null
                                && originalKey.mOverlayPaths.length == orderedOverlays.size()));
        }

        @NonNull ResourcesKey collectedKey() {
            return new ResourcesKey(
                    originalKey == null ? null : originalKey.mResDir,
                    originalKey == null ? null : originalKey.mSplitResDirs,
                    orderedOverlays.toArray(new String[0]), orderedLibs.toArray(new String[0]),
                    originalKey == null ? 0 : originalKey.mDisplayId,
                    originalKey == null ? null : originalKey.mOverrideConfiguration,
                    originalKey == null ? null : originalKey.mCompatInfo,
                    originalKey == null ? null : originalKey.mLoaders);
        }
    }

    /**
     * Takes the original resources key and the one containing a set of library paths and overlays
     * to append, and combines them together. In case when the original key already contains all
     * those paths this function returns null, otherwise it makes a new ResourcesKey object.
     */
    private @Nullable ResourcesKey createNewResourceKeyIfNeeded(
            @NonNull ResourcesKey original, @NonNull ResourcesKey library) {
        final var collector = new PathCollector(original);
        collector.appendKey(library);
        return collector.isSameAsOriginal() ? null : collector.collectedKey();
    }

    /**
     * Append the newly registered shared library asset paths to all existing resources objects.
     */
    private void appendLibAssetsLocked(@NonNull SharedLibraryAssets libAssets) {
        // Record the ResourcesImpl's that need updating, and what ResourcesKey they should
        // update to.
    private void appendLibAssetsLocked(String[] libAssets) {
        synchronized (mLock) {
            // Record which ResourcesImpl need updating
            // (and what ResourcesKey they should update to).
            final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys = new ArrayMap<>();

            final int implCount = mResourceImpls.size();
            for (int i = 0; i < implCount; i++) {
                final ResourcesKey key = mResourceImpls.keyAt(i);
                final WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
                final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
                if (impl == null) {
                Slog.w(TAG, "Found a null ResourcesImpl, skipped.");
                    Slog.w(TAG, "Found a ResourcesImpl which is null, skip it and continue to "
                            + "append shared library assets for next ResourcesImpl.");
                    continue;
                }

            final var newKey = createNewResourceKeyIfNeeded(key, libAssets.getResourcesKey());
            if (newKey != null) {
                updatedResourceKeys.put(impl, newKey);
                var newDirs = new ArrayList<String>();
                var dirsSet = new ArraySet<String>();
                if (key.mLibDirs != null) {
                    final int dirsLength = key.mLibDirs.length;
                    for (int k = 0; k < dirsLength; k++) {
                        newDirs.add(key.mLibDirs[k]);
                        dirsSet.add(key.mLibDirs[k]);
                    }
                }
                final int assetsLength = libAssets.length;
                for (int j = 0; j < assetsLength; j++) {
                    if (dirsSet.add(libAssets[j])) {
                        newDirs.add(libAssets[j]);
                    }
                }
                String[] newLibAssets = newDirs.toArray(new String[0]);
                if (!Arrays.equals(newLibAssets, key.mLibDirs)) {
                    updatedResourceKeys.put(impl, new ResourcesKey(
                            key.mResDir,
                            key.mSplitResDirs,
                            key.mOverlayPaths,
                            newLibAssets,
                            key.mDisplayId,
                            key.mOverrideConfiguration,
                            key.mCompatInfo,
                            key.mLoaders));
                }
            }

            redirectAllResourcesToNewImplLocked(updatedResourceKeys);
        }
    }

    private void applyNewResourceDirsLocked(@Nullable final String[] oldSourceDirs,
            @NonNull final ApplicationInfo appInfo) {
@@ -1806,9 +1718,8 @@ public class ResourcesManager {
        }
    }

    // Another redirect function which will loop through all Resources in the process, even the ones
    // the app created outside of the regular Android Runtime, and reload their ResourcesImpl if it
    // needs a shared library asset paths update.
    // Another redirect function which will loop through all Resources and reload ResourcesImpl
    // if it needs a shared library asset paths update.
    private void redirectAllResourcesToNewImplLocked(
            @NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) {
        cleanupReferences(mAllResourceReferences, mAllResourceReferencesQueue);
@@ -1919,35 +1830,52 @@ public class ResourcesManager {
        }
    }

    @VisibleForTesting
    public static class SharedLibraryAssets{
        private final ResourcesKey mResourcesKey;

        private SharedLibraryAssets(ApplicationInfo appInfo) {
            // We're loading all library's files as shared libs, regardless where they are in
            // its own ApplicationInfo.
            final var collector = new PathCollector(null);
            PathCollector.appendNewPath(appInfo.sourceDir, collector.libsSet,
                    collector.orderedLibs);
            PathCollector.appendAllNewPaths(appInfo.splitSourceDirs, collector.libsSet,
                    collector.orderedLibs);
            PathCollector.appendAllNewPaths(appInfo.sharedLibraryFiles, collector.libsSet,
                    collector.orderedLibs);
            PathCollector.appendAllNewPaths(appInfo.resourceDirs, collector.overlaysSet,
                    collector.orderedOverlays);
            PathCollector.appendAllNewPaths(appInfo.overlayPaths, collector.overlaysSet,
                    collector.orderedOverlays);
            mResourcesKey = collector.collectedKey();
        private final String[] mAssetPaths;

        SharedLibraryAssets(String sourceDir, String[] splitSourceDirs, String[] sharedLibraryFiles,
                String[] resourceDirs, String[] overlayPaths) {
            mAssetPaths = collectAssetPaths(sourceDir, splitSourceDirs, sharedLibraryFiles,
                    resourceDirs, overlayPaths);
        }

        private @NonNull String[] collectAssetPaths(String sourceDir, String[] splitSourceDirs,
                String[] sharedLibraryFiles, String[] resourceDirs, String[] overlayPaths) {
            final String[][] inputLists = {
                    splitSourceDirs, sharedLibraryFiles, resourceDirs, overlayPaths
            };

            final ArraySet<String> assetPathSet = new ArraySet<>();
            final List<String> assetPathList = new ArrayList<>();
            if (sourceDir != null) {
                assetPathSet.add(sourceDir);
                assetPathList.add(sourceDir);
            }

            for (int i = 0; i < inputLists.length; i++) {
                if (inputLists[i] != null) {
                    for (int j = 0; j < inputLists[i].length; j++) {
                        if (assetPathSet.add(inputLists[i][j])) {
                            assetPathList.add(inputLists[i][j]);
                        }
                    }
                }
            }
            return assetPathList.toArray(new String[0]);
        }

        /**
         * @return the resources key for this library assets.
         * @return all the asset paths of this collected in this class.
         */
        public @NonNull ResourcesKey getResourcesKey() {
            return mResourcesKey;
        public @NonNull String[] getAllAssetPaths() {
            return mAssetPaths;
        }
    }

    public @NonNull ArrayMap<String, SharedLibraryAssets> getSharedLibAssetsMap() {
        return new ArrayMap<>(mSharedLibAssetsMap);
    }

    /**
     * Add all resources references to the list which is designed to help to append shared library
     * asset paths. This is invoked in Resources constructor to include all Resources instances.
+28 −90
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package android.content.res;

import static android.content.res.Resources.ID_NULL;
import static android.app.ResourcesManager.ApkKey;

import android.annotation.AnyRes;
import android.annotation.ArrayRes;
@@ -27,7 +26,6 @@ import android.annotation.Nullable;
import android.annotation.StringRes;
import android.annotation.StyleRes;
import android.annotation.TestApi;
import android.app.ResourcesManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration.NativeConfig;
@@ -266,7 +264,7 @@ public final class AssetManager implements AutoCloseable {
            }

            sSystemApkAssetsSet = new ArraySet<>(apkAssets);
            sSystemApkAssets = apkAssets.toArray(new ApkAssets[0]);
            sSystemApkAssets = apkAssets.toArray(new ApkAssets[apkAssets.size()]);
            if (sSystem == null) {
                sSystem = new AssetManager(true /*sentinel*/);
            }
@@ -450,7 +448,7 @@ public final class AssetManager implements AutoCloseable {
    @Deprecated
    @UnsupportedAppUsage
    public int addAssetPath(String path) {
        return addAssetPathInternal(List.of(new ApkKey(path, false, false)), false);
        return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
    }

    /**
@@ -460,7 +458,7 @@ public final class AssetManager implements AutoCloseable {
    @Deprecated
    @UnsupportedAppUsage
    public int addAssetPathAsSharedLibrary(String path) {
        return addAssetPathInternal(List.of(new ApkKey(path, true, false)), false);
        return addAssetPathInternal(path, false /*overlay*/, true /*appAsLib*/);
    }

    /**
@@ -470,107 +468,47 @@ public final class AssetManager implements AutoCloseable {
    @Deprecated
    @UnsupportedAppUsage
    public int addOverlayPath(String path) {
        return addAssetPathInternal(List.of(new ApkKey(path, false, true)), false);
        return addAssetPathInternal(path, true /*overlay*/, false /*appAsLib*/);
    }

    /**
     * @hide
     */
    public void addPresetApkKeys(@NonNull List<ApkKey> keys) {
        addAssetPathInternal(keys, true);
    public void addSharedLibraryPaths(@NonNull String[] paths) {
        final int length = paths.length;
        for (int i = 0; i < length; i++) {
            addAssetPathInternal(paths[i], false, true);
        }

    private int addAssetPathInternal(List<ApkKey> apkKeys, boolean presetAssets) {
        Objects.requireNonNull(apkKeys, "apkKeys");
        if (apkKeys.isEmpty()) {
            return 0;
    }

    private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {
        Objects.requireNonNull(path, "path");
        synchronized (this) {
            ensureOpenLocked();
            final int count = mApkAssets.length;

            // See if we already have some of the apkKeys loaded.
            final int originalAssetsCount = mApkAssets.length;

            // Getting an assets' path is a relatively expensive operation, cache them.
            final ArrayMap<String, Integer> assetPaths = new ArrayMap<>(originalAssetsCount);
            for (int i = 0; i < originalAssetsCount; i++) {
                assetPaths.put(mApkAssets[i].getAssetPath(), i);
            }

            final var newKeys = new ArrayList<ApkKey>(apkKeys.size());
            int lastFoundIndex = -1;
            for (int i = 0, pathsSize = apkKeys.size(); i < pathsSize; i++) {
                final var key = apkKeys.get(i);
                final var index = assetPaths.get(key.path);
                if (index == null) {
                    newKeys.add(key);
                } else {
                    lastFoundIndex = index;
                }
            }
            if (newKeys.isEmpty()) {
                return lastFoundIndex + 1;
            }

            final var newAssets = loadAssets(newKeys);
            if (newAssets.isEmpty()) {
                return 0;
            }
            mApkAssets = makeNewAssetsArrayLocked(newAssets);
            nativeSetApkAssets(mObject, mApkAssets, true, presetAssets);
            invalidateCachesLocked(-1);
            return originalAssetsCount + 1;
            // See if we already have it loaded.
            for (int i = 0; i < count; i++) {
                if (mApkAssets[i].getAssetPath().equals(path)) {
                    return i + 1;
                }
            }

    /**
     * Insert the new assets preserving the correct order: all non-loader assets go before all
     * of the loader assets.
     */
    @GuardedBy("this")
    private @NonNull ApkAssets[] makeNewAssetsArrayLocked(
            @NonNull ArrayList<ApkAssets> newNonLoaderAssets) {
        final int originalAssetsCount = mApkAssets.length;
        int firstLoaderIndex = originalAssetsCount;
        for (int i = 0; i < originalAssetsCount; i++) {
            if (mApkAssets[i].isForLoader()) {
                firstLoaderIndex = i;
                break;
            }
        }
        final int newAssetsSize = newNonLoaderAssets.size();
        final var newAssetsArray = new ApkAssets[originalAssetsCount + newAssetsSize];
        if (firstLoaderIndex > 0) {
            // This should always be true, but who knows...
            System.arraycopy(mApkAssets, 0, newAssetsArray, 0, firstLoaderIndex);
        }
        for (int i = 0; i < newAssetsSize; i++) {
            newAssetsArray[firstLoaderIndex + i] = newNonLoaderAssets.get(i);
        }
        if (originalAssetsCount > firstLoaderIndex) {
            System.arraycopy(
                    mApkAssets, firstLoaderIndex,
                    newAssetsArray, firstLoaderIndex + newAssetsSize,
                    originalAssetsCount - firstLoaderIndex);
        }
        return newAssetsArray;
    }

    private static @NonNull ArrayList<ApkAssets> loadAssets(@NonNull ArrayList<ApkKey> keys) {
        final int pathsSize = keys.size();
        final var loadedAssets = new ArrayList<ApkAssets>(pathsSize);
        final var resourcesManager = ResourcesManager.getInstance();
        for (int i = 0; i < pathsSize; i++) {
            final var key = keys.get(i);
            final ApkAssets assets;
            try {
                // ResourcesManager has a cache of loaded assets, ensuring we don't open the same
                // file repeatedly, which is useful for the common overlays and registered
                // shared libraries.
                loadedAssets.add(resourcesManager.loadApkAssets(key));
                if (overlay) {
                    // TODO(b/70343104): This hardcoded path will be removed once
                    // addAssetPathInternal is deleted.
                    final String idmapPath = "/data/resource-cache/"
                            + path.substring(1).replace('/', '@')
                            + "@idmap";
                    assets = ApkAssets.loadOverlayFromPath(idmapPath, 0 /* flags */);
                } else {
                    assets = ApkAssets.loadFromPath(path,
                            appAsLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
                }
            } catch (IOException e) {
                Log.w(TAG, "Failed to load asset, key = " + key, e);
                return 0;
            }

            mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
+17 −7
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.annotation.StyleRes;
import android.annotation.StyleableRes;
import android.app.LocaleConfig;
import android.app.ResourcesManager;
import android.app.ResourcesManager.SharedLibraryAssets;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo.Config;
@@ -47,6 +48,7 @@ import android.os.Build;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -145,9 +147,10 @@ public class ResourcesImpl {
    // Cyclical cache used for recently-accessed XML files.
    private int mLastCachedXmlBlockIndex = -1;

    // The hash that allows to detect when the shared libraries applied to this object have changed,
    // and it is outdated and needs to be replaced.
    private final int mAppliedSharedLibsHash;
    // The number of shared libraries registered within this ResourcesImpl, which is designed to
    // help to determine whether this ResourcesImpl is outdated on shared library information and
    // needs to be replaced.
    private int mSharedLibCount;
    private final int[] mCachedXmlBlockCookies = new int[XML_BLOCK_CACHE_SIZE];
    private final String[] mCachedXmlBlockFiles = new String[XML_BLOCK_CACHE_SIZE];
    private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[XML_BLOCK_CACHE_SIZE];
@@ -201,8 +204,15 @@ public class ResourcesImpl {
    public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
            @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
        mAssets = assets;
        mAppliedSharedLibsHash =
                ResourcesManager.getInstance().updateResourceImplWithRegisteredLibs(this);
        if (Flags.registerResourcePaths()) {
            ArrayMap<String, SharedLibraryAssets> sharedLibMap =
                    ResourcesManager.getInstance().getSharedLibAssetsMap();
            final int size = sharedLibMap.size();
            for (int i = 0; i < size; i++) {
                assets.addSharedLibraryPaths(sharedLibMap.valueAt(i).getAllAssetPaths());
            }
            mSharedLibCount = sharedLibMap.size();
        }
        mMetrics.setToDefaults();
        mDisplayAdjustments = displayAdjustments;
        mConfiguration.setToDefaults();
@@ -1605,7 +1615,7 @@ public class ResourcesImpl {
        }
    }

    public int getAppliedSharedLibsHash() {
        return mAppliedSharedLibsHash;
    public int getSharedLibCount() {
        return mSharedLibCount;
    }
}
+12 −4

File changed.

Preview size limit exceeded, changes collapsed.