Repository navigation
Conversation
…ject Looking up a single file, checking whether a path exists, or reading the head version number previously built details (and, for getObject, a file retriever) for every file in the version, and describeObject did so for every file in every version. For objects with many files this made each lookup cost O(files), or O(versions x files). Add VersionStateMap, a read-only map over a version's state that creates values on demand, and use it for ResponseMapper.mapFileDetails, DefaultOcflStorage.getObjectStreams and DefaultOcflRepository.getObject. No public API changes; the file maps returned by VersionDetails and OcflObjectVersion are now read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iterating over a VersionStateMap's entries or values created every value on each iteration, so code that iterated the same VersionDetails or OcflObjectVersion more than once paid the full mapping cost each time, and the stream-based iterator added overhead to a single iteration. Build a regular map the first time the entries or values are iterated and use it for later iterations and lookups. Iterating once or repeatedly now costs about the same as before VersionStateMap, and repeated lookups of a path return the same instance after the map has been iterated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
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.
Map version files lazily in describeVersion, describeObject and getObject
Fixes #153
Summary
describeVersion(),getObject()anddescribeObject()built details for every file in a version before returning (describeObject()for every version). Looking up one file, checking a path, or reading the head version number therefore cost O(files in the object), anddescribeObject()cost O(versions × files). See # for details and how Fedora hits it.This PR returns read-only maps that create a file's details when that path is looked up, using the inventory's existing O(1) per-path lookups. The first full iteration creates every entry once and keeps them, so iterating costs about the same as before.
Changes
io.ocfl.core.util.VersionStateMap: a read-onlyMap<String, V>over one version's state.get/containsKeyare O(1).keySet()andsize()never create values.entrySet()/values()builds a regular map and keeps it for later iterations and lookups.ResponseMapper.mapFileDetailsreturns aVersionStateMap<FileDetails>. This is whatdescribeVersion()anddescribeObject()use.DefaultOcflStorage.getObjectStreamsreturns aVersionStateMap<OcflFileRetriever>.DefaultOcflRepository.getObjectbuilds itsOcflObjectVersionFilemap as aVersionStateMapon top of those two maps.No public method signatures change and no dependencies are added.
Compatibility
Code that only reads the results needs no changes. Behaviour changes downstream code could notice:
VersionDetails.getFileMap()/getFiles(),OcflObjectVersion.getFiles()andOcflStorage.getObjectStreams()are now read-only. Code that modifies them (or a customOcflStoragewrapper that rewrites the stream map) getsUnsupportedOperationException. Workaround:new HashMap<>(versionDetails.getFileMap()).getFile(path)returns a newFileDetails/OcflObjectVersionFile. Changes made to a returnedFileDetailsare not reflected in later lookups.equals()comparisons are unaffected.fileMap) differs from the previousHashMaporder. Neither order was ever guaranteed.describeVersion()/getObject()is called.Because of the read-only change, a minor release (2.3.0) may be more appropriate than a patch release.
Benchmarks
JMH, one object whose every version holds all of its files, default inventory cache enabled. Apple M2 Pro, JDK 26, 1 fork, 3 × 1 s warmup, 5 × 1 s measurement. Time per call, 2.2.3 → this PR (1 version unless noted).
Lookups
describeVersion(head).getVersionNum()describeVersion(head).containsFile(path)getObject(head).getFile(path)+ readdescribeObject(id).getHeadVersionNum()* High variance in this run (±243 ns); about 60 ns in every other configuration.
Allocation per lookup drops from 1.0 KB (1 file) up to 8.7 MB (10,000 files) to 216 B, and for
describeObjectwith 10 versions from up to 80.6 MB to 3.1 KB. Reading one file is dominated by disk access, so it is unchanged for very small objects.Iterating over every file (the case laziness can't help)
getFiles()oncegetFiles()twiceIteration is within about ±15% of 2.2.3, with allocation unchanged. The largest relative difference is up to 15% (16–75 ns) slower for objects of 1–10 files.
In Fedora: a 500-page item stored as an archival group (2,003 resources in one OCFL object), Fedora 7 with Postgres 17, ocfl-java 2.2.1 → this PR:
This puts the archival group on a par with the same item stored as separate objects (0.3–0.4 s to list, 4–6 s to walk). In the storage layer alone (fcrepo-storage-ocfl 7.0.0), reading every member's headers and RDF went from 3.0 s to 23 ms, or from 28.0 s to 94 ms for a 1,290-member group. Fedora's full test suite (2,056 tests) passes against this branch.
Benchmark source
Testing
VersionStateMapTest: lookups map only the requested path; keys and size map nothing; repeated iteration maps each path once and returns the same instances; equality with an eagerly built map; modifications rejected.ResponseMapperTest: file details for every path, including duplicate content and fixity.FileSystemOcflStorageTest:getObjectStreams()creates no retrievers until a path is looked up, then exactly one../mvnw clean installpasses: ocfl-java-api 14, ocfl-java-core 326, ocfl-java-aws 19 and ocfl-java-itest 367 tests (filesystem and S3), 0 failures.