Skip to content

SchemaRegistry.resolveObjectKey scans the whole registry per lookup, making kernel boot quadratic in stored metadata #10945

Description

@os-zhuang

SchemaRegistry.resolveObjectKey rescans the ENTIRE object registry on every name lookup, so kernel boot is quadratic in how much metadata an environment has stored.

// packages/objectql/src/registry.ts:2366
private resolveObjectKey(name: string): string | undefined {
    const matches: string[] = [];
    for (const fqn of this.objectContributors.keys()) {   // ← every registered object
      const { shortName } = parseFQN(fqn);                 // ← per entry, per lookup
      if (shortName === name) matches.push(fqn);
    }
    
}

It is called from at least seven sites in the same file (1842, 2346, 2453, 2476, 2703, 3133, 3365) — getObject(name) among them — so a boot that registers N objects and looks up O(N) names does O(N²) string work with parseFQN as the inner loop.

Measured

Instrumenting a real per-environment kernel build (ArtifactKernelFactory.create(), full hosted capability slate, real TursoDriver in remote transport over a local libsql file) and sweeping the number of env-wide sys_metadata rows the kernel hydrates at boot (ObjectQLPlugin({ hydrateMetadataFromDb: true }), which the cloud tenant runtime always sets):

sys_metadata rows   bootstrap step   marginal ms/row   database round trips
              0            63 ms            —                  242
            500           232 ms         0.338                 243
          1,000           422 ms         0.380                 243
          2,000         1,036 ms         0.614                 243
          4,000         2,627 ms         0.798                 243
          8,000         8,533 ms         1.496                 315

Round trips are FLAT — this is not I/O. Log-log fit of (bootstrap − baseline) against row count: exponent 1.413, R² = 0.994.

A --cpu-prof of the 8,000-row boot names the loop directly. Self time attributed to parseFQN + resolveObjectKey + getPackagedObjectOwner:

sys_metadata rows    registry-scan self time
          1,000                80 ms
          2,000               192 ms   (2.4x)
          4,000               631 ms   (3.3x)
          8,000             2,325 ms   (3.7x)

Each doubling multiplies the cost by ~3.7 — exponent ≈ 1.89, i.e. quadratic. parseFQN is the single largest non-database entry in the profile.

Why it matters

This is the mechanism behind objectstack-ai/cloud#1555: a hosted environment whose bootstrap step takes 134 s against a 20 s request waiter, so every request answers kernel_warming and the environment can never be opened. The build never fails — it is just slower than anything will wait for. Boot cost that is quadratic in stored metadata means an environment gets permanently unservable purely by being used, with no error anywhere.

Direction

objectContributors is keyed by FQN; the short-name→FQN direction wants its own index maintained alongside it (a Map<shortName, fqn[]> updated in register/unregister), turning every one of these lookups into O(1). The ambiguity warning the current loop emits is preserved by keeping the array of matches per short name.

Two properties worth pinning with the fix: a lookup must not depend on registration order, and unregisterObject must keep both maps in step — the extraction note at registry.ts:2350 says the read and the name-addressed removal must not disagree about which contributor a bare name addresses, and a second index is exactly where that could drift.

Reproducing

objectstack-ai/cloud: node scripts/dev-local/bootstrap-curve.mjs --hosted --meta-rows 0,1000,4000,8000

Refs objectstack-ai/cloud#1555

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions