From 1a378ce640b5b255ff7e4693c8cf7c5be3d45956 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Sun, 26 Feb 2023 20:16:08 +0100 Subject: [PATCH] Enable profiler and profile-guided builds I used the chance to simplify the Makefile, as this is required for the file check to work correctly. Variables should not contain quotes, as these will be included in the value otherwise. --- .gitignore | 3 +++ Makefile | 30 ++++++++++++++++-------- cmd/poseidon/main.go | 48 ++++++++++++++++++++++++++++++++++++-- configuration.example.yaml | 8 +++++++ internal/config/config.go | 19 ++++++++++----- 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 0f2bb3c..97c84f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Project binary /poseidon +# CPU profiling information +cmd/poseidon/default.pgo + # Configuration file configuration.yaml tests/e2e/configuration.yaml diff --git a/Makefile b/Makefile index 45b8bfe..d05082a 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,23 @@ -PROJECT_NAME := "poseidon" -REPOSITORY_OWNER = "openHPI" -PKG := "github.com/$(REPOSITORY_OWNER)/$(PROJECT_NAME)/cmd/$(PROJECT_NAME)" +PROJECT_NAME = poseidon +REPOSITORY_OWNER = openHPI +PKG = github.com/$(REPOSITORY_OWNER)/$(PROJECT_NAME)/cmd/$(PROJECT_NAME) UNIT_TESTS = $(shell go list ./... | grep -v /e2e | grep -v /recovery) -DOCKER_TAG := "poseidon:latest" -DOCKER_OPTS := -v $(shell pwd)/configuration.yaml:/configuration.yaml +# Define the PGO file to be used for the build +PGO_FILE = ./cmd/$(PROJECT_NAME)/default.pgo + +# Docker options +DOCKER_TAG = poseidon:latest +DOCKER_OPTS = -v $(shell pwd)/configuration.yaml:/configuration.yaml LOWER_REPOSITORY_OWNER = $(shell echo $(REPOSITORY_OWNER) | tr A-Z a-z) # Define image to be used in e2e tests. Requires `make` to be available. -E2E_TEST_DOCKER_CONTAINER := co_execenv_java -E2E_TEST_DOCKER_TAG := 17 -E2E_TEST_DOCKER_IMAGE = "$(LOWER_REPOSITORY_OWNER)/$(E2E_TEST_DOCKER_CONTAINER):$(E2E_TEST_DOCKER_TAG)" +E2E_TEST_DOCKER_CONTAINER = co_execenv_java +E2E_TEST_DOCKER_TAG = 17 +E2E_TEST_DOCKER_IMAGE = $(LOWER_REPOSITORY_OWNER)/$(E2E_TEST_DOCKER_CONTAINER):$(E2E_TEST_DOCKER_TAG) # The base image of the e2e test image. This is used to build the base image as well. E2E_TEST_BASE_CONTAINER := docker_exec_phusion -E2E_TEST_BASE_IMAGE = "$(LOWER_REPOSITORY_OWNER)/$(E2E_TEST_BASE_CONTAINER)" +E2E_TEST_BASE_IMAGE = $(LOWER_REPOSITORY_OWNER)/$(E2E_TEST_BASE_CONTAINER) default: help @@ -45,7 +49,13 @@ git-hooks: .git/hooks/pre-commit ## Install the git-hooks .PHONY: build build: deps ## Build the binary +ifneq ("$(wildcard $(PGO_FILE))","") +# PGO_FILE exists + @go build -pgo=$(PGO_FILE) -ldflags "-X main.pgoEnabled=true" -o $(PROJECT_NAME) -v $(PKG) +else +# PGO_FILE does not exist @go build -o $(PROJECT_NAME) -v $(PKG) +endif .PHONY: clean clean: ## Remove previous build @@ -101,7 +111,7 @@ deploy/dockerfiles: ## Clone Dockerfiles repository .PHONY: e2e-test-docker-image e2e-test-docker-image: deploy/dockerfiles ## Build Docker image that is used in e2e tests - @docker build -t $(E2E_TEST_BASE_IMAGE) -f deploy/dockerfiles/$(E2E_TEST_BASE_CONTAINER) + @docker build -t $(E2E_TEST_BASE_IMAGE) deploy/dockerfiles/$(E2E_TEST_BASE_CONTAINER) @docker build -t $(E2E_TEST_DOCKER_IMAGE) deploy/dockerfiles/$(E2E_TEST_DOCKER_CONTAINER)/$(E2E_TEST_DOCKER_TAG) .PHONY: e2e-test diff --git a/cmd/poseidon/main.go b/cmd/poseidon/main.go index e4cb116..a2c54ab 100644 --- a/cmd/poseidon/main.go +++ b/cmd/poseidon/main.go @@ -15,6 +15,8 @@ import ( "net/http" "os" "os/signal" + "runtime/pprof" + "strconv" "syscall" "time" ) @@ -22,9 +24,21 @@ import ( var ( gracefulShutdownWait = 15 * time.Second log = logging.GetLogger("main") + // If pgoEnabled is true, the binary was built with PGO enabled. + // This is set during compilation with our Makefile as a STRING. + pgoEnabled = "false" ) -func initSentry(options *sentry.ClientOptions) { +func initSentry(options *sentry.ClientOptions, profilingEnabled bool) { + options.BeforeSendTransaction = func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { + if event.Tags == nil { + event.Tags = make(map[string]string) + } + event.Tags["go_pgo"] = pgoEnabled + event.Tags["go_profiling"] = strconv.FormatBool(profilingEnabled) + return event + } + if err := sentry.Init(*options); err != nil { log.Errorf("sentry.Init: %s", err) } @@ -37,6 +51,33 @@ func shutdownSentry() { } } +func initProfiling(options config.Profiling) (cancel func()) { + if options.Enabled { + profile, err := os.Create(options.File) + if err != nil { + log.WithError(err).Error("Error while opening the profile file") + } + + log.Debug("Starting CPU profiler") + if err := pprof.StartCPUProfile(profile); err != nil { + log.WithError(err).Error("Error while starting the CPU profiler!!") + } + + cancel = func() { + if options.Enabled { + log.Debug("Stopping CPU profiler") + pprof.StopCPUProfile() + if err := profile.Close(); err != nil { + log.WithError(err).Error("Error while closing profile file") + } + } + } + } else { + cancel = func() {} + } + return cancel +} + func runServer(server *http.Server) { log.WithField("address", server.Addr).Info("Starting server") var err error @@ -139,9 +180,12 @@ func main() { log.WithError(err).Warn("Could not initialize configuration") } logging.InitializeLogging(config.Config.Logger.Level) - initSentry(&config.Config.Sentry) + initSentry(&config.Config.Sentry, config.Config.Profiling.Enabled) defer shutdownSentry() + stopProfiling := initProfiling(config.Config.Profiling) + defer stopProfiling() + cancel := monitoring.InitializeInfluxDB(&config.Config.InfluxDB) defer cancel() diff --git a/configuration.example.yaml b/configuration.example.yaml index 963c575..5b24f2a 100644 --- a/configuration.example.yaml +++ b/configuration.example.yaml @@ -61,6 +61,14 @@ logger: # Log level that is used after reading the config (INFO until then) level: DEBUG +# Configuration of the embedded profiler +profiling: + # Enables the runtime profiler + enabled: false + # The file to which the profile is written to. + # The default location `cmd/poseidon/default.pgo` will be picked up during the build process to create a profile-guided build. + file: cmd/poseidon/default.pgo + # Configuration of the sentry logging sentry: # The DSN of the sentry endpoint to use. diff --git a/internal/config/config.go b/internal/config/config.go index 64a3f54..61a469e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,6 +123,12 @@ type logger struct { Level string } +// Profiling configures the usage of a runtime profiler to create optimized binaries. +type Profiling struct { + Enabled bool + File string +} + // InfluxDB configures the usage of an Influx db monitoring. type InfluxDB struct { URL string @@ -134,12 +140,13 @@ type InfluxDB struct { // configuration contains the complete configuration of Poseidon. type configuration struct { - Server server - Nomad Nomad - AWS AWS - Logger logger - Sentry sentry.ClientOptions - InfluxDB InfluxDB + Server server + Nomad Nomad + AWS AWS + Logger logger + Profiling Profiling + Sentry sentry.ClientOptions + InfluxDB InfluxDB } // InitConfig merges configuration options from environment variables and