Skip to content

Commit

Permalink
internal/apps: Add two new apps (#2868)
Browse files Browse the repository at this point in the history
Co-authored-by: Nick Ripley <nick.ripley@datadoghq.com>
  • Loading branch information
felixge and nsrip-dd authored Sep 18, 2024
1 parent 1f0966d commit 3646321
Show file tree
Hide file tree
Showing 7 changed files with 471 additions and 3 deletions.
2 changes: 1 addition & 1 deletion internal/apps/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.22
FROM golang:1.23
COPY . /dd-trace-go
WORKDIR /dd-trace-go/internal/apps
# -t will download all dependencies, including test dependencies
Expand Down
9 changes: 8 additions & 1 deletion internal/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ type Config struct {
// default we configure non-stop execution tracing for the test apps unless
// a DD_PROFILING_EXECUTION_TRACE_PERIOD env is set or this option is true.
DisableExecutionTracing bool

httpAddr net.Addr
}

func (c Config) RunHTTP(handler func() http.Handler) {
func (c *Config) RunHTTP(handler func() http.Handler) {
// Parse common test app flags
var (
httpF = flag.String("http", "localhost:8080", "HTTP addr to listen on.")
Expand Down Expand Up @@ -69,6 +71,7 @@ func (c Config) RunHTTP(handler func() http.Handler) {
log.Fatalf("failed to listen: %s", err)
}
defer l.Close()
c.httpAddr = l.Addr()
log.Printf("Listening on: http://%s", *httpF)
// handler is a func, because if we create a traced handler before starting
// the tracer, the service name will default to http.router.
Expand All @@ -79,3 +82,7 @@ func (c Config) RunHTTP(handler func() http.Handler) {
<-ctx.Done()
log.Printf("Received interrupt, shutting down")
}

func (c Config) HTTPAddr() net.Addr {
return c.httpAddr
}
177 changes: 177 additions & 0 deletions internal/apps/gc-overhead/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2023 Datadog, Inc.

// gc-overhead implements a http service that demonstrates high GC overhead. The
// primary use case is to take screenshots of CPU and Memory profiles for blog
// posts. The code is intentionally inefficient, but should produce plausible
// FlameGraphs. Loop and data sizes are chosen so that the hotspots in the CPU
// profile, the Allocated Memory Profile, and the Heap Live Objects profile are
// different.
package main

import (
"bytes"
"encoding/json"
"fmt"
"maps"
"math"
"math/rand/v2"
"net/http"
"runtime/debug"
"slices"
"sync"
"time"

"github.com/DataDog/dd-trace-go/internal/apps"
httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
)

func main() {
// Initialize fake data
initFakeData()

// Experimentally determined value to keep GC overhead around 30%.
debug.SetGCPercent(35)

// Start app
app := apps.Config{}
app.RunHTTP(func() http.Handler {
mux := httptrace.NewServeMux()
mux.HandleFunc("/vehicles/update_location", VehiclesUpdateLocationHandler)
mux.HandleFunc("/vehicles/list", VehiclesListHandler)
return mux
})
}

func VehiclesUpdateLocationHandler(w http.ResponseWriter, r *http.Request) {
load := int(sineLoad() * 2e5)
for i := 0; i < load; i++ {
u := &VehicleLocationUpdate{}
data := fakeData.vehicleLocationUpdates[i%len(fakeData.vehicleLocationUpdates)]
if err := parseVehicleLocationUpdate(data, u); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
store.Update(u)
}
w.Write([]byte("ok"))
}

func parseVehicleLocationUpdate(data []byte, u *VehicleLocationUpdate) error {
return json.Unmarshal(data, u)
}

func VehiclesListHandler(w http.ResponseWriter, r *http.Request) {
w.Write(renderVehiclesList().Bytes())
}

func renderVehiclesList() *bytes.Buffer {
buf := &bytes.Buffer{}
list := store.List()
load := sineLoad() * float64(len(list))
list = list[0:int(load)]
for _, v := range list {
fmt.Fprintf(buf, "%s: %v\n", v.ID, v.History)
}
return buf
}

var fakeData struct {
vehicleLocationUpdates [1000][]byte
}

var store = MemoryStore{}

func initFakeData() {
for i := 0; i < len(fakeData.vehicleLocationUpdates); i++ {
update := VehicleLocationUpdate{
ID: fmt.Sprintf("vehicle-%d", i),
Position: Position{
Longitude: rand.Float64()*180 - 90,
Latitude: rand.Float64()*360 - 180,
},
}
fakeData.vehicleLocationUpdates[i], _ = json.Marshal(update)
}
}

type MemoryStore struct {
mu sync.RWMutex
vehicles map[string]*Vehicle
}

func (m *MemoryStore) Update(u *VehicleLocationUpdate) {
m.mu.Lock()
defer m.mu.Unlock()

if m.vehicles == nil {
m.vehicles = make(map[string]*Vehicle)
}

vehicle, ok := m.vehicles[u.ID]
if !ok {
vehicle = NewVehicle(u.ID)
m.vehicles[u.ID] = vehicle
}
vehicle.History = append(vehicle.History, &u.Position)
const historyLimit = 2000
if len(vehicle.History) > historyLimit {
// Keep only the last positions
copy(vehicle.History, vehicle.History[len(vehicle.History)-historyLimit:])
vehicle.History = vehicle.History[:historyLimit]
}
}

func NewVehicle(id string) *Vehicle {
return &Vehicle{ID: id, Data: make([]byte, 1024*1024)}
}

func (m *MemoryStore) List() (vehicles []*Vehicle) {
m.mu.RLock()
defer m.mu.RUnlock()

for _, key := range slices.Sorted(maps.Keys(m.vehicles)) {
vehicles = append(vehicles, m.vehicles[key].Copy())
}
return vehicles
}

type Position struct {
Longitude float64
Latitude float64
}

type VehicleLocationUpdate struct {
ID string
Position Position
}

type Vehicle struct {
ID string
History []*Position
Data []byte
}

func (v *Vehicle) Copy() *Vehicle {
history := make([]*Position, len(v.History))
copy(history, v.History)
return &Vehicle{
ID: v.ID,
History: history,
}
}

// sineLoad returns a value between 0 and 1 that varies sinusoidally over time.
func sineLoad() float64 {
period := 5 * time.Minute
// Get the current time in seconds since Unix epoch
currentTime := time.Now().UnixNano()
// Compute the phase of the sine wave, current time modulo period
phase := float64(currentTime) / float64(period) * 2 * math.Pi
// Generate the sine wave value (-1 to 1)
sineValue := math.Sin(phase)
// Normalize the sine wave value to be between 0 and 1
return (sineValue + 1) * 0.5
}
2 changes: 1 addition & 1 deletion internal/apps/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/DataDog/dd-trace-go/internal/apps

go 1.22.0
go 1.23.0

require (
golang.org/x/sync v0.7.0
Expand Down
43 changes: 43 additions & 0 deletions internal/apps/scenario_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ func TestScenario(t *testing.T) {
})
}
})

t.Run("gc-overhead", func(t *testing.T) {
scenarios := []struct {
version string
endpoints []string
}{
{"v1", []string{"/vehicles/update_location", "/vehicles/list"}},
}
for _, s := range scenarios {
t.Run(s.version, func(t *testing.T) {
lc := newLaunchConfig(t)
lc.Version = s.version
process := lc.Launch(t)
defer process.Stop(t)
wc.HitEndpoints(t, process, s.endpoints...)
})
}
})

t.Run("worker-pool-bottleneck", func(t *testing.T) {
scenarios := []struct {
version string
endpoints []string
}{
{"v1", []string{"/queue/push"}},
}
for _, s := range scenarios {
t.Run(s.version, func(t *testing.T) {
lc := newLaunchConfig(t)
lc.Version = s.version
process := lc.Launch(t)
defer process.Stop(t)
wc.HitEndpoints(t, process, s.endpoints...)
})
}
})
}

func newWorkloadConfig(t *testing.T) (wc workloadConfig) {
Expand Down Expand Up @@ -152,6 +188,13 @@ func appName(t *testing.T) string {
}

func serviceName(t *testing.T) string {
// Allow overriding the service name via env var
ddService := os.Getenv("DD_SERVICE")
if ddService != "" {
return ddService
}

// Otherwise derive the service name from the test name
return "dd-trace-go/" + strings.Join(strings.Split(t.Name(), "/")[1:], "/")
}

Expand Down
Loading

0 comments on commit 3646321

Please sign in to comment.