Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observed deriveds are now recomputed lazily. #180487

Merged
merged 3 commits into from Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 55 additions & 6 deletions src/vs/base/common/event.ts
Expand Up @@ -599,13 +599,13 @@ export namespace Event {
private _counter = 0;
private _hasChanged = false;

constructor(readonly obs: IObservable<T, any>, store: DisposableStore | undefined) {
constructor(readonly _observable: IObservable<T, any>, store: DisposableStore | undefined) {
const options: EmitterOptions = {
onWillAddFirstListener: () => {
obs.addObserver(this);
_observable.addObserver(this);
},
onDidRemoveLastListener: () => {
obs.removeObserver(this);
_observable.removeObserver(this);
}
};
if (!store) {
Expand All @@ -618,28 +618,77 @@ export namespace Event {
}

beginUpdate<T>(_observable: IObservable<T, void>): void {
// console.assert(_observable === this.obs);
// assert(_observable === this.obs);
this._counter++;
}

handlePossibleChange<T>(_observable: IObservable<T, unknown>): void {
// assert(_observable === this.obs);
}

handleChange<T, TChange>(_observable: IObservable<T, TChange>, _change: TChange): void {
// assert(_observable === this.obs);
this._hasChanged = true;
}

endUpdate<T>(_observable: IObservable<T, void>): void {
if (--this._counter === 0) {
// assert(_observable === this.obs);
this._counter--;
if (this._counter === 0) {
this._observable.reportChanges();
if (this._hasChanged) {
this._hasChanged = false;
this.emitter.fire(this.obs.get());
this.emitter.fire(this._observable.get());
}
}
}
}

/**
* Creates an event emitter that is fired when the observable changes.
* Each listeners subscribes to the emitter.
*/
export function fromObservable<T>(obs: IObservable<T, any>, store?: DisposableStore): Event<T> {
const observer = new EmitterObserver(obs, store);
return observer.emitter.event;
}

/**
* Each listener is attached to the observable directly.
*/
export function fromObservableLight(observable: IObservable<any>): Event<void> {
return (listener) => {
let count = 0;
let didChange = false;
const observer: IObserver = {
beginUpdate() {
count++;
},
endUpdate() {
count--;
if (count === 0) {
observable.reportChanges();
if (didChange) {
didChange = false;
listener();
}
}
},
handlePossibleChange() {
// noop
},
handleChange() {
didChange = true;
}
};
observable.addObserver(observer);
return {
dispose() {
observable.removeObserver(observer);
}
};
};
}
}

export interface EmitterOptions {
Expand Down
87 changes: 63 additions & 24 deletions src/vs/base/common/observableImpl/autorun.ts
Expand Up @@ -49,8 +49,22 @@ export function autorunWithStore(
});
}

const enum AutorunState {
/**
* A dependency could have changed.
* We need to explicitly ask them if at least one dependency changed.
*/
dependenciesMightHaveChanged = 1,

/**
* A dependency changed and we need to recompute.
*/
stale = 2,
upToDate = 3,
}

export class AutorunObserver implements IObserver, IReader, IDisposable {
public needsToRun = true;
private state = AutorunState.stale;
private updateCount = 0;
private disposed = false;

Expand All @@ -76,51 +90,76 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
this.runIfNeeded();
}

public subscribeTo<T>(observable: IObservable<T>) {
public readObservable<T>(observable: IObservable<T>): T {
// In case the run action disposes the autorun
if (this.disposed) {
return;
return observable.get();
}
this._dependencies.add(observable);
if (!this.staleDependencies.delete(observable)) {
observable.addObserver(this);
}
}

public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,
didChange: o => o === observable as any,
}) : true;
this.needsToRun = this.needsToRun || shouldReact;

if (this.updateCount === 0) {
this.runIfNeeded();
}
observable.addObserver(this);
const value = observable.get();
this._dependencies.add(observable);
this.staleDependencies.delete(observable);
return value;
}

public beginUpdate(): void {
if (this.state === AutorunState.upToDate) {
this.state = AutorunState.dependenciesMightHaveChanged;
}
this.updateCount++;
}

public endUpdate(): void {
if (this.updateCount === 1) {
do {
if (this.state === AutorunState.dependenciesMightHaveChanged) {
this.state = AutorunState.upToDate;
for (const d of this._dependencies) {
d.reportChanges();
if (this.state as AutorunState === AutorunState.stale) {
// The other dependencies will refresh on demand
break;
}
}
}

this.runIfNeeded();
} while (this.state !== AutorunState.upToDate);
}
this.updateCount--;
if (this.updateCount === 0) {
this.runIfNeeded();
}

public handlePossibleChange(observable: IObservable<any>): void {
if (this.state === AutorunState.upToDate && this.dependencies.has(observable)) {
this.state = AutorunState.dependenciesMightHaveChanged;
}
}

private runIfNeeded(): void {
if (!this.needsToRun) {
public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
if (this.dependencies.has(observable)) {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,
didChange: o => o === observable as any,
}) : true;
if (shouldReact) {
this.state = AutorunState.stale;
}
}
}

private runIfNeeded() {
if (this.state === AutorunState.upToDate) {
return;
}

// Assert: this.staleDependencies is an empty set.
const emptySet = this.staleDependencies;
this.staleDependencies = this._dependencies;
this._dependencies = emptySet;

this.needsToRun = false;
this.state = AutorunState.upToDate;

getLogger()?.handleAutorunTriggered(this);

Expand Down