Skip to content

perf: reduce template loop render scope churn#135

Merged
joshcramer merged 2 commits into
mainfrom
perf/template-render-scope-fastpath
Jun 23, 2026
Merged

perf: reduce template loop render scope churn#135
joshcramer merged 2 commits into
mainfrom
perf/template-render-scope-fastpath

Conversation

@larimonious

Copy link
Copy Markdown
Contributor

Summary

  • Reuses a single loop render scope per template {{#for}} instead of allocating a fresh Environment for every row.
  • Builds template data scopes directly from the data map and restores template environments through a shared helper on error paths.
  • Preserves loop metadata, map pair iteration, missing-variable empty rendering, nested loop parent-scope fallback, and partial/template cache behavior.
  • Updates DD-061 to mark PR3 complete, mark this PR4 candidate slice complete, and point the next recommendation at PR5 profiling/fast-path validation.

Benchmark

Local DD-061 benchmark harness, dev-release binaries, wrk 3s, 3 runs, 16 connections, 2 threads. Baseline binary was origin/main at 52554e9; branch binary was this PR.

Benchmark                    main          branch        delta
plaintext route              187389.61     189109.42     +0.9%
small JSON route             177718.21     179263.89     +0.9%
route param + map read       165301.09     163800.24     -0.9%
compute loop route           3597.02       3576.05       -0.6%
template layout + partial    23807.29      24701.15      +3.8%
template 100-row loop        6053.89       6420.77       +6.1%
CLI compute median           95.31ms       95.76ms       +0.5% slower

Raw outputs:

  • /tmp/ntnt-dd061-pr4-baseline-20260623T062459/ntnt-perf-20260623T122518Z.md
  • /tmp/ntnt-dd061-pr4-branch-20260623T062525/ntnt-perf-20260623T122544Z.md

Verification

  • cargo build --profile dev-release
  • cargo test --lib template_cache_
  • cargo test --lib template_loop_scope
  • cargo test --lib template_environment_helper_restores_after_error
  • cargo test --test language_features_tests test_template_partial
  • cargo test --lib
  • cargo test --test language_features_tests --test type_checker_tests --test cli_tests
  • ./target/dev-release/ntnt docs --generate
  • cargo fmt -- --check
  • git diff --check

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces per-row Rc<RefCell<Environment>> allocation inside {{#for}} loops with a single reusable scope that is cleared and rebound at the start of each iteration via bind_template_loop_scope, and consolidates template data scope construction into a new with_parent_and_values constructor.

  • render_template_loop_values now allocates one loop_env per loop and calls env.values.clear() + rebind before each row, yielding a measured +6.1% RPS improvement on the 100-row template benchmark.
  • with_template_environment provides a uniform RAII-style helper that always restores the previous environment, covering both the data-scope and loop-scope paths; tests verify environment restoration on error.
  • The design-doc is updated to mark PR3 and PR4 complete and redirect the next recommendation toward PR5 profiling.

Confidence Score: 5/5

Safe to merge — the scope-reuse refactor is semantically equivalent to the old per-iteration allocation and all changed code paths are covered by new and existing tests.

The core invariant — clearing and rebinding loop_env.values before each row — exactly replicates the old fresh-scope behavior. The parent chain is untouched so nested-loop parent-scope lookups remain correct. Error paths in render_template_loop_values explicitly restore self.environment before returning, matching with_template_environment's own restore guarantee. The @last computation was tightened from usize subtraction to index+1==length, removing the theoretical underflow path. New unit tests cover metadata accuracy, map-pair iteration, nested loops, missing-variable empty rendering, and environment restoration on error.

No files require special attention.

Important Files Changed

Filename Overview
src/interpreter.rs Replaces per-iteration scope allocation with a single reusable loop_env; introduces with_parent_and_values, bind_template_loop_scope, render_template_loop_values, and with_template_environment helpers. Logic is correct: env.values.clear() + rebind is semantically equivalent to the old fresh-scope approach, parent chain is preserved, @last uses safe index+1==length instead of length-1, and error paths always restore self.environment.
design-docs/dd-061-interpreter-performance-roadmap.md Marks all PR3 and PR4 checklist items complete, appends benchmark note, and updates the current recommendation to PR5. Documentation-only change; no logic affected.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["render_template_parts_with_data(parts, data)"] --> B["with_parent_and_values(parent, data.clone())
    → template_env"]
    B --> C["with_template_environment(template_env, ...)
    saves previous, sets self.environment"]
    C --> D["eval_template_parts(parts)"]
    D --> E{"TemplatePart::ForLoop?"}
    E -- No --> F["render other parts"]
    E -- Yes --> G["eval iterable"]
    G --> H{"Value type?"}
    H -- "Array (non-empty)" --> I["render_template_loop_values(var, length, items, body)"]
    H -- "Map (non-empty)" --> J["map pairs as Values → render_template_loop_values"]
    H -- "Array/Map empty" --> K["render empty_body or skip"]
    I --> L["Allocate loop_env once\nself.environment = loop_env"]
    L --> M{"Next item?"}
    M -- Yes --> N["bind_template_loop_scope:\nclear + rebind var, @index, @last…"]
    N --> O["eval_template_parts(body)"]
    O --> P["append to result string"]
    P --> M
    M -- No --> Q["self.environment = previous"]
    Q --> R["return render_result"]
    C --> S["restore previous environment"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["render_template_parts_with_data(parts, data)"] --> B["with_parent_and_values(parent, data.clone())
    → template_env"]
    B --> C["with_template_environment(template_env, ...)
    saves previous, sets self.environment"]
    C --> D["eval_template_parts(parts)"]
    D --> E{"TemplatePart::ForLoop?"}
    E -- No --> F["render other parts"]
    E -- Yes --> G["eval iterable"]
    G --> H{"Value type?"}
    H -- "Array (non-empty)" --> I["render_template_loop_values(var, length, items, body)"]
    H -- "Map (non-empty)" --> J["map pairs as Values → render_template_loop_values"]
    H -- "Array/Map empty" --> K["render empty_body or skip"]
    I --> L["Allocate loop_env once\nself.environment = loop_env"]
    L --> M{"Next item?"}
    M -- Yes --> N["bind_template_loop_scope:\nclear + rebind var, @index, @last…"]
    N --> O["eval_template_parts(body)"]
    O --> P["append to result string"]
    P --> M
    M -- No --> Q["self.environment = previous"]
    Q --> R["return render_result"]
    C --> S["restore previous environment"]
Loading

Reviews (2): Last reviewed commit: "fix: guard template loop last metadata" | Re-trigger Greptile

Comment thread src/interpreter.rs
@joshcramer joshcramer merged commit 5b3bbd8 into main Jun 23, 2026
9 checks passed
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