Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions third_party/blink/renderer/bindings/core/v8/local_window_proxy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ void LocalWindowProxy::DisposeContext(Lifecycle next_status,

// Record/replay state is initialized along with the first LocalWindowProxy.
static bool gRecordReplayStateInitialized;
static LocalFrame* gRecordReplayFrame = nullptr;

void LocalWindowProxy::Initialize() {
// https://linear.app/replay/issue/RUN-749
Expand Down Expand Up @@ -223,8 +222,8 @@ void LocalWindowProxy::Initialize() {
!origin->Host().empty()) {
// Initialize and re-initialize Replay state, command handlers and more.

bool doInit = !gRecordReplayStateInitialized;
if (doInit) {
bool initGlobally = !gRecordReplayStateInitialized;
if (initGlobally) {
gRecordReplayStateInitialized = true;

// After creating the first context that is associated with a non-empty
Expand All @@ -233,24 +232,24 @@ void LocalWindowProxy::Initialize() {
InitializeRecordReplay(GetIsolate(), GetFrame(), context);
}

if (doInit || GetFrame() == gRecordReplayFrame) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This did not handle potential cross-domain (and maybe other types of?) navigation where the frame does not get re-used.

bool initFrame = !RecordReplayIsReplayScriptAlive();
if (initFrame) {
// Root-level navigation event, initially happens before
// first checkpoint.
// NOTE: We cannot check for GetFrame()->IsOutermostMainFrame() because
// we also need to (re-)init CSP'ed iframes, which run in their own
// process when recording.
gRecordReplayFrame = GetFrame();
// We need our scripts to live in the root frame. Good thing, the
// root frame should always be the first to get initialized.
CHECK(GetFrame()->IsLocalRoot());
OnRootFrameInit(GetIsolate(), GetFrame(), context);
}

if (doInit) {
if (initGlobally) {
// Create the first checkpoint at which execution can pause.
recordreplay::NewCheckpoint();
// Initialize some more.
InitializeRecordReplayAfterCheckpoint();
}

if (GetFrame() == gRecordReplayFrame) {
if (initFrame) {
// Root-level navigation event, after first checkpoint.
OnRootFrameInitAfterCheckpoint(GetIsolate(), GetFrame(), context);
}
Expand Down
129 changes: 88 additions & 41 deletions third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ extern v8::Local<v8::Object> RecordReplayGetBytecode(
} // namespace v8

#define CDPERROR_MISSINGCONTEXT 1001
#define CDPERROR_NOTALIVE 1002

namespace blink {
// using RemoteObjectIdTypeRaw = v8_inspector::String16;
Expand All @@ -85,6 +86,7 @@ using RemoteObjectIdTypeRaw = std::u16string;
using RemoteObjectIdType = WTF::String;

extern "C" void V8RecordReplaySetDefaultContext(v8::Isolate* isolate, v8::Local<v8::Context> cx);
extern "C" int V8RecordReplayGetContextId(v8::Local<v8::Context> cx);
extern "C" void V8RecordReplayFinishRecording();
extern "C" void V8RecordReplaySetCrashReason(const char* reason);

Expand Down Expand Up @@ -152,7 +154,7 @@ class InspectorData {
LocalFrame* GetLocalFrameRoot() const { return blink::GetLocalFrameRoot(isolate); }
};

static LocalFrame* gLocalRootFrame = nullptr;
static LocalFrame* gRootLocalFrame = nullptr;

typedef std::unordered_map<int, InspectorData*> ContextGroupIdInspectorMap;

Expand All @@ -174,6 +176,7 @@ const {
log: log_,
logTrace: logTrace_,
warning: warning_,
fromJsIsReplayScriptAlive: isReplayScriptAlive,
setCDPMessageCallback,
sendCDPMessage,
setCommandCallback,
Expand All @@ -198,9 +201,22 @@ const {

// constants
CDPERROR_MISSINGCONTEXT,
CDPERROR_NOTALIVE,
REPLAY_CDT_PAUSE_OBJECT_GROUP
} = __RECORD_REPLAY_ARGUMENTS__;

function log(...args) {
log_(args.join(' '));
}

function logTrace(...args) {
logTrace_(args.join(' '));
}

function warning(...args) {
warning_(args.join(' '));
}

const gSourceMapData = new Map();

try {
Expand All @@ -223,18 +239,6 @@ const Array_push = Array.prototype.push;
// Some of these are duplicated in gSourceMapScript, so watch out when making
// modifications to update both versions...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments (and including utils.js above) are making less sense now. should we move other things up above (like assert, isFunction, isObject, typeofMaybeNull)? or remove the utils.js comment?

function log(...args) {
log_(args.join(' '));
}

function logTrace(...args) {
logTrace_(args.join(' '));
}

function warning(...args) {
warning_(args.join(' '));
}

function assert(v, msg = "") {
if (!v) {
const m = `Assertion failed when handling command (${msg})`;
Expand Down Expand Up @@ -325,6 +329,7 @@ class CdpRequest {
}

const gCdpRequestStack = [];
const gEventListeners = new Map();


class CDPMessageError extends Error {
Expand Down Expand Up @@ -357,7 +362,7 @@ function sendMessage(method, params) {
}
} finally {
const req = gCdpRequestStack.pop();
assert(req === cdpRequest, "CDP request stack corrupted");
assert(req === cdpRequest, "[RuntimeError] CDP request stack corrupted");
}

if (cdpRequest.result?.result) {
Expand All @@ -369,7 +374,6 @@ function sendMessage(method, params) {
return undefined;
}

const gEventListeners = new Map();

function addEventListener(method, callback) {
gEventListeners.set(method, callback);
Expand All @@ -396,6 +400,7 @@ function messageCallback(message) {
is_error: true,
message: e?.message || (e + ''),
stack: e?.stack?.split?.("\n") || e?.stack || [],
code: e?.code,
});
}
}
Expand Down Expand Up @@ -438,7 +443,21 @@ const CommandCallbacks = {
"CSS.getAppliedRules": CSS_getAppliedRules
};

function CHECK_ALIVE(message) {
if (!isReplayScriptAlive()) {
const err = new Error(`ReplayScriptContext UNALIVE - ${message}`);
err.code = CDPERROR_NOTALIVE;
throw err;
}
}

function getAliveLabel() {
return isReplayScriptAlive() ? "" : " [UNALIVE]"
}

function executeCommand(method, params) {
CHECK_ALIVE(`executeCommand ${method}`);

VerboseCommands && log(`[Command ${method}] Handling command, params=${JSON_stringify(params)}...`);
const result = CommandCallbacks[method](params);
VerboseCommands && log(`[Command ${method}] Handled command, result=${JSON_stringify(result)}`);
Expand All @@ -447,21 +466,22 @@ function executeCommand(method, params) {

function commandCallback(method, params) {
if (!CommandCallbacks[method]) {
log(`[Command ${method}] Missing command callback: ${method}`);
log(`[RuntimeError][Command ${method}] Missing command callback: ${method}`);
return {};
}

try {
return executeCommand(method, params);
} catch (e) {
log(`[RuntimeError][Command ${method}] ${e?.stack || e}`);
log(`[RuntimeError][Command ${method}]${getAliveLabel()} ${e?.stack || e}`);
// Pass the error up to V8; it can (for now) decide how to handle itself, whether
// it should crash or not, etc. Eventually, the caller of the command should make
// that decision.
return {
is_error: true,
message: e?.message || (e + ''),
stack: e?.stack?.split?.("\n") || e?.stack || [],
code: e?.code,
};
}
}
Expand Down Expand Up @@ -655,6 +675,7 @@ function buildRrpObjectResult(cdpReturnValue) {
// Sometimes things go wrong.
// E.g. sometimes we get "Cannot find default execution context (-32000) when executing" sendMessage
// from Pause_evaluateIn*.
log(`[RuntimeError] buildRrpObjectResult called without cdpReturnValue ()`);
rrpResult.failed = true;
}
return { result: rrpResult };
Expand Down Expand Up @@ -3202,7 +3223,7 @@ addEventListener("Runtime.executionContextsCleared", () => {
sendMessage("Runtime.enable");

} catch (e) {
log(`Error: Initialization exception ${e}`);
warning(`JS_ERROR Initialization: ${e?.stack || e}`);
}

})();
Expand Down Expand Up @@ -3891,6 +3912,42 @@ static void LogWarningCallback(const v8::FunctionCallbackInfo<v8::Value>& args)
recordreplay::Warning("%s", *text);
}

void
RecordReplayRegisterV8Inspector(v8_inspector::V8Inspector* inspector,
v8::Isolate* isolate) {
if (v8::IsMainThread() && IsGReplayScriptEnabled()) {
if (!gV8Inspectors) {
gV8Inspectors = new std::unordered_map<v8::Isolate*,v8_inspector::V8Inspector*>();
gInspectorData = new std::unordered_map<v8::Isolate*, ContextGroupIdInspectorMap*>();
}

gV8Inspectors->insert(std::make_pair(isolate, inspector));
}
}

static bool gReplayScriptAlive = false;

bool RecordReplayIsReplayScriptAlive() {
return gReplayScriptAlive;
}

/**
* This is called when gReplayScript's context is about to shut down.
*/
void RecordReplayHandleScriptShutdown(const char* reason, LocalFrame* frame) {
CHECK(v8::IsMainThread());
if (!gReplayScriptAlive || frame != gRootLocalFrame) {
return;
}
recordreplay::Print("ReplayScriptContext STATUS_CHANGE_UNALIVE - %s", reason);
gReplayScriptAlive = false;
}

static void fromJsIsReplayScriptAlive(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(v8::Number::New(isolate, gReplayScriptAlive));
}

// Function to invoke on CDP responses and events.
static v8::Eternal<v8::Function>* gCDPMessageCallback;

Expand Down Expand Up @@ -4010,19 +4067,6 @@ v8_inspector::V8InspectorSession* getInspectorSession(v8::Isolate* isolate, int
return data->inspectorSession;
}

void
RecordReplayRegisterV8Inspector(v8_inspector::V8Inspector* inspector,
v8::Isolate* isolate) {
if (v8::IsMainThread() && IsGReplayScriptEnabled()) {
if (!gV8Inspectors) {
gV8Inspectors = new std::unordered_map<v8::Isolate*,v8_inspector::V8Inspector*>();
gInspectorData = new std::unordered_map<v8::Isolate*, ContextGroupIdInspectorMap*>();
}

gV8Inspectors->insert(std::make_pair(isolate, inspector));
}
}

static int GetBlinkPersistentId(v8::Local<v8::Object> object) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();

Expand Down Expand Up @@ -5501,15 +5545,19 @@ static void InitializeRecordReplayApiObjects(v8::Isolate* isolate, LocalFrame* l
v8::Boolean::New(isolate,
TestEnv("RECORD_REPLAY_DISABLE_SOURCEMAP_CACHE")));


DefineProperty(isolate, args, "CDPERROR_MISSINGCONTEXT",
v8::Number::New(isolate, (double)CDPERROR_MISSINGCONTEXT));

DefineProperty(isolate, args, "CDPERROR_NOTALIVE",
v8::Number::New(isolate, (double)CDPERROR_NOTALIVE));

SetFunctionProperty(isolate, args, "log", LogCallback);
SetFunctionProperty(isolate, args, "logTrace", LogTraceCallback);
SetFunctionProperty(isolate, args, "warning", LogWarningCallback);

// CDP debugger functionality
SetFunctionProperty(isolate, args, "fromJsIsReplayScriptAlive",
fromJsIsReplayScriptAlive);
SetFunctionProperty(isolate, args, "setCDPMessageCallback",
SetCDPMessageCallback);
SetFunctionProperty(isolate, args, "sendCDPMessage", SendCDPMessage);
Expand Down Expand Up @@ -5583,13 +5631,8 @@ static void InitializeRecordReplayApiObjects(v8::Isolate* isolate, LocalFrame* l
SetFunctionProperty(isolate, args, "checkPersistentId", fromJsCheckPersistentId);
}

static void RecordReplaySetDefaultContext(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local<v8::Context> context) {
V8RecordReplaySetDefaultContext(isolate, context);
}

void InitializeRecordReplay(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local<v8::Context> context) {
V8RecordReplaySetAPIObjectIdCallback(GetBlinkPersistentId);
RecordReplaySetDefaultContext(isolate, localFrame, context);
gActiveNetworkRequests =
new std::unordered_map<std::string, NetworkRequestStatus>();
gCurrentNetworkStreamData = new std::vector<uint8_t>();
Expand All @@ -5606,7 +5649,7 @@ static void InitializeReplayScripts(v8::Isolate* isolate, LocalFrame* localFrame
// JS stack, we can always use the current root frame's context.
// Note: We are assuming that each tab has its own process, for now.
// (That might not hold true for tabs of the same domain - not sure)
RecordReplaySetDefaultContext(isolate, localFrame, context);
V8RecordReplaySetDefaultContext(isolate, context);

// Initialize __RECORD_REPLAY__ things.
InitializeRecordReplayApiObjects(isolate, localFrame);
Expand All @@ -5629,6 +5672,7 @@ static void InitializeReplayScripts(v8::Isolate* isolate, LocalFrame* localFrame
if (IsGReplayScriptEnabled()) {
recordreplay::AutoMarkReplayCode amrc;
recordreplay::AutoDisallowEvents disallow("InitializeReplayScripts");

// Run `gReplayScript`.
RunScript(isolate, context, gReplayScript, InternalScriptURL);
}
Expand All @@ -5644,15 +5688,18 @@ void OnRootFrameInit(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local<v8:
localFrame->GetDocument()->Url().GetString().Utf8().c_str()
);

// NOTE: The root `LocalFrame` will actually not change over time.
gLocalRootFrame = localFrame;
// NOTE: The root `LocalFrame` will not necessarily change over time.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this isn't a strong enough assertion. how about:

Suggested change
// NOTE: The root `LocalFrame` will not necessarily change over time.
// NOTE: The root `LocalFrame` can change over time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its also less wordy!

gRootLocalFrame = localFrame;

// 1. Reset paint surface so that paints to the new root's surface are not ignored.
// See: https://linear.app/replay/issue/RUN-2400
recordreplay::DoResetPaintSurface();

// 2. Initialize our scripts, command handlers etc.
// 2. Initialize sourcemap worker, command handlers etc.
InitializeReplayScripts(isolate, localFrame, context);

gReplayScriptAlive = true;
recordreplay::Print("ReplayScriptContext STATUS_CHANGE_ALIVE");
}

void OnRootFrameInitAfterCheckpoint(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local<v8::Context> context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ void OnNewWindowAfterCheckpoint(v8::Isolate* isolate, LocalFrame* localFrame, v8
// Notify the driver that we're adding an error to the console.
void RecordReplayOnErrorEvent(ErrorEvent* error_event);

// Whether gReplayScript can service arbirary commands.
// Meaning its frame and debugger session are both alive.
bool RecordReplayIsReplayScriptAlive();

// Notify our blink bindings that the page that was running gReplayScript has
// shutdown, and our V8 debugger session with it.
// From this point forward, command handling is not possible anymore
// until a new page is spawned.
void RecordReplayHandleScriptShutdown(const char* reason, LocalFrame* frame);

// Notify record/replay about new inspectors that have been created.
void RecordReplayRegisterV8Inspector(v8_inspector::V8Inspector* inspector,
v8::Isolate* isolate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"

#include "third_party/blink/renderer/bindings/core/v8/record_replay_interface.h"

namespace blink {

namespace {
Expand Down Expand Up @@ -127,8 +129,12 @@ void MainThreadDebugger::SetClientMessageLoop(

void MainThreadDebugger::DidClearContextsForFrame(LocalFrame* frame) {
DCHECK(IsMainThread());
if (frame->LocalFrameRoot() == frame)
if (frame->LocalFrameRoot() == frame) {
if (recordreplay::IsRecordingOrReplaying("DidClearContextsForFrame")) {
RecordReplayHandleScriptShutdown("MainThreadDebugger::DidClearContextsForFrame", frame);
}
GetV8Inspector()->resetContextGroup(ContextGroupId(frame));
}
}

void MainThreadDebugger::ContextCreated(ScriptState* script_state,
Expand Down