blob: 79ec89585d938f3228124f85619cdb167902f26c [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Bindings from '../bindings/bindings.js';
import {resolveScopeChain} from './NamesResolver.js';
/**
* Resolves and caches the scope chain for {@link SDK.DebuggerModel.CallFrame}s.
*
* There is one instance per `Universe`. Caching the resolved scope chain allows different consumers
* (e.g. the Scope view and the inline values in the editor) to share the exact same
* {@link SDK.DebuggerModel.ScopeChainEntry} and {@link SDK.RemoteObject.RemoteObject} instances for a
* given pause, without redundant CDP evaluations.
*
* The cache for a script is invalidated whenever debugging info (e.g. from a debugger language plugin)
* or a source map is attached to or detached from that script. The {@link Events.SCOPE_CHAIN_INVALIDATED}
* event is dispatched afterwards, so consumers can re-resolve the scope chain.
*/
export class ScopeChainResolver extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
readonly #debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding;
readonly #cache = new WeakMap<SDK.Script.Script,
WeakMap<SDK.DebuggerModel.CallFrame, Promise<SDK.DebuggerModel.ScopeChainEntry[]>>>();
constructor(targetManager: SDK.TargetManager.TargetManager,
debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding) {
super();
this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
}
/**
* @deprecated Pass the `ScopeChainResolver` of the `Universe` via constructor instead.
*/
static instance(): ScopeChainResolver {
// TODO(crbug.com/458180550): Remove once all callers receive the instance via constructor.
return Root.DevToolsContext.globalInstance().get(ScopeChainResolver);
}
modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebugInfoAttached, this.#debugInfoAttached, this);
debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapAttached,
this.#sourceMapChanged, this);
debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapDetached,
this.#sourceMapChanged, this);
}
modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.DebugInfoAttached, this.#debugInfoAttached, this);
debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapAttached,
this.#sourceMapChanged, this);
debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapDetached,
this.#sourceMapChanged, this);
}
/**
* Returns the (cached) resolved scope chain for `callFrame`. Repeated calls return the same promise
* until the cache for the call frame's script is invalidated.
*/
resolveScopeChain(callFrame: SDK.DebuggerModel.CallFrame): Promise<SDK.DebuggerModel.ScopeChainEntry[]> {
let cacheForScript = this.#cache.get(callFrame.script);
if (!cacheForScript) {
cacheForScript = new WeakMap();
this.#cache.set(callFrame.script, cacheForScript);
}
let cachedPromise = cacheForScript.get(callFrame);
if (!cachedPromise) {
cachedPromise = resolveScopeChain(callFrame, this.#debuggerWorkspaceBinding);
cacheForScript.set(callFrame, cachedPromise);
}
return cachedPromise;
}
#invalidate(script: SDK.Script.Script): void {
this.#cache.delete(script);
this.dispatchEventToListeners(Events.SCOPE_CHAIN_INVALIDATED, script);
}
#debugInfoAttached(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
this.#invalidate(event.data);
}
#sourceMapChanged(event: Common.EventTarget
.EventTargetEvent<{client: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap}>): void {
this.#invalidate(event.data.client);
}
}
export const enum Events {
SCOPE_CHAIN_INVALIDATED = 'ScopeChainInvalidated',
}
export interface EventTypes {
/** Dispatched with the script for which the cached scope chains have been invalidated. */
[Events.SCOPE_CHAIN_INVALIDATED]: SDK.Script.Script;
}