| // 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. |
| |
| #pragma once |
| |
| #include <algorithm> |
| #include <memory> |
| #include <vector> |
| |
| #include <gflags/gflags.h> |
| #include <glog/logging.h> |
| #include <gperftools/heap-profiler.h> |
| #include <gperftools/malloc_extension.h> |
| #include <gperftools/profiler.h> |
| |
| #include "gutil/strings/substitute.h" |
| #include "util/malloc-util.h" |
| #include "util/metrics.h" |
| #include "util/parse-util.h" |
| #include "util/thread.h" |
| #include "util/time.h" |
| |
| DECLARE_int64(tcmalloc_max_total_thread_cache_bytes); |
| DECLARE_bool(tcmalloc_aggressive_memory_decommit); |
| DECLARE_string(tcmalloc_max_free_bytes); |
| DECLARE_int64(tcmalloc_garbage_collection_chunk_size); |
| |
| using strings::Substitute; |
| |
| namespace impala { |
| |
| /// Specialised metric which exposes numeric properties from tcmalloc. |
| /// These metrics line up with the text description used for /memz: |
| /// MALLOC: 3388499040 ( 3231.5 MiB) Bytes in use by application (#1) |
| /// MALLOC: + 0 ( 0.0 MiB) Bytes in page heap freelist (#2) |
| /// MALLOC: + 98309992 ( 93.8 MiB) Bytes in central cache freelist (#3) |
| /// MALLOC: + 4746496 ( 4.5 MiB) Bytes in transfer cache freelist (#4) |
| /// MALLOC: + 177297208 ( 169.1 MiB) Bytes in thread cache freelists (#5) |
| /// MALLOC: + 14942208 ( 14.2 MiB) Bytes in malloc metadata |
| /// MALLOC: ------------ |
| /// MALLOC: = 3683794944 ( 3513.1 MiB) Actual memory used (physical + swap) (#6) |
| /// MALLOC: + 149757952 ( 142.8 MiB) Bytes released to OS (aka unmapped) (#7) |
| /// MALLOC: ------------ |
| /// MALLOC: = 3833552896 ( 3656.0 MiB) Virtual address space used (#8) |
| class TcmallocMetric : public IntGauge { |
| public: |
| /// #1: Number of bytes allocated by tcmalloc, currently used by the application. |
| static TcmallocMetric* BYTES_IN_USE; |
| |
| /// #2: Number of bytes reserved and still mapped by tcmalloc that are not allocated |
| /// to the application. |
| static TcmallocMetric* PAGEHEAP_FREE_BYTES; |
| |
| /// #3: Number of free bytes in the central cache assigned to size classes. |
| static TcmallocMetric* CENTRAL_CACHE_FREE_BYTES; |
| |
| /// #4: Number of free bytes waiting to be transferred between central and thread |
| /// caches. |
| static TcmallocMetric* TRANSFER_CACHE_FREE_BYTES; |
| |
| /// #5: Number of bytes used across all thread caches. |
| static TcmallocMetric* CURRENT_TOTAL_THREAD_CACHE_BYTES; |
| |
| /// #6: Number of physical bytes in use (including any tcmalloc caches and metadata) |
| static TcmallocMetric* PHYSICAL_BYTES_RESERVED; |
| |
| /// #7: Number of bytes once reserved by tcmalloc, but released back to the operating |
| /// system so that their use incurs a pagefault. Contributes to the total amount of |
| /// virtual address space used, but not to the physical memory usage. |
| static TcmallocMetric* PAGEHEAP_UNMAPPED_BYTES; |
| |
| /// #8: Derived metric computing the amount of virtual memory (in bytes) used by the |
| /// process, including all different types of tcmalloc memory including unmapped |
| /// virtual memory. |
| class TotalBytesReservedMetric : public IntGauge { |
| public: |
| TotalBytesReservedMetric(const TMetricDef& def) : IntGauge(def, 0) { } |
| |
| virtual int64_t GetValue() override { |
| return PHYSICAL_BYTES_RESERVED->GetValue() + PAGEHEAP_UNMAPPED_BYTES->GetValue(); |
| } |
| }; |
| static TotalBytesReservedMetric* TOTAL_BYTES_RESERVED; |
| |
| /// Derived metric computing the amount of memory (in bytes) used by tcmalloc for |
| /// overhead (thread caches, unused memory in page heap, etc). |
| class OverheadBytesMetric : public IntGauge { |
| public: |
| OverheadBytesMetric(const TMetricDef& def) : IntGauge(def, 0) { } |
| |
| int64_t GetValue() override { |
| return PHYSICAL_BYTES_RESERVED->GetValue() - BYTES_IN_USE->GetValue(); |
| } |
| }; |
| |
| static OverheadBytesMetric* OVERHEAD_BYTES; |
| |
| static TcmallocMetric* CreateAndRegister(MetricGroup* metrics, const std::string& key, |
| const std::string& tcmalloc_var) { |
| return metrics->RegisterMetric( |
| new TcmallocMetric(MetricDefs::Get(key), tcmalloc_var)); |
| } |
| |
| int64_t GetValue() override { |
| int64_t retval = 0; |
| MallocExtension::instance()->GetNumericProperty(tcmalloc_var_.c_str(), |
| reinterpret_cast<size_t*>(&retval)); |
| return retval; |
| } |
| |
| private: |
| /// Name of the tcmalloc property this metric should fetch. |
| const std::string tcmalloc_var_; |
| |
| TcmallocMetric(const TMetricDef& def, const std::string& tcmalloc_var) |
| : IntGauge(def, 0), tcmalloc_var_(tcmalloc_var) { } |
| }; |
| |
| TcmallocMetric* TcmallocMetric::BYTES_IN_USE = nullptr; |
| TcmallocMetric* TcmallocMetric::PAGEHEAP_FREE_BYTES = nullptr; |
| TcmallocMetric* TcmallocMetric::CENTRAL_CACHE_FREE_BYTES = nullptr; |
| TcmallocMetric* TcmallocMetric::TRANSFER_CACHE_FREE_BYTES = nullptr; |
| TcmallocMetric* TcmallocMetric::CURRENT_TOTAL_THREAD_CACHE_BYTES = nullptr; |
| TcmallocMetric* TcmallocMetric::PHYSICAL_BYTES_RESERVED = nullptr; |
| TcmallocMetric* TcmallocMetric::PAGEHEAP_UNMAPPED_BYTES = nullptr; |
| TcmallocMetric::TotalBytesReservedMetric* TcmallocMetric::TOTAL_BYTES_RESERVED = nullptr; |
| TcmallocMetric::OverheadBytesMetric* TcmallocMetric::OVERHEAD_BYTES = nullptr; |
| |
| class GperftoolsMallocUtil : public MallocUtil { |
| public: |
| Status Init(int64_t process_mem_limit) override { |
| // Some backend tests call this multiple times |
| if (initialized_) return Status::OK(); |
| |
| if (FLAGS_tcmalloc_aggressive_memory_decommit) { |
| // By default tcmalloc does not use aggressive decommit. Set it if it is enabled. |
| MallocExtension::instance()->SetNumericProperty( |
| "tcmalloc.aggressive_memory_decommit", 1); |
| } |
| |
| const static char* TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES = |
| "tcmalloc.max_total_thread_cache_bytes"; |
| // Change the total TCMalloc thread cache size if necessary. |
| if (FLAGS_tcmalloc_max_total_thread_cache_bytes > 0 && |
| !MallocExtension::instance()->SetNumericProperty( |
| TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES, |
| FLAGS_tcmalloc_max_total_thread_cache_bytes)) { |
| return Status(Substitute("Failed to change {0}", |
| TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES)); |
| } |
| // Read the value back from tcmalloc to verify it matches what we set. |
| size_t actual_max_total_thread_cache_bytes = 0; |
| bool retval = MallocExtension::instance()->GetNumericProperty( |
| TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES, |
| &actual_max_total_thread_cache_bytes); |
| if (!retval) { |
| return Status(Substitute("Could not retrieve value of {0}.", |
| TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES)); |
| } |
| if (actual_max_total_thread_cache_bytes != |
| FLAGS_tcmalloc_max_total_thread_cache_bytes) { |
| LOG(WARNING) << "Set " << TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES << " to " |
| << FLAGS_tcmalloc_max_total_thread_cache_bytes << " bytes but " |
| << "actually using " << actual_max_total_thread_cache_bytes |
| << " bytes."; |
| } |
| |
| // Start the background garbage collector thread if aggressive decommit |
| // is disabled. |
| if (!FLAGS_tcmalloc_aggressive_memory_decommit) { |
| // Determine the maximum overhead |
| bool is_percent; |
| max_overhead_ = ParseUtil::ParseMemSpec(FLAGS_tcmalloc_max_free_bytes, |
| &is_percent, process_mem_limit); |
| if (max_overhead_ <= 0) { |
| if (process_mem_limit <= 0) { |
| // If the process_mem_limit is not specified, then this cannot accept a |
| // percentage value. |
| return Status(Substitute("Invalid --tcmalloc_max_free_bytes value, must be a " |
| "positive bytes value: $0", FLAGS_tcmalloc_max_free_bytes)); |
| } else { |
| return Status(Substitute("Invalid --tcmalloc_max_free_bytes value, must be a " |
| "positive bytes value or percentage: $0", FLAGS_tcmalloc_max_free_bytes)); |
| } |
| } |
| LOG(INFO) << "TCMalloc max overhead = " << max_overhead_; |
| |
| RETURN_IF_ERROR(Thread::Create("malloc-util", "gc_thread", |
| [this]() { this->GarbageCollectorThread(); }, &gc_thread_)); |
| } |
| initialized_ = true; |
| return Status::OK(); |
| } |
| |
| void ReleaseMemoryToSystem(int64_t bytes_to_free) override { |
| // If aggressive memory decommit is enabled, tcmalloc is not holding on to large |
| // amounts of memory, so there is no need to manually release it. |
| if (FLAGS_tcmalloc_aggressive_memory_decommit) return; |
| |
| int64_t extra = bytes_to_free; |
| while (extra > 0) { |
| // Tcmalloc holds the page heap lock while releasing the memory, so release in |
| // chunks to avoid holding the lock for an extended period of time. This does |
| // not call sched_yield() between calls, because the kernel already gave us a |
| // time slice and sched_yield() is often a no-op in that case. |
| int64_t amount_to_release = |
| std::min(FLAGS_tcmalloc_garbage_collection_chunk_size, extra); |
| MallocExtension::instance()->ReleaseToSystem(amount_to_release); |
| extra -= amount_to_release; |
| } |
| } |
| |
| std::string GetTextDescription() const override { |
| char buf[2048]; |
| MallocExtension::instance()->GetStats(buf, 2048); |
| return std::string(buf); |
| } |
| |
| std::string GetName() const override { return "TCMalloc"; } |
| |
| Status RegisterMemoryMetrics(MetricGroup* metrics) override { |
| DCHECK(initialized_); |
| MetricGroup* tcmalloc_metrics = metrics->GetOrCreateChildGroup("tcmalloc"); |
| |
| /// These metrics line up with the text description used for /memz: |
| /// MALLOC: 3388499040 ( 3231.5 MiB) Bytes in use by application (#1) |
| /// MALLOC: + 0 ( 0.0 MiB) Bytes in page heap freelist (#2) |
| /// MALLOC: + 98309992 ( 93.8 MiB) Bytes in central cache freelist (#3) |
| /// MALLOC: + 4746496 ( 4.5 MiB) Bytes in transfer cache freelist (#4) |
| /// MALLOC: + 177297208 ( 169.1 MiB) Bytes in thread cache freelists (#5) |
| /// MALLOC: + 14942208 ( 14.2 MiB) Bytes in malloc metadata |
| /// MALLOC: ------------ |
| /// MALLOC: = 3683794944 ( 3513.1 MiB) Actual memory used (physical + swap) (#6) |
| /// MALLOC: + 149757952 ( 142.8 MiB) Bytes released to OS (aka unmapped) (#7) |
| /// MALLOC: ------------ |
| /// MALLOC: = 3833552896 ( 3656.0 MiB) Virtual address space used (#8) |
| |
| /// #1 |
| TcmallocMetric::BYTES_IN_USE = TcmallocMetric::CreateAndRegister( |
| tcmalloc_metrics, "tcmalloc.bytes-in-use", "generic.current_allocated_bytes"); |
| |
| /// #2 |
| TcmallocMetric::PAGEHEAP_FREE_BYTES = TcmallocMetric::CreateAndRegister( |
| tcmalloc_metrics, "tcmalloc.pageheap-free-bytes", "tcmalloc.pageheap_free_bytes"); |
| |
| /// #3 |
| TcmallocMetric::CENTRAL_CACHE_FREE_BYTES = TcmallocMetric::CreateAndRegister( |
| tcmalloc_metrics, "tcmalloc.central-cache-free-bytes", |
| "tcmalloc.central_cache_free_bytes"); |
| |
| /// #4 |
| TcmallocMetric::TRANSFER_CACHE_FREE_BYTES = TcmallocMetric::CreateAndRegister( |
| tcmalloc_metrics, "tcmalloc.transfer-cache-free-bytes", |
| "tcmalloc.transfer_cache_free_bytes"); |
| |
| /// #5 |
| TcmallocMetric::CURRENT_TOTAL_THREAD_CACHE_BYTES = TcmallocMetric::CreateAndRegister( |
| tcmalloc_metrics, "tcmalloc.current-total-thread-cache-bytes", |
| "tcmalloc.current_total_thread_cache_bytes"); |
| |
| /// #6 |
| TcmallocMetric::PHYSICAL_BYTES_RESERVED = TcmallocMetric::CreateAndRegister( |
| tcmalloc_metrics, "tcmalloc.physical-bytes-reserved", |
| "generic.total_physical_bytes"); |
| |
| /// #7 |
| TcmallocMetric::PAGEHEAP_UNMAPPED_BYTES = |
| TcmallocMetric::CreateAndRegister(tcmalloc_metrics, |
| "tcmalloc.pageheap-unmapped-bytes", "tcmalloc.pageheap_unmapped_bytes"); |
| |
| /// #8 |
| TcmallocMetric::TOTAL_BYTES_RESERVED = |
| tcmalloc_metrics->RegisterMetric(new TcmallocMetric::TotalBytesReservedMetric( |
| MetricDefs::Get("tcmalloc.total-bytes-reserved"))); |
| |
| /// Additional overhead metric (#6 - #1) |
| TcmallocMetric::OVERHEAD_BYTES = |
| tcmalloc_metrics->RegisterMetric(new TcmallocMetric::OverheadBytesMetric( |
| MetricDefs::Get("tcmalloc.overhead-bytes"))); |
| |
| return Status::OK(); |
| } |
| |
| IntGauge* GetUsedBytesMetric(bool include_overhead) const override { |
| DCHECK(initialized_); |
| if (include_overhead) { |
| DCHECK(TcmallocMetric::PHYSICAL_BYTES_RESERVED != nullptr); |
| return TcmallocMetric::PHYSICAL_BYTES_RESERVED; |
| } else { |
| DCHECK(TcmallocMetric::BYTES_IN_USE != nullptr); |
| return TcmallocMetric::BYTES_IN_USE; |
| } |
| } |
| |
| IntGauge* GetOverheadBytesMetric() const override { |
| DCHECK(initialized_); |
| DCHECK(TcmallocMetric::OVERHEAD_BYTES != nullptr); |
| return TcmallocMetric::OVERHEAD_BYTES; |
| } |
| |
| HugePageSupport GetHugePageSupport() const override { |
| DCHECK(initialized_); |
| // As an extra precaution, read the actual value from tcmalloc rather than simply |
| // reading the startup parameter. |
| size_t aggressive_decommit_enabled; |
| MallocExtension::instance()->GetNumericProperty( |
| "tcmalloc.aggressive_memory_decommit", &aggressive_decommit_enabled); |
| if (FLAGS_tcmalloc_aggressive_memory_decommit) { |
| DCHECK(aggressive_decommit_enabled); |
| } else { |
| DCHECK(!aggressive_decommit_enabled); |
| } |
| if (aggressive_decommit_enabled) { |
| // With aggressive decommit, large allocations are immediately freed, |
| // so it is compatible with using madvise to get huge pages. |
| return HugePageSupport::MADVISE_COMPATIBLE; |
| } else { |
| return HugePageSupport::MADVISE_INCOMPATIBLE; |
| } |
| } |
| |
| bool SupportsHeapProfiling() const override { return true; } |
| void HeapProfilerStart(const std::string& tmp_prof_file_name) override { |
| ::HeapProfilerStart(tmp_prof_file_name.c_str()); |
| } |
| |
| void HeapProfilerStop() override { |
| ::HeapProfilerStop(); |
| } |
| |
| char* GetHeapProfile() override { |
| return ::GetHeapProfile(); |
| } |
| |
| bool SupportsHeapGrowthStacks() const override { return true; } |
| void GetHeapGrowthStacks(std::string* heap_growth_stacks) override { |
| MallocExtension::instance()->GetHeapGrowthStacks(heap_growth_stacks); |
| } |
| |
| bool SupportsCPUProfiling() const override { return true; } |
| void CPUProfilerStart(const std::string& tmp_prof_file_name) override { |
| ::ProfilerStart(tmp_prof_file_name.c_str()); |
| } |
| void CPUProfilerStop() override { |
| ::ProfilerStop(); |
| } |
| |
| private: |
| int64_t max_overhead_; |
| std::unique_ptr<Thread> gc_thread_; |
| bool initialized_ = false; |
| |
| // When using TCMalloc with aggressive decommit off, TCMalloc accumulates memory |
| // indefinitely unless we manually garbage collect it. TCMalloc releases memory |
| // every N deletes, where N is based on the TCMALLOC_RELEASE_RATE property. |
| // When TCMalloc decides to release memory, it removes a single span from the |
| // page heap. This means that there are certain allocation patterns that can lead |
| // to continuous accumulation of memory. One example is continually resizing a |
| // vector, which results in many allocations. Even after the vector goes out of |
| // scope, it will not release all the memory unless there are enough other deletions |
| // occuring in the system. Impala must have these types of memory patterns, as |
| // our experience is that even high TCMALLOC_RELEASE_RATE settings do not bound |
| // memory use. |
| // |
| // This background thread frees memory periodically to keep TCMalloc's memory overhead |
| // limited. To smooth out the release, this keeps track of the last N samples and |
| // frees memory based on the minimum sample. This avoids releasing memory that will |
| // be reused quickly. |
| static constexpr int GARBAGE_COLLECTOR_HISTORY_SIZE = 10; |
| [[noreturn]] void GarbageCollectorThread() { |
| DCHECK(!FLAGS_tcmalloc_aggressive_memory_decommit); |
| // Initialize the history to all zeros |
| std::vector<int64_t> bytes_overhead_history(GARBAGE_COLLECTOR_HISTORY_SIZE, 0); |
| int cur_history_index = 0; |
| while (true) { |
| // Number of bytes in the 'NORMAL' free list (i.e. reserved by tcmalloc but |
| // not in use). |
| if (TcmallocMetric::PAGEHEAP_FREE_BYTES != nullptr) { |
| bytes_overhead_history[cur_history_index] = |
| TcmallocMetric::PAGEHEAP_FREE_BYTES->GetValue(); |
| cur_history_index++; |
| if (cur_history_index == bytes_overhead_history.size()) { |
| cur_history_index = 0; |
| } |
| } |
| |
| // Free based on the minimum overhead in the history. This avoids releasing memory |
| // that will be reused very quickly. However, the max_overhead is usually a fairly |
| // substantial amount of memory, so it is ok to aggressively enforce the limit. |
| int64_t min_bytes_overhead = *std::min_element(bytes_overhead_history.cbegin(), |
| bytes_overhead_history.cend()); |
| if (min_bytes_overhead > max_overhead_) { |
| ReleaseMemoryToSystem(min_bytes_overhead - max_overhead_); |
| } |
| SleepForMs(1000); |
| } |
| } |
| }; |
| |
| } // namespace impala |