ui.go 3.31 KB
package memsizeui

import (
	"bytes"
	"fmt"
	"html/template"
	"net/http"
	"reflect"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/fjl/memsize"
)

type Handler struct {
	init     sync.Once
	mux      http.ServeMux
	mu       sync.Mutex
	reports  map[int]Report
	roots    map[string]interface{}
	reportID int
}

type Report struct {
	ID       int
	Date     time.Time
	Duration time.Duration
	RootName string
	Sizes    memsize.Sizes
}

type templateInfo struct {
	Roots     []string
	Reports   map[int]Report
	PathDepth int
	Data      interface{}
}

func (ti *templateInfo) Link(path ...string) string {
	prefix := strings.Repeat("../", ti.PathDepth)
	return prefix + strings.Join(path, "")
}

func (h *Handler) Add(name string, v interface{}) {
	rv := reflect.ValueOf(v)
	if rv.Kind() != reflect.Ptr || rv.IsNil() {
		panic("root must be non-nil pointer")
	}
	h.mu.Lock()
	if h.roots == nil {
		h.roots = make(map[string]interface{})
	}
	h.roots[name] = v
	h.mu.Unlock()
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.init.Do(func() {
		h.reports = make(map[int]Report)
		h.mux.HandleFunc("/", h.handleRoot)
		h.mux.HandleFunc("/scan", h.handleScan)
		h.mux.HandleFunc("/report/", h.handleReport)
	})
	h.mux.ServeHTTP(w, r)
}

func (h *Handler) templateInfo(r *http.Request, data interface{}) *templateInfo {
	h.mu.Lock()
	roots := make([]string, 0, len(h.roots))
	for name := range h.roots {
		roots = append(roots, name)
	}
	h.mu.Unlock()
	sort.Strings(roots)

	return &templateInfo{
		Roots:     roots,
		Reports:   h.reports,
		PathDepth: strings.Count(r.URL.Path, "/") - 1,
		Data:      data,
	}
}

func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	serveHTML(w, rootTemplate, http.StatusOK, h.templateInfo(r, nil))
}

func (h *Handler) handleScan(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "invalid HTTP method, want POST", http.StatusMethodNotAllowed)
		return
	}
	ti := h.templateInfo(r, "Unknown root")
	id, ok := h.scan(r.URL.Query().Get("root"))
	if !ok {
		serveHTML(w, notFoundTemplate, http.StatusNotFound, ti)
		return
	}
	w.Header().Add("Location", ti.Link(fmt.Sprintf("report/%d", id)))
	w.WriteHeader(http.StatusSeeOther)
}

func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) {
	var id int
	fmt.Sscan(strings.TrimPrefix(r.URL.Path, "/report/"), &id)
	h.mu.Lock()
	report, ok := h.reports[id]
	h.mu.Unlock()

	if !ok {
		serveHTML(w, notFoundTemplate, http.StatusNotFound, h.templateInfo(r, "Report not found"))
	} else {
		serveHTML(w, reportTemplate, http.StatusOK, h.templateInfo(r, report))
	}
}

func (h *Handler) scan(root string) (int, bool) {
	h.mu.Lock()
	defer h.mu.Unlock()

	val, ok := h.roots[root]
	if !ok {
		return 0, false
	}
	id := h.reportID
	start := time.Now()
	sizes := memsize.Scan(val)
	h.reports[id] = Report{
		ID:       id,
		RootName: root,
		Date:     start.Truncate(1 * time.Second),
		Duration: time.Since(start),
		Sizes:    sizes,
	}
	h.reportID++
	return id, true
}

func serveHTML(w http.ResponseWriter, tpl *template.Template, status int, ti *templateInfo) {
	w.Header().Set("content-type", "text/html")
	var buf bytes.Buffer
	if err := tpl.Execute(&buf, ti); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	buf.WriteTo(w)
}