Skip to content

perf: take llmgate's guard fix; report where Judge time goes (HAL-1708) - #69

Open
hallelx2 wants to merge 2 commits into
halleluyaholudele/hal-1371-judgewalk-on-persisted-pagesfrom
halleluyaholudele/hal-1708-engine-llmgate-guard-fix
Open

hallelx2 wants to merge 2 commits into
halleluyaholudele/hal-1371-judgewalk-on-persisted-pagesfrom
halleluyaholudele/hal-1708-engine-llmgate-guard-fix

Conversation

@hallelx2

@hallelx2 hallelx2 commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Why

The 37–50 s query was our own CPU. llmgate's context guard rebuilt its tokenizer on every count (256 ms), once per question, inside the limiter slot. The provider answered in ~0.6 s the whole time.

What

  • Pins llmgate to the fix (perf(typesafe): build the guard's tokenizer once and count long text in parallel (HAL-1708) llmgate#19). Swap to the tag once that merges.
  • Every Judge request reports prepare, connect, first byte and total. The engine and server log it at debug, and warn when local preparation passes 250 ms. navbench and tocdump print the split at the end of a run.
  • Warms the Judge at startup, so the first query doesn't open the connection or build the tokenizer.
  • Evaluation: docs/evaluations/2026-09-28-latency-was-our-cpu.md. It marks the 2026-09-25 latency figures as superseded.

Evidence

Same engine code, only llmgate swapped, first 12 FinanceBench questions, back to back: median 50.5 s → 2.2 s. Still 12/12, with identical requests, tokens and $/q.

Ingest, 21 filings: median per filing 66.1 s → 8.8 s with this change alone. Coverage still 47/47.

Locally: go build ./..., go vet, go test -race green. CI cannot run because the GitHub account is locked on billing (HAL-1712). Not merge-ready until it does.

Stacked on #68. #70 and #71 stack on this.

Closes HAL-1708

Summary by Sourcery

Reduce Judge latency by adopting the llmgate guard fix, warming clients, and exposing request timing breakdowns in services and benchmarks.

New Features:

  • Add Judge request timing instrumentation that separates local preparation, connection, provider first-byte, and total latency.
  • Display aggregated Judge timing summaries in the navigation benchmark and TOC dump tools.
  • Warm the Judge client during application startup to reduce first-request latency.

Bug Fixes:

  • Update llmgate to a version containing the context-guard tokenizer performance fix, substantially reducing query and ingest latency caused by local CPU work.

Enhancements:

  • Warn when local Judge request preparation exceeds 250 ms and log request timing details for diagnosis.
  • Add concurrent-safe Judge statistics recording and coverage for timing summaries and slow-preparation logging.

Documentation:

  • Document the latency investigation and supersede earlier measurements that attributed local preparation time to the provider.

Tests:

  • Add tests verifying slow Judge preparation warnings and the separation of local versus provider timing statistics.

The Judge client rebuilt its tokenizer on every count, 256 ms a time,
once per question, inside the limiter's slot (HAL-1708). Taking the
fix, same engine code, first 12 FinanceBench questions, back to back:

  llmgate v0.5.0      median 50.5 s   12/12   4.2 req   $0.00368
  llmgate 010e07c     median  2.2 s   12/12   4.2 req   $0.00369

The provider did identical work. The 37-to-50-second query was our
own CPU, and nothing reported it: a round-trip number cannot tell
local preparation from the provider.

So every Judge request now reports itself. The engine and server log
each one at debug, and warn when local preparation passes 250 ms,
which is a few milliseconds when healthy. navbench and tocdump print
prepare, first-byte and total percentiles at the end of a run. The
Judge is warmed at startup so the first query does not open the
connection or build the tokenizer.

llmgate is pinned to the fix's commit until its PR merges and is
tagged.
Measured before and after the guard fix, the ingest fan-out and the
retrieval skim, on the FinanceBench corpus: query median 50.5 s to
1.9 s at 36/40 and a lower cost per question, ingest median 66.1 s to
4.1 s per filing at 47/47. Marks the 2026-09-25 latency figures as
superseded.
Copilot AI lite review requested due to automatic review settings September 28, 2026 03:25
@sourcery-ai

sourcery-ai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR upgrades llmgate to the tokenizer-guard fix, warms Judge clients before use, and adds tracing from client preparation through provider response so engine logs and benchmark summaries expose the true latency split. It also adds concurrency-safe benchmark statistics, tests for warning and summary behavior, and documentation showing the resulting query and ingest improvements while superseding prior latency figures.

Sequence diagram for warmed and traced Judge requests

sequenceDiagram
    participant Engine
    participant Judge as TypeSafeJudge
    participant Guard as TokenizerGuard
    participant Provider
    participant Logger

    Engine->>Judge: Warm(ctx)
    Judge->>Guard: initialize tokenizer
    Judge->>Provider: open connection
    Provider-->>Judge: connection ready

    Engine->>Judge: Judge request
    Judge->>Guard: count context
    Guard-->>Judge: preparation complete
    Judge->>Provider: send request
    Provider-->>Judge: first byte and response
    Judge-->>Engine: result
    Judge-->>Logger: OnRequest(RequestTrace)
    alt prepare exceeds 250 ms
        Logger-->>Logger: Warn slow local preparation
    else normal preparation
        Logger-->>Logger: Debug request timing
    end
Loading

File-Level Changes

Change Details Files
Adopt the llmgate tokenizer-guard performance fix and prewarm Judge clients.
  • Pin llmgate to the fix commit.
  • Initialize the Judge connection and tokenizer asynchronously at engine and server startup.
  • Preserve limiter/retry wrapping while adding request tracing hooks.
go.mod
go.sum
cmd/engine/main.go
cmd/server/main.go
Add end-to-end Judge latency instrumentation that distinguishes local preparation from provider time.
  • Log request preparation, connection, first-byte, total, status, token, and guard metrics.
  • Warn when local preparation exceeds 250 ms and otherwise emit request details at debug level.
  • Add coverage proving warnings occur only for slow preparation.
cmd/engine/main.go
cmd/engine/judge_log_test.go
cmd/server/main.go
Collect and report benchmark request timing summaries.
  • Record concurrent request traces safely and print p50, p95, and maximum preparation, first-byte, and total times.
  • Integrate summaries into navbench and tocdump output.
  • Test aggregation, percentile reporting, counters, and empty-run behavior.
internal/judgestats/judgestats.go
internal/judgestats/judgestats_test.go
cmd/navbench/main.go
cmd/tocdump/main.go
Document the corrected latency findings and supersede misleading historical measurements.
  • Add an evaluation attributing the prior latency to repeated tokenizer construction and quantifying the improvement.
  • Record query and ingest performance, accuracy/cost comparisons, instrumentation findings, limitations, and reproduction commands.
  • Mark the earlier latency evaluation as superseded while retaining unaffected recall results.
docs/evaluations/2026-09-28-latency-was-our-cpu.md
docs/evaluations/2026-09-25-query-latency-and-the-embedding-prefilter.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: ab637913-b86f-4530-9e98-df23c0eb6e81

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="cmd/engine/main.go" line_range="458-464" />
<code_context>
 	}
+	// Open the connection and build the guard's tokenizer now, so the
+	// first query after boot does not pay for either.
+	go func() {
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		if err := j.Warm(ctx); err != nil {
+			logger.Warn("judge: warm-up failed; the first request will open its own connection", "err", err)
+		}
</code_context>
<issue_to_address>
**issue (bug_risk):** `Warm` runs asynchronously and the wrapped Judge is returned immediately, so the first request can start before warm-up completes and still pay the connection/tokenizer initialization cost; concurrent initialization also depends on `llmgate` making `Warm` and request preparation safe to run together.

**Triggers:** When a request arrives immediately after engine or server startup.

**Suggested fix:** Run warm-up synchronously before returning the Judge, or expose a readiness barrier that requests await until `Warm` completes.

```suggestion
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := j.Warm(ctx); err != nil {
		logger.Warn("judge: warm-up failed; the first request will open its own connection", "err", err)
	}
```
</issue_to_address>

### Comment 2
<location path="cmd/navbench/main.go" line_range="85-87" />
<code_context>
 	}

 	key := os.Getenv(typesafe.EnvAPIKey)
-	tj, err := typesafe.New(typesafe.Config{APIKey: key})
+	tj, err := typesafe.New(typesafe.Config{APIKey: key, OnRequest: stats.Observe})
 	if err != nil {
 		fmt.Fprintln(os.Stderr, "judge:", err)
</code_context>
<issue_to_address>
**issue (broader_impact):** The benchmark Judge clients register timing hooks but never call `Warm`, so the first navigation question or first document still pays cold connection/tokenizer setup and its timing is included in the run instead of benefiting from the advertised startup warm-up.

**Triggers:** On the first request of every navbench run or the first Judge request of a tocdump run.

**Suggested fix:** Warm these clients before starting the measured run, using a bounded context and handling a warm-up failure explicitly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread cmd/engine/main.go
Comment on lines +458 to +464
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := j.Warm(ctx); err != nil {
logger.Warn("judge: warm-up failed; the first request will open its own connection", "err", err)
}
}()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Warm runs asynchronously and the wrapped Judge is returned immediately, so the first request can start before warm-up completes and still pay the connection/tokenizer initialization cost; concurrent initialization also depends on llmgate making Warm and request preparation safe to run together.

Triggers: When a request arrives immediately after engine or server startup.

Suggested fix: Run warm-up synchronously before returning the Judge, or expose a readiness barrier that requests await until Warm completes.

Suggested change
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := j.Warm(ctx); err != nil {
logger.Warn("judge: warm-up failed; the first request will open its own connection", "err", err)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := j.Warm(ctx); err != nil {
logger.Warn("judge: warm-up failed; the first request will open its own connection", "err", err)
}

Comment thread cmd/navbench/main.go
Comment on lines -85 to 87
tj, err := typesafe.New(typesafe.Config{APIKey: key})
tj, err := typesafe.New(typesafe.Config{APIKey: key, OnRequest: stats.Observe})
if err != nil {
fmt.Fprintln(os.Stderr, "judge:", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (broader_impact): The benchmark Judge clients register timing hooks but never call Warm, so the first navigation question or first document still pays cold connection/tokenizer setup and its timing is included in the run instead of benefiting from the advertised startup warm-up.

Triggers: On the first request of every navbench run or the first Judge request of a tocdump run.

Suggested fix: Warm these clients before starting the measured run, using a bounded context and handling a warm-up failure explicitly.

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.

2 participants