Merge branch 'MDL-73734-streaming-output-api' of https://github.com/brendanheywood/moodle
This commit is contained in:
@@ -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') {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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.';
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user