diff options
| -rw-r--r-- | core/java/android/database/BulkCursorNative.java | 4 | ||||
| -rw-r--r-- | core/java/android/database/CursorToBulkCursorAdaptor.java | 11 | ||||
| -rw-r--r-- | core/java/android/database/CursorWindow.java | 18 | ||||
| -rw-r--r-- | core/java/android/database/DatabaseUtils.java | 26 | ||||
| -rw-r--r-- | core/java/android/database/IBulkCursor.java | 12 | ||||
| -rw-r--r-- | core/java/android/database/sqlite/SQLiteCursor.java | 26 | ||||
| -rw-r--r-- | core/java/android/database/sqlite/SQLiteQuery.java | 36 | ||||
| -rw-r--r-- | core/jni/android_database_CursorWindow.cpp | 7 | ||||
| -rw-r--r-- | core/jni/android_database_SQLiteQuery.cpp | 223 | ||||
| -rw-r--r-- | libs/binder/CursorWindow.cpp | 2 |
10 files changed, 240 insertions, 125 deletions
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java index 20a9c67198b2..67cf0f82700a 100644 --- a/core/java/android/database/BulkCursorNative.java +++ b/core/java/android/database/BulkCursorNative.java @@ -180,13 +180,13 @@ final class BulkCursorProxy implements IBulkCursor { return mRemote; } - public CursorWindow getWindow(int startPos) throws RemoteException + public CursorWindow getWindow(int position) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken(IBulkCursor.descriptor); - data.writeInt(startPos); + data.writeInt(position); mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 215035de3076..aa0f61e377e4 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -132,11 +132,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative } @Override - public CursorWindow getWindow(int startPos) { + public CursorWindow getWindow(int position) { synchronized (mLock) { throwIfCursorIsClosed(); - if (!mCursor.moveToPosition(startPos)) { + if (!mCursor.moveToPosition(position)) { closeFilledWindowLocked(); return null; } @@ -149,12 +149,11 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative if (window == null) { mFilledWindow = new CursorWindow(mProviderName); window = mFilledWindow; - mCursor.fillWindow(startPos, window); - } else if (startPos < window.getStartPosition() - || startPos >= window.getStartPosition() + window.getNumRows()) { + } else if (position < window.getStartPosition() + || position >= window.getStartPosition() + window.getNumRows()) { window.clear(); - mCursor.fillWindow(startPos, window); } + mCursor.fillWindow(position, window); } // Acquire a reference before returning from this RPC. diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 31e6f02c371e..e9675e8a1d22 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -55,6 +55,7 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { public int mWindowPtr; private int mStartPos; + private final String mName; private final CloseGuard mCloseGuard = CloseGuard.get(); @@ -84,6 +85,8 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { private static native boolean nativePutDouble(int windowPtr, double value, int row, int column); private static native boolean nativePutNull(int windowPtr, int row, int column); + private static native String nativeGetName(int windowPtr); + /** * Creates a new empty cursor window and gives it a name. * <p> @@ -95,6 +98,7 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { */ public CursorWindow(String name) { mStartPos = 0; + mName = name; mWindowPtr = nativeCreate(name, sCursorWindowSize); if (mWindowPtr == 0) { throw new CursorWindowAllocationException("Cursor window allocation of " + @@ -129,6 +133,7 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { throw new CursorWindowAllocationException("Cursor window could not be " + "created from binder."); } + mName = nativeGetName(mWindowPtr); mCloseGuard.open("close"); } @@ -156,6 +161,14 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { } /** + * Gets the name of this cursor window. + * @hide + */ + public String getName() { + return mName; + } + + /** * Closes the cursor window and frees its underlying resources when all other * remaining references have been released. */ @@ -758,4 +771,9 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { String s = (buff.length() > 980) ? buff.substring(0, 980) : buff.toString(); return "# Open Cursors=" + total + s; } + + @Override + public String toString() { + return getName() + " {" + Integer.toHexString(mWindowPtr) + "}"; + } } diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index a10ca1502a77..a8ba9a36ad2e 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -726,6 +726,32 @@ public class DatabaseUtils { } /** + * Picks a start position for {@link Cursor#fillWindow} such that the + * window will contain the requested row and a useful range of rows + * around it. + * + * When the data set is too large to fit in a cursor window, seeking the + * cursor can become a very expensive operation since we have to run the + * query again when we move outside the bounds of the current window. + * + * We try to choose a start position for the cursor window such that + * 1/3 of the window's capacity is used to hold rows before the requested + * position and 2/3 of the window's capacity is used to hold rows after the + * requested position. + * + * @param cursorPosition The row index of the row we want to get. + * @param cursorWindowCapacity The estimated number of rows that can fit in + * a cursor window, or 0 if unknown. + * @return The recommended start position, always less than or equal to + * the requested row. + * @hide + */ + public static int cursorPickFillWindowStartPosition( + int cursorPosition, int cursorWindowCapacity) { + return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); + } + + /** * Query the table for the number of rows in the table. * @param db the database the table is in * @param table the name of the table to query diff --git a/core/java/android/database/IBulkCursor.java b/core/java/android/database/IBulkCursor.java index 7c967970a34f..0f4500a7aec8 100644 --- a/core/java/android/database/IBulkCursor.java +++ b/core/java/android/database/IBulkCursor.java @@ -30,11 +30,17 @@ import android.os.RemoteException; */ public interface IBulkCursor extends IInterface { /** - * Returns a BulkCursorWindow, which either has a reference to a shared - * memory segment with the rows, or an array of JSON strings. + * Gets a cursor window that contains the specified position. + * The window will contain a range of rows around the specified position. */ - public CursorWindow getWindow(int startPos) throws RemoteException; + public CursorWindow getWindow(int position) throws RemoteException; + /** + * Notifies the cursor that the position has changed. + * Only called when {@link #getWantsAllOnMoveCalls()} returns true. + * + * @param position The new position + */ public void onMove(int position) throws RemoteException; /** diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index c24acd456908..8dcedf2fdd05 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -18,6 +18,7 @@ package android.database.sqlite; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; +import android.database.DatabaseUtils; import android.os.StrictMode; import android.util.Log; @@ -48,7 +49,10 @@ public class SQLiteCursor extends AbstractWindowedCursor { private final SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ - private volatile int mCount = NO_COUNT; + private int mCount = NO_COUNT; + + /** The number of rows that can fit in the cursor window, 0 if unknown */ + private int mCursorWindowCapacity; /** A mapping of column names to column indices, to speed up lookups */ private Map<String, Integer> mColumnNameMap; @@ -158,18 +162,20 @@ public class SQLiteCursor extends AbstractWindowedCursor { return mCount; } - private void fillWindow(int startPos) { + private void fillWindow(int requiredPos) { clearOrCreateWindow(getDatabase().getPath()); - mWindow.setStartPosition(startPos); - int count = getQuery().fillWindow(mWindow); - if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 + + if (mCount == NO_COUNT) { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); + mCount = getQuery().fillWindow(mWindow, startPos, requiredPos, true); + mCursorWindowCapacity = mWindow.getNumRows(); if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "received count(*) from native_fill_window: " + count); + Log.d(TAG, "received count(*) from native_fill_window: " + mCount); } - mCount = count; - } else if (mCount <= 0) { - throw new IllegalStateException("Row count should never be zero or negative " - + "when the start position is non-zero"); + } else { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, + mCursorWindowCapacity); + getQuery().fillWindow(mWindow, startPos, requiredPos, false); } } diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 7db0914140b2..5229f1286149 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -30,8 +30,10 @@ import android.util.Log; public class SQLiteQuery extends SQLiteProgram { private static final String TAG = "SQLiteQuery"; - private static native int nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, - int startPos, int offsetParam); + private static final boolean DEBUG_FILL_WINDOW_PERFORMANCE = false; + + private static native long nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, + int offsetParam, int startPos, int requiredPos, boolean countAllRows); private static native int nativeColumnCount(int statementPtr); private static native String nativeColumnName(int statementPtr, int columnIndex); @@ -71,19 +73,39 @@ public class SQLiteQuery extends SQLiteProgram { * Reads rows into a buffer. This method acquires the database lock. * * @param window The window to fill into - * @return number of total rows in the query + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled. + * @param countAllRows True to count all rows that the query would + * return regardless of whether they fit in the window. + * @return Number of rows that were enumerated. Might not be all rows + * unless countAllRows is true. */ - /* package */ int fillWindow(CursorWindow window) { + /* package */ int fillWindow(CursorWindow window, + int startPos, int requiredPos, boolean countAllRows) { mDatabase.lock(mSql); long timeStart = SystemClock.uptimeMillis(); try { acquireReference(); try { window.acquireReference(); - int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, - window.getStartPosition(), mOffsetIndex); + long result = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, + mOffsetIndex, startPos, requiredPos, countAllRows); + int actualPos = (int)(result >> 32); + int countedRows = (int)result; + window.setStartPosition(actualPos); + if (DEBUG_FILL_WINDOW_PERFORMANCE) { + Log.d(TAG, "fillWindow: window=\"" + window + + "\", startPos=" + startPos + ", requiredPos=" + requiredPos + + ", countAllRows=" + countAllRows + + ", offset=" + mOffsetIndex + + ", actualPos=" + actualPos + ", filledRows=" + window.getNumRows() + + ", countedRows=" + countedRows + + ", took " + (SystemClock.uptimeMillis() - timeStart) + + " ms, query=\"" + mSql + "\""); + } mDatabase.logTimeStat(mSql, timeStart); - return numRows; + return countedRows; } catch (IllegalStateException e){ // simply ignore it return 0; diff --git a/core/jni/android_database_CursorWindow.cpp b/core/jni/android_database_CursorWindow.cpp index 4a9fcf22c71a..9a87a10d83f5 100644 --- a/core/jni/android_database_CursorWindow.cpp +++ b/core/jni/android_database_CursorWindow.cpp @@ -103,6 +103,11 @@ static void nativeDispose(JNIEnv* env, jclass clazz, jint windowPtr) { } } +static jstring nativeGetName(JNIEnv* env, jclass clazz, jint windowPtr) { + CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr); + return env->NewStringUTF(window->name().string()); +} + static void nativeWriteToParcel(JNIEnv * env, jclass clazz, jint windowPtr, jobject parcelObj) { CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr); @@ -484,6 +489,8 @@ static JNINativeMethod sMethods[] = (void*)nativeDispose }, { "nativeWriteToParcel", "(ILandroid/os/Parcel;)V", (void*)nativeWriteToParcel }, + { "nativeGetName", "(I)Ljava/lang/String;", + (void*)nativeGetName }, { "nativeClear", "(I)V", (void*)nativeClear }, { "nativeGetNumRows", "(I)I", diff --git a/core/jni/android_database_SQLiteQuery.cpp b/core/jni/android_database_SQLiteQuery.cpp index 8170f46ecb29..ea4200a59231 100644 --- a/core/jni/android_database_SQLiteQuery.cpp +++ b/core/jni/android_database_SQLiteQuery.cpp @@ -35,8 +35,110 @@ namespace android { -static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, - jint statementPtr, jint windowPtr, jint startPos, jint offsetParam) { +enum CopyRowResult { + CPR_OK, + CPR_FULL, + CPR_ERROR, +}; + +static CopyRowResult copyRow(JNIEnv* env, CursorWindow* window, + sqlite3_stmt* statement, int numColumns, int startPos, int addedRows) { + // Allocate a new field directory for the row. This pointer is not reused + // since it may be possible for it to be relocated on a call to alloc() when + // the field data is being allocated. + status_t status = window->allocRow(); + if (status) { + LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d, error=%d", + startPos, addedRows, status); + return CPR_FULL; + } + + // Pack the row into the window. + CopyRowResult result = CPR_OK; + for (int i = 0; i < numColumns; i++) { + int type = sqlite3_column_type(statement, i); + if (type == SQLITE_TEXT) { + // TEXT data + const char* text = reinterpret_cast<const char*>( + sqlite3_column_text(statement, i)); + // SQLite does not include the NULL terminator in size, but does + // ensure all strings are NULL terminated, so increase size by + // one to make sure we store the terminator. + size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1; + status = window->putString(addedRows, i, text, sizeIncludingNull); + if (status) { + LOG_WINDOW("Failed allocating %u bytes for text at %d,%d, error=%d", + sizeIncludingNull, startPos + addedRows, i, status); + result = CPR_FULL; + break; + } + LOG_WINDOW("%d,%d is TEXT with %u bytes", + startPos + addedRows, i, sizeIncludingNull); + } else if (type == SQLITE_INTEGER) { + // INTEGER data + int64_t value = sqlite3_column_int64(statement, i); + status = window->putLong(addedRows, i, value); + if (status) { + LOG_WINDOW("Failed allocating space for a long in column %d, error=%d", + i, status); + result = CPR_FULL; + break; + } + LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + addedRows, i, value); + } else if (type == SQLITE_FLOAT) { + // FLOAT data + double value = sqlite3_column_double(statement, i); + status = window->putDouble(addedRows, i, value); + if (status) { + LOG_WINDOW("Failed allocating space for a double in column %d, error=%d", + i, status); + result = CPR_FULL; + break; + } + LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value); + } else if (type == SQLITE_BLOB) { + // BLOB data + const void* blob = sqlite3_column_blob(statement, i); + size_t size = sqlite3_column_bytes(statement, i); + status = window->putBlob(addedRows, i, blob, size); + if (status) { + LOG_WINDOW("Failed allocating %u bytes for blob at %d,%d, error=%d", + size, startPos + addedRows, i, status); + result = CPR_FULL; + break; + } + LOG_WINDOW("%d,%d is Blob with %u bytes", + startPos + addedRows, i, size); + } else if (type == SQLITE_NULL) { + // NULL field + status = window->putNull(addedRows, i); + if (status) { + LOG_WINDOW("Failed allocating space for a null in column %d, error=%d", + i, status); + result = CPR_FULL; + break; + } + + LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i); + } else { + // Unknown data + LOGE("Unknown column type when filling database window"); + throw_sqlite3_exception(env, "Unknown column type when filling window"); + result = CPR_ERROR; + break; + } + } + + // Free the last row if if was not successfully copied. + if (result != CPR_OK) { + window->freeLastRow(); + } + return result; +} + +static jlong nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, + jint statementPtr, jint windowPtr, jint offsetParam, + jint startPos, jint requiredPos, jboolean countAllRows) { sqlite3* database = reinterpret_cast<sqlite3*>(databasePtr); sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr); CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr); @@ -45,15 +147,17 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, // offsetParam will be set to 0, an invalid value. if (offsetParam > 0) { // Bind the offset parameter, telling the program which row to start with + // If an offset parameter is used, we cannot simply clear the window if it + // turns out that the requiredPos won't fit because the result set may + // depend on startPos, so we set startPos to requiredPos. + startPos = requiredPos; int err = sqlite3_bind_int(statement, offsetParam, startPos); if (err != SQLITE_OK) { LOGE("Unable to bind offset position, offsetParam = %d", offsetParam); throw_sqlite3_exception(env, database); return 0; } - LOG_WINDOW("Bound to startPos %d", startPos); - } else { - LOG_WINDOW("Not binding to startPos %d", startPos); + LOG_WINDOW("Bound offset position to startPos %d", startPos); } // We assume numRows is initially 0. @@ -73,7 +177,6 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, int addedRows = 0; bool windowFull = false; bool gotException = false; - const bool countAllRows = (startPos == 0); // when startPos is 0, we count all rows while (!gotException && (!windowFull || countAllRows)) { int err = sqlite3_step(statement); if (err == SQLITE_ROW) { @@ -86,97 +189,24 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, continue; } - // Allocate a new field directory for the row. This pointer is not reused - // since it may be possible for it to be relocated on a call to alloc() when - // the field data is being allocated. - status = window->allocRow(); - if (status) { - LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d, error=%d", - startPos, addedRows, status); - windowFull = true; - continue; - } - - // Pack the row into the window. - for (int i = 0; i < numColumns; i++) { - int type = sqlite3_column_type(statement, i); - if (type == SQLITE_TEXT) { - // TEXT data - const char* text = reinterpret_cast<const char*>( - sqlite3_column_text(statement, i)); - // SQLite does not include the NULL terminator in size, but does - // ensure all strings are NULL terminated, so increase size by - // one to make sure we store the terminator. - size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1; - status = window->putString(addedRows, i, text, sizeIncludingNull); - if (status) { - LOG_WINDOW("Failed allocating %u bytes for text at %d,%d, error=%d", - sizeIncludingNull, startPos + addedRows, i, status); - windowFull = true; - break; - } - LOG_WINDOW("%d,%d is TEXT with %u bytes", - startPos + addedRows, i, sizeIncludingNull); - } else if (type == SQLITE_INTEGER) { - // INTEGER data - int64_t value = sqlite3_column_int64(statement, i); - status = window->putLong(addedRows, i, value); - if (status) { - LOG_WINDOW("Failed allocating space for a long in column %d, error=%d", - i, status); - windowFull = true; - break; - } - LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + addedRows, i, value); - } else if (type == SQLITE_FLOAT) { - // FLOAT data - double value = sqlite3_column_double(statement, i); - status = window->putDouble(addedRows, i, value); - if (status) { - LOG_WINDOW("Failed allocating space for a double in column %d, error=%d", - i, status); - windowFull = true; - break; - } - LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value); - } else if (type == SQLITE_BLOB) { - // BLOB data - const void* blob = sqlite3_column_blob(statement, i); - size_t size = sqlite3_column_bytes(statement, i); - status = window->putBlob(addedRows, i, blob, size); - if (status) { - LOG_WINDOW("Failed allocating %u bytes for blob at %d,%d, error=%d", - size, startPos + addedRows, i, status); - windowFull = true; - break; - } - LOG_WINDOW("%d,%d is Blob with %u bytes", - startPos + addedRows, i, size); - } else if (type == SQLITE_NULL) { - // NULL field - status = window->putNull(addedRows, i); - if (status) { - LOG_WINDOW("Failed allocating space for a null in column %d, error=%d", - i, status); - windowFull = true; - break; - } - - LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i); - } else { - // Unknown data - LOGE("Unknown column type when filling database window"); - throw_sqlite3_exception(env, "Unknown column type when filling window"); - gotException = true; - break; - } + CopyRowResult cpr = copyRow(env, window, statement, numColumns, startPos, addedRows); + if (cpr == CPR_FULL && addedRows && startPos + addedRows < requiredPos) { + // We filled the window before we got to the one row that we really wanted. + // Clear the window and start filling it again from here. + // TODO: Would be nicer if we could progressively replace earlier rows. + window->clear(); + window->setNumColumns(numColumns); + startPos += addedRows; + addedRows = 0; + cpr = copyRow(env, window, statement, numColumns, startPos, addedRows); } - // Update the final row tally. - if (windowFull || gotException) { - window->freeLastRow(); - } else { + if (cpr == CPR_OK) { addedRows += 1; + } else if (cpr == CPR_FULL) { + windowFull = true; + } else { + gotException = true; } } else if (err == SQLITE_DONE) { // All rows processed, bail @@ -209,7 +239,8 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, if (startPos > totalRows) { LOGE("startPos %d > actual rows %d", startPos, totalRows); } - return countAllRows ? totalRows : 0; + jlong result = jlong(startPos) << 32 | jlong(totalRows); + return result; } static jint nativeColumnCount(JNIEnv* env, jclass clazz, jint statementPtr) { @@ -228,7 +259,7 @@ static jstring nativeColumnName(JNIEnv* env, jclass clazz, jint statementPtr, static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ - { "nativeFillWindow", "(IIIII)I", + { "nativeFillWindow", "(IIIIIIZ)J", (void*)nativeFillWindow }, { "nativeColumnCount", "(I)I", (void*)nativeColumnCount}, diff --git a/libs/binder/CursorWindow.cpp b/libs/binder/CursorWindow.cpp index bf8d7a6f8f8e..07333787cbb0 100644 --- a/libs/binder/CursorWindow.cpp +++ b/libs/binder/CursorWindow.cpp @@ -209,7 +209,7 @@ uint32_t CursorWindow::alloc(size_t size, bool aligned) { uint32_t offset = mHeader->freeOffset + padding; uint32_t nextFreeOffset = offset + size; if (nextFreeOffset > mSize) { - LOGE("Window is full: requested allocation %d bytes, " + LOGW("Window is full: requested allocation %d bytes, " "free space %d bytes, window size %d bytes", size, freeSpace(), mSize); return 0; |