Skip to content

Bypass BatchReader for single-selection loads with no navigate paths #127

Description

@jtnelson

Summary

A single-selection db.load(class, id) against a Concourse server >= 1.0.0 routes through BatchReader / CommandGroup machinery whenever selections.length == 1 and the class's navigate-path set is non-null — even when the navigate-path set is null/empty, i.e. when there is no batching benefit. In that narrow case the load pays the prepare() / submit() / drain() ceremony to ship a single operation that IncrementalReader could ship with less machinery.

Scope and impact

This is contained perf hygiene with narrow applicability, not a fix for any specific latency regression. Worth understanding when this short-circuit actually fires before assuming it helps:

  • For routes that go through the framework's prefetch middleware (e.g., MetaRouter in cinchapi-platform-core), the prefetch enjoins the record selection with the audience-User selection into one BatchReader submit. By the time the call reaches Runway.selectOne, selections.length >= 2, so the short-circuit doesn't apply. The handler's subsequent audience.load(clazz, id) is then a reservation cache hit and doesn't reach this code path at all.
  • It does apply to: direct db.load(class, id) calls in application code (e.g., from @Computed methods or other model code), routes that bypass the prefetch middleware, and any future code path that calls selectOne with a single selection against a class that has no Record-typed fields.

For the typical authenticated /collection/<id> route shape, this short-circuit is unlikely to be the lever that moves a measured regression.

Evidence

The length-1 branch in selectOne already exists, but inside it the choice between BatchReader and IncrementalReader is gated only on supportsBulkCommands, with no consideration of whether there is anything to batch:

Runway.java:1051-1064

if(selections.length == 1) {
    DatabaseSelection<?> selection = selections[0];
    if(selection.state == Selection.State.RESOLVED) {
        selection.setState(Selection.State.FINISHED);
    }
    else {
        try (Reader reader = supportsBulkCommands
                ? new BatchReader(connections)
                : new IncrementalReader(connections)) {
            $selectWithPossibleSources(reader, selection, null);
            reader.drain();
        }
        reserve(selection);
    }

BatchReader.prepare() / submit(): BatchReader.java:186, :395

When BatchReader is the right choice

  • selections.length > 1 — genuine batching benefit.
  • selections.length == 1 && navigatePaths != null — navigate is folded into the same CommandGroup as the select; one wire RTT instead of two.

When it is pure overhead

  • selections.length == 1 && (navigatePaths == null || navigatePaths.isEmpty()) — nothing to fold; the IncrementalReader path does the same wire work with less machinery.

Proposed fix

Inside the existing selections.length == 1 branch, additionally choose IncrementalReader when the class for that selection has no navigate paths:

boolean hasNavigate = getNavigatePathsForClassIfSupported(selection.clazz) != null;
try (Reader reader = (supportsBulkCommands && hasNavigate)
        ? new BatchReader(connections)
        : new IncrementalReader(connections)) {
    ...
}

(With the class-hierarchy variant for the needsSectionLookup path.)

Verification

  1. Add a counter on BatchReader.submit() / IncrementalReader.read() in a sandbox build.
  2. Load one record of a class with no Record-typed fields. Pre-fix: BatchReader.submit count = 1. Post-fix: IncrementalReader.read count = 1.
  3. Benchmark a single-record load of a leaf-ish model. Expect a measurable drop in wall time per call.

Out of scope

  • The unconditional cleanup BFS following raw Link values in non-DeferredReference fields (Runway.java:1773, :2056, :2595) — separate concern, larger impact.

Priority

Low. Real inefficiency where it applies, but the cases where it applies are narrow and don't include the typical authed /collection/<id> route shape served by the framework. File and fix when convenient; not a blocker for any known latency regression.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions