← All issues

Cloned SymbolTable cache per JSGlobalObject fixes DFG constant-folding of mutable variables

5980a42

Source/JavaScriptCore/bytecode/CodeBlock.cpp

- SymbolTable* clone = symbolTable->cloneScopePart(vm);
+ // We have to make sure to use a single code block for constant watchpointing.
+ SymbolTable* clone = globalObject->symbolTableCache().get(symbolTable);
+ if (!clone) {
+ clone = symbolTable->cloneScopePart(vm);
+ globalObject->symbolTableCache().set(symbolTable, clone);
+ }
if (wasCompiledWithDebuggingOpcodes())
- clone->setRareDataCodeBlock(this);
+ clone->collectDebuggerInfo(this);

Source/JavaScriptCore/runtime/SymbolTable.cpp

-void SymbolTable::setRareDataCodeBlock(CodeBlock* codeBlock) {
+void SymbolTable::collectDebuggerInfo(CodeBlock* codeBlock) {
auto& rareData = ensureRareData();
- ASSERT(!rareData.m_codeBlock);
- rareData.m_codeBlock.set(codeBlock->vm(), this, codeBlock);
+ if (!rareData.m_inferredName.isNull())
+ return;
+ rareData.m_inferredName = String::fromUTF8(codeBlock->inferredName().span());
+ ScriptExecutable* executable = codeBlock->ownerExecutable();
+ if (!executable->isHostFunction()) {
+ rareData.m_debuggerSourceID = executable->sourceID();
+ rareData.m_debuggerLineColumn = { static_cast<unsigned>(executable->firstLine()), executable->startColumn() };
+ }
}

DFG JIT는 WatchpointSet 객체를 이용해 speculative optimization을 보호합니다. 변수의 WatchpointSetIsWatched 상태(한 번도 발동되지 않은 상태)를 유지하는 동안, DFG는 해당 값을 constant-fold하는 것이 허용됩니다. SymbolTable은 lexical scope의 변수별 메타데이터를 저장하며, DFG가 참조하는 WatchpointSetEntry도 여기에 포함됩니다. CodeBlock이 jettison(deoptimization으로 인해 무효화)되면, 다음 실행 시 처음부터 다시 생성될 수 있습니다. 이전에는 재생성할 때마다 cloneScopePart()를 호출했고, 이 과정에서 새로운 WatchpointSet을 가진 SymbolTable clone이 매번 새로 할당되었습니다.

문제의 핵심은 다음과 같습니다. op_put_to_scope bytecode는 clone의 WatchpointSet을 올바르게 발동시키지만, 살아 있는 JSLexicalEnvironment는 여전히 이전 clone을 참조하고 있습니다. DFG는 environment를 기반으로 재컴파일할 때 오래된 WatchpointSet을 읽고 IsWatched 상태를 확인한 뒤, 이미 변경된 변수에 대해 constant load를 생성합니다. 이번 수정에서는 JSGlobalObjectWeakGCMap<SymbolTable*, SymbolTable> cache를 도입해 clone의 중복 생성을 방지했습니다. 동일한 원본 SymbolTable에서 파생된 모든 CodeBlock은 같은 clone(그리고 같은 WatchpointSet)을 재사용하게 됩니다.

Before (bug):
  CodeBlock v1  →  clone_A (WatchpointSet_A: IsWatched)
  JSLexicalEnv  →  holds clone_A
  CodeBlock v1 jettisoned
  CodeBlock v2  →  clone_B (WatchpointSet_B: IsWatched)
  op_put_to_scope fires WatchpointSet_B  ← wrong clone
  DFG reads env → clone_A → WatchpointSet_A still IsWatched → WRONG fold

After (fix):
  CodeBlock v1  →  cache miss → clone_A, stored in symbolTableCache
  JSLexicalEnv  →  holds clone_A
  CodeBlock v1 jettisoned
  CodeBlock v2  →  cache hit → reuse clone_A
  op_put_to_scope fires WatchpointSet_A  ← correct clone
  DFG reads env → clone_A → WatchpointSet_A: Invalidated → correct deopt

setRareDataCodeBlock을 대체한 collectDebuggerInfo는 clone을 처음 방문하는 CodeBlock에서 debugger 메타데이터(inferred name, source ID, line/column)를 복사합니다. 이후 호출자에게는 아무 작업 없이 반환됩니다. 이 "처음 호출자 우선" 방식은 clone 재사용 환경에서 유지될 수 없었던 기존의 ASSERT(!m_codeBlock) 패턴을 대체합니다.

DFG-compiled closure 안에서 가변 let 변수가 잘못 constant-fold되는 상황은 JIT type confusion bug의 교과서적인 원인입니다. 컴파일러가 더 이상 제어하지 못하는 값에 대해 오래된 assumption을 바탕으로 동작하기 때문입니다. 이번 수정은 CodeBlock jettison/재생성 사이클을 거치면서 WatchpointSet identity가 실제 객체 상태와 달라지던 구체적인 watchpoint lifecycle hole을 닫았습니다.

🔒

Cache key identity and first-writer-wins debugger info on shared clones both have audit-worthy edge cases.

더 확인하려면 구독해 주세요