| // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- |
| // Copyright (c) 2006, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // --- |
| // Author: Sanjay Ghemawat |
| // Maxim Lifantsev (refactoring) |
| // |
| |
| #include <config.h> |
| |
| #ifdef HAVE_UNISTD_H |
| #include <unistd.h> // for write() |
| #endif |
| #include <fcntl.h> // for open() |
| #ifdef HAVE_GLOB_H |
| #include <glob.h> |
| #ifndef GLOB_NOMATCH // true on some old cygwins |
| # define GLOB_NOMATCH 0 |
| #endif |
| #endif |
| #ifdef HAVE_INTTYPES_H |
| #include <inttypes.h> // for PRIxPTR |
| #endif |
| #ifdef HAVE_POLL_H |
| #include <poll.h> |
| #endif |
| #include <errno.h> |
| #include <stdarg.h> |
| #include <string> |
| #include <map> |
| #include <algorithm> // for sort(), equal(), and copy() |
| |
| #include "heap-profile-table.h" |
| |
| #include "base/logging.h" |
| #include "raw_printer.h" |
| #include "symbolize.h" |
| #include <gperftools/stacktrace.h> |
| #include <gperftools/malloc_hook.h> |
| #include "memory_region_map.h" |
| #include "base/commandlineflags.h" |
| #include "base/logging.h" // for the RawFD I/O commands |
| #include "base/sysinfo.h" |
| |
| using std::sort; |
| using std::equal; |
| using std::copy; |
| using std::string; |
| using std::map; |
| |
| using tcmalloc::FillProcSelfMaps; // from sysinfo.h |
| using tcmalloc::DumpProcSelfMaps; // from sysinfo.h |
| |
| //---------------------------------------------------------------------- |
| |
| DEFINE_bool(cleanup_old_heap_profiles, |
| EnvToBool("HEAP_PROFILE_CLEANUP", true), |
| "At initialization time, delete old heap profiles."); |
| |
| DEFINE_int32(heap_check_max_leaks, |
| EnvToInt("HEAP_CHECK_MAX_LEAKS", 20), |
| "The maximum number of leak reports to print."); |
| |
| //---------------------------------------------------------------------- |
| |
| // header of the dumped heap profile |
| static const char kProfileHeader[] = "heap profile: "; |
| static const char kProcSelfMapsHeader[] = "\nMAPPED_LIBRARIES:\n"; |
| |
| //---------------------------------------------------------------------- |
| |
| const char HeapProfileTable::kFileExt[] = ".heap"; |
| |
| //---------------------------------------------------------------------- |
| |
| static const int kHashTableSize = 179999; // Size for bucket_table_. |
| /*static*/ const int HeapProfileTable::kMaxStackDepth; |
| |
| //---------------------------------------------------------------------- |
| |
| // We strip out different number of stack frames in debug mode |
| // because less inlining happens in that case |
| #ifdef NDEBUG |
| static const int kStripFrames = 2; |
| #else |
| static const int kStripFrames = 3; |
| #endif |
| |
| // For sorting Stats or Buckets by in-use space |
| static bool ByAllocatedSpace(HeapProfileTable::Stats* a, |
| HeapProfileTable::Stats* b) { |
| // Return true iff "a" has more allocated space than "b" |
| return (a->alloc_size - a->free_size) > (b->alloc_size - b->free_size); |
| } |
| |
| //---------------------------------------------------------------------- |
| |
| HeapProfileTable::HeapProfileTable(Allocator alloc, |
| DeAllocator dealloc, |
| bool profile_mmap) |
| : alloc_(alloc), |
| dealloc_(dealloc), |
| profile_mmap_(profile_mmap), |
| bucket_table_(NULL), |
| num_buckets_(0), |
| address_map_(NULL) { |
| // Make a hash table for buckets. |
| const int table_bytes = kHashTableSize * sizeof(*bucket_table_); |
| bucket_table_ = static_cast<Bucket**>(alloc_(table_bytes)); |
| memset(bucket_table_, 0, table_bytes); |
| |
| // Make an allocation map. |
| address_map_ = |
| new(alloc_(sizeof(AllocationMap))) AllocationMap(alloc_, dealloc_); |
| |
| // Initialize. |
| memset(&total_, 0, sizeof(total_)); |
| num_buckets_ = 0; |
| } |
| |
| HeapProfileTable::~HeapProfileTable() { |
| // Free the allocation map. |
| address_map_->~AllocationMap(); |
| dealloc_(address_map_); |
| address_map_ = NULL; |
| |
| // Free the hash table. |
| for (int i = 0; i < kHashTableSize; i++) { |
| for (Bucket* curr = bucket_table_[i]; curr != 0; /**/) { |
| Bucket* bucket = curr; |
| curr = curr->next; |
| dealloc_(bucket->stack); |
| dealloc_(bucket); |
| } |
| } |
| dealloc_(bucket_table_); |
| bucket_table_ = NULL; |
| } |
| |
| HeapProfileTable::Bucket* HeapProfileTable::GetBucket(int depth, |
| const void* const key[]) { |
| // Make hash-value |
| uintptr_t h = 0; |
| for (int i = 0; i < depth; i++) { |
| h += reinterpret_cast<uintptr_t>(key[i]); |
| h += h << 10; |
| h ^= h >> 6; |
| } |
| h += h << 3; |
| h ^= h >> 11; |
| |
| // Lookup stack trace in table |
| unsigned int buck = ((unsigned int) h) % kHashTableSize; |
| for (Bucket* b = bucket_table_[buck]; b != 0; b = b->next) { |
| if ((b->hash == h) && |
| (b->depth == depth) && |
| equal(key, key + depth, b->stack)) { |
| return b; |
| } |
| } |
| |
| // Create new bucket |
| const size_t key_size = sizeof(key[0]) * depth; |
| const void** kcopy = reinterpret_cast<const void**>(alloc_(key_size)); |
| copy(key, key + depth, kcopy); |
| Bucket* b = reinterpret_cast<Bucket*>(alloc_(sizeof(Bucket))); |
| memset(b, 0, sizeof(*b)); |
| b->hash = h; |
| b->depth = depth; |
| b->stack = kcopy; |
| b->next = bucket_table_[buck]; |
| bucket_table_[buck] = b; |
| num_buckets_++; |
| return b; |
| } |
| |
| int HeapProfileTable::GetCallerStackTrace( |
| int skip_count, void* stack[kMaxStackDepth]) { |
| return MallocHook::GetCallerStackTrace( |
| stack, kMaxStackDepth, kStripFrames + skip_count + 1); |
| } |
| |
| void HeapProfileTable::RecordAlloc( |
| const void* ptr, size_t bytes, int stack_depth, |
| const void* const call_stack[]) { |
| Bucket* b = GetBucket(stack_depth, call_stack); |
| b->allocs++; |
| b->alloc_size += bytes; |
| total_.allocs++; |
| total_.alloc_size += bytes; |
| |
| AllocValue v; |
| v.set_bucket(b); // also did set_live(false); set_ignore(false) |
| v.bytes = bytes; |
| address_map_->Insert(ptr, v); |
| } |
| |
| void HeapProfileTable::RecordFree(const void* ptr) { |
| AllocValue v; |
| if (address_map_->FindAndRemove(ptr, &v)) { |
| Bucket* b = v.bucket(); |
| b->frees++; |
| b->free_size += v.bytes; |
| total_.frees++; |
| total_.free_size += v.bytes; |
| } |
| } |
| |
| bool HeapProfileTable::FindAlloc(const void* ptr, size_t* object_size) const { |
| const AllocValue* alloc_value = address_map_->Find(ptr); |
| if (alloc_value != NULL) *object_size = alloc_value->bytes; |
| return alloc_value != NULL; |
| } |
| |
| bool HeapProfileTable::FindAllocDetails(const void* ptr, |
| AllocInfo* info) const { |
| const AllocValue* alloc_value = address_map_->Find(ptr); |
| if (alloc_value != NULL) { |
| info->object_size = alloc_value->bytes; |
| info->call_stack = alloc_value->bucket()->stack; |
| info->stack_depth = alloc_value->bucket()->depth; |
| } |
| return alloc_value != NULL; |
| } |
| |
| bool HeapProfileTable::FindInsideAlloc(const void* ptr, |
| size_t max_size, |
| const void** object_ptr, |
| size_t* object_size) const { |
| const AllocValue* alloc_value = |
| address_map_->FindInside(&AllocValueSize, max_size, ptr, object_ptr); |
| if (alloc_value != NULL) *object_size = alloc_value->bytes; |
| return alloc_value != NULL; |
| } |
| |
| bool HeapProfileTable::MarkAsLive(const void* ptr) { |
| AllocValue* alloc = address_map_->FindMutable(ptr); |
| if (alloc && !alloc->live()) { |
| alloc->set_live(true); |
| return true; |
| } |
| return false; |
| } |
| |
| void HeapProfileTable::MarkAsIgnored(const void* ptr) { |
| AllocValue* alloc = address_map_->FindMutable(ptr); |
| if (alloc) { |
| alloc->set_ignore(true); |
| } |
| } |
| |
| // We'd be happier using snprintfer, but we don't to reduce dependencies. |
| int HeapProfileTable::UnparseBucket(const Bucket& b, |
| char* buf, int buflen, int bufsize, |
| const char* extra, |
| Stats* profile_stats) { |
| if (profile_stats != NULL) { |
| profile_stats->allocs += b.allocs; |
| profile_stats->alloc_size += b.alloc_size; |
| profile_stats->frees += b.frees; |
| profile_stats->free_size += b.free_size; |
| } |
| int printed = |
| snprintf(buf + buflen, bufsize - buflen, "%6d: %8" PRId64 " [%6d: %8" PRId64 "] @%s", |
| b.allocs - b.frees, |
| b.alloc_size - b.free_size, |
| b.allocs, |
| b.alloc_size, |
| extra); |
| // If it looks like the snprintf failed, ignore the fact we printed anything |
| if (printed < 0 || printed >= bufsize - buflen) return buflen; |
| buflen += printed; |
| for (int d = 0; d < b.depth; d++) { |
| printed = snprintf(buf + buflen, bufsize - buflen, " 0x%08" PRIxPTR, |
| reinterpret_cast<uintptr_t>(b.stack[d])); |
| if (printed < 0 || printed >= bufsize - buflen) return buflen; |
| buflen += printed; |
| } |
| printed = snprintf(buf + buflen, bufsize - buflen, "\n"); |
| if (printed < 0 || printed >= bufsize - buflen) return buflen; |
| buflen += printed; |
| return buflen; |
| } |
| |
| HeapProfileTable::Bucket** |
| HeapProfileTable::MakeSortedBucketList() const { |
| Bucket** list = static_cast<Bucket**>(alloc_(sizeof(Bucket) * num_buckets_)); |
| |
| int bucket_count = 0; |
| for (int i = 0; i < kHashTableSize; i++) { |
| for (Bucket* curr = bucket_table_[i]; curr != 0; curr = curr->next) { |
| list[bucket_count++] = curr; |
| } |
| } |
| RAW_DCHECK(bucket_count == num_buckets_, ""); |
| |
| sort(list, list + num_buckets_, ByAllocatedSpace); |
| |
| return list; |
| } |
| |
| void HeapProfileTable::IterateOrderedAllocContexts( |
| AllocContextIterator callback) const { |
| Bucket** list = MakeSortedBucketList(); |
| AllocContextInfo info; |
| for (int i = 0; i < num_buckets_; ++i) { |
| *static_cast<Stats*>(&info) = *static_cast<Stats*>(list[i]); |
| info.stack_depth = list[i]->depth; |
| info.call_stack = list[i]->stack; |
| callback(info); |
| } |
| dealloc_(list); |
| } |
| |
| int HeapProfileTable::FillOrderedProfile(char buf[], int size) const { |
| Bucket** list = MakeSortedBucketList(); |
| |
| // Our file format is "bucket, bucket, ..., bucket, proc_self_maps_info". |
| // In the cases buf is too small, we'd rather leave out the last |
| // buckets than leave out the /proc/self/maps info. To ensure that, |
| // we actually print the /proc/self/maps info first, then move it to |
| // the end of the buffer, then write the bucket info into whatever |
| // is remaining, and then move the maps info one last time to close |
| // any gaps. Whew! |
| int map_length = snprintf(buf, size, "%s", kProcSelfMapsHeader); |
| if (map_length < 0 || map_length >= size) return 0; |
| bool dummy; // "wrote_all" -- did /proc/self/maps fit in its entirety? |
| map_length += FillProcSelfMaps(buf + map_length, size - map_length, &dummy); |
| RAW_DCHECK(map_length <= size, ""); |
| char* const map_start = buf + size - map_length; // move to end |
| memmove(map_start, buf, map_length); |
| size -= map_length; |
| |
| Stats stats; |
| memset(&stats, 0, sizeof(stats)); |
| int bucket_length = snprintf(buf, size, "%s", kProfileHeader); |
| if (bucket_length < 0 || bucket_length >= size) return 0; |
| bucket_length = UnparseBucket(total_, buf, bucket_length, size, |
| " heapprofile", &stats); |
| |
| // Dump the mmap list first. |
| if (profile_mmap_) { |
| BufferArgs buffer(buf, bucket_length, size); |
| MemoryRegionMap::IterateBuckets<BufferArgs*>(DumpBucketIterator, &buffer); |
| bucket_length = buffer.buflen; |
| } |
| |
| for (int i = 0; i < num_buckets_; i++) { |
| bucket_length = UnparseBucket(*list[i], buf, bucket_length, size, "", |
| &stats); |
| } |
| RAW_DCHECK(bucket_length < size, ""); |
| |
| dealloc_(list); |
| |
| RAW_DCHECK(buf + bucket_length <= map_start, ""); |
| memmove(buf + bucket_length, map_start, map_length); // close the gap |
| |
| return bucket_length + map_length; |
| } |
| |
| // static |
| void HeapProfileTable::DumpBucketIterator(const Bucket* bucket, |
| BufferArgs* args) { |
| args->buflen = UnparseBucket(*bucket, args->buf, args->buflen, args->bufsize, |
| "", NULL); |
| } |
| |
| inline |
| void HeapProfileTable::DumpNonLiveIterator(const void* ptr, AllocValue* v, |
| const DumpArgs& args) { |
| if (v->live()) { |
| v->set_live(false); |
| return; |
| } |
| if (v->ignore()) { |
| return; |
| } |
| Bucket b; |
| memset(&b, 0, sizeof(b)); |
| b.allocs = 1; |
| b.alloc_size = v->bytes; |
| b.depth = v->bucket()->depth; |
| b.stack = v->bucket()->stack; |
| char buf[1024]; |
| int len = UnparseBucket(b, buf, 0, sizeof(buf), "", args.profile_stats); |
| RawWrite(args.fd, buf, len); |
| } |
| |
| // Callback from NonLiveSnapshot; adds entry to arg->dest |
| // if not the entry is not live and is not present in arg->base. |
| void HeapProfileTable::AddIfNonLive(const void* ptr, AllocValue* v, |
| AddNonLiveArgs* arg) { |
| if (v->live()) { |
| v->set_live(false); |
| } else { |
| if (arg->base != NULL && arg->base->map_.Find(ptr) != NULL) { |
| // Present in arg->base, so do not save |
| } else { |
| arg->dest->Add(ptr, *v); |
| } |
| } |
| } |
| |
| bool HeapProfileTable::WriteProfile(const char* file_name, |
| const Bucket& total, |
| AllocationMap* allocations) { |
| RAW_VLOG(1, "Dumping non-live heap profile to %s", file_name); |
| RawFD fd = RawOpenForWriting(file_name); |
| if (fd != kIllegalRawFD) { |
| RawWrite(fd, kProfileHeader, strlen(kProfileHeader)); |
| char buf[512]; |
| int len = UnparseBucket(total, buf, 0, sizeof(buf), " heapprofile", |
| NULL); |
| RawWrite(fd, buf, len); |
| const DumpArgs args(fd, NULL); |
| allocations->Iterate<const DumpArgs&>(DumpNonLiveIterator, args); |
| RawWrite(fd, kProcSelfMapsHeader, strlen(kProcSelfMapsHeader)); |
| DumpProcSelfMaps(fd); |
| RawClose(fd); |
| return true; |
| } else { |
| RAW_LOG(ERROR, "Failed dumping filtered heap profile to %s", file_name); |
| return false; |
| } |
| } |
| |
| void HeapProfileTable::CleanupOldProfiles(const char* prefix) { |
| if (!FLAGS_cleanup_old_heap_profiles) |
| return; |
| string pattern = string(prefix) + ".*" + kFileExt; |
| #if defined(HAVE_GLOB_H) |
| glob_t g; |
| const int r = glob(pattern.c_str(), GLOB_ERR, NULL, &g); |
| if (r == 0 || r == GLOB_NOMATCH) { |
| const int prefix_length = strlen(prefix); |
| for (int i = 0; i < g.gl_pathc; i++) { |
| const char* fname = g.gl_pathv[i]; |
| if ((strlen(fname) >= prefix_length) && |
| (memcmp(fname, prefix, prefix_length) == 0)) { |
| RAW_VLOG(1, "Removing old heap profile %s", fname); |
| unlink(fname); |
| } |
| } |
| } |
| globfree(&g); |
| #else /* HAVE_GLOB_H */ |
| RAW_LOG(WARNING, "Unable to remove old heap profiles (can't run glob())"); |
| #endif |
| } |
| |
| HeapProfileTable::Snapshot* HeapProfileTable::TakeSnapshot() { |
| Snapshot* s = new (alloc_(sizeof(Snapshot))) Snapshot(alloc_, dealloc_); |
| address_map_->Iterate(AddToSnapshot, s); |
| return s; |
| } |
| |
| void HeapProfileTable::ReleaseSnapshot(Snapshot* s) { |
| s->~Snapshot(); |
| dealloc_(s); |
| } |
| |
| // Callback from TakeSnapshot; adds a single entry to snapshot |
| void HeapProfileTable::AddToSnapshot(const void* ptr, AllocValue* v, |
| Snapshot* snapshot) { |
| snapshot->Add(ptr, *v); |
| } |
| |
| HeapProfileTable::Snapshot* HeapProfileTable::NonLiveSnapshot( |
| Snapshot* base) { |
| RAW_VLOG(2, "NonLiveSnapshot input: %d %d\n", |
| int(total_.allocs - total_.frees), |
| int(total_.alloc_size - total_.free_size)); |
| |
| Snapshot* s = new (alloc_(sizeof(Snapshot))) Snapshot(alloc_, dealloc_); |
| AddNonLiveArgs args; |
| args.dest = s; |
| args.base = base; |
| address_map_->Iterate<AddNonLiveArgs*>(AddIfNonLive, &args); |
| RAW_VLOG(2, "NonLiveSnapshot output: %d %d\n", |
| int(s->total_.allocs - s->total_.frees), |
| int(s->total_.alloc_size - s->total_.free_size)); |
| return s; |
| } |
| |
| // Information kept per unique bucket seen |
| struct HeapProfileTable::Snapshot::Entry { |
| int count; |
| int bytes; |
| Bucket* bucket; |
| Entry() : count(0), bytes(0) { } |
| |
| // Order by decreasing bytes |
| bool operator<(const Entry& x) const { |
| return this->bytes > x.bytes; |
| } |
| }; |
| |
| // State used to generate leak report. We keep a mapping from Bucket pointer |
| // the collected stats for that bucket. |
| struct HeapProfileTable::Snapshot::ReportState { |
| map<Bucket*, Entry> buckets_; |
| }; |
| |
| // Callback from ReportLeaks; updates ReportState. |
| void HeapProfileTable::Snapshot::ReportCallback(const void* ptr, |
| AllocValue* v, |
| ReportState* state) { |
| Entry* e = &state->buckets_[v->bucket()]; // Creates empty Entry first time |
| e->bucket = v->bucket(); |
| e->count++; |
| e->bytes += v->bytes; |
| } |
| |
| void HeapProfileTable::Snapshot::ReportLeaks(const char* checker_name, |
| const char* filename, |
| bool should_symbolize) { |
| // This is only used by the heap leak checker, but is intimately |
| // tied to the allocation map that belongs in this module and is |
| // therefore placed here. |
| RAW_LOG(ERROR, "Leak check %s detected leaks of %" PRIuS " bytes " |
| "in %" PRIuS " objects", |
| checker_name, |
| size_t(total_.alloc_size), |
| size_t(total_.allocs)); |
| |
| // Group objects by Bucket |
| ReportState state; |
| map_.Iterate(&ReportCallback, &state); |
| |
| // Sort buckets by decreasing leaked size |
| const int n = state.buckets_.size(); |
| Entry* entries = new Entry[n]; |
| int dst = 0; |
| for (map<Bucket*,Entry>::const_iterator iter = state.buckets_.begin(); |
| iter != state.buckets_.end(); |
| ++iter) { |
| entries[dst++] = iter->second; |
| } |
| sort(entries, entries + n); |
| |
| // Report a bounded number of leaks to keep the leak report from |
| // growing too long. |
| const int to_report = |
| (FLAGS_heap_check_max_leaks > 0 && |
| n > FLAGS_heap_check_max_leaks) ? FLAGS_heap_check_max_leaks : n; |
| RAW_LOG(ERROR, "The %d largest leaks:", to_report); |
| |
| // Print |
| SymbolTable symbolization_table; |
| for (int i = 0; i < to_report; i++) { |
| const Entry& e = entries[i]; |
| for (int j = 0; j < e.bucket->depth; j++) { |
| symbolization_table.Add(e.bucket->stack[j]); |
| } |
| } |
| static const int kBufSize = 2<<10; |
| char buffer[kBufSize]; |
| if (should_symbolize) |
| symbolization_table.Symbolize(); |
| for (int i = 0; i < to_report; i++) { |
| const Entry& e = entries[i]; |
| base::RawPrinter printer(buffer, kBufSize); |
| printer.Printf("Leak of %d bytes in %d objects allocated from:\n", |
| e.bytes, e.count); |
| for (int j = 0; j < e.bucket->depth; j++) { |
| const void* pc = e.bucket->stack[j]; |
| printer.Printf("\t@ %" PRIxPTR " %s\n", |
| reinterpret_cast<uintptr_t>(pc), symbolization_table.GetSymbol(pc)); |
| } |
| RAW_LOG(ERROR, "%s", buffer); |
| } |
| |
| if (to_report < n) { |
| RAW_LOG(ERROR, "Skipping leaks numbered %d..%d", |
| to_report, n-1); |
| } |
| delete[] entries; |
| |
| // TODO: Dump the sorted Entry list instead of dumping raw data? |
| // (should be much shorter) |
| if (!HeapProfileTable::WriteProfile(filename, total_, &map_)) { |
| RAW_LOG(ERROR, "Could not write pprof profile to %s", filename); |
| } |
| } |
| |
| void HeapProfileTable::Snapshot::ReportObject(const void* ptr, |
| AllocValue* v, |
| char* unused) { |
| // Perhaps also log the allocation stack trace (unsymbolized) |
| // on this line in case somebody finds it useful. |
| RAW_LOG(ERROR, "leaked %" PRIuS " byte object %p", v->bytes, ptr); |
| } |
| |
| void HeapProfileTable::Snapshot::ReportIndividualObjects() { |
| char unused; |
| map_.Iterate(ReportObject, &unused); |
| } |