From f2499f2a678cfd9cf62cbca7e8acfba3e15ee7ea Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 7 Nov 2025 10:55:34 +0100 Subject: [PATCH] Improves observable debugging helper --- src/vs/base/common/observableInternal/base.ts | 5 -- .../base/common/observableInternal/index.ts | 6 +- .../logging/debugGetDependencyGraph.ts | 62 ++++++++++++++++--- .../observables/baseObservable.ts | 25 ++++++-- .../common/observableInternal/utils/utils.ts | 3 +- .../test/common/observables/debug.test.ts | 4 +- 6 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 69922fce403..772297def7f 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -95,11 +95,6 @@ export interface IObservableWithChange { */ readonly debugName: string; - /** - * ONLY FOR DEBUGGING! - */ - debugGetDependencyGraph(): string; - /** * This property captures the type of the change object. Do not use it at runtime! */ diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index fd23c427331..c4f31a4783a 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -40,10 +40,10 @@ import { addLogger, setLogObservableFn } from './logging/logging.js'; import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js'; import { DevToolsLogger } from './logging/debugger/devToolsLogger.js'; import { env } from '../process.js'; -import { _setDebugGetDependencyGraph } from './observables/baseObservable.js'; -import { debugGetDependencyGraph } from './logging/debugGetDependencyGraph.js'; +import { _setDebugGetObservableGraph } from './observables/baseObservable.js'; +import { debugGetObservableGraph } from './logging/debugGetDependencyGraph.js'; -_setDebugGetDependencyGraph(debugGetDependencyGraph); +_setDebugGetObservableGraph(debugGetObservableGraph); setLogObservableFn(logObservableToConsole); // Remove "//" in the next line to enable logging diff --git a/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts b/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts index 88c9346d28c..9a13ba8840f 100644 --- a/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts +++ b/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts @@ -10,7 +10,12 @@ import { ObservableValue } from '../observables/observableValue.js'; import { AutorunObserver } from '../reactions/autorunImpl.js'; import { formatValue } from './consoleObservableLogger.js'; -export function debugGetDependencyGraph(obs: IObservable | IObserver, options?: { debugNamePostProcessor?: (name: string) => string }): string { +interface IOptions { + type: 'dependencies' | 'observers'; + debugNamePostProcessor?: (name: string) => string; +} + +export function debugGetObservableGraph(obs: IObservable | IObserver, options: IOptions): string { const debugNamePostProcessor = options?.debugNamePostProcessor ?? ((str: string) => str); const info = Info.from(obs, debugNamePostProcessor); if (!info) { @@ -18,10 +23,15 @@ export function debugGetDependencyGraph(obs: IObservable | IObserver, optio } const alreadyListed = new Set | IObserver>(); - return formatObservableInfo(info, 0, alreadyListed).trim(); + + if (options.type === 'observers') { + return formatObservableInfoWithObservers(info, 0, alreadyListed, options).trim(); + } else { + return formatObservableInfoWithDependencies(info, 0, alreadyListed, options).trim(); + } } -function formatObservableInfo(info: Info, indentLevel: number, alreadyListed: Set | IObserver>): string { +function formatObservableInfoWithDependencies(info: Info, indentLevel: number, alreadyListed: Set | IObserver>, options: IOptions): string { const indent = '\t\t'.repeat(indentLevel); const lines: string[] = []; @@ -40,7 +50,35 @@ function formatObservableInfo(info: Info, indentLevel: number, alreadyListed: Se if (info.dependencies.length > 0) { lines.push(`${indent} dependencies:`); for (const dep of info.dependencies) { - lines.push(formatObservableInfo(dep, indentLevel + 1, alreadyListed)); + const info = Info.from(dep, options.debugNamePostProcessor ?? (name => name)) ?? Info.unknown(dep); + lines.push(formatObservableInfoWithDependencies(info, indentLevel + 1, alreadyListed, options)); + } + } + + return lines.join('\n'); +} + +function formatObservableInfoWithObservers(info: Info, indentLevel: number, alreadyListed: Set | IObserver>, options: IOptions): string { + const indent = '\t\t'.repeat(indentLevel); + const lines: string[] = []; + + const isAlreadyListed = alreadyListed.has(info.sourceObj); + if (isAlreadyListed) { + lines.push(`${indent}* ${info.type} ${info.name} (already listed)`); + return lines.join('\n'); + } + + alreadyListed.add(info.sourceObj); + + lines.push(`${indent}* ${info.type} ${info.name}:`); + lines.push(`${indent} value: ${formatValue(info.value, 50)}`); + lines.push(`${indent} state: ${info.state}`); + + if (info.observers.length > 0) { + lines.push(`${indent} observers:`); + for (const observer of info.observers) { + const info = Info.from(observer, options.debugNamePostProcessor ?? (name => name)) ?? Info.unknown(observer); + lines.push(formatObservableInfoWithObservers(info, indentLevel + 1, alreadyListed, options)); } } @@ -57,7 +95,8 @@ class Info { 'autorun', undefined, state.stateStr, - Array.from(state.dependencies).map(dep => Info.from(dep, debugNamePostProcessor) || Info.unknown(dep)) + Array.from(state.dependencies), + [] ); } else if (obs instanceof Derived) { const state = obs.debugGetState(); @@ -67,7 +106,8 @@ class Info { 'derived', state.value, state.stateStr, - Array.from(state.dependencies).map(dep => Info.from(dep, debugNamePostProcessor) || Info.unknown(dep)) + Array.from(state.dependencies), + Array.from(obs.debugGetObservers()) ); } else if (obs instanceof ObservableValue) { const state = obs.debugGetState(); @@ -77,7 +117,8 @@ class Info { 'observableValue', state.value, 'upToDate', - [] + [], + Array.from(obs.debugGetObservers()) ); } else if (obs instanceof FromEventObservable) { const state = obs.debugGetState(); @@ -87,7 +128,8 @@ class Info { 'fromEvent', state.value, state.hasValue ? 'upToDate' : 'initial', - [] + [], + Array.from(obs.debugGetObservers()) ); } return undefined; @@ -100,6 +142,7 @@ class Info { 'unknown', undefined, 'unknown', + [], [] ); } @@ -110,6 +153,7 @@ class Info { public readonly type: string, public readonly value: any, public readonly state: string, - public readonly dependencies: Info[] + public readonly dependencies: (IObservable | IObserver)[], + public readonly observers: (IObservable | IObserver)[], ) { } } diff --git a/src/vs/base/common/observableInternal/observables/baseObservable.ts b/src/vs/base/common/observableInternal/observables/baseObservable.ts index 1fe2181695e..4903e1a6e57 100644 --- a/src/vs/base/common/observableInternal/observables/baseObservable.ts +++ b/src/vs/base/common/observableInternal/observables/baseObservable.ts @@ -7,7 +7,7 @@ import { IObservableWithChange, IObserver, IReader, IObservable } from '../base. import { DisposableStore } from '../commonFacade/deps.js'; import { DebugLocation } from '../debugLocation.js'; import { DebugOwner, getFunctionName } from '../debugName.js'; -import { debugGetDependencyGraph } from '../logging/debugGetDependencyGraph.js'; +import { debugGetObservableGraph } from '../logging/debugGetDependencyGraph.js'; import { getLogger, logObservable } from '../logging/logging.js'; import type { keepObserved, recomputeInitiallyAndOnChange } from '../utils/utils.js'; import { derivedOpts } from './derived.js'; @@ -31,9 +31,9 @@ export function _setKeepObserved(keepObserved: typeof _keepObserved) { _keepObserved = keepObserved; } -let _debugGetDependencyGraph: typeof debugGetDependencyGraph; -export function _setDebugGetDependencyGraph(debugGetDependencyGraph: typeof _debugGetDependencyGraph) { - _debugGetDependencyGraph = debugGetDependencyGraph; +let _debugGetObservableGraph: typeof debugGetObservableGraph; +export function _setDebugGetObservableGraph(debugGetObservableGraph: typeof _debugGetObservableGraph) { + _debugGetObservableGraph = debugGetObservableGraph; } export abstract class ConvenientObservable implements IObservableWithChange { @@ -128,8 +128,21 @@ export abstract class ConvenientObservable implements IObservableWit return this.get(); } - debugGetDependencyGraph(): string { - return _debugGetDependencyGraph(this); + get debug(): DebugHelper { + return new DebugHelper(this); + } +} + +class DebugHelper { + constructor(public readonly observable: IObservableWithChange) { + } + + getDependencyGraph(): string { + return _debugGetObservableGraph(this.observable, { type: 'dependencies' }); + } + + getObserverGraph(): string { + return _debugGetObservableGraph(this.observable, { type: 'observers' }); } } diff --git a/src/vs/base/common/observableInternal/utils/utils.ts b/src/vs/base/common/observableInternal/utils/utils.ts index ed35204c7e3..efee1599c78 100644 --- a/src/vs/base/common/observableInternal/utils/utils.ts +++ b/src/vs/base/common/observableInternal/utils/utils.ts @@ -225,7 +225,8 @@ export function mapObservableArrayCached(owner: DebugOwne m = new ArrayMap(map); } }, (reader) => { - m.setItems(items.read(reader)); + const i = items.read(reader); + m.setItems(i); return m.getItems(); }); return self; diff --git a/src/vs/base/test/common/observables/debug.test.ts b/src/vs/base/test/common/observables/debug.test.ts index 5be20a336d6..c046999ef59 100644 --- a/src/vs/base/test/common/observables/debug.test.ts +++ b/src/vs/base/test/common/observables/debug.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { observableValue, derived, autorun } from '../../../common/observable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../utils.js'; // eslint-disable-next-line local/code-no-deep-import-of-internal -import { debugGetDependencyGraph } from '../../../common/observableInternal/logging/debugGetDependencyGraph.js'; +import { debugGetObservableGraph } from '../../../common/observableInternal/logging/debugGetDependencyGraph.js'; suite('debug', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); @@ -50,7 +50,7 @@ suite('debug', () => { let idx = 0; assert.deepStrictEqual( - debugGetDependencyGraph(myComputed3, { debugNamePostProcessor: name => `name${++idx}` }), + debugGetObservableGraph(myComputed3, { type: 'dependencies', debugNamePostProcessor: name => `name${++idx}` }), '* derived name1:\n value: 0\n state: upToDate\n dependencies:\n\t\t* derived name2:\n\t\t value: 0\n\t\t state: upToDate\n\t\t dependencies:\n\t\t\t\t* derived name3:\n\t\t\t\t value: 0\n\t\t\t\t state: upToDate\n\t\t\t\t dependencies:\n\t\t\t\t\t\t* observableValue name4:\n\t\t\t\t\t\t value: 0\n\t\t\t\t\t\t state: upToDate\n\t\t\t\t\t\t* observableValue name5:\n\t\t\t\t\t\t value: 0\n\t\t\t\t\t\t state: upToDate\n\t\t\t\t* observableValue name6 (already listed)\n\t\t\t\t* observableValue name7 (already listed)\n\t\t* observableValue name8 (already listed)\n\t\t* observableValue name9 (already listed)', ); });