michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * ProgressReporter michael@0: * michael@0: * This class is used by long-winded tasks to report progress to observers. michael@0: * If a task has subtasks that want to report their own progress, these michael@0: * subtasks can have their own progress reporters which are hooked up to the michael@0: * parent progress reporter, resulting in a tree structure. A parent progress michael@0: * reporter will calculate its progress value as a weighted sum of its michael@0: * subreporters' progress values. michael@0: * michael@0: * A progress reporter has a state, an action, and a progress value. michael@0: * michael@0: * - state is one of STATE_WAITING, STATE_DOING and STATE_FINISHED. michael@0: * - action is a string that describes the current task. michael@0: * - progress is the progress value as a number between 0 and 1, or NaN if michael@0: * indeterminate. michael@0: * michael@0: * A progress reporter starts out in the WAITING state. The DOING state is michael@0: * entered with the begin method which also sets the action. While the task is michael@0: * executing, the progress value can be updated with the setProgress method. michael@0: * When a task has finished, it can call the finish method which is just a michael@0: * shorthand for setProgress(1); this will set the state to FINISHED. michael@0: * michael@0: * Progress observers can be added with the addListener method which takes a michael@0: * function callback. Whenever the progress value or state change, all michael@0: * listener callbacks will be called with the progress reporter object. The michael@0: * observer can get state, progress value and action by calling the getter michael@0: * methods getState(), getProgress() and getAction(). michael@0: * michael@0: * Creating child progress reporters for subtasks can be done with the michael@0: * addSubreporter(s) methods. If a progress reporter has subreporters, normal michael@0: * progress report functions (setProgress and finish) can no longer be called. michael@0: * Instead, the parent reporter will listen to progress changes on its michael@0: * subreporters and update its state automatically, and then notify its own michael@0: * listeners. michael@0: * When adding a subreporter, you are expected to provide an estimated michael@0: * duration for the subtask. This value will be used as a weight when michael@0: * calculating the progress of the parent reporter. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: const gDebugExpectedDurations = false; michael@0: michael@0: function ProgressReporter() { michael@0: this._observers = []; michael@0: this._subreporters = []; michael@0: this._subreporterExpectedDurationsSum = 0; michael@0: this._progress = 0; michael@0: this._state = ProgressReporter.STATE_WAITING; michael@0: this._action = ""; michael@0: } michael@0: michael@0: ProgressReporter.STATE_WAITING = 0; michael@0: ProgressReporter.STATE_DOING = 1; michael@0: ProgressReporter.STATE_FINISHED = 2; michael@0: michael@0: ProgressReporter.prototype = { michael@0: getProgress: function () { michael@0: return this._progress; michael@0: }, michael@0: getState: function () { michael@0: return this._state; michael@0: }, michael@0: setAction: function (action) { michael@0: this._action = action; michael@0: this._reportProgress(); michael@0: }, michael@0: getAction: function () { michael@0: switch (this._state) { michael@0: case ProgressReporter.STATE_WAITING: michael@0: return "Waiting for preceding tasks to finish..."; michael@0: case ProgressReporter.STATE_DOING: michael@0: return this._action; michael@0: case ProgressReporter.STATE_FINISHED: michael@0: return "Finished."; michael@0: default: michael@0: throw "Broken state"; michael@0: } michael@0: }, michael@0: addListener: function (callback) { michael@0: this._observers.push(callback); michael@0: }, michael@0: addSubreporter: function (expectedDuration) { michael@0: this._subreporterExpectedDurationsSum += expectedDuration; michael@0: var subreporter = new ProgressReporter(); michael@0: var self = this; michael@0: subreporter.addListener(function (progress) { michael@0: self._recalculateProgressFromSubreporters(); michael@0: self._recalculateStateAndActionFromSubreporters(); michael@0: self._reportProgress(); michael@0: }); michael@0: this._subreporters.push({ expectedDuration: expectedDuration, reporter: subreporter }); michael@0: return subreporter; michael@0: }, michael@0: addSubreporters: function (expectedDurations) { michael@0: var reporters = {}; michael@0: for (var key in expectedDurations) { michael@0: reporters[key] = this.addSubreporter(expectedDurations[key]); michael@0: } michael@0: return reporters; michael@0: }, michael@0: begin: function (action) { michael@0: this._startTime = Date.now(); michael@0: this._state = ProgressReporter.STATE_DOING; michael@0: this._action = action; michael@0: this._reportProgress(); michael@0: }, michael@0: setProgress: function (progress) { michael@0: if (this._subreporters.length > 0) michael@0: throw "Can't call setProgress on a progress reporter with subreporters"; michael@0: if (progress != this._progress && michael@0: (progress == 1 || michael@0: (isNaN(progress) != isNaN(this._progress)) || michael@0: (progress - this._progress >= 0.01))) { michael@0: this._progress = progress; michael@0: if (progress == 1) michael@0: this._transitionToFinished(); michael@0: this._reportProgress(); michael@0: } michael@0: }, michael@0: finish: function () { michael@0: this.setProgress(1); michael@0: }, michael@0: _recalculateProgressFromSubreporters: function () { michael@0: if (this._subreporters.length == 0) michael@0: throw "Can't _recalculateProgressFromSubreporters on a progress reporter without any subreporters"; michael@0: this._progress = 0; michael@0: for (var i = 0; i < this._subreporters.length; i++) { michael@0: var expectedDuration = this._subreporters[i].expectedDuration; michael@0: var reporter = this._subreporters[i].reporter; michael@0: this._progress += reporter.getProgress() * expectedDuration / this._subreporterExpectedDurationsSum; michael@0: } michael@0: }, michael@0: _recalculateStateAndActionFromSubreporters: function () { michael@0: if (this._subreporters.length == 0) michael@0: throw "Can't _recalculateStateAndActionFromSubreporters on a progress reporter without any subreporters"; michael@0: var actions = []; michael@0: var allWaiting = true; michael@0: var allFinished = true; michael@0: for (var i = 0; i < this._subreporters.length; i++) { michael@0: var expectedDuration = this._subreporters[i].expectedDuration; michael@0: var reporter = this._subreporters[i].reporter; michael@0: var state = reporter.getState(); michael@0: if (state != ProgressReporter.STATE_WAITING) michael@0: allWaiting = false; michael@0: if (state != ProgressReporter.STATE_FINISHED) michael@0: allFinished = false; michael@0: if (state == ProgressReporter.STATE_DOING) michael@0: actions.push(reporter.getAction()); michael@0: } michael@0: if (allFinished) { michael@0: this._transitionToFinished(); michael@0: } else if (!allWaiting) { michael@0: this._state = ProgressReporter.STATE_DOING; michael@0: if (actions.length == 0) { michael@0: this._action = "About to start next task..." michael@0: } else { michael@0: this._action = actions.join("\n"); michael@0: } michael@0: } michael@0: }, michael@0: _transitionToFinished: function () { michael@0: this._state = ProgressReporter.STATE_FINISHED; michael@0: michael@0: if (gDebugExpectedDurations) { michael@0: this._realDuration = Date.now() - this._startTime; michael@0: if (this._subreporters.length) { michael@0: for (var i = 0; i < this._subreporters.length; i++) { michael@0: var expectedDuration = this._subreporters[i].expectedDuration; michael@0: var reporter = this._subreporters[i].reporter; michael@0: var realDuration = reporter._realDuration; michael@0: dump("For reporter with expectedDuration " + expectedDuration + ", real duration was " + realDuration + "\n"); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: _reportProgress: function () { michael@0: for (var i = 0; i < this._observers.length; i++) { michael@0: this._observers[i](this); michael@0: } michael@0: }, michael@0: };