Skip to content

perf: cache resourceLoader across embedded runs to eliminate 5-9s reload overhead#82523

Closed
sunvember wants to merge 1 commit into
openclaw:mainfrom
sunvember:perf/embedded-resource-loader-cache
Closed

perf: cache resourceLoader across embedded runs to eliminate 5-9s reload overhead#82523
sunvember wants to merge 1 commit into
openclaw:mainfrom
sunvember:perf/embedded-resource-loader-cache

Conversation

@sunvember

Copy link
Copy Markdown

Problem

DefaultResourceLoader construction triggers packageManager.resolve() which scans skills directories and blocks the event loop for 5-9 seconds on every embedded run. This is pure overhead when the same workspace/agent is reused across consecutive runs (normal session flow).

Solution

Add in-memory cache keyed by (cwd, agentDir) in resource-loader.ts:

  • Cache TTL defaults to 60s, configurable via OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS (min 10s, max 300s)
  • markResourceLoaderReloaded() tracks reload completion
  • pruneResourceLoaderCache() for periodic cleanup
  • invalidateResourceLoaderCache() for targeted or full cache invalidation

Files Changed

File Change
resource-loader.ts Cache logic (cache map, TTL, validity check, prune)
resource-loader.test.ts 10 new test cases covering cache hit/miss/invalidate/prune/TTL
attempt.ts Call markResourceLoaderReloaded after reload()
compact.ts Call markResourceLoaderReloaded after reload()

Checklist

  • Backward compatible (no behavior change when cache is empty)
  • Configurable TTL via env var
  • Targeted invalidation for workspace-specific changes
  • Tests added
  • AI-assisted development

…oad overhead

DefaultResourceLoader construction triggers packageManager.resolve() which
scans skills directories and blocks the event loop for 5-9 seconds on every
embedded run. This is pure overhead when the same workspace/agent is reused
across consecutive runs (normal session flow).

Solution:
- Add in-memory cache keyed by (cwd, agentDir) in resource-loader.ts
- Cache TTL defaults to 60s, configurable via OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS
- add markResourceLoaderReloaded() to track reload completion
- Add pruneResourceLoaderCache() for periodic cleanup
- Add invalidateResourceLoaderCache() for targeted or full cache invalidation
- Call markResourceLoaderReloaded in attempt.ts and compact.ts after reload()

Files changed:
- resource-loader.ts: cache logic
- resource-loader.test.ts: 10 new test cases
- attempt.ts: call markResourceLoaderReloaded after reload
- compact.ts: call markResourceLoaderReloaded after reload

AI-assisted development
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. triage: refactor-only Candidate: refactor/cleanup-only PR without maintainer context. labels May 16, 2026
@clawsweeper

clawsweeper Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR adds a process-local DefaultResourceLoader cache for embedded Pi runs keyed by cwd and agentDir, with TTL/invalidation helpers, reload markers in the run paths, and cache tests.

Reproducibility: no. high-confidence real benchmark reproduction was provided. Source inspection is high-confidence that the PR still calls the upstream reload() path that runs packageManager.resolve() on every embedded run, and the stale-input regression is reproducible by same-workspace runs with different config or provider-derived factories.

Real behavior proof
Needs real behavior proof before merge: The PR body and live metadata include no after-fix real setup benchmark, terminal output, logs, recording, or linked artifact showing the claimed embedded-run speedup. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Needs contributor changes, owner review of the cache contract, and real behavior proof; automation cannot supply the contributor's benchmark evidence for this PR.

Security
Cleared: The diff does not touch dependencies, CI, secrets, release scripts, or other supply-chain execution surfaces.

Review findings

  • [P2] Include run-specific inputs in the cache contract — src/agents/pi-embedded-runner/resource-loader.ts:133
  • [P2] Avoid reloading on cache hits — src/agents/pi-embedded-runner/run/attempt.ts:1845
  • [P2] Fix the TTL test to honor the minimum clamp — src/agents/pi-embedded-runner/resource-loader.test.ts:231
Review details

Best possible solution:

Keep the current fresh-loader behavior unless a benchmarked optimization caches the actual expensive work while preserving per-run settings, extension factory inputs, invalidation, and real timing proof.

Do we have a high-confidence way to reproduce the issue?

No high-confidence real benchmark reproduction was provided. Source inspection is high-confidence that the PR still calls the upstream reload() path that runs packageManager.resolve() on every embedded run, and the stale-input regression is reproducible by same-workspace runs with different config or provider-derived factories.

Is this the best way to solve the issue?

No. This caches the wrong level of state, still calls the expensive reload path, and adds a TTL env knob without proof; a safer solution would benchmark the hotspot and cache only immutable resolved resources or reuse loaders with a complete input key and explicit invalidation.

Full review comments:

  • [P2] Include run-specific inputs in the cache contract — src/agents/pi-embedded-runner/resource-loader.ts:133
    createEmbeddedPiResourceLoader now keys the cache only by cwd and agentDir, but the callers build a fresh settingsManager and extensionFactories each run from config, sessionManager, provider, and model. A cache hit returns the first loader and silently ignores later inputs, so subsequent same-workspace runs can use stale compaction, context-pruning, and tool-result middleware state.
    Confidence: 0.92
  • [P2] Avoid reloading on cache hits — src/agents/pi-embedded-runner/run/attempt.ts:1845
    The expensive upstream path is DefaultResourceLoader.reload(), which calls packageManager.resolve(). This caller still awaits reload() before marking the loader as reloaded, so repeated embedded runs still pay the scan even when the loader was cached; move reload into the cache-miss/freshness path or return an explicit hit/miss result.
    Confidence: 0.9
  • [P2] Fix the TTL test to honor the minimum clamp — src/agents/pi-embedded-runner/resource-loader.test.ts:231
    This test sets OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS to 100, but resolveCacheTtlMs() clamps positive values to the 10s minimum. Waiting 150ms cannot expire the entry, so the prune assertion will fail instead of proving cache cleanup.
    Confidence: 0.95

Overall correctness: patch is incorrect
Overall confidence: 0.92

Acceptance criteria:

  • node scripts/run-vitest.mjs src/agents/pi-embedded-runner/resource-loader.test.ts
  • Benchmark repeated embedded Pi runs before and after the patch in a real workspace, with redacted timing output.
  • Exercise same-workspace runs that change config/provider/model context-pruning inputs to prove no stale extension factory or settings state is reused.

What I checked:

  • PR cache key drops dynamic run inputs: The PR diff caches loaders by cwd and agentDir and returns the cached loader on hits, while settingsManager and extensionFactories are not part of the cache key or refreshed on reuse. (src/agents/pi-embedded-runner/resource-loader.ts:133, 559095a130d3)
  • Current main builds fresh settings per embedded run: Current runEmbeddedAttempt creates a prepared Pi settings manager and extension factories for each run before constructing the loader, so reusing the first loader can preserve stale config-derived state. (src/agents/pi-embedded-runner/run/attempt.ts:1804, 9c5acb7ea363)
  • Extension factories depend on per-run provider/model/session state: buildEmbeddedExtensionFactories derives compaction and context-pruning runtime from cfg, sessionManager, provider, modelId, and model, making reuse by only workspace and agent directory too coarse. (src/agents/pi-embedded-runner/extensions.ts:141, 9c5acb7ea363)
  • Upstream reload remains the expensive path: In @earendil-works/[email protected], DefaultResourceLoader.reload() calls settingsManager.reload() and packageManager.resolve(); the constructor only stores options and creates a package manager.
  • PR still reloads on every run: The PR adds markResourceLoaderReloaded after the existing unconditional await resourceLoader.reload(), so cache hits still run the package-resolution reload path. (src/agents/pi-embedded-runner/run/attempt.ts:1845, 559095a130d3)
  • Real behavior proof is absent: The live PR body contains rationale, a checklist, and tests, but no after-fix benchmark output, terminal log, recording, or other real setup proof. (559095a130d3)

Likely related people:

  • steipete: Authored and committed the current embedded resource-loader helper in fix(agents): avoid Pi resource discovery stalls, the exact surface this PR changes. (role: feature-history owner; confidence: high; commits: a3c7fea5126e, 694ca50e9775; files: src/agents/pi-embedded-runner/resource-loader.ts, src/agents/pi-embedded-runner/run/attempt.ts, src/agents/pi-embedded-runner/compact.ts)
  • dataCenter430: Co-authored the commit that introduced the current embedded resource-loader helper and was credited in the changelog entry for avoiding Pi resource discovery stalls. (role: co-author on current behavior; confidence: medium; commits: a3c7fea5126e; files: src/agents/pi-embedded-runner/resource-loader.ts, src/agents/pi-embedded-runner/run/attempt.ts, src/agents/pi-embedded-runner/compact.ts)
  • MonkeyLeeT: Recent merged work touched embedded Pi extension behavior, which is one of the dynamic inputs this PR would accidentally freeze behind the loader cache. (role: recent adjacent contributor; confidence: medium; commits: 832b65ccea33; files: src/agents/pi-embedded-runner/extensions.ts, src/agents/pi-embedded-runner.extensions.test.ts)

Remaining risk / open question:

  • The exact claimed 5-9s overhead remains unverified because the PR does not include real before/after runtime proof.
  • I did not run tests or benchmarks in this read-only review; the blocking findings are from source and dependency-contract inspection.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 9c5acb7ea363.

@sunvember

Copy link
Copy Markdown
Author

Closing: after deeper analysis, the caching approach was fundamentally flawed. DefaultResourceLoader constructor is cheap — the expensive work (packageManager.resolve()) happens in reload(), which is called every time. Caching the loader instance doesn't avoid the bottleneck. See detailed analysis in the thread. Will redesign and submit a new PR.

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

Labels

agents Agent runtime and tooling size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. triage: refactor-only Candidate: refactor/cleanup-only PR without maintainer context.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant