| # SPDX-License-Identifier: GPL-2.0 |
| |
| """ |
| Contains core functions for DAMON control. |
| """ |
| |
| import collections |
| import copy |
| import json |
| import os |
| import random |
| import subprocess |
| import time |
| |
| import _damo_fmt_str |
| |
| # Core data structures |
| |
| class OpsAttrs: |
| use_reports = None |
| write_only = None |
| cpus = None #cpus list string |
| tids = None #thread ids list string |
| |
| def __init__(self, use_reports=False, write_only=False, cpus=None, |
| tids=None): |
| self.use_reports = _damo_fmt_str.text_to_bool(use_reports) |
| self.write_only = _damo_fmt_str.text_to_bool(write_only) |
| self.cpus = cpus if cpus is not None else 'all' |
| self.tids = tids if tids is not None else '' |
| |
| def to_str(self, raw): |
| return 'use_reports: %s, write_only: %s, cpus: %s, threads: %s' % ( |
| self.use_reports, self.write_only, self.cpus, self.tids) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and \ |
| self.use_reports == other.use_reports and \ |
| self.write_only == other.write_only and \ |
| self.cpus == other.cpus and \ |
| self.tids == other.tids |
| |
| @classmethod |
| def from_kvpairs(cls, kvpairs): |
| use_reports = kvpairs['use_reports'] \ |
| if 'use_reports' in kvpiars else False |
| write_only = kvpairs['write_only'] \ |
| if 'write_only' in kvpairs else False |
| cpus = kvpairs['cpus'] if 'cpus' in kvpairs else 'all' |
| tids = kvpairs['tids'] if 'tids' in kvpairs else '' |
| return OpsAttrs(use_reports=use_reports, write_only=write_only, |
| cpus=cpus, tids=tids) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('use_reports', self.use_reports), |
| ('write_only', self.write_only), |
| ('cpus', self.cpus), |
| ('tids', self.tids), |
| ]) |
| |
| class DamonIntervalsGoal: |
| access_bp = None |
| aggrs = None |
| min_sample_us = None |
| max_sample_us = None |
| |
| def __init__(self, access_bp='0%', aggrs=0, min_sample_us=0, max_sample_us=0): |
| self.access_bp = _damo_fmt_str.text_to_bp(access_bp) |
| self.aggrs = _damo_fmt_str.text_to_nr(aggrs) |
| self.min_sample_us = _damo_fmt_str.text_to_us(min_sample_us) |
| self.max_sample_us = _damo_fmt_str.text_to_us(max_sample_us) |
| |
| def to_str(self, raw): |
| return 'target %s accesses per %s aggrs, [%s, %s] sampling interval' % ( |
| _damo_fmt_str.format_bp(self.access_bp, raw), |
| _damo_fmt_str.format_nr(self.aggrs, raw), |
| _damo_fmt_str.format_time_us(self.min_sample_us, raw), |
| _damo_fmt_str.format_time_us(self.max_sample_us, raw)) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| @classmethod |
| def from_kvpairs(cls, kvpairs): |
| return DamonIntervalsGoal( |
| kvpairs['access_bp'], kvpairs['aggrs'], |
| kvpairs['min_sample_us'], kvpairs['max_sample_us']) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('access_bp', _damo_fmt_str.format_bp(self.access_bp, raw)), |
| ('aggrs', _damo_fmt_str.format_nr(self.aggrs, raw)), |
| ('min_sample_us', _damo_fmt_str.format_time_us( |
| self.min_sample_us, raw)), |
| ('max_sample_us', _damo_fmt_str.format_time_us( |
| self.max_sample_us, raw)), |
| ]) |
| |
| def enabled(self): |
| return self.aggrs != 0 |
| |
| class DamonIntervals: |
| sample = None |
| aggr = None |
| ops_update = None |
| intervals_goal = None |
| |
| def __init__(self, sample='5ms', aggr='100ms', ops_update='1s', |
| intervals_goal=DamonIntervalsGoal()): |
| self.sample = _damo_fmt_str.text_to_us(sample) |
| self.aggr = _damo_fmt_str.text_to_us(aggr) |
| self.ops_update = _damo_fmt_str.text_to_us(ops_update) |
| self.intervals_goal = intervals_goal |
| |
| def to_str(self, raw): |
| lines = ['sample %s, aggr %s, update %s' % ( |
| _damo_fmt_str.format_time_us(self.sample, raw), |
| _damo_fmt_str.format_time_us(self.aggr, raw), |
| _damo_fmt_str.format_time_us(self.ops_update, raw))] |
| if self.intervals_goal.enabled(): |
| lines.append('%s' % self.intervals_goal.to_str(raw)) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| @classmethod |
| def from_kvpairs(cls, kvpairs): |
| if not 'intervals_goal' in kvpairs: |
| return DamonIntervals( |
| kvpairs['sample_us'], kvpairs['aggr_us'], |
| kvpairs['ops_update_us']) |
| return DamonIntervals( |
| kvpairs['sample_us'], kvpairs['aggr_us'], |
| kvpairs['ops_update_us'], |
| DamonIntervalsGoal.from_kvpairs(kvpairs['intervals_goal'])) |
| |
| def to_kvpairs(self, raw=False, omit_defaults=False): |
| kvp = collections.OrderedDict([ |
| ('sample_us', _damo_fmt_str.format_time_us(self.sample, raw)), |
| ('aggr_us', _damo_fmt_str.format_time_us(self.aggr, raw)), |
| ('ops_update_us', |
| _damo_fmt_str.format_time_us(self.ops_update, raw))]) |
| if not omit_defaults or self.intervals_goal != DamonIntervalsGoal(): |
| kvp['intervals_goal'] = self.intervals_goal.to_kvpairs(raw) |
| return kvp |
| |
| class DamonNrRegionsRange: |
| minimum = None |
| maximum = None |
| |
| def __init__(self, min_=10, max_=1000): |
| self.minimum = _damo_fmt_str.text_to_nr(min_) |
| self.maximum = _damo_fmt_str.text_to_nr(max_) |
| |
| def to_str(self, raw): |
| return '[%s, %s]' % ( |
| _damo_fmt_str.format_nr(self.minimum, raw), |
| _damo_fmt_str.format_nr(self.maximum, raw)) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| @classmethod |
| def from_kvpairs(cls, kvpairs): |
| return DamonNrRegionsRange(kvpairs['min'], kvpairs['max']) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('min', _damo_fmt_str.format_nr(self.minimum, raw)), |
| ('max', _damo_fmt_str.format_nr(self.maximum, raw)), |
| ]) |
| |
| damon_filter_type_cpumask = 'cpumask' |
| damon_filter_type_threads = 'threads' |
| damon_filter_type_write = 'write' |
| |
| class DamonSampleFilter: |
| filter_type = None |
| matching = None |
| allow = None |
| cpumask = None # the kernel-accepting cpumask string or None |
| tid_arr = None # the kernel-accepting integer array string or None |
| |
| def __init__(self, filter_type, matching, allow, cpumask=None, |
| tid_arr=None): |
| self.filter_type = filter_type |
| self.matching = matching |
| self.allow = allow |
| self.cpumask = cpumask |
| self.tid_arr = tid_arr |
| |
| def to_str(self, raw): |
| words = [] |
| if self.allow: |
| words.append('allow') |
| else: |
| words.append('reject') |
| if self.matching is False: |
| words.append('none') |
| words.append(self.filter_type) |
| if self.filter_type == damon_filter_type_cpumask: |
| words.append(self.cpumask) |
| elif self.filter_type == damon_filter_type_threads: |
| words.append(self.tid_arr) |
| return ' '.join(words) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and \ |
| self.filter_type == other.filter_type and \ |
| self.matching == other.matching and \ |
| self.allow == other.allow and \ |
| self.cpumask == other.cpumask and self.tid_arr == other.tid_arr |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| return DamonSampleFilter( |
| filter_type=kv['filter_type'], matching=kv['matching'], |
| allow=kv['allow'], cpumask=kv['cpumask'], |
| tid_arr=kv['tid_arr']) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('filter_type', self.filter_type), |
| ('matching', self.matching), |
| ('allow', self.allow), |
| ('cpumask', self.cpumask), |
| ('tid_arr', self.tid_arr), |
| ]) |
| |
| class DamonPrimitivesEnabled: |
| page_table = None |
| page_fault = None |
| |
| def __init__(self, page_table=True, page_fault=False): |
| self.page_table = _damo_fmt_str.text_to_bool(page_table) |
| self.page_fault = _damo_fmt_str.text_to_bool(page_fault) |
| |
| def to_str(self, raw): |
| words = [] |
| if self.page_table is True: |
| words.append('page_table') |
| if self.page_fault is True: |
| words.append('page_fault') |
| return ', '.join(words) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and \ |
| self.page_table == other.page_table and \ |
| self.page_fault == other.page_fault |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| return DamonPrimitivesEnabled( |
| page_table=kv['page_table'], page_fault=kv['page_fault']) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('page_table', self.page_table), |
| ('page_fault', self.page_fault), |
| ]) |
| |
| class DamonSampleControl: |
| primitives_enabled = None |
| sample_filters = None |
| |
| def __init__(self, primitives_enabled=None, sample_filters=None): |
| if primitives_enabled is None: |
| primitives_enabled = DamonPrimitivesEnabled() |
| if sample_filters is None: |
| sample_filters = [] |
| self.primitives_enabled = primitives_enabled |
| self.sample_filters = sample_filters |
| |
| def to_str(self, raw): |
| lines = [ |
| 'enabled primitives: %s' % self.primitives_enabled.to_str(raw)] |
| if len(self.sample_filters) > 0: |
| lines.append('Filters') |
| for filter in self.sample_filters: |
| lines.append('- %s' % filter.to_str(raw)) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and \ |
| self.primitives_enabled == other.primitives_enabled and \ |
| self.sample_filters == other.sample_filters |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| return DamonSampleControl( |
| primitives_enabled=DamonPrimitivesEnabled.from_kvpairs( |
| kv['primitives_enabled']), |
| sample_filters=[DamonSampleFilter.from_kvpairs(kvpairs) |
| for kvpairs in kv['sample_filters']]) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('primitives_enabled', self.primitives_enabled.to_kvpairs(raw)), |
| ('sample_filters', [ |
| f.to_kvpairs(raw) for f in self.sample_filters]), |
| ]) |
| |
| unit_percent = 'percent' |
| unit_samples = 'samples' |
| unit_usec = 'usec' |
| unit_aggr_intervals = 'aggr_intervals' |
| |
| class DamonNrAccesses: |
| samples = None |
| percent = None |
| hz = None |
| |
| def __init__(self, val, unit): |
| if val == None or unit == None: |
| return |
| if unit == unit_samples: |
| self.samples = _damo_fmt_str.text_to_nr(val) |
| elif unit == unit_percent: |
| self.percent = _damo_fmt_str.text_to_percent(val) |
| else: |
| raise Exception('invalid DamonNrAccesses unit \'%s\'' % unit) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and |
| ((self.samples != None and self.samples == other.samples) or |
| (self.percent != None and self.percent == other.percent) |
| )) |
| |
| def in_hz(self, aggr_us): |
| return self.samples / (aggr_us / 1000000) |
| |
| def in_percent(self, sample_us, aggr_us): |
| max_samples = aggr_us / sample_us |
| return self.samples * 100.0 / max_samples |
| |
| def add_unset_unit(self, intervals): |
| if self.samples != None and self.percent != None: |
| return |
| max_val = intervals.aggr / intervals.sample |
| if self.samples == None: |
| self.samples = int(self.percent * max_val / 100) |
| if self.percent == None: |
| self.percent = int(self.samples * 100.0 / max_val) |
| |
| def to_str(self, unit, raw): |
| if unit == unit_percent: |
| return '%s %%' % (_damo_fmt_str.format_nr(self.percent, raw)) |
| elif unit == unit_samples: |
| return '%s %s' % (_damo_fmt_str.format_nr(self.samples, raw), |
| unit_samples) |
| raise Exception('unsupported unit for NrAccesses (%s)' % unit) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| ret = DamonNrAccesses(None, None) |
| if 'samples' in kv and kv['samples'] != None: |
| ret.samples = _damo_fmt_str.text_to_nr(kv['samples']) |
| if 'percent' in kv and kv['percent'] != None: |
| ret.percent = _damo_fmt_str.text_to_percent(kv['percent']) |
| return ret |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict( |
| [('samples', self.samples), ('percent', self.percent), |
| ]) |
| |
| class DamonAge: |
| usec = None |
| aggr_intervals = None |
| |
| def __init__(self, val, unit): |
| if val == None and unit != None: |
| self.unit = unit |
| return |
| if val == None and unit == None: |
| return |
| if unit == unit_usec: |
| self.usec = _damo_fmt_str.text_to_us(val) |
| elif unit == unit_aggr_intervals: |
| self.aggr_intervals = _damo_fmt_str.text_to_nr(val) |
| else: |
| raise Exception('DamonAge unsupported unit (%s)' % unit) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and |
| ((self.usec != None and self.usec == other.usec) or |
| (self.aggr_intervals != None and |
| self.aggr_intervals == other.aggr_intervals))) |
| |
| def add_unset_unit(self, intervals): |
| if self.usec != None and self.aggr_intervals != None: |
| return |
| if self.usec == None: |
| self.usec = self.aggr_intervals * intervals.aggr |
| elif self.aggr_intervals == None: |
| self.aggr_intervals = int(self.usec / intervals.aggr) |
| |
| def to_str(self, unit, raw): |
| if unit == unit_usec: |
| return _damo_fmt_str.format_time_us_exact(self.usec, raw) |
| if self.aggr_intervals is None: |
| return 'unkown %s' % unit_aggr_intervals |
| return '%s %s' % (_damo_fmt_str.format_nr(self.aggr_intervals, raw), |
| unit_aggr_intervals) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| ret = DamonAge(None, None) |
| if kv['usec'] != None: |
| ret.usec = _damo_fmt_str.text_to_us(kv['usec']) |
| if kv['aggr_intervals'] != None: |
| ret.aggr_intervals = _damo_fmt_str.text_to_nr(kv['aggr_intervals']) |
| return ret |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict( |
| [('usec', _damo_fmt_str.format_time_us_exact(self.usec, raw) |
| if self.usec != None else None), |
| ('aggr_intervals', |
| _damo_fmt_str.format_nr(self.aggr_intervals, raw) |
| if self.aggr_intervals != None else None)]) |
| |
| class DamonRegion: |
| # [start, end) |
| start = None |
| end = None |
| # nr_accesses and age could be None |
| nr_accesses = None |
| age = None |
| sz_filter_passed = None |
| scheme = None # non-None if tried region |
| |
| def __init__(self, start, end, nr_accesses=None, nr_accesses_unit=None, |
| age=None, age_unit=None, sz_filter_passed=0): |
| self.start = _damo_fmt_str.text_to_bytes(start) |
| self.end = _damo_fmt_str.text_to_bytes(end) |
| |
| if nr_accesses == None: |
| return |
| self.nr_accesses = DamonNrAccesses(nr_accesses, nr_accesses_unit) |
| self.age = DamonAge(age, age_unit) |
| self.sz_filter_passed = sz_filter_passed |
| |
| def to_str(self, raw, intervals=None): |
| if self.nr_accesses == None: |
| return _damo_fmt_str.format_addr_range(self.start, self.end, raw) |
| |
| if intervals != None: |
| self.nr_accesses.add_unset_unit(intervals) |
| self.age.add_unset_unit(intervals) |
| |
| if raw == False and intervals != None: |
| nr_accesses_unit = unit_percent |
| age_unit = unit_usec |
| else: |
| nr_accesses_unit = unit_samples |
| age_unit = unit_aggr_intervals |
| str = '%s: nr_accesses: %s, age: %s' % ( |
| _damo_fmt_str.format_addr_range(self.start, self.end, raw), |
| self.nr_accesses.to_str(nr_accesses_unit, raw), |
| self.age.to_str(age_unit, raw)) |
| if self.sz_filter_passed is not None: |
| str += ', filter_passed: %s' % _damo_fmt_str.format_sz( |
| self.sz_filter_passed, raw) |
| return str |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| # For aggregate_snapshots() support |
| def __hash__(self): |
| identification = '%s-%s' % (self.start, self.end) |
| return hash(identification) |
| |
| @classmethod |
| def from_kvpairs(cls, kvpairs): |
| if not 'nr_accesses' in kvpairs: |
| return DamonRegion(kvpairs['start'], kvpairs['end']) |
| region = DamonRegion(kvpairs['start'], kvpairs['end']) |
| region.nr_accesses = DamonNrAccesses.from_kvpairs( |
| kvpairs['nr_accesses']) |
| region.age = DamonAge.from_kvpairs(kvpairs['age']) |
| if 'sz_filter_passed' in kvpairs: |
| region.sz_filter_passed = _damo_fmt_str.text_to_bytes( |
| kvpairs['sz_filter_passed']) |
| else: |
| region.sz_filter_passed = 0 |
| return region |
| |
| def to_kvpairs(self, raw=False): |
| if self.nr_accesses == None: |
| return collections.OrderedDict([ |
| ('start', _damo_fmt_str.format_nr(self.start, raw)), |
| ('end', _damo_fmt_str.format_nr(self.end, raw))]) |
| return collections.OrderedDict([ |
| ('start', _damo_fmt_str.format_nr(self.start, raw)), |
| ('end', _damo_fmt_str.format_nr(self.end, raw)), |
| ('nr_accesses', self.nr_accesses.to_kvpairs(raw)), |
| ('age', self.age.to_kvpairs(raw)), |
| ('sz_filter_passed', _damo_fmt_str.format_sz( |
| self.sz_filter_passed, raw)), |
| ]) |
| |
| def size(self): |
| return self.end - self.start |
| |
| class DamonTarget: |
| pid = None |
| obsolete = None |
| regions = None |
| context = None |
| |
| def __init__(self, pid, regions=[], obsolete=False): |
| self.pid = pid |
| self.regions = regions |
| self.obsolete = _damo_fmt_str.text_to_bool(obsolete) |
| |
| def to_str(self, raw): |
| lines = [] |
| if self.pid is not None: |
| lines.append('pid: %s' % self.pid) |
| if self.obsolete is True: |
| line.sappend('(obsolete)') |
| for region in self.regions: |
| lines.append('region %s' % region.to_str(raw)) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| @classmethod |
| def from_kvpairs(cls, kvpairs): |
| regions = [DamonRegion.from_kvpairs(kvp) for kvp in kvpairs['regions']] |
| obsolete = False |
| if 'obsolete' in kvpairs: |
| obsolete = kvpairs['obsolete'] |
| return DamonTarget(kvpairs['pid'], regions, obsolete=obsolete) |
| |
| def to_kvpairs(self, raw=False): |
| kvp = collections.OrderedDict() |
| kvp['pid'] = self.pid |
| kvp['obsolete'] = self.obsolete |
| kvp['regions'] = [r.to_kvpairs(raw) for r in self.regions] |
| return kvp |
| |
| class DamosAccessPattern: |
| sz_bytes = None |
| nr_acc_min_max = None # [min/max DamonNrAccesses] |
| nr_accesses_unit = None |
| age_min_max = None # [min/max DamonAge] |
| age_unit = None |
| |
| # every region by default, so that it can be used for monitoring |
| def __init__(self, sz_bytes=['min', 'max'], |
| nr_accesses=['min', 'max'], nr_accesses_unit=unit_percent, |
| age=['min', 'max'], age_unit=unit_usec): |
| self.sz_bytes = [_damo_fmt_str.text_to_bytes(sz_bytes[0]), |
| _damo_fmt_str.text_to_bytes(sz_bytes[1])] |
| |
| self.nr_acc_min_max = [ |
| DamonNrAccesses(nr_accesses[0], nr_accesses_unit), |
| DamonNrAccesses(nr_accesses[1], nr_accesses_unit)] |
| self.nr_accesses_unit = nr_accesses_unit |
| self.age_min_max = [ |
| DamonAge(age[0], age_unit), DamonAge(age[1], age_unit)] |
| self.age_unit = age_unit |
| |
| def to_str(self, raw): |
| lines = [ |
| 'sz: [%s, %s]' % (_damo_fmt_str.format_sz(self.sz_bytes[0], raw), |
| _damo_fmt_str.format_sz(self.sz_bytes[1], raw)), |
| ] |
| lines.append('nr_accesses: [%s, %s]' % ( |
| self.nr_acc_min_max[0].to_str(self.nr_accesses_unit, raw), |
| self.nr_acc_min_max[1].to_str(self.nr_accesses_unit, raw))) |
| lines.append('age: [%s, %s]' % ( |
| self.age_min_max[0].to_str(self.age_unit, raw), |
| self.age_min_max[1].to_str(self.age_unit, raw))) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and |
| self.sz_bytes == other.sz_bytes and |
| self.nr_acc_min_max == other.nr_acc_min_max and |
| self.age_min_max == other.age_min_max) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| sz_bytes = [_damo_fmt_str.text_to_bytes(kv['sz_bytes']['min']), |
| _damo_fmt_str.text_to_bytes(kv['sz_bytes']['max'])] |
| |
| kv_ = kv['nr_accesses'] |
| try: |
| nr_accesses = [_damo_fmt_str.text_to_percent(kv_['min']), |
| _damo_fmt_str.text_to_percent(kv_['max'])] |
| nr_accesses_unit = unit_percent |
| except: |
| min_, nr_accesses_unit = _damo_fmt_str.text_to_nr_unit(kv_['min']) |
| max_, nr_accesses_unit2 = _damo_fmt_str.text_to_nr_unit(kv_['max']) |
| if nr_accesses_unit != nr_accesses_unit2: |
| raise Exception('nr_accesses units should be same') |
| nr_accesses = [min_, max_] |
| |
| kv_ = kv['age'] |
| try: |
| age = [_damo_fmt_str.text_to_us(kv_['min']), |
| _damo_fmt_str.text_to_us(kv_['max'])] |
| age_unit = unit_usec |
| except: |
| min_age, age_unit = _damo_fmt_str.text_to_nr_unit(kv_['min']) |
| max_age, age_unit2 = _damo_fmt_str.text_to_nr_unit(kv_['max']) |
| if age_unit != age_unit2: |
| raise Exception('age units should be same') |
| age = [min_age, max_age] |
| |
| return DamosAccessPattern(sz_bytes, nr_accesses, nr_accesses_unit, age, |
| age_unit) |
| |
| def to_kvpairs(self, raw=False): |
| min_nr_accesses = self.nr_acc_min_max[0].to_str( |
| self.nr_accesses_unit, raw) |
| max_nr_accesses = self.nr_acc_min_max[1].to_str( |
| self.nr_accesses_unit, raw) |
| min_age = self.age_min_max[0].to_str(self.age_unit, raw) |
| max_age = self.age_min_max[1].to_str(self.age_unit, raw) |
| |
| return collections.OrderedDict([ |
| ('sz_bytes', (collections.OrderedDict([ |
| ('min', _damo_fmt_str.format_sz(self.sz_bytes[0], raw)), |
| ('max', _damo_fmt_str.format_sz(self.sz_bytes[1], raw))]))), |
| ('nr_accesses', (collections.OrderedDict([ |
| ('min', min_nr_accesses), ('max', max_nr_accesses)]))), |
| ('age', (collections.OrderedDict([ |
| ('min', min_age), ('max', max_age)]))), |
| ]) |
| |
| def convert_for_units(self, nr_accesses_unit, age_unit, intervals): |
| self.nr_acc_min_max[0].add_unset_unit(intervals) |
| self.nr_acc_min_max[1].add_unset_unit(intervals) |
| self.age_min_max[0].add_unset_unit(intervals) |
| self.age_min_max[1].add_unset_unit(intervals) |
| self.nr_accesses_unit = nr_accesses_unit |
| self.age_unit = age_unit |
| |
| def converted_for_units(self, nr_accesses_unit, age_unit, intervals): |
| copied = copy.deepcopy(self) |
| copied.convert_for_units(nr_accesses_unit, age_unit, intervals) |
| return copied |
| |
| def effectively_equal(self, other, intervals): |
| return ( |
| self.converted_for_units( |
| unit_samples, unit_aggr_intervals, intervals) == |
| other.converted_for_units( |
| unit_samples, unit_aggr_intervals, intervals)) |
| |
| class DamosDest: |
| id = None |
| weight = None |
| |
| def __init__(self, id=0, weight=0): |
| self.id = _damo_fmt_str.text_to_nr(id) |
| self.weight = _damo_fmt_str.text_to_nr(weight) |
| |
| def to_str(self, raw): |
| return 'id: %s, weight: %s' % ( |
| _damo_fmt_str.format_nr(self.id, raw), |
| _damo_fmt_str.format_nr(self.weight, raw)) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and |
| self.id == other.id and self.weight == other.weight) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| return DamosDest(kv['id'], kv['weight']) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('id', self.id), |
| ('weight', self.weight), |
| ]) |
| |
| qgoal_user_input = 'user_input' |
| qgoal_some_mem_psi_us = 'some_mem_psi_us' |
| qgoal_node_mem_used_bp = 'node_mem_used_bp' |
| qgoal_node_mem_free_bp = 'node_mem_free_bp' |
| qgoal_node_memcg_used_bp = 'node_memcg_used_bp' |
| qgoal_node_memcg_free_bp = 'node_memcg_free_bp' |
| qgoal_active_mem_bp = 'active_mem_bp' |
| qgoal_inactive_mem_bp = 'inactive_mem_bp' |
| qgoal_metrics = [qgoal_user_input, qgoal_some_mem_psi_us, |
| qgoal_node_mem_used_bp, qgoal_node_mem_free_bp, |
| qgoal_node_memcg_used_bp, qgoal_node_memcg_free_bp, |
| qgoal_active_mem_bp, qgoal_inactive_mem_bp] |
| |
| class DamosQuotaGoal: |
| metric = None |
| target_value = None |
| current_value = None |
| nid = None |
| memcg_path = None |
| quotas = None |
| |
| def __init__(self, metric=qgoal_user_input, |
| target_value='0', current_value='0', nid=None, |
| memcg_path=None): |
| if not metric in qgoal_metrics: |
| raise Exception('unsupported DAMOS quota goal metric') |
| self.metric = metric |
| if metric == qgoal_some_mem_psi_us: |
| self.target_value = _damo_fmt_str.text_to_us(target_value) |
| elif metric in [qgoal_node_mem_used_bp, qgoal_node_mem_free_bp, |
| qgoal_node_memcg_used_bp, qgoal_node_memcg_free_bp, |
| qgoal_active_mem_bp, qgoal_inactive_mem_bp]: |
| self.target_value = _damo_fmt_str.text_to_bp(target_value) |
| else: |
| self.target_value = _damo_fmt_str.text_to_nr(target_value) |
| self.current_value = _damo_fmt_str.text_to_nr(current_value) |
| if nid is None: |
| self.nid = None |
| else: |
| self.nid = _damo_fmt_str.text_to_nr(nid) |
| self.memcg_path = memcg_path |
| |
| @classmethod |
| def metric_require_nid(cls, metric): |
| return metric in [qgoal_node_mem_used_bp, qgoal_node_mem_free_bp] |
| |
| def has_nid(self): |
| return DamosQuotaGoal.metric_require_nid(self.metric) |
| |
| @classmethod |
| def metric_require_memcg_path(cls, metric): |
| return metric in [qgoal_node_memcg_used_bp, qgoal_node_memcg_free_bp] |
| |
| def has_memcg_path(self): |
| return DamosQuotaGoal.metric_require_memcg_path(self.metric) |
| |
| def to_str(self, raw): |
| metric_str = self.metric |
| additional_words = [] |
| if self.has_nid(): |
| additional_words.append('nid %s' % |
| _damo_fmt_str.format_nr(self.nid, raw)) |
| if self.has_memcg_path(): |
| additional_words.append('memcg %s' % self.memcg_path) |
| |
| if len(additional_words) > 0: |
| metric_str = '%s (%s)' % (metric_str, ', '.join(additional_words)) |
| return 'metric %s target %s current %s' % ( |
| metric_str, |
| _damo_fmt_str.format_nr(self.target_value, raw), |
| _damo_fmt_str.format_nr(self.current_value, raw),) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and self.metric == other.metric and |
| self.nid == other.nid and |
| self.memcg_path == other.memcg_path and |
| self.target_value == other.target_value and |
| self.current_value == other.current_value) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| if 'target_value_bp' in kv: |
| # For supporting old version of bad naming. Should deprecate |
| # later. |
| return DamosQuotaGoal(target_value=kv['target_value_bp'], |
| current_value=kv['current_value_bp']) |
| if 'memcg_path' in kv: |
| memcg_path = kv['memcg_path'] |
| else: |
| memcg_path = None |
| return DamosQuotaGoal( |
| metric=kv['metric'], nid=kv['nid'] if 'nid' in kv else None, |
| memcg_path=memcg_path, |
| target_value=kv['target_value'], |
| current_value=kv['current_value']) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('metric', self.metric), |
| ('nid', _damo_fmt_str.format_nr(self.nid, raw) |
| if self.nid is not None else None), |
| ('memcg_path', self.memcg_path), |
| ('target_value', _damo_fmt_str.format_nr(self.target_value, |
| raw)), |
| ('current_value', _damo_fmt_str.format_nr(self.current_value, |
| raw))]) |
| |
| class DamosQuotas: |
| time_ms = None |
| sz_bytes = None |
| reset_interval_ms = None |
| weight_sz_permil = None |
| weight_nr_accesses_permil = None |
| weight_age_permil = None |
| goals = None |
| effective_sz_bytes = None |
| scheme = None |
| |
| def __init__(self, time_ms=0, sz_bytes=0, reset_interval_ms='max', |
| weights=['0 %', '0 %', '0 %'], goals=[], effective_sz_bytes=0): |
| self.time_ms = _damo_fmt_str.text_to_ms(time_ms) |
| self.sz_bytes = _damo_fmt_str.text_to_bytes(sz_bytes) |
| self.reset_interval_ms = _damo_fmt_str.text_to_ms(reset_interval_ms) |
| self.weight_sz_permil = _damo_fmt_str.text_to_permil(weights[0]) |
| self.weight_nr_accesses_permil = _damo_fmt_str.text_to_permil( |
| weights[1]) |
| self.weight_age_permil = _damo_fmt_str.text_to_permil(weights[2]) |
| self.goals = goals |
| self.effective_sz_bytes = _damo_fmt_str.text_to_bytes( |
| effective_sz_bytes) |
| for goal in self.goals: |
| goal.quotas = self |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and self.time_ms == other.time_ms and |
| self.sz_bytes == other.sz_bytes and self.reset_interval_ms == |
| other.reset_interval_ms and self.weight_sz_permil == |
| other.weight_sz_permil and self.weight_nr_accesses_permil == |
| other.weight_nr_accesses_permil and self.weight_age_permil == |
| other.weight_age_permil and self.goals == other.goals) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| if 'goals' in kv: |
| goals = [DamosQuotaGoal.from_kvpairs(goal) for goal in kv['goals']] |
| else: |
| goals = [] |
| return DamosQuotas(kv['time_ms'], kv['sz_bytes'], |
| kv['reset_interval_ms'], |
| [kv['weights']['sz_permil'], |
| kv['weights']['nr_accesses_permil'], |
| kv['weights']['age_permil'],], |
| goals, |
| kv['effective_sz_bytes'] if 'effective_sz_bytes' in kv else 0) |
| |
| def to_str(self, raw, params_only=False): |
| if params_only is False: |
| lines = [ |
| '%s / %s / %s per %s' % ( |
| _damo_fmt_str.format_time_ns(self.time_ms * 1000000, raw), |
| _damo_fmt_str.format_sz(self.sz_bytes, raw), |
| _damo_fmt_str.format_sz(self.effective_sz_bytes, raw), |
| _damo_fmt_str.format_time_ms(self.reset_interval_ms, raw))] |
| else: |
| lines = [ |
| '%s / %s per %s' % ( |
| _damo_fmt_str.format_time_ns(self.time_ms * 1000000, raw), |
| _damo_fmt_str.format_sz(self.sz_bytes, raw), |
| _damo_fmt_str.format_time_ms(self.reset_interval_ms, raw))] |
| |
| for idx, goal in enumerate(self.goals): |
| lines.append('goal %d: %s' % (idx, goal.to_str(raw))) |
| lines.append( |
| 'priority: sz %s, nr_accesses %s, age %s' % ( |
| _damo_fmt_str.format_permil(self.weight_sz_permil, raw), |
| _damo_fmt_str.format_permil( |
| self.weight_nr_accesses_permil, raw), |
| _damo_fmt_str.format_permil(self.weight_age_permil, raw))) |
| return '\n'.join(lines) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('time_ms', _damo_fmt_str.format_time_ms_exact(self.time_ms, raw)), |
| ('sz_bytes', _damo_fmt_str.format_sz(self.sz_bytes, raw)), |
| ('reset_interval_ms', _damo_fmt_str.format_time_ms_exact( |
| self.reset_interval_ms, raw)), |
| ('goals', [goal.to_kvpairs(raw) for goal in self.goals]), |
| ('effective_sz_bytes', |
| _damo_fmt_str.format_sz(self.effective_sz_bytes, raw)), |
| ('weights', (collections.OrderedDict([ |
| ('sz_permil', |
| _damo_fmt_str.format_permil(self.weight_sz_permil, raw)), |
| ('nr_accesses_permil', _damo_fmt_str.format_permil( |
| self.weight_nr_accesses_permil, raw)), |
| ('age_permil', |
| _damo_fmt_str.format_permil(self.weight_age_permil, raw))]) |
| ))]) |
| |
| damos_wmarks_metric_none = 'none' |
| damos_wmarks_metric_free_mem_rate = 'free_mem_rate' |
| |
| class DamosWatermarks: |
| metric = None |
| interval_us = None |
| high_permil = None |
| mid_permil = None |
| low_permil = None |
| |
| # no limit by default |
| def __init__(self, metric=damos_wmarks_metric_none, interval_us=0, |
| high='0 %', mid='0 %', low='0 %'): |
| # 'none' or 'free_mem_rate' |
| if not metric in [damos_wmarks_metric_none, |
| damos_wmarks_metric_free_mem_rate]: |
| raise Exception('wrong watermark metric (%s)' % metric) |
| self.metric = metric |
| self.interval_us = _damo_fmt_str.text_to_us(interval_us) |
| self.high_permil = _damo_fmt_str.text_to_permil(high) |
| self.mid_permil = _damo_fmt_str.text_to_permil(mid) |
| self.low_permil = _damo_fmt_str.text_to_permil(low) |
| |
| def to_str(self, raw): |
| return '\n'.join([ |
| 'metric %s, interval %s' % (self.metric, |
| _damo_fmt_str.format_time_us(self.interval_us, raw)), |
| '%s, %s, %s' % ( |
| _damo_fmt_str.format_permil(self.high_permil, raw), |
| _damo_fmt_str.format_permil(self.mid_permil, raw), |
| _damo_fmt_str.format_permil(self.low_permil, raw)), |
| ]) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and self.metric == other.metric and |
| self.interval_us == other.interval_us and |
| self.high_permil == other.high_permil and |
| self.mid_permil == other.mid_permil and |
| self.low_permil == other.low_permil) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| return DamosWatermarks(*[kv[x] for x in |
| ['metric', 'interval_us', 'high_permil', 'mid_permil', |
| 'low_permil']]) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('metric', self.metric), |
| ('interval_us', _damo_fmt_str.format_time_us_exact( |
| self.interval_us, raw)), |
| ('high_permil', |
| _damo_fmt_str.format_permil(self.high_permil, raw)), |
| ('mid_permil', |
| _damo_fmt_str.format_permil(self.mid_permil, raw)), |
| ('low_permil', |
| _damo_fmt_str.format_permil(self.low_permil, raw)), |
| ]) |
| |
| class DamosFilter: |
| filter_type = None # anon, active, memcg, young, hugepage_size, unmapped, |
| # addr, or target |
| matching = None |
| allow = None |
| memcg_path = None |
| address_range = None # DamonRegion |
| hugepage_size = None # list of min/max hugepage size in bytes |
| damon_target_idx = None |
| scheme = None |
| |
| def __init__(self, filter_type, matching, allow=False, |
| memcg_path=None, address_range=None, damon_target_idx=None, |
| hugepage_size=None): |
| self.filter_type = filter_type |
| self.matching = _damo_fmt_str.text_to_bool(matching) |
| self.memcg_path = memcg_path |
| self.allow = _damo_fmt_str.text_to_bool(allow) |
| self.address_range = address_range |
| if hugepage_size is not None: |
| if not type(hugepage_size) is list and len(hugepage_size) != 2: |
| raise Exception('wrong hugepage_size for DamosFilter()') |
| self.hugepage_size = [ |
| _damo_fmt_str.text_to_bytes(x) for x in hugepage_size] |
| if damon_target_idx != None: |
| self.damon_target_idx = _damo_fmt_str.text_to_nr(damon_target_idx) |
| |
| def to_str(self, raw): |
| words = [] |
| if self.allow: |
| words.append('allow') |
| else: |
| words.append('reject') |
| if self.matching is False: |
| words.append('none') |
| words.append(self.filter_type) |
| if self.filter_type in ['anon', 'active', 'young', 'unmapped']: |
| return ' '.join(words) |
| if self.filter_type == 'memcg': |
| return ' '.join(words + [self.memcg_path]) |
| if self.filter_type == 'addr': |
| return ' '.join(words + [self.address_range.to_str(raw)]) |
| if self.filter_type == 'target': |
| return ' '.join(words + [_damo_fmt_str.format_nr( |
| self.damon_target_idx, raw)]) |
| if self.filter_type == 'hugepage_size': |
| if self.hugepage_size is not None: |
| words.append( |
| '[%s, %s]' % |
| (_damo_fmt_str.format_sz(self.hugepage_size[0], raw), |
| _damo_fmt_str.format_sz(self.hugepage_size[1], raw))) |
| return ' '.join(words) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| allow = False |
| if 'allow' in kv: |
| allow = kv['allow'] |
| # filter_pass has renamed to allow |
| elif 'filter_pass' in kv: |
| allow = kv['filter_pass'] |
| if 'hugepage_size' in kv: |
| hugepage_size = kv['hugepage_size'] |
| else: |
| hugepage_size = None |
| return DamosFilter( |
| kv['filter_type'], kv['matching'], |
| allow, |
| kv['memcg_path'] if kv['filter_type'] == 'memcg' else '', |
| DamonRegion.from_kvpairs(kv['address_range']) |
| if kv['filter_type'] == 'addr' else None, |
| kv['damon_target_idx'] |
| if kv['filter_type'] == 'target' else None, |
| hugepage_size=hugepage_size) |
| |
| def to_kvpairs(self, raw=False): |
| return collections.OrderedDict([ |
| ('filter_type', self.filter_type), |
| ('matching', self.matching), |
| ('allow', self.allow), |
| ('memcg_path', self.memcg_path), |
| ('address_range', self.address_range.to_kvpairs(raw) if |
| self.address_range != None else None), |
| ('damon_target_idx', |
| _damo_fmt_str.format_nr(self.damon_target_idx, raw) |
| if self.damon_target_idx != None else None), |
| ('hugepage_size', |
| [_damo_fmt_str.format_sz(x, raw) for x in self.hugepage_size] |
| if self.hugepage_size is not None else None), |
| ]) |
| |
| def handled_by_ops(self): |
| # whether this filter is handled by DAMON operations set layer |
| return self.filter_type in ['anon', 'active', 'memcg', 'young', |
| 'hugepage_size', 'unmapped'] |
| |
| class DamosStats: |
| nr_tried = None |
| sz_tried = None |
| nr_applied = None |
| sz_applied = None |
| sz_ops_filter_passed = None |
| qt_exceeds = None |
| nr_snapshots = None |
| max_nr_snapshots = None |
| |
| def __init__(self, nr_tried=0, sz_tried=0, nr_applied=0, sz_applied=0, |
| sz_ops_filter_passed=0, qt_exceeds=0, nr_snapshots=0, |
| max_nr_snapshots=0): |
| self.nr_tried = _damo_fmt_str.text_to_nr(nr_tried) |
| self.sz_tried = _damo_fmt_str.text_to_bytes(sz_tried) |
| self.nr_applied = _damo_fmt_str.text_to_nr(nr_applied) |
| self.sz_applied = _damo_fmt_str.text_to_bytes(sz_applied) |
| self.sz_ops_filter_passed = _damo_fmt_str.text_to_bytes( |
| sz_ops_filter_passed) |
| self.qt_exceeds = _damo_fmt_str.text_to_nr(qt_exceeds) |
| self.nr_snapshots = _damo_fmt_str.text_to_nr(nr_snapshots) |
| self.max_nr_snapshots = _damo_fmt_str.text_to_nr(max_nr_snapshots) |
| |
| def to_str(self, raw): |
| return '\n'.join([ |
| 'tried %s times (%s)' % ( |
| _damo_fmt_str.format_nr(self.nr_tried, raw), |
| _damo_fmt_str.format_sz(self.sz_tried, raw)), |
| 'applied %s times (%s)' % ( |
| _damo_fmt_str.format_nr(self.nr_applied, raw), |
| _damo_fmt_str.format_sz(self.sz_applied, raw)), |
| '%s passed filters' % |
| _damo_fmt_str.format_sz(self.sz_ops_filter_passed, raw), |
| 'quota exceeded %s times' % |
| _damo_fmt_str.format_nr(self.qt_exceeds, raw), |
| _damo_fmt_str.format_sz(self.sz_ops_filter_passed, raw), |
| 'tried %s snapshots (max %s)' % |
| (_damo_fmt_str.format_nr(self.nr_snapshots, raw), |
| _damo_fmt_str.format_nr(self.max_nr_snapshots, raw)), |
| ]) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def to_kvpairs(self, raw=False): |
| kv = collections.OrderedDict() |
| kv['nr_tried'] = _damo_fmt_str.format_nr(self.nr_tried, raw) |
| kv['sz_tried'] = _damo_fmt_str.format_sz(self.sz_tried, raw) |
| kv['nr_applied'] = _damo_fmt_str.format_nr(self.nr_applied, raw) |
| kv['sz_applied'] = _damo_fmt_str.format_sz(self.sz_applied, raw) |
| kv['sz_ops_filter_passed'] = _damo_fmt_str.format_sz( |
| self.sz_ops_filter_passed, raw) |
| kv['qt_exceeds'] = _damo_fmt_str.format_nr(self.qt_exceeds, raw) |
| kv['nr_snapshots'] = _damo_fmt_str.format_nr(self.nr_snapshots, raw) |
| kv['max_nr_snapshots'] = _damo_fmt_str.format_nr( |
| self.max_nr_snapshots, raw) |
| return kv |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| if 'nr_snapshots' in kv: |
| nr_snapshots = kv['nr_snapshots'] |
| else: |
| nr_snapshots = 0 |
| if 'max_nr_snapshots' in kv: |
| max_nr_snapshots = kv['max_nr_snapshots'] |
| else: |
| max_nr_snapshots = 0 |
| return cls(kv['nr_tried'], kv['sz_tried'], |
| kv['nr_applied'], kv['sz_applied'], |
| kv['sz_ops_filter_passed'], kv['qt_exceeds'], |
| nr_snapshots=nr_snapshots, |
| max_nr_snapshots=max_nr_snapshots) |
| |
| # TODO: check support of pageout and lru_(de)prio |
| damos_actions = [ |
| 'willneed', |
| 'cold', |
| 'pageout', |
| 'hugepage', |
| 'nohugepage', |
| 'lru_prio', |
| 'lru_deprio', |
| 'migrate_hot', |
| 'migrate_cold', |
| 'stat', |
| ] |
| |
| damos_action_willneed = damos_actions[0] |
| damos_action_cold = damos_actions[1] |
| damos_action_pageout = damos_actions[2] |
| damos_action_hugepage = damos_actions[3] |
| damos_action_nohugepage = damos_actions[4] |
| damos_action_lru_prio = damos_actions[5] |
| damos_action_lru_deprio = damos_actions[6] |
| damos_action_migrate_hot = damos_actions[7] |
| damos_action_migrate_cold = damos_actions[8] |
| damos_action_stat = damos_actions[9] |
| |
| def is_damos_migrate_action(action): |
| if action == damos_action_migrate_hot or \ |
| action == damos_action_migrate_cold: |
| return True |
| return False |
| |
| class Damos: |
| access_pattern = None |
| action = None |
| target_nid = None |
| dests = None |
| apply_interval_us = None |
| quotas = None |
| watermarks = None |
| filters = None |
| stats = None |
| tried_regions = None |
| tried_bytes = None |
| context = None |
| |
| |
| # for monitoring only by default |
| def __init__( |
| self, access_pattern=None, |
| action=damos_action_stat, target_nid=None, apply_interval_us=None, |
| quotas=None, watermarks=None, filters=None, stats=None, |
| tried_regions=None, tried_bytes=None, dests=None): |
| self.access_pattern = (access_pattern |
| if access_pattern != None else DamosAccessPattern()) |
| if not action in damos_actions: |
| raise Exception('wrong damos action: %s' % action) |
| self.action = action |
| self.dests = dests if dests is not None else [] |
| self.target_nid = target_nid |
| if apply_interval_us != None: |
| self.apply_interval_us = _damo_fmt_str.text_to_us( |
| apply_interval_us) |
| else: |
| self.apply_interval_us = 0 |
| self.quotas = quotas if quotas != None else DamosQuotas() |
| self.quotas.scheme = self |
| self.watermarks = (watermarks |
| if watermarks != None else DamosWatermarks()) |
| self.filters = filters if filters != None else [] |
| for filter_ in self.filters: |
| filter_.scheme = self |
| self.stats = stats if stats != None else DamosStats() |
| self.tried_regions = tried_regions if tried_regions != None else [] |
| for tried_region in self.tried_regions: |
| tried_region.scheme = self |
| self.tried_bytes = 0 |
| if tried_bytes: |
| self.tried_bytes = _damo_fmt_str.text_to_bytes( |
| tried_bytes) |
| else: |
| for region in self.tried_regions: |
| self.tried_bytes += region.size() |
| |
| def str_action_line(self, raw): |
| action_words = ['action: %s' % self.action] |
| if is_damos_migrate_action(self.action): |
| action_words.append('to node %s' % self.target_nid) |
| action_words.append('per %s' % |
| _damo_fmt_str.format_time_us(self.apply_interval_us, raw) |
| if self.apply_interval_us != 0 else 'per aggr interval') |
| return ' '.join(action_words) |
| |
| def to_str(self, raw, params_only=False, omit_tried_regions=False): |
| lines = [self.str_action_line(raw)] |
| if self.access_pattern is not None: |
| lines.append('target access pattern') |
| lines.append(_damo_fmt_str.indent_lines( |
| self.access_pattern.to_str(raw), 4)) |
| for dest in self.dests: |
| lines.append('dest %d weight %d' % (dest.id, dest.weight)) |
| if self.quotas is not None: |
| lines.append('quotas') |
| lines.append(_damo_fmt_str.indent_lines( |
| self.quotas.to_str(raw, params_only), 4)) |
| if self.watermarks is not None: |
| lines.append('watermarks') |
| lines.append(_damo_fmt_str.indent_lines( |
| self.watermarks.to_str(raw), 4)) |
| for idx, damos_filter in enumerate(self.filters): |
| lines.append('filter %d' % idx) |
| lines.append(_damo_fmt_str.indent_lines( |
| damos_filter.to_str(raw), 4)) |
| if params_only is False and self.stats is not None: |
| lines.append('statistics') |
| lines.append(_damo_fmt_str.indent_lines(self.stats.to_str(raw), 4)) |
| if params_only is False and omit_tried_regions is False and \ |
| self.tried_regions is not None: |
| lines.append('tried regions (%s)' % _damo_fmt_str.format_sz( |
| self.tried_bytes, raw)) |
| for region in self.tried_regions: |
| lines.append(_damo_fmt_str.indent_lines(region.to_str(raw), 4)) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __repr__(self): |
| return self.__str__() |
| |
| def __eq__(self, other): |
| return (type(self) == type(other) and |
| self.access_pattern == other.access_pattern and |
| self.action == other.action and |
| self.dests == other.dests and |
| self.apply_interval_us == other.apply_interval_us and |
| self.quotas == other.quotas and |
| self.watermarks == other.watermarks and |
| self.filters == other.filters) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| filters = [] |
| if 'filters' in kv: |
| for damos_filter_kv in kv['filters']: |
| filters.append(DamosFilter.from_kvpairs(damos_filter_kv)) |
| dests = [] |
| if 'dests' in kv: |
| for dest_kv in kv['dests']: |
| dests.append(DamosDest.from_kvpairs(dest_kv)) |
| return Damos(DamosAccessPattern.from_kvpairs(kv['access_pattern']) |
| if 'access_pattern' in kv else DamosAccessPattern(), |
| kv['action'] if 'action' in kv else damos_action_stat, |
| kv['target_nid'] if 'target_nid' in kv else None, |
| kv['apply_interval_us'] if 'apply_interval_us' in kv else None, |
| DamosQuotas.from_kvpairs(kv['quotas']) |
| if 'quotas' in kv else DamosQuotas(), |
| DamosWatermarks.from_kvpairs(kv['watermarks']) |
| if 'watermarks' in kv else DamosWatermarks(), |
| filters, |
| None, None, dests=dests) |
| |
| def to_kvpairs(self, raw=False, omit_defaults=False, params_only=False): |
| kv = collections.OrderedDict() |
| kv['action'] = self.action |
| kv['dests'] = [dest.to_kvpairs(raw) for dest in self.dests] |
| if is_damos_migrate_action(self.action): |
| kv['target_nid'] = self.target_nid |
| if not omit_defaults or self.access_pattern != DamosAccessPattern(): |
| kv['access_pattern'] = self.access_pattern.to_kvpairs(raw) |
| kv['apply_interval_us'] = _damo_fmt_str.format_time_us(self.apply_interval_us, raw) |
| if not omit_defaults or self.quotas != DamosQuotas(): |
| kv['quotas'] = self.quotas.to_kvpairs(raw) |
| if not omit_defaults or self.watermarks != DamosWatermarks(): |
| kv['watermarks'] = self.watermarks.to_kvpairs(raw) |
| if not omit_defaults or self.filters != []: |
| filters = [] |
| for damos_filter in self.filters: |
| filters.append(damos_filter.to_kvpairs(raw)) |
| kv['filters'] = filters |
| if not params_only and self.stats is not None: |
| kv['stats'] = self.stats.to_kvpairs(raw) |
| return kv |
| |
| def effectively_equal(self, other, intervals): |
| return (type(self) == type(other) and |
| self.access_pattern.effectively_equal( |
| other.access_pattern, intervals) and |
| self.action == other.action and |
| self.dests == other.dests and |
| self.apply_interval_us == other.apply_interval_us and |
| self.quotas == other.quotas and |
| self.watermarks == other.watermarks and |
| self.filters == other.filters) |
| |
| class DamonCtx: |
| ops = None |
| ops_attrs = None |
| targets = None |
| intervals = None |
| nr_regions = None |
| sample_control = None |
| schemes = None |
| kdamond = None |
| |
| def __init__(self, ops='paddr', targets=None, intervals=None, |
| nr_regions=None, schemes=None, ops_attrs=None, |
| sample_control=None): |
| self.ops = ops |
| self.ops_attrs = ops_attrs if ops_attrs is not None else OpsAttrs() |
| self.targets = targets if targets is not None else [] |
| for target in self.targets: |
| target.context = self |
| self.intervals = (intervals |
| if intervals is not None else DamonIntervals()) |
| self.nr_regions = (nr_regions if nr_regions is not None |
| else DamonNrRegionsRange()) |
| if sample_control is None: |
| sample_control = DamonSampleControl() |
| self.sample_control = sample_control |
| self.schemes = schemes if schemes is not None else Damos() |
| for scheme in self.schemes: |
| scheme.context = self |
| |
| def to_str(self, raw, params_only=False, omit_damos_tried_regions=False): |
| ops_line_tokens = ['ops: %s' % self.ops] |
| if self.ops_attrs.use_reports: |
| ops_line_tokens.append('use_reports') |
| ops_line_tokens.append('cpus %s' % self.ops_attrs.cpus) |
| ops_line_tokens.append('threads %s' % self.ops_attrs.tids) |
| if self.ops_attrs.write_only: |
| ops_line_tokens.append('write-only') |
| lines = [' '.join(ops_line_tokens)] |
| for idx, target in enumerate(self.targets): |
| lines.append('target %d' % idx) |
| lines.append(_damo_fmt_str.indent_lines(target.to_str(raw), 4)) |
| intervals_lines = self.intervals.to_str(raw).split('\n') |
| if len(intervals_lines) == 1: |
| lines.append('intervals: %s' % intervals_lines[0]) |
| else: |
| lines.append('intervals') |
| lines.append( |
| _damo_fmt_str.indent_lines(self.intervals.to_str(raw), 4)) |
| lines.append('nr_regions: %s' % self.nr_regions.to_str(raw)) |
| for idx, scheme in enumerate(self.schemes): |
| lines.append('scheme %d' % idx) |
| lines.append(_damo_fmt_str.indent_lines( |
| scheme.to_str(raw, params_only, omit_damos_tried_regions), 4)) |
| lines.append('access sample control') |
| lines.append('%s' % self.sample_control.to_str(raw)) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| def __hash__(self): |
| return hash(self.__str__()) |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| if 'sample_control' in kv: |
| sample_control = DamonSampleControl.from_kvpairs( |
| kv['sample_control']) |
| else: |
| sample_control = DamonSampleControl() |
| ctx = DamonCtx( |
| kv['ops'], |
| [DamonTarget.from_kvpairs(t) for t in kv['targets']], |
| DamonIntervals.from_kvpairs(kv['intervals']) |
| if 'intervals' in kv else DamonIntervals(), |
| DamonNrRegionsRange.from_kvpairs(kv['nr_regions']) |
| if 'nr_regions' in kv else DamonNrRegionsRange(), |
| [Damos.from_kvpairs(s) for s in kv['schemes']] |
| if 'schemes' in kv else [], |
| sample_control=sample_control) |
| return ctx |
| |
| def to_kvpairs(self, raw=False, omit_defaults=False, params_only=False): |
| kv = collections.OrderedDict({}) |
| kv['ops'] = self.ops |
| kv['targets'] = [t.to_kvpairs(raw) for t in self.targets] |
| if not omit_defaults or self.intervals != DamonIntervals(): |
| kv['intervals'] = self.intervals.to_kvpairs(raw) |
| if not omit_defaults or self.nr_regions != DamonNrRegionsRange(): |
| kv['nr_regions'] = self.nr_regions.to_kvpairs(raw) |
| kv['sample_control'] = self.sample_control.to_kvpairs(raw) |
| kv['schemes'] = [s.to_kvpairs(raw, omit_defaults, params_only) |
| for s in self.schemes] |
| return kv |
| |
| def target_has_pid(ops): |
| return ops in ['vaddr', 'fvaddr'] |
| |
| def target_regions_fixed(ops): |
| return ops in ['fvaddr', 'paddr'] |
| |
| class Kdamond: |
| state = None |
| pid = None |
| refresh_ms = None |
| contexts = None |
| |
| def __init__(self, state, pid, contexts, refresh_ms=0): |
| self.state = state |
| self.pid = pid |
| self.refresh_ms = _damo_fmt_str.text_to_ms(refresh_ms) |
| self.contexts = contexts |
| for ctx in self.contexts: |
| ctx.kdamond = self |
| |
| def summary_str(self, show_cpu=False, params_only=False, |
| omit_defaults=False, raw_number=False): |
| words = [] |
| if params_only is False and self.state is not None: |
| words.append('state: %s' % self.state) |
| if params_only is False and self.pid is not None: |
| words.append('pid: %s' % self.pid) |
| if self.refresh_ms != 0: |
| words.append('stats refresh per: %s' % |
| _damo_fmt_str.format_time_ms( |
| self.refresh_ms, raw_number)) |
| if show_cpu: |
| words.append('cpu usage: %s' % self.get_cpu_usage()) |
| return ', '.join(words) |
| |
| def to_str(self, raw, show_cpu=False, params_only=False, |
| omit_damos_tried_regions=False): |
| lines = [] |
| summary_line = self.summary_str(show_cpu, params_only, |
| omit_defaults=False, raw_number=raw) |
| if summary_line != '': |
| lines.append(summary_line) |
| for idx, ctx in enumerate(self.contexts): |
| lines.append('context %d' % idx) |
| lines.append(_damo_fmt_str.indent_lines( |
| ctx.to_str(raw, params_only, omit_damos_tried_regions), 4)) |
| return '\n'.join(lines) |
| |
| def __str__(self): |
| return self.to_str(False) |
| |
| def __eq__(self, other): |
| return type(self) == type(other) and '%s' % self == '%s' % other |
| |
| def __hash__(self): |
| return hash(self.__str__()) |
| |
| def get_cpu_usage(self): |
| if self.state == "off": |
| return "0.0" |
| try: |
| res = subprocess.check_output(['ps', '-p', self.pid, '-o', '%cpu'], text=True) |
| return res.split("\n")[1].strip() |
| except: |
| return 'error' |
| |
| @classmethod |
| def from_kvpairs(cls, kv): |
| return Kdamond( |
| kv['state'] if 'state' in kv else 'off', |
| kv['pid'] if 'pid' in kv else None, |
| [DamonCtx.from_kvpairs(c) for c in kv['contexts']], |
| refresh_ms=kv['refresh_ms'] if 'refresh_ms' in kv else 0, |
| ) |
| |
| def to_kvpairs(self, raw=False, omit_defaults=False, params_only=False): |
| kv = collections.OrderedDict() |
| if not params_only: |
| kv['state'] = self.state |
| kv['pid'] = self.pid |
| kv['refresh_ms'] = self.refresh_ms |
| kv['contexts'] = [c.to_kvpairs(raw, omit_defaults, params_only) |
| for c in self.contexts] |
| return kv |
| |
| import _damo_fs |
| import _damo_sysinfo |
| import _damon_dbgfs |
| import _damon_sysfs |
| |
| _damon_fs = None |
| |
| def ensure_root_permission(): |
| if os.geteuid() != 0: |
| print('Run as root') |
| exit(1) |
| |
| def set_damon_interface(damon_interface): |
| global _damon_fs |
| if damon_interface == 'sysfs': |
| _damon_fs = _damon_sysfs |
| elif damon_interface == 'debugfs': |
| _damon_fs = _damon_dbgfs |
| elif damon_interface == 'auto': |
| if _damon_sysfs.supported(): |
| _damon_fs = _damon_sysfs |
| else: |
| _damon_fs = _damon_dbgfs |
| if not _damon_fs.supported(): |
| return 'DAMON interface (%s) not supported' % damon_interface |
| return None |
| |
| def initialize(damon_interface, debug_damon, load_sysinfo): |
| err = set_damon_interface(damon_interface) |
| if err is not None: |
| return err |
| if debug_damon: |
| _damo_fs.debug_print_ops(True) |
| if load_sysinfo: |
| err = _damo_sysinfo.load_sysinfo() |
| if err is not None: |
| return err |
| return None |
| |
| initialized = False |
| def ensure_initialized(args, load_sysinfo): |
| global initialized |
| |
| if initialized: |
| return |
| err = initialize( |
| args.damon_interface_DEPRECATED, args.debug_damon, load_sysinfo) |
| if err != None: |
| print(err) |
| exit(1) |
| initialized = True |
| |
| def ensure_root_and_initialized(args, load_sysinfo=True): |
| ensure_root_permission() |
| ensure_initialized(args, load_sysinfo) |
| |
| # DAMON control |
| |
| def stage_kdamonds(kdamonds): |
| return _damon_fs.stage_kdamonds(kdamonds) |
| |
| def stage_kdamonds_targets(kdamonds): |
| if _damon_fs == _damon_dbgfs: |
| return 'debugfs interface does not support stage_kdamonds_target' |
| return _damon_fs.stage_kdamonds_targets(kdamonds) |
| |
| def commit_staged(kdamond_idxs): |
| if _damon_fs == _damon_dbgfs: |
| return 'debugfs interface does not support commit_staged()' |
| return _damon_fs.commit_staged(kdamond_idxs) |
| |
| def commit_quota_goals(kdamond_idxs): |
| if _damon_fs == _damon_dbgfs: |
| return 'debugfs interface does not support commit_quota_goals()' |
| return _damon_fs.commit_quota_goals(kdamond_idxs) |
| |
| def cleanup_obsolete_targets(kdamonds): |
| ''' |
| When obsolete targets are committed to DAMON, DAMON removes the obsolete |
| ones inside kernel, but leave the sysfs files as is. Cleanup the obsolete |
| sysfs files by staging only non-obsolete targets again. |
| ''' |
| need_cleanup = False |
| for kd in kdamonds: |
| for ctx in kd.contexts: |
| for target in ctx.targets: |
| if target.obsolete: |
| need_cleanup = True |
| ctx.targets = [t for t in ctx.targets if not t.obsolete] |
| if not need_cleanup: |
| return None |
| return stage_kdamonds(kdamonds) |
| |
| def commit(kdamonds, commit_quota_goals_only=False, commit_targets_only=False): |
| if not commit_quota_goals_only and not commit_targets_only: |
| err = stage_kdamonds(kdamonds) |
| if err: |
| return 'staging updates failed (%s)' % err |
| elif commit_targets_only: |
| err = stage_kdamonds_targets(kdamonds) |
| if err: |
| return 'staging target updates failed (%s)' % err |
| |
| kdamond_idxs = ['%s' % idx for idx, k in enumerate(kdamonds)] |
| |
| if commit_quota_goals_only: |
| err = commit_quota_goals(kdamond_idxs) |
| if err: |
| return 'commit quotas failed (%s)' % err |
| return None |
| |
| err = commit_staged(kdamond_idxs) |
| if err: |
| return 'commit staged updates filed (%s)' % err |
| |
| err = cleanup_obsolete_targets(kdamonds) |
| if err is not None: |
| return 'obsolte targets cleanup failed (%s)' % err |
| |
| return None |
| |
| def update_tuned_intervals(kdamond_idxs=None): |
| if kdamond_idxs is None: |
| kdamond_idxs = running_kdamond_idxs() |
| if _damon_fs == _damon_dbgfs: |
| return None |
| err = _damon_fs.update_tuned_intervals(kdamond_idxs) |
| # 'Invalid argument' means the feature is not supported on the kernel |
| if err is not None and not 'Invalid argument' in err: |
| return err |
| return None |
| |
| def update_schemes_stats(kdamond_idxs=None): |
| if kdamond_idxs == None: |
| kdamond_idxs = running_kdamond_idxs() |
| return _damon_fs.update_schemes_stats(kdamond_idxs) |
| |
| def update_schemes_tried_bytes(kdamond_idxs=None): |
| if kdamond_idxs == None: |
| kdamond_idxs = running_kdamond_idxs() |
| return _damon_fs.update_schemes_tried_bytes(kdamond_idxs) |
| |
| def update_schemes_tried_regions(kdamond_idxs=None): |
| if not _damo_sysinfo.damon_feature_available( |
| 'sysfs/schemes_tried_regions'): |
| return 'DAMON feature \'schemes_tried_regions\' is not supported' \ |
| ' on the current kernel. ' \ |
| 'It is available on kernel version 6.2 and later' |
| if kdamond_idxs == None: |
| kdamond_idxs = running_kdamond_idxs() |
| return _damon_fs.update_schemes_tried_regions(kdamond_idxs) |
| |
| def update_schemes_quota_effective_bytes(kdamond_idxs=None): |
| if kdamond_idxs == None: |
| kdamond_idxs = running_kdamond_idxs() |
| return _damon_fs.update_schemes_quota_effective_bytes(kdamond_idxs) |
| |
| def update_schemes_status(stats=True, tried_regions=True, |
| quota_effective_bytes=False): |
| '''Returns error string or None''' |
| schemes_exist = False |
| for kdamond in current_kdamonds(): |
| for ctx in kdamond.contexts: |
| if len(ctx.schemes) > 0: |
| schemes_exist = True |
| break |
| if schemes_exist is True: |
| break |
| if schemes_exist is False: |
| return None |
| |
| idxs = running_kdamond_idxs() |
| if len(idxs) == 0: |
| return None |
| if stats: |
| err = update_schemes_stats(idxs) |
| if err != None: |
| return err |
| if tried_regions and _damo_sysinfo.damon_feature_available( |
| 'sysfs/schemes_tried_regions'): |
| err = update_schemes_tried_regions(idxs) |
| if err != None: |
| return err |
| |
| if quota_effective_bytes and _damo_sysinfo.damon_feature_available( |
| 'sysfs/schemes_quota_effective_bytes'): |
| return update_schemes_quota_effective_bytes(idxs) |
| |
| return None |
| |
| def get_childs_pids(pid): |
| try: |
| childs_pids = subprocess.check_output( |
| ['ps', '--ppid', pid, '-o', 'pid='] |
| ).decode().split() |
| except: |
| childs_pids = [] |
| |
| ret = childs_pids |
| for child_pid in childs_pids: |
| childs_childs_pids = get_childs_pids(child_pid) |
| ret.extend(childs_childs_pids) |
| |
| return ret |
| |
| def pid_running(pid): |
| try: |
| subprocess.check_output(['ps', '--pid', '%s' % pid]) |
| return True |
| except: |
| return False |
| |
| def best_effort_target_arrange(updated_targets, new_targets): |
| ''' |
| Targets arrangement for kernels not supporting obsolete_target feature. |
| |
| On the kernel, users cannot specify whether a target is obsolete. Remove |
| those. Also, DAMON will inherit monitoring results of targets of same |
| index. Try to keep existing targets have same index as a best effort. |
| ''' |
| for idx, old_target in enumerate(updated_targets): |
| if old_target.obsolete and len(new_targets) > 0: |
| updated_targets[idx] = new_targets[0] |
| new_targets = new_targets[1:] |
| # todo: the new target will unnecessarily inherit old target's |
| # monitoring results. Avoid it if possible. |
| if len(new_targets) > 0: |
| return updated_targets + new_targets |
| # todo: try to further keep the index |
| return [t for t in updated_targets if t.obsolete is False] |
| |
| def add_vaddr_child_targets(ctx): |
| ''' |
| Returns whether a change is made, and old targets list if a change was made |
| ''' |
| if not target_has_pid(ctx.ops): |
| return False, None |
| if target_regions_fixed(ctx.ops): |
| return False, None |
| |
| changes_made = False |
| orig_targets = ctx.targets |
| updated_targets = [] |
| child_targets = [] |
| for orig_target in orig_targets: |
| updated_targets.append(DamonTarget(pid=orig_target.pid, regions=[])) |
| if not pid_running(orig_target.pid): |
| updated_targets[-1].obsolete = True |
| changes_made = True |
| for child_pid in get_childs_pids('%s' % orig_target.pid): |
| child_targets.append(DamonTarget(pid=child_pid, regions=[])) |
| changes_made = True |
| if changes_made: |
| if _damo_sysinfo.damon_feature_available('sysfs/obsolete_target'): |
| ctx.targets = updated_targets + child_targets |
| else: |
| ctx.targets = best_effort_target_arrange( |
| updated_targets, child_targets) |
| return changes_made, orig_targets |
| |
| def add_commit_vaddr_child_targets(kdamonds): |
| need_commit = False |
| old_targets_list = [] |
| for kd in kdamonds: |
| for ctx in kd.contexts: |
| ctx = kdamonds[0].contexts[0] |
| need_commit_, old_targets = add_vaddr_child_targets(ctx) |
| if need_commit_ is True: |
| need_commit = True |
| old_targets_list.append(old_targets) |
| if not need_commit: |
| return |
| err = commit(kdamonds, commit_targets_only=True) |
| if err is not None: |
| idx = 0 |
| for kd in kdamonds: |
| for ctx in kd.contexts: |
| ctx.targets = old_targets[idx] |
| idx += 1 |
| return 'commit failed (%s)' % err |
| |
| return None |
| |
| def turn_damon_on(kdamonds_idxs): |
| err = _damon_fs.turn_damon_on(kdamonds_idxs) |
| if err: |
| return err |
| wait_kdamonds_turned_on() |
| |
| def turn_damon_off(kdamonds_idxs): |
| err = _damon_fs.turn_damon_off(kdamonds_idxs) |
| if err: |
| return err |
| wait_kdamonds_turned_off() |
| |
| # DAMON status reading |
| |
| def is_kdamond_running(kdamond_idx): |
| return _damon_fs.is_kdamond_running(kdamond_idx) |
| |
| def current_kdamonds(): |
| return _damon_fs.current_kdamonds() |
| |
| def update_read_kdamonds( |
| nr_retries=0, update_stats=True, update_tried_regions=True, |
| update_quota_effective_bytes=False, do_update_tuned_intervals=False): |
| err = 'assumed error' |
| nr_tries = 0 |
| while True: |
| if do_update_tuned_intervals: |
| err = update_tuned_intervals() |
| if err is not None: |
| nr_tries += 1 |
| time.sleep( |
| random.randrange(2**(nr_tries - 1), 2**nr_tries) / 100) |
| continue |
| err = update_schemes_status(update_stats, update_tried_regions, |
| update_quota_effective_bytes) |
| nr_tries += 1 |
| if err == None or nr_tries > nr_retries: |
| break |
| time.sleep(random.randrange(2**(nr_tries - 1), 2**nr_tries) / 100) |
| if err: |
| return None, err |
| return current_kdamonds(), None |
| |
| def nr_kdamonds(): |
| return _damon_fs.nr_kdamonds() |
| |
| def running_kdamond_idxs(): |
| return [idx for idx in range(nr_kdamonds()) |
| if is_kdamond_running(idx)] |
| |
| def any_kdamond_running(): |
| for idx in range(nr_kdamonds()): |
| if is_kdamond_running(idx): |
| return True |
| return False |
| |
| def wait_kdamonds_turned_on(): |
| for idx in range(nr_kdamonds()): |
| while not is_kdamond_running(idx): |
| time.sleep(1) |
| |
| def wait_kdamonds_turned_off(): |
| for idx in range(nr_kdamonds()): |
| while is_kdamond_running(idx): |
| time.sleep(1) |