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