Skip to content

Commit 555800a

Browse files
committed
Issue #224: Add task load metric
1 parent 7049a8b commit 555800a

8 files changed

Lines changed: 684 additions & 2 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
namespace tool_cloudmetrics\local\task_load;
18+
19+
use core\task\manager as taskmanager;
20+
21+
/**
22+
* Base class for task load estimators.
23+
*
24+
* @package tool_cloudmetrics
25+
* @author Jason den Dulk <jasondendulk@catalyst-au.net>
26+
* @copyright 2026 Catalyst IT
27+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28+
*/
29+
abstract class task_load_estimator {
30+
/**
31+
* Returns the object to calculate the task load estimates.
32+
* Which implementation is used depends on the availability of task statistics feature (MDL-85173).
33+
*
34+
* @return task_load_estimator
35+
*/
36+
public static function create(): task_load_estimator {
37+
if (class_exists('\core\task\stat_processor')) {
38+
return new task_load_from_stats();
39+
} else {
40+
return new task_load_from_log();
41+
}
42+
}
43+
44+
/**
45+
* Generate task load estimates for the near future. The load is taken from the current
46+
* time determined with a DI clock.
47+
*
48+
* @param int $window The number of seconds into the future to look.
49+
* @return array Returns an array of task load estimates, indexed by classname. Each entry gives the count, total
50+
* time and average time estimates.
51+
*/
52+
abstract public function generate_load_estimates(int $window): array;
53+
54+
/**
55+
* Main function for generating the load estimates array.
56+
*
57+
* @param array $scheduled
58+
* @param array $adhoc
59+
* @param int $start
60+
* @param int $finish
61+
* @return array
62+
*/
63+
protected function generate_load_estimates_inner(array $scheduled, array $adhoc, int $start, int $finish): array {
64+
// Loads will contain the estimates.
65+
$loads = [];
66+
67+
foreach ($scheduled as $record) {
68+
$task = taskmanager::scheduled_task_from_record($record);
69+
$time = $start;
70+
$count = 0;
71+
$totalduration = 0;
72+
73+
// If the task has already started, then we add the remaining time.
74+
if ($record->timestarted > 0) {
75+
$started = (int) $record->timestarted;
76+
$elapsed = max(0, $start - $started);
77+
$remaining = max(0.0, $record->mean - $elapsed);
78+
79+
++$count;
80+
$totalduration += $remaining;
81+
// We do not want to include this in the loop below. So we get the next scheduled time.
82+
$time = $task->get_next_scheduled_time($record->timestarted);
83+
}
84+
$time = max($time, $record->nextruntime);
85+
// A scheduled task may run multiple times within the time window, so we add the mean time for each run.
86+
while ($time < $finish) {
87+
// If the task is expected to run past the window, then we add the remaining time,
88+
// otherwise we add the mean.
89+
++$count;
90+
if ($time + $record->mean > $finish) {
91+
$totalduration += ($finish - $time);
92+
} else {
93+
$totalduration += $record->mean;
94+
}
95+
$time = $task->get_next_scheduled_time($time);
96+
}
97+
$classname = taskmanager::get_canonical_class_name($task::class);
98+
$loads[$classname] = [
99+
'type' => 'scheduled',
100+
'classname' => $classname,
101+
'count' => $count,
102+
'totalduration' => $totalduration,
103+
'avgduration' => $totalduration / $count,
104+
];
105+
}
106+
107+
foreach ($adhoc as $record) {
108+
$classname = taskmanager::get_canonical_class_name($record->classname);
109+
if (!isset($loads[$classname])) {
110+
$loads[$classname] = [
111+
'type' => 'adhoc',
112+
'classname' => $classname,
113+
'count' => 0,
114+
'totalduration' => 0,
115+
'avgduration' => 0,
116+
];
117+
}
118+
if ($record->timestarted > 0) {
119+
// Task has already started. Add remaining time.
120+
$started = (int) $record->timestarted;
121+
$elapsed = max(0, $start - $started);
122+
$remaining = max(0.0, $record->mean - $elapsed);
123+
$loads[$classname]['totalduration'] += $remaining;
124+
} else if ($record->nextruntime + $record->mean > $finish) {
125+
// Task will finish after window ends. Add time left in window.
126+
$loads[$classname]['totalduration'] += ($finish - $record->nextruntime);
127+
} else {
128+
// Task will run wholly within the window, so add the full mean.
129+
$loads[$classname]['totalduration'] += $record->mean;
130+
}
131+
++$loads[$classname]['count'];
132+
$loads[$classname]['avgduration'] = $loads[$classname]['totalduration'] / $loads[$classname]['count'];
133+
}
134+
135+
// Order by queue then by estimated_seconds DESC to match the SQL ORDER BY.
136+
usort($loads, static function ($a, $b) {
137+
$q = strcmp($a['type'], $b['type']);
138+
if ($q !== 0) {
139+
return $q;
140+
}
141+
return $b['totalduration'] <=> $a['totalduration'];
142+
});
143+
144+
return $loads;
145+
}
146+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
namespace tool_cloudmetrics\local\task_load;
18+
19+
/**
20+
* Generates task load estimates from the task log. Useful for versions of Modoel prior to
21+
* the introduction of task statistics.
22+
*
23+
* @package tool_cloudmetrics
24+
* @author Jason den Dulk <jasondendulk@catalyst-au.net>
25+
* @copyright 2026 Catalyst IT
26+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27+
*/
28+
class task_load_from_log extends task_load_estimator {
29+
/**
30+
* Time cutoff for log aggregation.
31+
*/
32+
const AVG_CUTOFF = 7243600; // 83.837 days
33+
34+
/**
35+
* Generate task load estimates for the near future. The load is taken from the current
36+
* time determined with a DI clock.
37+
*
38+
* @param int $window The number of seconds into the future to look.
39+
* @return array Returns an array of task load estimates, indexed by classname. Each entry gives the count, total
40+
* time and average time estimates.
41+
*/
42+
public function generate_load_estimates(int $window): array {
43+
global $DB;
44+
45+
$clock = \core\di::get(\core\clock::class);
46+
$now = $clock->time();
47+
$finish = $now + $window;
48+
49+
// Timecutoff for log aggregation (matches original SQL: last 7,243,600 seconds).
50+
$cutoff = $now - self::AVG_CUTOFF;
51+
52+
// Per-class average durations from task_log.
53+
$sql = "SELECT classname, AVG(timeend - timestart) AS avg_duration
54+
FROM {task_log}
55+
WHERE timestart >= ?
56+
GROUP BY classname";
57+
$avgtimes = $DB->get_records_sql_menu($sql, [$cutoff]);
58+
59+
// Global average (fallback), defaulting to 10 if no data.
60+
$globalsql = "SELECT COALESCE(AVG(avg_duration), 10)
61+
FROM (
62+
SELECT AVG(timeend - timestart) AS avg_duration
63+
FROM {task_log}
64+
WHERE timestart >= ?
65+
GROUP BY classname
66+
) AS t";
67+
$globalavg = (float) $DB->get_field_sql($globalsql, [$cutoff]);
68+
69+
// Count scheduled tasks in the next $window seconds.
70+
$scheduled = $DB->get_records_sql(
71+
"SELECT {task_scheduled}.*
72+
FROM {task_scheduled}
73+
WHERE {task_scheduled}.nextruntime < :finish",
74+
['finish' => $finish, 'globalavg' => $globalavg]
75+
);
76+
77+
foreach ($scheduled as $record) {
78+
$record->mean = (float) ($avgtimes[ltrim($record->classname, '\\')] ?? $globalavg);
79+
}
80+
81+
$adhoc = $DB->get_records_sql(
82+
"SELECT {task_adhoc}.*
83+
FROM {task_adhoc}
84+
WHERE {task_adhoc}.nextruntime < :finish",
85+
['finish' => $finish]
86+
);
87+
88+
foreach ($adhoc as $record) {
89+
$record->mean = (float) ($avgtimes[ltrim($record->classname, '\\')] ?? $globalavg);
90+
}
91+
92+
return $this->generate_load_estimates_inner($scheduled, $adhoc, $now, $finish);
93+
}
94+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
namespace tool_cloudmetrics\local\task_load;
18+
19+
/**
20+
* Generates task load estimates from the task stats table.
21+
*
22+
* @package tool_cloudmetrics
23+
* @author Jason den Dulk <jasondendulk@catalyst-au.net>
24+
* @copyright 2026 Catalyst IT
25+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26+
*/
27+
class task_load_from_stats extends task_load_estimator {
28+
/**
29+
* Generate task load estimates for the near future. The load is taken from the current
30+
* time determined with a DI clock.
31+
*
32+
* @param int $window The number of seconds into the future to look.
33+
* @return array Returns an array of task load estimates, indexed by classname. Each entry gives the count, total
34+
* time and average time estimates.
35+
*/
36+
public function generate_load_estimates(int $window): array {
37+
global $DB;
38+
39+
$clock = \core\di::get(\core\clock::class);
40+
$now = $clock->time();
41+
$finish = $now + $window;
42+
43+
$sql = "SELECT COALESCE(AVG(mean), 10) AS avg_duration FROM {task_stats};";
44+
$globalavg = (float)$DB->get_field_sql($sql);
45+
46+
// Get scheduled tasks, along with averages, that will run before the time window ends.
47+
$scheduled = $DB->get_records_sql(
48+
"SELECT {task_scheduled}.*, COALESCE({task_stats}.mean, :globalavg) AS mean
49+
FROM {task_scheduled}
50+
RIGHT JOIN {task_stats} ON {task_stats}.classname = {task_scheduled}.classname
51+
WHERE {task_scheduled}.nextruntime < :finish",
52+
['finish' => $finish, 'globalavg' => $globalavg]
53+
);
54+
55+
// Get adhoc tasks, along with averages, that will run before the time window ends.
56+
$adhoc = $DB->get_records_sql(
57+
"SELECT {task_adhoc}.*, COALESCE({task_stats}.mean, :globalavg) AS mean
58+
FROM {task_adhoc}
59+
LEFT JOIN {task_stats} ON {task_stats}.classname = {task_adhoc}.classname
60+
WHERE {task_adhoc}.nextruntime < :finish",
61+
['finish' => $finish, 'globalavg' => $globalavg]
62+
);
63+
64+
return $this->generate_load_estimates_inner($scheduled, $adhoc, $now, $finish);
65+
}
66+
}

classes/metric/manager.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public static function get_frequency_labels(): array {
8585
'onlineusers' => self::FREQ_5MIN,
8686
'dailyusers' => self::FREQ_DAY,
8787
'yearlyactiveusers' => self::FREQ_DAY,
88+
'currenttaskcount' => self::FREQ_5MIN,
89+
'estimatedtaskload' => self::FREQ_5MIN,
8890
];
8991

9092
/**
@@ -102,6 +104,7 @@ public static function get_metrics(bool $enabledonly = true): array {
102104
'dailyusers' => new daily_users_metric(),
103105
'yearlyactiveusers' => new yearly_active_users_metric(),
104106
'currenttaskcount' => new current_task_count_metric(),
107+
'estimatedtaskload' => new task_load_metric(),
105108
];
106109
if (!empty(PHPUNIT_TEST)) {
107110
// Add testing metrics to the list.

0 commit comments

Comments
 (0)