From 6faf7893b6307a3295993380d61af49f2cda965c Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Mon, 25 Jan 2010 19:00:00 -0800 Subject: [PATCH] Simplify the MemoryDealer implementation At some point the implementation became complicated because of SurfaceFlinger's special needs, since we are now relying on gralloc we can go back to much simpler MemoryDealer. Removed HeapInterface and AllocatorInterface, since those don't need to be paramterized anymore. Merged SimpleMemory and Allocation. Made SimplisticAllocator non virtual. Removed MemoryDealer flags (READ_ONLY, PAGE_ALIGNED) Removed a lot of unneeded code. --- core/jni/CursorWindow.cpp | 2 +- include/binder/MemoryDealer.h | 215 +------------- include/binder/MemoryHeapPmem.h | 4 +- libs/audioflinger/AudioFlinger.cpp | 2 +- libs/binder/MemoryDealer.cpp | 315 ++++++++++++--------- libs/binder/MemoryHeapPmem.cpp | 2 +- media/libmedia/AudioRecord.cpp | 1 - media/libmedia/AudioTrack.cpp | 1 - .../MetadataRetrieverClient.cpp | 4 +- media/libstagefright/OMXCodec.cpp | 2 +- media/libstagefright/omx/tests/OMXHarness.cpp | 2 +- 11 files changed, 197 insertions(+), 353 deletions(-) diff --git a/core/jni/CursorWindow.cpp b/core/jni/CursorWindow.cpp index 78641899856d..694514e9a440 100644 --- a/core/jni/CursorWindow.cpp +++ b/core/jni/CursorWindow.cpp @@ -60,7 +60,7 @@ bool CursorWindow::initBuffer(bool localOnly) { //TODO Use a non-memory dealer mmap region for localOnly - mHeap = new MemoryDealer(new SharedHeap(mMaxSize, 0, "CursorWindow")); + mHeap = new MemoryDealer(mMaxSize, "CursorWindow"); if (mHeap != NULL) { mMemory = mHeap->allocate(mMaxSize); if (mMemory != NULL) { diff --git a/include/binder/MemoryDealer.h b/include/binder/MemoryDealer.h index 03ac70a6b325..170f20d67a02 100644 --- a/include/binder/MemoryDealer.h +++ b/include/binder/MemoryDealer.h @@ -22,232 +22,35 @@ #include #include -#include #include namespace android { // ---------------------------------------------------------------------------- -class String8; -/* - * interface for implementing a "heap". A heap basically provides - * the IMemoryHeap interface for cross-process sharing and the - * ability to map/unmap pages within the heap. - */ -class HeapInterface : public virtual BnMemoryHeap -{ -public: - // all values must be page-aligned - virtual sp mapMemory(size_t offset, size_t size) = 0; - - HeapInterface(); -protected: - virtual ~HeapInterface(); -}; - -// ---------------------------------------------------------------------------- - -/* - * interface for implementing an allocator. An allocator provides - * methods for allocating and freeing memory blocks and dumping - * its state. - */ -class AllocatorInterface : public RefBase -{ -public: - enum { - PAGE_ALIGNED = 0x00000001 - }; - - virtual size_t allocate(size_t size, uint32_t flags = 0) = 0; - virtual status_t deallocate(size_t offset) = 0; - virtual size_t size() const = 0; - virtual void dump(const char* what, uint32_t flags = 0) const = 0; - virtual void dump(String8& res, - const char* what, uint32_t flags = 0) const = 0; - - AllocatorInterface(); -protected: - virtual ~AllocatorInterface(); -}; - -// ---------------------------------------------------------------------------- - -/* - * concrete implementation of HeapInterface on top of mmap() - */ -class SharedHeap : public HeapInterface, public MemoryHeapBase -{ -public: - SharedHeap(); - SharedHeap(size_t size, uint32_t flags = 0, char const * name = NULL); - virtual ~SharedHeap(); - virtual sp mapMemory(size_t offset, size_t size); -}; - -// ---------------------------------------------------------------------------- - -/* - * A simple templatized doubly linked-list implementation - */ - -template -class LinkedList -{ - NODE* mFirst; - NODE* mLast; - -public: - LinkedList() : mFirst(0), mLast(0) { } - bool isEmpty() const { return mFirst == 0; } - NODE const* head() const { return mFirst; } - NODE* head() { return mFirst; } - NODE const* tail() const { return mLast; } - NODE* tail() { return mLast; } - - void insertAfter(NODE* node, NODE* newNode) { - newNode->prev = node; - newNode->next = node->next; - if (node->next == 0) mLast = newNode; - else node->next->prev = newNode; - node->next = newNode; - } - - void insertBefore(NODE* node, NODE* newNode) { - newNode->prev = node->prev; - newNode->next = node; - if (node->prev == 0) mFirst = newNode; - else node->prev->next = newNode; - node->prev = newNode; - } - - void insertHead(NODE* newNode) { - if (mFirst == 0) { - mFirst = mLast = newNode; - newNode->prev = newNode->next = 0; - } else { - newNode->prev = 0; - newNode->next = mFirst; - mFirst->prev = newNode; - mFirst = newNode; - } - } - - void insertTail(NODE* newNode) { - if (mLast == 0) { - insertHead(newNode); - } else { - newNode->prev = mLast; - newNode->next = 0; - mLast->next = newNode; - mLast = newNode; - } - } - - NODE* remove(NODE* node) { - if (node->prev == 0) mFirst = node->next; - else node->prev->next = node->next; - if (node->next == 0) mLast = node->prev; - else node->next->prev = node->prev; - return node; - } -}; - - -/* - * concrete implementation of AllocatorInterface using a simple - * best-fit allocation scheme - */ -class SimpleBestFitAllocator : public AllocatorInterface -{ -public: - - SimpleBestFitAllocator(size_t size); - virtual ~SimpleBestFitAllocator(); - - virtual size_t allocate(size_t size, uint32_t flags = 0); - virtual status_t deallocate(size_t offset); - virtual size_t size() const; - virtual void dump(const char* what, uint32_t flags = 0) const; - virtual void dump(String8& res, - const char* what, uint32_t flags = 0) const; - -private: - - struct chunk_t { - chunk_t(size_t start, size_t size) - : start(start), size(size), free(1), prev(0), next(0) { - } - size_t start; - size_t size : 28; - int free : 4; - mutable chunk_t* prev; - mutable chunk_t* next; - }; - - ssize_t alloc(size_t size, uint32_t flags); - chunk_t* dealloc(size_t start); - void dump_l(const char* what, uint32_t flags = 0) const; - void dump_l(String8& res, const char* what, uint32_t flags = 0) const; - - static const int kMemoryAlign; - mutable Mutex mLock; - LinkedList mList; - size_t mHeapSize; -}; +class SimpleBestFitAllocator; // ---------------------------------------------------------------------------- class MemoryDealer : public RefBase { public: + MemoryDealer(size_t size, const char* name = 0); - enum { - READ_ONLY = MemoryHeapBase::READ_ONLY, - PAGE_ALIGNED = AllocatorInterface::PAGE_ALIGNED - }; - - // creates a memory dealer with the SharedHeap and SimpleBestFitAllocator - MemoryDealer(size_t size, uint32_t flags = 0, const char* name = 0); - - // provide a custom heap but use the SimpleBestFitAllocator - MemoryDealer(const sp& heap); - - // provide both custom heap and allocotar - MemoryDealer( - const sp& heap, - const sp& allocator); - - virtual sp allocate(size_t size, uint32_t flags = 0); + virtual sp allocate(size_t size); virtual void deallocate(size_t offset); - virtual void dump(const char* what, uint32_t flags = 0) const; - + virtual void dump(const char* what) const; sp getMemoryHeap() const { return heap(); } - sp getAllocator() const { return allocator(); } protected: virtual ~MemoryDealer(); -private: - const sp& heap() const; - const sp& allocator() const; - - class Allocation : public BnMemory { - public: - Allocation(const sp& dealer, - ssize_t offset, size_t size, const sp& memory); - virtual ~Allocation(); - virtual sp getMemory(ssize_t* offset, size_t* size) const; - private: - sp mDealer; - ssize_t mOffset; - size_t mSize; - sp mMemory; - }; +private: + const sp& heap() const; + SimpleBestFitAllocator* allocator() const; - sp mHeap; - sp mAllocator; + sp mHeap; + SimpleBestFitAllocator* mAllocator; }; diff --git a/include/binder/MemoryHeapPmem.h b/include/binder/MemoryHeapPmem.h index dbf26ff1ffc6..aac164f72b1d 100644 --- a/include/binder/MemoryHeapPmem.h +++ b/include/binder/MemoryHeapPmem.h @@ -20,10 +20,10 @@ #include #include -#include #include #include #include +#include namespace android { @@ -31,7 +31,7 @@ class MemoryHeapBase; // --------------------------------------------------------------------------- -class MemoryHeapPmem : public HeapInterface, public MemoryHeapBase +class MemoryHeapPmem : public MemoryHeapBase { public: class MemoryPmem : public BnMemory { diff --git a/libs/audioflinger/AudioFlinger.cpp b/libs/audioflinger/AudioFlinger.cpp index cad420a9b153..9d52882da786 100644 --- a/libs/audioflinger/AudioFlinger.cpp +++ b/libs/audioflinger/AudioFlinger.cpp @@ -2929,7 +2929,7 @@ void AudioFlinger::PlaybackThread::OutputTrack::clearBufferQueue() AudioFlinger::Client::Client(const sp& audioFlinger, pid_t pid) : RefBase(), mAudioFlinger(audioFlinger), - mMemoryDealer(new MemoryDealer(1024*1024)), + mMemoryDealer(new MemoryDealer(1024*1024, "AudioFlinger::Client")), mPid(pid) { // 1 MB of address space is good for 32 tracks, 8 buffers each, 4 KB/buffer diff --git a/libs/binder/MemoryDealer.cpp b/libs/binder/MemoryDealer.cpp index d5ffe7f6d388..18669f76dde0 100644 --- a/libs/binder/MemoryDealer.cpp +++ b/libs/binder/MemoryDealer.cpp @@ -17,12 +17,13 @@ #define LOG_TAG "MemoryDealer" #include +#include +#include #include -#include #include #include -#include +#include #include #include @@ -40,90 +41,203 @@ namespace android { // ---------------------------------------------------------------------------- -HeapInterface::HeapInterface() { } -HeapInterface::~HeapInterface() { } +/* + * A simple templatized doubly linked-list implementation + */ + +template +class LinkedList +{ + NODE* mFirst; + NODE* mLast; -// ---------------------------------------------------------------------------- +public: + LinkedList() : mFirst(0), mLast(0) { } + bool isEmpty() const { return mFirst == 0; } + NODE const* head() const { return mFirst; } + NODE* head() { return mFirst; } + NODE const* tail() const { return mLast; } + NODE* tail() { return mLast; } + + void insertAfter(NODE* node, NODE* newNode) { + newNode->prev = node; + newNode->next = node->next; + if (node->next == 0) mLast = newNode; + else node->next->prev = newNode; + node->next = newNode; + } -AllocatorInterface::AllocatorInterface() { } -AllocatorInterface::~AllocatorInterface() { } + void insertBefore(NODE* node, NODE* newNode) { + newNode->prev = node->prev; + newNode->next = node; + if (node->prev == 0) mFirst = newNode; + else node->prev->next = newNode; + node->prev = newNode; + } + + void insertHead(NODE* newNode) { + if (mFirst == 0) { + mFirst = mLast = newNode; + newNode->prev = newNode->next = 0; + } else { + newNode->prev = 0; + newNode->next = mFirst; + mFirst->prev = newNode; + mFirst = newNode; + } + } + + void insertTail(NODE* newNode) { + if (mLast == 0) { + insertHead(newNode); + } else { + newNode->prev = mLast; + newNode->next = 0; + mLast->next = newNode; + mLast = newNode; + } + } + + NODE* remove(NODE* node) { + if (node->prev == 0) mFirst = node->next; + else node->prev->next = node->next; + if (node->next == 0) mLast = node->prev; + else node->next->prev = node->prev; + return node; + } +}; // ---------------------------------------------------------------------------- -class SimpleMemory : public MemoryBase { +class Allocation : public MemoryBase { public: - SimpleMemory(const sp& heap, ssize_t offset, size_t size); - virtual ~SimpleMemory(); + Allocation(const sp& dealer, + const sp& heap, ssize_t offset, size_t size); + virtual ~Allocation(); +private: + sp mDealer; }; +// ---------------------------------------------------------------------------- + +class SimpleBestFitAllocator +{ + enum { + PAGE_ALIGNED = 0x00000001 + }; +public: + SimpleBestFitAllocator(size_t size); + ~SimpleBestFitAllocator(); + + size_t allocate(size_t size, uint32_t flags = 0); + status_t deallocate(size_t offset); + size_t size() const; + void dump(const char* what) const; + void dump(String8& res, const char* what) const; + +private: + + struct chunk_t { + chunk_t(size_t start, size_t size) + : start(start), size(size), free(1), prev(0), next(0) { + } + size_t start; + size_t size : 28; + int free : 4; + mutable chunk_t* prev; + mutable chunk_t* next; + }; + + ssize_t alloc(size_t size, uint32_t flags); + chunk_t* dealloc(size_t start); + void dump_l(const char* what) const; + void dump_l(String8& res, const char* what) const; + + static const int kMemoryAlign; + mutable Mutex mLock; + LinkedList mList; + size_t mHeapSize; +}; // ---------------------------------------------------------------------------- -MemoryDealer::Allocation::Allocation( - const sp& dealer, ssize_t offset, size_t size, - const sp& memory) - : mDealer(dealer), mOffset(offset), mSize(size), mMemory(memory) +Allocation::Allocation( + const sp& dealer, + const sp& heap, ssize_t offset, size_t size) + : MemoryBase(heap, offset, size), mDealer(dealer) { +#ifndef NDEBUG + void* const start_ptr = (void*)(intptr_t(heap->base()) + offset); + memset(start_ptr, 0xda, size); +#endif } -MemoryDealer::Allocation::~Allocation() +Allocation::~Allocation() { - if (mSize) { + size_t freedOffset = getOffset(); + size_t freedSize = getSize(); + if (freedSize) { /* NOTE: it's VERY important to not free allocations of size 0 because * they're special as they don't have any record in the allocator * and could alias some real allocation (their offset is zero). */ - mDealer->deallocate(mOffset); - } -} + mDealer->deallocate(freedOffset); + + // keep the size to unmap in excess + size_t pagesize = getpagesize(); + size_t start = freedOffset; + size_t end = start + freedSize; + start &= ~(pagesize-1); + end = (end + pagesize-1) & ~(pagesize-1); + + // give back to the kernel the pages we don't need + size_t free_start = freedOffset; + size_t free_end = free_start + freedSize; + if (start < free_start) + start = free_start; + if (end > free_end) + end = free_end; + start = (start + pagesize-1) & ~(pagesize-1); + end &= ~(pagesize-1); + + if (start < end) { + void* const start_ptr = (void*)(intptr_t(getHeap()->base()) + start); + size_t size = end-start; -sp MemoryDealer::Allocation::getMemory( - ssize_t* offset, size_t* size) const -{ - return mMemory->getMemory(offset, size); +#ifndef NDEBUG + memset(start_ptr, 0xdf, size); +#endif + + // MADV_REMOVE is not defined on Dapper based Goobuntu +#ifdef MADV_REMOVE + if (size) { + int err = madvise(start_ptr, size, MADV_REMOVE); + LOGW_IF(err, "madvise(%p, %u, MADV_REMOVE) returned %s", + start_ptr, size, err<0 ? strerror(errno) : "Ok"); + } +#endif + } + } } // ---------------------------------------------------------------------------- -MemoryDealer::MemoryDealer(size_t size, uint32_t flags, const char* name) - : mHeap(new SharedHeap(size, flags, name)), +MemoryDealer::MemoryDealer(size_t size, const char* name) + : mHeap(new MemoryHeapBase(size, 0, name)), mAllocator(new SimpleBestFitAllocator(size)) { } -MemoryDealer::MemoryDealer(const sp& heap) - : mHeap(heap), - mAllocator(new SimpleBestFitAllocator(heap->virtualSize())) -{ -} - -MemoryDealer::MemoryDealer( const sp& heap, - const sp& allocator) - : mHeap(heap), mAllocator(allocator) -{ -} - MemoryDealer::~MemoryDealer() { + delete mAllocator; } -sp MemoryDealer::allocate(size_t size, uint32_t flags) +sp MemoryDealer::allocate(size_t size) { sp memory; - const ssize_t offset = allocator()->allocate(size, flags); + const ssize_t offset = allocator()->allocate(size); if (offset >= 0) { - sp new_memory = heap()->mapMemory(offset, size); - if (new_memory != 0) { - memory = new Allocation(this, offset, size, new_memory); - } else { - LOGE("couldn't map [%8lx, %u]", offset, size); - if (size) { - /* NOTE: it's VERY important to not free allocations of size 0 - * because they're special as they don't have any record in the - * allocator and could alias some real allocation - * (their offset is zero). */ - allocator()->deallocate(offset); - } - } + memory = new Allocation(this, heap(), offset, size); } return memory; } @@ -133,16 +247,16 @@ void MemoryDealer::deallocate(size_t offset) allocator()->deallocate(offset); } -void MemoryDealer::dump(const char* what, uint32_t flags) const +void MemoryDealer::dump(const char* what) const { - allocator()->dump(what, flags); + allocator()->dump(what); } -const sp& MemoryDealer::heap() const { +const sp& MemoryDealer::heap() const { return mHeap; } -const sp& MemoryDealer::allocator() const { +SimpleBestFitAllocator* MemoryDealer::allocator() const { return mAllocator; } @@ -287,28 +401,28 @@ SimpleBestFitAllocator::chunk_t* SimpleBestFitAllocator::dealloc(size_t start) return 0; } -void SimpleBestFitAllocator::dump(const char* what, uint32_t flags) const +void SimpleBestFitAllocator::dump(const char* what) const { Mutex::Autolock _l(mLock); - dump_l(what, flags); + dump_l(what); } -void SimpleBestFitAllocator::dump_l(const char* what, uint32_t flags) const +void SimpleBestFitAllocator::dump_l(const char* what) const { String8 result; - dump_l(result, what, flags); + dump_l(result, what); LOGD("%s", result.string()); } void SimpleBestFitAllocator::dump(String8& result, - const char* what, uint32_t flags) const + const char* what) const { Mutex::Autolock _l(mLock); - dump_l(result, what, flags); + dump_l(result, what); } void SimpleBestFitAllocator::dump_l(String8& result, - const char* what, uint32_t flags) const + const char* what) const { size_t size = 0; int32_t i = 0; @@ -341,81 +455,10 @@ void SimpleBestFitAllocator::dump_l(String8& result, i++; cur = cur->next; } - snprintf(buffer, SIZE, " size allocated: %u (%u KB)\n", int(size), int(size/1024)); + snprintf(buffer, SIZE, + " size allocated: %u (%u KB)\n", int(size), int(size/1024)); result.append(buffer); } - -// ---------------------------------------------------------------------------- - -SharedHeap::SharedHeap() - : HeapInterface(), MemoryHeapBase() -{ -} -SharedHeap::SharedHeap(size_t size, uint32_t flags, char const * name) - : MemoryHeapBase(size, flags, name) -{ -} - -SharedHeap::~SharedHeap() -{ -} - -sp SharedHeap::mapMemory(size_t offset, size_t size) -{ - return new SimpleMemory(this, offset, size); -} - - -SimpleMemory::SimpleMemory(const sp& heap, - ssize_t offset, size_t size) - : MemoryBase(heap, offset, size) -{ -#ifndef NDEBUG - void* const start_ptr = (void*)(intptr_t(heap->base()) + offset); - memset(start_ptr, 0xda, size); -#endif -} - -SimpleMemory::~SimpleMemory() -{ - size_t freedOffset = getOffset(); - size_t freedSize = getSize(); - - // keep the size to unmap in excess - size_t pagesize = getpagesize(); - size_t start = freedOffset; - size_t end = start + freedSize; - start &= ~(pagesize-1); - end = (end + pagesize-1) & ~(pagesize-1); - - // give back to the kernel the pages we don't need - size_t free_start = freedOffset; - size_t free_end = free_start + freedSize; - if (start < free_start) - start = free_start; - if (end > free_end) - end = free_end; - start = (start + pagesize-1) & ~(pagesize-1); - end &= ~(pagesize-1); - - if (start < end) { - void* const start_ptr = (void*)(intptr_t(getHeap()->base()) + start); - size_t size = end-start; - -#ifndef NDEBUG - memset(start_ptr, 0xdf, size); -#endif - - // MADV_REMOVE is not defined on Dapper based Goobuntu -#ifdef MADV_REMOVE - if (size) { - int err = madvise(start_ptr, size, MADV_REMOVE); - LOGW_IF(err, "madvise(%p, %u, MADV_REMOVE) returned %s", - start_ptr, size, err<0 ? strerror(errno) : "Ok"); - } -#endif - } -} }; // namespace android diff --git a/libs/binder/MemoryHeapPmem.cpp b/libs/binder/MemoryHeapPmem.cpp index c66094761df5..16e92f939ee3 100644 --- a/libs/binder/MemoryHeapPmem.cpp +++ b/libs/binder/MemoryHeapPmem.cpp @@ -127,7 +127,7 @@ void SubRegionMemory::revoke() MemoryHeapPmem::MemoryHeapPmem(const sp& pmemHeap, uint32_t flags) - : HeapInterface(), MemoryHeapBase() + : MemoryHeapBase() { char const * const device = pmemHeap->getDevice(); #if HAVE_ANDROID_OS diff --git a/media/libmedia/AudioRecord.cpp b/media/libmedia/AudioRecord.cpp index f4165ffede88..7bbd0b2e8e91 100644 --- a/media/libmedia/AudioRecord.cpp +++ b/media/libmedia/AudioRecord.cpp @@ -32,7 +32,6 @@ #include #include -#include #include #include #include diff --git a/media/libmedia/AudioTrack.cpp b/media/libmedia/AudioTrack.cpp index ad0f42e95acc..74852dcd7a79 100644 --- a/media/libmedia/AudioTrack.cpp +++ b/media/libmedia/AudioTrack.cpp @@ -32,7 +32,6 @@ #include #include -#include #include #include #include diff --git a/media/libmediaplayerservice/MetadataRetrieverClient.cpp b/media/libmediaplayerservice/MetadataRetrieverClient.cpp index 66de2eee69a4..162bebbad560 100644 --- a/media/libmediaplayerservice/MetadataRetrieverClient.cpp +++ b/media/libmediaplayerservice/MetadataRetrieverClient.cpp @@ -253,7 +253,7 @@ sp MetadataRetrieverClient::captureFrame() return NULL; } size_t size = sizeof(VideoFrame) + frame->mSize; - mThumbnailDealer = new MemoryDealer(size); + mThumbnailDealer = new MemoryDealer(size, "MetadataRetrieverClient"); if (mThumbnailDealer == NULL) { LOGE("failed to create MemoryDealer"); delete frame; @@ -294,7 +294,7 @@ sp MetadataRetrieverClient::extractAlbumArt() return NULL; } size_t size = sizeof(MediaAlbumArt) + albumArt->mSize; - mAlbumArtDealer = new MemoryDealer(size); + mAlbumArtDealer = new MemoryDealer(size, "MetadataRetrieverClient"); if (mAlbumArtDealer == NULL) { LOGE("failed to create MemoryDealer object"); delete albumArt; diff --git a/media/libstagefright/OMXCodec.cpp b/media/libstagefright/OMXCodec.cpp index 986dcb2f998d..e17fbb8ed31e 100644 --- a/media/libstagefright/OMXCodec.cpp +++ b/media/libstagefright/OMXCodec.cpp @@ -1183,7 +1183,7 @@ status_t OMXCodec::allocateBuffersOnPort(OMX_U32 portIndex) { } size_t totalSize = def.nBufferCountActual * def.nBufferSize; - mDealer[portIndex] = new MemoryDealer(totalSize); + mDealer[portIndex] = new MemoryDealer(totalSize, "OMXCodec"); for (OMX_U32 i = 0; i < def.nBufferCountActual; ++i) { sp mem = mDealer[portIndex]->allocate(def.nBufferSize); diff --git a/media/libstagefright/omx/tests/OMXHarness.cpp b/media/libstagefright/omx/tests/OMXHarness.cpp index 6c36163fb23b..51fcaf5f4438 100644 --- a/media/libstagefright/omx/tests/OMXHarness.cpp +++ b/media/libstagefright/omx/tests/OMXHarness.cpp @@ -286,7 +286,7 @@ status_t Harness::testStateTransitions( return OK; } - sp dealer = new MemoryDealer(8 * 1024 * 1024); + sp dealer = new MemoryDealer(8 * 1024 * 1024, "OMXHarness"); IOMX::node_id node; status_t err = -- 2.11.0