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