FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

perf(storage): stream container listings to avoid O(children) memory by jeswr · Pull Request #2211 · CommunitySolidServer/CommunitySolidServer · GitHub

perf(storage): stream container listings to avoid O(children) memory - #2211

Open
jeswr wants to merge 11 commits into
CommunitySolidServer:mainfrom
jeswr:perf/stream-container-listings-pr
Open

perf(storage): stream container listings to avoid O(children) memory#2211
jeswr wants to merge 11 commits into
CommunitySolidServer:mainfrom
jeswr:perf/stream-container-listings-pr

Conversation

jeswr commented Aug 18, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Problem

Container reads currently collect every child's metadata in one N3 store and flatten it to a Quad[] before the response starts. Peak memory is therefore O(children), and concurrent reads can exhaust the process heap.

Change

  • Stream containment and child metadata quads from DataAccessorBasedStore with backpressure and iterator cleanup.
  • Keep DataAccessor.getChildren lazy in the in-memory implementation.
  • Expose container emptiness as response metadata so AllowAcceptHeaderWriter does not require a materialised containment list.
  • Let JsonResourceStorage and SingleContainerJsonStorage consume direct ldp:contains statements from the streamed body.
  • Keep the listing—and its read lock—open while consumers iterate it.
  • Add a constrained-heap memory regression test to CI.

The streamed body preserves the previous RDF output. Consumers that destroy it without reading, such as notification ETag generation, no longer enumerate the children.

Impact

The production benchmark used 20 concurrent reads of a 15,000-child container. Peak live heap dropped from roughly 3,056 MB to 140 MB. The CI workload runs the same shape with a 128 MB old-space limit; local runs peak at 23–36 MB, while the previous implementation runs out of memory.

Tests

  • Full unit suite: 358 suites and 2,380 tests passed with 100% source coverage.
  • Focused storage and metadata tests: 5 suites and 110 tests passed.
  • Affected integration suites: 7 suites and 189 tests passed.
  • Build, lint, and test type-check passed.
  • npm run test:memory: 35.6 MB peak heap in the latest local run.

jeswr and others added 8 commits August 18, 2026 21:06
Reading a container previously folded every child's metadata into a single
N3 store and flattened it to a Quad[] before the response streamed, so peak
memory was O(children): hundreds of MB to several GB of transient heap for
very large containers, on every GET. Under fast in-memory locks many such
reads (client listings and notification fan-out, which reads the whole
container to compute an ETag) run concurrently and their peaks stack until
the process is killed by the memory ceiling.

DataAccessorBasedStore.getRepresentation now emits the container listing as
a lazy Readable<Quad> (streamContainerRepresentation): peak memory is O(1).
The full ldp:contains list lives only in the streamed body; a single
ldp:contains marker for the first non-auxiliary child is kept in the
response metadata so AllowAcceptHeaderWriter's empty-container check still
works. Consumers that enumerated members from metadata now read them from
the body: JsonResourceStorage and SingleContainerJsonStorage.

Measured: a 15k-child container listing under 20 concurrent reads drops peak
live heap from ~3056MB to ~140MB (flat). Full unit + integration suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 23:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Pull request overview

This PR reduces peak memory usage for large container reads by streaming ldp:contains and child metadata quads instead of materializing full containment state in memory, and by propagating explicit “container empty” metadata for downstream HTTP header generation.

Changes:

  • Stream container listing quads from DataAccessorBasedStore and make the in-memory accessor’s getChildren() lazy.
  • Update JSON-based storages to consume ldp:contains statements directly from streamed quad bodies.
  • Add a CI memory regression test (constrained heap) and supporting workflow/script changes.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/unit/storage/keyvalue/JsonResourceStorage.test.ts Adds coverage ensuring container member streams are lazy and properly closed on cancellation.
test/unit/storage/DataAccessorBasedStore.test.ts Adds tests for streamed container listings, cancellation cleanup, and containerEmpty metadata.
test/unit/storage/accessors/InMemoryDataAccessor.test.ts Verifies child metadata is generated lazily.
test/unit/init/migration/SingleContainerJsonStorage.test.ts Updates tests to read container membership from quad bodies (and ignore invalid/non-direct statements).
test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts Adds tests validating containerEmpty drives header behavior for streamed containers.
test/memory/container-listing.js Adds heap-usage regression test for concurrent large container listings.
src/util/Vocabularies.ts Introduces solid:containerEmpty and adds xsd:boolean to the vocabulary export.
src/storage/keyvalue/JsonResourceStorage.ts Streams direct members from quad bodies instead of materializing containment metadata.
src/storage/DataAccessorBasedStore.ts Streams container listing output, adds containerEmpty response metadata.
src/storage/accessors/InMemoryDataAccessor.ts Switches container entries to Map and makes getChildren() lazy.
src/init/migration/SingleContainerJsonStorage.ts Uses streamed containment from container quad bodies for migration listing.
src/http/output/metadata/AllowAcceptHeaderWriter.ts Uses explicit containerEmpty response metadata when present.
package.json Adds test:memory script with constrained heap and GC enabled.
.github/workflows/npm-test.yml Runs the new memory regression test on Ubuntu + Node 20.x.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copy link
Copy Markdown
Member

@jeswr both for this and the other PR. Since you added comments to this PR afterwards, it is not clear to me if this is now ready for review or not?

jeswr commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

This PR is ready for review @joachimvh - the commits here post marking as ready-for-review were addressing the co-pilot review comments.

I've marked the other as a draft, and will tag you when it is ready.

Comment thread src/storage/DataAccessorBasedStore.ts Outdated
}

return representation;
private isAuxiliaryChild(child: RepresentationMetadata): boolean {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
private isAuxiliaryChild(child: RepresentationMetadata): boolean {
private isAuxiliaryResourceMetadata(metadata: RepresentationMetadata): boolean {

Nothing about this function is specific to child resources.

jeswr Aug 24, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Good point, applied in a1eb694 !

/**
* Creates a lazy quad stream for a container listing.
*/
protected async streamContainerRepresentation(identifier: ResourceIdentifier, metadata: RepresentationMetadata):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This function seems overly complicated? I might be missing something, but what is the reason you can not just have a function that yields the metadata quads, and then yields all the container listing quads (with perhaps an exception for the first element because of the boolean) without all the code overhead below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

I've cleaned this up as much as I can in 059b2ae.

The main complexity is that childQuads is initialised to determine containerEmpty; so we need to close it - this is done through the return in the finally.

jeswr requested a review from joachimvh August 24, 2026 17:38

jeswr commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@joachimvh - FYI, this PR is ready for re-review

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL