1# SPDX-License-Identifier: GPL-2.0
2
3import os
4
5ksft_skip=4
6
7sysfs_root = None
8with open('/proc/mounts', 'r') as f:
9    for line in f:
10        dev_name, mount_point, dev_fs = line.split()[:3]
11        if dev_fs == 'sysfs':
12            sysfs_root = '%s/kernel/mm/damon/admin' % mount_point
13            break
14if sysfs_root is None:
15    print('Seems sysfs not mounted?')
16    exit(ksft_skip)
17
18def write_file(path, string):
19    "Returns error string if failed, or None otherwise"
20    string = '%s' % string
21    try:
22        with open(path, 'w') as f:
23            f.write(string)
24    except Exception as e:
25        return '%s' % e
26    return None
27
28def read_file(path):
29    '''Returns the read content and error string.  The read content is None if
30    the reading failed'''
31    try:
32        with open(path, 'r') as f:
33            return f.read(), None
34    except Exception as e:
35        return None, '%s' % e
36
37class DamosAccessPattern:
38    size = None
39    nr_accesses = None
40    age = None
41    scheme = None
42
43    def __init__(self, size=None, nr_accesses=None, age=None):
44        self.size = size
45        self.nr_accesses = nr_accesses
46        self.age = age
47
48        if self.size is None:
49            self.size = [0, 2**64 - 1]
50        if self.nr_accesses is None:
51            self.nr_accesses = [0, 2**64 - 1]
52        if self.age is None:
53            self.age = [0, 2**64 - 1]
54
55    def sysfs_dir(self):
56        return os.path.join(self.scheme.sysfs_dir(), 'access_pattern')
57
58    def stage(self):
59        err = write_file(
60                os.path.join(self.sysfs_dir(), 'sz', 'min'), self.size[0])
61        if err is not None:
62            return err
63        err = write_file(
64                os.path.join(self.sysfs_dir(), 'sz', 'max'), self.size[1])
65        if err is not None:
66            return err
67        err = write_file(os.path.join(self.sysfs_dir(), 'nr_accesses', 'min'),
68                self.nr_accesses[0])
69        if err is not None:
70            return err
71        err = write_file(os.path.join(self.sysfs_dir(), 'nr_accesses', 'max'),
72                self.nr_accesses[1])
73        if err is not None:
74            return err
75        err = write_file(
76                os.path.join(self.sysfs_dir(), 'age', 'min'), self.age[0])
77        if err is not None:
78            return err
79        err = write_file(
80                os.path.join(self.sysfs_dir(), 'age', 'max'), self.age[1])
81        if err is not None:
82            return err
83
84qgoal_metric_user_input = 'user_input'
85qgoal_metric_some_mem_psi_us = 'some_mem_psi_us'
86qgoal_metrics = [qgoal_metric_user_input, qgoal_metric_some_mem_psi_us]
87
88class DamosQuotaGoal:
89    metric = None
90    target_value = None
91    current_value = None
92    effective_bytes = None
93    quota = None            # owner quota
94    idx = None
95
96    def __init__(self, metric, target_value=10000, current_value=0):
97        self.metric = metric
98        self.target_value = target_value
99        self.current_value = current_value
100
101    def sysfs_dir(self):
102        return os.path.join(self.quota.sysfs_dir(), 'goals', '%d' % self.idx)
103
104    def stage(self):
105        err = write_file(os.path.join(self.sysfs_dir(), 'target_metric'),
106                         self.metric)
107        if err is not None:
108            return err
109        err = write_file(os.path.join(self.sysfs_dir(), 'target_value'),
110                         self.target_value)
111        if err is not None:
112            return err
113        err = write_file(os.path.join(self.sysfs_dir(), 'current_value'),
114                         self.current_value)
115        if err is not None:
116            return err
117        return None
118
119class DamosQuota:
120    sz = None                   # size quota, in bytes
121    ms = None                   # time quota
122    goals = None                # quota goals
123    reset_interval_ms = None    # quota reset interval
124    scheme = None               # owner scheme
125
126    def __init__(self, sz=0, ms=0, goals=None, reset_interval_ms=0):
127        self.sz = sz
128        self.ms = ms
129        self.reset_interval_ms = reset_interval_ms
130        self.goals = goals if goals is not None else []
131        for idx, goal in enumerate(self.goals):
132            goal.idx = idx
133            goal.quota = self
134
135    def sysfs_dir(self):
136        return os.path.join(self.scheme.sysfs_dir(), 'quotas')
137
138    def stage(self):
139        err = write_file(os.path.join(self.sysfs_dir(), 'bytes'), self.sz)
140        if err is not None:
141            return err
142        err = write_file(os.path.join(self.sysfs_dir(), 'ms'), self.ms)
143        if err is not None:
144            return err
145        err = write_file(os.path.join(self.sysfs_dir(), 'reset_interval_ms'),
146                         self.reset_interval_ms)
147        if err is not None:
148            return err
149
150        nr_goals_file = os.path.join(self.sysfs_dir(), 'goals', 'nr_goals')
151        content, err = read_file(nr_goals_file)
152        if err is not None:
153            return err
154        if int(content) != len(self.goals):
155            err = write_file(nr_goals_file, len(self.goals))
156            if err is not None:
157                return err
158        for goal in self.goals:
159            err = goal.stage()
160            if err is not None:
161                return err
162        return None
163
164class DamosStats:
165    nr_tried = None
166    sz_tried = None
167    nr_applied = None
168    sz_applied = None
169    qt_exceeds = None
170
171    def __init__(self, nr_tried, sz_tried, nr_applied, sz_applied, qt_exceeds):
172        self.nr_tried = nr_tried
173        self.sz_tried = sz_tried
174        self.nr_applied = nr_applied
175        self.sz_applied = sz_applied
176        self.qt_exceeds = qt_exceeds
177
178class DamosTriedRegion:
179    def __init__(self, start, end, nr_accesses, age):
180        self.start = start
181        self.end = end
182        self.nr_accesses = nr_accesses
183        self.age = age
184
185class Damos:
186    action = None
187    access_pattern = None
188    quota = None
189    apply_interval_us = None
190    # todo: Support watermarks, stats
191    idx = None
192    context = None
193    tried_bytes = None
194    stats = None
195    tried_regions = None
196
197    def __init__(self, action='stat', access_pattern=DamosAccessPattern(),
198                 quota=DamosQuota(), apply_interval_us=0):
199        self.action = action
200        self.access_pattern = access_pattern
201        self.access_pattern.scheme = self
202        self.quota = quota
203        self.quota.scheme = self
204        self.apply_interval_us = apply_interval_us
205
206    def sysfs_dir(self):
207        return os.path.join(
208                self.context.sysfs_dir(), 'schemes', '%d' % self.idx)
209
210    def stage(self):
211        err = write_file(os.path.join(self.sysfs_dir(), 'action'), self.action)
212        if err is not None:
213            return err
214        err = self.access_pattern.stage()
215        if err is not None:
216            return err
217        err = write_file(os.path.join(self.sysfs_dir(), 'apply_interval_us'),
218                         '%d' % self.apply_interval_us)
219        if err is not None:
220            return err
221
222        err = self.quota.stage()
223        if err is not None:
224            return err
225
226        # disable watermarks
227        err = write_file(
228                os.path.join(self.sysfs_dir(), 'watermarks', 'metric'), 'none')
229        if err is not None:
230            return err
231
232        # disable filters
233        err = write_file(
234                os.path.join(self.sysfs_dir(), 'filters', 'nr_filters'), '0')
235        if err is not None:
236            return err
237
238class DamonTarget:
239    pid = None
240    # todo: Support target regions if test is made
241    idx = None
242    context = None
243
244    def __init__(self, pid):
245        self.pid = pid
246
247    def sysfs_dir(self):
248        return os.path.join(
249                self.context.sysfs_dir(), 'targets', '%d' % self.idx)
250
251    def stage(self):
252        err = write_file(
253                os.path.join(self.sysfs_dir(), 'regions', 'nr_regions'), '0')
254        if err is not None:
255            return err
256        return write_file(
257                os.path.join(self.sysfs_dir(), 'pid_target'), self.pid)
258
259class DamonAttrs:
260    sample_us = None
261    aggr_us = None
262    update_us = None
263    min_nr_regions = None
264    max_nr_regions = None
265    context = None
266
267    def __init__(self, sample_us=5000, aggr_us=100000, update_us=1000000,
268            min_nr_regions=10, max_nr_regions=1000):
269        self.sample_us = sample_us
270        self.aggr_us = aggr_us
271        self.update_us = update_us
272        self.min_nr_regions = min_nr_regions
273        self.max_nr_regions = max_nr_regions
274
275    def interval_sysfs_dir(self):
276        return os.path.join(self.context.sysfs_dir(), 'monitoring_attrs',
277                'intervals')
278
279    def nr_regions_range_sysfs_dir(self):
280        return os.path.join(self.context.sysfs_dir(), 'monitoring_attrs',
281                'nr_regions')
282
283    def stage(self):
284        err = write_file(os.path.join(self.interval_sysfs_dir(), 'sample_us'),
285                self.sample_us)
286        if err is not None:
287            return err
288        err = write_file(os.path.join(self.interval_sysfs_dir(), 'aggr_us'),
289                self.aggr_us)
290        if err is not None:
291            return err
292        err = write_file(os.path.join(self.interval_sysfs_dir(), 'update_us'),
293                self.update_us)
294        if err is not None:
295            return err
296
297        err = write_file(
298                os.path.join(self.nr_regions_range_sysfs_dir(), 'min'),
299                self.min_nr_regions)
300        if err is not None:
301            return err
302
303        err = write_file(
304                os.path.join(self.nr_regions_range_sysfs_dir(), 'max'),
305                self.max_nr_regions)
306        if err is not None:
307            return err
308
309class DamonCtx:
310    ops = None
311    monitoring_attrs = None
312    targets = None
313    schemes = None
314    kdamond = None
315    idx = None
316
317    def __init__(self, ops='paddr', monitoring_attrs=DamonAttrs(), targets=[],
318            schemes=[]):
319        self.ops = ops
320        self.monitoring_attrs = monitoring_attrs
321        self.monitoring_attrs.context = self
322
323        self.targets = targets
324        for idx, target in enumerate(self.targets):
325            target.idx = idx
326            target.context = self
327
328        self.schemes = schemes
329        for idx, scheme in enumerate(self.schemes):
330            scheme.idx = idx
331            scheme.context = self
332
333    def sysfs_dir(self):
334        return os.path.join(self.kdamond.sysfs_dir(), 'contexts',
335                '%d' % self.idx)
336
337    def stage(self):
338        err = write_file(
339                os.path.join(self.sysfs_dir(), 'operations'), self.ops)
340        if err is not None:
341            return err
342        err = self.monitoring_attrs.stage()
343        if err is not None:
344            return err
345
346        nr_targets_file = os.path.join(
347                self.sysfs_dir(), 'targets', 'nr_targets')
348        content, err = read_file(nr_targets_file)
349        if err is not None:
350            return err
351        if int(content) != len(self.targets):
352            err = write_file(nr_targets_file, '%d' % len(self.targets))
353            if err is not None:
354                return err
355        for target in self.targets:
356            err = target.stage()
357            if err is not None:
358                return err
359
360        nr_schemes_file = os.path.join(
361                self.sysfs_dir(), 'schemes', 'nr_schemes')
362        content, err = read_file(nr_schemes_file)
363        if err is not None:
364            return err
365        if int(content) != len(self.schemes):
366            err = write_file(nr_schemes_file, '%d' % len(self.schemes))
367            if err is not None:
368                return err
369        for scheme in self.schemes:
370            err = scheme.stage()
371            if err is not None:
372                return err
373        return None
374
375class Kdamond:
376    state = None
377    pid = None
378    contexts = None
379    idx = None      # index of this kdamond between siblings
380    kdamonds = None # parent
381
382    def __init__(self, contexts=[]):
383        self.contexts = contexts
384        for idx, context in enumerate(self.contexts):
385            context.idx = idx
386            context.kdamond = self
387
388    def sysfs_dir(self):
389        return os.path.join(self.kdamonds.sysfs_dir(), '%d' % self.idx)
390
391    def start(self):
392        nr_contexts_file = os.path.join(self.sysfs_dir(),
393                'contexts', 'nr_contexts')
394        content, err = read_file(nr_contexts_file)
395        if err is not None:
396            return err
397        if int(content) != len(self.contexts):
398            err = write_file(nr_contexts_file, '%d' % len(self.contexts))
399            if err is not None:
400                return err
401
402        for context in self.contexts:
403            err = context.stage()
404            if err is not None:
405                return err
406        err = write_file(os.path.join(self.sysfs_dir(), 'state'), 'on')
407        return err
408
409    def stop(self):
410        err = write_file(os.path.join(self.sysfs_dir(), 'state'), 'off')
411        return err
412
413    def update_schemes_tried_regions(self):
414        err = write_file(os.path.join(self.sysfs_dir(), 'state'),
415                         'update_schemes_tried_regions')
416        if err is not None:
417            return err
418        for context in self.contexts:
419            for scheme in context.schemes:
420                tried_regions = []
421                tried_regions_dir = os.path.join(
422                        scheme.sysfs_dir(), 'tried_regions')
423                for filename in os.listdir(
424                        os.path.join(scheme.sysfs_dir(), 'tried_regions')):
425                    tried_region_dir = os.path.join(tried_regions_dir, filename)
426                    if not os.path.isdir(tried_region_dir):
427                        continue
428                    region_values = []
429                    for f in ['start', 'end', 'nr_accesses', 'age']:
430                        content, err = read_file(
431                                os.path.join(tried_region_dir, f))
432                        if err is not None:
433                            return err
434                        region_values.append(int(content))
435                    tried_regions.append(DamosTriedRegion(*region_values))
436                scheme.tried_regions = tried_regions
437
438    def update_schemes_tried_bytes(self):
439        err = write_file(os.path.join(self.sysfs_dir(), 'state'),
440                'update_schemes_tried_bytes')
441        if err is not None:
442            return err
443        for context in self.contexts:
444            for scheme in context.schemes:
445                content, err = read_file(os.path.join(scheme.sysfs_dir(),
446                    'tried_regions', 'total_bytes'))
447                if err is not None:
448                    return err
449                scheme.tried_bytes = int(content)
450
451    def update_schemes_stats(self):
452        err = write_file(os.path.join(self.sysfs_dir(), 'state'),
453                'update_schemes_stats')
454        if err is not None:
455            return err
456        for context in self.contexts:
457            for scheme in context.schemes:
458                stat_values = []
459                for stat in ['nr_tried', 'sz_tried', 'nr_applied',
460                             'sz_applied', 'qt_exceeds']:
461                    content, err = read_file(
462                            os.path.join(scheme.sysfs_dir(), 'stats', stat))
463                    if err is not None:
464                        return err
465                    stat_values.append(int(content))
466                scheme.stats = DamosStats(*stat_values)
467
468    def update_schemes_effective_quotas(self):
469        err = write_file(os.path.join(self.sysfs_dir(), 'state'),
470                         'update_schemes_effective_quotas')
471        if err is not None:
472            return err
473        for context in self.contexts:
474            for scheme in context.schemes:
475                for goal in scheme.quota.goals:
476                    content, err = read_file(
477                            os.path.join(scheme.quota.sysfs_dir(),
478                                         'effective_bytes'))
479                    if err is not None:
480                        return err
481                    goal.effective_bytes = int(content)
482        return None
483
484    def commit(self):
485        nr_contexts_file = os.path.join(self.sysfs_dir(),
486                'contexts', 'nr_contexts')
487        content, err = read_file(nr_contexts_file)
488        if err is not None:
489            return err
490        if int(content) != len(self.contexts):
491            err = write_file(nr_contexts_file, '%d' % len(self.contexts))
492            if err is not None:
493                return err
494
495        for context in self.contexts:
496            err = context.stage()
497            if err is not None:
498                return err
499        err = write_file(os.path.join(self.sysfs_dir(), 'state'), 'commit')
500        return err
501
502
503    def commit_schemes_quota_goals(self):
504        for context in self.contexts:
505            for scheme in context.schemes:
506                for goal in scheme.quota.goals:
507                    err = goal.stage()
508                    if err is not None:
509                        print('commit_schemes_quota_goals failed stagign: %s'%
510                              err)
511                        exit(1)
512        return write_file(os.path.join(self.sysfs_dir(), 'state'),
513                         'commit_schemes_quota_goals')
514
515class Kdamonds:
516    kdamonds = []
517
518    def __init__(self, kdamonds=[]):
519        self.kdamonds = kdamonds
520        for idx, kdamond in enumerate(self.kdamonds):
521            kdamond.idx = idx
522            kdamond.kdamonds = self
523
524    def sysfs_dir(self):
525        return os.path.join(sysfs_root, 'kdamonds')
526
527    def start(self):
528        err = write_file(os.path.join(self.sysfs_dir(),  'nr_kdamonds'),
529                '%s' % len(self.kdamonds))
530        if err is not None:
531            return err
532        for kdamond in self.kdamonds:
533            err = kdamond.start()
534            if err is not None:
535                return err
536        return None
537
538    def stop(self):
539        for kdamond in self.kdamonds:
540            err = kdamond.stop()
541            if err is not None:
542                return err
543        return None
544