Rewrite of CursorWindow internals.
The original CursorWindow implementation was created in Android 1.0
and has remained relatively unchanged since then. Unfortunately that
design results in very poor performance on large windows, since
reading or writing each FieldSlot is O(row/100) to traverse through
a chain of RowSlotChunks. It's also memory-inefficient due to how
it allocates RowSlotChunks in 404 byte chunks, even when there's only
a single row to store.
This change is a complete redesign of the CursorWindow internals to
use a "heap-and-stack" style approach, where a "heap" of strings
and blobs increment up from the bottom of the window while a "stack"
of FieldSlots increment down from the top of the window.
The included benchmarks show the following improvements, ensuring
no regressions for small windows, while offering very dramatic
improvements for larger windows:
Big cores Little cores
4x4 cursor no regression no regression
1024x4 cursor 2.2x faster 2.0x faster
16384x4 cursor 48.5x faster 24.4x faster
Detailed unit testing is also included to ensure that the rewrite
behaves correctly.
Bug: 169251528
Test: atest libandroidfw_tests
Test: atest CtsDatabaseTestCases
Test: atest FrameworksCoreTests:android.database
Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest libandroidfw_benchmarks
Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest CorePerfTests:android.database.CrossProcessCursorPerfTest
Change-Id: I90dff31fd550130dae917a33e0e1fa684e15c107
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index 8ab7da5..45ad94f 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -155,11 +155,12 @@
android: {
srcs: [
"tests/BackupData_test.cpp",
- "tests/BackupHelpers_test.cpp",
+ "tests/BackupHelpers_test.cpp",
+ "tests/CursorWindow_test.cpp",
"tests/ObbFile_test.cpp",
"tests/PosixUtils_test.cpp",
],
- shared_libs: common_test_libs + ["libui"],
+ shared_libs: common_test_libs + ["libbinder", "liblog", "libui"],
},
host: {
static_libs: common_test_libs + ["liblog", "libz"],
@@ -185,6 +186,7 @@
// Actual benchmarks.
"tests/AssetManager2_bench.cpp",
"tests/AttributeResolution_bench.cpp",
+ "tests/CursorWindow_bench.cpp",
"tests/SparseEntry_bench.cpp",
"tests/Theme_bench.cpp",
],
diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp
index 71c8e1f..1e6de67 100644
--- a/libs/androidfw/CursorWindow.cpp
+++ b/libs/androidfw/CursorWindow.cpp
@@ -14,19 +14,14 @@
* limitations under the License.
*/
-#undef LOG_TAG
#define LOG_TAG "CursorWindow"
#include <androidfw/CursorWindow.h>
-#include <binder/Parcel.h>
-#include <utils/Log.h>
-#include <cutils/ashmem.h>
#include <sys/mman.h>
-#include <assert.h>
-#include <string.h>
-#include <stdlib.h>
+#include "android-base/logging.h"
+#include "cutils/ashmem.h"
namespace android {
@@ -36,11 +31,10 @@
*/
static constexpr const size_t kInlineSize = 16384;
-CursorWindow::CursorWindow(const String8& name, int ashmemFd, void* data, size_t size,
- size_t inflatedSize, bool readOnly) :
- mName(name), mAshmemFd(ashmemFd), mData(data), mSize(size),
- mInflatedSize(inflatedSize), mReadOnly(readOnly) {
- mHeader = static_cast<Header*>(mData);
+static constexpr const size_t kSlotShift = 4;
+static constexpr const size_t kSlotSizeBytes = 1 << kSlotShift;
+
+CursorWindow::CursorWindow() {
}
CursorWindow::~CursorWindow() {
@@ -52,234 +46,242 @@
}
}
-status_t CursorWindow::create(const String8& name, size_t inflatedSize,
- CursorWindow** outCursorWindow) {
- *outCursorWindow = nullptr;
+status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWindow **outWindow) {
+ *outWindow = nullptr;
- size_t size = std::min(kInlineSize, inflatedSize);
- void* data = calloc(size, 1);
- if (!data) return NO_MEMORY;
+ CursorWindow* window = new CursorWindow();
+ if (!window) goto fail;
- CursorWindow* window = new CursorWindow(name, -1, data, size,
- inflatedSize, false /*readOnly*/);
- status_t result = window->clear();
- if (!result) {
- LOG_WINDOW("Created new CursorWindow: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset,
- window->mHeader->numRows,
- window->mHeader->numColumns,
- window->mSize, window->mData);
- *outCursorWindow = window;
- return OK;
- }
+ window->mName = name;
+ window->mSize = std::min(kInlineSize, inflatedSize);
+ window->mInflatedSize = inflatedSize;
+ window->mData = malloc(window->mSize);
+ if (!window->mData) goto fail;
+ window->mReadOnly = false;
+
+ window->clear();
+ window->updateSlotsData();
+
+ LOG(DEBUG) << "Created: " << window->toString();
+ *outWindow = window;
+ return OK;
+
+fail:
+ LOG(ERROR) << "Failed create";
+fail_silent:
delete window;
- return result;
+ return UNKNOWN_ERROR;
}
-status_t CursorWindow::inflate() {
- // Shortcut when we can't expand any further
- if (mSize == mInflatedSize) return INVALID_OPERATION;
+status_t CursorWindow::maybeInflate() {
+ int ashmemFd = 0;
+ void* newData = nullptr;
+
+ // Bail early when we can't expand any further
+ if (mReadOnly || mSize == mInflatedSize) {
+ return INVALID_OPERATION;
+ }
String8 ashmemName("CursorWindow: ");
ashmemName.append(mName);
- status_t result;
- int ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize);
+ ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize);
if (ashmemFd < 0) {
- result = -errno;
- ALOGE("CursorWindow: ashmem_create_region() failed: errno=%d.", errno);
- } else {
- result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE);
- if (result < 0) {
- ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d",errno);
- } else {
- void* data = ::mmap(NULL, mInflatedSize, PROT_READ | PROT_WRITE,
- MAP_SHARED, ashmemFd, 0);
- if (data == MAP_FAILED) {
- result = -errno;
- ALOGE("CursorWindow: mmap() failed: errno=%d.", errno);
- } else {
- result = ashmem_set_prot_region(ashmemFd, PROT_READ);
- if (result < 0) {
- ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d.", errno);
- } else {
- // Move inline contents into new ashmem region
- memcpy(data, mData, mSize);
- free(mData);
- mAshmemFd = ashmemFd;
- mData = data;
- mHeader = static_cast<Header*>(mData);
- mSize = mInflatedSize;
- LOG_WINDOW("Inflated CursorWindow: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- mHeader->freeOffset,
- mHeader->numRows,
- mHeader->numColumns,
- mSize, mData);
- return OK;
- }
- }
- ::munmap(data, mInflatedSize);
- }
- ::close(ashmemFd);
+ PLOG(ERROR) << "Failed ashmem_create_region";
+ goto fail_silent;
}
- return result;
+
+ if (ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE) < 0) {
+ PLOG(ERROR) << "Failed ashmem_set_prot_region";
+ goto fail_silent;
+ }
+
+ newData = ::mmap(nullptr, mInflatedSize, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);
+ if (newData == MAP_FAILED) {
+ PLOG(ERROR) << "Failed mmap";
+ goto fail_silent;
+ }
+
+ if (ashmem_set_prot_region(ashmemFd, PROT_READ) < 0) {
+ PLOG(ERROR) << "Failed ashmem_set_prot_region";
+ goto fail_silent;
+ }
+
+ {
+ // Migrate existing contents into new ashmem region
+ uint32_t slotsSize = mSize - mSlotsOffset;
+ uint32_t newSlotsOffset = mInflatedSize - slotsSize;
+ memcpy(static_cast<uint8_t*>(newData),
+ static_cast<uint8_t*>(mData), mAllocOffset);
+ memcpy(static_cast<uint8_t*>(newData) + newSlotsOffset,
+ static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);
+
+ free(mData);
+ mAshmemFd = ashmemFd;
+ mData = newData;
+ mSize = mInflatedSize;
+ mSlotsOffset = newSlotsOffset;
+
+ updateSlotsData();
+ }
+
+ LOG(DEBUG) << "Inflated: " << this->toString();
+ return OK;
+
+fail:
+ LOG(ERROR) << "Failed maybeInflate";
+fail_silent:
+ ::munmap(newData, mInflatedSize);
+ ::close(ashmemFd);
+ return UNKNOWN_ERROR;
}
-status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outCursorWindow) {
- *outCursorWindow = nullptr;
+status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outWindow) {
+ *outWindow = nullptr;
- String8 name;
- status_t result = parcel->readString8(&name);
- if (result) return result;
+ CursorWindow* window = new CursorWindow();
+ if (!window) goto fail;
+
+ if (parcel->readString8(&window->mName)) goto fail;
+ if (parcel->readUint32(&window->mNumRows)) goto fail;
+ if (parcel->readUint32(&window->mNumColumns)) goto fail;
+ if (parcel->readUint32(&window->mSize)) goto fail;
+
+ if ((window->mNumRows * window->mNumColumns * kSlotSizeBytes) > window->mSize) {
+ LOG(ERROR) << "Unexpected size " << window->mSize << " for " << window->mNumRows
+ << " rows and " << window->mNumColumns << " columns";
+ goto fail_silent;
+ }
bool isAshmem;
- result = parcel->readBool(&isAshmem);
- if (result) return result;
-
+ if (parcel->readBool(&isAshmem)) goto fail;
if (isAshmem) {
- return createFromParcelAshmem(parcel, name, outCursorWindow);
- } else {
- return createFromParcelInline(parcel, name, outCursorWindow);
- }
-}
-
-status_t CursorWindow::createFromParcelAshmem(Parcel* parcel, String8& name,
- CursorWindow** outCursorWindow) {
- status_t result;
- int actualSize;
- int ashmemFd = parcel->readFileDescriptor();
- if (ashmemFd == int(BAD_TYPE)) {
- result = BAD_TYPE;
- ALOGE("CursorWindow: readFileDescriptor() failed");
- } else {
- ssize_t size = ashmem_get_size_region(ashmemFd);
- if (size < 0) {
- result = UNKNOWN_ERROR;
- ALOGE("CursorWindow: ashmem_get_size_region() failed: errno=%d.", errno);
- } else {
- int dupAshmemFd = ::fcntl(ashmemFd, F_DUPFD_CLOEXEC, 0);
- if (dupAshmemFd < 0) {
- result = -errno;
- ALOGE("CursorWindow: fcntl() failed: errno=%d.", errno);
- } else {
- // the size of the ashmem descriptor can be modified between ashmem_get_size_region
- // call and mmap, so we'll check again immediately after memory is mapped
- void* data = ::mmap(NULL, size, PROT_READ, MAP_SHARED, dupAshmemFd, 0);
- if (data == MAP_FAILED) {
- result = -errno;
- ALOGE("CursorWindow: mmap() failed: errno=%d.", errno);
- } else if ((actualSize = ashmem_get_size_region(dupAshmemFd)) != size) {
- ::munmap(data, size);
- result = BAD_VALUE;
- ALOGE("CursorWindow: ashmem_get_size_region() returned %d, expected %d"
- " errno=%d",
- actualSize, (int) size, errno);
- } else {
- CursorWindow* window = new CursorWindow(name, dupAshmemFd,
- data, size, size, true /*readOnly*/);
- LOG_WINDOW("Created CursorWindow from ashmem parcel: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset,
- window->mHeader->numRows,
- window->mHeader->numColumns,
- window->mSize, window->mData);
- *outCursorWindow = window;
- return OK;
- }
- ::close(dupAshmemFd);
- }
+ window->mAshmemFd = parcel->readFileDescriptor();
+ if (window->mAshmemFd < 0) {
+ LOG(ERROR) << "Failed readFileDescriptor";
+ goto fail_silent;
}
+
+ window->mAshmemFd = ::fcntl(window->mAshmemFd, F_DUPFD_CLOEXEC, 0);
+ if (window->mAshmemFd < 0) {
+ PLOG(ERROR) << "Failed F_DUPFD_CLOEXEC";
+ goto fail_silent;
+ }
+
+ window->mData = ::mmap(nullptr, window->mSize, PROT_READ, MAP_SHARED, window->mAshmemFd, 0);
+ if (window->mData == MAP_FAILED) {
+ PLOG(ERROR) << "Failed mmap";
+ goto fail_silent;
+ }
+ } else {
+ window->mAshmemFd = -1;
+
+ if (window->mSize > kInlineSize) {
+ LOG(ERROR) << "Unexpected size " << window->mSize << " for inline window";
+ goto fail_silent;
+ }
+
+ window->mData = malloc(window->mSize);
+ if (!window->mData) goto fail;
+
+ if (parcel->read(window->mData, window->mSize)) goto fail;
}
- *outCursorWindow = NULL;
- return result;
-}
-status_t CursorWindow::createFromParcelInline(Parcel* parcel, String8& name,
- CursorWindow** outCursorWindow) {
- uint32_t sentSize;
- status_t result = parcel->readUint32(&sentSize);
- if (result) return result;
- if (sentSize > kInlineSize) return NO_MEMORY;
+ // We just came from a remote source, so we're read-only
+ // and we can't inflate ourselves
+ window->mInflatedSize = window->mSize;
+ window->mReadOnly = true;
- void* data = calloc(sentSize, 1);
- if (!data) return NO_MEMORY;
+ window->updateSlotsData();
- result = parcel->read(data, sentSize);
- if (result) return result;
-
- CursorWindow* window = new CursorWindow(name, -1, data, sentSize,
- sentSize, true /*readOnly*/);
- LOG_WINDOW("Created CursorWindow from inline parcel: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset,
- window->mHeader->numRows,
- window->mHeader->numColumns,
- window->mSize, window->mData);
- *outCursorWindow = window;
+ LOG(DEBUG) << "Created from parcel: " << window->toString();
+ *outWindow = window;
return OK;
+
+fail:
+ LOG(ERROR) << "Failed createFromParcel";
+fail_silent:
+ delete window;
+ return UNKNOWN_ERROR;
}
status_t CursorWindow::writeToParcel(Parcel* parcel) {
- LOG_WINDOW("Writing CursorWindow: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- mHeader->freeOffset,
- mHeader->numRows,
- mHeader->numColumns,
- mSize, mData);
+ LOG(DEBUG) << "Writing to parcel: " << this->toString();
- status_t result = parcel->writeString8(mName);
- if (result) return result;
-
+ if (parcel->writeString8(mName)) goto fail;
+ if (parcel->writeUint32(mNumRows)) goto fail;
+ if (parcel->writeUint32(mNumColumns)) goto fail;
if (mAshmemFd != -1) {
- result = parcel->writeBool(true);
- if (result) return result;
- return writeToParcelAshmem(parcel);
+ if (parcel->writeUint32(mSize)) goto fail;
+ if (parcel->writeBool(true)) goto fail;
+ if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail;
} else {
- result = parcel->writeBool(false);
- if (result) return result;
- return writeToParcelInline(parcel);
+ // Since we know we're going to be read-only on the remote side,
+ // we can compact ourselves on the wire, with just enough padding
+ // to ensure our slots stay aligned
+ size_t slotsSize = mSize - mSlotsOffset;
+ size_t compactedSize = mAllocOffset + slotsSize;
+ compactedSize = (compactedSize + 3) & ~3;
+ if (parcel->writeUint32(compactedSize)) goto fail;
+ if (parcel->writeBool(false)) goto fail;
+ void* dest = parcel->writeInplace(compactedSize);
+ if (!dest) goto fail;
+ memcpy(static_cast<uint8_t*>(dest),
+ static_cast<uint8_t*>(mData), mAllocOffset);
+ memcpy(static_cast<uint8_t*>(dest) + compactedSize - slotsSize,
+ static_cast<uint8_t*>(mData) + mSlotsOffset, slotsSize);
}
-}
+ return OK;
-status_t CursorWindow::writeToParcelAshmem(Parcel* parcel) {
- return parcel->writeDupFileDescriptor(mAshmemFd);
-}
-
-status_t CursorWindow::writeToParcelInline(Parcel* parcel) {
- status_t result = parcel->writeUint32(mHeader->freeOffset);
- if (result) return result;
-
- return parcel->write(mData, mHeader->freeOffset);
+fail:
+ LOG(ERROR) << "Failed writeToParcel";
+fail_silent:
+ return UNKNOWN_ERROR;
}
status_t CursorWindow::clear() {
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk);
- mHeader->firstChunkOffset = sizeof(Header);
- mHeader->numRows = 0;
- mHeader->numColumns = 0;
-
- RowSlotChunk* firstChunk = static_cast<RowSlotChunk*>(offsetToPtr(mHeader->firstChunkOffset));
- firstChunk->nextChunkOffset = 0;
+ mAllocOffset = 0;
+ mSlotsOffset = mSize;
+ mNumRows = 0;
+ mNumColumns = 0;
return OK;
}
+void CursorWindow::updateSlotsData() {
+ mSlotsData = static_cast<uint8_t*>(mData) + mSize - kSlotSizeBytes;
+}
+
+void* CursorWindow::offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) {
+ if (offset > mSize) {
+ LOG(ERROR) << "Offset " << offset
+ << " out of bounds, max value " << mSize;
+ return nullptr;
+ }
+ if (offset + bufferSize > mSize) {
+ LOG(ERROR) << "End offset " << (offset + bufferSize)
+ << " out of bounds, max value " << mSize;
+ return nullptr;
+ }
+ return static_cast<uint8_t*>(mData) + offset;
+}
+
+uint32_t CursorWindow::offsetFromPtr(void* ptr) {
+ return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData);
+}
+
status_t CursorWindow::setNumColumns(uint32_t numColumns) {
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- uint32_t cur = mHeader->numColumns;
- if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) {
- ALOGE("Trying to go from %d columns to %d", cur, numColumns);
+ uint32_t cur = mNumColumns;
+ if ((cur > 0 || mNumRows > 0) && cur != numColumns) {
+ LOG(ERROR) << "Trying to go from " << cur << " columns to " << numColumns;
return INVALID_OPERATION;
}
- mHeader->numColumns = numColumns;
+ mNumColumns = numColumns;
return OK;
}
@@ -287,30 +289,18 @@
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- // Fill in the row slot
- RowSlot* rowSlot = allocRowSlot();
- if (rowSlot == NULL) {
- return NO_MEMORY;
+ size_t size = mNumColumns * kSlotSizeBytes;
+ off_t newOffset = mSlotsOffset - size;
+ if (newOffset < mAllocOffset) {
+ maybeInflate();
+ newOffset = mSlotsOffset - size;
+ if (newOffset < mAllocOffset) {
+ return NO_MEMORY;
+ }
}
- uint32_t rowSlotOffset = offsetFromPtr(rowSlot);
-
- // Allocate the slots for the field directory
- size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot);
- uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/);
- if (!fieldDirOffset) {
- mHeader->numRows--;
- LOG_WINDOW("The row failed, so back out the new row accounting "
- "from allocRowSlot %d", mHeader->numRows);
- return NO_MEMORY;
- }
- FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(fieldDirOffset));
- memset(fieldDir, 0, fieldDirSize);
-
- LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %zu bytes at offset %u\n",
- mHeader->numRows - 1, rowSlotOffset, fieldDirSize, fieldDirOffset);
- rowSlot = static_cast<RowSlot*>(offsetToPtr(rowSlotOffset));
- rowSlot->offset = fieldDirOffset;
+ memset(offsetToPtr(newOffset), 0, size);
+ mSlotsOffset = newOffset;
+ mNumRows++;
return OK;
}
@@ -318,90 +308,47 @@
if (mReadOnly) {
return INVALID_OPERATION;
}
-
- if (mHeader->numRows > 0) {
- mHeader->numRows--;
+ size_t size = mNumColumns * kSlotSizeBytes;
+ off_t newOffset = mSlotsOffset + size;
+ if (newOffset > mSize) {
+ return NO_MEMORY;
}
+ mSlotsOffset = newOffset;
+ mNumRows--;
return OK;
}
-uint32_t CursorWindow::alloc(size_t size, bool aligned) {
- uint32_t padding;
- if (aligned) {
- // 4 byte alignment
- padding = (~mHeader->freeOffset + 1) & 3;
- } else {
- padding = 0;
+status_t CursorWindow::alloc(size_t size, uint32_t* outOffset) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
}
-
- uint32_t offset = mHeader->freeOffset + padding;
- uint32_t nextFreeOffset = offset + size;
- if (nextFreeOffset > mSize) {
- // Try inflating to ashmem before finally giving up
- inflate();
- if (nextFreeOffset > mSize) {
- ALOGW("Window is full: requested allocation %zu bytes, "
- "free space %zu bytes, window size %zu bytes",
- size, freeSpace(), mSize);
- return 0;
+ size_t alignedSize = (size + 3) & ~3;
+ off_t newOffset = mAllocOffset + alignedSize;
+ if (newOffset > mSlotsOffset) {
+ maybeInflate();
+ newOffset = mAllocOffset + alignedSize;
+ if (newOffset > mSlotsOffset) {
+ return NO_MEMORY;
}
}
-
- mHeader->freeOffset = nextFreeOffset;
- return offset;
-}
-
-CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) {
- uint32_t chunkPos = row;
- RowSlotChunk* chunk = static_cast<RowSlotChunk*>(
- offsetToPtr(mHeader->firstChunkOffset));
- while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) {
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
- }
- return &chunk->slots[chunkPos];
-}
-
-CursorWindow::RowSlot* CursorWindow::allocRowSlot() {
- uint32_t chunkPos = mHeader->numRows;
- RowSlotChunk* chunk = static_cast<RowSlotChunk*>(
- offsetToPtr(mHeader->firstChunkOffset));
- while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) {
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
- }
- if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) {
- if (!chunk->nextChunkOffset) {
- uint32_t chunkOffset = offsetFromPtr(chunk);
- uint32_t newChunk = alloc(sizeof(RowSlotChunk), true /*aligned*/);
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunkOffset));
- chunk->nextChunkOffset = newChunk;
- if (!chunk->nextChunkOffset) {
- return NULL;
- }
- }
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunk->nextChunkOffset = 0;
- chunkPos = 0;
- }
- mHeader->numRows += 1;
- return &chunk->slots[chunkPos];
+ *outOffset = mAllocOffset;
+ mAllocOffset = newOffset;
+ return OK;
}
CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) {
- if (row >= mHeader->numRows || column >= mHeader->numColumns) {
- ALOGE("Failed to read row %d, column %d from a CursorWindow which "
- "has %d rows, %d columns.",
- row, column, mHeader->numRows, mHeader->numColumns);
- return NULL;
+ if (row >= mNumRows || column >= mNumColumns) {
+ LOG(ERROR) << "Failed to read row " << row << ", column " << column
+ << " from a window with " << mNumRows << " rows, " << mNumColumns << " columns";
+ return nullptr;
}
- RowSlot* rowSlot = getRowSlot(row);
- if (!rowSlot) {
- ALOGE("Failed to find rowSlot for row %d.", row);
- return NULL;
- }
- FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(rowSlot->offset));
- return &fieldDir[column];
+
+ // This is carefully tuned to use as few cycles as
+ // possible, since this is an extremely hot code path;
+ // see CursorWindow_bench.cpp for more details
+ void *result = static_cast<uint8_t*>(mSlotsData)
+ - (((row * mNumColumns) + column) << kSlotShift);
+ return static_cast<FieldSlot*>(result);
}
status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) {
@@ -423,16 +370,15 @@
if (!fieldSlot) {
return BAD_VALUE;
}
- uint32_t fieldSlotOffset = offsetFromPtr(fieldSlot);
- uint32_t offset = alloc(size);
- if (!offset) {
+ uint32_t offset;
+ if (alloc(size, &offset)) {
return NO_MEMORY;
}
memcpy(offsetToPtr(offset), value, size);
- fieldSlot = static_cast<FieldSlot*>(offsetToPtr(fieldSlotOffset));
+ fieldSlot = getFieldSlot(row, column);
fieldSlot->type = type;
fieldSlot->data.buffer.offset = offset;
fieldSlot->data.buffer.size = size;
diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h
index 0bee609..4c4f733 100644
--- a/libs/androidfw/include/androidfw/CursorWindow.h
+++ b/libs/androidfw/include/androidfw/CursorWindow.h
@@ -20,38 +20,36 @@
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
+#include <string>
-#include <binder/Parcel.h>
-#include <log/log.h>
-#include <utils/String8.h>
+#include "android-base/stringprintf.h"
+#include "binder/Parcel.h"
+#include "utils/String8.h"
-#if LOG_NDEBUG
-
-#define IF_LOG_WINDOW() if (false)
#define LOG_WINDOW(...)
-#else
-
-#define IF_LOG_WINDOW() IF_ALOG(LOG_DEBUG, "CursorWindow")
-#define LOG_WINDOW(...) ALOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__)
-
-#endif
-
namespace android {
/**
- * This class stores a set of rows from a database in a buffer. The begining of the
- * window has first chunk of RowSlots, which are offsets to the row directory, followed by
- * an offset to the next chunk in a linked-list of additional chunk of RowSlots in case
- * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a
- * FieldSlot per column, which has the size, offset, and type of the data for that field.
- * Note that the data types come from sqlite3.h.
+ * This class stores a set of rows from a database in a buffer. Internally
+ * data is structured as a "heap" of string/blob allocations at the bottom
+ * of the memory region, and a "stack" of FieldSlot allocations at the top
+ * of the memory region. Here's an example visual representation:
+ *
+ * +----------------------------------------------------------------+
+ * |heap\0of\0strings\0 222211110000| ...
+ * +-------------------+--------------------------------+-------+---+
+ * ^ ^ ^ ^ ^ ^
+ * | | | | | |
+ * | +- mAllocOffset mSlotsOffset -+ | | |
+ * +- mData mSlotsStart -+ | |
+ * mSize -+ |
+ * mInflatedSize -+
*
* Strings are stored in UTF-8.
*/
class CursorWindow {
- CursorWindow(const String8& name, int ashmemFd, void* data, size_t size,
- size_t inflatedSize, bool readOnly);
+ CursorWindow();
public:
/* Field types. */
@@ -88,9 +86,9 @@
inline String8 name() { return mName; }
inline size_t size() { return mSize; }
- inline size_t freeSpace() { return mSize - mHeader->freeOffset; }
- inline uint32_t getNumRows() { return mHeader->numRows; }
- inline uint32_t getNumColumns() { return mHeader->numColumns; }
+ inline size_t freeSpace() { return mSlotsOffset - mAllocOffset; }
+ inline uint32_t getNumRows() { return mNumRows; }
+ inline uint32_t getNumColumns() { return mNumColumns; }
status_t clear();
status_t setNumColumns(uint32_t numColumns);
@@ -138,75 +136,56 @@
return offsetToPtr(fieldSlot->data.buffer.offset, fieldSlot->data.buffer.size);
}
+ inline std::string toString() const {
+ return android::base::StringPrintf("CursorWindow{name=%s, fd=%d, size=%d, inflatedSize=%d, "
+ "allocOffset=%d, slotsOffset=%d, numRows=%d, numColumns=%d}", mName.c_str(),
+ mAshmemFd, mSize, mInflatedSize, mAllocOffset, mSlotsOffset, mNumRows, mNumColumns);
+ }
+
private:
- static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100;
-
- struct Header {
- // Offset of the lowest unused byte in the window.
- uint32_t freeOffset;
-
- // Offset of the first row slot chunk.
- uint32_t firstChunkOffset;
-
- uint32_t numRows;
- uint32_t numColumns;
- };
-
- struct RowSlot {
- uint32_t offset;
- };
-
- struct RowSlotChunk {
- RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS];
- uint32_t nextChunkOffset;
- };
-
String8 mName;
- int mAshmemFd;
- void* mData;
- size_t mSize;
- size_t mInflatedSize;
- bool mReadOnly;
- Header* mHeader;
+ int mAshmemFd = -1;
+ void* mData = nullptr;
+ /**
+ * Pointer to the first FieldSlot, used to optimize the extremely
+ * hot code path of getFieldSlot().
+ */
+ void* mSlotsData = nullptr;
+ uint32_t mSize = 0;
+ /**
+ * When a window starts as lightweight inline allocation, this value
+ * holds the "full" size to be created after ashmem inflation.
+ */
+ uint32_t mInflatedSize = 0;
+ /**
+ * Offset to the top of the "heap" of string/blob allocations. By
+ * storing these allocations at the bottom of our memory region we
+ * avoid having to rewrite offsets when inflating.
+ */
+ uint32_t mAllocOffset = 0;
+ /**
+ * Offset to the bottom of the "stack" of FieldSlot allocations.
+ */
+ uint32_t mSlotsOffset = 0;
+ uint32_t mNumRows = 0;
+ uint32_t mNumColumns = 0;
+ bool mReadOnly = false;
- inline void* offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) {
- if (offset > mSize) {
- ALOGE("Offset %" PRIu32 " out of bounds, max value %zu", offset, mSize);
- return NULL;
- }
- if (offset + bufferSize > mSize) {
- ALOGE("End offset %" PRIu32 " out of bounds, max value %zu",
- offset + bufferSize, mSize);
- return NULL;
- }
- return static_cast<uint8_t*>(mData) + offset;
- }
+ void updateSlotsData();
- inline uint32_t offsetFromPtr(void* ptr) {
- return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData);
- }
-
- static status_t createFromParcelAshmem(Parcel*, String8&, CursorWindow**);
- static status_t createFromParcelInline(Parcel*, String8&, CursorWindow**);
-
- status_t writeToParcelAshmem(Parcel*);
- status_t writeToParcelInline(Parcel*);
+ void* offsetToPtr(uint32_t offset, uint32_t bufferSize);
+ uint32_t offsetFromPtr(void* ptr);
/**
* By default windows are lightweight inline allocations; this method
* inflates the window into a larger ashmem region.
*/
- status_t inflate();
+ status_t maybeInflate();
/**
- * Allocate a portion of the window. Returns the offset
- * of the allocation, or 0 if there isn't enough space.
- * If aligned is true, the allocation gets 4 byte alignment.
+ * Allocate a portion of the window.
*/
- uint32_t alloc(size_t size, bool aligned = false);
-
- RowSlot* getRowSlot(uint32_t row);
- RowSlot* allocRowSlot();
+ status_t alloc(size_t size, uint32_t* outOffset);
status_t putBlobOrString(uint32_t row, uint32_t column,
const void* value, size_t size, int32_t type);
diff --git a/libs/androidfw/tests/CursorWindow_bench.cpp b/libs/androidfw/tests/CursorWindow_bench.cpp
new file mode 100644
index 0000000..f1191c3
--- /dev/null
+++ b/libs/androidfw/tests/CursorWindow_bench.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include "benchmark/benchmark.h"
+
+#include "androidfw/CursorWindow.h"
+
+namespace android {
+
+static void BM_CursorWindowWrite(benchmark::State& state, size_t rows, size_t cols) {
+ CursorWindow* w;
+ CursorWindow::create(String8("test"), 1 << 21, &w);
+
+ while (state.KeepRunning()) {
+ w->clear();
+ w->setNumColumns(cols);
+ for (int row = 0; row < rows; row++) {
+ w->allocRow();
+ for (int col = 0; col < cols; col++) {
+ w->putLong(row, col, 0xcafe);
+ }
+ }
+ }
+}
+
+static void BM_CursorWindowWrite4x4(benchmark::State& state) {
+ BM_CursorWindowWrite(state, 4, 4);
+}
+BENCHMARK(BM_CursorWindowWrite4x4);
+
+static void BM_CursorWindowWrite1Kx4(benchmark::State& state) {
+ BM_CursorWindowWrite(state, 1024, 4);
+}
+BENCHMARK(BM_CursorWindowWrite1Kx4);
+
+static void BM_CursorWindowWrite16Kx4(benchmark::State& state) {
+ BM_CursorWindowWrite(state, 16384, 4);
+}
+BENCHMARK(BM_CursorWindowWrite16Kx4);
+
+static void BM_CursorWindowRead(benchmark::State& state, size_t rows, size_t cols) {
+ CursorWindow* w;
+ CursorWindow::create(String8("test"), 1 << 21, &w);
+ w->setNumColumns(cols);
+ for (int row = 0; row < rows; row++) {
+ w->allocRow();
+ }
+
+ while (state.KeepRunning()) {
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < cols; col++) {
+ w->getFieldSlot(row, col);
+ }
+ }
+ }
+}
+
+static void BM_CursorWindowRead4x4(benchmark::State& state) {
+ BM_CursorWindowRead(state, 4, 4);
+}
+BENCHMARK(BM_CursorWindowRead4x4);
+
+static void BM_CursorWindowRead1Kx4(benchmark::State& state) {
+ BM_CursorWindowRead(state, 1024, 4);
+}
+BENCHMARK(BM_CursorWindowRead1Kx4);
+
+static void BM_CursorWindowRead16Kx4(benchmark::State& state) {
+ BM_CursorWindowRead(state, 16384, 4);
+}
+BENCHMARK(BM_CursorWindowRead16Kx4);
+
+} // namespace android
diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp
new file mode 100644
index 0000000..dfcf76e
--- /dev/null
+++ b/libs/androidfw/tests/CursorWindow_test.cpp
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include <utility>
+
+#include "androidfw/CursorWindow.h"
+
+#include "TestHelpers.h"
+
+#define CREATE_WINDOW_1K \
+ CursorWindow* w; \
+ CursorWindow::create(String8("test"), 1 << 10, &w);
+
+#define CREATE_WINDOW_1K_3X3 \
+ CursorWindow* w; \
+ CursorWindow::create(String8("test"), 1 << 10, &w); \
+ ASSERT_EQ(w->setNumColumns(3), OK); \
+ ASSERT_EQ(w->allocRow(), OK); \
+ ASSERT_EQ(w->allocRow(), OK); \
+ ASSERT_EQ(w->allocRow(), OK);
+
+#define CREATE_WINDOW_2M \
+ CursorWindow* w; \
+ CursorWindow::create(String8("test"), 1 << 21, &w);
+
+static constexpr const size_t kHalfInlineSize = 8192;
+static constexpr const size_t kGiantSize = 1048576;
+
+namespace android {
+
+TEST(CursorWindowTest, Empty) {
+ CREATE_WINDOW_1K;
+
+ ASSERT_EQ(w->getNumRows(), 0);
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->size(), 1 << 10);
+ ASSERT_EQ(w->freeSpace(), 1 << 10);
+}
+
+TEST(CursorWindowTest, SetNumColumns) {
+ CREATE_WINDOW_1K;
+
+ // Once we've locked in columns, we can't adjust
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_NE(w->setNumColumns(5), OK);
+ ASSERT_NE(w->setNumColumns(3), OK);
+ ASSERT_EQ(w->getNumColumns(), 4);
+}
+
+TEST(CursorWindowTest, SetNumColumnsAfterRow) {
+ CREATE_WINDOW_1K;
+
+ // Once we've locked in a row, we can't adjust columns
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_NE(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->getNumColumns(), 0);
+}
+
+TEST(CursorWindowTest, AllocRow) {
+ CREATE_WINDOW_1K;
+
+ ASSERT_EQ(w->setNumColumns(4), OK);
+
+ // Rolling forward means we have less free space
+ ASSERT_EQ(w->getNumRows(), 0);
+ auto before = w->freeSpace();
+ ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_LT(w->freeSpace(), before);
+ ASSERT_EQ(w->getNumRows(), 1);
+
+ // Verify we can unwind
+ ASSERT_EQ(w->freeLastRow(), OK);
+ ASSERT_EQ(w->freeSpace(), before);
+ ASSERT_EQ(w->getNumRows(), 0);
+
+ // Can't unwind when no rows left
+ ASSERT_NE(w->freeLastRow(), OK);
+}
+
+TEST(CursorWindowTest, AllocRowBounds) {
+ CREATE_WINDOW_1K;
+
+ // 60 columns is 960 bytes, which means only a single row can fit
+ ASSERT_EQ(w->setNumColumns(60), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+ ASSERT_NE(w->allocRow(), OK);
+}
+
+TEST(CursorWindowTest, StoreNull) {
+ CREATE_WINDOW_1K_3X3;
+
+ ASSERT_EQ(w->putNull(1, 1), OK);
+ ASSERT_EQ(w->putNull(0, 0), OK);
+
+ {
+ auto field = w->getFieldSlot(1, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL);
+ }
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL);
+ }
+}
+
+TEST(CursorWindowTest, StoreLong) {
+ CREATE_WINDOW_1K_3X3;
+
+ ASSERT_EQ(w->putLong(1, 1, 0xf00d), OK);
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ {
+ auto field = w->getFieldSlot(1, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xf00d);
+ }
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+}
+
+TEST(CursorWindowTest, StoreString) {
+ CREATE_WINDOW_1K_3X3;
+
+ ASSERT_EQ(w->putString(1, 1, "food", 5), OK);
+ ASSERT_EQ(w->putString(0, 0, "cafe", 5), OK);
+
+ size_t size;
+ {
+ auto field = w->getFieldSlot(1, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING);
+ auto actual = w->getFieldSlotValueString(field, &size);
+ ASSERT_EQ(std::string(actual), "food");
+ }
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING);
+ auto actual = w->getFieldSlotValueString(field, &size);
+ ASSERT_EQ(std::string(actual), "cafe");
+ }
+}
+
+TEST(CursorWindowTest, StoreBounds) {
+ CREATE_WINDOW_1K_3X3;
+
+ // Can't work with values beyond bounds
+ ASSERT_NE(w->putLong(0, 3, 0xcafe), OK);
+ ASSERT_NE(w->putLong(3, 0, 0xcafe), OK);
+ ASSERT_NE(w->putLong(3, 3, 0xcafe), OK);
+ ASSERT_EQ(w->getFieldSlot(0, 3), nullptr);
+ ASSERT_EQ(w->getFieldSlot(3, 0), nullptr);
+ ASSERT_EQ(w->getFieldSlot(3, 3), nullptr);
+}
+
+TEST(CursorWindowTest, Inflate) {
+ CREATE_WINDOW_2M;
+
+ auto before = w->size();
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+
+ // Scratch buffer that will fit before inflation
+ void* buf = malloc(kHalfInlineSize);
+
+ // Store simple value
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ // Store first object that fits inside
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK);
+ ASSERT_EQ(w->size(), before);
+
+ // Store second simple value
+ ASSERT_EQ(w->putLong(0, 2, 0xface), OK);
+
+ // Store second object that requires inflation
+ memset(buf, 84, kHalfInlineSize);
+ ASSERT_EQ(w->putBlob(0, 3, buf, kHalfInlineSize), OK);
+ ASSERT_GT(w->size(), before);
+
+ // Verify data is intact
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+ {
+ auto field = w->getFieldSlot(0, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kHalfInlineSize);
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_NE(actual, buf);
+ ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
+ }
+ {
+ auto field = w->getFieldSlot(0, 2);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xface);
+ }
+ {
+ auto field = w->getFieldSlot(0, 3);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kHalfInlineSize);
+ memset(buf, 84, kHalfInlineSize);
+ ASSERT_NE(actual, buf);
+ ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
+ }
+}
+
+TEST(CursorWindowTest, ParcelEmpty) {
+ CREATE_WINDOW_2M;
+
+ Parcel p;
+ w->writeToParcel(&p);
+ p.setDataPosition(0);
+ w = nullptr;
+
+ ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK);
+ ASSERT_EQ(w->getNumRows(), 0);
+ ASSERT_EQ(w->getNumColumns(), 0);
+ ASSERT_EQ(w->size(), 0);
+ ASSERT_EQ(w->freeSpace(), 0);
+
+ // We can't mutate the window after parceling
+ ASSERT_NE(w->setNumColumns(4), OK);
+ ASSERT_NE(w->allocRow(), OK);
+}
+
+TEST(CursorWindowTest, ParcelSmall) {
+ CREATE_WINDOW_2M;
+
+ auto before = w->size();
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+
+ // Scratch buffer that will fit before inflation
+ void* buf = malloc(kHalfInlineSize);
+
+ // Store simple value
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ // Store first object that fits inside
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK);
+ ASSERT_EQ(w->size(), before);
+
+ // Store second object with zero length
+ ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK);
+ ASSERT_EQ(w->size(), before);
+
+ // Force through a parcel
+ Parcel p;
+ w->writeToParcel(&p);
+ p.setDataPosition(0);
+ w = nullptr;
+
+ ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK);
+ ASSERT_EQ(w->getNumRows(), 1);
+ ASSERT_EQ(w->getNumColumns(), 4);
+
+ // Verify data is intact
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+ {
+ auto field = w->getFieldSlot(0, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kHalfInlineSize);
+ memset(buf, 42, kHalfInlineSize);
+ ASSERT_NE(actual, buf);
+ ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0);
+ }
+ {
+ auto field = w->getFieldSlot(0, 2);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, 0);
+ ASSERT_NE(actual, nullptr);
+ }
+}
+
+TEST(CursorWindowTest, ParcelLarge) {
+ CREATE_WINDOW_2M;
+
+ ASSERT_EQ(w->setNumColumns(4), OK);
+ ASSERT_EQ(w->allocRow(), OK);
+
+ // Store simple value
+ ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
+
+ // Store object that forces inflation
+ void* buf = malloc(kGiantSize);
+ memset(buf, 42, kGiantSize);
+ ASSERT_EQ(w->putBlob(0, 1, buf, kGiantSize), OK);
+
+ // Store second object with zero length
+ ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK);
+
+ // Force through a parcel
+ Parcel p;
+ w->writeToParcel(&p);
+ p.setDataPosition(0);
+ w = nullptr;
+
+ ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK);
+ ASSERT_EQ(w->getNumRows(), 1);
+ ASSERT_EQ(w->getNumColumns(), 4);
+
+ // Verify data is intact
+ {
+ auto field = w->getFieldSlot(0, 0);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER);
+ ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe);
+ }
+ {
+ auto field = w->getFieldSlot(0, 1);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, kGiantSize);
+ memset(buf, 42, kGiantSize);
+ ASSERT_EQ(memcmp(buf, actual, kGiantSize), 0);
+ }
+ {
+ auto field = w->getFieldSlot(0, 2);
+ ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB);
+ size_t actualSize;
+ auto actual = w->getFieldSlotValueBlob(field, &actualSize);
+ ASSERT_EQ(actualSize, 0);
+ ASSERT_NE(actual, nullptr);
+ }
+}
+
+} // android