7204e58575075d13439fdba1ff1698f389b9b578
[moodle.git] / backup / util / progress / core_backup_progress.class.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Base class for handling progress information during a backup and restore.
19  *
20  * Subclasses should generally override the current_progress function which
21  * summarises all progress information.
22  *
23  * @package core_backup
24  * @copyright 2013 The Open University
25  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
27 abstract class core_backup_progress {
28     /**
29      * @var int Constant indicating that the number of progress calls is unknown.
30      */
31     const INDETERMINATE = -1;
33     /**
34      * This value is set rather high to ensure there are no regressions from
35      * previous behaviour. For testing, it may be useful to set the
36      * frontendservertimeout config option to a lower value, such as 180
37      * seconds (default for some commercial products).
38      *
39      * @var int The number of seconds that can pass without progress() calls.
40      */
41     const TIME_LIMIT_WITHOUT_PROGRESS = 3600;
43     /**
44      * @var int Time of last progress call.
45      */
46     protected $lastprogresstime;
48     /**
49      * @var int Number of progress calls (restricted to ~ 1/second).
50      */
51     protected $count;
53     /**
54      * @var array Array of progress descriptions for each stack level.
55      */
56     protected $descriptions = array();
58     /**
59      * @var array Array of maximum progress values for each stack level.
60      */
61     protected $maxes = array();
63     /**
64      * @var array Array of current progress values.
65      */
66     protected $currents = array();
68     /**
69      * @var int Array of counts within parent progress entry (ignored for first)
70      */
71     protected $parentcounts = array();
73     /**
74      * Marks the start of an operation that will display progress.
75      *
76      * This can be called multiple times for nested progress sections. It must
77      * be paired with calls to end_progress.
78      *
79      * The progress maximum may be INDETERMINATE if the current operation has
80      * an unknown number of steps. (This is default.)
81      *
82      * Calling this function will always result in a new display, so this
83      * should not be called exceedingly frequently.
84      *
85      * When it is complete by calling end_progress, each start_progress section
86      * automatically adds progress to its parent, as defined by $parentcount.
87      *
88      * @param string $description Description to display
89      * @param int $max Maximum value of progress for this section
90      * @param int $parentcount How many progress points this section counts for
91      * @throws coding_exception If max is invalid
92      */
93     public function start_progress($description, $max = self::INDETERMINATE,
94             $parentcount = 1) {
95         if ($max != self::INDETERMINATE && $max < 0) {
96             throw new coding_exception(
97                     'start_progress() max value cannot be negative');
98         }
99         if ($parentcount < 1) {
100             throw new coding_exception(
101                     'start_progress() parent progress count must be at least 1');
102         }
103         if (!empty($this->descriptions)) {
104             $prevmax = end($this->maxes);
105             if ($prevmax !== self::INDETERMINATE) {
106                 $prevcurrent = end($this->currents);
107                 if ($prevcurrent + $parentcount > $prevmax) {
108                     throw new coding_exception(
109                             'start_progress() parent progress would exceed max');
110                 }
111             }
112         } else {
113             if ($parentcount != 1) {
114                 throw new coding_exception(
115                         'start_progress() progress count must be 1 when no parent');
116             }
117         }
118         $this->descriptions[] = $description;
119         $this->maxes[] = $max;
120         $this->currents[] = 0;
121         $this->parentcounts[] = $parentcount;
122         $this->update_progress();
123     }
125     /**
126      * Marks the end of an operation that will display progress.
127      *
128      * This must be paired with each start_progress call.
129      *
130      * If there is a parent progress section, its progress will be increased
131      * automatically to reflect the end of the child section.
132      *
133      * @throws coding_exception If progress hasn't been started
134      */
135     public function end_progress() {
136         if (!count($this->descriptions)) {
137             throw new coding_exception('end_progress() without start_progress()');
138         }
139         array_pop($this->descriptions);
140         array_pop($this->maxes);
141         array_pop($this->currents);
142         $parentcount = array_pop($this->parentcounts);
143         if (!empty($this->descriptions)) {
144             $lastmax = end($this->maxes);
145             if ($lastmax != self::INDETERMINATE) {
146                 $lastvalue = end($this->currents);
147                 $this->currents[key($this->currents)] = $lastvalue + $parentcount;
148             }
149         }
150         $this->update_progress();
151     }
153     /**
154      * Indicates that progress has occurred.
155      *
156      * The progress value should indicate the total progress so far, from 0
157      * to the value supplied for $max (inclusive) in start_progress.
158      *
159      * You do not need to call this function for every value. It is OK to skip
160      * values. It is also OK to call this function as often as desired; it
161      * doesn't do anything if called more than once per second.
162      *
163      * It must be INDETERMINATE if start_progress was called with $max set to
164      * INDETERMINATE. Otherwise it must not be indeterminate.
165      *
166      * @param int $progress Progress so far
167      * @throws coding_exception If progress value is invalid
168      */
169     public function progress($progress = self::INDETERMINATE) {
170         // Ignore too-frequent progress calls (more than once per second).
171         $now = $this->get_time();
172         if ($now === $this->lastprogresstime) {
173             return;
174         }
176         // Check we are inside a progress section.
177         $max = end($this->maxes);
178         if ($max === false) {
179             throw new coding_exception(
180                     'progress() without start_progress');
181         }
183         // Check and apply new progress.
184         if ($progress === self::INDETERMINATE) {
185             // Indeterminate progress.
186             if ($max !== self::INDETERMINATE) {
187                 throw new coding_exception(
188                         'progress() INDETERMINATE, expecting value');
189             }
190         } else {
191             // Determinate progress.
192             $current = end($this->currents);
193             if ($max === self::INDETERMINATE) {
194                 throw new coding_exception(
195                         'progress() with value, expecting INDETERMINATE');
196             } else if ($progress < 0 || $progress > $max) {
197                 throw new coding_exception(
198                         'progress() value out of range');
199             } else if ($progress < $current) {
200                 throw new coding_Exception(
201                         'progress() value may not go backwards');
202             }
203             $this->currents[key($this->currents)] = $progress;
204         }
206         // Update progress.
207         $this->count++;
208         $this->lastprogresstime = $now;
210         // Update time limit before next progress display.
211         core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
212         $this->update_progress();
213     }
215     /**
216      * Gets time (this is provided so that unit tests can override it).
217      *
218      * @return int Current system time
219      */
220     protected function get_time() {
221         return time();
222     }
224     /**
225      * Called whenever new progress should be displayed.
226      */
227     protected abstract function update_progress();
229     /**
230      * @return bool True if currently inside a progress section
231      */
232     public function is_in_progress_section() {
233         return !empty($this->descriptions);
234     }
236     /**
237      * Checks max value of current progress section.
238      *
239      * @return int Current max value (may be core_backup_progress::INDETERMINATE)
240      * @throws coding_exception If not in a progress section
241      */
242     public function get_current_max() {
243         $max = end($this->maxes);
244         if ($max === false) {
245             throw new coding_exception('Not inside progress section');
246         }
247         return $max;
248     }
250     /**
251      * @return string Current progress section description
252      */
253     public function get_current_description() {
254         $description = end($this->descriptions);
255         if ($description === false) {
256             throw new coding_exception('Not inside progress section');
257         }
258         return $description;
259     }
261     /**
262      * Obtains current progress in a way suitable for drawing a progress bar.
263      *
264      * Progress is returned as a minimum and maximum value. If there is no
265      * indeterminate progress, these values will be identical. If there is
266      * intermediate progress, these values can be different. (For example, if
267      * the top level progress sections is indeterminate, then the values will
268      * always be 0.0 and 1.0.)
269      *
270      * @return array Minimum and maximum possible progress proportions
271      */
272     public function get_progress_proportion_range() {
273         // If there is no progress underway, we must have finished.
274         if (empty($this->currents)) {
275             return array(1.0, 1.0);
276         }
277         $count = count($this->currents);
278         $min = 0.0;
279         $max = 1.0;
280         for ($i = 0; $i < $count; $i++) {
281             // Get max value at that section - if it's indeterminate we can tell
282             // no more.
283             $sectionmax = $this->maxes[$i];
284             if ($sectionmax === self::INDETERMINATE) {
285                 return array($min, $max);
286             }
288             // Special case if current value is max (this should only happen
289             // just before ending a section).
290             $sectioncurrent = $this->currents[$i];
291             if ($sectioncurrent === $sectionmax) {
292                 return array($max, $max);
293             }
295             // Using the current value at that section, we know we are somewhere
296             // between 'current' and the next 'current' value which depends on
297             // the parentcount of the nested section (if any).
298             $newmin = ($sectioncurrent / $sectionmax) * ($max - $min) + $min;
299             $nextcurrent = $sectioncurrent + 1;
300             if ($i + 1 < $count) {
301                 $weight = $this->parentcounts[$i + 1];
302                 $nextcurrent = $sectioncurrent + $weight;
303             }
304             $newmax = ($nextcurrent / $sectionmax) * ($max - $min) + $min;
305             $min = $newmin;
306             $max = $newmax;
307         }
309         // If there was nothing indeterminate, we use the min value as current.
310         return array($min, $min);
311     }
313     /**
314      * Obtains current indeterminate progress in a way suitable for adding to
315      * the progress display.
316      *
317      * This returns the number of indeterminate calls (at any level) during the
318      * lifetime of this progress reporter, whether or not there is a current
319      * indeterminate step. (The number will not be ridiculously high because
320      * progress calls are limited to one per second.)
321      *
322      * @return int Number of indeterminate progress calls
323      */
324     public function get_progress_count() {
325         return $this->count;
326     }