View
@@ -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;
+}
+}
View
@@ -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_
View
@@ -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);
@@ -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) + ">"; }
View
@@ -8,6 +8,9 @@
#include "src/llv8-constants.h"
namespace llnode {
+
+class FindJSObjectsVisitor;
+
namespace v8 {
// Forward declarations
@@ -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:
@@ -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 {
@@ -476,6 +481,7 @@ class LLV8 {
friend class JSRegExp;
friend class JSDate;
friend class CodeMap;
+ friend class llnode::FindJSObjectsVisitor;
};
#undef V8_VALUE_DEFAULT_METHODS