Merge branch 'MDL-73734-streaming-output-api' of https://github.com/brendanheywood/moodle

This commit is contained in:
Andrew Nicols
2023-09-04 22:21:17 +08:00
11 changed files with 155 additions and 13 deletions
+7 -3
View File
@@ -57,13 +57,17 @@ if ($action === 'checkout') {
$progressbar = new progress_bar();
$progressbar->create(); // prints the HTML code of the progress bar
// we may need a bit of extra execution time and memory here
echo $output->continue_button(new moodle_url("/admin/tool/customlang/{$next}.php", array('lng' => $lng)), 'get');
echo $output->footer();
\core\session\manager::write_close();
echo $OUTPUT->select_element_for_append();
// We may need a bit of extra execution time and memory here.
core_php_time_limit::raise(HOURSECS);
raise_memory_limit(MEMORY_EXTRA);
tool_customlang_utils::checkout($lng, $progressbar);
echo $output->continue_button(new moodle_url("/admin/tool/customlang/{$next}.php", array('lng' => $lng)), 'get');
echo $output->footer();
exit;
}
if ($action === 'checkin') {
+9 -2
View File
@@ -49,6 +49,15 @@ if (!debugging('', DEBUG_DEVELOPER)) {
exit;
}
// Finish page.
echo $OUTPUT->footer();
echo $OUTPUT->select_element_for_append();
// Preemptively reset the navcache before closing, so it remains the same on shutdown.
navigation_cache::destroy_volatile_caches();
\core\session\manager::write_close();
// Set up the form.
$mform = new tool_generator_make_course_form('maketestcourse.php');
if ($data = $mform->get_data()) {
@@ -75,5 +84,3 @@ if ($data = $mform->get_data()) {
$mform->display();
}
// Finish page.
echo $OUTPUT->footer();
@@ -151,6 +151,7 @@ class url_finder {
sort($tables); // Make it easier to see progress because they are ordered.
$numberoftables = count($tables);
$tablenumber = 0;
sort($tables);
foreach ($tables as $table) {
if ($progress) {
$progress->update($tablenumber, $numberoftables, get_string('searching', 'tool_httpsreplace', $table));
+8 -1
View File
@@ -58,6 +58,14 @@ $finder = new \tool_httpsreplace\url_finder();
$PAGE->set_cacheable(false);
$progressbar = new progress_bar();
// Preemptively reset the navcache before closing, so it remains the same on shutdown.
navigation_cache::destroy_volatile_caches();
\core\session\manager::write_close();
// Prepare for streamed output.
echo $OUTPUT->footer();
echo $OUTPUT->select_element_for_append();
if (!$data = $form->get_data()) {
echo $progressbar->create();
@@ -103,4 +111,3 @@ if (!$data = $form->get_data()) {
echo $OUTPUT->continue_button(new moodle_url('/admin/settings.php', ['section' => 'httpsecurity']));
}
echo $OUTPUT->footer();
@@ -58,6 +58,7 @@ Feature: Basic recycle bin functionality
And I am on "Course 1" course homepage
And I should see "Test assign 1" in the "Topic 1" "section"
@javascript
Scenario: Restore a deleted course
Given I log in as "admin"
And I go to the courses management page
+5 -3
View File
@@ -83,13 +83,16 @@ require_sesskey();
\core\session\manager::write_close();
// Prepare for streamed output.
echo $OUTPUT->footer();
echo $OUTPUT->select_element_for_append();
// Prepare to handle output via mtrace.
echo html_writer::start_tag('pre');
echo html_writer::start_tag('pre', ['style' => 'color: #fff; background: #333; padding: 1em; min-height: 24lh']);
$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
// Run the specified task (this will output an error if it doesn't exist).
\core\task\manager::run_from_cli($task);
echo html_writer::end_tag('pre');
$output = $PAGE->get_renderer('tool_task');
@@ -100,4 +103,3 @@ echo $OUTPUT->single_button(new moodle_url('/admin/tool/task/schedule_task.php',
get_string('runagain', 'tool_task'));
echo $output->link_back(get_class($task));
echo $OUTPUT->footer();
+10 -1
View File
@@ -22,6 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('NO_OUTPUT_BUFFERING', true);
require_once(__DIR__ . '/../config.php');
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
@@ -64,13 +66,20 @@ if ($delete === md5($course->timemodified)) {
echo $OUTPUT->heading($strdeletingcourse);
// This might take a while. Raise the execution time limit.
core_php_time_limit::raise();
// We do this here because it spits out feedback as it goes.
echo $OUTPUT->footer();
echo $OUTPUT->select_element_for_append();
// Preemptively reset the navcache before closing, so it remains the same on shutdown.
navigation_cache::destroy_volatile_caches();
\core\session\manager::write_close();
delete_course($course);
echo $OUTPUT->heading( get_string("deletedcourse", "", $courseshortname) );
// Update course count in categories.
fix_course_sortorder();
echo $OUTPUT->continue_button($categoryurl);
echo $OUTPUT->footer();
exit; // We must exit here!!!
}
@@ -32,6 +32,7 @@ Feature: Test we can both create and delete a course.
And I should see "Cat 1" in the "#category-listing" "css_element"
And I should see "Test course: create a course" in the "#course-listing" "css_element"
@javascript
Scenario: Delete a course via its management listing
Given the following "categories" exist:
| name | category 0| idnumber |
@@ -64,6 +65,7 @@ Feature: Test we can both create and delete a course.
And I should see "Cat 1" in the "#category-listing" "css_element"
And I should see "Test course 2: create another course" in the "#course-listing" "css_element"
@javascript
Scenario: Delete a course via its management details page
Given the following "categories" exist:
| name | category 0| idnumber |
+1
View File
@@ -1022,6 +1022,7 @@ $string['pathtosassc'] = 'Path to SassC';
$string['pathtosassc_help'] = 'Specifying the location of the SassC binary will switch the SASS compiler from Moodle\'s PHP implementation to SassC. See https://github.com/sass/sassc for more information.';
$string['pcreunicodewarning'] = 'It is strongly recommended to use PCRE PHP extension that is compatible with Unicode characters.';
$string['perfdebug'] = 'Performance info';
$string['perfdebugdeferred'] = 'Waiting until the script ends to show the performance debugging ...';
$string['performance'] = 'Performance';
$string['pgcluster'] = 'PostgreSQL Cluster';
$string['pgclusterdescription'] = 'PostgreSQL version/cluster parameter for command line operations. If you only have one postgresql on your system or you are not sure what this is, leave this blank.';
+10 -1
View File
@@ -195,7 +195,7 @@ class core_shutdown_manager {
* Standard shutdown sequence.
*/
protected static function request_shutdown() {
global $CFG;
global $CFG, $OUTPUT;
// Help apache server if possible.
$apachereleasemem = false;
@@ -216,6 +216,15 @@ class core_shutdown_manager {
$perf = get_performance_info();
error_log("PERF: " . $perf['txt']);
}
if (MDL_PERFTOFOOT || debugging() || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
if (NO_OUTPUT_BUFFERING) {
// If the performance footer was deferred then print it now.
if (!CLI_SCRIPT && !WS_SERVER) {
$perf = get_performance_info();
echo $OUTPUT->select_element_for_replace('#perfdebugfooter', $perf['html']);
}
}
}
if (MDL_PERFINC) {
$inc = get_included_files();
$ts = 0;
+101 -2
View File
@@ -1512,9 +1512,20 @@ class core_renderer extends renderer_base {
// Provide some performance info if required
$performanceinfo = '';
if (MDL_PERF || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
$perf = get_performance_info();
if (MDL_PERFTOFOOT || debugging() || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
$performanceinfo = $perf['html'];
if (NO_OUTPUT_BUFFERING) {
// If the output buffer was off then we render a placeholder and stream the
// performance debugging into it at the very end in the shutdown handler.
$performanceinfo .= html_writer::tag('div',
get_string('perfdebugdeferred', 'admin'),
[
'id' => 'perfdebugfooter',
'style' => 'min-height: 30em',
]);
} else {
$perf = get_performance_info();
$performanceinfo = $perf['html'];
}
}
}
@@ -5165,6 +5176,94 @@ EOD;
}
return $addblockbutton;
}
/**
* Prepares an element for streaming output
*
* This must be used with NO_OUTPUT_BUFFERING set to true. After using this method
* any subsequent prints or echos to STDOUT result in the outputted content magically
* being appended inside that element rather than where the current html would be
* normally. This enables pages which take some time to render incremental content to
* first output a fully formed html page, including the footer, and to then stream
* into an element such as the main content div. This fixes a class of page layout
* bugs and reduces layout shift issues and was inspired by Facebook BigPipe.
*
* Some use cases such as a simple page which loads content via ajax could be swapped
* to this method wich saves another http request and its network latency resulting
* in both lower server load and better front end performance.
*
* You should consider giving the element you stream into a minimum height to further
* reduce layout shift as the content initally streams into the element.
*
* You can safely finish the output without closing the streamed element. You can also
* call this method again to swap the target of the streaming to a new element as
* often as you want.
* https://www.youtube.com/watch?v=LLRig4s1_yA&t=1022s
* Watch this video segment to explain how and why this 'One Weird Trick' works.
*
* @param string $selector where new content should be appended
* @param string $element which contains the streamed content
*/
public function select_element_for_append(string $selector = '#region-main [role=main]', string $element = 'div') {
static $currentselector = '';
static $currentelement = '';
if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
throw new coding_exception('select_element_for_append used in a non-CLI script without setting NO_OUTPUT_BUFFERING.',
DEBUG_DEVELOPER);
}
// We are already streaming into this element so don't change anything.
if ($currentselector === $selector && $currentelement === $element) {
return;
}
$html = '';
// We have a streaming element so close it before starting a new one.
if ($currentselector !== '') {
$html .= html_writer::end_tag($currentelement);
}
$currentselector = $selector;
$currentelement = $element;
// Create an unclosed element for the streamed content to append into.
$id = uniqid();
$html .= html_writer::start_tag($element, ['id' => $id]);
$html .= html_writer::tag('script', "document.querySelector('$selector').append(document.getElementById('$id'))");
return $html;
}
/**
* A companion method to select_element_for_append
*
* This must be used with NO_OUTPUT_BUFFERING set to true.
*
* This is similar but instead of appending into the element it replaces
* the content in the element. Depending on the 3rd argument it can replace
* the innerHTML or the outerHTML which can be useful to completely remove
* the element if needed.
*
* @param string $selector where new content should be replaced
* @param string $html A chunk of well formed html
* @param bool $outer Wether it replaces the innerHTML or the outerHTML
*/
public function select_element_for_replace(string $selector, string $html, bool $outer = false) {
if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
throw new coding_exception('select_element_for_replace used in a non-CLI script without setting NO_OUTPUT_BUFFERING.',
DEBUG_DEVELOPER);
}
// Escape html for use inside a javascript string.
$html = addslashes_js($html);
$property = $outer ? 'outerHTML' : 'innerHTML';
$output = html_writer::tag('script', "document.querySelector('$selector').$property = '$html';");
return $output;
}
}
/**