@Override
	public CompletableFuture<Void> readMemoryAsync(AddressSetView set, TaskMonitor monitor) {
		/**
		 * I still separate into blocks, because I want user to be able to cancel. I don't intend to
		 * warn about the number of requests. They're delivered in serial, and there's a cancel
		 * button that works
		 */

		MatchedMethod readMem =
			matches.getBest(ReadMemMatcher.class, null, ActionName.READ_MEM, ReadMemMatcher.ALL);
		if (readMem == null) {
			return AsyncUtils.nil();
		}
		RemoteParameter paramRange = readMem.params.get("range");
		RemoteParameter paramProcess = readMem.params.get("process");

		final Map<AddressSpace, TraceObject> procsBySpace;
		if (paramProcess != null) {
			procsBySpace = new HashMap<>();
		}
		else {
			procsBySpace = null;
		}

		int total = 0;
		AddressSetView quantized = quantize(set);
		for (AddressRange r : quantized) {
			total += Long.divideUnsigned(r.getLength() + BLOCK_SIZE - 1, BLOCK_SIZE);
		}
		monitor.initialize(total);
		monitor.setMessage("Reading memory");

		/**
		 * 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.
		 */
		return quantized.stream().flatMap(r -> {
			if (r.getAddressSpace().isRegisterSpace()) {
				Msg.warn(this, "Request to read registers via readMemory: " + r + ". Ignoring.");
				return Stream.of();
			}
			return new AddressRangeChunker(r, BLOCK_SIZE).stream();
		}).reduce(AsyncUtils.nil(), (f, blk) -> {
			if (monitor.isCancelled()) {
				return f;
			}
			final Map<String, Object> args;
			if (paramProcess != null) {
				TraceObject process = procsBySpace.computeIfAbsent(blk.getAddressSpace(),
					this::getProcessForSpace);
				if (process == null) {
					return f;
				}
				args = Map.ofEntries(
					Map.entry(paramProcess.name(), process),
					Map.entry(paramRange.name(), blk));
			}
			else {
				args = Map.ofEntries(
					Map.entry(paramRange.name(), blk));
			}

			return f.thenComposeAsync(__ -> {
				if (monitor.isCancelled()) {
					return AsyncUtils.nil();
				}
				monitor.incrementProgress(1);
				return requestCaches.readBlock(blk.getMinAddress(), readMem.method, args);
			}, AsyncUtils.FRAMEWORK_EXECUTOR).exceptionally(e -> {
				Msg.error(this, "Could not read " + blk + ": " + e);
				return null; // Continue looping on errors
			});
		}, (f1, f2) -> {
			throw new AssertionError("Should be sequential");
		});
	}
Search for the most preferred method for a given operation, with respect to a given path

<p>
A given path should be given as a point of reference, usually the current object or the
object from the UI action context. If given, parameters that require a certain
{@link TraceObjectInterface} will seek a suitable schema from that path and require it.
Otherwise, any parameter whose schema includes the interface will be accepted.

@param <T> the matcher class representing the desired operation
@param cls the matcher class representing the desired operation
@param path a path as a point of reference, or null for "any" point of reference.
@param action the required action name for a matching method
@param preferred the list of matchers (signatures) in preferred order
@return the best method, or null
Ghidra/Debug/Debugger-rmi-trace/src/main/java/ghidra/app/plugin/core/debug/service/tracermi/TraceRmiTarget.java