Skip to content

Commit

Permalink
Display Perfetto UI from JavaScript. (#478)
Browse files Browse the repository at this point in the history
* Display Perfetto UI from JavaScript.

This fixes the issue where the Perfetto server has to run
on port 9001 and serve traces on the "/" path.

Now, the dashboard serves traces on its local server.

* Addressed reviewer comments.
  • Loading branch information
spetrovic77 authored Jul 24, 2023
1 parent 3d5e307 commit 0be465f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 156 deletions.
23 changes: 13 additions & 10 deletions godeps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ github.com/ServiceWeaver/weaver/internal/status
github.com/ServiceWeaver/weaver/runtime/colors
github.com/ServiceWeaver/weaver/runtime/logging
github.com/ServiceWeaver/weaver/runtime/metrics
github.com/ServiceWeaver/weaver/runtime/perfetto
github.com/ServiceWeaver/weaver/runtime/prometheus
github.com/ServiceWeaver/weaver/runtime/protomsg
github.com/ServiceWeaver/weaver/runtime/protos
Expand All @@ -472,7 +473,6 @@ github.com/ServiceWeaver/weaver/internal/status
io
net
net/http
net/url
os
path/filepath
reflect
Expand Down Expand Up @@ -863,6 +863,18 @@ github.com/ServiceWeaver/weaver/runtime/metrics
sync/atomic
unicode
unicode/utf8
github.com/ServiceWeaver/weaver/runtime/perfetto
crypto/sha256
encoding/binary
encoding/json
github.com/ServiceWeaver/weaver/internal/traceio
github.com/ServiceWeaver/weaver/runtime/protos
github.com/ServiceWeaver/weaver/runtime/traces
go.opentelemetry.io/otel/sdk/trace
go.opentelemetry.io/otel/semconv/v1.4.0
go.opentelemetry.io/otel/trace
strconv
strings
github.com/ServiceWeaver/weaver/runtime/profiling
bytes
errors
Expand Down Expand Up @@ -925,34 +937,25 @@ github.com/ServiceWeaver/weaver/runtime/tool
github.com/ServiceWeaver/weaver/runtime/traces
bytes
context
crypto/sha256
database/sql
encoding/binary
encoding/hex
encoding/json
errors
fmt
github.com/ServiceWeaver/weaver/internal/traceio
github.com/ServiceWeaver/weaver/runtime/protos
github.com/ServiceWeaver/weaver/runtime/retry
go.opentelemetry.io/otel/attribute
go.opentelemetry.io/otel/codes
go.opentelemetry.io/otel/sdk/instrumentation
go.opentelemetry.io/otel/sdk/resource
go.opentelemetry.io/otel/sdk/trace
go.opentelemetry.io/otel/semconv/v1.4.0
go.opentelemetry.io/otel/trace
google.golang.org/protobuf/proto
math
modernc.org/sqlite
modernc.org/sqlite/lib
net
net/http
os
path/filepath
strconv
strings
sync
time
github.com/ServiceWeaver/weaver/runtime/version
fmt
Expand Down
41 changes: 30 additions & 11 deletions internal/status/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"html/template"
"net"
"net/http"
"net/url"
"os"
"sort"
"strings"
Expand All @@ -33,6 +32,7 @@ import (
"github.com/ServiceWeaver/weaver/runtime/codegen"
"github.com/ServiceWeaver/weaver/runtime/logging"
"github.com/ServiceWeaver/weaver/runtime/metrics"
"github.com/ServiceWeaver/weaver/runtime/perfetto"
imetrics "github.com/ServiceWeaver/weaver/runtime/prometheus"
protos "github.com/ServiceWeaver/weaver/runtime/protos"
dtool "github.com/ServiceWeaver/weaver/runtime/tool"
Expand Down Expand Up @@ -72,12 +72,6 @@ var (
//go:embed templates/traces.html
tracesHTML string
tracesTemplate = template.Must(template.New("traces").Funcs(template.FuncMap{
"traceurl": func(traceID string) string {
v := url.Values{}
v.Set("trace_id", traceID)
tracerURL := url.QueryEscape("http://127.0.0.1:9001?" + v.Encode())
return "https://ui.perfetto.dev/#!/?url=" + tracerURL
},
"sub": func(endTime, startTime time.Time) string {
return endTime.Sub(startTime).String()
},
Expand Down Expand Up @@ -139,6 +133,7 @@ Flags:
http.HandleFunc("/deployment", dashboard.handleDeployment)
http.HandleFunc("/metrics", dashboard.handleMetrics)
http.HandleFunc("/traces", dashboard.handleTraces)
http.HandleFunc("/tracefetch", dashboard.handleTraceFetch)
http.Handle("/assets/", http.FileServer(http.FS(assets)))

lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *dashboardHost, *dashboardPort))
Expand All @@ -147,10 +142,6 @@ Flags:
}
url := "http://" + lis.Addr().String()

if traceDB != nil {
go traces.ServePerfetto(ctx, traceDB)
}

fmt.Fprintln(os.Stderr, "Dashboard available at:", url)
go browser.OpenURL(url) //nolint:errcheck // browser open is optional
return http.Serve(lis, nil)
Expand Down Expand Up @@ -386,3 +377,31 @@ func (d *dashboard) handleTraces(w http.ResponseWriter, r *http.Request) {
return
}
}

// handleTraceFetch handles requests to /tracefetch?trace_id=<trace_id>.
func (d *dashboard) handleTraceFetch(w http.ResponseWriter, r *http.Request) {
if d.traceDB == nil {
http.Error(w, "cannot open trace database", http.StatusInternalServerError)
return
}
traceID := r.URL.Query().Get("trace_id")
if traceID == "" {
http.Error(w, fmt.Sprintf("invalid trace id %q", traceID), http.StatusBadRequest)
return
}
spans, err := d.traceDB.FetchSpans(r.Context(), traceID)
if err != nil {
http.Error(w, fmt.Sprintf("cannot fetch spans: %v", err), http.StatusInternalServerError)
return
}
if len(spans) == 0 {
http.Error(w, "no matching spans", http.StatusNotFound)
return
}
data, err := perfetto.EncodeSpans(spans)
if err != nil {
http.Error(w, fmt.Sprintf("cannot encode spans: %v", err), http.StatusInternalServerError)
return
}
w.Write(data) //nolint:errcheck // response write error
}
112 changes: 75 additions & 37 deletions internal/status/templates/traces.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,46 +38,84 @@
<header class="navbar">
<a href="/">{{.Tool}} dashboard</a>
</header>

<script type="text/javascript">
// The code below largely taken from:
// https://perfetto.dev/docs/visualization/deep-linking-to-perfetto-ui
const ORIGIN = 'https://ui.perfetto.dev';

async function fetchAndOpen(traceUrl) {
const resp = await fetch(traceUrl);
const blob = await resp.blob();
const arrayBuffer = await blob.arrayBuffer();
openTrace(arrayBuffer, traceUrl);
}

function openTrace(arrayBuffer, traceId, traceUrl) {
const win = window.open(ORIGIN);
if (!win) {
alert('Popups blocked. Please allow popups in order to be able to' +
'see traces');
return
}
const timer = setInterval(() => win.postMessage('PING', ORIGIN), 50);
const onMessageHandler = (evt) => {
if (evt.data !== 'PONG') return;

// We got a PONG, the UI is ready.
window.clearInterval(timer);
window.removeEventListener('message', onMessageHandler);

const reopenUrl = new URL(location.href);
reopenUrl.hash = `#reopen=${traceUrl}`;
win.postMessage({
perfetto: {
buffer: arrayBuffer,
title: 'Trace Id ' + traceId,
url: reopenUrl.toString(),
}}, ORIGIN);
};

window.addEventListener('message', onMessageHandler);
}
</script>
<div class="container">
<div class="card">
<div class="card-title">Traces</div>
<div class="card-body">
<table id = buckets class = "data-table">
<tbody>
<tr>
<td><a href="/traces?id={{.ID}}&lat_hi=1ms">0-1ms</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=1ms&lat_hi=10ms">1-10ms</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=10ms&lat_hi=100ms">10-100ms</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=100ms&lat_hi=1s">100ms-1s</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=1s&lat_hi=10s">1-10s</a></td>
<td><a href="/traces?id={{.ID}}">all</a></td>
<td><a href="/traces?id={{.ID}}&errs=true">errors</a></td>
</tr>
</tbody>
</table>
<br>
<table id="traces" class="data-table">
<thead>
<div class="card">
<div class="card-title">Traces</div>
<div class="card-body">
<table id = buckets class = "data-table">
<tbody>
<tr>
<td><a href="/traces?id={{.ID}}&lat_hi=1ms">0-1ms</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=1ms&lat_hi=10ms">1-10ms</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=10ms&lat_hi=100ms">10-100ms</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=100ms&lat_hi=1s">100ms-1s</a></td>
<td><a href="/traces?id={{.ID}}&lat_low=1s&lat_hi=10s">1-10s</a></td>
<td><a href="/traces?id={{.ID}}">all</a></td>
<td><a href="/traces?id={{.ID}}&errs=true">errors</a></td>
</tr>
</tbody>
</table>
<br>
<table id="traces" class="data-table">
<thead>
<tr>
<th scope="col">Trace URL</th>
<th scope="col">Start Time</th>
<th scope="col">Latency</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{{range .Traces}}
<tr>
<th scope="col">Trace URL</th>
<th scope="col">Start Time</th>
<th scope="col">Latency</th>
<th scope="col">Status</th>
<td><a href="javascript:fetchAndOpen('/tracefetch?trace_id={{.TraceID}}')">link</a></td>
<td>{{.StartTime}}</td>
<td>{{sub .EndTime .StartTime}}</td>
<td>{{.Status}}</td>
</tr>
</thead>
<tbody>
{{range .Traces}}
<tr>
<td><a href="{{traceurl .TraceID}}">link</a></td>
<td>{{.StartTime}}</td>
<td>{{sub .EndTime .StartTime}}</td>
<td>{{.Status}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</tbody>
</table>
</div>
</div>
</body>
Expand Down
Loading

0 comments on commit 0be465f

Please sign in to comment.