Skip to content

Commit

Permalink
Add HeapProfiler.lastSeenObjectId and HeapProfiler.heapStatsUpdate to…
Browse files Browse the repository at this point in the history
… inspector

Summary:
When taking a heap timeline, Hermes wasn't showing any data until the timeline
was written to disk and then reloaded.

Turns out we were missing support for two events:
* `HeapProfiler.lastSeenObjectId`: This event reports the most recently
allocated object ID. Used to know when objects were allocated in the
timeline.
* `HeapProfiler.heapStatsUpdate`: Report how many objects and bytes
exist for a "time fragment", represented by a fragment index. Later updates
to the same index can decrease the amount of live memory, which show up
as grey spikes instead of blue spikes

Previously, we only supported these by writing out to a file, and they didn't work
with a "live" profiling view. To fix this, I changed the periodic sampling thread
to instead be a periodic flush of a sample every few allocations. The performance
impact is tucked away only when profiling is turned on, and it's very non-invasive to
the rest of the GC. The flush calls a callback with the relevant information if the
inspector is on, and the inspector sends a message back to the browser.

Changelog: [Internal] Fix for Hermes heap timeline profiling

Reviewed By: neildhar

Differential Revision: D23993363

fbshipit-source-id: 8e0b571130cbb7e839dfb009b04f584f5179085d
  • Loading branch information
dulinriley authored and facebook-github-bot committed Oct 10, 2020
1 parent 9a7f2b5 commit d8b0e9d
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 12 deletions.
52 changes: 46 additions & 6 deletions ReactCommon/hermes/inspector/chrome/Connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,13 @@ void Connection::Impl::sendSnapshot(
message,
[this, reportProgress, stopStackTraceCapture](
const debugger::ProgramState &) {
// Stop taking any new traces before sending out the heap snapshot.
if (stopStackTraceCapture) {
getRuntime()
.instrumentation()
.stopTrackingHeapObjectStackTraces();
}

if (reportProgress) {
// A progress notification with finished = true indicates the
// snapshot has been captured and is ready to be sent. Our
Expand All @@ -492,11 +499,6 @@ void Connection::Impl::sendSnapshot(
});

getRuntime().instrumentation().createSnapshotToStream(cos);
if (stopStackTraceCapture) {
getRuntime()
.instrumentation()
.stopTrackingHeapObjectStackTraces();
}
})
.via(executor_.get())
.thenValue([this, reqId](auto &&) {
Expand All @@ -522,7 +524,45 @@ void Connection::Impl::handle(
->executeIfEnabled(
"HeapProfiler.startTrackingHeapObjects",
[this](const debugger::ProgramState &) {
getRuntime().instrumentation().startTrackingHeapObjectStackTraces();
getRuntime().instrumentation().startTrackingHeapObjectStackTraces(
[this](
uint64_t lastSeenObjectId,
std::chrono::microseconds timestamp,
std::vector<jsi::Instrumentation::HeapStatsUpdate> stats) {
// Send the last object ID notification first.
m::heapProfiler::LastSeenObjectIdNotification note;
note.lastSeenObjectId = lastSeenObjectId;
// The protocol uses milliseconds with a fraction for
// microseconds.
note.timestamp =
static_cast<double>(timestamp.count()) / 1000;
sendNotificationToClient(note);

m::heapProfiler::HeapStatsUpdateNotification heapStatsNote;
// Flatten the HeapStatsUpdate list.
heapStatsNote.statsUpdate.reserve(stats.size() * 3);
for (const jsi::Instrumentation::HeapStatsUpdate &fragment :
stats) {
// Each triplet is the fragment number, the total count of
// objects for the fragment, and the total size of objects
// for the fragment.
heapStatsNote.statsUpdate.push_back(
static_cast<int>(std::get<0>(fragment)));
heapStatsNote.statsUpdate.push_back(
static_cast<int>(std::get<1>(fragment)));
heapStatsNote.statsUpdate.push_back(
static_cast<int>(std::get<2>(fragment)));
}
assert(
heapStatsNote.statsUpdate.size() == stats.size() * 3 &&
"Should be exactly 3x the stats vector");
// TODO: Chunk this if there are too many fragments to update.
// Unlikely to be a problem in practice unless there's a huge
// amount of allocation and freeing.
sendNotificationToClient(heapStatsNote);
});
// At this point we need the equivalent of a setInterval, where each
// interval samples the existing
})
.via(executor_.get())
.thenValue(
Expand Down
48 changes: 47 additions & 1 deletion ReactCommon/hermes/inspector/chrome/MessageTypes.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// @generated SignedSource<<addf39d6b92b8dc857e6e5ffe2a441d4>>
// @generated SignedSource<<80b4da5580a1fe18b9551723bdf7cab0>>

#include "MessageTypes.h"

Expand Down Expand Up @@ -1126,6 +1126,52 @@ dynamic heapProfiler::AddHeapSnapshotChunkNotification::toDynamic() const {
return obj;
}

heapProfiler::HeapStatsUpdateNotification::HeapStatsUpdateNotification()
: Notification("HeapProfiler.heapStatsUpdate") {}

heapProfiler::HeapStatsUpdateNotification::HeapStatsUpdateNotification(
const dynamic &obj)
: Notification("HeapProfiler.heapStatsUpdate") {
assign(method, obj, "method");

dynamic params = obj.at("params");
assign(statsUpdate, params, "statsUpdate");
}

dynamic heapProfiler::HeapStatsUpdateNotification::toDynamic() const {
dynamic params = dynamic::object;
put(params, "statsUpdate", statsUpdate);

dynamic obj = dynamic::object;
put(obj, "method", method);
put(obj, "params", std::move(params));
return obj;
}

heapProfiler::LastSeenObjectIdNotification::LastSeenObjectIdNotification()
: Notification("HeapProfiler.lastSeenObjectId") {}

heapProfiler::LastSeenObjectIdNotification::LastSeenObjectIdNotification(
const dynamic &obj)
: Notification("HeapProfiler.lastSeenObjectId") {
assign(method, obj, "method");

dynamic params = obj.at("params");
assign(lastSeenObjectId, params, "lastSeenObjectId");
assign(timestamp, params, "timestamp");
}

dynamic heapProfiler::LastSeenObjectIdNotification::toDynamic() const {
dynamic params = dynamic::object;
put(params, "lastSeenObjectId", lastSeenObjectId);
put(params, "timestamp", timestamp);

dynamic obj = dynamic::object;
put(obj, "method", method);
put(obj, "params", std::move(params));
return obj;
}

heapProfiler::ReportHeapSnapshotProgressNotification::
ReportHeapSnapshotProgressNotification()
: Notification("HeapProfiler.reportHeapSnapshotProgress") {}
Expand Down
21 changes: 20 additions & 1 deletion ReactCommon/hermes/inspector/chrome/MessageTypes.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// @generated SignedSource<<0563169b47d73a70d7540528f28d1d13>>
// @generated SignedSource<<7383ffe2d956ba3aef8262dbee2f3427>>

#pragma once

Expand Down Expand Up @@ -71,6 +71,8 @@ using UnserializableValue = std::string;

namespace heapProfiler {
struct AddHeapSnapshotChunkNotification;
struct HeapStatsUpdateNotification;
struct LastSeenObjectIdNotification;
struct ReportHeapSnapshotProgressNotification;
struct StartTrackingHeapObjectsRequest;
struct StopTrackingHeapObjectsRequest;
Expand Down Expand Up @@ -607,6 +609,23 @@ struct heapProfiler::AddHeapSnapshotChunkNotification : public Notification {
std::string chunk;
};

struct heapProfiler::HeapStatsUpdateNotification : public Notification {
HeapStatsUpdateNotification();
explicit HeapStatsUpdateNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;

std::vector<int> statsUpdate;
};

struct heapProfiler::LastSeenObjectIdNotification : public Notification {
LastSeenObjectIdNotification();
explicit LastSeenObjectIdNotification(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;

int lastSeenObjectId{};
double timestamp{};
};

struct heapProfiler::ReportHeapSnapshotProgressNotification
: public Notification {
ReportHeapSnapshotProgressNotification();
Expand Down
2 changes: 2 additions & 0 deletions ReactCommon/hermes/inspector/tools/message_types.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ HeapProfiler.reportHeapSnapshotProgress
HeapProfiler.takeHeapSnapshot
HeapProfiler.startTrackingHeapObjects
HeapProfiler.stopTrackingHeapObjects
HeapProfiler.heapStatsUpdate
HeapProfiler.lastSeenObjectId
Runtime.consoleAPICalled
Runtime.evaluate
Runtime.executionContextCreated
Expand Down
9 changes: 7 additions & 2 deletions ReactCommon/jsi/jsi/decorator.h
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,13 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation {
plain().instrumentation().collectGarbage(std::move(cause));
}

void startTrackingHeapObjectStackTraces() override {
plain().instrumentation().startTrackingHeapObjectStackTraces();
void startTrackingHeapObjectStackTraces(
std::function<void(
uint64_t,
std::chrono::microseconds,
std::vector<HeapStatsUpdate>)> callback) override {
plain().instrumentation().startTrackingHeapObjectStackTraces(
std::move(callback));
}

void stopTrackingHeapObjectStackTraces() override {
Expand Down
17 changes: 16 additions & 1 deletion ReactCommon/jsi/jsi/instrumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

#pragma once

#include <chrono>
#include <iosfwd>
#include <string>
#include <tuple>
#include <unordered_map>

#include <jsi/jsi.h>
Expand Down Expand Up @@ -54,9 +56,22 @@ class JSI_EXPORT Instrumentation {
/// logs.
virtual void collectGarbage(std::string cause) = 0;

/// A HeapStatsUpdate is a tuple of the fragment index, the number of objects
/// in that fragment, and the number of bytes used by those objects.
/// A "fragment" is a view of all objects allocated within a time slice.
using HeapStatsUpdate = std::tuple<uint64_t, uint64_t, uint64_t>;

/// Start capturing JS stack-traces for all JS heap allocated objects. These
/// can be accessed via \c ::createSnapshotToFile().
virtual void startTrackingHeapObjectStackTraces() = 0;
/// \param fragmentCallback If present, invoke this callback every so often
/// with the most recently seen object ID, and a list of fragments that have
/// been updated. This callback will be invoked on the same thread that the
/// runtime is using.
virtual void startTrackingHeapObjectStackTraces(
std::function<void(
uint64_t lastSeenObjectID,
std::chrono::microseconds timestamp,
std::vector<HeapStatsUpdate> stats)> fragmentCallback) = 0;

/// Stop capture JS stack-traces for JS heap allocated objects.
virtual void stopTrackingHeapObjectStackTraces() = 0;
Expand Down
6 changes: 5 additions & 1 deletion ReactCommon/jsi/jsi/jsi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ Instrumentation& Runtime::instrumentation() {

void collectGarbage(std::string) override {}

void startTrackingHeapObjectStackTraces() override {}
void startTrackingHeapObjectStackTraces(
std::function<void(
uint64_t,
std::chrono::microseconds,
std::vector<HeapStatsUpdate>)>) override {}
void stopTrackingHeapObjectStackTraces() override {}

void createSnapshotToFile(const std::string&) override {
Expand Down

0 comments on commit d8b0e9d

Please sign in to comment.