[python]Comparing execution time without threading, with threadpoolexecutor and threading subclass

3 round results to print out the execution time of calling the functions

This is a comparison in execution time by sending show version to three Cisco ASA – fw01, fw02 and fw03.
All connections with netmiko.ConnectHandler has a global_delay_factor of 0.5s.

I have made three functions:

  1. connect_device_type_1: This function does not use threading but use netmiko’s ConnectHandler to send show version with use_textfsm.
  2. connect_device_type_2: This function uses concurrent.futures ThreadPoolExecutor to do the same thing.
  3. connect_device_type_3: This function uses modified threading subclass
  4. First round:
    thread1

    Second round:
    threading2

    Third round:
    threading3

    The conclusions:

    1. There is no significant time saver, only save 2-3 seconds
    2. The commands and use of textfsm parsing may not be time consuming enough to show significant difference with and without threading
    3. ThreadPoolExecutor is easy to use, and have similar effect as the threading.Thread class,
    4. With concurrent.futures.ThreadPoolExecutor’s futures.result returns the value of the function running in threads without the need to create a subclass from threading.Thread.

    timer decorator

    This decorator decorates on the functions for testing.

    def timer(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            start_time = time()
            result = fn(*args, **kwargs)
            print(f"Running {fn.__name__} takes {time() - start_time} seconds\n")
            return result
        return wrapper
    

    connect_device_type_1 code

    This is a normal usage of netmiko’s ConnectHandler.

    @timer
    def connect_device_type_1(cmd=None, devices=None, **kwargs):
        collect_results = []
        for device in devices:
            config = get_asa_credential(hostname=device)
            config.update({"device_type": "cisco_asa",
                           "global_delay_factor": 0.5})
            with ConnectHandler(**config) as asa:
                try:
                    collect_results.append(asa.send_command(cmd, use_textfsm=True))
                except Exceptions as e:
                    return e
        return collect_results
    

    connect_device_type_2 code

    This uses ThreadPoolExecutor, I have abbreviate the module name to tpe.

    @timer
    def connect_device_type_2(cmd=None, devices=None, **kwargs):
        collect_results = []
        with tpe(max_workers=4) as executor:
            for device in devices:
                config = get_asa_credential(hostname=device)
                config.update({"device_type": "cisco_asa",
                               "global_delay_factor": 0.5})
                try:
                    with ConnectHandler(**config) as asa:
                        result = executor.submit(asa.send_command, cmd, use_textfsm=True)
                    collect_results.append(result.result())
                except Exceptions as e:
                    return e
        return collect_results
    

    connect_device_type_3 code

    This uses a subclass I created from threading.Thread. The effect is the same as ThreadPoolExecutor.

    @timer
    def connect_device_type_3(cmd=None, devices=None, **kwargs):
        collect_results = []
        threads = []
        for device in devices:
            config = get_asa_credential(hostname=device)
            config.update({"device_type": "cisco_asa",
                           "global_delay_factor": 0.5})
            with ConnectHandler(**config) as asa:
                try:
                    t = CmdThread(target=asa.send_command, args=(cmd,), kwargs={"use_textfsm": True})
                    threads.append(t)
                    t.start()
                except Exceptions as e:
                    print(e)
        for thread in threads:
            collect_results.append(thread.join())
        return collect_results
    

    threading.Thread subclass

    from threading import Thread
    """
    See reference: 
    https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread-in-python/40344234#40344234
    Check the answer by GuySoft.
    """
    
    
    class CmdThread(Thread):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._return = None
    
        def run(self):
            """
            The original run method does not return value after self._target is run.
            This child class added a return value.
            :return:
            """
            if self._target is not None:
                self._return = self._target(*self._args, **self._kwargs)
    
        def join(self, *args, **kwargs):
            """
            Join normally like the parent class, but added a return value which
            the parent class join method does not have.
            """
            super().join(*args, **kwargs)
            return self._return
    

    Entire code for testing

    from netmiko import ConnectHandler
    from network.ciscoasa import get_asa_credential
    from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException, AuthenticationException
    from paramiko.ssh_exception import SSHException
    from textfsm.parser import TextFSMError
    from concurrent.futures import ThreadPoolExecutor as tpe
    from time import time
    from network.threads import CmdThread
    from functools import wraps
    
    Exceptions = (NetMikoAuthenticationException,
                  NetMikoAuthenticationException,
                  NetMikoTimeoutException,
                  AuthenticationException,
                  SSHException,
                  TextFSMError)
    
    
    def timer(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            start_time = time()
            result = fn(*args, **kwargs)
            print(f"Running {fn.__name__} takes {time() - start_time} seconds\n")
            return result
        return wrapper
    
    
    @timer
    def connect_device_type_1(cmd=None, devices=None, **kwargs):
        collect_results = []
        for device in devices:
            config = get_asa_credential(hostname=device)
            config.update({"device_type": "cisco_asa",
                           "global_delay_factor": 0.5})
            with ConnectHandler(**config) as asa:
                try:
                    collect_results.append(asa.send_command(cmd, use_textfsm=True))
                except Exceptions as e:
                    return e
        return collect_results
    
    
    @timer
    def connect_device_type_2(cmd=None, devices=None, **kwargs):
        collect_results = []
        with tpe(max_workers=4) as executor:
            for device in devices:
                config = get_asa_credential(hostname=device)
                config.update({"device_type": "cisco_asa",
                               "global_delay_factor": 0.5})
                try:
                    with ConnectHandler(**config) as asa:
                        result = executor.submit(asa.send_command, cmd, use_textfsm=True)
                    collect_results.append(result.result())
                except Exceptions as e:
                    return e
        return collect_results
    
    
    @timer
    def connect_device_type_3(cmd=None, devices=None, **kwargs):
        collect_results = []
        threads = []
        for device in devices:
            config = get_asa_credential(hostname=device)
            config.update({"device_type": "cisco_asa",
                           "global_delay_factor": 0.5})
            with ConnectHandler(**config) as asa:
                try:
                    t = CmdThread(target=asa.send_command, args=(cmd,), kwargs={"use_textfsm": True})
                    threads.append(t)
                    t.start()
                except Exceptions as e:
                    print(e)
        for thread in threads:
            collect_results.append(thread.join())
        return collect_results
    
    
    if __name__ == "__main__":
        devices = ["fw01", "fw02", "fw03"]
        payload = {
            "cmd": "show version",
            "devices": devices
        }
        print("Without threading:")
        connect_device_type_1(**payload)
        print("With ThreadPoolExecutor:")
        connect_device_type_2(**payload)
        print("With subclass Threading:")
        connect_device_type_3(**payload)
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s