Java tutorial
/* * Copyright (C) 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.gapid.models; import static com.google.gapid.proto.service.memory.Memory.PoolNames.Application_VALUE; import static com.google.gapid.util.Paths.commandTree; import static com.google.gapid.util.Paths.lastCommand; import static com.google.gapid.util.Paths.observationsAfter; import static com.google.gapid.widgets.Widgets.submitIfNotDisposed; import static java.util.logging.Level.FINE; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.gapid.models.ApiContext.FilteringContext; import com.google.gapid.proto.service.Service; import com.google.gapid.proto.service.api.API; import com.google.gapid.proto.service.path.Path; import com.google.gapid.rpc.Rpc; import com.google.gapid.rpc.RpcException; import com.google.gapid.rpc.UiCallback; import com.google.gapid.server.Client; import com.google.gapid.util.Events; import com.google.gapid.util.Loadable; import com.google.gapid.util.Messages; import com.google.gapid.util.Paths; import com.google.gapid.util.Ranges; import org.eclipse.swt.widgets.Shell; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.logging.Logger; /** * Model containing the API commands of the capture. */ public class CommandStream extends ModelBase.ForPath<CommandStream.Node, Void, CommandStream.Listener> implements ApiContext.Listener, Capture.Listener { protected static final Logger LOG = Logger.getLogger(CommandStream.class.getName()); private final Capture capture; private final ApiContext context; private final ConstantSets constants; private CommandIndex selection; public CommandStream(Shell shell, Analytics analytics, Client client, Capture capture, ApiContext context, ConstantSets constants) { super(LOG, shell, analytics, client, Listener.class); this.capture = capture; this.context = context; this.constants = constants; capture.addListener(this); context.addListener(this); } @Override public void onCaptureLoadingStart(boolean maintainState) { if (!maintainState) { selection = null; } reset(); } @Override public void onCaptureLoaded(Loadable.Message error) { if (error == null && selection != null) { selection = selection.withCapture(capture.getData()); if (isLoaded()) { resolve(selection.getCommand(), node -> selectCommands(selection.withNode(node), true)); } } } @Override public void onContextsLoaded() { onContextSelected(context.getSelectedContext()); } @Override public void onContextSelected(FilteringContext ctx) { if (selection != null && selection.getNode() != null) { // Clear the node, so the selection will be re-resolved once the context has updated. selection = selection.withNode(null); } load(commandTree(capture.getData(), ctx), false); } @Override protected ListenableFuture<Node> doLoad(Path.Any path) { return Futures.transformAsync(client.get(path), tree -> Futures.transform(client.get(Paths.toAny(tree.getCommandTree().getRoot())), val -> new RootNode(tree.getCommandTree().getRoot().getTree(), val.getCommandTreeNode()))); } public ListenableFuture<Node> load(Node node) { return node.load(shell, () -> Futures .transformAsync(client.get(Paths.toAny(node.getPath(Path.CommandTreeNode.newBuilder()))), v1 -> { Service.CommandTreeNode data = v1.getCommandTreeNode(); if (data.getGroup().isEmpty() && data.hasCommands()) { return Futures.transform(loadCommand(lastCommand(data.getCommands())), cmd -> new NodeData(data, cmd)); } return Futures.immediateFuture(new NodeData(data, null)); })); } public ListenableFuture<API.Command> loadCommand(Path.Command path) { return Futures.transformAsync(client.get(Paths.toAny(path)), value -> Futures .transform(constants.loadConstants(value.getCommand()), ignore -> value.getCommand())); } public void load(Node node, Runnable callback) { ListenableFuture<Node> future = load(node); if (future != null) { Rpc.listen(future, new UiCallback<Node, Node>(shell, LOG) { @Override protected Node onRpcThread(Rpc.Result<Node> result) throws RpcException, ExecutionException { return result.get(); } @Override protected void onUiThread(Node result) { callback.run(); } }); } } @Override protected void fireLoadStartEvent() { listeners.fire().onCommandsLoadingStart(); } @Override protected void fireLoadedEvent() { listeners.fire().onCommandsLoaded(); if (selection != null) { selectCommands(selection, true); } } public CommandIndex getSelectedCommands() { return (selection != null && selection.getNode() != null) ? selection : null; } public void selectCommands(CommandIndex index, boolean force) { if (!force && Objects.equal(selection, index)) { return; } else if (!isLoaded()) { this.selection = index; return; } RootNode root = (RootNode) getData(); if (index.getNode() == null) { resolve(index.getCommand(), node -> selectCommands(index.withNode(node), force)); } else if (!index.getNode().getTree().equals(root.tree)) { // TODO throw new UnsupportedOperationException("This is not yet supported, needs API clarification"); } else { selection = index; listeners.fire().onCommandsSelected(selection); } } private void resolve(Path.Command command, Consumer<Path.CommandTreeNode> cb) { Rpc.listen(client.get(commandTree(((RootNode) getData()).tree, command)), new UiCallback<Service.Value, Path.CommandTreeNode>(shell, LOG) { @Override protected Path.CommandTreeNode onRpcThread(Rpc.Result<Service.Value> result) throws RpcException, ExecutionException { Service.Value value = result.get(); LOG.log(FINE, "Resolved selection to {0}", value); return value.getPath().getCommandTreeNode(); } @Override protected void onUiThread(Path.CommandTreeNode result) { cb.accept(result); } }); } public ListenableFuture<Observation[]> getObservations(CommandIndex index) { return Futures.transform(client.get(observationsAfter(index, Application_VALUE)), v -> { Service.Memory mem = v.getMemory(); Observation[] obs = new Observation[mem.getReadsCount() + mem.getWritesCount()]; int idx = 0; for (Service.MemoryRange read : mem.getReadsList()) { obs[idx++] = new Observation(index, true, read); } for (Service.MemoryRange write : mem.getWritesList()) { obs[idx++] = new Observation(index, false, write); } return obs; }); } /** * Read or write memory observation at a specific command. */ public static class Observation { public static final Observation NULL_OBSERVATION = new Observation(null, false, null) { @Override public String toString() { return Messages.SELECT_OBSERVATION; } @Override public boolean contains(long address) { return false; } }; private final CommandIndex index; private final boolean read; private final Service.MemoryRange range; public Observation(CommandIndex index, boolean read, Service.MemoryRange range) { this.index = index; this.read = read; this.range = range; } public Path.Memory getPath() { return Paths.memoryAfter(index, Application_VALUE, range).getMemory(); } public boolean contains(long address) { return Ranges.contains(range, address); } @Override public String toString() { long base = range.getBase(), count = range.getSize(); return (read ? "Read " : "Write ") + count + " byte" + (count == 1 ? "" : "s") + String.format(" at 0x%016x", base); } } /** * An index into the command stream, representing a specific "point in time" in the trace. */ public static class CommandIndex implements Comparable<CommandIndex> { private final Path.Command command; private final Path.CommandTreeNode node; private final boolean group; private CommandIndex(Path.Command command, Path.CommandTreeNode node, boolean group) { this.command = command; this.node = node; this.group = group; } /** * Create an index pointing to the given command and node. */ public static CommandIndex forNode(Path.Command command, Path.CommandTreeNode node) { return new CommandIndex(command, node, false); } /** * Create an index pointing to the given command, without knowing the tree node. * The tree nodes is then resolved when it is needed. */ public static CommandIndex forCommand(Path.Command command) { return new CommandIndex(command, null, false); } /** * Same as {@link #forCommand}, except that group selection is to be preferred when * resolving to a tree node. */ public static CommandIndex forGroup(Path.Command command) { return new CommandIndex(command, null, true); } public CommandIndex withNode(Path.CommandTreeNode newNode) { return new CommandIndex(command, newNode, group); } public CommandIndex withCapture(Path.Capture capture) { return new CommandIndex(command.toBuilder().setCapture(capture).build(), null, group); } public Path.Command getCommand() { return command; } public Path.CommandTreeNode getNode() { return node; } public boolean isGroup() { return group; } @Override public String toString() { return command.getIndicesList().toString(); } @Override public int hashCode() { return command.getIndicesList().hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof CommandIndex)) { return false; } return command.getIndicesList().equals(((CommandIndex) obj).command.getIndicesList()); } @Override public int compareTo(CommandIndex o) { return Paths.compare(command, o.command); } } public static class Node { private final Node parent; private final int index; private Node[] children; private Service.CommandTreeNode data; private API.Command command; private ListenableFuture<Node> loadFuture; public Node(Service.CommandTreeNode data) { this(null, 0); this.data = data; } public Node(Node parent, int index) { this.parent = parent; this.index = index; } public Node getParent() { return parent; } public int getChildCount() { return (data == null) ? 0 : (int) data.getNumChildren(); } public Node getChild(int child) { return getOrCreateChildren()[child]; } public Node[] getChildren() { return getOrCreateChildren().clone(); } private Node[] getOrCreateChildren() { if (children == null) { Preconditions.checkState(data != null, "Querying children before loaded"); children = new Node[(int) data.getNumChildren()]; for (int i = 0; i < children.length; i++) { children[i] = new Node(this, i); } } return children; } public boolean isLastChild() { return parent == null || (parent.getChildCount() - 1 == index); } public Service.CommandTreeNode getData() { return data; } public API.Command getCommand() { return command; } public Path.CommandTreeNode.Builder getPath(Path.CommandTreeNode.Builder path) { return parent.getPath(path).addIndices(index); } public CommandIndex getIndex() { return (data == null) ? null : CommandIndex.forNode(data.getRepresentation(), getPath(Path.CommandTreeNode.newBuilder()).build()); } public ListenableFuture<Node> load(Shell shell, Supplier<ListenableFuture<NodeData>> loader) { if (data != null) { // Already loaded. return null; } else if (loadFuture != null && !loadFuture.isCancelled()) { return loadFuture; } return loadFuture = Futures.transformAsync(loader.get(), newData -> submitIfNotDisposed(shell, () -> { data = newData.data; command = newData.command; loadFuture = null; // Don't hang on to listeners. return Node.this; })); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof Node)) { return false; } Node n = (Node) obj; return index == n.index && parent.equals(n.parent); } @Override public int hashCode() { return parent.hashCode() * 31 + index; } @Override public String toString() { return parent + "/" + index + (data == null ? "" : " " + data.getGroup() + data.getCommands().getToList()); } } private static class RootNode extends Node { public final Path.ID tree; public RootNode(Path.ID tree, Service.CommandTreeNode data) { super(data); this.tree = tree; } @Override public Path.CommandTreeNode.Builder getPath(Path.CommandTreeNode.Builder path) { return path.setTree(tree); } @Override public String toString() { return "Root"; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof RootNode)) { return false; } return tree.equals(((RootNode) obj).tree); } @Override public int hashCode() { return tree.hashCode(); } } private static class NodeData { public final Service.CommandTreeNode data; public final API.Command command; public NodeData(Service.CommandTreeNode data, API.Command command) { this.data = data; this.command = command; } } public interface Listener extends Events.Listener { /** * Event indicating that the tree root has changed and is being loaded. */ public default void onCommandsLoadingStart() { /* emtpy */ } /** * Event indicating that the tree root has finished loading. */ public default void onCommandsLoaded() { /* empty */ } /** * Event indicating that the currently selected command range has changed. */ @SuppressWarnings("unused") public default void onCommandsSelected(CommandIndex selection) { /* empty */ } } }