|
package perf
import (
"context"
"encoding/json"
"fmt"
"html/template"
"log"
"math/rand"
"net/http"
"strings"
"sync"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type Perf struct {
mu sync.Mutex
measurements map[string]map[string]interface{}
}
func NewPerf() *Perf {
return &Perf{
measurements: make(map[string]map[string]interface{}),
}
}
type key int
var perfKey key = 0
func (p *Perf) MeasureRequest(req *http.Request) {
id := p.RequestID(req)
start := time.Now()
ctx := req.Context()
go func() {
select {
case <-ctx.Done():
p.measure(id, "request-time", time.Since(start).String())
}
time.Sleep(1 * time.Second)
p.Delete(id)
}()
}
func (p *Perf) Measure(req *http.Request, key string, val interface{}) {
p.measure(p.RequestID(req), key, val)
}
func (p *Perf) measure(id string, key string, val interface{}) {
p.mu.Lock()
data, ok := p.measurements[id]
if !ok {
data = make(map[string]interface{})
p.measurements[id] = data
}
data[key] = val
p.mu.Unlock()
}
func (p *Perf) RequestID(req *http.Request) string {
id, ok := req.Context().Value(perfKey).(string)
if !ok {
data := make([]byte, 10)
_, err := rand.Read(data)
if err != nil {
panic(err)
}
id = fmt.Sprintf("%x", data)
*req = *req.WithContext(context.WithValue(req.Context(), perfKey, id))
}
return id
}
func (p *Perf) Measurements(id string) map[string]interface{} {
p.mu.Lock()
ms := p.measurements[id]
p.mu.Unlock()
return ms
}
func (p *Perf) Delete(id string) {
p.mu.Lock()
delete(p.measurements, id)
p.mu.Unlock()
}
func (p *Perf) Size() int {
p.mu.Lock()
defer p.mu.Unlock()
return len(p.measurements)
}
func (p *Perf) ServeHTTP(w http.ResponseWriter, req *http.Request) {
parts := strings.SplitN(req.URL.Path, "/", 3)
if len(parts) < 3 {
http.Error(w, "missing request id", http.StatusBadRequest)
return
}
id := parts[2]
perfData := p.Measurements(id)
data, err := json.MarshalIndent(perfData, "", " ")
if err != nil {
log.Fatal("marshaling perf data: ", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)
}
func (p *Perf) Render(req *http.Request) template.HTML {
return template.HTML(`<style>
body {
transform: translate(0, 2.5em);
}
#performance-bar {
display: flex;
position: fixed;
width: 99.1vw;
transform: translate(-0.5em, -3em);
padding: 0.75em;
background-color: #222;
color: #ddd;
}
.performance-pair {
margin-right: 1em;
font-family: sans-serif;
}
.performance-value {
color: #eee;
font-weight: 600;
}
.performance-key {
margin-right: 0.5em;
color: #777;
}
</style>
` + `
<script>var performanceId = "` + p.RequestID(req) + `";
</script>
` + `
<script>
var performanceBar = document.getElementById("performance-bar");
var xhr = new XMLHttpRequest();
xhr.open("GET", "/__performance__/" + performanceId);
xhr.responseType = "json";
xhr.onreadystatechange = ev => {
if (xhr.readyState != XMLHttpRequest.DONE || xhr.status != 200) {
return
}
var performanceData = xhr.response;
if (performanceData == null) {
performanceBar.style = "color: red;";
performanceBar.textContent = "Could not load performance data";
}
Object.keys(performanceData).forEach(k => {
var kv = document.createElement("div");
kv.className = "performance-pair";
var ke = document.createElement("span");
ke.className = "performance-key";
ke.textContent = k;
kv.appendChild(ke);
var ve = document.createElement("span");
ve.className = "performance-value";
ve.textContent = performanceData[k];
kv.appendChild(ve);
performanceBar.appendChild(kv);
})
};
xhr.send();
</script>`)
}
|