Skip to content

perf: short-circuit observe() when OTEL_ENABLE_EXTRA_METRICS is disabled#1087

Open
hugo-ccabral wants to merge 1 commit intomainfrom
perf/shortcircuit-observe-when-disabled
Open

perf: short-circuit observe() when OTEL_ENABLE_EXTRA_METRICS is disabled#1087
hugo-ccabral wants to merge 1 commit intomainfrom
perf/shortcircuit-observe-when-disabled

Conversation

@hugo-ccabral
Copy link
Contributor

@hugo-ccabral hugo-ccabral commented Feb 25, 2026

Summary

  • When OTEL_ENABLE_EXTRA_METRICS is disabled (common production case), observe() now directly calls f() without any wrapping
  • Previously, even with metrics off, every resolver invocation went through an async wrapper with performance.now(), try/catch, and isWrappedError() check
  • The histogram and full implementation are now lazily initialized only when metrics are enabled

Context

CPU profile showed observability overhead at ~0.9% of total CPU (548 hits as self time in observe.ts + timings.ts). This is called for every resolver invocation in the pipeline.

Test plan

  • Run existing deco test suite
  • Verify metrics still work when OTEL_ENABLE_EXTRA_METRICS is set
  • Verify no functional change when metrics are disabled (the pass-through preserves the Promise chain)

Summary by cubic

Short-circuits observe() when OTEL_ENABLE_EXTRA_METRICS is disabled, calling the resolver directly. This removes per-call wrapper overhead and reduces CPU (~0.9% in profiles).

  • Refactors
    • Bypass performance.now/try-catch/isWrappedError when metrics are off; call f() directly.
    • Lazily initialize the histogram and metrics wrapper only when metrics are on; behavior and Promise chain remain the same.

Written for commit f5997fa. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Refactor
    • Restructured observability metrics handling with conditional enabling based on feature configuration
    • When metrics collection is disabled, eliminates unnecessary async wrapper performance overhead
    • Maintains full backward compatibility with existing API contracts

When extra metrics are disabled (the common production case), observe() now directly calls f() without any wrapping. Previously it always wrapped in an async function with performance.now(), try/catch, and isWrappedError check, even when metrics were off. CPU profile showed observability overhead at ~0.9 percent with 548 hits as self time.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions
Copy link
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 1.164.1 update
  • 🎉 for Minor 1.165.0 update
  • 🚀 for Major 2.0.0 update

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

The observe function was refactored to conditionally enable operation duration metrics based on the OTEL_ENABLE_EXTRA_METRICS environment flag. The metrics-enabled logic was extracted into an internal observeWithMetrics wrapper, while the exported observe function now resolves to either the metrics implementation or a no-op passthrough.

Changes

Cohort / File(s) Summary
Core Observability
observability/observe.ts
Refactored observe function to conditionally enable metrics. Introduced observeWithMetrics wrapper containing the metrics-enabled logic with histogram creation. Exported observe now resolves to either metrics implementation or no-op passthrough based on OTEL_ENABLE_EXTRA_METRICS flag.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 With metrics now dancing through conditional hoops,
We observe with flair in two graceful loops!
When the flag says "yes" we measure with pride,
When it says "no" we swiftly glide,
Performance and choice, now hand in paw! 📊

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: short-circuit observe() when OTEL_ENABLE_EXTRA_METRICS is disabled' clearly and specifically describes the main change: adding a performance optimization that bypasses the observe() wrapper when metrics are disabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/shortcircuit-observe-when-disabled

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@observability/observe.ts`:
- Around line 5-10: The observeWithMetrics IIFE eagerly calls
meter.createHistogram (operationDuration) at import time; change
observeWithMetrics into a lazily-initialized function that checks
OTEL_ENABLE_EXTRA_METRICS (or the existing enable flag) on first invocation,
creates operationDuration via meter.createHistogram only when enabled, caches it
for subsequent calls, and returns a no-op behavior when disabled so no histogram
is created during import; update any references to observeWithMetrics to use the
new function shape if necessary.
- Around line 40-45: The disabled branch of the exported observe function can
throw synchronously because it directly invokes f(), violating the Promise
contract; change the fallback implementation to wrap the invocation in a
resolved Promise (e.g., use Promise.resolve().then(() => f()) or
Promise.resolve().then(f)) so any synchronous throw becomes a rejected Promise;
update the branch that currently returns (_op, f) => f() to instead return (_op,
f) => Promise.resolve().then(() => f()) while keeping the same signature and
preserving use of observeWithMetrics when OTEL_ENABLE_EXTRA_METRICS is true.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e02918 and f5997fa.

📒 Files selected for processing (1)
  • observability/observe.ts

Comment on lines +5 to +10
const observeWithMetrics = (() => {
const operationDuration = meter.createHistogram("block_op_duration", {
description: "operation duration",
unit: "ms",
valueType: ValueType.DOUBLE,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

observeWithMetrics is still eagerly initialized when metrics are disabled.

Line 5 uses an IIFE, so meter.createHistogram(...) on Line 6 runs at import time even when OTEL_ENABLE_EXTRA_METRICS is false. That defeats the lazy-init/perf goal in disabled mode.

💡 Proposed fix
-const observeWithMetrics = (() => {
+const createObserveWithMetrics = () => {
   const operationDuration = meter.createHistogram("block_op_duration", {
     description: "operation duration",
     unit: "ms",
     valueType: ValueType.DOUBLE,
   });

   return async <T>(
     op: string,
     f: () => Promise<T>,
   ): Promise<T> => {
@@
-  };
-})();
+  };
+};

 export const observe: <T>(
   op: string,
   f: () => Promise<T>,
 ) => Promise<T> = OTEL_ENABLE_EXTRA_METRICS
-  ? observeWithMetrics
+  ? createObserveWithMetrics()
   : (_op, f) => f();

Also applies to: 34-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@observability/observe.ts` around lines 5 - 10, The observeWithMetrics IIFE
eagerly calls meter.createHistogram (operationDuration) at import time; change
observeWithMetrics into a lazily-initialized function that checks
OTEL_ENABLE_EXTRA_METRICS (or the existing enable flag) on first invocation,
creates operationDuration via meter.createHistogram only when enabled, caches it
for subsequent calls, and returns a no-op behavior when disabled so no histogram
is created during import; update any references to observeWithMetrics to use the
new function shape if necessary.

Comment on lines +40 to +45
export const observe: <T>(
op: string,
f: () => Promise<T>,
) => Promise<T> = OTEL_ENABLE_EXTRA_METRICS
? observeWithMetrics
: (_op, f) => f();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the observability directory and the observe.ts file
fd -t f observe.ts

Repository: deco-cx/deco

Length of output: 80


🏁 Script executed:

# Read the observe.ts file to see full context
cat -n observability/observe.ts

Repository: deco-cx/deco

Length of output: 1555


🏁 Script executed:

# Search for callers of the observe function
rg "observe\(" -A 3 -B 1 --type ts --type js

Repository: deco-cx/deco

Length of output: 1210


🏁 Script executed:

# Check git history to see previous implementations
git log --oneline -20 -- observability/observe.ts

Repository: deco-cx/deco

Length of output: 136


🏁 Script executed:

# Look for tests of the observe function
fd -t f "*.test\|*.spec" | xargs grep -l "observe"

Repository: deco-cx/deco

Length of output: 351


🏁 Script executed:

# Search for imports of observe function from observe.ts
rg "from ['\"].*observe" --type ts --type js | head -20

Repository: deco-cx/deco

Length of output: 125


🏁 Script executed:

# Search for "observe" being called with a function argument
rg "observe\s*\(" -A 2 --type ts --type js | grep -B 2 -A 2 "observe.*=>|observe.*function"

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Get the git diff for the perf commit
git show f5997fa --stat

Repository: deco-cx/deco

Length of output: 18468


🏁 Script executed:

# Check what the previous implementation looked like
git show f5997fa

Repository: deco-cx/deco

Length of output: 50372


🏁 Script executed:

# Find actual calls to observe() from observe.ts module
rg "observe\(" runtime/mod.ts -A 3 -B 1

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Search for how observe is used across the codebase
rg "observe\(" --type ts -B 2 -A 5 | grep -A 7 -B 3 "observe.*Promise\|observe.*async\|observe.*catch"

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Look for error handling around observe calls
rg "observe\(" --type ts --type js -A 10 | head -50

Repository: deco-cx/deco

Length of output: 1888


🏁 Script executed:

# Read runtime/mod.ts to see how observe is actually used
cat -n runtime/mod.ts | head -100

Repository: deco-cx/deco

Length of output: 4211


🏁 Script executed:

# Search more carefully for the observe function usage pattern
rg "observe\s*\(" runtime/mod.ts -A 5 -B 2

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Look for the exact pattern where observe() is called with a function
rg "observe\(['\"]" --type ts --type js -A 3

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Check if there are any tests for the observe function
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "observe" 2>/dev/null || echo "No test files found matching observe"

Repository: deco-cx/deco

Length of output: 92


🏁 Script executed:

# Search for actual invocations of observe() with string and function
rg "observe\s*\(\s*['\"]" --type ts --type js -A 3

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Look at more of runtime/mod.ts to find where observe is called
cat -n runtime/mod.ts | sed -n '100,276p'

Repository: deco-cx/deco

Length of output: 5771


🏁 Script executed:

# Search for "observe(" as a function call in the entire codebase
rg "observe\s*\(\s*['\"]" --type ts --type js

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Check if observe function is used in any render or invoke functions
rg "observe" runtime/features/render.tsx -A 3 -B 2
rg "observe" runtime/features/invoke.ts -A 3 -B 2
rg "observe" runtime/features/meta.ts -A 3 -B 2

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Search for how monitoring.metrics is used
rg "monitoring\.metrics\|state\.monitoring\.metrics" --type ts --type js -A 3 -B 1

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Search for uses of the metrics property in monitoring
rg "\.metrics\s*\(" --type ts --type js -A 3 -B 1 | head -50

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Look for where observe might be called as a function
rg "metrics\s*\(\s*['\"]" --type ts --type js -A 2

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Look for where the function passed to observe might throw
rg "observe\|metrics" engine/ --type ts -A 5 | head -80

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Check the resolver to see how observe/metrics is used
rg "monitoring" engine/core/resolver.ts -A 3 -B 3

Repository: deco-cx/deco

Length of output: 1006


🏁 Script executed:

# Look at blocks/loader.ts since observe is imported with isWrappedError from there
rg "isWrappedError" blocks/loader.ts -A 3 -B 3

Repository: deco-cx/deco

Length of output: 234


🏁 Script executed:

# Search for any synchronous error throwing patterns in the codebase
rg "throw\s+new\s+Error|throw\s+\w" blocks/loader.ts -B 2 -A 2 | head -40

Repository: deco-cx/deco

Length of output: 327


🏁 Script executed:

# Check if there are any function blocks that might synchronously throw before returning Promise
rg "observe\(" --type ts -B 5 -A 5 runtime/ | head -100

Repository: deco-cx/deco

Length of output: 856


🏁 Script executed:

# Check if metricsFunc is actually used in resolver.ts
cat -n engine/core/resolver.ts | grep -A 10 -B 5 "metricsFunc"

Repository: deco-cx/deco

Length of output: 1206


🏁 Script executed:

# Search for any patterns where observe might be invoked
rg "observe\s*\(\s*[\"\']|metrics\s*\(\s*[\"\']" --type ts --type js

Repository: deco-cx/deco

Length of output: 180


🏁 Script executed:

# Look at the complete monitoring type definition
rg "interface Monitoring|type Monitoring" --type ts -A 10

Repository: deco-cx/deco

Length of output: 566


🏁 Script executed:

# Check what the previous version of observe looked like before the perf commit
git show f5997fa^:observability/observe.ts 2>/dev/null || echo "Cannot retrieve previous version"

Repository: deco-cx/deco

Length of output: 88


🏁 Script executed:

# Find where metricsFunc is actually called with arguments
rg "metricsFunc" engine/core/resolver.ts -A 5 -B 2

Repository: deco-cx/deco

Length of output: 796


🏁 Script executed:

# Check the ObserveFunc type definition
rg "type ObserveFunc|interface ObserveFunc" --type ts -A 3

Repository: deco-cx/deco

Length of output: 240


🏁 Script executed:

# Verify that the function passed to observe at line 478 is always async
sed -n '476,490p' engine/core/resolver.ts

Repository: deco-cx/deco

Length of output: 786


🏁 Script executed:

# Check if there are any other callers of metrics function besides the one in resolver
rg "metrics\?" engine/core/resolver.ts -n | grep -v "monitoring"

Repository: deco-cx/deco

Length of output: 38


🏁 Script executed:

# Check if there are any other uses of observe/metrics in the codebase
rg "metricsFunc" engine/ --type ts -A 3 -B 1

Repository: deco-cx/deco

Length of output: 784


🏁 Script executed:

# Verify the complete call signature at line 478
sed -n '475,495p' engine/core/resolver.ts

Repository: deco-cx/deco

Length of output: 926


🏁 Script executed:

# Check if the function signature constraint is enforced
rg "ObserveFunc|<T>\(" observability/observe.ts -B 2 -A 3

Repository: deco-cx/deco

Length of output: 323


🏁 Script executed:

# Do a final check - see if any tests would break with the proposed fix
find . -name "*.test.ts" -o -name "*.spec.ts" | head -20

Repository: deco-cx/deco

Length of output: 254


🏁 Script executed:

# Check if there are any other places where observe might be called besides the one in resolver.ts
rg "observe\(" --type ts --type js | grep -v "observer\|IntersectionObserver"

Repository: deco-cx/deco

Length of output: 521


Disabled branch can break Promise contract via synchronous throws.

At line 45, (_op, f) => f() may throw synchronously if f throws before returning a Promise. The type signature requires f: () => Promise<T>, which provides compile-time protection. In practice, the sole caller in engine/core/resolver.ts always passes an async function that cannot throw synchronously. However, the type constraint can be violated at runtime.

For stricter Promise contract compliance, consider wrapping the call:

Proposed fix
) => Promise<T> = OTEL_ENABLE_EXTRA_METRICS
  ? observeWithMetrics
- : (_op, f) => f();
+ : (_op, f) => Promise.resolve().then(f);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const observe: <T>(
op: string,
f: () => Promise<T>,
) => Promise<T> = OTEL_ENABLE_EXTRA_METRICS
? observeWithMetrics
: (_op, f) => f();
export const observe: <T>(
op: string,
f: () => Promise<T>,
) => Promise<T> = OTEL_ENABLE_EXTRA_METRICS
? observeWithMetrics
: (_op, f) => Promise.resolve().then(f);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@observability/observe.ts` around lines 40 - 45, The disabled branch of the
exported observe function can throw synchronously because it directly invokes
f(), violating the Promise contract; change the fallback implementation to wrap
the invocation in a resolved Promise (e.g., use Promise.resolve().then(() =>
f()) or Promise.resolve().then(f)) so any synchronous throw becomes a rejected
Promise; update the branch that currently returns (_op, f) => f() to instead
return (_op, f) => Promise.resolve().then(() => f()) while keeping the same
signature and preserving use of observeWithMetrics when
OTEL_ENABLE_EXTRA_METRICS is true.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant