366 changes: 366 additions & 0 deletions src/llscan.cc
Original file line number Diff line number Diff line change
@@ -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;
}
}
129 changes: 129 additions & 0 deletions src/llscan.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#ifndef SRC_LLSCAN_H_
#define SRC_LLSCAN_H_

#include <lldb/API/LLDB.h>
#include <map>
#include <set>

namespace llnode {

class FindObjectsCmd : public CommandBase {
public:
~FindObjectsCmd() override{};

bool DoExecute(lldb::SBDebugger d, char** cmd,
lldb::SBCommandReturnObject& result) override;
};

class FindInstancesCmd : public CommandBase {
public:
~FindInstancesCmd() override{};

bool DoExecute(lldb::SBDebugger d, char** cmd,
lldb::SBCommandReturnObject& result) override;

private:
bool detailed_;
};

class MemoryVisitor {
public:
virtual ~MemoryVisitor(){};

virtual uint64_t Visit(uint64_t location, uint64_t available) = 0;
};

class TypeRecord {
public:
TypeRecord(std::string& type_name)
: type_name_(type_name), instance_count_(0), total_instance_size_(0){};

inline std::string& GetTypeName() { return type_name_; };
inline uint64_t GetInstanceCount() { return instance_count_; };
inline uint64_t GetTotalInstanceSize() { return total_instance_size_; };
inline std::set<uint64_t>& GetInstances() { return instances_; };

inline void AddInstance(uint64_t address, uint64_t size) {
instances_.insert(address);
instance_count_++;
total_instance_size_ += size;
};

/* Sort records by instance count, use the other fields as tie breakers
* to give consistent ordering.
*/
static bool CompareInstanceCounts(TypeRecord* a, TypeRecord* b) {
if (a->instance_count_ == b->instance_count_) {
if (a->total_instance_size_ == b->total_instance_size_) {
return a->type_name_ < b->type_name_;
}
return a->total_instance_size_ < b->total_instance_size_;
}
return a->instance_count_ < b->instance_count_;
}


private:
std::string type_name_;
uint64_t instance_count_;
uint64_t total_instance_size_;
std::set<uint64_t> instances_;
};

typedef std::map<std::string, TypeRecord*> TypeRecordMap;

class FindJSObjectsVisitor : MemoryVisitor {
public:
FindJSObjectsVisitor(lldb::SBTarget& target, TypeRecordMap& mapstoinstances);
~FindJSObjectsVisitor() {}

uint64_t Visit(uint64_t location, uint64_t available);

uint32_t FoundCount() { return found_count_; }

private:
bool IsAHistogramType(v8::HeapObject& heap_object, v8::Error err);

lldb::SBTarget& target_;
uint32_t address_byte_size_;
uint32_t found_count_;

TypeRecordMap& mapstoinstances_;
};


class LLScan {
public:
LLScan(){};

bool ScanHeapForObjects(lldb::SBTarget target,
lldb::SBCommandReturnObject& result);
bool GenerateMemoryRanges(lldb::SBTarget target,
const char* segmentsfilename);

inline TypeRecordMap& GetMapsToInstances() { return mapstoinstances_; };

private:
void ScanMemoryRanges(FindJSObjectsVisitor& v);
void ClearMemoryRanges();

class MemoryRange {
public:
MemoryRange(uint64_t start, uint64_t length)
: start_(start), length_(length), next_(nullptr){};

uint64_t start_;
uint64_t length_;
MemoryRange* next_;
};

lldb::SBTarget target_;
lldb::SBProcess process_;
MemoryRange* ranges_ = nullptr;
TypeRecordMap mapstoinstances_;
};

} // llnode


#endif // SRC_LLSCAN_H_
96 changes: 96 additions & 0 deletions src/llv8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,20 @@ std::string Value::Inspect(InspectOptions* options, Error& err) {
}


std::string Value::GetTypeName(InspectOptions* options, Error& err) {
Smi smi(this);
if (smi.Check()) return "(Smi)";

HeapObject obj(this);
if (!obj.Check()) {
err = Error::Failure("Not object and not smi");
return std::string();
}

return obj.GetTypeName(options, err);
}


std::string Value::ToString(Error& err) {
Smi smi(this);
if (smi.Check()) return smi.ToString(err);
Expand Down Expand Up @@ -672,6 +686,88 @@ std::string Smi::ToString(Error& err) {
}


/* Utility function to generate short type names for objects.
*/
std::string HeapObject::GetTypeName(InspectOptions* options, Error& err) {
int64_t type = GetType(err);
if (type == v8()->types()->kGlobalObjectType) return "(Global)";
if (type == v8()->types()->kCodeType) return "(Code)";
if (type == v8()->types()->kMapType) {
return "(Map)";
}

if (type == v8()->types()->kJSObjectType) {
v8::HeapObject map_obj = GetMap(err);
if (err.Fail()) {
return std::string();
}

v8::Map map(map_obj);
v8::HeapObject constructor_obj = map.Constructor(err);
if (err.Fail()) {
return std::string();
}

int64_t constructor_type = constructor_obj.GetType(err);
if (err.Fail()) {
return std::string();
}

if (constructor_type != v8()->types()->kJSFunctionType) {
return "(Object)";
}

v8::JSFunction constructor(constructor_obj);

return constructor.Name(err);
}

if (type == v8()->types()->kHeapNumberType) {
return "(HeapNumber)";
}

if (type == v8()->types()->kJSArrayType) {
return "(Array)";
}

if (type == v8()->types()->kOddballType) {
return "(Oddball)";
}

if (type == v8()->types()->kJSFunctionType) {
return "(Function)";
}

if (type == v8()->types()->kJSRegExpType) {
return "(RegExp)";
}

if (type < v8()->types()->kFirstNonstringType) {
return "(String)";
}

if (type == v8()->types()->kFixedArrayType) {
return "(FixedArray)";
}

if (type == v8()->types()->kJSArrayBufferType) {
return "(ArrayBuffer)";
}

if (type == v8()->types()->kJSTypedArrayType) {
return "(ArrayBufferView)";
}

if (type == v8()->types()->kJSDateType) {
return "(Date)";
}

std::string unknown("unknown: ");

return unknown + std::to_string(type);
}


std::string Smi::Inspect(Error& err) { return "<Smi: " + ToString(err) + ">"; }


Expand Down
6 changes: 6 additions & 0 deletions src/llv8.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include "src/llv8-constants.h"

namespace llnode {

class FindJSObjectsVisitor;

namespace v8 {

// Forward declarations
Expand Down Expand Up @@ -67,6 +70,7 @@ class Value {
bool IsHole(Error& err);

std::string Inspect(InspectOptions* options, Error& err);
std::string GetTypeName(InspectOptions* options, Error& err);
std::string ToString(Error& err);

protected:
Expand Down Expand Up @@ -101,6 +105,7 @@ class HeapObject : public Value {

std::string ToString(Error& err);
std::string Inspect(InspectOptions* options, Error& err);
std::string GetTypeName(InspectOptions* options, Error& err);
};

class Map : public HeapObject {
Expand Down Expand Up @@ -476,6 +481,7 @@ class LLV8 {
friend class JSRegExp;
friend class JSDate;
friend class CodeMap;
friend class llnode::FindJSObjectsVisitor;
};

#undef V8_VALUE_DEFAULT_METHODS
Expand Down