Modify the implementation of MemTracker: 1. Simplify a lot of useless logic; 2. Added MemTrackerTaskPool, as the ancestor of all query and import trackers, This is used to track the local memory usage of all tasks executing; 3. Add cosume/release cache, trigger a cosume/release when the memory accumulation exceeds the parameter mem_tracker_consume_min_size_bytes; 4. Add a new memory leak detection mode (Experimental feature), throw an exception when the remaining statistical value is greater than the specified range when the MemTracker is destructed, and print the accurate statistical value in HTTP, the parameter memory_leak_detection 5. Added Virtual MemTracker, cosume/release will not sync to parent. It will be used when introducing TCMalloc Hook to record memory later, to record the specified memory independently; 6. Modify the GC logic, register the buffer cached in DiskIoMgr as a GC function, and add other GC functions later; 7. Change the global root node from Root MemTracker to Process MemTracker, and remove Process MemTracker in exec_env; 8. Modify the macro that detects whether the memory has reached the upper limit, modify the parameters and default behavior of creating MemTracker, modify the error message format in mem_limit_exceeded, extend and apply transfer_to, remove Metric in MemTracker, etc.; Modify where MemTracker is used: 1. MemPool adds a constructor to create a temporary tracker to avoid a lot of redundant code; 2. Added trackers for global objects such as ChunkAllocator and StorageEngine; 3. Added more fine-grained trackers such as ExprContext; 4. RuntimeState removes FragmentMemTracker, that is, PlanFragmentExecutor mem_tracker, which was previously used for independent statistical scan process memory, and replaces it with _scanner_mem_tracker in OlapScanNode; 5. MemTracker is no longer recorded in ReservationTracker, and ReservationTracker will be removed later;
164 lines
5.7 KiB
C++
164 lines
5.7 KiB
C++
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you 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.
|
|
|
|
#ifndef DORIS_BE_SRC_QUERY_BE_RUNTIME_FREE_POOL_H
|
|
#define DORIS_BE_SRC_QUERY_BE_RUNTIME_FREE_POOL_H
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <string>
|
|
|
|
#include "common/logging.h"
|
|
#include "runtime/mem_pool.h"
|
|
#include "util/bit_util.h"
|
|
|
|
namespace doris {
|
|
|
|
// Implementation of a free pool to recycle allocations. The pool is broken
|
|
// up into 64 lists, one for each power of 2. Each allocation is rounded up
|
|
// to the next power of 2. When the allocation is freed, it is added to the
|
|
// corresponding free list.
|
|
// Each allocation has an 8 byte header that immediately precedes the actual
|
|
// allocation. If the allocation is owned by the user, the header contains
|
|
// the ptr to the list that it should be added to on Free().
|
|
// When the allocation is in the pool (i.e. available to be handed out), it
|
|
// contains the link to the next allocation.
|
|
// This has O(1) Allocate() and Free().
|
|
// This is not thread safe.
|
|
// TODO(zxy): consider integrating this with MemPool.
|
|
// TODO: consider changing to something more granular than doubling.
|
|
class FreePool {
|
|
public:
|
|
// C'tor, initializes the FreePool to be empty. All allocations come from the
|
|
// 'mem_pool'.
|
|
FreePool(MemPool* mem_pool) : _mem_pool(mem_pool) { memset(&_lists, 0, sizeof(_lists)); }
|
|
|
|
virtual ~FreePool() {}
|
|
|
|
// Allocates a buffer of size.
|
|
uint8_t* allocate(int64_t size) {
|
|
// This is the typical malloc behavior. NULL is reserved for failures.
|
|
if (size == 0) {
|
|
return reinterpret_cast<uint8_t*>(0x1);
|
|
}
|
|
|
|
// Do ceil(log_2(size))
|
|
int free_list_idx = BitUtil::log2(size);
|
|
DCHECK_LT(free_list_idx, NUM_LISTS);
|
|
|
|
FreeListNode* allocation = _lists[free_list_idx].next;
|
|
|
|
if (allocation == NULL) {
|
|
// There wasn't an existing allocation of the right size, allocate a new one.
|
|
size = 1L << free_list_idx;
|
|
allocation = reinterpret_cast<FreeListNode*>(
|
|
_mem_pool->allocate(size + sizeof(FreeListNode)));
|
|
} else {
|
|
// Remove this allocation from the list.
|
|
_lists[free_list_idx].next = allocation->next;
|
|
}
|
|
|
|
DCHECK(allocation != NULL);
|
|
// Set the back node to point back to the list it came from so know where
|
|
// to add it on free().
|
|
allocation->list = &_lists[free_list_idx];
|
|
return reinterpret_cast<uint8_t*>(allocation) + sizeof(FreeListNode);
|
|
}
|
|
|
|
void free(uint8_t* ptr) {
|
|
if (ptr == NULL || reinterpret_cast<int64_t>(ptr) == 0x1) {
|
|
return;
|
|
}
|
|
|
|
FreeListNode* node = reinterpret_cast<FreeListNode*>(ptr - sizeof(FreeListNode));
|
|
FreeListNode* list = node->list;
|
|
#ifndef NDEBUG
|
|
check_valid_allocation(list);
|
|
#endif
|
|
// Add node to front of list.
|
|
node->next = list->next;
|
|
list->next = node;
|
|
}
|
|
|
|
// Returns an allocation that is at least 'size'. If the current allocation
|
|
// backing 'ptr' is big enough, 'ptr' is returned. Otherwise a new one is
|
|
// made and the contents of ptr are copied into it.
|
|
uint8_t* reallocate(uint8_t* ptr, int64_t size) {
|
|
if (ptr == NULL || reinterpret_cast<int64_t>(ptr) == 0x1) {
|
|
return allocate(size);
|
|
}
|
|
|
|
FreeListNode* node = reinterpret_cast<FreeListNode*>(ptr - sizeof(FreeListNode));
|
|
FreeListNode* list = node->list;
|
|
#ifndef NDEBUG
|
|
check_valid_allocation(list);
|
|
#endif
|
|
int bucket_idx = (list - &_lists[0]);
|
|
// This is the actual size of ptr.
|
|
int allocation_size = 1 << bucket_idx;
|
|
|
|
// If it's already big enough, just return the ptr.
|
|
if (allocation_size >= size) {
|
|
return ptr;
|
|
}
|
|
|
|
// Make a new one. Since allocate() already rounds up to powers of 2, this
|
|
// effectively doubles for the caller.
|
|
uint8_t* new_ptr = allocate(size);
|
|
memcpy(new_ptr, ptr, allocation_size);
|
|
free(ptr);
|
|
return new_ptr;
|
|
}
|
|
|
|
private:
|
|
static const int NUM_LISTS = 64;
|
|
|
|
struct FreeListNode {
|
|
// Union for clarity when manipulating the node.
|
|
union {
|
|
FreeListNode* next; // Used when it is in the free list
|
|
FreeListNode* list; // Used when it is being used by the caller.
|
|
};
|
|
};
|
|
|
|
void check_valid_allocation(FreeListNode* computed_list_ptr) {
|
|
// On debug, check that list is valid.
|
|
bool found = false;
|
|
|
|
for (int i = 0; i < NUM_LISTS && !found; ++i) {
|
|
if (computed_list_ptr == &_lists[i]) {
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
DCHECK(found);
|
|
}
|
|
|
|
// MemPool to allocate from. Unowned.
|
|
MemPool* _mem_pool;
|
|
|
|
// One list head for each allocation size indexed by the LOG_2 of the allocation size.
|
|
// While it doesn't make too much sense to use this for very small (e.g. 8 byte)
|
|
// allocations, it makes the indexing easy.
|
|
FreeListNode _lists[NUM_LISTS];
|
|
};
|
|
|
|
} // namespace doris
|
|
|
|
#endif
|