ralph: defer per-Note Pandoc AST for #66 (-57% RSS)#742
Draft
srid wants to merge 1 commit into
Draft
Conversation
Drop the eager Pandoc-AST retention in `Note._noteDoc`. The field is
now a lazy thunk bound to a pure markdown re-parse over
`_noteSourceText`; Haskell's normal lazy-evaluation semantics memoise
the parsed Pandoc back into the field on first access.
Lua-filter notes, synthetic notes (mkEmptyNoteWith), and error-carrying
notes still bind `_noteDoc` to the already-evaluated Pandoc because
their post-filter shape can't be reproduced by a pure re-parse.
Pre-extracted summary fields land at parse time so neither `noteRels`
nor `noteTasks` needs to walk the full AST:
- `_noteSourceText :: !Text` -- raw source for re-parse
- `_noteLinks :: ![NoteLink]` -- url + attrs for relation index
- `_noteTaskList :: ![NoteTask]` -- checked + inlines for task index
`parseAndInsert` deepseq-forces the two summary fields at insert time
(but not `_noteDoc` itself — forcing it would re-parse immediately,
defeating the purpose). `mkNoteWith` branches eagerly on the
retain/defer decision so each Note's `_noteDoc` references only one
of `{doc, deferredDoc}`, never both via an if-thunk closure.
`Eq` / `Ord` on `Note` are now manual and compare by `_noteRoute`
alone — derived versions would force `_noteDoc` on every IxSet
insertion-time comparison.
`modelLookupBacklinks` re-extracts per-link context on demand via a
new `Rel.noteRelCtxToTarget`, since `_relCtx` is no longer carried
inside `Rel`.
5-run median peak RSS on the 4.5k synthetic corpus:
| Metric | Baseline | This PR | Δ |
| ------------- | -------: | -------: | --- |
| LOAD_HWM | 5165 MiB | 1660 MiB | -67.9 % |
| AFTER_HWM | 5181 MiB | 2219 MiB | -57.2 % |
Tests pass (125 examples, 0 failures).
Known limitation: dense-embed render latency on the synthetic corpus.
A hub note that transitively reaches hundreds of others through `![[]]`
embeds pays one cold parse per unique reachable note on first render.
The real `docs/` notebook does not surface this (RSS ~255 MiB, sub-100ms
renders).
Refs #66.
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.
Independent of PR #740 — this is a fresh re-implementation of PR #678's approach (deferred per-Note Pandoc retention) without reading or rebasing its commits. Full design notes are in
docs/dev/ralph/memory-66-deferred/README.md.Result (5-run median, 4.5k synthetic corpus,
+RTS -N)READY(s)LOAD_HWMAFTER_HWMHow it works (short)
Note._noteDoc :: Pandocis now a lazy field bound to a re-parse closure over_noteSourceText. First access on the render path forces the closure (pure markdown re-parse, no Lua); Haskell's normal memoisation writes the result back into the field. Three categories opt out and keep the eager Pandoc: Lua-filter notes, synthetic notes (folder placeholders / banners), and parse-error notes.Two new pre-extracted summary fields land at parse time so the model never walks the AST during insertion:
_noteLinks :: ![NoteLink]for the relation index_noteTaskList :: ![NoteTask]for the task indexA few surgical bits make the deferral actually free memory rather than just delay it:
mkNoteWithbranches eagerly on retain-vs-defer, so each Note's_noteDocreferences only one of{doc, deferredDoc}. Anif mustRetain then doc else deferredDocfield value would keep both alive in a thunk closure.parseAndInsertdeepseq-forces only the two summary fields._noteDocis deliberately not forced — forcing it would immediately materialise the re-parse and undo the optimisation.Eq/OrdonNoteare now manual and compare by_noteRouteonly. The derived versions would force_noteDocon every IxSet insertion-time comparison.modelLookupBacklinksrecovers per-link context on demand via a newRel.noteRelCtxToTarget.Test plan
cabal test emanotepasses (125 examples, 0 failures)docs/notebook renders correctly on the routes that work (the pre-existing/guide,/wikilinks,/yaml-config500s reproduce onorigin/mastertoo — not introduced by this PR)Known limitation
The synthetic corpus is intentionally pathological. A note that transitively reaches hundreds of others through
![[]]embeds pays one cold parse per unique reachable note on first render —/topic00/n00032(19 direct embeds, dense fan-out) exceeds the 30 s curl timeout on a cold process. Subsequent renders are fast. On the realdocs/notebook this does not surface (RSS ~255 MiB, sub-100ms renders).Mitigations (not implemented here): stripped-down summary Pandoc for embed bodies (PR #678's approach), request-scoped Pandoc cache plumbed through
RenderCtx, orunsafePerformIO IORefper Note with explicit eviction.Relationship to other PRs
-F1.5+_relCtxdrop_noteDocPR #740 and this PR are independent and additive in spirit. PR #678 still wins on absolute numbers — its
NoteBodyDeferredcarries a stripped-down "summary doc" that bounds embed-render cost, which is the headroom this PR leaves unaddressed.Refs #66.