Skip to content
Snippets Groups Projects
Commit 61d8513c authored by David Sehnal's avatar David Sehnal
Browse files

mol-state: optimized state updates when only one transform changes

parent 502a4b0f
No related branches found
No related tags found
No related merge requests found
...@@ -37,7 +37,7 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { ...@@ -37,7 +37,7 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
} }
export class _test_CurrentObject extends PluginComponent { export class _test_CurrentObject extends PluginComponent {
init() { componentDidMount() {
this.subscribe(this.context.behaviors.state.data.currentObject, () => this.forceUpdate()); this.subscribe(this.context.behaviors.state.data.currentObject, () => this.forceUpdate());
} }
......
...@@ -11,7 +11,7 @@ import { PluginCommands } from 'mol-plugin/command'; ...@@ -11,7 +11,7 @@ import { PluginCommands } from 'mol-plugin/command';
import { PluginComponent } from './base'; import { PluginComponent } from './base';
export class StateTree extends PluginComponent<{ state: State }, { }> { export class StateTree extends PluginComponent<{ state: State }, { }> {
init() { componentDidMount() {
this.subscribe(this.props.state.events.changed, () => this.forceUpdate()); this.subscribe(this.props.state.events.changed, () => this.forceUpdate());
} }
......
...@@ -21,6 +21,8 @@ export { State } ...@@ -21,6 +21,8 @@ export { State }
class State { class State {
private _tree: StateTree = StateTree.createEmpty(); private _tree: StateTree = StateTree.createEmpty();
private _current: Transform.Ref = this._tree.root.ref; private _current: Transform.Ref = this._tree.root.ref;
protected errorFree = true;
private transformCache = new Map<Transform.Ref, unknown>(); private transformCache = new Map<Transform.Ref, unknown>();
private ev = RxEventHelper.create(); private ev = RxEventHelper.create();
...@@ -106,13 +108,18 @@ class State { ...@@ -106,13 +108,18 @@ class State {
const ctx: UpdateContext = { const ctx: UpdateContext = {
parent: this, parent: this,
errorFree: this.errorFree,
taskCtx, taskCtx,
oldTree, oldTree,
tree: _tree, tree: _tree,
cells: this.cells as Map<Transform.Ref, StateObjectCell>, cells: this.cells as Map<Transform.Ref, StateObjectCell>,
transformCache: this.transformCache, transformCache: this.transformCache,
changed: false changed: false,
editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0
}; };
this.errorFree = true;
// TODO: handle "cancelled" error? Or would this be handled automatically? // TODO: handle "cancelled" error? Or would this be handled automatically?
updated = await update(ctx); updated = await update(ctx);
} finally { } finally {
...@@ -162,37 +169,51 @@ type Ref = Transform.Ref ...@@ -162,37 +169,51 @@ type Ref = Transform.Ref
interface UpdateContext { interface UpdateContext {
parent: State, parent: State,
errorFree: boolean,
taskCtx: RuntimeContext, taskCtx: RuntimeContext,
oldTree: StateTree, oldTree: StateTree,
tree: StateTree, tree: StateTree,
cells: Map<Transform.Ref, StateObjectCell>, cells: Map<Transform.Ref, StateObjectCell>,
transformCache: Map<Ref, unknown>, transformCache: Map<Ref, unknown>,
changed: boolean changed: boolean,
editInfo: StateTreeBuilder.EditInfo | undefined
} }
async function update(ctx: UpdateContext) { async function update(ctx: UpdateContext) {
// 1: find all nodes that will definitely be deleted.
// this is done in "post order", meaning that leaves will be deleted first.
const deletes = findDeletes(ctx);
for (const d of deletes) {
const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
ctx.cells.delete(d);
ctx.transformCache.delete(d);
ctx.parent.events.object.removed.next({ state: ctx.parent, ref: d, obj });
// TODO: handle current object change
}
// 2: Find roots where transform version changed or where nodes will be added. // if only a single node was added/updated, we can skip potentially expensive diffing
const roots = findUpdateRoots(ctx.cells, ctx.tree); const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree);
let deletes: Transform.Ref[], roots: Transform.Ref[];
// 3: Init empty cells where not present if (fastTrack) {
deletes = [];
roots = [ctx.editInfo!.lastUpdate!];
} else {
// find all nodes that will definitely be deleted.
// this is done in "post order", meaning that leaves will be deleted first.
deletes = findDeletes(ctx);
for (const d of deletes) {
const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
ctx.cells.delete(d);
ctx.transformCache.delete(d);
ctx.parent.events.object.removed.next({ state: ctx.parent, ref: d, obj });
// TODO: handle current object change
}
// Find roots where transform version changed or where nodes will be added.
roots = findUpdateRoots(ctx.cells, ctx.tree);
}
// Init empty cells where not present
// this is done in "pre order", meaning that "parents" will be created 1st. // this is done in "pre order", meaning that "parents" will be created 1st.
initCells(ctx, roots); initCells(ctx, roots);
// 4: Set status of cells that will be updated to 'pending'. // Set status of cells that will be updated to 'pending'.
initCellStatus(ctx, roots); initCellStatus(ctx, roots);
// 6: Sequentially update all the subtrees. // Sequentially update all the subtrees.
for (const root of roots) { for (const root of roots) {
await updateSubtree(ctx, root); await updateSubtree(ctx, root);
} }
...@@ -266,7 +287,10 @@ function initCells(ctx: UpdateContext, roots: Ref[]) { ...@@ -266,7 +287,10 @@ function initCells(ctx: UpdateContext, roots: Ref[]) {
/** Set status and error text of the cell. Remove all existing objects in the subtree. */ /** Set status and error text of the cell. Remove all existing objects in the subtree. */
function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) {
if (errorText) setCellStatus(ctx, ref, 'error', errorText); if (errorText) {
(ctx.parent as any as { errorFree: boolean }).errorFree = false;
setCellStatus(ctx, ref, 'error', errorText);
}
const cell = ctx.cells.get(ref)!; const cell = ctx.cells.get(ref)!;
if (cell.obj) { if (cell.obj) {
......
...@@ -14,12 +14,20 @@ import { shallowEqual } from 'mol-util'; ...@@ -14,12 +14,20 @@ import { shallowEqual } from 'mol-util';
export { StateTreeBuilder } export { StateTreeBuilder }
interface StateTreeBuilder { interface StateTreeBuilder {
readonly editInfo: StateTreeBuilder.EditInfo,
getTree(): StateTree getTree(): StateTree
} }
namespace StateTreeBuilder { namespace StateTreeBuilder {
export interface EditInfo {
sourceTree: StateTree,
count: number,
lastUpdate?: Transform.Ref
}
interface State { interface State {
tree: TransientTree tree: TransientTree,
editInfo: EditInfo
} }
export function is(obj: any): obj is StateTreeBuilder { export function is(obj: any): obj is StateTreeBuilder {
...@@ -28,20 +36,27 @@ namespace StateTreeBuilder { ...@@ -28,20 +36,27 @@ namespace StateTreeBuilder {
export class Root implements StateTreeBuilder { export class Root implements StateTreeBuilder {
private state: State; private state: State;
get editInfo() { return this.state.editInfo; }
to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); } to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); } toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
delete(ref: Transform.Ref) { delete(ref: Transform.Ref) {
this.editInfo.count++;
this.state.tree.remove(ref); this.state.tree.remove(ref);
return this; return this;
} }
getTree(): StateTree { return this.state.tree.asImmutable(); } getTree(): StateTree { return this.state.tree.asImmutable(); }
constructor(tree: StateTree) { this.state = { tree: tree.asTransient() } } constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
} }
export class To<A extends StateObject> implements StateTreeBuilder { export class To<A extends StateObject> implements StateTreeBuilder {
get editInfo() { return this.state.editInfo; }
apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Options>): To<Transformer.To<T>> { apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Options>): To<Transformer.To<T>> {
const t = tr.apply(this.ref, params, props); const t = tr.apply(this.ref, params, props);
this.state.tree.add(t); this.state.tree.add(t);
this.editInfo.count++;
this.editInfo.lastUpdate = t.ref;
return new To(this.state, t.ref, this.root); return new To(this.state, t.ref, this.root);
} }
...@@ -64,6 +79,9 @@ namespace StateTreeBuilder { ...@@ -64,6 +79,9 @@ namespace StateTreeBuilder {
} }
} }
this.editInfo.count++;
this.editInfo.lastUpdate = this.ref;
this.state.tree.set(Transform.updateParams(old, params)); this.state.tree.set(Transform.updateParams(old, params));
return this.root; return this.root;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment