Skip to content

[Bug]: Every command freezes Revit ~10 s in long sessions — per-click IronPython engine churn (cache misses after Reload) triggers blocking gen2 GCs #3472

Description

@cucuradu

✈ Pre-Flight checks

  • I don't have SentinelOne antivirus installed
  • I have searched in the issues (open and closed) but couldn't find a similar issue (closest is [Bug]: Telemetry config fields exhibit progressive escape-doubling, growing to 8MB+ each over reload cycles #3334, which is a different config bug we also observed on this install)
  • I have searched in the pyRevit Forum for similar issues
  • I already followed the installation troubleshooting guide thoroughly
  • I am using the latest pyRevit Version — reproduced on 6.4.0.26100+0515; not yet re-tested on 6.5.3, but the code path described below is unchanged on current master (ScriptEngineManager.cs, ScriptEngines.cs, IronPythonEngine.cs)

🐞 Describe the bug

In a long Revit 2026 session, every pyRevit command freezes the Revit UI for ~8–13 seconds — including a no-op IronPython script. The freeze is not the script: instrumentation shows the script body completes in ~1.3 s (or milliseconds for a no-op), while the Revit journal's API External Command Time reports 8.5–13.2 s for the same click. A built-in Revit command (Thin Lines) executed in 6 ms in the same session, so the tax is specific to the pyRevit command pipeline.

Measuring the .NET runtime across a single button click (via GC.GetTotalPauseDuration() / GC.CollectionCount() from another add-in in the same process):

before: gen0=12030 gen1=2249 gen2=158 pause_ms=469945 heap_mb=9217
after:  gen0=12062 gen1=2270 gen2=160 pause_ms=479607 heap_mb=9263

One click = +9.7 s of GC pause time and +2 gen2 (full, blocking) collections on a ~9.2 GB managed heap. The freeze is the garbage collector, and each pyRevit command is what triggers it. Session-wide symptoms match: even ribbon tooltips take 0.3–3.2 s (Jrn.Tooltip … ElapsedTime) between commands.

Why every command triggers full GCs: every click executes on a brand-new IronPython engine. A script-level marker (sys._counter, which survives inside a cached engine) reset to 1 on every click, and the journal shows 300–450 MB of allocation churn per command. A fresh engine per click (assembly-bound scopes, recompiled pyrevit package imports, etc.) generates enough garbage to force gen2 collections, and on a multi-gigabyte heap each one blocks the UI thread for several seconds. The effect compounds as the session heap grows, which is why the same buttons feel fine in a fresh session and take ~10 s hours later.

Suspected mechanism (from reading master): this session had used pyRevit → Reload. After a reload on .NET 8 / Revit 2026, the runtime types live in a new AssemblyLoadContext, so in ScriptEngineManager.GetCachedEngine<T>() the cast of the previously cached engine object throws InvalidCastException and returns null — the cache misses forever after a reload:

  • GetEngine<T> → cache miss → new engine created every click;
  • SetCachedEngine<T> → its inner GetCachedEngine also cast-misses → the stale engine is replaced in the dict without Shutdown() (CleanupBuiltins/CleanupStreams never run on it).

The engines dictionary itself is alive and correctly keyed ({SessionUUID}:IronPython:{Extension} — verified via AppDomain.CurrentDomain.GetData("PYREVITCachedEngines"), 3 entries), it just can never be hit across an ALC boundary.

⌨ Error/Debug Message

No error is raised. Journal excerpts for consecutive clicks of the same extension button
(a launcher whose script body completes in ~1.3 s):

'  10.238982   1:<<<API External Command Time
'  10.110983   1:<<<API External Command Time
'  10.160997   1:<<<API External Command Time

Same button with its script replaced by a no-op (write one line to a temp file):

'  8.487061    1:<<<API External Command Time

Built-in Revit command (Thin Lines), same session, same minute: 6 ms.

♻️ To Reproduce

  1. Revit 2026, pyRevit 6.4.0, any IronPython extension button (a no-op script.py is enough).
  2. Work a long session so the process' managed heap grows to several GB; use pyRevit → Reload at least once.
  3. Click any pyRevit button and watch the UI freeze for many seconds; check API External Command Time in the journal, or GC.GetTotalPauseDuration() before/after the click.
  4. In-engine marker (import sys; sys._n = getattr(sys, "_n", 0) + 1, logged to a file) shows the engine is fresh on every click.

⏲️ Expected behavior

  • The cached engine is reused across clicks of the same extension (as designed), including after a Reload — or the cache is keyed/stored in a way that survives the new AssemblyLoadContext.
  • A stale engine that is dropped from the cache gets Shutdown() before being replaced.
  • A pyRevit command on a warm engine should cost milliseconds, not two full GC cycles.

🖥️ Hardware and Software Setup

pyRevit version: 6.4.0.26100+0515 (deployed from image), IronPython engine 342, rocket mode on, telemetry off
pyRevit environment: single clone "master", one third-party extension + pyRevitCore/pyRevitTools
Revit: 2026.4 (26.4.0.32, build 20251103_1515(x64)), .NET 8
OS: Windows 11 Pro 10.0.26200
Hardware: 32 GB RAM, local SSD install (no network paths, no SentinelOne)

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions