peridot/third_party/github.com/bazelbuild/bazel-watcher/internal/ibazel/profiler/profiler.go

428 lines
10 KiB
Go
Raw Normal View History

2023-08-22 00:43:49 +00:00
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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 profiler
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
golog "log"
"math/rand"
"net"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/bazelbuild/bazel-watcher/internal/ibazel/log"
"github.com/bazelbuild/bazel-watcher/third_party/bazel/master/src/main/protobuf/blaze_query"
)
var profileDev = flag.String("profile_dev", "", "Turn on profiling and append report to file")
const (
// DefaultPort is the profiler Server's default server port
DefaultPort uint16 = 30000
)
type Profiler struct {
server *http.Server
file *os.File
version string
targets []string
iteration string
iterationStartTime int64
iterationBuildStart bool
iterationReloadTriggered bool
changes []string
lock sync.Mutex // guards events
}
type profileEvent struct {
// common
Type string `json:"type"`
Iteration string `json:"iteration"`
Time int64 `json:"time"`
Targets []string `json:"targets,omitempty"`
Elapsed int64 `json:"elapsed,omitempty"`
// start event
IBazelVersion string `json:"iBazelVersion,omitempty"`
BazelVersion string `json:"bazelVersion,omitempty"`
MaxHeapSize string `json:"maxHeapSize,omitempty"`
CommittedHeapSize string `json:"committedHeapSize,omitempty"`
// change event
Change string `json:"change,omitempty"`
// build & reload event
Changes []string `json:"changes,omitempty"`
// browser event
RemoteType string `json:"remoteType,omitempty"`
RemoteTime int64 `json:"remoteTime,omitempty"`
RemoteElapsed int64 `json:"remoteElapsed,omitempty"`
RemoteData string `json:"remoteData,omitempty"`
}
type profilerRemoteEvent struct {
Type string `json:"type"`
Time int64 `json:"time"`
TimeSinceNavigationStart int64 `json:"timeSinceNavigationStart"`
Data string `json:"data"`
}
func New(version string) *Profiler {
p := &Profiler{}
p.version = version
return p
}
func (i *Profiler) Initialize(info *map[string]string) {
if *profileDev == "" {
return
}
var err error
i.file, err = os.OpenFile(*profileDev, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Errorf("Failed to open profile output file: %s", *profileDev)
return
}
log.Errorf("Profile output: %s", *profileDev)
i.iterationBuildStart = true
i.newIteration()
i.startEvent(info)
i.startProfilerServer()
}
func (i *Profiler) TargetDecider(rule *blaze_query.Rule) {}
func (i *Profiler) ChangeDetected(targets []string, changeType string, change string) {
if i.file == nil {
return
}
i.targets = targets
switch changeType {
case "source":
i.changeEvent("SOURCE_CHANGE", change)
case "graph":
i.changeEvent("GRAPH_CHANGE", change)
}
}
func (i *Profiler) BeforeCommand(targets []string, command string) {
if i.file == nil {
return
}
i.targets = targets
switch command {
case "build":
i.buildEvent("BUILD_START")
case "test":
i.buildEvent("TEST_START")
case "run":
i.buildEvent("RUN_START")
}
}
func (i *Profiler) AfterCommand(targets []string, command string, success bool, output *bytes.Buffer) {
if i.file == nil {
return
}
i.targets = targets
if success {
switch command {
case "build":
i.buildEvent("BUILD_DONE")
case "test":
i.buildEvent("TEST_DONE")
case "run":
i.buildEvent("RUN_DONE")
}
} else {
switch command {
case "build":
i.buildEvent("BUILD_FAILED")
case "test":
i.buildEvent("TEST_FAILED")
case "run":
i.buildEvent("RUN_FAILED")
}
}
}
func (i *Profiler) Cleanup() {
if i.file != nil {
i.file.Close()
}
i.closeServer()
}
func (i *Profiler) ReloadTriggered(targets []string) {
if i.file == nil {
return
}
i.targets = targets
i.reloadTriggeredEvent()
}
func (i *Profiler) startProfilerServer() {
port := DefaultPort
for ; port < DefaultPort+100; port++ {
if testPort(port) {
go func() {
err := i.listen(port)
if err != nil {
log.Errorf("Profiler server failed to start: %v", err)
}
}()
url := fmt.Sprintf("http://localhost:%d/profiler.js", port)
os.Setenv("IBAZEL_PROFILER_URL", url)
return
}
}
log.Log("Could not find open port for profiler server")
}
func (i *Profiler) listen(port uint16) error {
if i.server != nil {
return errors.New("Profiler already listening")
}
// Create router
router := http.NewServeMux()
// Create server
i.server = &http.Server{
Handler: router,
ErrorLog: golog.New(os.Stderr, "[profiler]", 0),
}
i.server.Addr = makeAddr(port)
// Handle profiler.js requests
router.HandleFunc("/profiler.js", i.jsHandler)
// Handle profiler events
router.HandleFunc("/profiler-event", i.profilerEventHandler)
// Create listener
l, err := net.Listen("tcp", makeAddr(port))
if err != nil {
i.closeServer()
return err
}
log.Logf("[profiler] listening on %s", i.server.Addr)
err = i.server.Serve(l)
i.closeServer()
return err
}
func (i *Profiler) startEvent(info *map[string]string) {
event := profileEvent{}
event.Type = "IBAZEL_START"
if info != nil {
event.IBazelVersion = i.version
event.BazelVersion = (*info)["release"]
event.MaxHeapSize = (*info)["max-heap-size"]
event.CommittedHeapSize = (*info)["committed-heap-size"]
}
i.lock.Lock()
i.processEvent(&event)
i.lock.Unlock()
}
func (i *Profiler) buildEvent(eventType string) {
i.lock.Lock()
i.buildingIteration()
event := profileEvent{}
event.Type = eventType
event.Changes = i.changes
i.processEvent(&event)
i.lock.Unlock()
}
func (i *Profiler) changeEvent(eventType string, change string) {
i.lock.Lock()
i.newIteration()
event := profileEvent{}
event.Type = eventType
event.Change = change
i.processEvent(&event)
i.changes = append(i.changes, change)
i.lock.Unlock()
}
func (i *Profiler) reloadTriggeredEvent() {
i.lock.Lock()
i.iterationReloadTriggered = true
event := profileEvent{}
event.Type = "RELOAD_TRIGGERED"
event.Changes = i.changes
i.processEvent(&event)
i.lock.Unlock()
}
func (i *Profiler) remoteEvent(remoteEvent *profilerRemoteEvent) {
i.lock.Lock()
if !i.iterationReloadTriggered {
log.Logf("Ignoring unexpected remote event")
return
}
event := profileEvent{}
event.Type = "REMOTE_EVENT"
event.RemoteType = remoteEvent.Type
event.RemoteTime = remoteEvent.Time
event.RemoteElapsed = remoteEvent.TimeSinceNavigationStart
event.RemoteData = remoteEvent.Data
i.processEvent(&event)
i.lock.Unlock()
}
func (i *Profiler) processEvent(event *profileEvent) {
if i.file != nil && event != nil {
// prepare the event
event.Iteration = i.iteration
event.Time = makeTimestamp()
event.Targets = i.targets
event.Elapsed = event.Time - i.iterationStartTime
// write the event to the output file
eventJson, _ := json.Marshal(event)
eventJson = append(eventJson, 10) // \n
_, err := i.file.Write(eventJson)
if err != nil {
log.Errorf("Error writing to profile file: %v", err)
}
}
}
func (i *Profiler) newIteration() {
if i.iterationBuildStart {
i.iteration = randomString(16)
i.changes = make([]string, 0, 100)
i.iterationStartTime = makeTimestamp()
i.iterationBuildStart = false
i.iterationReloadTriggered = false
}
}
func (i *Profiler) buildingIteration() {
i.iterationBuildStart = true
}
func (i *Profiler) jsHandler(rw http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
log.Errorf("profiler.js invalid request method: %s", req.Method)
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}
rw.Header().Set("Content-Type", "application/javascript")
_, err := rw.Write(js)
if err != nil {
log.Errorf("Error handling profile.js request: %v", err)
}
}
func (i *Profiler) profilerEventHandler(rw http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
log.Errorf("Profiler invalid request method: %s", req.Method)
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}
defer req.Body.Close()
decoder := json.NewDecoder(req.Body)
var remoteEvent profilerRemoteEvent
err := decoder.Decode(&remoteEvent)
if err != nil {
log.Errorf("Failed to decode profile post data: %v", err)
rw.WriteHeader(http.StatusBadRequest)
return
}
log.Logf("Remote event: %s", remoteEvent.Type)
i.remoteEvent(&remoteEvent)
}
func (i *Profiler) closeServer() {
if i.server != nil {
err := i.server.Close()
if err != nil {
log.Errorf("Error closing profiler server: %v", err)
}
i.server = nil
}
}
// makeAddr converts uint16(x) to ":x"
func makeAddr(port uint16) string {
return fmt.Sprintf(":%d", port)
}
func makeTimestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
func testPort(port uint16) bool {
ln, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(port), 10))
if err != nil {
log.Errorf("Error opening port %d: %v", port, err)
return false
}
ln.Close()
return true
}
const letterBytes = "0123456789abcdef"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var randSrc = rand.NewSource(time.Now().UnixNano())
// Fast random string generator
// See: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
func randomString(n int) string {
b := make([]byte, n)
// A randSrc.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, randSrc.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = randSrc.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}