[inspector] Don't hold on to Script objects strongly for caching.

Previously we'd hold on to Script objects strongly after they are
considered unreachable by V8 itself, and keep them around for the
V8DebuggerAgent cache (whose upper limit can be controlled with a
parameter to `Debugger.enable`).

This CL changes that to instead copy out the script source and the
WebAssembly bytecode (depending on whether it's JavaScript or Wasm)
to the C++ heap and keep it cached there.

Fixed: chromium:1295659
Bug: chromium:1246884
Change-Id: Idfcd7172715eafca6b011826ae03a573d58803f2
Doc: https://bit.ly/v8-inspector-script-caching
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3472082
Reviewed-by: Jaroslav Sevcik <jarin@chromium.org>
Reviewed-by: Michael Lippautz <mlippautz@chromium.org>
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79217}
This commit is contained in:
Benedikt Meurer 2022-02-18 16:27:57 +01:00 committed by V8 LUCI CQ
parent 44b2405736
commit 877dcdfc3a
5 changed files with 46 additions and 22 deletions

View File

@ -435,7 +435,7 @@ Response V8DebuggerAgentImpl::disable() {
resetBlackboxedStateCache();
m_skipList.clear();
m_scripts.clear();
m_cachedScriptIds.clear();
m_cachedScripts.clear();
m_cachedScriptSize = 0;
for (const auto& it : m_debuggerBreakpointIdToBreakpointId) {
v8::debug::RemoveBreakpoint(m_isolate, it.first);
@ -1072,8 +1072,20 @@ Response V8DebuggerAgentImpl::getScriptSource(
Maybe<protocol::Binary>* bytecode) {
if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
ScriptsMap::iterator it = m_scripts.find(scriptId);
if (it == m_scripts.end())
if (it == m_scripts.end()) {
auto cachedScriptIt =
std::find_if(m_cachedScripts.begin(), m_cachedScripts.end(),
[&scriptId](const CachedScript& cachedScript) {
return cachedScript.scriptId == scriptId;
});
if (cachedScriptIt != m_cachedScripts.end()) {
*scriptSource = cachedScriptIt->source;
*bytecode = protocol::Binary::fromSpan(cachedScriptIt->bytecode.data(),
cachedScriptIt->bytecode.size());
return Response::Success();
}
return Response::ServerError("No script for id: " + scriptId.utf8());
}
*scriptSource = it->second->source(0);
#if V8_ENABLE_WEBASSEMBLY
v8::MemorySpan<const uint8_t> span;
@ -1949,23 +1961,31 @@ void V8DebuggerAgentImpl::reset() {
resetBlackboxedStateCache();
m_skipList.clear();
m_scripts.clear();
m_cachedScriptIds.clear();
m_cachedScripts.clear();
m_cachedScriptSize = 0;
}
void V8DebuggerAgentImpl::ScriptCollected(const V8DebuggerScript* script) {
DCHECK_NE(m_scripts.find(script->scriptId()), m_scripts.end());
m_cachedScriptIds.push_back(script->scriptId());
// TODO(alph): Properly calculate size when sources are one-byte strings.
m_cachedScriptSize += script->length() * sizeof(uint16_t);
std::vector<uint8_t> bytecode;
#if V8_ENABLE_WEBASSEMBLY
v8::MemorySpan<const uint8_t> span;
if (script->wasmBytecode().To(&span)) {
bytecode.reserve(span.size());
bytecode.insert(bytecode.begin(), span.data(), span.data() + span.size());
}
#endif
CachedScript cachedScript{script->scriptId(), script->source(0),
std::move(bytecode)};
m_cachedScriptSize += cachedScript.size();
m_cachedScripts.push_back(std::move(cachedScript));
m_scripts.erase(script->scriptId());
while (m_cachedScriptSize > m_maxScriptCacheSize) {
const String16& scriptId = m_cachedScriptIds.front();
size_t scriptSize = m_scripts[scriptId]->length() * sizeof(uint16_t);
DCHECK_GE(m_cachedScriptSize, scriptSize);
m_cachedScriptSize -= scriptSize;
m_scripts.erase(scriptId);
m_cachedScriptIds.pop_front();
const CachedScript& cachedScript = m_cachedScripts.front();
DCHECK_GE(m_cachedScriptSize, cachedScript.size());
m_cachedScriptSize -= cachedScript.size();
m_cachedScripts.pop_front();
}
}

View File

@ -226,7 +226,16 @@ class V8DebuggerAgentImpl : public protocol::Debugger::Backend {
size_t m_maxScriptCacheSize = 0;
size_t m_cachedScriptSize = 0;
std::deque<String16> m_cachedScriptIds;
struct CachedScript {
String16 scriptId;
String16 source;
std::vector<uint8_t> bytecode;
size_t size() const {
return source.length() * sizeof(UChar) + bytecode.size();
}
};
std::deque<CachedScript> m_cachedScripts;
using BreakReason =
std::pair<String16, std::unique_ptr<protocol::DictionaryValue>>;

View File

@ -1,7 +1,5 @@
Checks if we keep alive breakpoint information for top-level functions when calling getPossibleBreakpoints.
Result of get possible breakpoints in topLevel.js
[{"scriptId":"3","lineNumber":0,"columnNumber":0},{"scriptId":"3","lineNumber":0,"columnNumber":8,"type":"call"},{"scriptId":"3","lineNumber":0,"columnNumber":43,"type":"return"}]
Result of get possible breakpoints in moduleFunc.js
[{"scriptId":"5","lineNumber":0,"columnNumber":22},{"scriptId":"5","lineNumber":0,"columnNumber":30,"type":"call"},{"scriptId":"5","lineNumber":0,"columnNumber":63,"type":"return"},{"scriptId":"5","lineNumber":0,"columnNumber":64,"type":"return"}]
[{"scriptId":"3","lineNumber":0,"columnNumber":22},{"scriptId":"3","lineNumber":0,"columnNumber":30,"type":"call"},{"scriptId":"3","lineNumber":0,"columnNumber":63,"type":"return"},{"scriptId":"3","lineNumber":0,"columnNumber":64,"type":"return"}]
Result of get possible breakpoints in mixedFunctions.js
[{"scriptId":"7","lineNumber":0,"columnNumber":15,"type":"return"},{"scriptId":"7","lineNumber":1,"columnNumber":2},{"scriptId":"7","lineNumber":1,"columnNumber":10,"type":"call"},{"scriptId":"7","lineNumber":2,"columnNumber":0,"type":"return"}]
[{"scriptId":"5","lineNumber":0,"columnNumber":15,"type":"return"},{"scriptId":"5","lineNumber":1,"columnNumber":2},{"scriptId":"5","lineNumber":1,"columnNumber":10,"type":"call"},{"scriptId":"5","lineNumber":2,"columnNumber":0,"type":"return"}]

View File

@ -17,7 +17,6 @@ const callGarbageCollector = `
%CollectGarbage("");
`;
const topLevelFunction = `console.log('This is a top level function')`;
const moduleFunction =
`function testFunc() { console.log('This is a module function') }`;
let mixedFunctions = ` function A() {}
@ -33,8 +32,6 @@ function onDebuggerEnabled() {
async function onExecutionContextCreated(messageObject) {
executionContextId = messageObject.params.context.id;
await testGetPossibleBreakpoints(
executionContextId, topLevelFunction, 'topLevel.js');
await testGetPossibleBreakpoints(
executionContextId, moduleFunction, 'moduleFunc.js');
await testGetPossibleBreakpoints(

View File

@ -1,5 +1,5 @@
Checks if we keep alive breakpoint information for top-level functions.
Result of setting breakpoint in topLevel.js
[{"scriptId":"3","lineNumber":0,"columnNumber":0}]
[]
Result of setting breakpoint in moduleFunc.js
[{"scriptId":"5","lineNumber":0,"columnNumber":22}]
[{"scriptId":"5","lineNumber":0,"columnNumber":22}]