@Override
	public CompletableFuture<Void> readRegistersAsync(TracePlatform platform, TraceThread thread,
			int frame, Set<Register> registers) {
		MatchedMethod readRegs =
			matches.getBest(ReadRegsMatcher.class, null, ActionName.REFRESH, ReadRegsMatcher.ALL);
		if (readRegs == null) {
			return AsyncUtils.nil();
		}
		if (!(thread instanceof TraceObjectThread tot)) {
			Msg.error(this, "Non-object trace with TraceRmi!");
			return AsyncUtils.nil();
		}
		TraceObject container = tot.getObject().findRegisterContainer(frame);
		if (container == null) {
			Msg.error(this,
				"Cannot find register container for thread,frame: " + thread + "," + frame);
			return AsyncUtils.nil();
		}
		RemoteParameter paramContainer = readRegs.params.get("container");
		if (paramContainer != null) {
			return requestCaches.readRegs(container, readRegs.method, Map.of(
				paramContainer.name(), container));
		}
		Set<String> keys = new HashSet<>();
		for (Register r : registers) {
			String lower = r.getName().toLowerCase();
			keys.add(lower);
			keys.add("[" + lower + "]");
		}
		Set<TraceObject> regs = container
				.findSuccessorsInterface(Lifespan.at(getSnap()), TraceObjectRegister.class,
					true)
				.filter(p -> keys.contains(p.getLastEntry().getEntryKey().toLowerCase()))
				.map(r -> r.getDestination(null))
				.collect(Collectors.toSet());
		RemoteParameter paramRegister = readRegs.params.get("register");
		if (paramRegister != null) {
			AsyncFence fence = new AsyncFence();
			regs.stream().forEach(r -> {
				fence.include(requestCaches.readRegs(r, readRegs.method, Map.of(
					paramRegister.name(), r)));
			});
			return fence.ready();
		}
		throw new AssertionError();
	}
NOTE: Don't read in parallel, lest we overload the connection. This does queue them all
up in a CF chain, though. Still, a request doesn't go out until the preceding one
completes.
Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java