Skip to content

Interpret calls from compiled code to cold methods without unwinding the caller - #27

Merged
Project516 merged 3 commits into
masterfrom
p516/nested-interp
Nov 3, 2026
Merged

Project516 merged 3 commits into
masterfrom
p516/nested-interp

Conversation

@Project516

@Project516 Project516 commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

When compiled code called a method that was not compiled yet, the caller saved its state into its frame and returned to the run loop. The interpreter then ran the callee, and the caller resumed from the saved state. javac has hundreds of visitor methods that each get fewer than 500 calls, so compiling 30 files made 3.2 million of these trips.

Now slowCall runs such a callee in the interpreter right away, nested on the JS stack the way a direct compiled call runs (callNested in interp.ts). The callee's frame is marked direct:

  • Its return value goes to t.ret.
  • When an exception leaves it, raise throws it to the compiled caller as a JavaThrow.
  • When the call cannot finish in place (a native frame such as a wait or an exception being constructed, a blocked monitor, a thread switch, or the thread's slice used up), the callee's frame loses its direct mark. callNested then returns PUSHED, and the caller saves its state as it does for a native that waits.
  • Synchronized callees and calls past the direct-call depth limit take the old path.

Thread fairness. Each compiled method used to yield after 4000 of its own backward branches, and loops in the methods it called did not count. Interpreted callees used to run in the run loop, so their instructions counted against the thread's slice and ended it. Doing that work in place let a thread spinning on compiled calls hold the CPU: java/util/concurrent/ConcurrentHashMap/ToArray.java timed out because its checking thread starved the threads filling the map. Now JThread.slice holds what is left of the run loop's budget while compiled code runs. Every backward branch in compiled code and every instruction interpreted in place spend it, so loop work anywhere on the stack ends the slice.

Results on javac compiling 30 files, 3 interleaved rounds:

master this PR
wall 43.4 s 41.7 s
user 73.5 s 70.7 s
run loop entries 3.18 million 0.57 million

Peak memory is about the same, and javac's output is byte-identical. bench/Bench.java is unchanged within noise. The ConcurrentHashMap jtreg tests pass, ToArray in 6.4 s.

test/java/NestedCalls.java covers compiled code calling cold methods:

  • results of each type;
  • exceptions thrown back to the caller, with stack traces and line numbers matching HotSpot;
  • an exception that passes compiled code to a cold caller's handler;
  • NPEs and thread waits that fall back to the run loop;
  • monitors, recursion and stack overflow.

Tests: pnpm test passes (130 tests). JW_JIT_STATS now also prints the total number of run loop entries.

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Summary

Summary by CodeRabbit

  • Improvements
    • Nested calls now execute more smoothly, with return values and exceptions handled consistently across interpreted and compiled code.
    • Long-running compiled code follows a shared execution budget, helping maintain predictable runtime behavior.
    • Exception details remain available when errors cross nested calls, including stack overflow conditions.

The JIT now routes eligible calls through direct interpreter frames. The interpreter handles direct-frame returns and exceptions, while compiled execution uses a shared thread slice. A Java test program exercises nested calls and related scenarios.

Changes: Nested Call Execution

Layer / File(s) Summary
Shared compiled-code budget
src/thread.ts, src/jit.ts, src/interp.ts
JThread gains a shared execution slice. Generated JIT code decrements it on backward branches, and the interpreter run loop initializes and reads it. The compiled-code entry message now includes the total entry count.
Direct-call routing and completion
src/jit.ts, src/thread.ts, src/interp.ts
The JIT calls the interpreter hook for eligible non-native methods. The interpreter creates direct frames, returns their results through t.ret, and throws JavaThrow to the compiled caller when unwinding a direct frame.
Nested-call scenarios
test/java/NestedCalls.java
The new test program exercises return values, exceptions, synchronization, recursion, stack overflow, loops, and thread joining.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant JIT
  participant Interpreter
  participant JThread
  JIT->>Interpreter: callNested(thread, method, args)
  Interpreter->>JThread: create and run direct frame
  Interpreter-->>JIT: return result, INTERPRET, or PUSHED
Loading

Possibly related PRs

Suggested reviewers: project516-review-bot

Merge Risk: Minimal · up to 20223

No concrete issue requiring a change before merge is established by the supplied evidence. Run the normal test and build checks before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly describes the primary change: interpreting calls to cold methods from compiled code without unwinding the caller.
Description check Passed The description directly explains the nested interpretation, fallback behavior, thread fairness changes, performance results, and test coverage.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 16 functions across 4 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Simplify code
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

A rabbit hops where call frames spring
Direct calls return with values in tow
The shared slice counts each backward turn
Exceptions find their caller’s path
Test loops burrow through the stack
Then quiet paws return to home

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 16914105-82d3-4ac6-a100-f51afb426387
Commits

Reviewing files that changed from the base of the PR and between 14e8679 and 20223ad.

Files selected for processing: 4
  • src/interp.ts
  • src/jit.ts
  • src/thread.ts
  • test/java/NestedCalls.java

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Recent review details
Context from checks skipped due to timeout: 5
  • GitHub Check: CodeRabbit
  • GitHub Check: jtreg / run (4)
  • GitHub Check: jtreg / run (2)
  • GitHub Check: jtreg / run (1)
  • GitHub Check: jtreg / run (3)
Additional comments: 4
src/thread.ts: 1

105-105: LGTM!

Also applies to: 132-134

src/jit.ts: 1

51-51: LGTM!

Also applies to: 133-133, 193-195, 197-198, 586-586, 694-696

src/interp.ts: 1

7-7: LGTM!

Also applies to: 11-11, 89-89, 294-295, 300-301, 305-306, 640-641, 706-746, 809-809, 844-844, 850-850

test/java/NestedCalls.java: 1

1-140: LGTM!

@project516-review-bot project516-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR optimizes compiled-to-cold calls by interpreting the callee nested on the JS stack (via callNested) instead of returning to the run loop. A direct flag on frames routes returns to t.ret and exceptions to JavaThrow. Fallback to the run loop occurs for synchronized methods, depth limits, native frames, thread waits, or budget exhaustion. Performance improves significantly (3.18M → 0.57M run loop entries). However, raise throws JavaThrow immediately when popping a direct frame, without continuing to unwind outer direct frames. This breaks exception handlers in nested direct calls (e.g., an interpreted caller catching an exception from a deeper direct callee).


review-bot, model nvidia/nemotron-3-ultra-550b-a55b:free, verdict request_changes

Comment thread src/interp.ts Resolved

@project516-review-bot project516-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR optimizes calls from compiled code to cold methods by interpreting them nested on the JS stack (via callNested) instead of returning to the run loop. The interpreter now marks such frames direct so returns go to t.ret and exceptions become JavaThrow. slowCall delegates to hooks.callNested for non-native methods. A new test NestedCalls.java covers results, exceptions, monitors, recursion, stack overflow, and thread waits. However, the callNested loop wraps only JIT calls in a try-catch; interpret calls can also throw JavaThrow from raise for direct frames, which would escape and crash the compiled caller. Also, the depth check uses t.base which is not visible in the provided JThread definition and should be verified.


review-bot, model nvidia/nemotron-3-ultra-550b-a55b:free, verdict request_changes

Comment thread src/interp.ts Resolved
@Project516
Project516 enabled auto-merge (squash) November 3, 2026 17:10

@project516-review-bot project516-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All points from the last review are settled in the threads.


review-bot, model nvidia/nemotron-3-ultra-550b-a55b:free, verdict approve

@Project516
Project516 disabled auto-merge November 3, 2026 18:06

@project516-review-bot project516-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The PR reworks how compiled code calls cold methods, moving from the run-loop save/restore pattern to nested interpretation on the JS stack. A new callNested function interprets callee methods directly with a direct frame marker, returning the result to t.ret or PUSHED/INTERPRET depending on whether the call can finish in place. The thread budget model shifts from per-method budget to a shared t.slice, affecting both compiled backward branches and in-place interpreted instructions. Performance results show reduced run loop entries and improved wall/user time.


review-bot, model nvidia/nemotron-3.5-lightning:free, verdict approve

@Project516
Project516 enabled auto-merge (squash) November 3, 2026 18:26
@Project516
Project516 merged commit 80b94d8 into master Nov 3, 2026
12 checks passed
@Project516
Project516 deleted the p516/nested-interp branch November 3, 2026 18:33
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