Skip to content

Commit

Permalink
Implemented #316: whyRun
Browse files Browse the repository at this point in the history
  • Loading branch information
mweststrate committed Jun 13, 2016
1 parent a01624f commit f9a236c
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 62 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Improved performance of decorators signficantly, and removed subtle differences between the implementation in babel and typescript.
* `@observable` is now always defined on the class and not in the instances. This means that `@observable` properties are enumerable, but won't appear if `Object.keys` or `hasOwnProperty` is used on a class _instance_.
* if an (argumentless) action is passed to `observable` / `extendObservable`, it will not be converted into a computed property.
* Implemented #316: `whyRun()`

# 2.2.2:

Expand Down
27 changes: 1 addition & 26 deletions src/api/extras.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import {IDepTreeNode} from "../core/observable";
import {ComputedValue} from "../core/computedvalue";
import {Reaction}from "../core/reaction";
import {unique, invariant} from "../utils/utils";
import {unique} from "../utils/utils";
import {getAtom} from "../types/type-utils";
import {globalState} from "../core/globalstate";

export interface IDependencyTree {
name: string;
Expand Down Expand Up @@ -40,25 +37,3 @@ function nodeToObserverTree(node: IDepTreeNode): IObserverTree {
result.observers = <any>unique(node.observers).map(<any>nodeToObserverTree);
return result;
}

export function whyRun(thing?: any, prop?: string) {
switch (arguments.length) {
case 0:
thing = globalState.derivationStack[globalState.derivationStack.length - 1];
if (!thing) {
console.log("whyRun() can only be used if a derivation is active, or by passing an computed value / reaction explicitly.");
return;
}
break;
case 2:
thing = getAtom(thing, prop);
break;
}
thing = getAtom(thing);
if (thing instanceof ComputedValue)
console.log(thing.whyRun());
else if (thing instanceof Reaction)
console.log(thing.whyRun());
else
invariant(false, "whyRun can only be used on reactions and computed values");
}
30 changes: 30 additions & 0 deletions src/api/whyrun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {globalState} from "../core/globalstate";
import {ComputedValue} from "../core/computedvalue";
import {Reaction} from "../core/reaction";
import {getAtom} from "../types/type-utils";
import {invariant} from "../utils/utils";

function log(msg: string): string {
console.log(msg);
return msg;
}

export function whyRun(thing?: any, prop?: string) {
switch (arguments.length) {
case 0:
thing = globalState.derivationStack[globalState.derivationStack.length - 1];
if (!thing)
return log("whyRun() can only be used if a derivation is active, or by passing an computed value / reaction explicitly. If you invoked whyRun from inside a computation; the computation is currently suspended but re-evaluating because somebody requested it's value.");
break;
case 2:
thing = getAtom(thing, prop);
break;
}
thing = getAtom(thing);
if (thing instanceof ComputedValue)
return log(thing.whyRun());
else if (thing instanceof Reaction)
return log(thing.whyRun());
else
invariant(false, "whyRun can only be used on reactions and computed values");
}
11 changes: 6 additions & 5 deletions src/core/atom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import {IObservable, propagateReadiness, propagateStaleness, reportObserved} from "./observable";
import {invariant, noop, getNextId} from "../utils/utils";
import {globalState} from "./globalstate";
import {runReactions} from "./reaction";

export interface IAtom extends IObservable {
isDirty: boolean;
Expand Down Expand Up @@ -70,4 +66,9 @@ export class Atom implements IAtom {
toString() {
return this.name;
}
}
}

import {globalState} from "./globalstate";
import {IObservable, propagateReadiness, propagateStaleness, reportObserved} from "./observable";
import {runReactions} from "./reaction";
import {invariant, noop, getNextId} from "../utils/utils";
48 changes: 27 additions & 21 deletions src/core/computedvalue.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {IObservable, reportObserved, removeObserver} from "./observable";
import {IDerivation, trackDerivedFunction, isComputingDerivation, untracked} from "./derivation";
import {globalState} from "./globalstate";
import {getNextId, valueDidChange, invariant, Lambda, unique} from "../utils/utils";
import {autorun} from "../api/autorun";
import {getNextId, valueDidChange, invariant, Lambda, unique, joinStrings} from "../utils/utils";
import {isSpyEnabled, spyReport} from "../core/spy";
import {autorun} from "../api/autorun";

/**
* A node in the state dependency root that observes other nodes, and can be observed itself.
Expand Down Expand Up @@ -139,48 +139,54 @@ export class ComputedValue<T> implements IObservable, IDerivation {

whyRun() {
const isTracking = globalState.derivationStack.length > 0;
const observing = unique(this.observing).map(dep => dep.name).join(" - ");
const observers = unique(this.observers).map(dep => dep.name).join(" - ");
const runReason = (
const observing = unique(this.observing).map(dep => dep.name);
const observers = unique(this.observers).map(dep => dep.name);
const runReason = (
this.isComputing
? isTracking
? this.dependencyChangeCount > 0
? this.observers.length > 0 // this computation already had observers
? RunReason.INVALIDATED
: RunReason.REQUIRED
: RunReason.PEEK
: RunReason.NOT_RUNNING
);
if (runReason === RunReason.REQUIRED) {
const requiredBy = globalState.derivationStack[globalState.derivationStack.length - 2];
if (requiredBy)
observers.push(requiredBy.name);
}

return (`
WhyRun? computation '${this.name}'
* Running because: ${runReasonText[runReason]}` +
WhyRun? computation '${this.name}':
* Running because: ${runReasonTexts[runReason]} ${(runReason === RunReason.NOT_RUNNING) && this.dependencyStaleCount > 0 ? "(a next run is scheduled)" : ""}
` +
(this.isLazy
?
` * This computation is suspended (not in use by any reaction) and won't run automatically.
Didn't expect this computation to be suspended at this point?
1. Make sure this computation is used by a reaction (reaction, autorun, observer).
2. Check whether you are using this computation synchronously (in the same stack as they reaction that needs it).`
2. Check whether you are using this computation synchronously (in the same stack as they reaction that needs it).
`
:
` * This computation will re-run if any of the following observables changes:
${observing.slice(0, whyRunNodeLimit)}${observing.length > whyRunNodeLimit ? "(... and " + (observing.length - whyRunNodeLimit) + "more)" : ""}
${(this.isComputing && isTracking) ? "(... and any observable accessed during the remainder of the current run)" : ""}
${joinStrings(observing)}
${(this.isComputing && isTracking) ? " (... or any observable accessed during the remainder of the current run)" : ""}
Missing items in this list?
1. Check whether all used values are properly marked as observable (use isObservable to verify)
2. Make sure you didn't dereference values too early. MobX observes props, not primitives. E.g: use 'person.name' instead of 'name' in your computation.
* If the outcome of this computation changes, the following observers will be re-run:
${observers.slice(0, whyRunNodeLimit)}${observers.length > whyRunNodeLimit ? "(... and " + (observers.length - whyRunNodeLimit) + "more)" : ""}`
${joinStrings(observers)}
`
)
// TODO: if required, then the thing in the stack should be listed here..
);
}
}

enum RunReason { PEEK, INVALIDATED, REQUIRED, NOT_RUNNING }
const runReasonText = {
[RunReason.PEEK]: "The value of this computed value was requested outside an reaction",
[RunReason.INVALIDATED]: "Some observables used by this computation did change",
[RunReason.REQUIRED]: "This computation is required by another computed value / reaction",
[RunReason.NOT_RUNNING]: "This compution is currently not running"
};
export enum RunReason { PEEK, INVALIDATED, REQUIRED, NOT_RUNNING }

const whyRunNodeLimit = 100;
export const runReasonTexts = {
[RunReason.PEEK]: "[peek] The value of this computed value was requested outside an reaction",
[RunReason.INVALIDATED]: "[invalidated] Some observables used by this computation did change",
[RunReason.REQUIRED]: "[started] This computation is required by another computed value / reaction",
[RunReason.NOT_RUNNING]: "[idle] This compution is currently not running"
};
2 changes: 1 addition & 1 deletion src/core/derivation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {IObservable, IDepTreeNode, propagateReadiness, propagateStaleness, addObserver, removeObserver} from "./observable";
import {quickDiff, invariant} from "../utils/utils";
import {globalState, resetGlobalState} from "./globalstate";
import {quickDiff, invariant} from "../utils/utils";
import {isSpyEnabled, spyReport} from "./spy";

/**
Expand Down
24 changes: 22 additions & 2 deletions src/core/reaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {IObservable, removeObserver} from "./observable";
import {IDerivation, trackDerivedFunction} from "./derivation";
import {globalState} from "./globalstate";
import {EMPTY_ARRAY, getNextId, Lambda} from "../utils/utils";
import {EMPTY_ARRAY, getNextId, Lambda, unique, joinStrings} from "../utils/utils";
import {isSpyEnabled, spyReport, spyReportStart, spyReportEnd} from "./spy";

/**
Expand Down Expand Up @@ -31,6 +31,7 @@ export class Reaction implements IDerivation {
isDisposed = false;
_isScheduled = false;
_isTrackPending = false;
_isRunning = false;

constructor(public name: string = "Reaction@" + getNextId(), private onInvalidate: () => void) { }

Expand Down Expand Up @@ -88,7 +89,9 @@ export class Reaction implements IDerivation {
fn
});
}
this._isRunning = true;
trackDerivedFunction(this, fn);
this._isRunning = false;
this._isTrackPending = false;
if (notify) {
spyReportEnd({
Expand All @@ -115,6 +118,22 @@ export class Reaction implements IDerivation {
toString() {
return `Reaction[${this.name}]`;
}

whyRun() {
const observing = unique(this.observing).map(dep => dep.name);

return (`
WhyRun? reaction '${this.name}':
* Status: [${this.isDisposed ? "stopped" : this._isRunning ? "running" : this.isScheduled() ? "scheduled" : "idle"}]
* This reaction will re-run if any of the following observables changes:
${joinStrings(observing)}
${(this._isRunning) ? " (... or any observable accessed during the remainder of the current run)" : ""}
Missing items in this list?
1. Check whether all used values are properly marked as observable (use isObservable to verify)
2. Make sure you didn't dereference values too early. MobX observes props, not primitives. E.g: use 'person.name' instead of 'name' in your computation.
`
);
}
}

/**
Expand Down Expand Up @@ -142,4 +161,5 @@ export function runReactions() {
remainingReactions[i].runReaction();
}
globalState.isRunningReactions = false;
}
}

14 changes: 7 additions & 7 deletions src/mobx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
import {registerGlobals} from "./core/globalstate";
registerGlobals();

export { Lambda } from "./utils/utils";
export { SimpleEventEmitter, ISimpleEventListener } from "./utils/simpleeventemitter";
export { IObserverTree, IDependencyTree } from "./api/extras";
export { IAtom, Atom } from "./core/atom";
export { IObservable, IDepTreeNode } from "./core/observable";
export { Reaction } from "./core/reaction";
export { IDerivation, untracked } from "./core/derivation";
export { action, useStrict, isAction } from "./core/action";
export { spy } from "./core/spy";
export { transaction } from "./core/transaction";

export { asReference, asFlat, asStructure, asMap } from "./types/modifiers";
export { IInterceptable, IInterceptor } from "./types/intercept-utils";
Expand All @@ -46,11 +46,11 @@ export { autorun, autorunAsync, autorunUntil, when, reaction } from "./api/auto
export { expr } from "./api/expr";
export { toJSON, toJS } from "./api/tojson";
export { ITransformer, createTransformer } from "./api/createtransformer";
export { whyRun } from "./api/extras";
export { whyRun } from "./api/whyrun";

export { transaction } from "./core/transaction";
export { Reaction } from "./core/reaction";
export { IAtom, Atom } from "./core/atom";
export { Lambda } from "./utils/utils";
export { SimpleEventEmitter, ISimpleEventListener } from "./utils/simpleeventemitter";
export { IObserverTree, IDependencyTree } from "./api/extras";

import { resetGlobalState } from "./core/globalstate";
import { quickDiff } from "./utils/utils";
Expand Down
7 changes: 7 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export function unique<T>(list: T[]): T[] {
return res;
}

export function joinStrings(things: string[], limit: number = 100, separator = " - "): string {
if (!things)
return "";
const sliced = things.slice(0, limit);
return `${sliced.join(separator)}${things.length > limit ? " (... and " + (things.length - limit) + "more)" : ""}`;
}

export function isPlainObject(value) {
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
}
Expand Down
93 changes: 93 additions & 0 deletions test/whyrun.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"use strict"

const test = require('tape');
const mobx = require('../');
const noop = () => {};

test("whyrun", t => {
const baselog = console.log;
let lastButOneLine = "";
let lastLine = "";

const whyRun = function () {
lastButOneLine = lastLine;
console.log = noop;
lastLine = mobx.whyRun.apply(null, arguments);
console.log = baselog;
return lastLine;
}

const x = mobx.observable({
firstname: "Michel",
lastname: "Weststrate",
fullname: function() {
var res = this.firstname + " " + this.lastname;
whyRun();
return res;
}
});

x.fullname;
t.ok(lastLine.match(/suspended/), "just accessed fullname"); // no normal report, just a notification that nothing is being derived atm

t.ok(whyRun(x, "fullname").match(/\[idle\]/));
t.ok(whyRun(x, "fullname").match(/suspended/));

const d = mobx.autorun(() => {
x.fullname;
whyRun();
})

t.ok(lastButOneLine.match(/\[started\]/), "created autorun");
t.ok(lastButOneLine.match(/will re-run/));
t.ok(lastButOneLine.match(/\.firstname/));
t.ok(lastButOneLine.match(/\.lastname/));
t.ok(lastButOneLine.match(/Autorun@/));

t.ok(lastLine.match(/\[running\]/));
t.ok(lastLine.match(/\.fullname/));

t.ok(whyRun(x, "fullname").match(/\[idle\]/));
t.ok(whyRun(x, "fullname").match(/\.firstname/));
t.ok(whyRun(x, "fullname").match(/\.lastname/));
t.ok(whyRun(x, "fullname").match(/Autorun@/));

t.ok(whyRun(d).match(/\[idle\]/));
t.ok(whyRun(d).match(/\.fullname/));

t.ok(whyRun(d).match(/Autorun@/));

mobx.transaction(() => {
x.firstname = "Veria";
t.ok(whyRun(x, "fullname").match(/\[idle\]/), "made change in transaction");
t.ok(whyRun(x, "fullname").match(/next run is scheduled/));

t.ok(whyRun(d).match(/\[scheduled\]/));
})

t.ok(lastButOneLine.match(/\[invalidated\]/),"post transaction");
t.ok(lastButOneLine.match(/will re-run/));
t.ok(lastButOneLine.match(/\.firstname/));
t.ok(lastButOneLine.match(/\.lastname/));
t.ok(lastButOneLine.match(/\Autorun@/));

t.ok(lastLine.match(/\[running\]/));
t.ok(lastLine.match(/\.fullname/));

t.ok(whyRun(x, "fullname").match(/\[idle\]/));
t.ok(whyRun(x, "fullname").match(/\.firstname/));
t.ok(whyRun(x, "fullname").match(/\.lastname/));
t.ok(whyRun(x, "fullname").match(/Autorun@/));

t.ok(whyRun(d).match(/\[idle\]/));
t.ok(whyRun(d).match(/\.fullname/));
t.ok(whyRun(d).match(/Autorun@/));

d();

t.ok(whyRun(d).match(/\[stopped\]/));
t.ok(whyRun(x, "fullname").match(/\[idle\]/));
t.ok(whyRun(x, "fullname").match(/suspended/));

t.end();
})

0 comments on commit f9a236c

Please sign in to comment.