##// END OF EJS Templates
Added priorities to render tasks, revisited rendering scheduler...
cin -
r146:af4f8424e83d v1.9.0 default
parent child
Show More
@@ -8,8 +8,8 const noop = () => { };
8 8 *
9 9 * Use this wrapper to prevent spawning multiple producers.
10 10 *
11 * The emitted values are not cached therefore the new subscriber will not receive
12 * the values emitted before it has been subscribed.
11 * The emitted values are not cached therefore new subscribers will not receive
12 * the values emitted before they had subscribed.
13 13 *
14 14 * @param source The source observable
15 15 * @returns The new observable
@@ -5,7 +5,7 import { isNode, isElementNode } from ".
5 5 import registry = require("dijit/registry");
6 6 import on = require("dojo/on");
7 7 import { Scope } from "./Scope";
8 import { render } from "./render";
8 import { queueRenderTask, getPriority, render } from "./render";
9 9 import { isNull } from "@implab/core-amd/safe";
10 10
11 11 // type Handle = dojo.Handle;
@@ -49,6 +49,8 type _super = {
49 49 export abstract class DjxWidgetBase<Attrs = object, Events = object> extends djbase<_super, _AttachMixin>(_WidgetBase, _AttachMixin) {
50 50 private readonly _scope = new Scope();
51 51
52 private readonly _priority = getPriority() + 1;
53
52 54 buildRendering() {
53 55 const node = render(this.render(), this._scope);
54 56 if (!isElementNode(node))
@@ -71,6 +73,11 export abstract class DjxWidgetBase<Attr
71 73 }
72 74 }
73 75
76 /** Schedules a new deferred rendition within the scope of the widget */
77 scheduleRender(task: () => void) {
78 return queueRenderTask(task, this._scope, this._priority);
79 }
80
74 81 abstract render(): JSX.Element;
75 82
76 83 private _connectEventHandlers() {
@@ -1,7 +1,7
1 1 import { id as mid } from "module";
2 2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 3 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getScope, render, scheduleRender } from "./render";
4 import { queueRenderTask, getPriority, getScope, render } from "./render";
5 5 import { RenditionBase } from "./RenditionBase";
6 6 import { Scope } from "./Scope";
7 7 import { Cancellation } from "@implab/core-amd/Cancellation";
@@ -73,6 +73,8 export class WatchForRendition<T> extend
73 73
74 74 private _ct = Cancellation.none;
75 75
76 private _priority = 0;
77
76 78 constructor({ subject, component, animate, animateIn, animateOut }: WatchForRenditionAttrs<T>) {
77 79 super();
78 80 argumentNotNull(component, "component");
@@ -88,6 +90,7 export class WatchForRendition<T> extend
88 90 }
89 91
90 92 protected _create() {
93 this._priority = getPriority() + 1;
91 94 const scope = getScope();
92 95 scope.own(() => {
93 96 this._itemRenditions.forEach(safeDestroy);
@@ -119,28 +122,24 export class WatchForRendition<T> extend
119 122 if (!this._renderTasks.length) {
120 123 // schedule a new job
121 124 this._renderTasks.push(item);
122 this._render().catch(e => trace.error(e));
125
126 // fork
127 // use dummy scope, because every item will have it's own scope
128 queueRenderTask(this._render, Scope.dummy, this._priority);
123 129 } else {
124 130 // update existing job
125 131 this._renderTasks.push(item);
126 132 }
127 133 };
128 134
129 private async _render() {
130 // fork
131 const beginRender = await scheduleRender();
132 const endRender = beginRender();
133 try {
135 private readonly _render = () => {
134 136 // don't render destroyed rendition
135 137 if (this._ct.isRequested())
136 138 return;
137 139
138 140 this._renderTasks.forEach(this._onRenderItem);
139 141 this._renderTasks.length = 0;
140 } finally {
141 endRender();
142 }
143 }
142 };
144 143
145 144 private readonly _onRenderItem = ({ item, newIndex, prevIndex, animate: _animate }: RenderTask<T>) => {
146 145 const animate = _animate && prevIndex !== newIndex;
@@ -1,15 +1,11
1 import { id as mid } from "module";
2 import { TraceSource } from "@implab/core-amd/log/TraceSource";
3 1 import { argumentNotNull } from "@implab/core-amd/safe";
4 import { getItemDom, getScope, scheduleRender } from "./render";
2 import { queueRenderTask, getItemDom, getPriority, getScope } from "./render";
5 3 import { RenditionBase } from "./RenditionBase";
6 4 import { Scope } from "./Scope";
7 5 import { Subscribable } from "../observable";
8 6 import { Cancellation } from "@implab/core-amd/Cancellation";
9 7 import { collectNodes, destroy, isDocumentFragmentNode, isMounted, placeAt, startupWidgets } from "./traits";
10 8
11 const trace = TraceSource.get(mid);
12
13 9 export class WatchRendition<T> extends RenditionBase<Node> {
14 10 private readonly _component: (arg: T) => unknown;
15 11
@@ -23,6 +19,8 export class WatchRendition<T> extends R
23 19
24 20 private _ct = Cancellation.none;
25 21
22 private _priority = 0;
23
26 24 constructor(component: (arg: T) => unknown, subject: Subscribable<T>) {
27 25 super();
28 26 argumentNotNull(component, "component");
@@ -36,6 +34,8 export class WatchRendition<T> extends R
36 34
37 35 protected _create() {
38 36 const scope = getScope();
37 this._priority = getPriority() + 1;
38
39 39 scope.own(() => {
40 40 this._scope.destroy();
41 41 destroy(this._node);
@@ -48,17 +48,15 export class WatchRendition<T> extends R
48 48 if (!this._renderJob) {
49 49 // schedule a new job
50 50 this._renderJob = { value };
51 this._render().catch(e => trace.error(e));
51 queueRenderTask(this._render, this._scope, this._priority);
52 52 } else {
53 53 // update existing job
54 54 this._renderJob = { value };
55 55 }
56 56 };
57 57
58 private async _render() {
59 const beginRender = await scheduleRender(this._scope);
60 const endRender = beginRender();
61 try {
58 private readonly _render = () => {
59
62 60 // don't render destroyed rendition
63 61 if (this._ct.isRequested())
64 62 return;
@@ -83,10 +81,7 export class WatchRendition<T> extends R
83 81 this._scope.own(() => pending.forEach(destroy));
84 82
85 83 this._renderJob = undefined;
86 } finally {
87 endRender();
88 }
89 }
84 };
90 85
91 86 protected _getDomNode() {
92 87 if (!this._node)
@@ -1,5 +1,4
1 1 import { TraceSource } from "@implab/core-amd/log/TraceSource";
2 import { isPromise } from "@implab/core-amd/safe";
3 2 import { id as mid } from "module";
4 3 import { IScope, Scope } from "./Scope";
5 4 import { isNode, isRendition, isWidget } from "./traits";
@@ -9,108 +8,223 const trace = TraceSource.get(mid);
9 8 interface Context {
10 9 readonly scope: IScope;
11 10
11 readonly priority: number;
12
12 13 readonly hooks?: (() => void)[];
13 14 }
14 15
15 let _context: Context = {
16 scope: Scope.dummy
16 type RenderTask = {
17 /**
18 * The priority for this task
19 */
20 readonly priority: number,
21
22 /**
23 * The rendering action performed in this task
24 */
25 readonly render: () => void;
26 };
27
28 type Range = {
29 /** minimum value in this range */
30 readonly min: number;
31 /** maximum value in this range */
32 readonly max: number;
17 33 };
18 34
35 // empty range
36 const emptyPriorities: Range = { min: NaN, max: NaN };
37
38 // holds render tasks
39 let _renderQueue: RenderTask[] = [];
40
41 // holds the minimum and the maximum task priorities in the queue. Used to
42 // optimize rendering process is all tasks are with the same priority.
43 let _renderQueuePriorities: Range = emptyPriorities;
44
45 // current context
46 let _context: Context = {
47 scope: Scope.dummy,
48 priority: 0
49 };
50
51 // started render operations
19 52 let _renderCount = 0;
53
54 // next id for render operations
20 55 let _renderId = 1;
56
57 // hooks for render completion, executed when all render operations has
58 // been completed
21 59 let _renderedHooks: (() => void)[] = [];
22 60
23
24 const guard = (cb: () => unknown) => {
61 const guard = (cb: () => void) => {
25 62 try {
26 const result = cb();
27 if (isPromise(result)) {
28 const warn = (ret: unknown) => trace.error("The callback {0} competed asynchronously. result = {1}", cb, ret);
29 result.then(warn, warn);
30 }
63 cb();
31 64 } catch (e) {
32 65 trace.error(e);
33 66 }
34 67 };
35 68
36 69 /**
70 * Creates a new rendition context with the specified parameters and makes it
71 * an active context.
37 72 *
38 * @param scope
39 * @returns
40 */
41 export const beginRender = (scope = getScope()) => {
42 const prev = _context;
43 _renderCount++;
44 const renderId = _renderId++;
45 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
46 if (_renderCount === 1)
47 onRendering();
48
49 _context = {
50 scope,
51 hooks: []
52 };
53 return endRender(prev, _context, renderId);
54 };
55
56 /**
57 * Method for a deferred rendering. Returns a promise with `beginRender()` function.
58 * Call to `scheduleRender` will save the current context, and will increment pending
59 * operations counter.
60 *
61 * @example
73 * @see getScope
74 * @see getPriority
62 75 *
63 * const begin = await scheduleRender();
64 * const end = begin();
65 * try {
66 * // do some DOM manipulations
67 * } finally {
68 * end();
69 * }
70 *
71 * @param scope
72 * @returns
76 * @param scope The scope for the current context
77 * @param priority The priority for the current context
78 * @returns The function to restore the previous context and execute pending hooks
73 79 */
74 export const scheduleRender = async (scope = getScope()) => {
75 _renderCount++;
76 const renderId = _renderId ++;
77 trace.debug("scheduleRender [{0}], pending = {1}", renderId, _renderCount);
78 if (_renderCount === 1)
79 onRendering();
80
81 await Promise.resolve();
82
83 return () => {
84 trace.debug("beginRender [{0}], pending = {1}", renderId, _renderCount);
80 const enterContext = (scope: IScope, priority: number) => {
85 81 const prev = _context;
86
87 _context = {
88 scope,
89 hooks: []
90 };
91 return endRender(prev, _context, renderId);
92 };
93 };
94
95 /**
96 * Completes render operation
97 */
98 const endRender = (prev: Context, current: Context, renderId: number) => () => {
99 if (_context !== current)
82 const captured = _context = { scope, priority, hooks: [] };
83 return () => {
84 if (_context !== captured)
100 85 trace.error("endRender mismatched beginRender call");
101 86
102 87 const { hooks } = _context;
103 88 if (hooks)
104 89 hooks.forEach(guard);
105 90
91 _context = prev;
92 };
93 };
94
95 /**
96 * Starts the new render operation. When the operation is started the counter
97 * of running operations is increased. If this is the first render operation
98 * then the `onRendering` event is fired.
99 *
100 * @returns The id of the started rendering operation.
101 */
102 const startRender = () => {
103 _renderCount++;
104 if (_renderCount === 1)
105 onRendering();
106
107 return _renderId++;
108 };
109
110 /**
111 * Completes the rendering operation. When the operation is completed the counter
112 * of running operations is decreased. If there is no more running operations left
113 * then the `onRendered` event is fired.
114 *
115 */
116 const completeRender = () => {
106 117 _renderCount--;
107 _context = prev;
108
109 trace.debug("endRender [{0}], pending = {1}", renderId, _renderCount);
110 118 if (_renderCount === 0)
111 119 onRendered();
112 120 };
113 121
122 /**
123 * Invokes the specified within the rendition context. The rendition context
124 * is created for the task invocation and restored after the task is competed.
125 *
126 * @param scope The cope for the rendition context
127 * @param priority The priority for the rendition context
128 * @returns The result returned by the task
129 */
130 export const renderTask = <T>(task: () => T, scope: IScope, priority: number, renderId: number) => {
131 const restoreContext = enterContext(scope, priority);
132 try {
133 trace.debug("beginRender [{0}], priority = {1}", renderId, priority);
134 return task();
135 } finally {
136 trace.debug("endRender [{0}]", renderId);
137 restoreContext();
138 completeRender();
139 }
140 };
141
142 const processRenderQueue = () => {
143 const q = _renderQueue;
144 const { min, max } = _renderQueuePriorities;
145
146 _renderQueue = [];
147 _renderQueuePriorities = emptyPriorities;
148
149 // all tasks scheduled due queue processing will be queued to the next
150 // processRenderQueue iteration
151 if (min !== max) {
152 // if there are tasks with different priorities in the queue
153 trace.debug("Processing render queue, {0} tasks, priorities=[{1}..{2}] ", q.length, min, max);
154 q.sort(({ priority: a }, { priority: b }) => a - b).forEach(({ render }) => guard(render));
155 } else {
156 // all tasks are have same priority
157 trace.debug("Processing render queue, {0} tasks, priority = {1} ", q.length, min);
158 q.forEach(({ render }) => guard(render));
159 }
160
161 if (_renderQueue.length)
162 trace.debug("Render queue is processed, {0} tasks rescheduled", _renderQueue.length);
163 };
164
165 /**
166 * Adds the specified task to the render queue. The task will be added with the
167 * specified priority.
168 *
169 * Render queue contains a list of render tasks. Each task is executed within
170 * its own rendering context with the specified scope. The priority determines
171 * the order in which tasks will be executed where the tasks with lower priority
172 * numbers are executed first.
173 *
174 * When the queue is empty and the task is added then the render queue will be
175 * scheduled for execution. While the current queue is being executed all
176 * enqueued tasks are added to the new queue and processed after the current
177 * execution has been completed.
178 *
179 * @param task The action to execute. This action will be executed with
180 * {@link renderTask} function in its own rendering context.
181 * @param scope The scope used to create a rendering context for the task.
182 * @param priority The priority
183 */
184 export const queueRenderTask = (task: () => void, scope = Scope.dummy, priority = getPriority()) => {
185 const renderId = startRender();
186 trace.debug("scheduleRender [{0}], priority = {1}", renderId, priority);
187
188 const render = () => renderTask(task, scope, priority, renderId);
189
190 if (!_renderQueue.length) {
191 // this is the first task, schedule next render queue processing
192 Promise.resolve().then(processRenderQueue, e => trace.error(e));
193
194 // initialize priorities
195 _renderQueuePriorities = { min: priority, max: priority };
196 } else {
197
198 // update priorities if needed
199 const { min, max } = _renderQueuePriorities;
200 if (priority < min)
201 _renderQueuePriorities = { min: priority, max };
202 else if (priority > max)
203 _renderQueuePriorities = { min, max: priority };
204 }
205
206 _renderQueue.push({ priority, render });
207 };
208
209 /**
210 * Starts the synchronous rendering process with the specified scope and priority.
211 *
212 * @param scope The scope for the current rendition
213 * @param priority The priority for the current scope
214 * @returns The function to complete the current rendering
215 */
216 export const beginRender = (scope = Scope.dummy, priority = 0) => {
217 const renderId = startRender();
218 const restoreContext = enterContext(scope, priority);
219 trace.debug("beginRender [{0}], priority = {1}", renderId, priority);
220
221 return () => {
222 trace.debug("endRender [{0}]", renderId);
223 restoreContext();
224 completeRender();
225 };
226 };
227
114 228 // called when the first beginRender is called for this iteration
115 229 const onRendering = () => {
116 230 trace.log("Rendering started");
@@ -127,6 +241,7 const onRendered = () => {
127 241 _renderedHooks = [];
128 242 };
129 243
244 /** Returns promise when the rendering has been completed. */
130 245 export const whenRendered = () => new Promise<void>((resolve) => {
131 246 if (_renderCount)
132 247 _renderedHooks.push(resolve);
@@ -134,6 +249,12 export const whenRendered = () => new Pr
134 249 resolve();
135 250 });
136 251
252 /**
253 * Registers hook which is called after the render operation is completed. The
254 * hook will be called once only for the current operation.
255 *
256 * @param hook The hook which should be called when rendering is complete.
257 */
137 258 export const renderHook = (hook: () => void) => {
138 259 const { hooks } = _context;
139 260 if (hooks)
@@ -142,6 +263,16 export const renderHook = (hook: () => v
142 263 guard(hook);
143 264 };
144 265
266 /**
267 * Registers special hook which will be called with the specified state. The
268 * hook is called once after the rendering is complete. When the rendition is
269 * destroyed the hook is called with the undefined parameter.
270 *
271 * This function is used to register `ref` hooks form a tsx rendition.
272 *
273 * @param value The state which will be supplied as a parameter for the hook
274 * @param ref reference hook
275 */
145 276 export const refHook = <T>(value: T, ref: JSX.Ref<T>) => {
146 277 const { hooks, scope } = _context;
147 278 if (hooks)
@@ -152,21 +283,24 export const refHook = <T>(value: T, ref
152 283 scope.own(() => ref(undefined));
153 284 };
154 285
155 /** Returns the current scope */
286 /** Returns the current scope. Scope is used to track resources bound to the
287 * current rendering. When the rendering is destroyed the scope is cleaned and
288 * all bound resources are released.
289 */
156 290 export const getScope = () => _context.scope;
157 291
292 /**
293 * Returns the current render task priority. This value is used by some renditions
294 * to schedule asynchronous nested updates with lower priority then themselves.
295 */
296 export const getPriority = () => _context.priority;
297
158 298 /** Schedules the rendition to be rendered to the DOM Node
159 299 * @param rendition The rendition to be rendered
160 300 * @param scope The scope
161 301 */
162 export const render = (rendition: unknown, scope = Scope.dummy) => {
163 const complete = beginRender(scope);
164 try {
165 return getItemDom(rendition);
166 } finally {
167 complete();
168 }
169 };
302 export const render = (rendition: unknown, scope = Scope.dummy) =>
303 renderTask(() => getItemDom(rendition), scope, getPriority(), startRender());
170 304
171 305 const emptyFragment = document.createDocumentFragment();
172 306
@@ -41,9 +41,17 export class MainContext implements IDes
41 41 );
42 42 }
43 43
44 async removeAppointment(appointmentId: string) {
45 await delay(10);
46 this._members.query({ appointmentId })
47 .map(m => this._members.getIdentity(m))
48 .forEach(id => this._members.remove(id));
49 this._appointments.remove(appointmentId);
50 }
51
44 52 async load() {
45 await Promise.resolve();
46 for (let i = 0; i < 2; i++) {
53 await delay(10);
54 for (let i = 0; i < 5; i++) {
47 55 const id = Uuid();
48 56 this._appointments.add({
49 57 id,
@@ -51,6 +59,16 export class MainContext implements IDes
51 59 duration: 30,
52 60 title: `Hello ${i+1}`
53 61 });
62
63 for (let ii = 0; ii < 3; ii++)
64
65 this._members.add({
66 appointmentId: id,
67 email: "some@no.mail",
68 name: `Peter ${ii}`,
69 position: "Manager",
70 role: "participant"
71 }, { id: Uuid() });
54 72 }
55 73 }
56 74
@@ -64,6 +64,10 export default class MainModel implement
64 64 .catch(error(trace));
65 65 }
66 66
67 removeAppointment(appointmentId: string) {
68 this._context.removeAppointment(appointmentId).catch(error(trace));
69 }
70
67 71
68 72 load() {
69 73 this._context.load().catch(error(trace));
@@ -1,3 +1,4
1 import { id as mid } from "module";
1 2 import { djbase, djclass } from "@implab/djx/declare";
2 3 import { DjxWidgetBase } from "@implab/djx/tsx/DjxWidgetBase";
3 4 import { attach, bind, createElement, prop, watch, watchFor } from "@implab/djx/tsx";
@@ -8,6 +9,9 import { Appointment } from "../model/Ap
8 9 import { LocalDate } from "@js-joda/core";
9 10 import Button = require("dijit/form/Button");
10 11 import NewAppointment from "./NewAppointment";
12 import { TraceSource } from "@implab/core-amd/log/TraceSource";
13
14 const trace = TraceSource.get(mid);
11 15
12 16 @djclass
13 17 export default class MainWidget extends djbase(DjxWidgetBase) {
@@ -46,6 +50,7 export default class MainWidget extends
46 50 </ul>
47 51 <div>
48 52 <Button onClick={() => this._onAddMemberClick(id)}>Add member</Button>
53 <Button onClick={() => this._onRemoveAppointmentClick(id)}>Remove appointment</Button>
49 54 </div>
50 55 </li>
51 56 )}
@@ -59,6 +64,7 export default class MainWidget extends
59 64 }
60 65
61 66 load() {
67 trace.log("Loading data");
62 68 this.model.load();
63 69 }
64 70
@@ -71,6 +77,10 export default class MainWidget extends
71 77 });
72 78 };
73 79
80 private readonly _onRemoveAppointmentClick = (appointmentId: string) => {
81 this.model.removeAppointment(appointmentId);
82 };
83
74 84 private readonly _onAddAppointmentClick = () => {
75 85 this.model.addAppointment("Appointment", new Date, 30);
76 86 };
General Comments 0
You need to be logged in to leave comments. Login now