From edf97a68dbdad763180dc8e689acd88227552ae0 Mon Sep 17 00:00:00 2001 From: Pascal Muetschard Date: Fri, 1 Feb 2019 14:11:16 -0800 Subject: [PATCH] Show the server status in the UI. --- .../src/main/com/google/gapid/MainWindow.java | 26 ++- .../main/com/google/gapid/server/Client.java | 9 + .../com/google/gapid/server/GapidClient.java | 2 + .../google/gapid/server/GapidClientGrpc.java | 9 + .../com/google/gapid/util/StatusWatcher.java | 197 ++++++++++++++++++ .../com/google/gapid/views/StatusBar.java | 109 +++++++++- 6 files changed, 342 insertions(+), 10 deletions(-) create mode 100644 gapic/src/main/com/google/gapid/util/StatusWatcher.java diff --git a/gapic/src/main/com/google/gapid/MainWindow.java b/gapic/src/main/com/google/gapid/MainWindow.java index 566b7ccead..50d600cf46 100644 --- a/gapic/src/main/com/google/gapid/MainWindow.java +++ b/gapic/src/main/com/google/gapid/MainWindow.java @@ -43,6 +43,7 @@ import com.google.gapid.util.MacApplication; import com.google.gapid.util.Messages; import com.google.gapid.util.OS; +import com.google.gapid.util.StatusWatcher; import com.google.gapid.util.UpdateWatcher; import com.google.gapid.views.StatusBar; import com.google.gapid.widgets.CopyPaste; @@ -76,7 +77,7 @@ public class MainWindow extends ApplicationWindow { private final Theme theme; private Composite mainArea; private LoadingScreen loadingScreen; - private StatusBar statusBar; + protected StatusBar statusBar; public MainWindow(Settings settings, Theme theme) { super(null); @@ -94,6 +95,7 @@ public void showLoadingMessage(String status) { public void initMainUi(Client client, Models models, Widgets widgets) { Shell shell = getShell(); + showLoadingMessage("Setting up UI..."); initMenus(client, models, widgets); LoadablePanel mainUi = new LoadablePanel( @@ -125,8 +127,16 @@ public void onCaptureLoaded(Message error) { file -> models.capture.loadCapture(new File(file))); } + if (settings.autoCheckForUpdates) { + // Only show the status message if we're actually checking for updates. watchForUpdates only + //schedules a periodic check to see if we should check for updates and if so, checks. + showLoadingMessage("Watching for updates..."); + } watchForUpdates(client, models); + showLoadingMessage("Tracking server status..."); + trackServerStatus(client); + showLoadingMessage("Ready! Please open or capture a trace file."); } @@ -140,6 +150,20 @@ private void watchForUpdates(Client client, Models models) { }); } + private void trackServerStatus(Client client) { + new StatusWatcher(client, new StatusWatcher.Listener() { + @Override + public void onStatus(String status) { + scheduleIfNotDisposed(statusBar, () -> statusBar.setServerStatus(status)); + } + + @Override + public void onHeap(long heap) { + scheduleIfNotDisposed(statusBar, () -> statusBar.setServerHeapSize(heap)); + } + }); + } + @Override protected void configureShell(Shell shell) { shell.setText(Messages.WINDOW_TITLE); diff --git a/gapic/src/main/com/google/gapid/server/Client.java b/gapic/src/main/com/google/gapid/server/Client.java index 032bd2e1ce..6f723f8a27 100644 --- a/gapic/src/main/com/google/gapid/server/Client.java +++ b/gapic/src/main/com/google/gapid/server/Client.java @@ -251,6 +251,15 @@ public ListenableFuture streamLog(Consumer onLogMessage) { return client.streamLog(onLogMessage); } + public ListenableFuture streamStatus( + float memoryS, float statusS, Consumer onStatus) { + LOG.log(FINE, "RPC->streamStatus({}, {})", new Object[] { memoryS, statusS }); + return client.streamStatus(Service.ServerStatusRequest.newBuilder() + .setMemorySnapshotInterval(memoryS) + .setStatusUpdateFrequency(statusS) + .build(), onStatus); + } + public ListenableFuture streamSearch( Service.FindRequest request, Consumer onResult) { LOG.log(FINE, "RPC->find({0})", request); diff --git a/gapic/src/main/com/google/gapid/server/GapidClient.java b/gapic/src/main/com/google/gapid/server/GapidClient.java index a2adc02d87..24215428d0 100644 --- a/gapic/src/main/com/google/gapid/server/GapidClient.java +++ b/gapic/src/main/com/google/gapid/server/GapidClient.java @@ -61,6 +61,8 @@ public ListenableFuture updateSettings( Service.UpdateSettingsRequest request); public ListenableFuture streamLog(Consumer onLogMessage); + public ListenableFuture streamStatus( + Service.ServerStatusRequest request, Consumer onStatus); public ListenableFuture streamSearch( Service.FindRequest request, Consumer onResult); public StreamSender streamTrace( diff --git a/gapic/src/main/com/google/gapid/server/GapidClientGrpc.java b/gapic/src/main/com/google/gapid/server/GapidClientGrpc.java index e97f0ef781..a2668de9a6 100644 --- a/gapic/src/main/com/google/gapid/server/GapidClientGrpc.java +++ b/gapic/src/main/com/google/gapid/server/GapidClientGrpc.java @@ -169,6 +169,15 @@ public ListenableFuture streamLog(Consumer onLogMessage) { return handler.future; } + @Override + public ListenableFuture streamStatus( + Service.ServerStatusRequest request, Consumer onStatus) { + StreamHandler handler = StreamHandler.wrap(onStatus); + stub.status(request, handler); + return handler.future; + } + + @Override public ListenableFuture streamSearch( Service.FindRequest request, Consumer onResult) { diff --git a/gapic/src/main/com/google/gapid/util/StatusWatcher.java b/gapic/src/main/com/google/gapid/util/StatusWatcher.java new file mode 100644 index 0000000000..a1311fe475 --- /dev/null +++ b/gapic/src/main/com/google/gapid/util/StatusWatcher.java @@ -0,0 +1,197 @@ +/* + * 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.util; + +import com.google.common.collect.Maps; +import com.google.gapid.proto.service.Service; +import com.google.gapid.server.Client; + +import java.util.HashMap; +import java.util.LinkedHashMap; + +/** + * Utility class for monitoring the server status. + */ +public class StatusWatcher { + private static final float STATUS_UPDATE_INTERVAL_S = 0.5f; + private static final float MEMORY_UPDATE_INTERVAL_S = 1.0f; + + private final Listener listener; + private final Task root = Task.newRoot(); + private final HashMap tasks = Maps.newHashMap(); + private String shownSummary = ""; + + public StatusWatcher(Client client, Listener listener) { + this.listener = listener; + + client.streamStatus(MEMORY_UPDATE_INTERVAL_S, STATUS_UPDATE_INTERVAL_S, update -> { + switch (update.getResCase()) { + case TASK: + onTaskUpdate(update.getTask()); + break; + case MEMORY: + onMemoryUpdate(update.getMemory()); + break; + default: + // Ignore. + } + }); + } + + private void onTaskUpdate(Service.TaskUpdate update) { + String summary; + synchronized (this) { + switch(update.getStatus()) { + case STARTING: + Task parent = tasks.getOrDefault(update.getParent(), root); + Task child = new Task(update, parent); + tasks.put(update.getId(), child); + parent.addChild(child); + break; + case FINISHED: + tasks.getOrDefault(update.getId(), root).remove(); + break; + case PROGRESS: + tasks.getOrDefault(update.getId(), root).setProgress(update.getCompletePercent()); + break; + case BLOCKED: + tasks.getOrDefault(update.getId(), root).setBlocked(true); + break; + case UNBLOCKED: + tasks.getOrDefault(update.getId(), root).setBlocked(false); + break; + default: + // Ignore. + return; + } + + summary = root.getFirstChild(Task.State.RUNNING).getStatusLabel(); + if (shownSummary.equals(summary)) { + return; + } + shownSummary = summary; + } + + listener.onStatus(summary); + } + + private void onMemoryUpdate(Service.MemoryStatus update) { + listener.onHeap(update.getTotalHeap()); + } + + public static interface Listener { + public void onStatus(String status); + public void onHeap(long heap); + } + + private static class Task { + private final long id; + private final Task parent; + private final String name; + private final LinkedHashMap children = Maps.newLinkedHashMap(); + private State state; + private int progress; + + public Task(long id, Task parent, String name, State state, int progress) { + this.id = id; + this.parent = parent; + this.name = name; + this.state = state; + this.progress = progress; + } + + public Task(Service.TaskUpdate update, Task parent) { + this(update.getId(), parent, update.getName(), getState(update), update.getCompletePercent()); + } + + private static State getState(Service.TaskUpdate update) { + return (update.getBackground()) ? State.BACKGROUND : State.RUNNING; + } + + public static Task newRoot() { + return new Task(-1, null, null, null, 0) { + @Override + public void setBlocked(boolean newVal) { + // Don't do anything. + } + + @Override + public void remove() { + // Don't do anything. + } + + @Override + public String getLabel() { + return ""; + } + }; + } + + public void setBlocked(boolean blocked){ + if (state != State.BACKGROUND) { + state = blocked ? State.BLOCKED: State.RUNNING; + parent.setBlocked(blocked); + } + } + + public void setProgress(int progress) { + this.progress = progress; + } + + public void addChild(Task child) { + children.put(child.id, child); + } + + public void remove() { + parent.children.remove(id); + } + + public Task getFirstChild(State inState) { + for (Task child : children.values()) { + if (child.state == inState) { + return child; + } + } + return this; + } + + public Task getLeftMostDecendant(State inState) { + for (Task child : children.values()) { + if (child.state == inState) { + return child.getLeftMostDecendant(inState); + } + } + return this; + } + + public String getLabel() { + return (progress == 0) ? name : name + "<" + progress + "%>"; + } + + public String getStatusLabel() { + Task leaf = getLeftMostDecendant(State.RUNNING); + if (leaf == this) { + return getLabel(); + } else { + return getLabel() + " ... " + leaf.getLabel(); + } + } + + private static enum State { + BACKGROUND, RUNNING, BLOCKED, + } + } +} diff --git a/gapic/src/main/com/google/gapid/views/StatusBar.java b/gapic/src/main/com/google/gapid/views/StatusBar.java index 7e315e9619..98a2fd5287 100644 --- a/gapic/src/main/com/google/gapid/views/StatusBar.java +++ b/gapic/src/main/com/google/gapid/views/StatusBar.java @@ -15,13 +15,22 @@ */ package com.google.gapid.views; +import static com.google.gapid.widgets.Widgets.createComposite; +import static com.google.gapid.widgets.Widgets.createLabel; +import static com.google.gapid.widgets.Widgets.createLink; +import static com.google.gapid.widgets.Widgets.filling; +import static com.google.gapid.widgets.Widgets.withLayoutData; import static com.google.gapid.widgets.Widgets.withMargin; -import com.google.gapid.widgets.Widgets; - import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; @@ -30,24 +39,31 @@ * Displays status information at the bottom of the main window. */ public class StatusBar extends Composite { - private final Label status; + private final Composite serverStatus; + private final HeapStatus heap; + private final Label server; private final Link notification; - private Runnable onNotificationClick; + private Runnable onNotificationClick = null; public StatusBar(Composite parent) { super(parent, SWT.NONE); setLayout(withMargin(new GridLayout(2, false), 0, 0)); - status = Widgets.createLabel(this, ""); - status.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, true, false)); + serverStatus = withLayoutData( + createComposite(this, filling(new RowLayout(SWT.HORIZONTAL), true, false)), + new GridData(SWT.LEFT, SWT.FILL, true, false)); + createLabel(serverStatus, "Server:"); + heap = new HeapStatus(serverStatus); + withLayoutData(new Label(serverStatus, SWT.SEPARATOR | SWT.VERTICAL), new RowData(SWT.DEFAULT, 1)); + server = createLabel(serverStatus, ""); + serverStatus.setVisible(false); - notification = Widgets.createLink(this, "", (e) -> { + notification = withLayoutData(createLink(this, "", $ -> { if (onNotificationClick != null) { onNotificationClick.run(); } - }); - notification.setLayoutData(new GridData(SWT.RIGHT, SWT.FILL, false, false)); + }), new GridData(SWT.RIGHT, SWT.FILL, false, false)); } /** @@ -61,4 +77,79 @@ public void setNotification(String text, Runnable onClick) { onNotificationClick = onClick; layout(); } + + public void setServerStatus(String text) { + serverStatus.setVisible(true); + server.setText(text); + layout(); + } + + public void setServerHeapSize(long heapSize) { + serverStatus.setVisible(true); + heap.setHeap(heapSize); + layout(); + } + + private static class HeapStatus extends Canvas { + private static final int PADDING = 2; + + private long heap = 0; + private long max = 1; + private String label = ""; + private int maxMeasuredWidth; + + public HeapStatus(Composite parent) { + super(parent, SWT.NONE); + + addListener(SWT.Paint, e -> { + Rectangle ca = getClientArea(); + e.gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); + e.gc.fillRectangle(0, 0, (int)(ca.width * heap / max), ca.height); + + Point ts = e.gc.stringExtent(label); + e.gc.drawText( + label, ca.width - PADDING - ts.x, (ca.height - ts.y) / 2, SWT.DRAW_TRANSPARENT); + }); + } + + public void setHeap(long newHeap) { + heap = newHeap; + max = Math.max(max, newHeap); + label = bytesToHuman(newHeap) + " of " + bytesToHuman(max); + redraw(); + } + + @Override + public Point computeSize(int wHint, int hHint, boolean changed) { + Point result; + if (label.isEmpty()) { + result = new Point(0, 0); + } else { + GC gc = new GC(this); + result = gc.stringExtent(label); + gc.dispose(); + maxMeasuredWidth = result.x = Math.max(maxMeasuredWidth, result.x); + } + + if (wHint != SWT.DEFAULT) { + result.x = wHint; + } else { + result.x += 2 * PADDING; + } + if (hHint != SWT.DEFAULT) { + result.y = hHint; + } + return result; + } + + private static String bytesToHuman(long bytes) { + long mb = bytes >> 20; // The heap is never smaller than 4MB. + if (mb > 1024) { + // Show GBs with a decimal. + return String.format("%.1fGB", mb / 1024.0); + } else { + return mb + "MB"; + } + } + } }