File: imb-speed.py

package info (click to toggle)
intel-ipsec-mb 2.0.1-2
  • links: PTS
  • area: main
  • in suites: forky, sid
  • size: 17,404 kB
  • sloc: ansic: 104,071; asm: 64,976; pascal: 18,149; javascript: 5,637; python: 1,464; makefile: 799
file content (542 lines) | stat: -rwxr-xr-x 19,589 bytes parent folder | download | duplicates (2)
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
#!/usr/bin/env python3

"""
**********************************************************************
  Copyright(c) 2024, Intel Corporation All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions
  are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in
      the documentation and/or other materials provided with the
      distribution.
    * Neither the name of Intel Corporation nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************************************
"""

import sys
import platform
import os
import queue
import threading
import subprocess

COL_WIDTH = 19
# number of variants to run
TOTAL_VARIANTS = 0
# dictionary to store env vars
ENVS = None
# queues to store todo and completed variants
TODO_Q = None
DONE_Q = None
# perf application name
PERF_APP = ''
# exit on error flag
EXIT_ERROR = False

class Variant:
    """Class to setup and run test case variant"""
    def __init__(self, idx=None, arch=None, direction='encrypt', cipher_alg=None,
                 hash_alg=None, aead_alg=None, sizes=None, time_box=3000,
                 throughput=None):
        """Build perf app command line"""
        global PERF_APP

        self.idx = idx
        self.arch = arch
        self.direction = direction
        self.cipher_alg = cipher_alg
        self.hash_alg = hash_alg
        self.aead_alg = aead_alg
        self.sizes = sizes
        self.cmd = '{} --no-progress-bar '.format(PERF_APP)
        self.cmd_output = ''
        self.out = []
        self.time_box = time_box
        self.throughput = throughput

        if self.arch is not None:
            self.cmd += ' --arch {}'.format(self.arch)

        if self.aead_alg is not None:
            self.cmd += ' --aead-algo {}'.format(self.aead_alg)

        if self.cipher_alg is not None:
            self.cmd += ' --cipher-algo {}'.format(self.cipher_alg)

        if self.hash_alg is not None:
            self.cmd += ' --hash-algo {}'.format(self.hash_alg)

        if self.cipher_alg is not None or \
           self.aead_alg is not None:
            self.cmd += ' --cipher-dir {}'.format(self.direction)

        if self.sizes is not None:
            self.cmd += ' --job-size {}'.format(self.sizes)

        if self.time_box is not None:
            self.cmd += ' --time-box {}'.format(self.time_box)
                
        if self.throughput is not None:
            self.cmd += ' --throughput'

    def run(self):
        """Run perf app and store output"""
        try:
            self.cmd_output = \
                subprocess.run(self.cmd, \
                               stdout=subprocess.PIPE, \
                               stderr=subprocess.PIPE, \
                               shell=True, env=ENVS, \
                               check=True).stdout.decode('utf-8')
            return True
        except subprocess.CalledProcessError as e:
            self.cmd_output = e.stderr.decode('utf-8')
            return False
    
    def set_core(self, core):
        """Set core to run perf app on"""
        self.core = core
        mask = 1 << core
        self.cmd += ' --cores {}'.format(str(hex(mask)))

    def get_output(self):
        """Get output from run"""
        return self.cmd_output

    def get_cmd(self):
        """Get variant command line"""
        return self.cmd

    def get_idx(self):
        """Get assigned index"""
        return self.idx

def run_test(core=None):
    """
    Main processing thread function
    1. Run performance test for variant
    2. Place completed variants in completed (done) queue
    """
    
    global EXIT_ERROR

    while TODO_Q.empty() is False:
        variant = TODO_Q.get()

        # skip if error encountered
        if EXIT_ERROR is True:
            continue

        # set core if specified
        if core is not None:
            variant.set_core(core)

        # run variant
        if variant.run() is False:
            print('Error encountered running: {}\nOutput:\n{}'\
                    .format(variant.get_cmd(),
                            variant.get_output()),
                    file=sys.stderr)
            EXIT_ERROR = True

        DONE_Q.put(variant)
        TODO_Q.task_done()

class SpeedTool(object):
    """
    Main class
    """
    @staticmethod
    def usage():
        """
        Prints usage
        """
        print("This tool prints imb_perf throughput in openssl speed format.")
        print("Usage:")
        print("\timb-speed.py [-evp algo] [-bytes int] [-seconds int]\n")
        print("\t             [-arch arch] [-c/-cores mask]\n")
        print("\t-arch      - takes the architecture. default: best architecture")
        print("\t-bytes     - takes custom-sized buffer - single value, list or range.")
        print("\t             list: sizes separated by comma (e.g. 16,64,256)")
        print("\t             range: min:step:max (e.g. 16:16:256)")
        print("\t             default: 16, 64, 256, 1024, 8192, 16384")
        print("\t-seconds   - takes timebox in seconds. default: 3")
        print("\t-c/-cores  - takes list/range of cores e.g. 2-8 or 3,4,5. default: none")
        print("\t-decrypt   - time decryption instead of encryption")
        print("\t-no-skip   - do not skip algorithms (some algorithms skipped " \
            "by default due to buffer size incompatibility)")
        print("\t-evp       - takes algorithm")
        print("Example:")
        print("\timb-speed.py -evp aes-gcm-256 -bytes 16834 -seconds 3 -cores 2,4 -arch AVX512")

    def parse_cores(self, core_str):
        """Parse core list passed through command line"""
        num_cores = os.cpu_count()
        cores = []

        # remove spaces
        core_str.replace(" ", "")

        # check if not a range
        if '-' not in core_str:
            cores = list(map(int, core_str.strip().split(',')))
        else:
            # parse range e.g. 2-8
            core_str = core_str.strip().split('-')
            for i in range(int(core_str[0]), int(core_str[1]) + 1):
                cores.append(i)

        # ensure valid cores specified
        for core in cores:
            if core < 0 or core >= num_cores:
                print("Core {} out of range!".format(core), file=sys.stderr)
                raise Exception()

        return cores

    def parse_args(self):
        """
        Get commandline arguments
        """
        global ARCH
        global ALGO
        global PACKET_SIZE
        global TIMEBOX
        global CORES
        global DECRYPT
        global SKIP

        ARCH = None
        ALGO = None
        PACKET_SIZE = "16,64,256,1024,8192,16384"
        TIMEBOX = 3000
        CORES = None
        DECRYPT = False
        SKIP = True

        for i in range(len(sys.argv)):
            arg = sys.argv[i]
            if arg == "-arch":
                if sys.argv[i+1]:
                    ARCH = sys.argv[i+1]
                else:
                    print("Please enter the architecture")
                    print("Default: AVX2, AVX512, SSE")
                    sys.exit(1)
            if arg == "-bytes":
                if sys.argv[i+1]:
                    PACKET_SIZE = sys.argv[i+1]
                else:
                    print("Please enter a single value, list or range for the packet sizes in bytes")
                    print("list: Sizes separated by comma, range: min:step:max (e.g. 16:16:256)")
                    print("Default: 16, 64, 256, 1024, 8192, 16384")
                    sys.exit(1)
            if arg == "-seconds":
                if sys.argv[i+1].isdigit():
                    TIMEBOX = int(sys.argv[i+1]) * 1000 # to ms
                else:
                    print("Please enter a number for the timebox in seconds")
                    print("Default: 3 seconds")
                    sys.exit(1)
            if arg == "-cores" or arg == "-c":
                if sys.argv[i+1]:
                    CORES = self.parse_cores(str(sys.argv[i+1]))
                else:
                    print("Please enter a list/range of cores")
                    print("Default: None")
                    sys.exit(1)
            if arg == "-evp":
                if sys.argv[i+1]:
                    ALGO = sys.argv[i+1]
                else:
                    print("Please enter the algorithm")
                    sys.exit(1)
            if arg == "-decrypt":
                DECRYPT = True
            if arg == "-no-skip":
                SKIP = False
            if arg == "-h":
                self.usage()
                sys.exit(1)

    def get_info(self):
        """get system and app info from perf app output"""
        global PERF_APP
        archs = None
        best_arch = None
        cipher_algos = None
        hash_algos = None
        aead_algos = None

        cmd = PERF_APP + ' --print-info'

        try:
            res = subprocess.run(cmd, stdout=subprocess.PIPE, \
                                stderr=subprocess.STDOUT, \
                                env=ENVS, shell=True, check=True)
            output = res.stdout.decode('utf-8')
        except subprocess.CalledProcessError as e:
            print("Error (" + str(e.returncode) + ")")
            print(e.output.decode('utf-8'))
            sys.exit(1)

        lines = output.rstrip().split('\n')
        try:
            for line in lines:
                info = line.split(':')
                if info[0] == 'Supported architectures':
                    archs = info[1].split()
                if info[0] == 'Best architecture':
                    best_arch = info[1].split()
                if info[0] == 'Supported cipher algorithms':
                    cipher_algos = info[1].split()
                if info[0] == 'Supported hash algorithms':
                    hash_algos = info[1].split()
                if info[0] == 'Supported aead algorithms':
                    aead_algos = info[1].split()
        except:
            print("Error parsing --print-info output:\n" \
                "{}".format(output), file=sys.stderr)

        if archs is None or best_arch is None or cipher_algos is None \
        or hash_algos is None or aead_algos is None:
            print("Error parsing system and app information", file=sys.stderr)
            sys.exit(1)

        return archs, best_arch, cipher_algos, hash_algos, aead_algos


    def parse_results(self, variants):
        """Parse output of perf app for variant"""

        # Generate the list of sizes
        if ':' in PACKET_SIZE:
            min_val, step, max_val = map(int, PACKET_SIZE.split(':'))
            sizes = [str(size) for size in range(min_val, max_val + 1, step)]
        else:
            sizes = PACKET_SIZE.split(',')

        print("\n" + f"Type".ljust(COL_WIDTH+10)+" ".join(f"{j} bytes".ljust(COL_WIDTH) for j in sizes))

        # print throughput for all variants
        for var in variants:
            lines = var.get_output().split('\n')

            hash_alg = None
            cipher_alg = None
            key_size = None
            direction = None
            arch = None
            for line in lines:
                if line.startswith('ARCH'):
                    arch = line.split('\t')[1]
                if line.startswith('HASH_ALG'):
                    hash_alg = line.split('\t')[1]
                    alg = hash_alg
                if line.startswith('CIPHER'):
                    cipher_alg = line.split('\t')[1]
                    alg = cipher_alg
                if line.startswith('KEY_SIZE'):
                    key_size = line.split('\t')[1]

            if hash_alg is not None and not hash_alg.startswith("NULL"):
                alg = hash_alg
            if cipher_alg is not None and not cipher_alg.startswith("NULL"):
                alg = cipher_alg + " " + key_size

            alg = alg + " " + arch 

            values = {}
            time_in_seconds = TIMEBOX / 1000
            for line in lines:
                parts = line.split('\t')
                if parts[0] in sizes and parts[0] != '':
                    values[parts[0]] = round((float(parts[1]) / 1000) / time_in_seconds, 2) # Convert bytes to kilobytes per second

            print(f"{alg}".ljust(COL_WIDTH+10)+" ".join(f"{values[j]}k".ljust(COL_WIDTH) for j in sizes))

    def measure(self):
        """
        Measure function to:
        - generate and enqueue list of variants to run
        - schedule and run variants 
        """
        global TOTAL_VARIANTS
        global ENVS
        global TODO_Q
        global DONE_Q
        global PERF_APP

        result = [] # list to store parsed results

        # init vars
        TOTAL_VARIANTS = 0

        # include perf directory in PATH
        path = '{}:{}'.format(os.getenv('PATH'), os.getenv('PWD'))

        # set LD_LIBRARY_PATH if not already set
        lib_path = os.getenv('LD_LIBRARY_PATH')
        if lib_path is None:
            lib_path = '../lib'

        # create env vars dictionary to pass to subprocess module
        ENVS = {'PATH' : path, 'LD_LIBRARY_PATH' : lib_path}

        # init queues to store todo and completed variants
        TODO_Q = queue.Queue()
        DONE_Q = queue.Queue()

        # detect OS and select app name
        if platform.system() == 'Windows':
            PERF_APP = 'imb-perf.exe'
        else:
            PERF_APP = 'imb-perf'

        # set directions
        directions = ['encrypt']
        if DECRYPT is True:
            directions = ['decrypt']

        supported_archs, best_arch, cipher_algos, hash_algos, aead_algos = self.get_info()

        if ARCH is None:
            archs = best_arch
        elif ARCH in supported_archs:
            archs = [ARCH]
        else:
            print('Supported architectures: {}'.format(', '.join(supported_archs)), file=sys.stderr)
            print('Unknown architecture: {}'.format(ARCH), file=sys.stderr)
            return result

        # print args
        print('Measuring:', file=sys.stderr)
        print('  Architecture: {}'.format(archs), file=sys.stderr)
        print('  Size(s): {}'.format(PACKET_SIZE), file=sys.stderr)
        if ALGO is not None:
            print('  Algo: {}'.format(ALGO), file=sys.stderr)
        else:
            print('  Algo: All', file=sys.stderr)
        if CORES is not None:
            print('  Cores: {}'.format(CORES), file=sys.stderr)
        if TIMEBOX is not None:
            print('  Timebox: {} seconds'.format(TIMEBOX/1000), file=sys.stderr)
        if DECRYPT is False:
            print('  Direction: encrypt', file=sys.stderr)
        else:
            print('  Direction: decrypt', file=sys.stderr)

        AEAD_ALGO = None
        HASH_ALGO = None
        CIPHER_ALGO = None
        # Determine the type of ALGO
        if ALGO is None:
            AEAD_ALGO = aead_algos
            HASH_ALGO = hash_algos
            CIPHER_ALGO = cipher_algos
        elif ALGO in aead_algos:
            AEAD_ALGO = [ALGO]
        elif ALGO in hash_algos:
            HASH_ALGO = [ALGO]
        elif ALGO in cipher_algos:
            CIPHER_ALGO = [ALGO]
        else:
            print('\nAEAD algorithms: {}'.format(', '.join(aead_algos)), file=sys.stderr)
            print('\nHash/digest algorithms: {}'.format(', '.join(hash_algos)), file=sys.stderr)
            print('\nCipher algorithms: {}'.format(', '.join(cipher_algos)), file=sys.stderr)
            print('\nUnknown algorithm: {}'.format(ALGO), file=sys.stderr)
            return result

        for arch in archs:
            skip_cipher_algos = ['zuc-eea3', 'zuc-eea3-256', 'kasumi-uea1']
            skip_hash_algos = ['zuc-eia3', 'zuc-eia3-256','kasumi-uia1']
            skip_aead_algos = ['aes-docsis-128-crc32', 'aes-docsis-256-crc32']

            if HASH_ALGO is not None:
                for algo in HASH_ALGO:
                    if SKIP and algo in skip_hash_algos:
                        continue
                    TODO_Q.put(Variant(idx=TOTAL_VARIANTS, arch=arch, direction=None,
                                    sizes=PACKET_SIZE, hash_alg=algo, time_box=TIMEBOX,
                                    throughput=True))
                    TOTAL_VARIANTS += 1
            if AEAD_ALGO is not None:
                for direction in directions:
                    for algo in AEAD_ALGO:
                        if SKIP and algo in skip_aead_algos:
                            continue
                        TODO_Q.put(Variant(idx=TOTAL_VARIANTS, arch=arch, direction=direction,
                                        sizes=PACKET_SIZE, aead_alg=algo, time_box=TIMEBOX,
                                        throughput=True))
                        TOTAL_VARIANTS += 1
            if CIPHER_ALGO is not None:
                for direction in directions:
                    for algo in CIPHER_ALGO:
                        if SKIP and algo in skip_cipher_algos:
                            continue
                        TODO_Q.put(Variant(idx=TOTAL_VARIANTS, arch=arch, direction=direction,
                                        sizes=PACKET_SIZE, cipher_alg=algo, time_box=TIMEBOX,
                                        throughput=True))
                        TOTAL_VARIANTS += 1
        
        # If cores selected start a new thread on each core
        # otherwise start single thread without specifying a core
        #
        # Each thread takes a variant from the todo queue
        # and places it in the done queue when complete
        if CORES is None:
            threading.Thread(target=run_test).start()
        else:
            for core in CORES:
                threading.Thread(target=run_test, args=(core,)).start()
        
        # wait for all threads to complete
        TODO_Q.join()

        # transfer completed runs from the
        # done queue to the results list
        while DONE_Q.empty() is False:
            variant = DONE_Q.get()
            result.append(variant)

        # sort by idx
        result.sort(key=lambda x: x.get_idx())

        return result

    def run(self):
        """
        Main method
        """

        # parse command line arguments
        self.parse_args()

        # measure performance
        results = self.measure()

        # parse and print results
        if len(results) > 0:
            self.parse_results(results)

if __name__ == '__main__':
    SpeedTool().run()