ronnie
2022-10-14 1504bb53e29d3d46222c0b3ea994fc494b48e153
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file.
 
import argparse
import logging
import mmap
import os
import signal
import struct
import sys
import threading
import time
 
# some magic numbers: see http://goo.gl/ecAgke for Intel docs
PCI_IMC_BAR_OFFSET = 0x48
IMC_DRAM_GT_REQUESTS = 0x5040 # GPU
IMC_DRAM_IA_REQUESTS = 0x5044 # CPU
IMC_DRAM_IO_REQUESTS = 0x5048 # PCIe, Display Engine, USB, etc.
IMC_DRAM_DATA_READS = 0x5050  # read traffic
IMC_DRAM_DATA_WRITES = 0x5054 # write traffic
IMC_MMAP_SIZE = 0x6000
 
CACHE_LINE = 64.0
MEGABYTE = 1048576.0
 
RATE_FIELD_FORMAT = '%s: %5d MB/s'
RAW_FIELD_FORMAT = '%s: %d'
 
class IMCCounter:
    """Small struct-like class to keep track of the
    location and attributes for each counter.
 
    Parameters:
      name: short, unique identifying token for this
        counter type
      idx: offset into the IMC memory where we can find
        this counter
      total: True if we should count this in the number
        for total bandwidth
    """
    def __init__(self, name, idx, total):
        self.name = name
        self.idx = idx
        self.total = total
 
 
counters = [
#              name          idx           total
    IMCCounter("GT", IMC_DRAM_GT_REQUESTS, False),
    IMCCounter("IA", IMC_DRAM_IA_REQUESTS, False),
    IMCCounter("IO", IMC_DRAM_IO_REQUESTS, False),
    IMCCounter("RD", IMC_DRAM_DATA_READS,  True),
    IMCCounter("WR", IMC_DRAM_DATA_WRITES, True),
]
 
 
class MappedFile:
    """Helper class to wrap mmap calls in a context
    manager so they are always cleaned up, and to
    help extract values from the bytes.
 
    Parameters:
      filename: name of file to mmap
      offset: offset from beginning of file to mmap
        from
      size: amount of the file to mmap
    """
    def __init__(self, filename, offset, size):
        self._filename = filename
        self._offset = offset
        self._size = size
 
 
    def __enter__(self):
        self._f = open(self._filename, 'rb')
        try:
            self._mm = mmap.mmap(self._f.fileno(),
                                 self._size,
                                 mmap.MAP_SHARED,
                                 mmap.PROT_READ,
                                 offset=self._offset)
        except mmap.error:
            self._f.close()
            raise
        return self
 
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self._mm.close()
        self._f.close()
 
 
    def bytes_to_python(self, offset, fmt):
        """Grab a portion of an mmapped file and return the bytes
        as a python object.
 
        Parameters:
          offset: offset into the mmapped file to start at
          fmt: string containing the struct type to extract from the
            file
        Returns: a Struct containing the bytes starting at offset
          into the mmapped file, reified as python values
        """
        s = struct.Struct(fmt)
        return s.unpack(self._mm[offset:offset+s.size])
 
 
def file_bytes_to_python(f, offset, fmt):
    """Grab a portion of a regular file and return the bytes
    as a python object.
 
    Parameters:
      f: file-like object to extract from
      offset: offset into the mmapped file to start at
      fmt: string containing the struct type to extract from the
        file
    Returns: a Struct containing the bytes starting at offset into
      f, reified as python values
    """
    s = struct.Struct(fmt)
    f.seek(0)
    bs = f.read()
    if len(bs) >= offset + s.size:
        return s.unpack(bs[offset:offset+s.size])
    else:
        raise IOError('Invalid seek in file')
 
 
def uint32_diff(l, r):
    """Compute the difference of two 32-bit numbers as
    another 32-bit number.
 
    Since the counters are monotonically increasing, we
    always want the unsigned difference.
    """
    return l - r if l >= r else l - r + 0x100000000
 
 
class MemoryBandwidthLogger(threading.Thread):
    """Class for gathering memory usage in MB/s on x86 systems.
    raw: dump raw counter values
    seconds_period: time period between reads
 
    If you are using non-raw mode and your seconds_period is
    too high, your results might be nonsense because the counters
    might have wrapped around.
 
    Parameters:
      raw: True if you want to dump raw counters. These will simply
        tell you the number of cache-line-size transactions that
        have occurred so far.
      seconds_period: Duration to wait before dumping counters again.
        Defaults to 2 seconds.
      """
    def __init__(self, raw, seconds_period=2):
        super(MemoryBandwidthLogger, self).__init__()
        self._raw = raw
        self._seconds_period = seconds_period
        self._running = True
 
 
    def run(self):
        # get base address register and align to 4k
        try:
            bar_addr = self._get_pci_imc_bar()
        except IOError:
            logging.error('Cannot read base address register')
            return
        bar_addr = (bar_addr // 4096) * 4096
 
        # set up the output formatting. raw counters don't have any
        # particular meaning in MB/s since they count how many cache
        # lines have been read from or written to up to that point,
        # and so don't represent a rate.
        # TOTAL is always given as a rate, though.
        rate_factor = CACHE_LINE / (self._seconds_period * MEGABYTE)
        if self._raw:
            field_format = RAW_FIELD_FORMAT
        else:
            field_format = RATE_FIELD_FORMAT
 
        # get /dev/mem and mmap it
        with MappedFile('/dev/mem', bar_addr, IMC_MMAP_SIZE) as mm:
            # take initial samples, then take samples every seconds_period
            last_values = self._take_samples(mm)
            while self._running:
                time.sleep(self._seconds_period)
                values = self._take_samples(mm)
                # we need to calculate the MB differences no matter what
                # because the "total" field uses it even when we are in
                # raw mode
                mb_diff = { c.name:
                    uint32_diff(values[c.name], last_values[c.name])
                        * rate_factor for c in counters }
                output_dict = values if self._raw else mb_diff
                output = list((c.name, output_dict[c.name]) for c in counters)
 
                total_rate = sum(mb_diff[c.name] for c in counters if c.total)
                output_str = \
                    ' '.join(field_format % (k, v) for k, v in output) + \
                    ' ' + (RATE_FIELD_FORMAT % ('TOTAL', total_rate))
 
                logging.debug(output_str)
                last_values = values
 
 
    def stop(self):
        self._running = False
 
 
    def _get_pci_imc_bar(self):
        """Get the base address register for the IMC (integrated
        memory controller). This is later used to extract counter
        values.
 
        Returns: physical address for the IMC.
        """
        with open('/proc/bus/pci/00/00.0', 'rb') as pci:
            return file_bytes_to_python(pci, PCI_IMC_BAR_OFFSET, '=Q')[0]
 
 
    def _take_samples(self, mm):
        """Get samples for each type of memory transaction.
 
        Parameters:
          mm: MappedFile representing physical memory
        Returns: dictionary mapping counter type to counter value
        """
        return { c.name: mm.bytes_to_python(c.idx, '=I')[0]
            for c in counters }