Repository navigation
feat: incrementally load large assessment units - #39298
abhalsod-sonata wants to merge 8 commits into
Conversation
ea796f4 to
5386736
Compare
robrap
left a comment
There was a problem hiding this comment.
Some quick initial comments.
Also, Claude noted the following, and you should correct the first issue at a minimum. Thank you:
Findings (all minor, none block correctness)
- Dangling reference: three comments in block_render.py (~1120, ~1185, ~1343) cite FieldDataCache._children_to_prefetch, which exists only downstream (added by excluded 04e7ed0). Not in master. Reword to cite has_dynamic_children()/get_child_blocks() / VerticalBlock.block_has_access_error only.
- Attribution: description credits Santhosh Kumar (original feature + first fix) but commits are all authored by abhalsod-sonata with no Co-authored-by: trailers. Consider adding trailers so history credits the original author.
| # the upstream request timeout. | ||
| # .. toggle_use_cases: opt_in | ||
| # .. toggle_creation_date: 2026-08-27 | ||
| # .. toggle_target_removal_date: None |
There was a problem hiding this comment.
This should be left out for permanent toggles.
| # .. toggle_target_removal_date: None |
There was a problem hiding this comment.
Done in b066cbd, removed toggle_target_removal_date since this is a permanent toggle.
| # .. setting_description: Maximum children accepted by a single request to | ||
| # /api/courseware/xblock_children/. Requests above this are rejected with a 400. |
There was a problem hiding this comment.
Does this make sense? I think it may be more clear, if it is correct. Thoughts?
| # .. setting_description: Maximum children accepted by a single request to | |
| # /api/courseware/xblock_children/. Requests above this are rejected with a 400. | |
| # .. setting_description: Maximum children expected and accepted by a single request to | |
| # /api/courseware/xblock_children/. Unexpected requests above this are rejected with a 400. |
There was a problem hiding this comment.
Yes, that's correct, so I applied it in b066cbd. The page script batches using the same XBLOCK_CHILDREN_BATCH_MAX, so only an unexpected client would send more.
5386736 to
cd08c7f
Compare
Applies the review changes made on openedx#39298, so the downstream copy stays identical to the upstream PR. No behavior change. - block_render.py: the comments no longer point at FieldDataCache._children_to_prefetch or VerticalBlock.block_has_access_error. Those cross-references only hold in this fork, so the comments now describe the has_dynamic_children() and get_child_blocks() block API directly. - toggles.py: drop toggle_target_removal_date from courseware.incremental_assessment_load, since it is a permanent toggle. - common.py: clearer XBLOCK_CHILDREN_BATCH_MAX description.
Rendering every problem of a very large unit, such as a 90-question
exam built from randomized question banks, in one response can exceed
the upstream request timeout, so the unit never loads.
Behind the new course waffle flag courseware.incremental_assessment_load
(off by default), a unit with more than INCREMENTAL_LOAD_PROBLEM_THRESHOLD
problems renders its first INCREMENTAL_LOAD_EAGER_COUNT children inline
and leaves placeholders for the rest. The page then fills them in, in
sequential batches of at most XBLOCK_CHILDREN_BATCH_MAX, from the new
GET /api/courseware/xblock_children/{usage_key} endpoint, retrying
transient failures. Mobile app requests and units at or below the
threshold always render in full.
With the flag off, rendering is unchanged.
Ported from #445, originally written by Santhosh Kumar.
This commit is the downstream change as it was merged there, except the
hunks that do not apply to master. Those are added in "chore: adapt the
incremental assessment load port to master".
Co-Authored-By: Santhosh Kumar <skumar4-apphelix@2u.com>
Children rendered by the xblock_children endpoint did not see the learner's saved state, so an answered problem rendered as a fresh attempt and resubmitting it failed with "The state of this problem has changed since you loaded this page". Resolve each requested child to a fresh, unbound descriptor and bind it once, after one FieldDataCache populated for the whole batch through the new FieldDataCache.add_block_descendents_batch, so a batch costs a single learner-state lookup. The page now also shows a per-child error with a Retry button for children the endpoint reports in its errors list. Ported from #465, originally written by Santhosh Kumar. This commit is the downstream change as it was merged there, except the hunks that do not apply to master. Those are added in "chore: adapt the incremental assessment load port to master". Co-Authored-By: Santhosh Kumar <skumar4-apphelix@2u.com>
Problems a randomized bank (item_bank / library_content) selected rendered as unanswered when they fell past the incremental-load eager window. The bank's selected problem is loaded through the runtime the parent vertical bound with its own depth-0 FieldDataCache, so it never saw the learner state prefetched into the separate batch cache. Resubmitting such a problem then failed with "The state of this problem has changed since you loaded this page". Bind the parent and the batch to one FieldDataCache so every block loaded through the shared runtime sees the same state. Lazily inserted children were also set with innerHTML, which never runs inline scripts, so setup_debug() from staff_problem_info.html never ran and Staff Debug Info / Submission History did nothing past the eager window. Insert them with edx.HtmlUtils.setHtml, which runs them as an inline render does and marks the server-rendered fragment trusted the same way capa does. Ported from #529. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three previous commits are the downstream changes as merged in edx/edx-platform. This commit adds the hunks that did not apply to master and the changes master needs: - Add the new imports to master's import blocks in block_render.py, courseware/views/views.py, courseware_api/views.py and its tests. - Split VerticalBlock children into eager and lazy blocks, keeping master's child_has_access_error check in the render loop. - Add the FieldDataCache.add_block_descendents_batch loop. Downstream, it sits next to fork-only prefetch code that master does not have. - Test add_block_descendents_batch in its own TestFieldDataCacheBatchDescendents class. Downstream, the test lives in a fork-only class. - Import the capa test factory from xblocks_contrib.problem.capa.testing, since xmodule.capa no longer exists on master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- B904: chain CourseAccessRedirect into NotFound with raise ... from exc. - I001: sort the test_incremental_render.py imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the standard setting annotations for INCREMENTAL_LOAD_PROBLEM_THRESHOLD, INCREMENTAL_LOAD_EAGER_COUNT and XBLOCK_CHILDREN_BATCH_MAX so they show up in the generated settings documentation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- block_render.py: three comments pointed at FieldDataCache._children_to_prefetch, which only exists downstream. They now describe the has_dynamic_children() / get_child_blocks() block API directly. - toggles.py: drop toggle_target_removal_date, since courseware.incremental_assessment_load is a permanent toggle. - common.py: clearer XBLOCK_CHILDREN_BATCH_MAX description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cd08c7f to
b066cbd
Compare
Thanks! Both are addressed:
|
Description
Very large assessment units, for example a 90-question mock exam where every question comes from a randomized question bank, can take longer to render in a single response than the request timeout allows, so the unit never loads.
This PR adds incremental loading for those units, behind a new course waffle flag,
courseware.incremental_assessment_load(opt-in per course). With the flag on, a unit with more thanINCREMENTAL_LOAD_PROBLEM_THRESHOLD(20) problems:INCREMENTAL_LOAD_EAGER_COUNT(5) children inline and leaves placeholders for the rest.GET /api/courseware/xblock_children/{usage_key}?child_usage_keys=...endpoint, in sequential batches of at mostXBLOCK_CHILDREN_BATCH_MAX(10). The page shows "Loading question X of Y...", retries transient failures with backoff, halves a batch that times out, and offers a Retry button for any child that fails.Units at or below the threshold, and requests from the mobile apps, always render in full.
The PR is seven commits. The first three are the downstream changes as they were merged in edx/edx-platform, so they can be compared with the originals. The rest adapt them to
master:feat: incrementally load large assessment units(feat: incrementally load large assessment xblocks edx/edx-platform#445): the flag, settings, endpoint, vertical shell and page script.fix: hydrate learner state for incrementally loaded children(feat: hydrate learner state for paginated assessment children edx/edx-platform#465): lazily loaded problems now show the learner's saved answer, score and attempts. Without this, an answered problem rendered as a fresh attempt and resubmitting it failed with "The state of this problem has changed since you loaded this page". OneFieldDataCacheis populated per batch (newFieldDataCache.add_block_descendents_batch), so a batch costs a single learner-state lookup.fix: hydrate item bank problems and staff debug in lazily loaded units(fix: LP-1397 hydrate item bank problems and staff debug in lazily loaded units edx/edx-platform#529): the same fix for problems inside randomized banks (item_bank / library_content), which are loaded through the shared runtime the parent bound. It also makes Staff Debug Info and Submission History work for lazily loaded children, by inserting them withedx.HtmlUtils.setHtmlso their inline setup scripts run, the same as an inline render.chore: adapt the incremental assessment load port to master: the hunks that did not apply tomaster(imports, the vertical render loop, the batch cache loop, the batch test) and the capa test factory path onmaster.style: fix ruff findings in the incremental assessment load port.docs: annotate incremental assessment load settings.docs: address review feedback on comments and annotations.Commits 1 to 4 do not pass CI on their own; only the full PR does, so it is meant to be squash-merged.
User roles impacted:
lms/djangoapps/courseware/toggles.py,lms/envs/common.py).xblock_children.Before vs after
Measured on a local devstack with a 90-question unit, each question an item bank holding one problem, warm cache. The absolute numbers depend on the machine; the comparison is what matters.
masterbehavior)The total time to show every question stays about the same. The difference is that no single request has to render the whole unit, and the learner can start right after the first response.
1. Before (flag off): the whole 90-question unit arrives in a single 2.9 MB response.

2. After (flag on): the page arrives with the first 5 questions; questions 6 to 90 are placeholders.

3. After: the rest load in batches while the status line reports progress. The batch responses were slowed down in the browser to capture this state.

4. After: Staff Debug Info on question 7, a lazily loaded problem inside an item bank, opens.

5. After: question 7 answered and the page reloaded: the saved answer, the score (1/1 point) and the attempts (1 of 1) are kept, and Submit stays disabled.

Supporting information
The original feature and the first fix were written by Santhosh Kumar, who is credited as co-author on those two commits. This PR ports them, with the follow-up fix, onto
master.Testing instructions
courseware.incremental_assessment_load, forced on, for that course.GET /api/courseware/xblock_children/<unit key>/?child_usage_keys=...requests with at most 10 keys each.Automated tests:
lms/djangoapps/courseware/tests/test_incremental_render.py: the threshold and flag decision, the shell split, the endpoint helpers.openedx/core/djangoapps/courseware_api/tests/test_views.py(XBlockChildrenApiTestViews):lms/djangoapps/courseware/tests/test_model_data.py:add_block_descendents_batchpopulates the cache once per batch.Deadline
None.
Other information
edx.HtmlUtils.setHtml(el, edx.HtmlUtils.HTML(html)), the same idiom capa'sdisplay.jsuses for server-rendered problem HTML.masterdoes today. It is independent of this feature and can be proposed separately.uv.lockdependencies:test_incremental_render.py,courseware_api/tests/test_views.py,test_model_data.py,xmodule/tests/test_vertical.py,lms/djangoapps/courseware/tests/test_block_render.pyandlms/djangoapps/courseware/tests/test_views.py(496 passed).ruff check .,make xsslint,make pii_check,make check_keywords,make lint-imports,make check-types, andpylinton the changed files.AI use
I used Claude Code (Anthropic Claude) for this PR:
masterand adapting them (imports, the capa test factory path, the setting annotations).I reviewed every change and verified it with the tests and checks listed above. Commits 3 to 7 have a
Co-authored-bytrailer for Claude. Commits 1 and 2 are Santhosh Kumar's downstream code and credit him as co-author.🤖 Generated with Claude Code