1194 lines
33 KiB
Go
1194 lines
33 KiB
Go
/*
|
|
Copyright 2019-2021 Intel Corporation
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package rdt
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"math/bits"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
grclog "github.com/intel/goresctrl/pkg/log"
|
|
"github.com/intel/goresctrl/pkg/utils"
|
|
)
|
|
|
|
// Config is the user-specified RDT configuration.
|
|
type Config struct {
|
|
Options Options `json:"options"`
|
|
Partitions map[string]struct {
|
|
L2Allocation CatConfig `json:"l2Allocation"`
|
|
L3Allocation CatConfig `json:"l3Allocation"`
|
|
MBAllocation MbaConfig `json:"mbAllocation"`
|
|
Classes map[string]struct {
|
|
L2Allocation CatConfig `json:"l2Allocation"`
|
|
L3Allocation CatConfig `json:"l3Allocation"`
|
|
MBAllocation MbaConfig `json:"mbAllocation"`
|
|
Kubernetes KubernetesOptions `json:"kubernetes"`
|
|
} `json:"classes"`
|
|
} `json:"partitions"`
|
|
}
|
|
|
|
// CatConfig contains the L2 or L3 cache allocation configuration for one partition or class.
|
|
type CatConfig map[string]CacheIdCatConfig
|
|
|
|
// MbaConfig contains the memory bandwidth configuration for one partition or class.
|
|
type MbaConfig map[string]CacheIdMbaConfig
|
|
|
|
// CacheIdCatConfig is the cache allocation configuration for one cache id.
|
|
// Code and Data represent an optional configuration for separate code and data
|
|
// paths and only have effect when RDT CDP (Code and Data Prioritization) is
|
|
// enabled in the system. Code and Data go in tandem so that both or neither
|
|
// must be specified - only specifying the other is considered a configuration
|
|
// error.
|
|
type CacheIdCatConfig struct {
|
|
Unified CacheProportion
|
|
Code CacheProportion
|
|
Data CacheProportion
|
|
}
|
|
|
|
// CacheIdMbaConfig is the memory bandwidth configuration for one cache id.
|
|
// It's an array of at most two values, specifying separate values to be used
|
|
// for percentage based and MBps based memory bandwidth allocation. For
|
|
// example, `{"80%", "1000MBps"}` would allocate 80% if percentage based
|
|
// allocation is used by the Linux kernel, or 1000 MBps in case MBps based
|
|
// allocation is in use.
|
|
type CacheIdMbaConfig []MbProportion
|
|
|
|
// MbProportion specifies a share of available memory bandwidth. It's an
|
|
// integer value followed by a unit. Two units are supported:
|
|
//
|
|
// - percentage, e.g. `80%`
|
|
// - MBps, e.g. `1000MBps`
|
|
type MbProportion string
|
|
|
|
// CacheProportion specifies a share of the available cache lines.
|
|
// Supported formats:
|
|
//
|
|
// - percentage, e.g. `50%`
|
|
// - percentage range, e.g. `50-60%`
|
|
// - bit numbers, e.g. `0-5`, `2,3`, must contain one contiguous block of bits set
|
|
// - hex bitmask, e.g. `0xff0`, must contain one contiguous block of bits set
|
|
type CacheProportion string
|
|
|
|
// CacheIdAll is a special cache id used to denote a default, used as a
|
|
// fallback for all cache ids that are not explicitly specified.
|
|
const CacheIdAll = "all"
|
|
|
|
// config represents the final (parsed and resolved) runtime configuration of
|
|
// RDT Control
|
|
type config struct {
|
|
Options Options
|
|
Partitions partitionSet
|
|
Classes classSet
|
|
}
|
|
|
|
// partitionSet represents the pool of rdt partitions
|
|
type partitionSet map[string]*partitionConfig
|
|
|
|
// classSet represents the pool of rdt classes
|
|
type classSet map[string]*classConfig
|
|
|
|
// partitionConfig is the final configuration of one partition
|
|
type partitionConfig struct {
|
|
CAT map[cacheLevel]catSchema
|
|
MB mbSchema
|
|
}
|
|
|
|
// classConfig represents configuration of one class, i.e. one CTRL group in
|
|
// the Linux resctrl interface
|
|
type classConfig struct {
|
|
Partition string
|
|
CATSchema map[cacheLevel]catSchema
|
|
MBSchema mbSchema
|
|
Kubernetes KubernetesOptions
|
|
}
|
|
|
|
// Options contains common settings.
|
|
type Options struct {
|
|
L2 CatOptions `json:"l2"`
|
|
L3 CatOptions `json:"l3"`
|
|
MB MbOptions `json:"mb"`
|
|
}
|
|
|
|
// CatOptions contains the common settings for cache allocation.
|
|
type CatOptions struct {
|
|
Optional bool
|
|
}
|
|
|
|
// MbOptions contains the common settings for memory bandwidth allocation.
|
|
type MbOptions struct {
|
|
Optional bool
|
|
}
|
|
|
|
// KubernetesOptions contains per-class settings for the Kubernetes-related functionality.
|
|
type KubernetesOptions struct {
|
|
DenyPodAnnotation bool `json:"denyPodAnnotation"`
|
|
DenyContainerAnnotation bool `json:"denyContainerAnnotation"`
|
|
}
|
|
|
|
// catSchema represents a cache part of the schemata of a class (i.e. resctrl group)
|
|
type catSchema struct {
|
|
Lvl cacheLevel
|
|
Alloc catSchemaRaw
|
|
}
|
|
|
|
// catSchemaRaw is the cache schemata without the information about cache level
|
|
type catSchemaRaw map[uint64]catAllocation
|
|
|
|
// mbSchema represents the MB part of the schemata of a class (i.e. resctrl group)
|
|
type mbSchema map[uint64]uint64
|
|
|
|
// catAllocation describes the allocation configuration for one cache id
|
|
type catAllocation struct {
|
|
Unified cacheAllocation
|
|
Code cacheAllocation `json:",omitempty"`
|
|
Data cacheAllocation `json:",omitempty"`
|
|
}
|
|
|
|
// cacheAllocation is the basic interface for handling cache allocations of one
|
|
// type (unified, code, data)
|
|
type cacheAllocation interface {
|
|
Overlay(bitmask, uint64) (bitmask, error)
|
|
}
|
|
|
|
// catAbsoluteAllocation represents an explicitly specified cache allocation
|
|
// bitmask
|
|
type catAbsoluteAllocation bitmask
|
|
|
|
// catPctAllocation represents a relative (percentage) share of the available
|
|
// bitmask
|
|
type catPctAllocation uint64
|
|
|
|
// catPctRangeAllocation represents a percentage range of the available bitmask
|
|
type catPctRangeAllocation struct {
|
|
lowPct uint64
|
|
highPct uint64
|
|
}
|
|
|
|
// catSchemaType represents different L3 cache allocation schemes
|
|
type catSchemaType string
|
|
|
|
const (
|
|
// catSchemaTypeUnified is the schema type when CDP is not enabled
|
|
catSchemaTypeUnified catSchemaType = "unified"
|
|
// catSchemaTypeCode is the 'code' part of CDP schema
|
|
catSchemaTypeCode catSchemaType = "code"
|
|
// catSchemaTypeData is the 'data' part of CDP schema
|
|
catSchemaTypeData catSchemaType = "data"
|
|
)
|
|
|
|
// cat returns CAT options for the specified cache level.
|
|
func (o Options) cat(lvl cacheLevel) CatOptions {
|
|
switch lvl {
|
|
case L2:
|
|
return o.L2
|
|
case L3:
|
|
return o.L3
|
|
}
|
|
return CatOptions{}
|
|
}
|
|
|
|
func (t catSchemaType) toResctrlStr() string {
|
|
if t == catSchemaTypeUnified {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(string(t))
|
|
}
|
|
|
|
const (
|
|
mbSuffixPct = "%"
|
|
mbSuffixMbps = "MBps"
|
|
)
|
|
|
|
func newCatSchema(typ cacheLevel) catSchema {
|
|
return catSchema{
|
|
Lvl: typ,
|
|
Alloc: make(map[uint64]catAllocation),
|
|
}
|
|
}
|
|
|
|
// toStr returns the CAT schema in a format accepted by the Linux kernel
|
|
// resctrl (schemata) interface
|
|
func (s catSchema) toStr(typ catSchemaType, baseSchema catSchema) (string, error) {
|
|
schema := string(s.Lvl) + typ.toResctrlStr() + ":"
|
|
sep := ""
|
|
|
|
// Get a sorted slice of cache ids for deterministic output
|
|
ids := make([]uint64, 0, len(baseSchema.Alloc))
|
|
for id := range baseSchema.Alloc {
|
|
ids = append(ids, id)
|
|
}
|
|
utils.SortUint64s(ids)
|
|
|
|
minBits := info.cat[s.Lvl].minCbmBits()
|
|
for _, id := range ids {
|
|
baseMask, ok := baseSchema.Alloc[id].getEffective(typ).(catAbsoluteAllocation)
|
|
if !ok {
|
|
return "", fmt.Errorf("BUG: basemask not of type catAbsoluteAllocation")
|
|
}
|
|
bmask := bitmask(baseMask)
|
|
|
|
if s.Alloc != nil {
|
|
var err error
|
|
|
|
masks := s.Alloc[id]
|
|
overlayMask := masks.getEffective(typ)
|
|
|
|
bmask, err = overlayMask.Overlay(bmask, minBits)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
schema += fmt.Sprintf("%s%d=%x", sep, id, bmask)
|
|
sep = ";"
|
|
}
|
|
|
|
return schema + "\n", nil
|
|
}
|
|
|
|
func (a catAllocation) get(typ catSchemaType) cacheAllocation {
|
|
switch typ {
|
|
case catSchemaTypeCode:
|
|
return a.Code
|
|
case catSchemaTypeData:
|
|
return a.Data
|
|
}
|
|
return a.Unified
|
|
}
|
|
|
|
func (a catAllocation) set(typ catSchemaType, v cacheAllocation) catAllocation {
|
|
switch typ {
|
|
case catSchemaTypeCode:
|
|
a.Code = v
|
|
case catSchemaTypeData:
|
|
a.Data = v
|
|
default:
|
|
a.Unified = v
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
func (a catAllocation) getEffective(typ catSchemaType) cacheAllocation {
|
|
switch typ {
|
|
case catSchemaTypeCode:
|
|
if a.Code != nil {
|
|
return a.Code
|
|
}
|
|
case catSchemaTypeData:
|
|
if a.Data != nil {
|
|
return a.Data
|
|
}
|
|
}
|
|
// Use Unified as the default/fallback for Code and Data
|
|
return a.Unified
|
|
}
|
|
|
|
// Overlay function of the cacheAllocation interface
|
|
func (a catAbsoluteAllocation) Overlay(baseMask bitmask, minBits uint64) (bitmask, error) {
|
|
if err := verifyCatBaseMask(baseMask, minBits); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
shiftWidth := baseMask.lsbOne()
|
|
|
|
// Treat our bitmask relative to the basemask
|
|
bmask := bitmask(a) << shiftWidth
|
|
|
|
// Do bounds checking that we're "inside" the base mask
|
|
if bmask|baseMask != baseMask {
|
|
return 0, fmt.Errorf("bitmask %#x (%#x << %d) does not fit basemask %#x", bmask, a, shiftWidth, baseMask)
|
|
}
|
|
|
|
return bmask, nil
|
|
}
|
|
|
|
// MarshalJSON implements the Marshaler interface of "encoding/json"
|
|
func (a catAbsoluteAllocation) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf("\"%#x\"", a)), nil
|
|
}
|
|
|
|
// Overlay function of the cacheAllocation interface
|
|
func (a catPctAllocation) Overlay(baseMask bitmask, minBits uint64) (bitmask, error) {
|
|
return catPctRangeAllocation{highPct: uint64(a)}.Overlay(baseMask, minBits)
|
|
}
|
|
|
|
// Overlay function of the cacheAllocation interface
|
|
func (a catPctRangeAllocation) Overlay(baseMask bitmask, minBits uint64) (bitmask, error) {
|
|
if err := verifyCatBaseMask(baseMask, minBits); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
baseMaskMsb := uint64(baseMask.msbOne())
|
|
baseMaskLsb := uint64(baseMask.lsbOne())
|
|
baseMaskNumBits := baseMaskMsb - baseMaskLsb + 1
|
|
|
|
low, high := a.lowPct, a.highPct
|
|
if low == 0 {
|
|
low = 1
|
|
}
|
|
if low > high || low > 100 || high > 100 {
|
|
return 0, fmt.Errorf("invalid percentage range in %v", a)
|
|
}
|
|
|
|
// Convert percentage limits to bit numbers
|
|
// Our effective range is 1%-100%, use substraction (-1) because of
|
|
// arithmetics, so that we don't overflow on 100%
|
|
lsb := (low - 1) * baseMaskNumBits / 100
|
|
msb := (high - 1) * baseMaskNumBits / 100
|
|
|
|
// Make sure the number of bits set satisfies the minimum requirement
|
|
numBits := msb - lsb + 1
|
|
if numBits < minBits {
|
|
gap := minBits - numBits
|
|
|
|
// First, widen the mask from the "lsb end"
|
|
if gap <= lsb {
|
|
lsb -= gap
|
|
gap = 0
|
|
} else {
|
|
gap -= lsb
|
|
lsb = 0
|
|
}
|
|
// If needed, widen the mask from the "msb end"
|
|
msbAvailable := baseMaskNumBits - msb - 1
|
|
if gap <= msbAvailable {
|
|
msb += gap
|
|
} else {
|
|
return 0, fmt.Errorf("BUG: not enough bits available for cache bitmask (%v applied on basemask %#x)", a, baseMask)
|
|
}
|
|
}
|
|
|
|
value := ((1 << (msb - lsb + 1)) - 1) << (lsb + baseMaskLsb)
|
|
|
|
return bitmask(value), nil
|
|
}
|
|
|
|
func verifyCatBaseMask(baseMask bitmask, minBits uint64) error {
|
|
if baseMask == 0 {
|
|
return fmt.Errorf("empty basemask not allowed")
|
|
}
|
|
|
|
// Check that the basemask contains one (and only one) contiguous block of
|
|
// (enough) bits set
|
|
baseMaskWidth := baseMask.msbOne() - baseMask.lsbOne() + 1
|
|
if bits.OnesCount64(uint64(baseMask)) != baseMaskWidth {
|
|
return fmt.Errorf("invalid basemask %#x: more than one block of bits set", baseMask)
|
|
}
|
|
if uint64(bits.OnesCount64(uint64(baseMask))) < minBits {
|
|
return fmt.Errorf("invalid basemask %#x: fewer than %d bits set", baseMask, minBits)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON implements the Marshaler interface of "encoding/json"
|
|
func (a catPctAllocation) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf("\"%d%%\"", a)), nil
|
|
}
|
|
|
|
// MarshalJSON implements the Marshaler interface of "encoding/json"
|
|
func (a catPctRangeAllocation) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf("\"%d-%d%%\"", a.lowPct, a.highPct)), nil
|
|
}
|
|
|
|
// toStr returns the MB schema in a format accepted by the Linux kernel
|
|
// resctrl (schemata) interface
|
|
func (s mbSchema) toStr(base map[uint64]uint64) string {
|
|
schema := "MB:"
|
|
sep := ""
|
|
|
|
// Get a sorted slice of cache ids for deterministic output
|
|
ids := make([]uint64, 0, len(base))
|
|
for id := range base {
|
|
ids = append(ids, id)
|
|
}
|
|
utils.SortUint64s(ids)
|
|
|
|
for _, id := range ids {
|
|
baseAllocation := base[id]
|
|
value := uint64(0)
|
|
if info.mb.mbpsEnabled {
|
|
value = math.MaxUint32
|
|
if s != nil {
|
|
value = s[id]
|
|
}
|
|
// Limit to given base value
|
|
if value > baseAllocation {
|
|
value = baseAllocation
|
|
}
|
|
} else {
|
|
allocation := uint64(100)
|
|
if s != nil {
|
|
allocation = s[id]
|
|
}
|
|
value = allocation * baseAllocation / 100
|
|
// Guarantee minimum bw so that writing out the schemata does not fail
|
|
if value < info.mb.minBandwidth {
|
|
value = info.mb.minBandwidth
|
|
}
|
|
}
|
|
|
|
schema += fmt.Sprintf("%s%d=%d", sep, id, value)
|
|
sep = ";"
|
|
}
|
|
|
|
return schema + "\n"
|
|
}
|
|
|
|
// listStrToArray parses a string containing a human-readable list of numbers
|
|
// into an integer array
|
|
func listStrToArray(str string) ([]int, error) {
|
|
a := []int{}
|
|
|
|
// Empty list
|
|
if len(str) == 0 {
|
|
return a, nil
|
|
}
|
|
|
|
ranges := strings.Split(str, ",")
|
|
for _, ran := range ranges {
|
|
split := strings.SplitN(ran, "-", 2)
|
|
|
|
// We limit to 8 bits in order to avoid accidental super long slices
|
|
num, err := strconv.ParseInt(split[0], 10, 8)
|
|
if err != nil {
|
|
return a, fmt.Errorf("invalid integer %q: %v", str, err)
|
|
}
|
|
|
|
if len(split) == 1 {
|
|
a = append(a, int(num))
|
|
} else {
|
|
endNum, err := strconv.ParseInt(split[1], 10, 8)
|
|
if err != nil {
|
|
return a, fmt.Errorf("invalid integer in range %q: %v", str, err)
|
|
}
|
|
if endNum <= num {
|
|
return a, fmt.Errorf("invalid integer range %q in %q", ran, str)
|
|
}
|
|
for i := num; i <= endNum; i++ {
|
|
a = append(a, int(i))
|
|
}
|
|
}
|
|
}
|
|
sort.Ints(a)
|
|
return a, nil
|
|
}
|
|
|
|
// resolve tries to resolve the requested configuration into a working
|
|
// configuration
|
|
func (c *Config) resolve() (config, error) {
|
|
var err error
|
|
conf := config{Options: c.Options}
|
|
|
|
grclog.DebugBlock(log, "resolving configuration:", " ", "%s", utils.DumpJSON(c))
|
|
|
|
conf.Partitions, err = c.resolvePartitions()
|
|
if err != nil {
|
|
return conf, err
|
|
}
|
|
|
|
conf.Classes, err = c.resolveClasses()
|
|
|
|
return conf, err
|
|
}
|
|
|
|
// resolvePartitions tries to resolve the requested resource allocations of
|
|
// partitions
|
|
func (c *Config) resolvePartitions() (partitionSet, error) {
|
|
// Initialize empty partition configuration
|
|
conf := make(partitionSet, len(c.Partitions))
|
|
for name := range c.Partitions {
|
|
conf[name] = &partitionConfig{
|
|
CAT: map[cacheLevel]catSchema{
|
|
L2: newCatSchema(L2),
|
|
L3: newCatSchema(L3),
|
|
},
|
|
MB: make(mbSchema, len(info.mb.cacheIds))}
|
|
}
|
|
|
|
// Resolve L2 partition allocations
|
|
err := c.resolveCatPartitions(L2, conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try to resolve L3 partition allocations
|
|
err = c.resolveCatPartitions(L3, conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Try to resolve MB partition allocations
|
|
err = c.resolveMBPartitions(conf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conf, nil
|
|
}
|
|
|
|
// resolveCatPartitions tries to resolve requested cache allocations between partitions
|
|
func (c *Config) resolveCatPartitions(lvl cacheLevel, conf partitionSet) error {
|
|
// Resolve partitions in sorted order for reproducibility
|
|
names := make([]string, 0, len(c.Partitions))
|
|
for name := range c.Partitions {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
resolver := newCacheResolver(lvl, names)
|
|
|
|
// Parse requested allocations from user config and load the resolver
|
|
for _, name := range names {
|
|
var allocations catSchema
|
|
var err error
|
|
switch lvl {
|
|
case L2:
|
|
allocations, err = c.Partitions[name].L2Allocation.toSchema(L2)
|
|
case L3:
|
|
allocations, err = c.Partitions[name].L3Allocation.toSchema(L3)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse %s allocation request for partition %q: %v", lvl, name, err)
|
|
}
|
|
|
|
resolver.requests[name] = allocations.Alloc
|
|
}
|
|
|
|
// Run resolver fo partition allocations
|
|
grants, err := resolver.resolve()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if grants == nil {
|
|
log.Debugf("%s allocation disabled for all partitions", lvl)
|
|
return nil
|
|
}
|
|
|
|
for name, grant := range grants {
|
|
conf[name].CAT[lvl] = grant
|
|
}
|
|
|
|
heading := fmt.Sprintf("actual (and requested) %s allocations per partition and cache id:", lvl)
|
|
infoStr := ""
|
|
for name, partition := range resolver.requests {
|
|
infoStr += name + "\n"
|
|
for _, id := range resolver.ids {
|
|
infoStr += fmt.Sprintf(" %2d: ", id)
|
|
allocationReq := partition[id]
|
|
for _, typ := range []catSchemaType{catSchemaTypeUnified, catSchemaTypeCode, catSchemaTypeData} {
|
|
infoStr += string(typ) + " "
|
|
requested := allocationReq.get(typ)
|
|
switch v := requested.(type) {
|
|
case catAbsoluteAllocation:
|
|
infoStr += fmt.Sprintf("<absolute %#x> ", v)
|
|
case catPctAllocation:
|
|
granted := grants[name].Alloc[id].get(typ).(catAbsoluteAllocation)
|
|
requestedPct := fmt.Sprintf("(%d%%)", v)
|
|
truePct := float64(bits.OnesCount64(uint64(granted))) * 100 / float64(resolver.bitsTotal)
|
|
infoStr += fmt.Sprintf("%5.1f%% %-6s ", truePct, requestedPct)
|
|
case nil:
|
|
infoStr += "<not specified> "
|
|
}
|
|
}
|
|
infoStr += "\n"
|
|
}
|
|
}
|
|
grclog.DebugBlock(log, heading, " ", "%s", infoStr)
|
|
|
|
return nil
|
|
}
|
|
|
|
// cacheResolver is a helper for resolving exclusive (partition) cache // allocation requests
|
|
type cacheResolver struct {
|
|
lvl cacheLevel
|
|
ids []uint64
|
|
minBits uint64
|
|
bitsTotal uint64
|
|
partitions []string
|
|
requests map[string]catSchemaRaw
|
|
grants map[string]catSchema
|
|
}
|
|
|
|
func newCacheResolver(lvl cacheLevel, partitions []string) *cacheResolver {
|
|
r := &cacheResolver{
|
|
lvl: lvl,
|
|
ids: info.cat[lvl].cacheIds,
|
|
minBits: info.cat[lvl].minCbmBits(),
|
|
bitsTotal: uint64(info.cat[lvl].cbmMask().lsbZero()),
|
|
partitions: partitions,
|
|
requests: make(map[string]catSchemaRaw, len(partitions)),
|
|
grants: make(map[string]catSchema, len(partitions))}
|
|
|
|
for _, p := range partitions {
|
|
r.grants[p] = catSchema{Lvl: lvl, Alloc: make(catSchemaRaw, len(r.ids))}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (r *cacheResolver) resolve() (map[string]catSchema, error) {
|
|
for _, id := range r.ids {
|
|
err := r.resolveID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return r.grants, nil
|
|
}
|
|
|
|
// resolveCacheID resolves the partition allocations for one cache id
|
|
func (r *cacheResolver) resolveID(id uint64) error {
|
|
for _, typ := range []catSchemaType{catSchemaTypeUnified, catSchemaTypeCode, catSchemaTypeData} {
|
|
log.Debugf("resolving partitions for %q schema for cache id %d", typ, id)
|
|
err := r.resolveType(id, typ)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// resolveType resolve one schema type for one cache id
|
|
func (r *cacheResolver) resolveType(id uint64, typ catSchemaType) error {
|
|
// Sanity check: if any partition has l3 allocation of this schema type
|
|
// configured check that all other partitions have it, too
|
|
nils := []string{}
|
|
for _, partition := range r.partitions {
|
|
if r.requests[partition][id].get(typ) == nil {
|
|
nils = append(nils, partition)
|
|
}
|
|
}
|
|
if len(nils) > 0 && len(nils) != len(r.partitions) {
|
|
return fmt.Errorf("some partitions (%s) missing %s %q allocation request for cache id %d",
|
|
strings.Join(nils, ", "), r.lvl, typ, id)
|
|
}
|
|
|
|
// Act depending on the type of the first request in the list
|
|
a := r.requests[r.partitions[0]][id].get(typ)
|
|
switch a.(type) {
|
|
case catAbsoluteAllocation:
|
|
return r.resolveAbsolute(id, typ)
|
|
case nil:
|
|
default:
|
|
return r.resolveRelative(id, typ)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *cacheResolver) resolveRelative(id uint64, typ catSchemaType) error {
|
|
type reqHelper struct {
|
|
name string
|
|
req uint64
|
|
}
|
|
|
|
// Sanity check:
|
|
// 1. allocation requests are of the same type (relative)
|
|
// 2. total allocation requested for this cache id does not exceed 100 percent
|
|
// Additionally fill a helper structure for sorting partitions
|
|
percentageTotal := uint64(0)
|
|
reqs := make([]reqHelper, 0, len(r.partitions))
|
|
for _, partition := range r.partitions {
|
|
switch a := r.requests[partition][id].get(typ).(type) {
|
|
case catPctAllocation:
|
|
percentageTotal += uint64(a)
|
|
reqs = append(reqs, reqHelper{name: partition, req: uint64(a)})
|
|
case catAbsoluteAllocation:
|
|
return fmt.Errorf("error resolving %s allocation for cache id %d: mixing "+
|
|
"relative and absolute allocations between partitions not supported", r.lvl, id)
|
|
case catPctRangeAllocation:
|
|
return fmt.Errorf("percentage ranges in partition allocation not supported")
|
|
default:
|
|
return fmt.Errorf("BUG: unknown cacheAllocation type %T", a)
|
|
}
|
|
}
|
|
if percentageTotal < 100 {
|
|
log.Infof("requested total %s %q partition allocation for cache id %d <100%% (%d%%)", r.lvl, typ, id, percentageTotal)
|
|
} else if percentageTotal > 100 {
|
|
return fmt.Errorf("accumulated %s %q partition allocation requests for cache id %d exceeds 100%% (%d%%)", r.lvl, typ, id, percentageTotal)
|
|
}
|
|
|
|
// Sort partition allocations. We want to resolve smallest allocations
|
|
// first in order to try to ensure that all allocations can be satisfied
|
|
// because small percentages might need to be rounded up
|
|
sort.Slice(reqs, func(i, j int) bool {
|
|
return reqs[i].req < reqs[j].req
|
|
})
|
|
|
|
// Calculate number of bits granted to each partition.
|
|
grants := make(map[string]uint64, len(r.partitions))
|
|
bitsTotal := percentageTotal * uint64(r.bitsTotal) / 100
|
|
bitsAvailable := bitsTotal
|
|
for i, req := range reqs {
|
|
percentageAvailable := bitsAvailable * percentageTotal / bitsTotal
|
|
|
|
// This might happen e.g. if number of partitions would be greater
|
|
// than the total number of bits
|
|
if bitsAvailable < r.minBits {
|
|
return fmt.Errorf("unable to resolve %s allocation for cache id %d, not enough exlusive bits available", r.lvl, id)
|
|
}
|
|
|
|
// Use integer arithmetics, effectively always rounding down
|
|
// fractional allocations i.e. trying to avoid over-allocation
|
|
numBits := req.req * bitsAvailable / percentageAvailable
|
|
|
|
// Guarantee a non-zero allocation
|
|
if numBits < r.minBits {
|
|
numBits = r.minBits
|
|
}
|
|
// Don't overflow, allocate all remaining bits to the last partition
|
|
if numBits > bitsAvailable || i == len(reqs)-1 {
|
|
numBits = bitsAvailable
|
|
}
|
|
|
|
grants[req.name] = numBits
|
|
bitsAvailable -= numBits
|
|
}
|
|
|
|
// Construct the actual bitmasks for each partition
|
|
lsbID := uint64(0)
|
|
for _, partition := range r.partitions {
|
|
// Compose the actual bitmask
|
|
v := r.grants[partition].Alloc[id].set(typ, catAbsoluteAllocation(bitmask(((1<<grants[partition])-1)<<lsbID)))
|
|
r.grants[partition].Alloc[id] = v
|
|
|
|
lsbID += grants[partition]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *cacheResolver) resolveAbsolute(id uint64, typ catSchemaType) error {
|
|
// Just sanity check:
|
|
// 1. allocation requests of the correct type (absolute)
|
|
// 2. allocations do not overlap
|
|
mask := bitmask(0)
|
|
for _, partition := range r.partitions {
|
|
a, ok := r.requests[partition][id].get(typ).(catAbsoluteAllocation)
|
|
if !ok {
|
|
return fmt.Errorf("error resolving %s allocation for cache id %d: mixing absolute and relative allocations between partitions not supported", r.lvl, id)
|
|
}
|
|
if bitmask(a)&mask > 0 {
|
|
return fmt.Errorf("overlapping %s partition allocation requests for cache id %d", r.lvl, id)
|
|
}
|
|
mask |= bitmask(a)
|
|
|
|
r.grants[partition].Alloc[id] = r.grants[partition].Alloc[id].set(typ, a)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// resolveMBPartitions tries to resolve requested MB allocations between partitions
|
|
func (c *Config) resolveMBPartitions(conf partitionSet) error {
|
|
// We use percentage values directly from the user conf
|
|
for name, partition := range c.Partitions {
|
|
allocations, err := partition.MBAllocation.toSchema()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve MB allocation for partition %q: %v", name, err)
|
|
}
|
|
for id, allocation := range allocations {
|
|
conf[name].MB[id] = allocation
|
|
// Check that we don't go under the minimum allowed bandwidth setting
|
|
if !info.mb.mbpsEnabled && allocation < info.mb.minBandwidth {
|
|
conf[name].MB[id] = info.mb.minBandwidth
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// resolveClasses tries to resolve class allocations of all partitions
|
|
func (c *Config) resolveClasses() (classSet, error) {
|
|
classes := make(classSet)
|
|
|
|
for bname, partition := range c.Partitions {
|
|
for gname, class := range partition.Classes {
|
|
gname = unaliasClassName(gname)
|
|
|
|
if !IsQualifiedClassName(gname) {
|
|
return classes, fmt.Errorf("unqualified class name %q (must not be '.' or '..' and must not contain '/' or newline)", gname)
|
|
}
|
|
if _, ok := classes[gname]; ok {
|
|
return classes, fmt.Errorf("class names must be unique, %q defined multiple times", gname)
|
|
}
|
|
|
|
var err error
|
|
gc := &classConfig{Partition: bname,
|
|
CATSchema: make(map[cacheLevel]catSchema),
|
|
Kubernetes: class.Kubernetes}
|
|
|
|
gc.CATSchema[L2], err = class.L2Allocation.toSchema(L2)
|
|
if err != nil {
|
|
return classes, fmt.Errorf("failed to resolve L2 allocation for class %q: %v", gname, err)
|
|
}
|
|
if gc.CATSchema[L2].Alloc != nil && partition.L2Allocation == nil {
|
|
return classes, fmt.Errorf("L2 allocation missing from partition %q but class %q specifies L2 schema", bname, gname)
|
|
}
|
|
|
|
gc.CATSchema[L3], err = class.L3Allocation.toSchema(L3)
|
|
if err != nil {
|
|
return classes, fmt.Errorf("failed to resolve L3 allocation for class %q: %v", gname, err)
|
|
}
|
|
if gc.CATSchema[L3].Alloc != nil && partition.L3Allocation == nil {
|
|
return classes, fmt.Errorf("L3 allocation missing from partition %q but class %q specifies L3 schema", bname, gname)
|
|
}
|
|
|
|
gc.MBSchema, err = class.MBAllocation.toSchema()
|
|
if err != nil {
|
|
return classes, fmt.Errorf("failed to resolve MB allocation for class %q: %v", gname, err)
|
|
}
|
|
if gc.MBSchema != nil && partition.MBAllocation == nil {
|
|
return classes, fmt.Errorf("MB allocation missing from partition %q but class %q specifies MB schema", bname, gname)
|
|
}
|
|
|
|
classes[gname] = gc
|
|
}
|
|
}
|
|
|
|
return classes, nil
|
|
}
|
|
|
|
// toSchema converts a cache allocation config to effective allocation schema covering all cache IDs
|
|
func (c CatConfig) toSchema(lvl cacheLevel) (catSchema, error) {
|
|
if c == nil {
|
|
return catSchema{Lvl: lvl}, nil
|
|
}
|
|
|
|
allocations := newCatSchema(lvl)
|
|
minBits := info.cat[lvl].minCbmBits()
|
|
|
|
d, ok := c[CacheIdAll]
|
|
if !ok {
|
|
d = CacheIdCatConfig{Unified: "100%"}
|
|
}
|
|
defaultVal, err := d.parse(minBits)
|
|
if err != nil {
|
|
return allocations, err
|
|
}
|
|
|
|
// Pre-fill with defaults
|
|
for _, i := range info.cat[lvl].cacheIds {
|
|
allocations.Alloc[i] = defaultVal
|
|
}
|
|
|
|
for key, val := range c {
|
|
if key == CacheIdAll {
|
|
continue
|
|
}
|
|
|
|
ids, err := listStrToArray(key)
|
|
if err != nil {
|
|
return allocations, err
|
|
}
|
|
|
|
schemaVal, err := val.parse(minBits)
|
|
if err != nil {
|
|
return allocations, err
|
|
}
|
|
|
|
for _, id := range ids {
|
|
if _, ok := allocations.Alloc[uint64(id)]; ok {
|
|
allocations.Alloc[uint64(id)] = schemaVal
|
|
}
|
|
}
|
|
}
|
|
|
|
return allocations, nil
|
|
}
|
|
|
|
// catConfig is a helper for unmarshalling CatConfig
|
|
type catConfig CatConfig
|
|
|
|
// UnmarshalJSON implements the Unmarshaler interface of "encoding/json"
|
|
func (c *CatConfig) UnmarshalJSON(data []byte) error {
|
|
raw := new(interface{})
|
|
|
|
err := json.Unmarshal(data, raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conf := CatConfig{}
|
|
switch v := (*raw).(type) {
|
|
case string:
|
|
conf[CacheIdAll] = CacheIdCatConfig{Unified: CacheProportion(v)}
|
|
default:
|
|
// Use the helper type to avoid infinite recursion
|
|
helper := catConfig{}
|
|
if err := json.Unmarshal(data, &helper); err != nil {
|
|
return err
|
|
}
|
|
for k, v := range helper {
|
|
conf[k] = v
|
|
}
|
|
}
|
|
*c = conf
|
|
return nil
|
|
}
|
|
|
|
// toSchema converts an MB allocation config to effective allocation schema covering all cache IDs
|
|
func (c MbaConfig) toSchema() (mbSchema, error) {
|
|
if c == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
d, ok := c[CacheIdAll]
|
|
if !ok {
|
|
d = CacheIdMbaConfig{"100" + mbSuffixPct, "4294967295" + mbSuffixMbps}
|
|
}
|
|
defaultVal, err := d.parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allocations := make(mbSchema, len(info.mb.cacheIds))
|
|
// Pre-fill with defaults
|
|
for _, i := range info.mb.cacheIds {
|
|
allocations[i] = defaultVal
|
|
}
|
|
|
|
for key, val := range c {
|
|
if key == CacheIdAll {
|
|
continue
|
|
}
|
|
|
|
ids, err := listStrToArray(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
schemaVal, err := val.parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, id := range ids {
|
|
if _, ok := allocations[uint64(id)]; ok {
|
|
allocations[uint64(id)] = schemaVal
|
|
}
|
|
}
|
|
}
|
|
|
|
return allocations, nil
|
|
}
|
|
|
|
// mbaConfig is a helper for unmarshalling MbaConfig
|
|
type mbaConfig MbaConfig
|
|
|
|
// UnmarshalJSON implements the Unmarshaler interface of "encoding/json"
|
|
func (c *MbaConfig) UnmarshalJSON(data []byte) error {
|
|
raw := new(interface{})
|
|
|
|
err := json.Unmarshal(data, raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conf := MbaConfig{}
|
|
switch (*raw).(type) {
|
|
case []interface{}:
|
|
helper := CacheIdMbaConfig{}
|
|
if err := json.Unmarshal(data, &helper); err != nil {
|
|
return err
|
|
}
|
|
conf[CacheIdAll] = helper
|
|
default:
|
|
// Use the helper type to avoid infinite recursion
|
|
helper := mbaConfig{}
|
|
if err := json.Unmarshal(data, &helper); err != nil {
|
|
return err
|
|
}
|
|
for k, v := range helper {
|
|
conf[k] = v
|
|
}
|
|
}
|
|
*c = conf
|
|
return nil
|
|
}
|
|
|
|
// parse per cache-id CAT configuration into an effective allocation to be used
|
|
// in the CAT schema
|
|
func (c *CacheIdCatConfig) parse(minBits uint64) (catAllocation, error) {
|
|
var err error
|
|
allocation := catAllocation{}
|
|
|
|
allocation.Unified, err = c.Unified.parse(minBits)
|
|
if err != nil {
|
|
return allocation, err
|
|
}
|
|
allocation.Code, err = c.Code.parse(minBits)
|
|
if err != nil {
|
|
return allocation, err
|
|
}
|
|
allocation.Data, err = c.Data.parse(minBits)
|
|
if err != nil {
|
|
return allocation, err
|
|
}
|
|
|
|
// Sanity check for the configuration
|
|
if allocation.Unified == nil {
|
|
return allocation, fmt.Errorf("'unified' not specified in cache schema %s", *c)
|
|
}
|
|
if allocation.Code != nil && allocation.Data == nil {
|
|
return allocation, fmt.Errorf("'code' specified but missing 'data' from cache schema %s", *c)
|
|
}
|
|
if allocation.Code == nil && allocation.Data != nil {
|
|
return allocation, fmt.Errorf("'data' specified but missing 'code' from cache schema %s", *c)
|
|
}
|
|
|
|
return allocation, nil
|
|
}
|
|
|
|
// cacheIdCatConfig is a helper for unmarshalling CacheIdCatConfig
|
|
type cacheIdCatConfig CacheIdCatConfig
|
|
|
|
// UnmarshalJSON implements the Unmarshaler interface of "encoding/json"
|
|
func (c *CacheIdCatConfig) UnmarshalJSON(data []byte) error {
|
|
raw := new(interface{})
|
|
|
|
err := json.Unmarshal(data, raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conf := CacheIdCatConfig{}
|
|
switch v := (*raw).(type) {
|
|
case string:
|
|
conf.Unified = CacheProportion(v)
|
|
default:
|
|
// Use the helper type to avoid infinite recursion
|
|
helper := cacheIdCatConfig{}
|
|
if err := json.Unmarshal(data, &helper); err != nil {
|
|
return err
|
|
}
|
|
conf.Unified = helper.Unified
|
|
conf.Code = helper.Code
|
|
conf.Data = helper.Data
|
|
}
|
|
*c = conf
|
|
return nil
|
|
}
|
|
|
|
// parse converts a per cache-id MBA configuration into effective value
|
|
// to be used in the MBA schema
|
|
func (c *CacheIdMbaConfig) parse() (uint64, error) {
|
|
for _, v := range *c {
|
|
str := string(v)
|
|
if strings.HasSuffix(str, mbSuffixPct) {
|
|
if !info.mb.mbpsEnabled {
|
|
value, err := strconv.ParseUint(strings.TrimSuffix(str, mbSuffixPct), 10, 7)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return value, nil
|
|
}
|
|
} else if strings.HasSuffix(str, mbSuffixMbps) {
|
|
if info.mb.mbpsEnabled {
|
|
value, err := strconv.ParseUint(strings.TrimSuffix(str, mbSuffixMbps), 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return value, nil
|
|
}
|
|
} else {
|
|
log.Warnf("unrecognized MBA allocation unit in %q", str)
|
|
}
|
|
}
|
|
|
|
// No value for the active mode was specified
|
|
if info.mb.mbpsEnabled {
|
|
return 0, fmt.Errorf("missing 'MBps' value from mbSchema; required because 'mba_MBps' is enabled in the system")
|
|
}
|
|
return 0, fmt.Errorf("missing '%%' value from mbSchema; required because percentage-based MBA allocation is enabled in the system")
|
|
}
|
|
|
|
// parse converts a string value into cacheAllocation type
|
|
func (c CacheProportion) parse(minBits uint64) (cacheAllocation, error) {
|
|
if c == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
if c[len(c)-1] == '%' {
|
|
// Percentages of the max number of bits
|
|
split := strings.SplitN(string(c)[0:len(c)-1], "-", 2)
|
|
var allocation cacheAllocation
|
|
|
|
if len(split) == 1 {
|
|
pct, err := strconv.ParseUint(split[0], 10, 7)
|
|
if err != nil {
|
|
return allocation, err
|
|
}
|
|
if pct > 100 {
|
|
return allocation, fmt.Errorf("invalid percentage value %q", c)
|
|
}
|
|
allocation = catPctAllocation(pct)
|
|
} else {
|
|
low, err := strconv.ParseUint(split[0], 10, 7)
|
|
if err != nil {
|
|
return allocation, err
|
|
}
|
|
high, err := strconv.ParseUint(split[1], 10, 7)
|
|
if err != nil {
|
|
return allocation, err
|
|
}
|
|
if low > high || low > 100 || high > 100 {
|
|
return allocation, fmt.Errorf("invalid percentage range %q", c)
|
|
}
|
|
allocation = catPctRangeAllocation{lowPct: low, highPct: high}
|
|
}
|
|
|
|
return allocation, nil
|
|
}
|
|
|
|
// Absolute allocation
|
|
var value uint64
|
|
var err error
|
|
if strings.HasPrefix(string(c), "0x") {
|
|
// Hex value
|
|
value, err = strconv.ParseUint(string(c[2:]), 16, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Last, try "list" format (i.e. smthg like 0,2,5-9,...)
|
|
tmp, err := listStrToBitmask(string(c))
|
|
value = uint64(tmp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Sanity check of absolute allocation: bitmask must (only) contain one
|
|
// contiguous block of ones wide enough
|
|
numOnes := bits.OnesCount64(value)
|
|
if numOnes != bits.Len64(value)-bits.TrailingZeros64(value) {
|
|
return nil, fmt.Errorf("invalid cache bitmask %q: more than one continuous block of ones", c)
|
|
}
|
|
if uint64(numOnes) < minBits {
|
|
return nil, fmt.Errorf("invalid cache bitmask %q: number of bits less than %d", c, minBits)
|
|
}
|
|
|
|
return catAbsoluteAllocation(value), nil
|
|
}
|