|
|
@@ -0,0 +1,366 @@ |
|
|
#include <errno.h> |
|
|
#include <inttypes.h> |
|
|
#include <stdlib.h> |
|
|
#include <string.h> |
|
|
|
|
|
#include <algorithm> |
|
|
#include <fstream> |
|
|
#include <vector> |
|
|
|
|
|
#include <lldb/API/SBExpressionOptions.h> |
|
|
|
|
|
#include "src/llnode.h" |
|
|
#include "src/llscan.h" |
|
|
#include "src/llv8-inl.h" |
|
|
#include "src/llv8.h" |
|
|
|
|
|
namespace llnode { |
|
|
|
|
|
// Defined in llnode.cc |
|
|
extern v8::LLV8 llv8; |
|
|
|
|
|
LLScan llscan; |
|
|
|
|
|
using namespace lldb; |
|
|
|
|
|
|
|
|
bool FindObjectsCmd::DoExecute(SBDebugger d, char** cmd, |
|
|
SBCommandReturnObject& result) { |
|
|
SBTarget target = d.GetSelectedTarget(); |
|
|
if (!target.IsValid()) { |
|
|
result.SetError("No valid process, please start something\n"); |
|
|
return false; |
|
|
} |
|
|
|
|
|
/* Ensure we have a map of objects. */ |
|
|
if (!llscan.ScanHeapForObjects(target, result)) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
/* Create a vector to hold the entries sorted by instance count |
|
|
* TODO(hhellyer) - Make sort type an option (by count, size or name) |
|
|
*/ |
|
|
std::vector<TypeRecord*> sorted_by_count; |
|
|
TypeRecordMap::iterator end = llscan.GetMapsToInstances().end(); |
|
|
for (TypeRecordMap::iterator it = llscan.GetMapsToInstances().begin(); |
|
|
it != end; ++it) { |
|
|
sorted_by_count.push_back(it->second); |
|
|
} |
|
|
|
|
|
std::sort(sorted_by_count.begin(), sorted_by_count.end(), |
|
|
TypeRecord::CompareInstanceCounts); |
|
|
|
|
|
uint64_t total_objects = 0; |
|
|
|
|
|
result.Printf(" Instances Total Size Name\n"); |
|
|
result.Printf(" ---------- ---------- ----\n"); |
|
|
|
|
|
for (std::vector<TypeRecord*>::iterator it = sorted_by_count.begin(); |
|
|
it != sorted_by_count.end(); ++it) { |
|
|
TypeRecord* t = *it; |
|
|
result.Printf(" %10" PRId64 " %10" PRId64 " %s\n", t->GetInstanceCount(), |
|
|
t->GetTotalInstanceSize(), t->GetTypeName().c_str()); |
|
|
total_objects += t->GetInstanceCount(); |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
bool FindInstancesCmd::DoExecute(SBDebugger d, char** cmd, |
|
|
SBCommandReturnObject& result) { |
|
|
SBTarget target = d.GetSelectedTarget(); |
|
|
if (!target.IsValid()) { |
|
|
result.SetError("No valid process, please start something\n"); |
|
|
return false; |
|
|
} |
|
|
|
|
|
v8::Value::InspectOptions inspect_options; |
|
|
|
|
|
inspect_options.detailed = detailed_; |
|
|
|
|
|
char** start = ParseInspectOptions(cmd, &inspect_options); |
|
|
|
|
|
std::string full_cmd; |
|
|
for (; start != nullptr && *start != nullptr; start++) full_cmd += *start; |
|
|
|
|
|
std::string type_name = full_cmd; |
|
|
|
|
|
// Load V8 constants from postmortem data |
|
|
llv8.Load(target); |
|
|
|
|
|
TypeRecordMap::iterator instance_it = |
|
|
llscan.GetMapsToInstances().find(type_name); |
|
|
if (instance_it != llscan.GetMapsToInstances().end()) { |
|
|
TypeRecord* t = instance_it->second; |
|
|
for (std::set<uint64_t>::iterator it = t->GetInstances().begin(); |
|
|
it != t->GetInstances().end(); ++it) { |
|
|
v8::Error err; |
|
|
v8::Value v8_value(&llv8, *it); |
|
|
std::string res = v8_value.Inspect(&inspect_options, err); |
|
|
result.Printf("%s\n", res.c_str()); |
|
|
} |
|
|
|
|
|
} else { |
|
|
result.Printf("No objects found with type name %s\n", type_name.c_str()); |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
FindJSObjectsVisitor::FindJSObjectsVisitor(SBTarget& target, |
|
|
TypeRecordMap& mapstoinstances) |
|
|
: target_(target), mapstoinstances_(mapstoinstances) { |
|
|
found_count_ = 0; |
|
|
address_byte_size_ = target_.GetProcess().GetAddressByteSize(); |
|
|
// Load V8 constants from postmortem data |
|
|
llv8.Load(target); |
|
|
} |
|
|
|
|
|
|
|
|
/* Visit every address, a bit brute force but it works. */ |
|
|
uint64_t FindJSObjectsVisitor::Visit(uint64_t location, uint64_t available) { |
|
|
lldb::SBError error; |
|
|
|
|
|
// Test if the map points to a real map. |
|
|
// Try to create an object out of it. |
|
|
|
|
|
uint64_t word = target_.GetProcess().ReadUnsignedFromMemory( |
|
|
location, address_byte_size_, error); |
|
|
|
|
|
v8::Value v8_value(&llv8, word); |
|
|
|
|
|
v8::Error err; |
|
|
// Test if this is SMI |
|
|
// Skip inspecting things that look like Smi's, they aren't objects. |
|
|
v8::Smi smi(v8_value); |
|
|
if (smi.Check()) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
v8::HeapObject heap_object(v8_value); |
|
|
if (!heap_object.Check()) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
if (heap_object.IsHoleOrUndefined(err)) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
if (err.Fail()) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
v8::HeapObject map_object = heap_object.GetMap(err); |
|
|
if (err.Fail() || !map_object.Check()) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
if (!IsAHistogramType(heap_object, err)) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
if (err.Fail()) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
v8::Map map(map_object); |
|
|
|
|
|
v8::Value::InspectOptions inspect_options; |
|
|
inspect_options.detailed = false; |
|
|
inspect_options.print_map = false; |
|
|
inspect_options.string_length = 0; |
|
|
|
|
|
std::string type_name = heap_object.GetTypeName(&inspect_options, err); |
|
|
|
|
|
/* No entry in the map, create a new one. */ |
|
|
if (mapstoinstances_.count(type_name) == 0) { |
|
|
TypeRecord* t = new TypeRecord(type_name); |
|
|
|
|
|
t->AddInstance(word, map.InstanceSize(err)); |
|
|
mapstoinstances_.insert(std::pair<std::string, TypeRecord*>(type_name, t)); |
|
|
|
|
|
} else { |
|
|
/* Update an existing instance, if we haven't seen this instance before. */ |
|
|
TypeRecord* t = mapstoinstances_.at(type_name); |
|
|
/* Determine if this is a new instance. |
|
|
* (We are scanning pointers to objects, we may have seen this location |
|
|
* before.) |
|
|
*/ |
|
|
if (t->GetInstances().count(word) == 0) { |
|
|
t->AddInstance(word, map.InstanceSize(err)); |
|
|
} |
|
|
} |
|
|
|
|
|
if (err.Fail()) { |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
found_count_++; |
|
|
|
|
|
/* Just advance one word. |
|
|
* (Should advance by object size, assuming objects can't overlap!) |
|
|
*/ |
|
|
return address_byte_size_; |
|
|
} |
|
|
|
|
|
|
|
|
bool FindJSObjectsVisitor::IsAHistogramType(v8::HeapObject& heap_object, |
|
|
v8::Error err) { |
|
|
int64_t type = heap_object.GetType(err); |
|
|
v8::LLV8* v8 = heap_object.v8(); |
|
|
if (type == v8->types()->kJSObjectType) return true; |
|
|
if (type == v8->types()->kJSArrayType) return true; |
|
|
if (type == v8->types()->kJSTypedArrayType) return true; |
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
bool LLScan::ScanHeapForObjects(lldb::SBTarget target, |
|
|
lldb::SBCommandReturnObject& result) { |
|
|
/* TODO(hhellyer) - Check whether we have the SBGetMemoryRegionInfoList API |
|
|
* available |
|
|
* and implemented. |
|
|
* (It may be available but not supported on this platform.) |
|
|
* If it is then check the last scan is still valid - the process hasn't moved |
|
|
* and we haven't changed target. |
|
|
*/ |
|
|
|
|
|
// Reload process anyway |
|
|
process_ = target.GetProcess(); |
|
|
|
|
|
// Need to reload memory ranges (though this does assume the user has also |
|
|
// updated |
|
|
// LLNODE_RANGESFILE with data for the new dump or things won't match up). |
|
|
if (target_ != target) { |
|
|
ClearMemoryRanges(); |
|
|
} |
|
|
|
|
|
/* Fall back to environment variable containing pre-parsed list of memory |
|
|
* ranges. */ |
|
|
if (nullptr == ranges_) { |
|
|
const char* segmentsfilename = getenv("LLNODE_RANGESFILE"); |
|
|
|
|
|
if (segmentsfilename == nullptr) { |
|
|
result.SetError( |
|
|
"No memory range information available for this process. Cannot scan " |
|
|
"for objects.\n" |
|
|
"Please set `LLNODE_RANGESFILE` environment variable\n"); |
|
|
return false; |
|
|
} |
|
|
|
|
|
if (!GenerateMemoryRanges(target, segmentsfilename)) { |
|
|
result.SetError( |
|
|
"No memory range information available for this process. Cannot scan " |
|
|
"for objects.\n"); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
/* If we've reached here we have access to information about the valid memory |
|
|
* ranges in the |
|
|
* process and can scan for objects. |
|
|
*/ |
|
|
|
|
|
/* Populate the map of objects. */ |
|
|
if (mapstoinstances_.empty()) { |
|
|
FindJSObjectsVisitor v(target, GetMapsToInstances()); |
|
|
|
|
|
ScanMemoryRanges(v); |
|
|
} |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
void LLScan::ScanMemoryRanges(FindJSObjectsVisitor& v) { |
|
|
MemoryRange* head = ranges_; |
|
|
|
|
|
bool done = false; |
|
|
|
|
|
while (head != nullptr && !done) { |
|
|
uint64_t address = head->start_; |
|
|
uint64_t len = head->length_; |
|
|
head = head->next_; |
|
|
|
|
|
/* Brute force search - query every address - but allow the visitor code to |
|
|
* say |
|
|
* how far to move on so we don't read every byte. |
|
|
*/ |
|
|
for (auto searchAddress = address; searchAddress < address + len;) { |
|
|
uint32_t increment = |
|
|
v.Visit(searchAddress, (address + len) - searchAddress); |
|
|
if (increment == 0) { |
|
|
done = true; |
|
|
break; |
|
|
} |
|
|
searchAddress += increment; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/* Read a file of memory ranges parsed from the core dump. |
|
|
* This is a work around for the lack of an API to get the memory ranges |
|
|
* within lldb. |
|
|
* There are scripts for generating this file on Mac and Linux stored in |
|
|
* the scripts directory of the llnode repository. |
|
|
* Export the name or full path to the ranges file in the LLNODE_RANGESFILE |
|
|
* env var before starting lldb and loading the llnode plugin. |
|
|
*/ |
|
|
bool LLScan::GenerateMemoryRanges(lldb::SBTarget target, |
|
|
const char* segmentsfilename) { |
|
|
std::ifstream input(segmentsfilename); |
|
|
|
|
|
if (!input.is_open()) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
uint64_t address = 0; |
|
|
uint64_t len = 0; |
|
|
|
|
|
MemoryRange** tailptr = &ranges_; |
|
|
|
|
|
lldb::addr_t address_byte_size = target.GetProcess().GetAddressByteSize(); |
|
|
|
|
|
while (input >> std::hex >> address >> std::hex >> len) { |
|
|
/* Check if the range is accessible. |
|
|
* The structure of a core file means if you check the start and the end of |
|
|
* a range then the middle will be there, ranges are contiguous in the file, |
|
|
* but cores often get truncated due to file size limits so ranges can be |
|
|
* missing or truncated. Sometimes shared memory segments are omitted so |
|
|
* it's also possible an entire section could be missing from the middle. |
|
|
*/ |
|
|
lldb::SBError error; |
|
|
|
|
|
target.GetProcess().ReadPointerFromMemory(address, error); |
|
|
if (!error.Success()) { |
|
|
/* Could not access first word, skip. */ |
|
|
continue; |
|
|
} |
|
|
|
|
|
target.GetProcess().ReadPointerFromMemory( |
|
|
(address + len) - address_byte_size, error); |
|
|
if (!error.Success()) { |
|
|
/* Could not access last word, skip. */ |
|
|
continue; |
|
|
} |
|
|
|
|
|
MemoryRange* newRange = new MemoryRange(address, len); |
|
|
|
|
|
*tailptr = newRange; |
|
|
tailptr = &(newRange->next_); |
|
|
} |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
void LLScan::ClearMemoryRanges() { |
|
|
MemoryRange* head = ranges_; |
|
|
while (head != nullptr) { |
|
|
MemoryRange* range = head; |
|
|
head = head->next_; |
|
|
delete range; |
|
|
} |
|
|
ranges_ = nullptr; |
|
|
} |
|
|
} |