-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Proof of concept, microtasks to batch batches. #2311
Conversation
As documented one of the design principles is that MobX is synchronous in
all circumstances, which makes it behavior very predictable and much easier
to debug (a change causing a reaction is always in the same call stack for
example). Doing things async is a valid design choice with it's own merits
as you mentioned. However, in a product like MobX that is already shipped
in thousands of products, such a significant fundamental semantic change
would result in a totally unmanageable support load so it is definitely not
an option to change that at this point imho, even if (which I wouldn't
agree upon) the benefits would outweigh the costs in case this was a
greenfield library.
For some background:
https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8
…On Fri, Mar 13, 2020 at 7:57 PM sliftist ***@***.***> wrote:
(This isn't a serious pull request, just a proof of concept).
mobx uses actions to batch observable changes, but I was thinking it
could have an option to use microtasks to batch changes, at least when
changes are made outside of an action.
I tried it out, and it seems to work, I deployed a super test build at
slift/mobx
<https://github.com/sliftist/mobx#6f3c0a1d65256bc2ec3fd4039f0c6fbd187caa46>
(yarn add sliftist/mobx#6f3c0a1d65256bc2ec3fd4039f0c6fbd187caa46), and am
going to start using that branch in projects to get additional testing.
My implementation isn't meant to be a serious implementation, it basically
just does this:
public reportChanged() {
startBatch()
propagateChanged(this)
endBatch()+ if (globalState.queueReportChangedEndBatchAsMicrotask) {+ Promise.resolve().then(() => {+ endBatch()+ })+ } else {
endBatch()+ }
}
If this is something mobx is interested in pursuing, I would need to make
changes to ensure microtasks are only used outside of actions, instead of
in all reportChanged calls.
Batching changes via a microtask can yield a large performance increase.
For example:
@observable lookup = {};constructor() {
this.runLoop();
autorun(this.reactionHandler.bind(this));
}async runLoop() {
while(true) {
await new Promise((resolve) => setTimeout(resolve, 1000));
for(let key in this.lookup) {
this.lookup[key]++;
}
this.lookup[Date.now()] = 0;
}
}reactionHandler() {
let sum = 0;
for(let key in this.lookup) {
sum += this.lookup[key];
}
console.log("Keys", Object.keys(this.lookup).length, "Sum", sum);
}
This case scales O(N^2) without batching, due to every lookup change
triggering the reactionHandler, but O(N) with batching.
I'm unsure on the full ramifications of this change; if there are parts of
mobx that are incompatible with this approach, or if instead it would be
a relatively stable change (behind a flag), so guidance would be greatly
appreciated!
------------------------------
You can view, comment on, or merge this pull request online at:
#2311
Commit Summary
- Proof of concept of microtasks to truly batch batches.
File Changes
- *M* src/v5/api/configure.ts
<https://github.com/mobxjs/mobx/pull/2311/files#diff-d833908dcbee9f8b4f3b957da35716af>
(6)
- *M* src/v5/core/atom.ts
<https://github.com/mobxjs/mobx/pull/2311/files#diff-89ba7cab2faf03daefa68848448eeaff>
(11)
- *M* src/v5/core/globalstate.ts
<https://github.com/mobxjs/mobx/pull/2311/files#diff-3472ae3e5c8fa3234f5f8a37e4c81390>
(6)
- *M* src/v5/core/observable.ts
<https://github.com/mobxjs/mobx/pull/2311/files#diff-61cae3db5749dfee60bcac0c99498856>
(2)
Patch Links:
- https://github.com/mobxjs/mobx/pull/2311.patch
- https://github.com/mobxjs/mobx/pull/2311.diff
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#2311>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBGNCUECLLNUJHZ22I3RHKF3BANCNFSM4LHJHZ6Q>
.
|
also note that reactions support having a custom scheduler, so that you can
batch individual reactions in the way you want, so what you want can
basically be achieved by creating a small userland utility around reaction,
as far as I can see.
On Sat, Mar 14, 2020 at 11:12 AM Michel Weststrate <mweststrate@gmail.com>
wrote:
… As documented one of the design principles is that MobX is synchronous in
all circumstances, which makes it behavior very predictable and much easier
to debug (a change causing a reaction is always in the same call stack for
example). Doing things async is a valid design choice with it's own merits
as you mentioned. However, in a product like MobX that is already shipped
in thousands of products, such a significant fundamental semantic change
would result in a totally unmanageable support load so it is definitely not
an option to change that at this point imho, even if (which I wouldn't
agree upon) the benefits would outweigh the costs in case this was a
greenfield library.
For some background:
https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8
On Fri, Mar 13, 2020 at 7:57 PM sliftist ***@***.***> wrote:
> (This isn't a serious pull request, just a proof of concept).
>
> mobx uses actions to batch observable changes, but I was thinking it
> could have an option to use microtasks to batch changes, at least when
> changes are made outside of an action.
>
> I tried it out, and it seems to work, I deployed a super test build at
> slift/mobx
> <https://github.com/sliftist/mobx#6f3c0a1d65256bc2ec3fd4039f0c6fbd187caa46>
> (yarn add sliftist/mobx#6f3c0a1d65256bc2ec3fd4039f0c6fbd187caa46), and
> am going to start using that branch in projects to get additional testing.
>
> My implementation isn't meant to be a serious implementation, it
> basically just does this:
>
> public reportChanged() {
> startBatch()
> propagateChanged(this)
> endBatch()+ if (globalState.queueReportChangedEndBatchAsMicrotask) {+ Promise.resolve().then(() => {+ endBatch()+ })+ } else {
> endBatch()+ }
> }
>
> If this is something mobx is interested in pursuing, I would need to
> make changes to ensure microtasks are only used outside of actions, instead
> of in all reportChanged calls.
>
> Batching changes via a microtask can yield a large performance increase.
> For example:
>
> @observable lookup = {};constructor() {
> this.runLoop();
> autorun(this.reactionHandler.bind(this));
> }async runLoop() {
> while(true) {
> await new Promise((resolve) => setTimeout(resolve, 1000));
> for(let key in this.lookup) {
> this.lookup[key]++;
> }
> this.lookup[Date.now()] = 0;
> }
> }reactionHandler() {
> let sum = 0;
> for(let key in this.lookup) {
> sum += this.lookup[key];
> }
> console.log("Keys", Object.keys(this.lookup).length, "Sum", sum);
> }
>
> This case scales O(N^2) without batching, due to every lookup change
> triggering the reactionHandler, but O(N) with batching.
>
> I'm unsure on the full ramifications of this change; if there are parts
> of mobx that are incompatible with this approach, or if instead it would
> be a relatively stable change (behind a flag), so guidance would be greatly
> appreciated!
> ------------------------------
> You can view, comment on, or merge this pull request online at:
>
> #2311
> Commit Summary
>
> - Proof of concept of microtasks to truly batch batches.
>
> File Changes
>
> - *M* src/v5/api/configure.ts
> <https://github.com/mobxjs/mobx/pull/2311/files#diff-d833908dcbee9f8b4f3b957da35716af>
> (6)
> - *M* src/v5/core/atom.ts
> <https://github.com/mobxjs/mobx/pull/2311/files#diff-89ba7cab2faf03daefa68848448eeaff>
> (11)
> - *M* src/v5/core/globalstate.ts
> <https://github.com/mobxjs/mobx/pull/2311/files#diff-3472ae3e5c8fa3234f5f8a37e4c81390>
> (6)
> - *M* src/v5/core/observable.ts
> <https://github.com/mobxjs/mobx/pull/2311/files#diff-61cae3db5749dfee60bcac0c99498856>
> (2)
>
> Patch Links:
>
> - https://github.com/mobxjs/mobx/pull/2311.patch
> - https://github.com/mobxjs/mobx/pull/2311.diff
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <#2311>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAN4NBGNCUECLLNUJHZ22I3RHKF3BANCNFSM4LHJHZ6Q>
> .
>
|
Beyond that, creating abstractions to better support (and thereby
encourage) anti-patterns (" at least when changes are made outside of an
action") isn't something I would aim for in the first place. If you are
concerned about perf, rather enable strict mode first.
On Sat, Mar 14, 2020 at 11:14 AM Michel Weststrate <mweststrate@gmail.com>
wrote:
… also note that reactions support having a custom scheduler, so that you
can batch individual reactions in the way you want, so what you want can
basically be achieved by creating a small userland utility around reaction,
as far as I can see.
On Sat, Mar 14, 2020 at 11:12 AM Michel Weststrate ***@***.***>
wrote:
> As documented one of the design principles is that MobX is synchronous in
> all circumstances, which makes it behavior very predictable and much easier
> to debug (a change causing a reaction is always in the same call stack for
> example). Doing things async is a valid design choice with it's own merits
> as you mentioned. However, in a product like MobX that is already shipped
> in thousands of products, such a significant fundamental semantic change
> would result in a totally unmanageable support load so it is definitely not
> an option to change that at this point imho, even if (which I wouldn't
> agree upon) the benefits would outweigh the costs in case this was a
> greenfield library.
>
> For some background:
> https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8
>
> On Fri, Mar 13, 2020 at 7:57 PM sliftist ***@***.***>
> wrote:
>
>> (This isn't a serious pull request, just a proof of concept).
>>
>> mobx uses actions to batch observable changes, but I was thinking it
>> could have an option to use microtasks to batch changes, at least when
>> changes are made outside of an action.
>>
>> I tried it out, and it seems to work, I deployed a super test build at
>> slift/mobx
>> <https://github.com/sliftist/mobx#6f3c0a1d65256bc2ec3fd4039f0c6fbd187caa46>
>> (yarn add sliftist/mobx#6f3c0a1d65256bc2ec3fd4039f0c6fbd187caa46), and
>> am going to start using that branch in projects to get additional testing.
>>
>> My implementation isn't meant to be a serious implementation, it
>> basically just does this:
>>
>> public reportChanged() {
>> startBatch()
>> propagateChanged(this)
>> endBatch()+ if (globalState.queueReportChangedEndBatchAsMicrotask) {+ Promise.resolve().then(() => {+ endBatch()+ })+ } else {
>> endBatch()+ }
>> }
>>
>> If this is something mobx is interested in pursuing, I would need to
>> make changes to ensure microtasks are only used outside of actions, instead
>> of in all reportChanged calls.
>>
>> Batching changes via a microtask can yield a large performance increase.
>> For example:
>>
>> @observable lookup = {};constructor() {
>> this.runLoop();
>> autorun(this.reactionHandler.bind(this));
>> }async runLoop() {
>> while(true) {
>> await new Promise((resolve) => setTimeout(resolve, 1000));
>> for(let key in this.lookup) {
>> this.lookup[key]++;
>> }
>> this.lookup[Date.now()] = 0;
>> }
>> }reactionHandler() {
>> let sum = 0;
>> for(let key in this.lookup) {
>> sum += this.lookup[key];
>> }
>> console.log("Keys", Object.keys(this.lookup).length, "Sum", sum);
>> }
>>
>> This case scales O(N^2) without batching, due to every lookup change
>> triggering the reactionHandler, but O(N) with batching.
>>
>> I'm unsure on the full ramifications of this change; if there are parts
>> of mobx that are incompatible with this approach, or if instead it
>> would be a relatively stable change (behind a flag), so guidance would be
>> greatly appreciated!
>> ------------------------------
>> You can view, comment on, or merge this pull request online at:
>>
>> #2311
>> Commit Summary
>>
>> - Proof of concept of microtasks to truly batch batches.
>>
>> File Changes
>>
>> - *M* src/v5/api/configure.ts
>> <https://github.com/mobxjs/mobx/pull/2311/files#diff-d833908dcbee9f8b4f3b957da35716af>
>> (6)
>> - *M* src/v5/core/atom.ts
>> <https://github.com/mobxjs/mobx/pull/2311/files#diff-89ba7cab2faf03daefa68848448eeaff>
>> (11)
>> - *M* src/v5/core/globalstate.ts
>> <https://github.com/mobxjs/mobx/pull/2311/files#diff-3472ae3e5c8fa3234f5f8a37e4c81390>
>> (6)
>> - *M* src/v5/core/observable.ts
>> <https://github.com/mobxjs/mobx/pull/2311/files#diff-61cae3db5749dfee60bcac0c99498856>
>> (2)
>>
>> Patch Links:
>>
>> - https://github.com/mobxjs/mobx/pull/2311.patch
>> - https://github.com/mobxjs/mobx/pull/2311.diff
>>
>> —
>> You are receiving this because you are subscribed to this thread.
>> Reply to this email directly, view it on GitHub
>> <#2311>, or unsubscribe
>> <https://github.com/notifications/unsubscribe-auth/AAN4NBGNCUECLLNUJHZ22I3RHKF3BANCNFSM4LHJHZ6Q>
>> .
>>
>
|
@mweststrate One of the things I've been thinking about is that strict mode doesn't fully solve the problem. You could have an action in a class, then loop over a list of instances of that class calling their actions. Strict mode will not complain, but reactions will definitely be slow. Some sort of warning in development mode where > 10 (or N=?) separate actions are detected within one microtask loop would definitely be useful for debugging these cases. |
@mweststrate , ah that makes sense, keeping side effects in the same call stack is indeed very useful. I didn't realize that was specifically a design principle of mobx, in which case I'll just close this pull request. Thanks! For anyone that wants this kind of behavior (as in, batching all reactions), the userland utility to accomplish this is: configure({ reactionScheduler: (callback) => Promise.resolve().then(callback) }) However as @mweststrate mentioned, this will detach the call stack from the original action, and so debugging what line of code triggered a reaction will be much more difficult. Devtools will give the async call stack, so you will be able to find the original action, however the programmatic stack trace (ie, the one you send to the server) from Also this impacts all actions, not just code that is not inside of an action, so existing actions will begin to trigger reactions differently (as in later). @spion , I'm not 100% sure, but it appears that nested actions are 'flattened' to some degree. At least in my limited testing if your outer loop is inside of an action (or wrapped with runInAction) side effects are only triggered after the outer most action finishes. That doesn't help the case if the actions are triggered in an independent fashion though. I agree with @spion that a warning when too many reactions are triggered within a single microtask loop would be very useful. |
I created a pull request to add a warning if too many reactions are triggered with a microtask here: #2312 . |
(This isn't a serious pull request, just a proof of concept).
mobx
uses actions to batch observable changes, but I was thinking it could have an option to use microtasks to batch changes, at least when changes are made outside of an action.I tried it out, and it seems to work, I deployed a highly experimental build at slift/mobx (
yarn add sliftist/mobx#0548ddc4179aa87452fd46f621ebf114f854b7da
), and am going to start using that branch in projects to get additional testing.My implementation isn't meant to be a serious implementation, it basically just does this:
If this is something
mobx
is interested in pursuing, I would need to make changes to ensure microtasks are only used outside of actions, instead of in all reportChanged calls.Batching changes via a microtask can yield a large performance increase. For example:
This case scales
O(N^2)
without batching, due to every lookup change triggering the reactionHandler, butO(N)
with batching.I'm unsure on the full ramifications of this change; if there are parts of
mobx
that are incompatible with this approach, or if instead it would be a relatively stable change (behind a flag), so guidance would be greatly appreciated!Related: