Cache completion state to eliminate redundant DB queries#755
Open
jamieburgess wants to merge 1 commit into
Open
Cache completion state to eliminate redundant DB queries#755jamieburgess wants to merge 1 commit into
jamieburgess wants to merge 1 commit into
Conversation
Introduces \mod_questionnaire\completion_cache class that bulk-loads all completed user IDs per questionnaire on first check, then serves subsequent checks from memory. Also caches questionnaire records to avoid repeated loads. Problem: questionnaire_get_completion_state() executed 2 DB queries per check. During grade recalculation with availability conditions based on questionnaire completion, this produced ~46,000 queries for 500 students (45 dependent activities). At 100,000 users this would reach ~9,000,000 queries. Solution: Lazy bulk preload loads all completed user IDs in a single query. All subsequent checks are PHP array lookups with zero DB queries. Benchmarks (510 students, 45 dependent activities): - DB reads during regrade: 7,196 -> 278 (96.1% reduction) - Regrade time: 0.98s -> 0.13s (86.3% faster) - Completion queries: 46,260 -> 40 (99.91% reduction) Cache is invalidated on response submit and delete. Version bumped to 4.4.0.02.
d6a12f0 to
79c072f
Compare
mchurchward
requested changes
May 21, 2026
Contributor
mchurchward
left a comment
There was a problem hiding this comment.
Thanks for the pull request. I do not accept any improvements to the 400 series any more. The current active branch is MOODLE_500_STABLE. This, as a minimum, needs to be based off of that. Also, while I appreciate the modification to CHANGES.md, that file gets updated only when a new version is released.
Note that I may requires this to be redone on the new codebase, currently under construction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completion State Caching for mod_questionnaire
Summary
Adding a static in-memory cache for questionnaire completion state checks, eliminates redundant database queries during grade recalculation and completion report generation.
Perfomance Issue at scale
The function
questionnaire_get_completion_state()is called every time Moodle needs to check whether a user has completed a questionnaire. Each call executes 2 DB queries:get_record('questionnaire', ...)- loads the questionnaire settingsrecord_exists('questionnaire_response', ...)— checks if the user has a completed responseThis is fine for a single check, but becomes a bottleneck in bulk operations:
Grade recalculation: When grade items are marked as needing update, Moodle recalculates grades for every student. Activities with availability conditions based on questionnaire completion trigger a completion check for each student per dependent activity — common in courses that gate later content on a pre-survey or consent questionnaire.
Completion reports: Progress reports and course completion reports evaluate completion for all students across all activities.
Real-world impact (measured on test data)
The figures below are from a course that uses questionnaire completion as an availability restriction extensively (e.g. a consent/intake questionnaire gating most subsequent activities). The cost scales as 'students × gated activities × 2 queries', so sites with lighter use of questionnaire-based gating will see the same percentage reduction (just at smaller scale).
Benchmark results
Solution
This PR adds
\mod_questionnaire\completion_cache— a static in-memory cache with two layers:1. Questionnaire record cache
Caches the questionnaire DB record by instance ID. Since the 'completionsubmit' setting is the same for all users in a questionnaire, this eliminates repeated identical queries.
2. Lazy bulk preload of completion status
On the first completion check for a given questionnaire, executes a single query:
This loads all completed user IDs for that questionnaire into a PHP array. All subsequent checks for any user on the same questionnaire are simple
isset()lookups — zero DB queries.Memory usage
Below are sample measurements of memory usage at scale:
Cache invalidation
The cache is invalidated at all points where questionnaire response data changes:
questionnaire.class.php(line ~324)completion_cache::invalidate($id)questionnaire.class.php(line ~382)completion_cache::invalidate($id)locallib.php(line ~407)completion_cache::invalidate($id)The
invalidate()method removes only the specific questionnaire's cache, not all cached data.A full
clear()method is also available for use in testing or maintenance contexts.Safety analysis
Stale cache risk
update_state(COMPLETION_COMPLETE)which writes directly, bypassingget_state()update_stateupdate_state(COMPLETION_INCOMPLETE)update_statecompletion_infocache handles per-user cachingcompletion_info::get_data()which may callget_state()PHPUnit considerations
Static caches persist across test methods within a single test class. A
setUp()method was added tocustom_completion_testto clear the cache between tests, preventing cross-test contamination.Files changed
classes/completion_cache.phpcheck(),clear(),invalidate()methodslib.phpquestionnaire_get_completion_state()delegates tocompletion_cache::check()questionnaire.class.phplocallib.phptests/custom_completion_test.phpsetUp()added to clear cache between testsTesting
How to verify
Functional testing (UI)
Setup: Use a course that has questionnaires with completion tracking enabled ("Student must submit this questionnaire to complete it") and other activities that require questionnaire completion as an access restriction.
Test completion still works:
Test completion reports:
Test grade regrade:
Test response deletion:
Performance testing (optional, requires admin access)
This test measures the reduction in database queries. You will need a course with 100+ enrolled students, questionnaires with completion enabled, and activities restricted by questionnaire completion.
Setup
Enable the Excimer profiler for a specific page:
&FLAMEME=1to the URL of the page you want to profile (e.g. the gradebook or activity completion report)Trigger a grade recalculation:
UPDATE mdl_grade_items SET needsupdate = 1 WHERE courseid = 123;
View the profiler results:
Compare before and after:
Alternative — use Moodle debug footer:
$CFG->perfdebug = 15)@mchurchward, is this something you can review? Thank you!