commit d0b12d668dd0e06314837de699c9412e04379689 Author: Kevin Trogant Date: Sun Apr 26 12:38:53 2026 +0200 dvfs agent 0.1 diff --git a/agent/Dockerfile b/agent/Dockerfile new file mode 100644 index 0000000..fa276c4 --- /dev/null +++ b/agent/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.25 +WORKDIR /usr/src/agent + +COPY go.mod ./ +RUN go mod download + +COPY . . +RUN go build -v -o /usr/local/bin/agent ./... + +CMD ["app"] + diff --git a/agent/go.mod b/agent/go.mod new file mode 100644 index 0000000..1fedeaf --- /dev/null +++ b/agent/go.mod @@ -0,0 +1,3 @@ +module scm.cms.hu-berlin.de/trogantk/dvfs-nf/agent + +go 1.25.6 diff --git a/agent/helpers.go b/agent/helpers.go new file mode 100644 index 0000000..f5c3790 --- /dev/null +++ b/agent/helpers.go @@ -0,0 +1,24 @@ +package main + +import ( + "log/slog" + "net/http" + "net/url" + "strings" +) + +// getStrippedRequestPath returns the request Path with the given prefix removed +func getStrippedRequestPath(url *url.URL, prefix string) string { + return strings.TrimPrefix(url.Path, prefix) +} + +func writeResponse(response string, status int, w http.ResponseWriter, logger *slog.Logger) { + if status != http.StatusOK { + w.WriteHeader(status) + } + _, err := w.Write([]byte(response)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + logger.Error("failed to write response", "err", err) + } +} diff --git a/agent/main.go b/agent/main.go new file mode 100644 index 0000000..51b6834 --- /dev/null +++ b/agent/main.go @@ -0,0 +1,181 @@ +package main + +import ( + "io" + "log/slog" + "math" + "net/http" + "os" + "regexp" + "strconv" + "strings" +) + +type cpuHandler struct { + corePaths []string + maxFreq int + minFreq int + logger *slog.Logger +} + +// readIntFromFile reads the given file until the end +// and converts the contained string into an integer +func readIntFromFile(path string) (int, error) { + b, err := os.ReadFile(path) + if err != nil { + return 0, err + } + return strconv.Atoi(strings.TrimSpace(string(b))) +} + +func enumerateCPUCores(logger *slog.Logger) ([]string, int, int, error) { + files, err := os.ReadDir("/sys/devices/system/cpu/") + if err != nil { + logger.Error("reading /sys/devices/system/cpu/", "err", err) + return nil, -1, -1, err + } + r, _ := regexp.Compile("cpu([0-9]+)") + + freqPaths := make([]string, 0) + maxF := math.MaxInt + minF := math.MinInt + + // we look for cpuN directories + for _, f := range files { + if !f.IsDir() || !r.MatchString(f.Name()) { + continue + } + + // read files for min & max freqs + dir := "/sys/devices/system/cpu/" + f.Name() + "/cpufreq/" + minFreq, err := readIntFromFile(dir + "cpuinfo_min_freq") + if err != nil { + logger.Error("failed to read cpu min freq", "path", dir+"cpuinfo_min_freq", "error", err) + continue + } + maxFreq, err := readIntFromFile(dir + "cpuinfo_max_freq") + if err != nil { + logger.Error("failed to read cpu max freq", "path", dir+"cpuinfo_min_freq", "error", err) + continue + } + freqPaths = append(freqPaths, dir+"scaling_max_freq") + maxF = min(maxF, maxFreq) + minF = max(minF, minFreq) + } + return freqPaths, minF, maxF, nil +} + +func (s *cpuHandler) handleMin(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + writeResponse("invalid method", http.StatusBadRequest, w, s.logger) + return + } + writeResponse(strconv.Itoa(s.minFreq), http.StatusOK, w, s.logger) +} + +func (s *cpuHandler) handleMax(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + writeResponse("invalid method", http.StatusBadRequest, w, s.logger) + return + } + writeResponse(strconv.Itoa(s.maxFreq), http.StatusOK, w, s.logger) +} + +func (s *cpuHandler) handleCurrent(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + maxFreq := math.MinInt + for _, core := range s.corePaths { + freq, err := readIntFromFile(core) + if err != nil { + s.logger.Error("error while reading core frequency", "core", core, "error", err) + writeResponse("internal server error", http.StatusInternalServerError, w, s.logger) + return + } + s.logger.Debug("core frequency", "core", core, "frequency", freq) + maxFreq = max(maxFreq, freq) + } + s.logger.Debug("reported frequency", "freq", maxFreq) + writeResponse(strconv.Itoa(maxFreq), http.StatusOK, w, s.logger) + case "PUT": + body, err := io.ReadAll(r.Body) + if err != nil { + s.logger.Error("failed to read the request body", "err", err) + writeResponse("internal server error", http.StatusInternalServerError, w, s.logger) + } + bodyStr := strings.TrimSpace(string(body)) + // Check if it is an integer + freqInt, err := strconv.Atoi(bodyStr) + if err != nil { + s.logger.Error("failed to convert body to int", "error", err) + writeResponse("bad request", http.StatusBadRequest, w, s.logger) + } + if freqInt < s.minFreq || freqInt > s.maxFreq { + s.logger.Warn("requested frequency is out-of-bounds", "freq", freqInt, "min", s.minFreq, "max", s.maxFreq) + freqInt = max(s.minFreq, min(freqInt, s.maxFreq)) + bodyStr = strconv.Itoa(freqInt) + } + s.logger.Info("requested to set current frequency", "frequency", bodyStr) + success := true + for _, core := range s.corePaths { + err = os.WriteFile(core, []byte(bodyStr), 0) + if err != nil { + s.logger.Error("failed to set core frequency", "core", core, "error", err) + success = false + } + } + if success { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + default: + s.logger.Error("unexpected method for current cpu", "method", r.Method) + writeResponse("invalid method", http.StatusBadRequest, w, s.logger) + } +} + +func (s *cpuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + requestedPath := getStrippedRequestPath(r.URL, r.Pattern) + switch requestedPath { + case "frequency/min": + s.handleMin(w, r) + return + case "frequency/max": + s.handleMax(w, r) + return + case "frequency/current": + s.handleCurrent(w, r) + return + default: + s.logger.Error("unknown resource", "path", requestedPath) + writeResponse("not found", http.StatusNotFound, w, s.logger) + } +} + +func main() { + appEnv := os.Getenv("AGENT_ENV") + loggerOpts := &slog.HandlerOptions{ + Level: slog.LevelDebug, + } + var handler slog.Handler = slog.NewTextHandler(os.Stdout, loggerOpts) + if appEnv == "production" { + loggerOpts.Level = slog.LevelInfo + handler = slog.NewJSONHandler(os.Stdout, loggerOpts) + } + logger := slog.New(handler) + + freqPaths, minF, maxF, err := enumerateCPUCores(logger) + if err != nil { + os.Exit(1) + } + + cpuHandler := &cpuHandler{ + logger: logger, + maxFreq: maxF, + minFreq: minF, + corePaths: freqPaths, + } + http.Handle("/cpu/", logRequests(logger, cpuHandler)) + logger.Info("exit", "result", http.ListenAndServe(":8080", nil)) +} diff --git a/agent/middleware.go b/agent/middleware.go new file mode 100644 index 0000000..87ff121 --- /dev/null +++ b/agent/middleware.go @@ -0,0 +1,13 @@ +package main + +import ( + "log/slog" + "net/http" +) + +func logRequests(logger *slog.Logger, handler http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logger.Info("request", "method", r.Method, "url", r.URL, "remote-addr", r.RemoteAddr) + handler.ServeHTTP(w, r) + } +}