Merge branch 'MDL-42905-master' of git://github.com/sammarshallou/moodle
[moodle.git] / backup / util / progress / core_backup_progress.class.php
CommitLineData
16cd7088 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/>.
16
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 */
27abstract class core_backup_progress {
28 /**
29 * @var int Constant indicating that the number of progress calls is unknown.
30 */
31 const INDETERMINATE = -1;
32
33 /**
34 * @var int The number of seconds that can pass without progress() calls.
35 */
36 const TIME_LIMIT_WITHOUT_PROGRESS = 120;
37
38 /**
39 * @var int Time of last progress call.
40 */
41 protected $lastprogresstime;
42
43 /**
44 * @var int Number of progress calls (restricted to ~ 1/second).
45 */
46 protected $count;
47
48 /**
49 * @var array Array of progress descriptions for each stack level.
50 */
51 protected $descriptions = array();
52
53 /**
54 * @var array Array of maximum progress values for each stack level.
55 */
56 protected $maxes = array();
57
58 /**
59 * @var array Array of current progress values.
60 */
61 protected $currents = array();
62
63 /**
64 * @var int Array of counts within parent progress entry (ignored for first)
65 */
66 protected $parentcounts = array();
67
68 /**
69 * Marks the start of an operation that will display progress.
70 *
71 * This can be called multiple times for nested progress sections. It must
72 * be paired with calls to end_progress.
73 *
74 * The progress maximum may be INDETERMINATE if the current operation has
75 * an unknown number of steps. (This is default.)
76 *
77 * Calling this function will always result in a new display, so this
78 * should not be called exceedingly frequently.
79 *
80 * When it is complete by calling end_progress, each start_progress section
81 * automatically adds progress to its parent, as defined by $parentcount.
82 *
83 * @param string $description Description to display
84 * @param int $max Maximum value of progress for this section
85 * @param int $parentcount How many progress points this section counts for
86 * @throws coding_exception If max is invalid
87 */
88 public function start_progress($description, $max = self::INDETERMINATE,
89 $parentcount = 1) {
6a0189eb 90 if ($max != self::INDETERMINATE && $max < 0) {
16cd7088 91 throw new coding_exception(
6a0189eb 92 'start_progress() max value cannot be negative');
16cd7088 93 }
94 if ($parentcount < 1) {
95 throw new coding_exception(
96 'start_progress() parent progress count must be at least 1');
97 }
98 if (!empty($this->descriptions)) {
99 $prevmax = end($this->maxes);
100 if ($prevmax !== self::INDETERMINATE) {
101 $prevcurrent = end($this->currents);
102 if ($prevcurrent + $parentcount > $prevmax) {
103 throw new coding_exception(
104 'start_progress() parent progress would exceed max');
105 }
106 }
107 } else {
108 if ($parentcount != 1) {
109 throw new coding_exception(
110 'start_progress() progress count must be 1 when no parent');
111 }
112 }
113 $this->descriptions[] = $description;
114 $this->maxes[] = $max;
115 $this->currents[] = 0;
116 $this->parentcounts[] = $parentcount;
117 $this->update_progress();
16cd7088 118 }
119
120 /**
121 * Marks the end of an operation that will display progress.
122 *
123 * This must be paired with each start_progress call.
124 *
125 * If there is a parent progress section, its progress will be increased
126 * automatically to reflect the end of the child section.
127 *
128 * @throws coding_exception If progress hasn't been started
129 */
130 public function end_progress() {
131 if (!count($this->descriptions)) {
132 throw new coding_exception('end_progress() without start_progress()');
133 }
134 array_pop($this->descriptions);
135 array_pop($this->maxes);
136 array_pop($this->currents);
137 $parentcount = array_pop($this->parentcounts);
138 if (!empty($this->descriptions)) {
139 $lastmax = end($this->maxes);
140 if ($lastmax != self::INDETERMINATE) {
141 $lastvalue = end($this->currents);
142 $this->currents[key($this->currents)] = $lastvalue + $parentcount;
143 }
144 }
145 $this->update_progress();
146 }
147
148 /**
149 * Indicates that progress has occurred.
150 *
151 * The progress value should indicate the total progress so far, from 0
152 * to the value supplied for $max (inclusive) in start_progress.
153 *
154 * You do not need to call this function for every value. It is OK to skip
155 * values. It is also OK to call this function as often as desired; it
156 * doesn't do anything if called more than once per second.
157 *
158 * It must be INDETERMINATE if start_progress was called with $max set to
159 * INDETERMINATE. Otherwise it must not be indeterminate.
160 *
161 * @param int $progress Progress so far
162 * @throws coding_exception If progress value is invalid
163 */
164 public function progress($progress = self::INDETERMINATE) {
165 // Ignore too-frequent progress calls (more than once per second).
166 $now = $this->get_time();
167 if ($now === $this->lastprogresstime) {
168 return;
169 }
170
171 // Check we are inside a progress section.
172 $max = end($this->maxes);
173 if ($max === false) {
174 throw new coding_exception(
175 'progress() without start_progress');
176 }
177
178 // Check and apply new progress.
179 if ($progress === self::INDETERMINATE) {
180 // Indeterminate progress.
181 if ($max !== self::INDETERMINATE) {
182 throw new coding_exception(
183 'progress() INDETERMINATE, expecting value');
184 }
185 } else {
186 // Determinate progress.
187 $current = end($this->currents);
188 if ($max === self::INDETERMINATE) {
189 throw new coding_exception(
190 'progress() with value, expecting INDETERMINATE');
191 } else if ($progress < 0 || $progress > $max) {
192 throw new coding_exception(
193 'progress() value out of range');
194 } else if ($progress < $current) {
195 throw new coding_Exception(
196 'progress() value may not go backwards');
197 }
198 $this->currents[key($this->currents)] = $progress;
199 }
200
201 // Update progress.
202 $this->count++;
203 $this->lastprogresstime = $now;
204 set_time_limit(self::TIME_LIMIT_WITHOUT_PROGRESS);
205 $this->update_progress();
206 }
207
208 /**
209 * Gets time (this is provided so that unit tests can override it).
210 *
211 * @return int Current system time
212 */
213 protected function get_time() {
214 return time();
215 }
216
217 /**
218 * Called whenever new progress should be displayed.
219 */
220 protected abstract function update_progress();
221
222 /**
223 * @return bool True if currently inside a progress section
224 */
225 public function is_in_progress_section() {
226 return !empty($this->descriptions);
227 }
228
37ff843d 229 /**
230 * Checks max value of current progress section.
231 *
232 * @return int Current max value (may be core_backup_progress::INDETERMINATE)
233 * @throws coding_exception If not in a progress section
234 */
235 public function get_current_max() {
236 $max = end($this->maxes);
237 if ($max === false) {
238 throw new coding_exception('Not inside progress section');
239 }
240 return $max;
241 }
242
16cd7088 243 /**
244 * @return string Current progress section description
245 */
246 public function get_current_description() {
247 $description = end($this->descriptions);
248 if ($description === false) {
249 throw new coding_exception('Not inside progress section');
250 }
251 return $description;
252 }
253
254 /**
255 * Obtains current progress in a way suitable for drawing a progress bar.
256 *
257 * Progress is returned as a minimum and maximum value. If there is no
258 * indeterminate progress, these values will be identical. If there is
259 * intermediate progress, these values can be different. (For example, if
260 * the top level progress sections is indeterminate, then the values will
261 * always be 0.0 and 1.0.)
262 *
263 * @return array Minimum and maximum possible progress proportions
264 */
265 public function get_progress_proportion_range() {
266 // If there is no progress underway, we must have finished.
267 if (empty($this->currents)) {
268 return array(1.0, 1.0);
269 }
270 $count = count($this->currents);
271 $min = 0.0;
272 $max = 1.0;
273 for ($i = 0; $i < $count; $i++) {
274 // Get max value at that section - if it's indeterminate we can tell
275 // no more.
276 $sectionmax = $this->maxes[$i];
277 if ($sectionmax === self::INDETERMINATE) {
278 return array($min, $max);
279 }
280
281 // Special case if current value is max (this should only happen
282 // just before ending a section).
283 $sectioncurrent = $this->currents[$i];
284 if ($sectioncurrent === $sectionmax) {
285 return array($max, $max);
286 }
287
288 // Using the current value at that section, we know we are somewhere
289 // between 'current' and the next 'current' value which depends on
290 // the parentcount of the nested section (if any).
291 $newmin = ($sectioncurrent / $sectionmax) * ($max - $min) + $min;
292 $nextcurrent = $sectioncurrent + 1;
293 if ($i + 1 < $count) {
294 $weight = $this->parentcounts[$i + 1];
295 $nextcurrent = $sectioncurrent + $weight;
296 }
297 $newmax = ($nextcurrent / $sectionmax) * ($max - $min) + $min;
298 $min = $newmin;
299 $max = $newmax;
300 }
301
302 // If there was nothing indeterminate, we use the min value as current.
303 return array($min, $min);
304 }
305
306 /**
307 * Obtains current indeterminate progress in a way suitable for adding to
308 * the progress display.
309 *
310 * This returns the number of indeterminate calls (at any level) during the
311 * lifetime of this progress reporter, whether or not there is a current
312 * indeterminate step. (The number will not be ridiculously high because
313 * progress calls are limited to one per second.)
314 *
315 * @return int Number of indeterminate progress calls
316 */
317 public function get_progress_count() {
318 return $this->count;
319 }
320}