@@ -5,11 +5,13 @@ import (
55 "bytes"
66 "context"
77 "encoding/base64"
8+ "encoding/json"
89 "fmt"
910 "io"
1011 "io/ioutil"
1112 "path/filepath"
1213 "strings"
14+ "time"
1315
1416 "github.com/SoftwareDefinedBuildings/spawnpoint/service"
1517 "github.com/docker/docker/api/types"
@@ -23,13 +25,35 @@ import (
2325
2426const defaultSpawnpointImage = "jhkolb/spawnable:amd64"
2527const logMaxSize = "50m"
28+ const cpuSharesPerCore = 1024
2629
2730type Docker struct {
2831 Alias string
2932 bw2Router string
3033 client * docker.Client
3134}
3235
36+ type dockerStatsResponse struct {
37+ Read time.Time `json:"read"`
38+ PreRead time.Time `json:"preread"`
39+ CPUStats struct {
40+ CPUUsage struct {
41+ TotalUsage uint64 `json:"total_usage"`
42+ } `json:"cpu_usage"`
43+ SystemCPUUsage uint64 `json:"system_cpu_usage"`
44+ OnlineCPUs uint64 `json:"online_cpus"`
45+ } `json:"cpu_stats"`
46+ PreCPUStats struct {
47+ CPUUsage struct {
48+ TotalUsage uint64 `json:"total_usage"`
49+ } `json:"cpu_usage"`
50+ SystemCPUUsage uint64 `json:"system_cpu_usage"`
51+ } `json:"precpu_stats"`
52+ MemoryStats struct {
53+ Usage uint64 `json:"usage"`
54+ } `json:"memory_stats"`
55+ }
56+
3357func NewDocker (alias , bw2Router string ) (* Docker , error ) {
3458 client , err := docker .NewEnvClient ()
3559 if err != nil {
@@ -208,6 +232,60 @@ func (dkr *Docker) ListServices(ctx context.Context) ([]string, error) {
208232 return IDs , nil
209233}
210234
235+ func (dkr * Docker ) ProfileService (ctx context.Context , id string , period time.Duration ) (<- chan Stats , <- chan error ) {
236+ statChan := make (chan Stats , 10 )
237+ errChan := make (chan error , 1 )
238+ response , err := dkr .client .ContainerStats (ctx , id , true )
239+ if err != nil {
240+ close (statChan )
241+ errChan <- errors .Wrap (err , "Failed to initialize container stat collection" )
242+ return statChan , errChan
243+ }
244+
245+ go func () {
246+ defer response .Body .Close ()
247+ decoder := json .NewDecoder (response .Body )
248+ lastEmitted := time .Now ()
249+ lastCPUCores := 0.0
250+ for {
251+ select {
252+ case <- ctx .Done ():
253+ close (statChan )
254+ return
255+ default :
256+ var statEntry dockerStatsResponse
257+ if err := decoder .Decode (& statEntry ); err != nil {
258+ close (statChan )
259+ if err != io .EOF {
260+ errChan <- errors .Wrap (err , "Failed to read and decode container stats entry" )
261+ }
262+ return
263+ }
264+ if statEntry .Read .Sub (lastEmitted ) > period {
265+ lastEmitted = time .Now ()
266+
267+ // Logic is based on Docker's `calculateCPUPercent` function
268+ containerCPUDelta := float64 (statEntry .CPUStats .CPUUsage .TotalUsage -
269+ statEntry .PreCPUStats .CPUUsage .TotalUsage )
270+ systemCPUDelta := float64 (statEntry .CPUStats .SystemCPUUsage -
271+ statEntry .PreCPUStats .SystemCPUUsage )
272+ if systemCPUDelta > 0.0 {
273+ numCores := float64 (statEntry .CPUStats .OnlineCPUs )
274+ lastCPUCores = (containerCPUDelta / systemCPUDelta ) * numCores
275+ }
276+
277+ statChan <- Stats {
278+ Memory : float64 (statEntry .MemoryStats .Usage ) / (1024.0 * 1024.0 ),
279+ CPUShares : lastCPUCores * cpuSharesPerCore ,
280+ }
281+ }
282+ }
283+ }
284+ }()
285+
286+ return statChan , errChan
287+ }
288+
211289func (dkr * Docker ) buildImage (ctx context.Context , svcConfig * service.Configuration ) (string , error ) {
212290 buildCtxt , err := generateBuildContext (svcConfig )
213291 if err != nil {
0 commit comments