aboutsummaryrefslogtreecommitdiff
path: root/lnt/testing/profile/profilev2impl.py
blob: ccb75e96687d9a1129b49845766e21cbb3c60b2e (plain)
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
from profile import ProfileImpl
import StringIO
import bz2
import copy
import io
import os
import struct

"""
ProfileV2 is a profile data representation designed to keep the
profile data on-disk as small as possible while still maintaining good
access speed, specifically to avoid having to load the entire file to
discover just the index (functions and top level counters).

The format:
  * Is a binary format consisting of an index and several sections.
  * Consists of only two datatypes:
    * Strings (newline terminated)
    * Positive integers (ULEB encoded)
  * Some sections are expected to be BZ2 compressed.

The sections are:
  Header
      Contains the disassembly format.

  Counter name pool
      Contains a list of strings for the counter names ("cycles" etc).
      In the rest of the file, counters are referred to by an index into
      this list.

  Top level counters
      Contains key/value pairs for the top level (per-file) counters.

  Functions
      For each function, contains the counters for that function, the number
      of instructions and indices into the LineAddresses, LineCounters and
      LineText sections.

  LineAddresses
      A flat list of numbers, addresses are encoded as offsets from the
      previous address. Each Function knows its starting index into this list
      (the first address is an offset from zero) and how many addresses to
      read.

  LineCounters
      A list of floating point values, one for each counter in the counter name
      pool. There are len(counterNamePool) counter values per instruction.

      Like LineAddresses, Functions know their own index into the LineCounter
      table.

      The numbers are floating point numbers that are bitconverted into
      integers.

  LineText
      A list of offsets into the TextPool, which is a simple string pool.
      Again, Functions know their index into the LineText section.

  TextPool
      A simple string pool.

  The LineAddresses and LineCounters sections are designed to hold very
  repetitive data that is very easy to compress. The LineText section allows
  repeated strings to be reused (for example 'add r0, r0, r0').

  The LineAddresses, LineCounters, LineText and TextPool sections are BZ2
  compressed.

  The TextPool section has the ability to be shared across multiple profiles
  to take advantage of inter-run redundancy (the image very rarely changes
  substantially). This pooling ability is not yet implemented but the
  appropriate scaffolding is in place.

  The ProfileV2 format gives a ~3x size improvement over the ProfileV1 (which
  is also compressed) - meaning a ProfileV2 is roughly 1/3 the size of
  ProfileV1. With text pooling, ProfileV2s can be even smaller.

  Not only this, but for the simple task of enumerating the functions in a
  profile we do not need to do any decompression at all.
"""

##############################################################################
# Utility functions


def readNum(fobj):
    """
    Reads a ULEB encoded number from a stream.
    """
    n = 0
    shift = 0
    while True:
        b = ord(fobj.read(1))
        n |= (b & 0x7F) << shift
        shift += 7
        if (b & 0x80) == 0:
            return n


def writeNum(fobj, n):
    """
    Write 'n' as a ULEB encoded number to a stream.
    """
    while True:
        b = n & 0x7F
        n >>= 7
        if n != 0:
            b |= 0x80
        fobj.write(chr(b))

        if n == 0:
            break


def readString(fobj):
    """
    Read a string from a stream.
    """
    return fobj.readline()[:-1]


def writeString(fobj, s):
    """
    Write a string to a stream.
    """
    fobj.write(str(s))
    fobj.write('\n')


def readFloat(fobj):
    """
    Read a floating point number from a stream.
    """
    num = readNum(fobj)
    packed = struct.pack('>l', num)
    f = struct.unpack('>f', packed)[0]
    return f


def writeFloat(fobj, f):
    """
    Write a floating point number to a stream.
    """
    if f == 0.0:
        writeNum(fobj, 0)
        return
    packed = struct.pack('>f', f)
    bits = struct.unpack('>l', packed)[0]
    writeNum(fobj, bits)

##############################################################################
# Abstract section types


class Section(object):
    def writeHeader(self, fobj, offset, size):
        writeNum(fobj, offset)
        writeNum(fobj, size)

    def readHeader(self, fobj):
        self.offset = readNum(fobj)
        self.size = readNum(fobj)

    def write(self, fobj):
        return self.serialize(fobj)

    def read(self, fobj):
        fobj.seek(self.offset + self.start)
        return self.deserialize(fobj)

    def setStart(self, start):
        """
        Set where this section's offset is calculated from.
        """
        self.start = start

    def copy(self):
        return copy.copy(self)


class CompressedSection(Section):
    def read(self, fobj):
        fobj.seek(self.offset + self.start)
        _io = StringIO.StringIO(bz2.decompress(fobj.read(self.size)))
        return self.deserialize(_io)

    def write(self, fobj):
        _io = io.BytesIO()
        self.serialize(_io)
        fobj.write(bz2.compress(_io.getvalue()))


class MaybePooledSection(Section):
    """
    A section that is normally compressed, but can optionally be
    inside an external file (where this section from multiple
    files are pooled together)
    """
    def __init__(self):
        self.pool_fname = ''

    def readHeader(self, fobj):
        Section.readHeader(self, fobj)
        self.pool_fname = readString(fobj)

    def writeHeader(self, fobj, offset, size):
        Section.writeHeader(self, fobj, offset, size)
        writeString(fobj, self.pool_fname)

    def read(self, fobj):
        if self.pool_fname:
            raise NotImplementedError()

        else:
            _io = StringIO.StringIO(bz2.decompress(fobj.read(self.size)))
            self.size = len(_io.getvalue())
            return self.deserialize(_io)

    def write(self, fobj):
        _io = StringIO.StringIO()
        if self.pool_fname:
            raise NotImplementedError()

        else:
            Section.write(self, _io)
            fobj.write(bz2.compress(_io.getvalue()))

##############################################################################
# Concrete section types


class Header(Section):
    def serialize(self, fobj):
        writeString(fobj, self.disassembly_format)

    def deserialize(self, fobj):
        self.disassembly_format = readString(fobj)

    def upgrade(self, impl):
        self.disassembly_format = impl.getDisassemblyFormat()

    def __repr__(self):
        pass


class CounterNamePool(Section):
    """
    Maps counter names to indices. It allows later sections to refer to
    counters by index.
    """
    def serialize(self, fobj):
        n_names = len(self.idx_to_name)
        writeNum(fobj, n_names)
        for i in xrange(n_names):
            writeString(fobj, self.idx_to_name[i])

    def deserialize(self, fobj):
        self.idx_to_name = {}
        for i in xrange(readNum(fobj)):
            self.idx_to_name[i] = readString(fobj)
        self.name_to_idx = {v: k
                            for k, v
                            in self.idx_to_name.items()}

    def upgrade(self, impl):
        self.idx_to_name = {}

        keys = impl.getTopLevelCounters().keys()
        for f in impl.getFunctions().values():
            keys += f['counters'].keys()
        keys = sorted(set(keys))

        self.idx_to_name = {k: v for k, v in enumerate(keys)}
        self.name_to_idx = {v: k for k, v in enumerate(keys)}


class TopLevelCounters(Section):
    def __init__(self, counter_name_pool):
        self.counter_name_pool = counter_name_pool

    def serialize(self, fobj):
        writeNum(fobj, len(self.counters))
        for k, v in sorted(self.counters.items()):
            writeNum(fobj, self.counter_name_pool.name_to_idx[k])
            writeNum(fobj, int(v))

    def deserialize(self, fobj):
        self.counters = {}
        for i in xrange(readNum(fobj)):
            k = readNum(fobj)
            v = readNum(fobj)
            self.counters[self.counter_name_pool.idx_to_name[k]] = v

    def upgrade(self, impl):
        self.counters = impl.data['counters'].copy()

    def copy(self, cnp):
        new = copy.copy(self)
        new.counter_name_pool = cnp
        return new


class LineCounters(CompressedSection):
    def __init__(self, impl=None):
        self.impl = impl
        self.function_offsets = {}

    def serialize(self, fobj):
        assert self.impl

        self.function_offsets = {}
        start = fobj.tell()
        for fname, f in sorted(self.impl.getFunctions().items()):
            self.function_offsets[fname] = fobj.tell() - start
            all_counters = f['counters'].keys()
            for counters, address, text in self.impl.getCodeForFunction(fname):
                for k in sorted(all_counters):
                    writeFloat(fobj, counters.get(k, 0))

    def deserialize(self, fobj):
        self.data = fobj.read()

    def upgrade(self, impl):
        self.impl = impl
        self.function_offsets = {}

    def getOffsetFor(self, fname):
        return self.function_offsets[fname]

    def setOffsetFor(self, fname, value):
        self.function_offsets[fname] = value

    def extractForFunction(self, fname, counters):
        offset = self.function_offsets[fname]
        _io = StringIO.StringIO(self.data)
        _io.seek(offset)
        counters.sort()
        while True:
            c = {}
            for k in counters:
                c[k] = readFloat(_io)
            yield c


class LineAddresses(CompressedSection):
    def __init__(self, impl=None):
        self.impl = impl
        self.function_offsets = {}

    def serialize(self, fobj):
        """
        Addresses are encoded as a delta from the previous address. This allows
        huge compression ratios as the increments (for RISC architectures) will
        usually be constant.
        """
        assert self.impl

        self.function_offsets = {}
        start = fobj.tell()
        for fname in sorted(self.impl.getFunctions()):
            self.function_offsets[fname] = fobj.tell() - start
            prev_address = 0
            for counters, address, text in self.impl.getCodeForFunction(fname):
                # FIXME: Hack around a bug in perf extraction somewhere - if
                # we go off the end of a symbol to a previous symbol,
                # addresses will go backwards!
                writeNum(fobj, max(0, address - prev_address))
                prev_address = address

    def deserialize(self, fobj):
        self.data = fobj.read()

    def upgrade(self, impl):
        self.impl = impl
        self.function_offsets = {}

    def getOffsetFor(self, fname):
        return self.function_offsets[fname]

    def setOffsetFor(self, fname, value):
        self.function_offsets[fname] = value

    def extractForFunction(self, fname):
        offset = self.function_offsets[fname]
        _io = StringIO.StringIO(self.data)
        _io.seek(offset)
        last_address = 0
        while True:
            address = readNum(_io) + last_address
            last_address = address
            yield address


class LineText(CompressedSection):
    """
    Text lines (like "add r0, r0, r0") can be repeated.

    Instead of just storing the text in raw form, we store pointers into
    a text pool. This allows text to be reused, but also reused between
    different profiles if required (the text pools can be extracted
    into a separate file)
    """
    def __init__(self, text_pool, impl=None):
        CompressedSection.__init__(self)
        self.impl = impl
        self.function_offsets = {}
        self.text_pool = text_pool

    def serialize(self, fobj):
        assert self.impl

        self.function_offsets = {}
        start = fobj.tell()
        for fname in sorted(self.impl.getFunctions()):
            self.function_offsets[fname] = fobj.tell() - start
            for counters, address, text in self.impl.getCodeForFunction(fname):
                writeNum(fobj, self.text_pool.getOrCreate(text))
            writeNum(fobj, 0)  # Write sequence terminator

    def deserialize(self, fobj):
        # FIXME: Make this lazy.
        self.data = fobj.read()

    def upgrade(self, impl):
        self.impl = impl
        self.function_offsets = {}

    def getOffsetFor(self, fname):
        return self.function_offsets[fname]

    def setOffsetFor(self, fname, value):
        self.function_offsets[fname] = value

    def extractForFunction(self, fname):
        offset = self.function_offsets[fname]

        _io = StringIO.StringIO(self.data)
        _io.seek(offset)
        while True:
            n = readNum(_io)
            yield self.text_pool.getAt(n)

    def copy(self, tp):
        new = copy.copy(self)
        new.text_pool = tp
        return new


class TextPool(MaybePooledSection):
    def __init__(self):
        MaybePooledSection.__init__(self)
        self.offsets = {}
        # Populate data with a single character initially so that zero is
        # never a valid string pool index. LineText relies upon this to use
        # zero as a sentinel.
        self.data = StringIO.StringIO('\n')
        self.pool_read = False

    def serialize(self, fobj):
        self.data.seek(0)
        fobj.write(self.data.read())

    def deserialize(self, fobj):
        # FIXME: Make this lazy!
        self.data = StringIO.StringIO(fobj.read(self.size))

    def upgrade(self, impl):
        pass

    def getOrCreate(self, text):
        if self.pool_fname and not self.pool_read:
            assert False
            self.readFromPool()

        if text in self.offsets:
            return self.offsets[text]
        self.offsets[text] = self.data.tell()
        writeString(self.data, text)
        return self.offsets[text]

    def getAt(self, offset):
        self.data.seek(offset, os.SEEK_SET)
        return readString(self.data)

    def copy(self):
        return copy.deepcopy(self)


class Functions(Section):
    def __init__(self, counter_name_pool, line_counters,
                 line_addresses, line_text, impl=None):
        self.counter_name_pool = counter_name_pool
        self.line_counters = line_counters
        self.line_addresses = line_addresses
        self.line_text = line_text
        self.impl = impl

    def serialize(self, fobj):
        writeNum(fobj, len(self.functions))
        for name in sorted(self.functions):
            f = self.functions[name]

            writeString(fobj, name)
            writeNum(fobj, f['length'])
            writeNum(fobj, self.line_counters.getOffsetFor(name))
            writeNum(fobj, self.line_addresses.getOffsetFor(name))
            writeNum(fobj, self.line_text.getOffsetFor(name))

            writeNum(fobj, len(f['counters']))
            for k, v in sorted(f['counters'].items()):
                writeNum(fobj, self.counter_name_pool.name_to_idx[k])
                writeFloat(fobj, v)

    def deserialize(self, fobj):
        self.functions = {}
        for i in xrange(readNum(fobj)):
            f = {}
            name = readString(fobj)
            f['length'] = readNum(fobj)
            self.line_counters.setOffsetFor(name, readNum(fobj))
            self.line_addresses.setOffsetFor(name, readNum(fobj))
            self.line_text.setOffsetFor(name, readNum(fobj))
            f['counters'] = {}

            for j in xrange(readNum(fobj)):
                k = self.counter_name_pool.idx_to_name[readNum(fobj)]
                v = readFloat(fobj)
                f['counters'][k] = v

            self.functions[name] = f

    def upgrade(self, impl):
        self.impl = impl
        self.functions = self.impl.getFunctions()

    def getCodeForFunction(self, fname):
        f = self.functions[fname]
        counter_gen = self.line_counters \
            .extractForFunction(fname, f['counters'].keys())
        address_gen = self.line_addresses.extractForFunction(fname)
        text_gen = self.line_text.extractForFunction(fname)
        for n in xrange(f['length']):
            yield (counter_gen.next(), address_gen.next(), text_gen.next())

    def copy(self, counter_name_pool, line_counters,
             line_addresses, line_text):
        new = copy.copy(self)
        new.counter_name_pool = counter_name_pool
        new.line_counters = line_counters
        new.line_addresses = line_addresses
        new.line_text = line_text
        return new


class ProfileV2(ProfileImpl):
    @staticmethod
    def checkFile(fn):
        # The first number is the version (2); ULEB encoded this is simply
        # 0x02.
        return ord(open(fn).read(1)) == 2

    @staticmethod
    def deserialize(fobj):
        p = ProfileV2()

        p.h = Header()
        p.cnp = CounterNamePool()
        p.tlc = TopLevelCounters(p.cnp)
        p.lc = LineCounters(p)
        p.la = LineAddresses(p)
        p.tp = TextPool()
        p.lt = LineText(p.tp, p)
        p.f = Functions(p.cnp, p.lc, p.la, p.lt, p)

        p.sections = [p.h, p.cnp, p.tlc, p.lc, p.la, p.lt, p.tp, p.f]

        version = readNum(fobj)
        assert version == 2

        for section in p.sections:
            section.readHeader(fobj)
        for section in p.sections:
            section.setStart(fobj.tell())
        for section in p.sections:
            section.read(fobj)

        return p

    def serialize(self, fname=None):
        # If we're not writing to a file, emulate a file object instead.
        if fname is None:
            fobj = StringIO.StringIO()
        else:
            fobj = open(fname, 'wb')

        # Take a copy of all sections. While writing we may change
        # offsets / indices, and we need to ensure we can modify our
        # sections' states without affecting the original object (we may
        # need to read from it while writing! (getCodeForFunction))
        h = self.h.copy()
        cnp = self.cnp.copy()
        tlc = self.tlc.copy(cnp)
        lc = self.lc.copy()
        la = self.la.copy()
        tp = self.tp.copy()
        lt = self.lt.copy(tp)
        f = self.f.copy(cnp, lc, la, lt)
        sections = [h, cnp, tlc, lc, la, lt, tp, f]

        writeNum(fobj, 2)  # Version

        # We need to write all sections first, so we know their offset
        # before we write the header.
        tmpio = StringIO.StringIO()
        offsets = {}
        sizes = {}
        for section in sections:
            offsets[section] = tmpio.tell()
            section.write(tmpio)
            sizes[section] = tmpio.tell() - offsets[section]

        for section in sections:
            section.writeHeader(fobj, offsets[section], sizes[section])
        fobj.write(tmpio.getvalue())

        if fname is None:
            return fobj.getvalue()

    @staticmethod
    def upgrade(v1impl):
        assert v1impl.getVersion() == 1

        p = ProfileV2()

        p.h = Header()
        p.cnp = CounterNamePool()
        p.tlc = TopLevelCounters(p.cnp)
        p.lc = LineCounters(p)
        p.la = LineAddresses(p)
        p.tp = TextPool()
        p.lt = LineText(p.tp, p)
        p.f = Functions(p.cnp, p.lc, p.la, p.lt, p)

        p.sections = [p.h, p.cnp, p.tlc, p.lc, p.la, p.lt, p.tp, p.f]

        for section in p.sections:
            section.upgrade(v1impl)

        return p

    def getVersion(self):
        return 2

    def getFunctions(self):
        return self.f.functions

    def getTopLevelCounters(self):
        return self.tlc.counters

    def getCodeForFunction(self, fname):
        return self.f.getCodeForFunction(fname)