| // Copyright 2020 The Chromium OS 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 memory |
| |
| import ( |
| "context" |
| "io/ioutil" |
| "math" |
| "strconv" |
| "strings" |
| |
| "chromiumos/tast/errors" |
| ) |
| |
| // Limit allows tests to determine if memory use is close to a limit without |
| // having to know the specific memory counters used. |
| type Limit interface { |
| // Distance returns the amount of memory that can be allocated in bytes |
| // before the limit is reached. If negative, abs(Distance()) bytes must be |
| // freed to go below the limit. |
| Distance(ctx context.Context) (int64, error) |
| // AssertNotReached returns an error if the limit has been reached. Useful |
| // for Polls to get information about which limit was exceeded, and by how |
| // much. |
| AssertNotReached(ctx context.Context) error |
| } |
| |
| // AvailableLimit is a Limit for ChromeOS available memory. |
| type AvailableLimit struct { |
| margin int64 |
| } |
| |
| // AvailableLimit conforms to Limit interface. |
| var _ Limit = (*AvailableLimit)(nil) |
| |
| // readFirstInt64 reads the first integer from a file. |
| func readFirstInt64(f string) (int64, error) { |
| // Files will always just be a single line, so it's OK to read everything. |
| data, err := ioutil.ReadFile(f) |
| if err != nil { |
| return 0, errors.Wrapf(err, "failed to read file %q", f) |
| } |
| firstString := strings.Split(strings.TrimSpace(string(data)), " ")[0] |
| firstInt64, err := strconv.ParseInt(firstString, 10, 64) |
| if err != nil { |
| return 0, errors.Wrapf(err, "unable to convert %q to integer", data) |
| } |
| return firstInt64, nil |
| } |
| |
| // Distance returns the difference between available memory and the critical |
| // margin, in bytes. Result will be negative if available memory is below the |
| // critical margin. |
| func (l *AvailableLimit) Distance(_ context.Context) (int64, error) { |
| const availableMemorySysFile = "/sys/kernel/mm/chromeos-low_mem/available" |
| availableMiB, err := readFirstInt64(availableMemorySysFile) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to read ChromeOS available memory") |
| } |
| return (availableMiB * MiB) - l.margin, nil |
| } |
| |
| // AssertNotReached checks that available memory is above the margin. |
| func (l *AvailableLimit) AssertNotReached(ctx context.Context) error { |
| distance, err := l.Distance(ctx) |
| if err != nil { |
| return err |
| } |
| if distance <= 0 { |
| return errors.Errorf("available memory %d is less than margin %d", distance+l.margin, l.margin) |
| } |
| return nil |
| } |
| |
| // CriticalMargin returns the value of ChromeOS available memory below which |
| // tabs are killed, in bytes. |
| func CriticalMargin() (int64, error) { |
| const marginMemorySysFile = "/sys/kernel/mm/chromeos-low_mem/margin" |
| criticalMarginMiB, err := readFirstInt64(marginMemorySysFile) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to read ChromeOS critical margin") |
| } |
| return criticalMarginMiB * MiB, nil |
| } |
| |
| // NewAvailableLimit creates a Limit that measures how far away ChromeOS |
| // available memory is from a specified margin, in bytes. |
| func NewAvailableLimit(margin int64) (*AvailableLimit, error) { |
| return &AvailableLimit{margin}, nil |
| } |
| |
| // NewAvailableCriticalLimit creates a Limit that measures how far away ChromeOS |
| // is from the critical memory pressure margin. Unlike |
| // NewAvailableIsCriticalLimit above, this it intended to keep ChromeOS memory |
| // pressure below critical. |
| func NewAvailableCriticalLimit() (*AvailableLimit, error) { |
| criticalMargin, err := CriticalMargin() |
| if err != nil { |
| return nil, err |
| } |
| return &AvailableLimit{ |
| margin: criticalMargin, |
| }, nil |
| } |
| |
| // ZoneInfoLimit is a Limit that uses /proc/zoneinfo to allow tests to |
| // allocate enough memory to trigger page reclaim, but not so much memory that |
| // they OOM. |
| type ZoneInfoLimit struct { |
| readZoneInfo func(context.Context) ([]ZoneInfo, error) |
| // lowZones is the set of zones we won't allow to get low. |
| lowZones map[string]bool |
| } |
| |
| // PageReclaimLimit conforms to Limit interface. |
| var _ Limit = (*ZoneInfoLimit)(nil) |
| |
| // IgnoreZone checks if a zone is not used. It checks if the pages free, min, low are all 0. |
| func IgnoreZone(info ZoneInfo) bool { |
| return info.Free == 0 && info.Min == 0 && info.Low == 0 |
| } |
| |
| // Distance computes how far away from OOMing we are. For each zone, compute |
| // zoneDistance := (min+low)/2. If any zoneDistance in l.lowZones is negative, |
| // return the lowest zoneDistance to keep any lowZone away from its min |
| // watermark. |
| // If no l.lowZones is negative, return the sum of all zoneDistance to indicate |
| // how many free pages there are in total before we start getting close to the |
| // min watermark in any of l.lowZones. |
| func (l *ZoneInfoLimit) Distance(ctx context.Context) (int64, error) { |
| infos, err := l.readZoneInfo(ctx) |
| if err != nil { |
| return 0, errors.Wrap(err, "failed to read zone counters") |
| } |
| var minDistance int64 = math.MaxInt64 |
| var sumDistance int64 |
| for _, info := range infos { |
| if IgnoreZone(info) { |
| continue |
| } |
| zoneDistance := int64(info.Free) - int64((info.Low+info.Min)/2) |
| sumDistance += zoneDistance |
| if _, ok := l.lowZones[info.Name]; ok && zoneDistance < minDistance { |
| minDistance = zoneDistance |
| } |
| } |
| if minDistance == math.MaxInt64 { |
| return 0, errors.New("no matching zones found") |
| } |
| if minDistance < 0 { |
| return minDistance, nil |
| } |
| return sumDistance, nil |
| } |
| |
| // AssertNotReached checks that no zone has its free pages counter below |
| // (min+low)/2. |
| func (l *ZoneInfoLimit) AssertNotReached(ctx context.Context) error { |
| infos, err := l.readZoneInfo(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to read zone counters") |
| } |
| for _, info := range infos { |
| if _, ok := l.lowZones[info.Name]; !ok { |
| // We don't care about this zone. |
| continue |
| } |
| if IgnoreZone(info) { |
| continue |
| } |
| distance := int64(info.Free) - int64((info.Low+info.Min)/2) |
| if distance <= 0 { |
| return errors.Errorf("zone %q free %d is less than (min+low)/2 (%d+%d)/2", info.Name, info.Free, info.Min, info.Low) |
| } |
| } |
| return nil |
| } |
| |
| // NewPageReclaimLimit creates a Limit that returns Distance 0 when Linux is |
| // reclaiming memory and is close to OOMing. |
| func NewPageReclaimLimit() *ZoneInfoLimit { |
| // NB: We only look at zones DMA and DMA32 because there is probably never |
| // going to be a Normal specific page allocation, and if Normal is low but |
| // there are still plenty of DMA and DMA32 pages, we're not actually close |
| // to OOMing because we'll just fetch pages from the other zones first. |
| return NewZoneInfoLimit( |
| func(_ context.Context) ([]ZoneInfo, error) { return ReadZoneInfo() }, |
| map[string]bool{ |
| "DMA": true, |
| "DMA32": true, |
| }, |
| ) |
| } |
| |
| // NewZoneInfoLimit creates a limit the returns |
| func NewZoneInfoLimit(readZoneInfo func(context.Context) ([]ZoneInfo, error), zones map[string]bool) *ZoneInfoLimit { |
| return &ZoneInfoLimit{readZoneInfo, zones} |
| } |
| |
| // CompositeLimit combines a set of Limits. |
| type CompositeLimit struct { |
| limits []Limit |
| } |
| |
| // CompositeLimit conforms to Limit interface. |
| var _ Limit = (*CompositeLimit)(nil) |
| |
| // Distance returns the minimum Distance returned by any child Limit. |
| func (l *CompositeLimit) Distance(ctx context.Context) (int64, error) { |
| if len(l.limits) == 0 { |
| return 0, errors.New("empty compositeLimit") |
| } |
| minDistance, err := l.limits[0].Distance(ctx) |
| if err != nil { |
| return 0, err |
| } |
| for i := 1; i < len(l.limits); i++ { |
| distance, err := l.limits[i].Distance(ctx) |
| if err != nil { |
| return 0, err |
| } |
| if distance < minDistance { |
| minDistance = distance |
| } |
| } |
| return minDistance, nil |
| } |
| |
| // AssertNotReached checks that child Limits are above their limits. |
| func (l *CompositeLimit) AssertNotReached(ctx context.Context) error { |
| if len(l.limits) == 0 { |
| return errors.New("empty compositeLimit") |
| } |
| for i := 1; i < len(l.limits); i++ { |
| if err := l.limits[i].AssertNotReached(ctx); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // NewCompositeLimit creates a Limit that returns the minimum Distance() |
| // returned by all the passed limits. |
| func NewCompositeLimit(limits ...Limit) *CompositeLimit { |
| return &CompositeLimit{limits} |
| } |