|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /** |
|
6 * ProgressReporter |
|
7 * |
|
8 * This class is used by long-winded tasks to report progress to observers. |
|
9 * If a task has subtasks that want to report their own progress, these |
|
10 * subtasks can have their own progress reporters which are hooked up to the |
|
11 * parent progress reporter, resulting in a tree structure. A parent progress |
|
12 * reporter will calculate its progress value as a weighted sum of its |
|
13 * subreporters' progress values. |
|
14 * |
|
15 * A progress reporter has a state, an action, and a progress value. |
|
16 * |
|
17 * - state is one of STATE_WAITING, STATE_DOING and STATE_FINISHED. |
|
18 * - action is a string that describes the current task. |
|
19 * - progress is the progress value as a number between 0 and 1, or NaN if |
|
20 * indeterminate. |
|
21 * |
|
22 * A progress reporter starts out in the WAITING state. The DOING state is |
|
23 * entered with the begin method which also sets the action. While the task is |
|
24 * executing, the progress value can be updated with the setProgress method. |
|
25 * When a task has finished, it can call the finish method which is just a |
|
26 * shorthand for setProgress(1); this will set the state to FINISHED. |
|
27 * |
|
28 * Progress observers can be added with the addListener method which takes a |
|
29 * function callback. Whenever the progress value or state change, all |
|
30 * listener callbacks will be called with the progress reporter object. The |
|
31 * observer can get state, progress value and action by calling the getter |
|
32 * methods getState(), getProgress() and getAction(). |
|
33 * |
|
34 * Creating child progress reporters for subtasks can be done with the |
|
35 * addSubreporter(s) methods. If a progress reporter has subreporters, normal |
|
36 * progress report functions (setProgress and finish) can no longer be called. |
|
37 * Instead, the parent reporter will listen to progress changes on its |
|
38 * subreporters and update its state automatically, and then notify its own |
|
39 * listeners. |
|
40 * When adding a subreporter, you are expected to provide an estimated |
|
41 * duration for the subtask. This value will be used as a weight when |
|
42 * calculating the progress of the parent reporter. |
|
43 */ |
|
44 |
|
45 "use strict"; |
|
46 |
|
47 const gDebugExpectedDurations = false; |
|
48 |
|
49 function ProgressReporter() { |
|
50 this._observers = []; |
|
51 this._subreporters = []; |
|
52 this._subreporterExpectedDurationsSum = 0; |
|
53 this._progress = 0; |
|
54 this._state = ProgressReporter.STATE_WAITING; |
|
55 this._action = ""; |
|
56 } |
|
57 |
|
58 ProgressReporter.STATE_WAITING = 0; |
|
59 ProgressReporter.STATE_DOING = 1; |
|
60 ProgressReporter.STATE_FINISHED = 2; |
|
61 |
|
62 ProgressReporter.prototype = { |
|
63 getProgress: function () { |
|
64 return this._progress; |
|
65 }, |
|
66 getState: function () { |
|
67 return this._state; |
|
68 }, |
|
69 setAction: function (action) { |
|
70 this._action = action; |
|
71 this._reportProgress(); |
|
72 }, |
|
73 getAction: function () { |
|
74 switch (this._state) { |
|
75 case ProgressReporter.STATE_WAITING: |
|
76 return "Waiting for preceding tasks to finish..."; |
|
77 case ProgressReporter.STATE_DOING: |
|
78 return this._action; |
|
79 case ProgressReporter.STATE_FINISHED: |
|
80 return "Finished."; |
|
81 default: |
|
82 throw "Broken state"; |
|
83 } |
|
84 }, |
|
85 addListener: function (callback) { |
|
86 this._observers.push(callback); |
|
87 }, |
|
88 addSubreporter: function (expectedDuration) { |
|
89 this._subreporterExpectedDurationsSum += expectedDuration; |
|
90 var subreporter = new ProgressReporter(); |
|
91 var self = this; |
|
92 subreporter.addListener(function (progress) { |
|
93 self._recalculateProgressFromSubreporters(); |
|
94 self._recalculateStateAndActionFromSubreporters(); |
|
95 self._reportProgress(); |
|
96 }); |
|
97 this._subreporters.push({ expectedDuration: expectedDuration, reporter: subreporter }); |
|
98 return subreporter; |
|
99 }, |
|
100 addSubreporters: function (expectedDurations) { |
|
101 var reporters = {}; |
|
102 for (var key in expectedDurations) { |
|
103 reporters[key] = this.addSubreporter(expectedDurations[key]); |
|
104 } |
|
105 return reporters; |
|
106 }, |
|
107 begin: function (action) { |
|
108 this._startTime = Date.now(); |
|
109 this._state = ProgressReporter.STATE_DOING; |
|
110 this._action = action; |
|
111 this._reportProgress(); |
|
112 }, |
|
113 setProgress: function (progress) { |
|
114 if (this._subreporters.length > 0) |
|
115 throw "Can't call setProgress on a progress reporter with subreporters"; |
|
116 if (progress != this._progress && |
|
117 (progress == 1 || |
|
118 (isNaN(progress) != isNaN(this._progress)) || |
|
119 (progress - this._progress >= 0.01))) { |
|
120 this._progress = progress; |
|
121 if (progress == 1) |
|
122 this._transitionToFinished(); |
|
123 this._reportProgress(); |
|
124 } |
|
125 }, |
|
126 finish: function () { |
|
127 this.setProgress(1); |
|
128 }, |
|
129 _recalculateProgressFromSubreporters: function () { |
|
130 if (this._subreporters.length == 0) |
|
131 throw "Can't _recalculateProgressFromSubreporters on a progress reporter without any subreporters"; |
|
132 this._progress = 0; |
|
133 for (var i = 0; i < this._subreporters.length; i++) { |
|
134 var expectedDuration = this._subreporters[i].expectedDuration; |
|
135 var reporter = this._subreporters[i].reporter; |
|
136 this._progress += reporter.getProgress() * expectedDuration / this._subreporterExpectedDurationsSum; |
|
137 } |
|
138 }, |
|
139 _recalculateStateAndActionFromSubreporters: function () { |
|
140 if (this._subreporters.length == 0) |
|
141 throw "Can't _recalculateStateAndActionFromSubreporters on a progress reporter without any subreporters"; |
|
142 var actions = []; |
|
143 var allWaiting = true; |
|
144 var allFinished = true; |
|
145 for (var i = 0; i < this._subreporters.length; i++) { |
|
146 var expectedDuration = this._subreporters[i].expectedDuration; |
|
147 var reporter = this._subreporters[i].reporter; |
|
148 var state = reporter.getState(); |
|
149 if (state != ProgressReporter.STATE_WAITING) |
|
150 allWaiting = false; |
|
151 if (state != ProgressReporter.STATE_FINISHED) |
|
152 allFinished = false; |
|
153 if (state == ProgressReporter.STATE_DOING) |
|
154 actions.push(reporter.getAction()); |
|
155 } |
|
156 if (allFinished) { |
|
157 this._transitionToFinished(); |
|
158 } else if (!allWaiting) { |
|
159 this._state = ProgressReporter.STATE_DOING; |
|
160 if (actions.length == 0) { |
|
161 this._action = "About to start next task..." |
|
162 } else { |
|
163 this._action = actions.join("\n"); |
|
164 } |
|
165 } |
|
166 }, |
|
167 _transitionToFinished: function () { |
|
168 this._state = ProgressReporter.STATE_FINISHED; |
|
169 |
|
170 if (gDebugExpectedDurations) { |
|
171 this._realDuration = Date.now() - this._startTime; |
|
172 if (this._subreporters.length) { |
|
173 for (var i = 0; i < this._subreporters.length; i++) { |
|
174 var expectedDuration = this._subreporters[i].expectedDuration; |
|
175 var reporter = this._subreporters[i].reporter; |
|
176 var realDuration = reporter._realDuration; |
|
177 dump("For reporter with expectedDuration " + expectedDuration + ", real duration was " + realDuration + "\n"); |
|
178 } |
|
179 } |
|
180 } |
|
181 }, |
|
182 _reportProgress: function () { |
|
183 for (var i = 0; i < this._observers.length; i++) { |
|
184 this._observers[i](this); |
|
185 } |
|
186 }, |
|
187 }; |