Devices loading slowly since new update UEFN

Title

Selecting ANY Verse device freezes/crawls the editor for 20s–3min (native devices instant) — worse than FORT-1024634

Body

Build: ++Fortnite+Release-41.30-CL-55979764
Platform: Windows 11, 64GB RAM (not hardware-bound — a single core pegs at 100%, editor “Not Responding”)
Project: team project under Unreal Revision Control, ~7,200 content files, ~1,780 placed actors

Repro

  1. Open our project (happy to share project name/ID privately with staff).
  2. Click any Verse device in the Outliner.
  3. The Details panel takes 20 seconds to 3 minutes to populate. During this the game thread crawls at ~4fps and nothing is written to the log — no load errors, no warnings; after it releases, only routine EOS heartbeats.

Measured selection times (same session, same map)

Actor clicked Time
StaticMeshActor <1s
Trigger device (native) <0.5s
Movement Modulator (native) ~1s
Verse device A (small class, 1 device ref + 1 anim ref) ~3 minutes
Verse device B (~10 @editable refs) ~2.5 minutes
Verse device C (freshly placed this week, ~5 refs) ~1 minute
Verse device D (old, few refs) ~20–30s

Time does NOT correlate with the device’s @editable picker count (device A has 2 refs and is the slowest), so this looks broader than FORT-1024634’s picker scaling.

What we ruled out (each tested)

  • Revision control: provider disconnected → unchanged
  • VS Code / Verse LSP: VS Code fully closed → unchanged
  • The “Branch History” tab: closed → unchanged
  • Antivirus: AV processes at 0.00 CPU during a live-captured freeze; write latency into the project folder is ~4ms
  • Broken asset references: we repaired every missing/corrupt package (editor log is clean of LoadErrors during the slow clicks)
  • Hardware: single-threaded; one core pegged, editor Not Responding while frames tick slowly

Additional signal

  • All Verse devices in the project are affected; the same symptom exists in every project we duplicated from this family, and every teammate reproduces it on their own machines — it’s project-data-dependent, not machine-dependent.
  • Old placed Verse devices carry very large nested __verse_0x… sub-object trees (we counted up to 221 nested exports on one placed device, ~2,000 across 42 Verse actors), including sub-objects of Verse classes that no longer exist (“CreateExport: Failed to load Outer for resource ‘_verse…’” spam at map load). Possibly related.

Logs from multiple sessions available (including one where the freeze was captured live with CPU sampling). Related tickets: FORT-1024634 (backlogged), FORT-1121263 (log collection thread).


We found it, fixed our project without an engine change, and the rule is simple enough that other teams can apply it today. Posting the full derivation in case it helps you locate the underlying issue.

### The rule

**An @editable field on a placed Verse device that holds a reference to *another Verse device* is what costs the time.** Opening that actor's Details panel appears to recursively materialize the referenced device's Verse class graph, and each Verse class touched re-runs the `/CRD_*/VerseDeviceWrapperClassMap` sweep described in our previous update (~3,177 built-in GameFeature plugins, loaded then immediately discarded, so it never amortizes).

The cost therefore scales with **what the wire points at**, not with how many wires exist. That single fact explains every number we could not previously reconcile:

| Device | Verse-device wires | Details open |
|---|---|---|
| Backflip NPC (1 wire → our central jump manager) | 1 | **3–4 min** |
| Trampolines Manager (wires → jump + persistence + others) | 5 | **~30 min** |
| Height Trophy Manager | 5 | **6.5 min** |
| Fart Device (21 wires, but all → plain audio/trigger actors) | 0 Verse-device | ~12 s |
| VBucksShop2 (186 baked properties, **176 orphaned**) | 0 live | **3 s** |
| FatVisual Manager (1 wire, but **stale/dangling**) | 0 live | **0.06 s** |

### The experiment that isolated it

Same session, same map, back-to-back selections of the same device class:

1. Freshly spawned wrapper with **no Verse class bound** → **2 s**
2. Fresh copy placed by hand from the Content Browser, class bound, **all ref slots empty** → **1 s**
3. **Duplicate of the old actor** (carrying its baked wires) → grinds for minutes, exactly like the original
4. The original old actor → minutes

So the cost is carried by the actor's serialized reference values and travels with duplication. Class, map, session and machine are all constant across those four.

### The fix (project-side, no re-placement needed)

Removing `@editable` from a device-reference field **orphans** its baked value, and an orphaned value is never resolved when the panel opens — so **the existing placed actor becomes fast in place**. We discovered this by accident: one manager we de-pickered but did *not* re-place went from ~25 s to instant. (VBucksShop2 above was the same evidence all along: 176 of its 186 baked properties are orphans from long-removed fields, and it was always one of the fastest devices in the level.)

We converted every cross-device link in the project to Verse tags instead:
verse
# target device: tag class + a VerseTagMarkupComponent stamped on the PLACED actor
MyManager_Tag := class(tag){}

# consumer: no @editable, bound at runtime
var Manager : My_Manager = My_Manager{}
OnBegin<override>()<suspends> : void =
    loop:                                   # retry: stamped tags can register late
        for (Obj : FindCreativeObjectsWithTag(MyManager_Tag), D := My_Manager[Obj]):
            set Manager = D
        ...

Scale of the change: 50 device classes, ~150 references. Every Verse-device Details panel in the project now opens in about a second, in a cold session, with no warm-up. Gameplay is unchanged and shipped.

Note we deliberately kept @editable pickers for native device references (triggers, audio players, volumes, buttons, modulators). Those are cheap — the Fart device above holds 21 of them and opens in ~12 s — which is further evidence that the cost comes specifically from resolving a Verse class graph.

What we would ask for

  1. Cache the wrapper-class-map resolution (or resolve device-ref rows lazily / on expand). Today every selection re-loads thousands of plugin packages and discards them, so nothing is ever amortized within a session.
  2. If that is expensive to change, surfacing the cost would help a lot: right now the editor is silently single-core-pegged for minutes with zero log output under 19 verbose categories, which is why this took us days to characterize rather than minutes.
  3. Trace.Start being compiled out of the UEFN editor is what blocked us from handing you callstacks. If there is a diagnostic build or console command that captures them, we are happy to run it and attach the trace.

For other teams hitting this

Keep Verse-device reference slots empty and wire cross-device links by tag. The wires are only cheap while your Verse codebase is small — ours became unusable as it grew (121 files / 358 classes), which is why this looks like it “appeared” rather than “was always there”. Four gotchas we hit while converting:

  • Run tag discovery first in OnBegin — anything that captures a reference at construction time, or subscribes to a manager’s event immediately, otherwise binds a default-constructed device and silently does nothing.
  • Any HaveX : logic = true style flag meaning “a Details wire exists” must be flipped to false, or it short-circuits discovery.
  • A MyTag : tag = X_Tag{} field on the Verse class is not enough; the tag must be stamped on the placed actor (VerseTagMarkupComponent).
  • Revision-control syncs of level actors can silently drop those stamps — re-verify them after resolving actor conflicts.