| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Connects to a running Chrome process, and outputs statistics about its |
| // bucket usage. |
| // |
| // To use this tool, chrome needs to be compiled with the RECORD_ALLOC_INFO |
| // flag. |
| |
| #include <algorithm> |
| #include <cstring> |
| #include <ios> |
| #include <iostream> |
| #include <map> |
| #include <optional> |
| #include <string> |
| #include <unordered_map> |
| #include <vector> |
| |
| #include "base/allocator/partition_allocator/src/partition_alloc/partition_root.h" |
| #include "base/allocator/partition_allocator/src/partition_alloc/thread_cache.h" |
| #include "base/check_op.h" |
| #include "base/debug/proc_maps_linux.h" |
| #include "base/files/file.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/thread_annotations.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "tools/memory/partition_allocator/inspect_utils.h" |
| |
| namespace partition_alloc::tools { |
| namespace { |
| |
| using partition_alloc::internal::BucketIndexLookup; |
| using partition_alloc::internal::kNumBuckets; |
| |
| constexpr const char* kDumpName = "dump.dat"; |
| constexpr const char* kTmpDumpName = "dump.dat.tmp"; |
| |
| uintptr_t FindAllocInfoAddress(RemoteProcessMemoryReader& reader) { |
| return IndexThreadCacheNeedleArray(reader, 2); |
| } |
| |
| void DisplayPerBucketData( |
| const std::unordered_map<uintptr_t, size_t>& live_allocs, |
| size_t allocations, |
| double allocations_per_second) { |
| constexpr BucketIndexLookup lookup{}; |
| std::cout << "Per-bucket stats:" |
| << "\nIndex\tBucket Size\t#Allocs\tTotal size\tFragmentation" |
| << std::string(80, '-') << "\n"; |
| |
| // Direct mapped allocations have an index of |kNumBuckets|, so add 1 here. |
| size_t alloc_size[kNumBuckets + 1] = {}; |
| size_t alloc_nums[kNumBuckets + 1] = {}; |
| size_t alt_alloc_size[kNumBuckets + 1] = {}; |
| size_t alt_alloc_nums[kNumBuckets + 1] = {}; |
| size_t total_memory = 0; |
| for (const auto& pair : live_allocs) { |
| const auto requested_size = pair.second; |
| total_memory += requested_size; |
| |
| // We record 2 distributions below. They can be whatever you want; edit the |
| // 2 blocks below to change which distributions are recorded. |
| |
| { |
| const auto i = |
| BucketIndexLookup::GetIndexForNeutralBuckets(requested_size); |
| alloc_size[i] += requested_size; |
| alloc_nums[i]++; |
| } |
| |
| { |
| const auto j = |
| BucketIndexLookup::GetIndexForDenserBuckets(requested_size); |
| alt_alloc_size[j] += requested_size; |
| alt_alloc_nums[j]++; |
| } |
| } |
| |
| base::File f(base::FilePath(kTmpDumpName), |
| base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); |
| // Do not record the direct mapped allocations below, since we only care about |
| // the bucket distribution, which direct mapped allocations do not affect. |
| for (size_t i = 0; i < kNumBuckets; i++) { |
| const auto bucket_size = lookup.bucket_sizes()[i]; |
| const size_t fragmentation = |
| alloc_nums[i] == 0 |
| ? 0 |
| : 100 - 100 * alloc_size[i] / (1.0 * bucket_size * alloc_nums[i]); |
| std::cout << i << "\t" << bucket_size << "\t\t" << alloc_nums[i] << "\t" |
| << (alloc_size[i] / 1024) << "KiB" |
| << "\t\t" << fragmentation << "%" |
| << "\n"; |
| std::string written = base::StringPrintf( |
| "%zu,%lu,%zu,%zu,%zu,%zu\n", i, bucket_size, alloc_nums[i], |
| alloc_size[i], alt_alloc_nums[i], alt_alloc_size[i]); |
| if (f.WriteAtCurrentPos(written.data(), written.size()) != |
| static_cast<int>(written.size())) { |
| std::cerr << "WARNING: Unable to write to temp file, data will be " |
| "stale/missing.\n"; |
| return; |
| } |
| } |
| |
| rename(kTmpDumpName, kDumpName); |
| |
| std::cout << "\nALL THREADS TOTAL: " << total_memory / 1024 << "kiB" |
| << "\tLive Allocations = " << allocations |
| << "\tAllocations per second = " << allocations_per_second |
| << std::endl; |
| } |
| |
| } // namespace |
| } // namespace partition_alloc::tools |
| |
| int main(int argc, char** argv) { |
| using partition_alloc::internal::AllocInfo; |
| using partition_alloc::internal::kAllocInfoSize; |
| |
| if (argc < 2) { |
| LOG(ERROR) << "Usage:" << argv[0] << " <PID> " |
| << "[address. 0 to scan the process memory]"; |
| return 1; |
| } |
| |
| int pid = atoi(argv[1]); |
| uintptr_t registry_address = 0; |
| |
| partition_alloc::tools::RemoteProcessMemoryReader reader{pid}; |
| |
| if (argc == 3) { |
| uint64_t address; |
| CHECK(base::StringToUint64(argv[2], &address)); |
| registry_address = static_cast<uintptr_t>(address); |
| } else { |
| // Scan the memory. |
| registry_address = partition_alloc::tools::FindAllocInfoAddress(reader); |
| } |
| |
| CHECK(registry_address); |
| |
| auto alloc_info = std::make_unique<AllocInfo>(); |
| reader.ReadMemory(registry_address, sizeof(AllocInfo), |
| reinterpret_cast<char*>(alloc_info.get())); |
| |
| // If this check fails, it means we have overflowed our circular buffer before |
| // we had time to start this script. Either the circular buffer needs to be |
| // bigger, or the script needs to be started sooner. |
| CHECK_LT(alloc_info->index.load(), kAllocInfoSize); |
| |
| size_t old_index = 0; |
| size_t new_index = alloc_info->index; |
| base::TimeTicks last_collection_time = base::TimeTicks::Now(); |
| double allocations_per_second = 0.; |
| |
| std::unordered_map<uintptr_t, size_t> live_allocs = {}; |
| while (true) { |
| base::TimeTicks tick = base::TimeTicks::Now(); |
| |
| size_t len = old_index < new_index ? new_index - old_index |
| : kAllocInfoSize - new_index + old_index; |
| |
| for (size_t i = 0; i < len; i++) { |
| size_t index = i % kAllocInfoSize; |
| const auto& entry = alloc_info->allocs[index]; |
| // Skip nulls. |
| if (entry.addr == 0x0) { |
| continue; |
| } |
| if (entry.addr & 0x01) { // alloc |
| uintptr_t addr = entry.addr & ~0x01; |
| live_allocs.insert({addr, entry.size}); |
| } else { // free |
| live_allocs.erase(entry.addr); |
| } |
| } |
| |
| int64_t gather_time_ms = (base::TimeTicks::Now() - tick).InMilliseconds(); |
| constexpr const char* kClearScreen = "\033[2J\033[1;1H"; |
| std::cout << kClearScreen << "Time to gather data = " << gather_time_ms |
| << "ms\n"; |
| partition_alloc::tools::DisplayPerBucketData( |
| live_allocs, live_allocs.size(), allocations_per_second); |
| |
| reader.ReadMemory(registry_address, sizeof(AllocInfo), |
| reinterpret_cast<char*>(alloc_info.get())); |
| base::TimeTicks now = base::TimeTicks::Now(); |
| allocations_per_second = len / (now - last_collection_time).InSecondsF(); |
| |
| old_index = new_index; |
| new_index = alloc_info->index; |
| last_collection_time = now; |
| usleep(1'000'000); |
| } |
| } |