1 /* 2 * RequestLogVisualization.java 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 package org.rstudio.studio.client.application.ui; 16 17 import com.google.gwt.aria.client.Roles; 18 import com.google.gwt.core.client.Scheduler; 19 import com.google.gwt.core.client.Scheduler.ScheduledCommand; 20 import com.google.gwt.dom.client.Style.Cursor; 21 import com.google.gwt.dom.client.Style.FontWeight; 22 import com.google.gwt.dom.client.Style.Overflow; 23 import com.google.gwt.dom.client.Style.Unit; 24 import com.google.gwt.event.dom.client.ClickEvent; 25 import com.google.gwt.event.dom.client.ClickHandler; 26 import com.google.gwt.event.dom.client.KeyCodes; 27 import com.google.gwt.event.logical.shared.CloseEvent; 28 import com.google.gwt.event.logical.shared.CloseHandler; 29 import com.google.gwt.event.logical.shared.HasCloseHandlers; 30 import com.google.gwt.event.shared.HandlerRegistration; 31 import com.google.gwt.user.client.Event; 32 import com.google.gwt.user.client.Event.NativePreviewEvent; 33 import com.google.gwt.user.client.Event.NativePreviewHandler; 34 import com.google.gwt.user.client.Timer; 35 import com.google.gwt.user.client.ui.*; 36 import org.rstudio.core.client.CsvReader; 37 import org.rstudio.core.client.CsvWriter; 38 import org.rstudio.core.client.command.KeyboardShortcut; 39 import org.rstudio.core.client.jsonrpc.RequestLog; 40 import org.rstudio.core.client.jsonrpc.RequestLogEntry; 41 import org.rstudio.core.client.jsonrpc.RequestLogEntry.ResponseType; 42 import org.rstudio.core.client.widget.ModalDialog; 43 import org.rstudio.core.client.widget.OperationWithInput; 44 import org.rstudio.core.client.widget.ScrollPanelWithClick; 45 46 import java.util.ArrayList; 47 import java.util.Iterator; 48 49 public class RequestLogVisualization extends Composite 50 implements HasCloseHandlers<RequestLogVisualization>, NativePreviewHandler 51 { 52 private class TextBoxDialog extends ModalDialog<String> 53 { TextBoxDialog(String caption, String initialValue, OperationWithInput<String> operation)54 private TextBoxDialog(String caption, 55 String initialValue, 56 OperationWithInput<String> operation) 57 { 58 super(caption, Roles.getDialogRole(), operation); 59 textArea_ = new TextArea(); 60 textArea_.setSize("400px", "300px"); 61 textArea_.setText(initialValue); 62 } 63 64 @Override collectInput()65 protected String collectInput() 66 { 67 return textArea_.getText(); 68 } 69 70 @Override validate(String input)71 protected boolean validate(String input) 72 { 73 return true; 74 } 75 76 @Override createMainWidget()77 protected Widget createMainWidget() 78 { 79 return textArea_; 80 } 81 82 private final TextArea textArea_; 83 } 84 RequestLogVisualization()85 public RequestLogVisualization() 86 { 87 overviewPanel_ = new LayoutPanel(); 88 overviewPanel_.getElement().getStyle().setProperty("borderRight", 89 "2px dashed #888"); 90 scrollPanel_ = new ScrollPanelWithClick(overviewPanel_); 91 scrollPanel_.setSize("100%", "100%"); 92 scrollPanel_.addClickHandler(new ClickHandler() 93 { 94 public void onClick(ClickEvent event) 95 { 96 detail_.setWidget(instructions_); 97 } 98 }); 99 100 101 SplitLayoutPanel outerPanel = new SplitLayoutPanel(); 102 outerPanel.getElement().getStyle().setBackgroundColor("white"); 103 outerPanel.getElement().getStyle().setZIndex(500); 104 outerPanel.getElement().getStyle().setOpacity(0.9); 105 106 detail_ = new SimplePanel(); 107 detail_.getElement().getStyle().setBackgroundColor("#FFE"); 108 109 instructions_ = new HTML(); 110 instructions_.setHTML("<p>Click on a request to see details. Click on the " + 111 "background to show these instructions again.</p>" + 112 "<h4>Available commands:</h4>" + 113 "<ul>" + 114 "<li>Esc: Close</li>" + 115 "<li>P: Play/pause</li>" + 116 "<li>E: Export</li>" + 117 "<li>I: Import</li>" + 118 "<li>+/-: Zoom in/out</li>" + 119 "</ul>"); 120 detail_.setWidget(instructions_); 121 122 outerPanel.addSouth(detail_, 200); 123 outerPanel.add(scrollPanel_); 124 125 initWidget(outerPanel); 126 127 handlerRegistration_ = Event.addNativePreviewHandler(this); 128 129 timer_ = new Timer() { 130 @Override 131 public void run() 132 { 133 refresh(true, false); 134 } 135 }; 136 137 refresh(true, true); 138 } 139 140 @Override onUnload()141 protected void onUnload() 142 { 143 timer_.cancel(); 144 super.onUnload(); 145 } 146 refresh(boolean reloadEntries, boolean scrollToEnd)147 private void refresh(boolean reloadEntries, boolean scrollToEnd) 148 { 149 if (reloadEntries) 150 { 151 entries_ = RequestLog.getEntries(); 152 now_ = System.currentTimeMillis(); 153 } 154 155 overviewPanel_.clear(); 156 157 startTime_ = entries_[0].getRequestTime(); 158 long duration = now_ - startTime_; 159 int totalWidth = (int) (duration * scaleMillisToPixels_); 160 totalHeight_ = entries_.length * BAR_HEIGHT; 161 162 overviewPanel_.setSize(totalWidth + "px", totalHeight_ + "px"); 163 164 for (int i = 0, entriesLength = entries_.length; i < entriesLength; i++) 165 { 166 RequestLogEntry entry = entries_[i]; 167 addEntry(i, entry); 168 } 169 170 if (scrollToEnd) 171 { 172 scrollPanel_.scrollToTop(); 173 scrollPanel_.scrollToRight(); 174 } 175 } 176 177 @Override onLoad()178 protected void onLoad() 179 { 180 super.onLoad(); 181 Scheduler.get().scheduleDeferred(new ScheduledCommand() 182 { 183 public void execute() 184 { 185 scrollPanel_.scrollToTop(); 186 scrollPanel_.scrollToRight(); 187 } 188 }); 189 } 190 addEntry(int i, final RequestLogEntry entry)191 private void addEntry(int i, final RequestLogEntry entry) 192 { 193 int top = totalHeight_ - (i+1) * BAR_HEIGHT; 194 int left = (int) ((entry.getRequestTime() - startTime_) * scaleMillisToPixels_); 195 long endTime = entry.getResponseTime() != null 196 ? entry.getResponseTime() 197 : now_; 198 int right = Math.max(0, (int) ((now_ - endTime) * scaleMillisToPixels_) - 1); 199 200 boolean active = entry.getResponseType() == ResponseType.None; 201 202 HTML html = new HTML(); 203 html.getElement().getStyle().setOverflow(Overflow.VISIBLE); 204 html.getElement().getStyle().setProperty("whiteSpace", "nowrap"); 205 206 String method = entry.getRequestMethodName(); 207 if (method == null) 208 method = entry.getRequestId(); 209 210 html.setText(method + (active ? " (active)" : "")); 211 if (active) 212 html.getElement().getStyle().setFontWeight(FontWeight.BOLD); 213 String color; 214 switch (entry.getResponseType()) 215 { 216 case ResponseType.Error: 217 color = "red"; 218 break; 219 case ResponseType.None: 220 color = "#f99"; 221 break; 222 case ResponseType.Normal: 223 color = "#88f"; 224 break; 225 case ResponseType.Cancelled: 226 color = "#E0E0E0"; 227 break; 228 case ResponseType.Unknown: 229 default: 230 color = "yellow"; 231 break; 232 } 233 html.getElement().getStyle().setBackgroundColor(color); 234 html.getElement().getStyle().setCursor(Cursor.POINTER); 235 236 html.addClickHandler(new ClickHandler() 237 { 238 public void onClick(ClickEvent event) 239 { 240 event.stopPropagation(); 241 detail_.clear(); 242 RequestLogDetail entryDetail = new RequestLogDetail(entry); 243 entryDetail.setSize("100%", "100%"); 244 detail_.setWidget(entryDetail); 245 } 246 }); 247 248 overviewPanel_.add(html); 249 overviewPanel_.setWidgetTopHeight(html, top, Unit.PX, BAR_HEIGHT, Unit.PX); 250 overviewPanel_.setWidgetLeftRight(html, left, Unit.PX, right, Unit.PX); 251 overviewPanel_.getWidgetContainerElement(html).getStyle().setOverflow(Overflow.VISIBLE); 252 } 253 addCloseHandler(CloseHandler<RequestLogVisualization> handler)254 public HandlerRegistration addCloseHandler(CloseHandler<RequestLogVisualization> handler) 255 { 256 return addHandler(handler, CloseEvent.getType()); 257 } 258 onPreviewNativeEvent(NativePreviewEvent event)259 public void onPreviewNativeEvent(NativePreviewEvent event) 260 { 261 if (event.getTypeInt() == Event.ONKEYDOWN) 262 { 263 int keyCode = event.getNativeEvent().getKeyCode(); 264 if (keyCode == KeyCodes.KEY_ESCAPE) 265 { 266 CloseEvent.fire(RequestLogVisualization.this, 267 RequestLogVisualization.this); 268 handlerRegistration_.removeHandler(); 269 } 270 else if (keyCode == 'R' 271 && KeyboardShortcut.getModifierValue(event.getNativeEvent()) == 0) 272 { 273 refresh(true, true); 274 } 275 else if (keyCode == 'P') 276 { 277 if (timerIsRunning_) 278 timer_.cancel(); 279 else 280 { 281 timer_.run(); 282 timer_.scheduleRepeating(PERIOD_MILLIS); 283 } 284 timerIsRunning_ = !timerIsRunning_; 285 } 286 else if (keyCode == 'E') 287 { 288 CsvWriter writer = new CsvWriter(); 289 writer.writeValue(now_ + ""); 290 writer.endLine(); 291 for (RequestLogEntry entry : entries_) 292 entry.toCsv(writer); 293 294 TextBoxDialog dialog = new TextBoxDialog("Export", 295 writer.getValue(), 296 null); 297 dialog.showModal(); 298 } 299 else if (keyCode == 'I') 300 { 301 TextBoxDialog dialog = new TextBoxDialog( 302 "Import", 303 "", 304 new OperationWithInput<String>() 305 { 306 public void execute(String input) 307 { 308 CsvReader reader = new CsvReader(input); 309 ArrayList<RequestLogEntry> entries = new ArrayList<>(); 310 Iterator<String[]> it = reader.iterator(); 311 String now = it.next()[0]; 312 while (it.hasNext()) 313 { 314 String[] line = it.next(); 315 RequestLogEntry entry = 316 RequestLogEntry.fromValues(line); 317 if (entry != null) 318 entries.add(entry); 319 } 320 now_ = Long.parseLong(now); 321 entries_ = entries.toArray(new RequestLogEntry[0]); 322 refresh(false, true); 323 } 324 }); 325 dialog.showModal(); 326 } 327 } 328 else if (event.getTypeInt() == Event.ONKEYPRESS) 329 { 330 if (event.getNativeEvent().getKeyCode() == '+') 331 { 332 scaleMillisToPixels_ *= 2.0; 333 refresh(false, false); 334 } 335 else if (event.getNativeEvent().getKeyCode() == '-') 336 { 337 scaleMillisToPixels_ /= 2.0; 338 refresh(false, false); 339 } 340 } 341 } 342 343 344 private static final int BAR_HEIGHT = 15; 345 private double scaleMillisToPixels_ = 0.02; 346 private long now_; 347 private RequestLogEntry[] entries_; 348 private int totalHeight_; 349 private LayoutPanel overviewPanel_; 350 private long startTime_; 351 private ScrollPanelWithClick scrollPanel_; 352 private HandlerRegistration handlerRegistration_; 353 private Timer timer_; 354 private boolean timerIsRunning_; 355 private static final int PERIOD_MILLIS = 2000; 356 private SimplePanel detail_; 357 private HTML instructions_; 358 } 359