aboutsummaryrefslogtreecommitdiff
path: root/traceviewer/src/main/java/io/perfmark/traceviewer/TraceEventViewer.java
blob: e37b9b4aa7e8e36f1c70a71ee81bd6f04b8714fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
 * Copyright 2019 Google LLC
 *
 * 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 io.perfmark.traceviewer;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.perfmark.tracewriter.TraceEventWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
 * This class converts from the Trace Event json data into a full HTML page. It includes the trace
 * viewer from Catapult, the Chromium trace UI.
 *
 * <p>This class is separate from {@link TraceEventWriter}, because it includes a fairly large HTML
 * chunk, and brings in a differently licenced piece of code.
 *
 * <p>This code is <strong>NOT</strong> API stable, and may be removed in the future, or changed
 * without notice.
 *
 * @since 0.17.0
 */
public final class TraceEventViewer {
  private static final Logger logger = Logger.getLogger(TraceEventViewer.class.getName());

  // Copied from trace2html.html in the Catapult tracing code.
  private static final String INLINE_TRACE_DATA =
      ""
          + "    const traces = [];\n"
          + "    const viewerDataScripts = Polymer.dom(document).querySelectorAll(\n"
          + "        '#viewer-data');\n"
          + "    for (let i = 0; i < viewerDataScripts.length; i++) {\n"
          + "      let text = Polymer.dom(viewerDataScripts[i]).textContent;\n"
          + "      // Trim leading newlines off the text. They happen during writing.\n"
          + "      while (text[0] === '\\n') {\n"
          + "        text = text.substring(1);\n"
          + "      }\n"
          + "      onResult(tr.b.Base64.atob(text));\n"
          + "      viewer.updateDocumentFavicon();\n"
          + "      viewer.globalMode = true;\n"
          + "      viewer.viewTitle = document.title;\n"
          + "      break;\n"
          + "    }\n";

  /**
   * A convenience function around {@link #writeTraceHtml(Writer)}. This writes the trace data to a
   * temporary file and logs the output location.
   *
   * @return the Path of the written file.
   * @throws IOException if it can't write to the destination.
   */
  @CanIgnoreReturnValue
  public static Path writeTraceHtml() throws IOException {
    Path path = Files.createTempFile("perfmark-trace-", ".html");
    try (OutputStream os = Files.newOutputStream(path, TRUNCATE_EXISTING);
        Writer w = new OutputStreamWriter(os, UTF_8)) {
      writeTraceHtml(w);
    }
    logger.log(Level.INFO, "Wrote PerfMark Trace file://{0}", new Object[] {path.toAbsolutePath()});
    return path;
  }

  /**
   * Writes all available trace data as a single HTML file into the given writer.
   *
   * @param writer The destination to write all HTML to.
   * @throws IOException if it can't write to the writer.
   */
  public static void writeTraceHtml(Writer writer) throws IOException {
    InputStream indexStream =
        TraceEventViewer.class.getResourceAsStream("third_party/catapult/index.html");
    if (indexStream == null) {
      throw new IOException("unable to find index.html");
    }
    String index = readAll(indexStream);

    InputStream webComponentsStream =
        TraceEventViewer.class.getResourceAsStream("third_party/polymer/webcomponents.min.js");
    if (webComponentsStream == null) {
      throw new IOException("unable to find webcomponents.min.js");
    }
    String webComponents = readAll(webComponentsStream);

    InputStream traceViewerStream =
        TraceEventViewer.class.getResourceAsStream("third_party/catapult/trace_viewer_full.html");
    if (traceViewerStream == null) {
      throw new IOException("unable to find trace_viewer_full.html");
    }
    String traceViewer = trimTraceViewer(readAll(traceViewerStream));

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (OutputStreamWriter w = new OutputStreamWriter(baos, UTF_8)) {
      TraceEventWriter.writeTraceEvents(w);
    }
    byte[] traceData64 = Base64.getEncoder().encode(baos.toByteArray());

    String indexWithWebComponents = replaceIndexWebComponents(index, webComponents);

    String indexWithTraceViewer =
        replaceIndexTraceImport(
            indexWithWebComponents, traceViewer, new String(traceData64, UTF_8));

    String fullIndex = replaceIndexTraceData(indexWithTraceViewer, INLINE_TRACE_DATA);
    writer.write(fullIndex);
    writer.flush();
  }

  /**
   * Replaces the normal {@code <link>} tag in index.html with a custom replacement, and optionally
   * the inlined Trace data as a base64 script. This is because the trace2html.html file imports the
   * data as a top level text/plain script.
   */
  private static String replaceIndexTraceImport(
      String index, String replacement, @Nullable String inlineTraceData64) {
    int start = index.indexOf("IO_PERFMARK_TRACE_IMPORT");
    if (start == -1) {
      throw new IllegalArgumentException("index doesn't contain IO_PERFMARK_TRACE_IMPORT");
    }
    int line0pos = index.lastIndexOf('\n', start);
    assert line0pos != -1;
    int line1pos = index.indexOf('\n', line0pos + 1);
    assert line1pos != -1;
    int line2pos = index.indexOf('\n', line1pos + 1);
    assert line2pos != -1;
    int line3pos = index.indexOf('\n', line2pos + 1);
    assert line3pos != -1;
    String inlineTraceData = "";
    if (inlineTraceData64 != null) {
      inlineTraceData =
          "\n<script id=\"viewer-data\" type=\"text/plain\">" + inlineTraceData64 + "</script>";
    }
    return index.substring(0, line0pos + 1)
        + replacement
        + inlineTraceData
        + index.substring(line3pos);
  }

  private static String replaceIndexWebComponents(String index, String replacement) {
    int start = index.indexOf("IO_PERFMARK_WEBCOMPONENTS");
    if (start == -1) {
      throw new IllegalArgumentException("index doesn't contain IO_PERFMARK_WEBCOMPONENTS");
    }
    int line0pos = index.lastIndexOf('\n', start);
    assert line0pos != -1;
    int line1pos = index.indexOf('\n', line0pos + 1);
    assert line1pos != -1;

    return index.substring(0, line0pos + 1) + replacement + index.substring(line1pos);
  }

  private static String replaceIndexTraceData(String index, String replacement) {
    int start = index.indexOf("IO_PERFMARK_TRACE_URL");
    if (start == -1) {
      throw new IllegalArgumentException("index doesn't contain IO_PERFMARK_TRACE_URL");
    }
    int line0pos = index.lastIndexOf('\n', start);
    assert line0pos != -1;
    int line1pos = index.indexOf('\n', line0pos + 1);
    assert line1pos != -1;
    int line2pos = index.indexOf('\n', line1pos + 1);
    assert line2pos != -1;
    int line3pos = index.indexOf('\n', line2pos + 1);
    assert line3pos != -1;
    return index.substring(0, line0pos + 1) + replacement + index.substring(line3pos);
  }

  private static String trimTraceViewer(String traceViewer) {
    int startpos = traceViewer.indexOf("<template");
    int lastpos = traceViewer.lastIndexOf("</head>");
    return traceViewer.substring(startpos, lastpos);
  }

  private static String readAll(InputStream stream) throws IOException {
    int available = stream.available();
    byte[] data;
    if (available > 0) {
      data = new byte[available + 1];
    } else {
      data = new byte[4096];
    }
    int pos = 0;
    while (true) {
      int read = stream.read(data, pos, data.length - pos);
      if (read == -1) {
        break;
      } else {
        pos += read;
      }
      if (pos == data.length) {
        data = Arrays.copyOf(data, data.length + (data.length >> 2));
      }
    }
    return new String(data, 0, pos, UTF_8);
  }

  private TraceEventViewer() {
    throw new AssertionError("nope");
  }
}