blob: 384645f0a826f039ca1f5397376c3129b1675b31 [file] [log] [blame]
// Copyright 2018 The Goma Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package server
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"runtime/debug"
"strconv"
"sync/atomic"
"time"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.chromium.org/goma/server/log"
)
var (
openFDs = stats.Int64("go.chromium.org/goma/server/server/process-open-fds",
"Number of open file descriptors",
stats.UnitDimensionless)
maxFDs = stats.Int64("go.chromium.org/goma/server/server/process-max-fds",
"Maximum number of open file descriptors",
stats.UnitDimensionless)
virtualMemorySize = stats.Int64("go.chromium.org/goma/server/server/process-virtual-memory",
"Virtual memory size",
stats.UnitBytes)
residentMemorySize = stats.Int64("go.chromium.org/goma/server/server/process-resident-memory",
"Resident memory size",
stats.UnitBytes)
procStatViews = []*view.View{
{
Name: "go.chromium.org/goma/server/server/process-open-fds",
Description: "Number of open file descriptors",
Measure: openFDs,
Aggregation: view.LastValue(),
},
{
Name: "go.chromium.org/goma/server/server/process-max-fds",
Description: "Maxinum number of open file descriptors",
Measure: maxFDs,
Aggregation: view.LastValue(),
},
{
Name: "go.chromium.org/goma/server/server/process-virtual-memory",
Description: "Virtual memory size",
Measure: virtualMemorySize,
Aggregation: view.LastValue(),
},
{
Name: "go.chromium.org/goma/server/server/process-resident-memory",
Description: "Resident memory size",
Measure: residentMemorySize,
Aggregation: view.LastValue(),
},
}
lastResidentMemorySize int64 // atomic.
samplingInterval = time.Second
gcSema = make(chan bool, 1)
)
// ResidentMemorySize reports latest measured resident memory size in bytes.
func ResidentMemorySize() int64 {
return atomic.LoadInt64(&lastResidentMemorySize)
}
// GC runs garbage-collector and reports latest measured resident memory size in bytes.
func GC(ctx context.Context) int64 {
logger := log.FromContext(ctx)
rss := ResidentMemorySize()
logger.Infof("GC start: rss=%d", rss)
select {
case gcSema <- true:
debug.FreeOSMemory()
<-gcSema
default:
logger.Infof("GC already running")
}
procStats(ctx)
rss = ResidentMemorySize()
logger.Infof("GC end: rss=%d", rss)
return rss
}
func numOpenFDs(ctx context.Context) (int64, error) {
d, err := os.Open("/proc/self/fd")
if err != nil {
return 0, err
}
defer d.Close()
names, err := d.Readdirnames(-1)
if err != nil {
return 0, err
}
return int64(len(names)), nil
}
func parseMaxFDs(ctx context.Context, r io.Reader) (int64, error) {
// soft limit of "Max open files"
const maxOpenFiles = `Max open files`
s := bufio.NewScanner(r)
for s.Scan() {
line := s.Bytes()
if !bytes.HasPrefix(line, []byte(maxOpenFiles)) {
continue
}
line = bytes.TrimPrefix(line, []byte(maxOpenFiles))
line = bytes.TrimSpace(line)
cols := bytes.Fields(line)
if len(cols) == 0 {
return 0, fmt.Errorf("wrong line for Max open files: %q", string(line))
}
return strconv.ParseInt(string(cols[0]), 10, 64)
}
err := s.Err()
if err != nil {
return 0, err
}
return 0, errors.New(`"Max open files" not found`)
}
func numMaxFDs(ctx context.Context) (int64, error) {
f, err := os.Open("/proc/self/limits")
if err != nil {
return 0, err
}
defer f.Close()
return parseMaxFDs(ctx, f)
}
func parseStatMemory(ctx context.Context, r io.Reader) (vsize, rss int64, err error) {
// see proc(5)
data, err := ioutil.ReadAll(r)
if err != nil {
return 0, 0, err
}
i := bytes.LastIndex(data, []byte(")"))
if i < 0 {
return 0, 0, fmt.Errorf("unexpected format of stat (no comm): %q", string(data))
}
cols := bytes.Fields(data[i+2:])
if len(cols) < 22 {
return 0, 0, fmt.Errorf("unexpected format of stat (few data): %q %d", string(data), len(cols))
}
// cols starts from (3) state.
// we want (23) vsize and (24) rss.
const vsizeIndex = 23 - 3
const rssIndex = 24 - 3
vsize, err = strconv.ParseInt(string(cols[vsizeIndex]), 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse vsize %q: %v", string(cols[vsizeIndex]), err)
}
rss, err = strconv.ParseInt(string(cols[rssIndex]), 10, 64)
if err != nil {
return 0, 0, fmt.Errorf("parse rss %q: %v", string(cols[rssIndex]), err)
}
// vsize is in bytes, rss is in pages.
rssBytes := rss * int64(os.Getpagesize())
atomic.StoreInt64(&lastResidentMemorySize, rssBytes)
return vsize, rssBytes, nil
}
func statMemory(ctx context.Context) (vsize, rss int64, err error) {
f, err := os.Open("/proc/self/stat")
if err != nil {
return 0, 0, err
}
defer f.Close()
return parseStatMemory(ctx, f)
}
func procStats(ctx context.Context) {
logger := log.FromContext(ctx)
var m []stats.Measurement
n, err := numOpenFDs(ctx)
if err != nil {
logger.Errorf("failed to get open-fds: %v", err)
} else {
m = append(m, openFDs.M(n))
}
n, err = numMaxFDs(ctx)
if err != nil {
logger.Errorf("failed to get max-fds: %v", err)
} else {
m = append(m, maxFDs.M(n))
}
vsize, rss, err := statMemory(ctx)
if err != nil {
logger.Errorf("failed to get stat: %v", err)
} else {
m = append(m,
virtualMemorySize.M(vsize),
residentMemorySize.M(rss))
}
stats.Record(ctx, m...)
}
func reportProcStats(ctx context.Context) {
t := time.NewTicker(samplingInterval)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
procStats(ctx)
}
}
}