Skip to content

Reading one file from an object costs O(files in the object); describeObject costs O(versions × files) #153

Open

Description

@mohideen

Problem

describeVersion(), getObject() and describeObject() build details for every file before returning, even when the caller needs one path or only version-level metadata:

  • ResponseMapper.mapFileDetails creates a FileDetails (storage path string, fixity map) for every file in the version. describeVersion() uses it, and describeObject() uses it for every version.
  • getObject() also calls DefaultOcflStorage.getObjectStreams, which creates an OcflFileRetriever per file, and then wraps every file in an OcflObjectVersionFile.

So common calls like these pay for every file in the object, on every call, even with the inventory cached:

repo.describeVersion(ObjectVersionId.head(id)).getVersionNum();      // head version number
repo.describeVersion(ObjectVersionId.head(id)).containsFile(path);   // does a path exist
repo.getObject(ObjectVersionId.head(id)).getFile(path).getStream();  // read one file
repo.describeObject(id).getHeadVersionNum();                         // object metadata

Measurements (ocfl-java 2.2.3)

JMH, one object; each version's state holds every file; default inventory cache enabled. Apple M2 Pro, JDK 26.

Operation 10 files 100 files 1,000 files 10,000 files
Head version number (describeVersion) 3.1 µs 37 µs 0.50 ms 7.0 ms
Read one file (getObject(...).getFile(...)) 34 µs 105 µs 1.3 ms 14.0 ms
describeObject, 10 versions 37.5 µs 0.65 ms 7.6 ms 66.1 ms

At 10,000 files, a head-version lookup allocates 8.7 MB and describeObject with 10 versions allocates 80.6 MB per call.

How we hit it

Fedora 7's storage layer (fcrepo-storage-ocfl) calls describeVersion(head) and getObject(head) on every resource read. In a Fedora archival group, all members live in one OCFL object, so every member read pays for the whole group, and reading all of a group's members grows with the square of the group size.

In Fedora 7 (ocfl-java 2.2.1, Postgres 17), a 500-page item stored as an archival group (2,003 resources) took 7.2 s to list its pages with their descriptions embedded, and 88 s to walk every resource (4,006 requests). The same item stored as separate objects took 0.4 s and 4.4 s. In the storage layer alone, reading the headers and RDF of all 500 members takes about 3.0 s in one session, and 4.6 s with one session per member. A larger synthetic test item with 1,290 pages took 55 s to list and 10 min 47 s to walk as an archival group, against 1.4 s and 14.4 s when it was not one.

Proposal

Return read-only maps that create a file's details (and retriever) when that path is looked up, built from the inventory's existing O(1) per-path lookups. This needs no public API changes and no new dependencies. I have a PR ready and will link it here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions