mirror of https://github.com/ARMmbed/mbed-os.git
				
				
				
			Merge pull request #2990 from bridadan/parallel-test-build
[tools] Parallel building of testspull/3076/head
						commit
						6bd44c59d3
					
				| 
						 | 
				
			
			@ -455,12 +455,29 @@ def build_project(src_paths, build_path, target, toolchain_name,
 | 
			
		|||
        # Link Program
 | 
			
		||||
        res, _ = toolchain.link_program(resources, build_path, name)
 | 
			
		||||
 | 
			
		||||
        memap_instance = getattr(toolchain, 'memap_instance', None)
 | 
			
		||||
        memap_table = ''
 | 
			
		||||
        if memap_instance:
 | 
			
		||||
            # Write output to stdout in text (pretty table) format
 | 
			
		||||
            memap_table = memap_instance.generate_output('table')
 | 
			
		||||
 | 
			
		||||
            if not silent:
 | 
			
		||||
                print memap_table
 | 
			
		||||
 | 
			
		||||
            # Write output to file in JSON format
 | 
			
		||||
            map_out = join(build_path, name + "_map.json")
 | 
			
		||||
            memap_instance.generate_output('json', map_out)
 | 
			
		||||
 | 
			
		||||
            # Write output to file in CSV format for the CI
 | 
			
		||||
            map_csv = join(build_path, name + "_map.csv")
 | 
			
		||||
            memap_instance.generate_output('csv-ci', map_csv)
 | 
			
		||||
 | 
			
		||||
        resources.detect_duplicates(toolchain)
 | 
			
		||||
 | 
			
		||||
        if report != None:
 | 
			
		||||
            end = time()
 | 
			
		||||
            cur_result["elapsed_time"] = end - start
 | 
			
		||||
            cur_result["output"] = toolchain.get_output()
 | 
			
		||||
            cur_result["output"] = toolchain.get_output() + memap_table
 | 
			
		||||
            cur_result["result"] = "OK"
 | 
			
		||||
            cur_result["memory_usage"] = toolchain.map_outputs
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -404,6 +404,8 @@ class MemapParser(object):
 | 
			
		|||
 | 
			
		||||
        Keyword arguments:
 | 
			
		||||
        file_desc - descriptor (either stdout or file)
 | 
			
		||||
 | 
			
		||||
        Returns: generated string for the 'table' format, otherwise None
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
| 
						 | 
				
			
			@ -418,11 +420,13 @@ class MemapParser(object):
 | 
			
		|||
        to_call = {'json': self.generate_json,
 | 
			
		||||
                   'csv-ci': self.generate_csv,
 | 
			
		||||
                   'table': self.generate_table}[export_format]
 | 
			
		||||
        to_call(file_desc)
 | 
			
		||||
        output = to_call(file_desc)
 | 
			
		||||
 | 
			
		||||
        if file_desc is not sys.stdout:
 | 
			
		||||
            file_desc.close()
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def generate_json(self, file_desc):
 | 
			
		||||
        """Generate a json file from a memory map
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -432,6 +436,8 @@ class MemapParser(object):
 | 
			
		|||
        file_desc.write(json.dumps(self.mem_report, indent=4))
 | 
			
		||||
        file_desc.write('\n')
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def generate_csv(self, file_desc):
 | 
			
		||||
        """Generate a CSV file from a memoy map
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -472,11 +478,15 @@ class MemapParser(object):
 | 
			
		|||
        csv_writer.writerow(csv_module_section)
 | 
			
		||||
        csv_writer.writerow(csv_sizes)
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def generate_table(self, file_desc):
 | 
			
		||||
        """Generate a table from a memoy map
 | 
			
		||||
 | 
			
		||||
        Positional arguments:
 | 
			
		||||
        file_desc - the file to write out the final report to
 | 
			
		||||
 | 
			
		||||
        Returns: string of the generated table
 | 
			
		||||
        """
 | 
			
		||||
        # Create table
 | 
			
		||||
        columns = ['Module']
 | 
			
		||||
| 
						 | 
				
			
			@ -504,28 +514,29 @@ class MemapParser(object):
 | 
			
		|||
 | 
			
		||||
        table.add_row(subtotal_row)
 | 
			
		||||
 | 
			
		||||
        file_desc.write(table.get_string())
 | 
			
		||||
        file_desc.write('\n')
 | 
			
		||||
        output = table.get_string()
 | 
			
		||||
        output += '\n'
 | 
			
		||||
 | 
			
		||||
        if self.mem_summary['heap'] == 0:
 | 
			
		||||
            file_desc.write("Allocated Heap: unknown\n")
 | 
			
		||||
            output += "Allocated Heap: unknown\n"
 | 
			
		||||
        else:
 | 
			
		||||
            file_desc.write("Allocated Heap: %s bytes\n" %
 | 
			
		||||
                            str(self.mem_summary['heap']))
 | 
			
		||||
            output += "Allocated Heap: %s bytes\n" % \
 | 
			
		||||
                        str(self.mem_summary['heap'])
 | 
			
		||||
 | 
			
		||||
        if self.mem_summary['stack'] == 0:
 | 
			
		||||
            file_desc.write("Allocated Stack: unknown\n")
 | 
			
		||||
            output += "Allocated Stack: unknown\n"
 | 
			
		||||
        else:
 | 
			
		||||
            file_desc.write("Allocated Stack: %s bytes\n" %
 | 
			
		||||
                            str(self.mem_summary['stack']))
 | 
			
		||||
            output += "Allocated Stack: %s bytes\n" % \
 | 
			
		||||
                        str(self.mem_summary['stack'])
 | 
			
		||||
 | 
			
		||||
        file_desc.write("Total Static RAM memory (data + bss): %s bytes\n" %
 | 
			
		||||
                        (str(self.mem_summary['static_ram'])))
 | 
			
		||||
        file_desc.write(
 | 
			
		||||
            "Total RAM memory (data + bss + heap + stack): %s bytes\n"
 | 
			
		||||
            % (str(self.mem_summary['total_ram'])))
 | 
			
		||||
        file_desc.write("Total Flash memory (text + data + misc): %s bytes\n" %
 | 
			
		||||
                        (str(self.mem_summary['total_flash'])))
 | 
			
		||||
        output += "Total Static RAM memory (data + bss): %s bytes\n" % \
 | 
			
		||||
                        str(self.mem_summary['static_ram'])
 | 
			
		||||
        output += "Total RAM memory (data + bss + heap + stack): %s bytes\n" % \
 | 
			
		||||
                        str(self.mem_summary['total_ram'])
 | 
			
		||||
        output += "Total Flash memory (text + data + misc): %s bytes\n" % \
 | 
			
		||||
                        str(self.mem_summary['total_flash'])
 | 
			
		||||
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    toolchains = ["ARM", "ARM_STD", "ARM_MICRO", "GCC_ARM", "IAR"]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -647,11 +658,15 @@ def main():
 | 
			
		|||
        if memap.parse(args.file, args.toolchain) is False:
 | 
			
		||||
            sys.exit(0)
 | 
			
		||||
 | 
			
		||||
    returned_string = None
 | 
			
		||||
    # Write output in file
 | 
			
		||||
    if args.output != None:
 | 
			
		||||
        memap.generate_output(args.export, args.output)
 | 
			
		||||
        returned_string = memap.generate_output(args.export, args.output)
 | 
			
		||||
    else: # Write output in screen
 | 
			
		||||
        memap.generate_output(args.export)
 | 
			
		||||
        returned_string = memap.generate_output(args.export)
 | 
			
		||||
 | 
			
		||||
    if args.export == 'table' and returned_string:
 | 
			
		||||
        print returned_string
 | 
			
		||||
 | 
			
		||||
    sys.exit(0)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,7 @@ from time import sleep, time
 | 
			
		|||
from Queue import Queue, Empty
 | 
			
		||||
from os.path import join, exists, basename, relpath
 | 
			
		||||
from threading import Thread, Lock
 | 
			
		||||
from multiprocessing import Pool, cpu_count
 | 
			
		||||
from subprocess import Popen, PIPE
 | 
			
		||||
 | 
			
		||||
# Imports related to mbed build api
 | 
			
		||||
| 
						 | 
				
			
			@ -2068,6 +2069,48 @@ def norm_relative_path(path, start):
 | 
			
		|||
    path = path.replace("\\", "/")
 | 
			
		||||
    return path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def build_test_worker(*args, **kwargs):
 | 
			
		||||
    """This is a worker function for the parallel building of tests. The `args`
 | 
			
		||||
    and `kwargs` are passed directly to `build_project`. It returns a dictionary
 | 
			
		||||
    with the following structure:
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
        'result': `True` if no exceptions were thrown, `False` otherwise
 | 
			
		||||
        'reason': Instance of exception that was thrown on failure
 | 
			
		||||
        'bin_file': Path to the created binary if `build_project` was
 | 
			
		||||
                    successful. Not present otherwise
 | 
			
		||||
        'kwargs': The keyword arguments that were passed to `build_project`.
 | 
			
		||||
                  This includes arguments that were modified (ex. report)
 | 
			
		||||
    }
 | 
			
		||||
    """
 | 
			
		||||
    bin_file = None
 | 
			
		||||
    ret = {
 | 
			
		||||
        'result': False,
 | 
			
		||||
        'args': args,
 | 
			
		||||
        'kwargs': kwargs
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        bin_file = build_project(*args, **kwargs)
 | 
			
		||||
        ret['result'] = True
 | 
			
		||||
        ret['bin_file'] = bin_file
 | 
			
		||||
        ret['kwargs'] = kwargs
 | 
			
		||||
 | 
			
		||||
    except NotSupportedException, e:
 | 
			
		||||
        ret['reason'] = e
 | 
			
		||||
    except ToolException, e:
 | 
			
		||||
        ret['reason'] = e
 | 
			
		||||
    except KeyboardInterrupt, e:
 | 
			
		||||
        ret['reason'] = e
 | 
			
		||||
    except:
 | 
			
		||||
        # Print unhandled exceptions here
 | 
			
		||||
        import traceback
 | 
			
		||||
        traceback.print_exc(file=sys.stdout)
 | 
			
		||||
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def build_tests(tests, base_source_paths, build_path, target, toolchain_name,
 | 
			
		||||
                clean=False, notify=None, verbose=False, jobs=1, macros=None,
 | 
			
		||||
                silent=False, report=None, properties=None,
 | 
			
		||||
| 
						 | 
				
			
			@ -2095,58 +2138,101 @@ def build_tests(tests, base_source_paths, build_path, target, toolchain_name,
 | 
			
		|||
 | 
			
		||||
    result = True
 | 
			
		||||
 | 
			
		||||
    map_outputs_total = list()
 | 
			
		||||
    jobs_count = int(jobs if jobs else cpu_count())
 | 
			
		||||
    p = Pool(processes=jobs_count)
 | 
			
		||||
    results = []
 | 
			
		||||
    for test_name, test_path in tests.iteritems():
 | 
			
		||||
        test_build_path = os.path.join(build_path, test_path)
 | 
			
		||||
        src_path = base_source_paths + [test_path]
 | 
			
		||||
        bin_file = None
 | 
			
		||||
        test_case_folder_name = os.path.basename(test_path)
 | 
			
		||||
        
 | 
			
		||||
        args = (src_path, test_build_path, target, toolchain_name)
 | 
			
		||||
        kwargs = {
 | 
			
		||||
            'jobs': jobs,
 | 
			
		||||
            'clean': clean,
 | 
			
		||||
            'macros': macros,
 | 
			
		||||
            'name': test_case_folder_name,
 | 
			
		||||
            'project_id': test_name,
 | 
			
		||||
            'report': report,
 | 
			
		||||
            'properties': properties,
 | 
			
		||||
            'verbose': verbose,
 | 
			
		||||
            'app_config': app_config,
 | 
			
		||||
            'build_profile': build_profile,
 | 
			
		||||
            'silent': True
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        try:
 | 
			
		||||
            bin_file = build_project(src_path, test_build_path, target, toolchain_name,
 | 
			
		||||
                                     jobs=jobs,
 | 
			
		||||
                                     clean=clean,
 | 
			
		||||
                                     macros=macros,
 | 
			
		||||
                                     name=test_case_folder_name,
 | 
			
		||||
                                     project_id=test_name,
 | 
			
		||||
                                     report=report,
 | 
			
		||||
                                     properties=properties,
 | 
			
		||||
                                     verbose=verbose,
 | 
			
		||||
                                     app_config=app_config,
 | 
			
		||||
                                     build_profile=build_profile)
 | 
			
		||||
        results.append(p.apply_async(build_test_worker, args, kwargs))
 | 
			
		||||
 | 
			
		||||
        except NotSupportedException:
 | 
			
		||||
            pass
 | 
			
		||||
        except ToolException:
 | 
			
		||||
            result = False
 | 
			
		||||
            if continue_on_build_fail:
 | 
			
		||||
                continue
 | 
			
		||||
            else:
 | 
			
		||||
    p.close()
 | 
			
		||||
    result = True
 | 
			
		||||
    itr = 0
 | 
			
		||||
    while len(results):
 | 
			
		||||
        itr += 1
 | 
			
		||||
        if itr > 360000:
 | 
			
		||||
            p.terminate()
 | 
			
		||||
            p.join()
 | 
			
		||||
            raise ToolException("Compile did not finish in 10 minutes")
 | 
			
		||||
        else:
 | 
			
		||||
            sleep(0.01)
 | 
			
		||||
            pending = 0
 | 
			
		||||
            for r in results:
 | 
			
		||||
                if r.ready() is True:
 | 
			
		||||
                    try:
 | 
			
		||||
                        worker_result = r.get()
 | 
			
		||||
                        results.remove(r)
 | 
			
		||||
 | 
			
		||||
                        # Take report from the kwargs and merge it into existing report
 | 
			
		||||
                        report_entry = worker_result['kwargs']['report'][target_name][toolchain_name]
 | 
			
		||||
                        for test_key in report_entry.keys():
 | 
			
		||||
                            report[target_name][toolchain_name][test_key] = report_entry[test_key]
 | 
			
		||||
                        
 | 
			
		||||
                        # Set the overall result to a failure if a build failure occurred
 | 
			
		||||
                        if not worker_result['result'] and not isinstance(worker_result['reason'], NotSupportedException):
 | 
			
		||||
                            result = False
 | 
			
		||||
                            break
 | 
			
		||||
 | 
			
		||||
                        # Adding binary path to test build result
 | 
			
		||||
                        if worker_result['result'] and 'bin_file' in worker_result:
 | 
			
		||||
                            bin_file = norm_relative_path(worker_result['bin_file'], execution_directory)
 | 
			
		||||
 | 
			
		||||
                            test_build['tests'][worker_result['kwargs']['project_id']] = {
 | 
			
		||||
                                "binaries": [
 | 
			
		||||
                                    {
 | 
			
		||||
                                        "path": bin_file
 | 
			
		||||
                                    }
 | 
			
		||||
                                ]
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            test_key = worker_result['kwargs']['project_id'].upper()
 | 
			
		||||
                            print report[target_name][toolchain_name][test_key][0][0]['output'].rstrip()
 | 
			
		||||
                            print 'Image: %s\n' % bin_file
 | 
			
		||||
 | 
			
		||||
                    except:
 | 
			
		||||
                        if p._taskqueue.queue:
 | 
			
		||||
                            p._taskqueue.queue.clear()
 | 
			
		||||
                            sleep(0.5)
 | 
			
		||||
                        p.terminate()
 | 
			
		||||
                        p.join()
 | 
			
		||||
                        raise
 | 
			
		||||
                else:
 | 
			
		||||
                    pending += 1
 | 
			
		||||
                    if pending >= jobs_count:
 | 
			
		||||
                        break
 | 
			
		||||
 | 
			
		||||
            # Break as soon as possible if there is a failure and we are not
 | 
			
		||||
            # continuing on build failures
 | 
			
		||||
            if not result and not continue_on_build_fail:
 | 
			
		||||
                if p._taskqueue.queue:
 | 
			
		||||
                    p._taskqueue.queue.clear()
 | 
			
		||||
                    sleep(0.5)
 | 
			
		||||
                p.terminate()
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        # If a clean build was carried out last time, disable it for the next build.
 | 
			
		||||
        # Otherwise the previously built test will be deleted.
 | 
			
		||||
        if clean:
 | 
			
		||||
            clean = False
 | 
			
		||||
 | 
			
		||||
        # Normalize the path
 | 
			
		||||
        if bin_file:
 | 
			
		||||
            bin_file = norm_relative_path(bin_file, execution_directory)
 | 
			
		||||
 | 
			
		||||
            test_build['tests'][test_name] = {
 | 
			
		||||
                "binaries": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "path": bin_file
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            print 'Image: %s'% bin_file
 | 
			
		||||
    p.join()
 | 
			
		||||
 | 
			
		||||
    test_builds = {}
 | 
			
		||||
    test_builds["%s-%s" % (target_name, toolchain_name)] = test_build
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    return result, test_builds
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -370,24 +370,24 @@ class mbedToolchain:
 | 
			
		|||
            msg = '[%(severity)s] %(file)s@%(line)s,%(col)s: %(message)s' % event
 | 
			
		||||
 | 
			
		||||
        elif event['type'] == 'progress':
 | 
			
		||||
            if not silent:
 | 
			
		||||
                if 'percent' in event:
 | 
			
		||||
                    msg = '{} [{:>5.1f}%]: {}'.format(event['action'].title(),
 | 
			
		||||
                                                      event['percent'],
 | 
			
		||||
                                                      basename(event['file']))
 | 
			
		||||
                else:
 | 
			
		||||
                    msg = '{}: {}'.format(event['action'].title(),
 | 
			
		||||
                                          basename(event['file']))
 | 
			
		||||
            if 'percent' in event:
 | 
			
		||||
                msg = '{} [{:>5.1f}%]: {}'.format(event['action'].title(),
 | 
			
		||||
                                                  event['percent'],
 | 
			
		||||
                                                  basename(event['file']))
 | 
			
		||||
            else:
 | 
			
		||||
                msg = '{}: {}'.format(event['action'].title(),
 | 
			
		||||
                                      basename(event['file']))
 | 
			
		||||
 | 
			
		||||
        if msg:
 | 
			
		||||
            print msg
 | 
			
		||||
            if not silent:
 | 
			
		||||
                print msg
 | 
			
		||||
            self.output += msg + "\n"
 | 
			
		||||
 | 
			
		||||
    def print_notify_verbose(self, event, silent=False):
 | 
			
		||||
        """ Default command line notification with more verbose mode
 | 
			
		||||
        """
 | 
			
		||||
        if event['type'] in ['info', 'debug']:
 | 
			
		||||
            self.print_notify(event) # standard handle
 | 
			
		||||
            self.print_notify(event, silent=silent) # standard handle
 | 
			
		||||
 | 
			
		||||
        elif event['type'] == 'cc':
 | 
			
		||||
            event['severity'] = event['severity'].title()
 | 
			
		||||
| 
						 | 
				
			
			@ -396,7 +396,8 @@ class mbedToolchain:
 | 
			
		|||
            event['target_name'] = event['target_name'].upper() if event['target_name'] else "Unknown"
 | 
			
		||||
            event['toolchain_name'] = event['toolchain_name'].upper() if event['toolchain_name'] else "Unknown"
 | 
			
		||||
            msg = '[%(severity)s] %(target_name)s::%(toolchain_name)s::%(file)s@%(line)s: %(message)s' % event
 | 
			
		||||
            print msg
 | 
			
		||||
            if not silent:
 | 
			
		||||
                print msg
 | 
			
		||||
            self.output += msg + "\n"
 | 
			
		||||
 | 
			
		||||
        elif event['type'] == 'progress':
 | 
			
		||||
| 
						 | 
				
			
			@ -1074,16 +1075,8 @@ class mbedToolchain:
 | 
			
		|||
            self.info("Unknown toolchain for memory statistics %s" % toolchain)
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        # Write output to stdout in text (pretty table) format
 | 
			
		||||
        memap.generate_output('table')
 | 
			
		||||
 | 
			
		||||
        # Write output to file in JSON format
 | 
			
		||||
        map_out = splitext(map)[0] + "_map.json"
 | 
			
		||||
        memap.generate_output('json', map_out)
 | 
			
		||||
 | 
			
		||||
        # Write output to file in CSV format for the CI
 | 
			
		||||
        map_csv = splitext(map)[0] + "_map.csv"
 | 
			
		||||
        memap.generate_output('csv-ci', map_csv)
 | 
			
		||||
        # Store the memap instance for later use
 | 
			
		||||
        self.memap_instance = memap
 | 
			
		||||
 | 
			
		||||
        # Here we return memory statistics structure (constructed after
 | 
			
		||||
        # call to generate_output) which contains raw data in bytes
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue