Skip to content

Commit 6bd44c5

Browse files
authored
Merge pull request #2990 from bridadan/parallel-test-build
[tools] Parallel building of tests
2 parents 0650988 + 3908672 commit 6bd44c5

File tree

4 files changed

+191
-80
lines changed

4 files changed

+191
-80
lines changed

tools/build_api.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,12 +455,29 @@ def build_project(src_paths, build_path, target, toolchain_name,
455455
# Link Program
456456
res, _ = toolchain.link_program(resources, build_path, name)
457457

458+
memap_instance = getattr(toolchain, 'memap_instance', None)
459+
memap_table = ''
460+
if memap_instance:
461+
# Write output to stdout in text (pretty table) format
462+
memap_table = memap_instance.generate_output('table')
463+
464+
if not silent:
465+
print memap_table
466+
467+
# Write output to file in JSON format
468+
map_out = join(build_path, name + "_map.json")
469+
memap_instance.generate_output('json', map_out)
470+
471+
# Write output to file in CSV format for the CI
472+
map_csv = join(build_path, name + "_map.csv")
473+
memap_instance.generate_output('csv-ci', map_csv)
474+
458475
resources.detect_duplicates(toolchain)
459476

460477
if report != None:
461478
end = time()
462479
cur_result["elapsed_time"] = end - start
463-
cur_result["output"] = toolchain.get_output()
480+
cur_result["output"] = toolchain.get_output() + memap_table
464481
cur_result["result"] = "OK"
465482
cur_result["memory_usage"] = toolchain.map_outputs
466483

tools/memap.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ def generate_output(self, export_format, file_output=None):
404404
405405
Keyword arguments:
406406
file_desc - descriptor (either stdout or file)
407+
408+
Returns: generated string for the 'table' format, otherwise None
407409
"""
408410

409411
try:
@@ -418,11 +420,13 @@ def generate_output(self, export_format, file_output=None):
418420
to_call = {'json': self.generate_json,
419421
'csv-ci': self.generate_csv,
420422
'table': self.generate_table}[export_format]
421-
to_call(file_desc)
423+
output = to_call(file_desc)
422424

423425
if file_desc is not sys.stdout:
424426
file_desc.close()
425427

428+
return output
429+
426430
def generate_json(self, file_desc):
427431
"""Generate a json file from a memory map
428432
@@ -432,6 +436,8 @@ def generate_json(self, file_desc):
432436
file_desc.write(json.dumps(self.mem_report, indent=4))
433437
file_desc.write('\n')
434438

439+
return None
440+
435441
def generate_csv(self, file_desc):
436442
"""Generate a CSV file from a memoy map
437443
@@ -472,11 +478,15 @@ def generate_csv(self, file_desc):
472478
csv_writer.writerow(csv_module_section)
473479
csv_writer.writerow(csv_sizes)
474480

481+
return None
482+
475483
def generate_table(self, file_desc):
476484
"""Generate a table from a memoy map
477485
478486
Positional arguments:
479487
file_desc - the file to write out the final report to
488+
489+
Returns: string of the generated table
480490
"""
481491
# Create table
482492
columns = ['Module']
@@ -504,28 +514,29 @@ def generate_table(self, file_desc):
504514

505515
table.add_row(subtotal_row)
506516

507-
file_desc.write(table.get_string())
508-
file_desc.write('\n')
517+
output = table.get_string()
518+
output += '\n'
509519

510520
if self.mem_summary['heap'] == 0:
511-
file_desc.write("Allocated Heap: unknown\n")
521+
output += "Allocated Heap: unknown\n"
512522
else:
513-
file_desc.write("Allocated Heap: %s bytes\n" %
514-
str(self.mem_summary['heap']))
523+
output += "Allocated Heap: %s bytes\n" % \
524+
str(self.mem_summary['heap'])
515525

516526
if self.mem_summary['stack'] == 0:
517-
file_desc.write("Allocated Stack: unknown\n")
527+
output += "Allocated Stack: unknown\n"
518528
else:
519-
file_desc.write("Allocated Stack: %s bytes\n" %
520-
str(self.mem_summary['stack']))
529+
output += "Allocated Stack: %s bytes\n" % \
530+
str(self.mem_summary['stack'])
521531

522-
file_desc.write("Total Static RAM memory (data + bss): %s bytes\n" %
523-
(str(self.mem_summary['static_ram'])))
524-
file_desc.write(
525-
"Total RAM memory (data + bss + heap + stack): %s bytes\n"
526-
% (str(self.mem_summary['total_ram'])))
527-
file_desc.write("Total Flash memory (text + data + misc): %s bytes\n" %
528-
(str(self.mem_summary['total_flash'])))
532+
output += "Total Static RAM memory (data + bss): %s bytes\n" % \
533+
str(self.mem_summary['static_ram'])
534+
output += "Total RAM memory (data + bss + heap + stack): %s bytes\n" % \
535+
str(self.mem_summary['total_ram'])
536+
output += "Total Flash memory (text + data + misc): %s bytes\n" % \
537+
str(self.mem_summary['total_flash'])
538+
539+
return output
529540

530541
toolchains = ["ARM", "ARM_STD", "ARM_MICRO", "GCC_ARM", "IAR"]
531542

@@ -647,11 +658,15 @@ def main():
647658
if memap.parse(args.file, args.toolchain) is False:
648659
sys.exit(0)
649660

661+
returned_string = None
650662
# Write output in file
651663
if args.output != None:
652-
memap.generate_output(args.export, args.output)
664+
returned_string = memap.generate_output(args.export, args.output)
653665
else: # Write output in screen
654-
memap.generate_output(args.export)
666+
returned_string = memap.generate_output(args.export)
667+
668+
if args.export == 'table' and returned_string:
669+
print returned_string
655670

656671
sys.exit(0)
657672

tools/test_api.py

Lines changed: 126 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from Queue import Queue, Empty
3838
from os.path import join, exists, basename, relpath
3939
from threading import Thread, Lock
40+
from multiprocessing import Pool, cpu_count
4041
from subprocess import Popen, PIPE
4142

4243
# Imports related to mbed build api
@@ -2068,6 +2069,48 @@ def norm_relative_path(path, start):
20682069
path = path.replace("\\", "/")
20692070
return path
20702071

2072+
2073+
def build_test_worker(*args, **kwargs):
2074+
"""This is a worker function for the parallel building of tests. The `args`
2075+
and `kwargs` are passed directly to `build_project`. It returns a dictionary
2076+
with the following structure:
2077+
2078+
{
2079+
'result': `True` if no exceptions were thrown, `False` otherwise
2080+
'reason': Instance of exception that was thrown on failure
2081+
'bin_file': Path to the created binary if `build_project` was
2082+
successful. Not present otherwise
2083+
'kwargs': The keyword arguments that were passed to `build_project`.
2084+
This includes arguments that were modified (ex. report)
2085+
}
2086+
"""
2087+
bin_file = None
2088+
ret = {
2089+
'result': False,
2090+
'args': args,
2091+
'kwargs': kwargs
2092+
}
2093+
2094+
try:
2095+
bin_file = build_project(*args, **kwargs)
2096+
ret['result'] = True
2097+
ret['bin_file'] = bin_file
2098+
ret['kwargs'] = kwargs
2099+
2100+
except NotSupportedException, e:
2101+
ret['reason'] = e
2102+
except ToolException, e:
2103+
ret['reason'] = e
2104+
except KeyboardInterrupt, e:
2105+
ret['reason'] = e
2106+
except:
2107+
# Print unhandled exceptions here
2108+
import traceback
2109+
traceback.print_exc(file=sys.stdout)
2110+
2111+
return ret
2112+
2113+
20712114
def build_tests(tests, base_source_paths, build_path, target, toolchain_name,
20722115
clean=False, notify=None, verbose=False, jobs=1, macros=None,
20732116
silent=False, report=None, properties=None,
@@ -2095,58 +2138,101 @@ def build_tests(tests, base_source_paths, build_path, target, toolchain_name,
20952138

20962139
result = True
20972140

2098-
map_outputs_total = list()
2141+
jobs_count = int(jobs if jobs else cpu_count())
2142+
p = Pool(processes=jobs_count)
2143+
results = []
20992144
for test_name, test_path in tests.iteritems():
21002145
test_build_path = os.path.join(build_path, test_path)
21012146
src_path = base_source_paths + [test_path]
21022147
bin_file = None
21032148
test_case_folder_name = os.path.basename(test_path)
21042149

2150+
args = (src_path, test_build_path, target, toolchain_name)
2151+
kwargs = {
2152+
'jobs': jobs,
2153+
'clean': clean,
2154+
'macros': macros,
2155+
'name': test_case_folder_name,
2156+
'project_id': test_name,
2157+
'report': report,
2158+
'properties': properties,
2159+
'verbose': verbose,
2160+
'app_config': app_config,
2161+
'build_profile': build_profile,
2162+
'silent': True
2163+
}
21052164

2106-
try:
2107-
bin_file = build_project(src_path, test_build_path, target, toolchain_name,
2108-
jobs=jobs,
2109-
clean=clean,
2110-
macros=macros,
2111-
name=test_case_folder_name,
2112-
project_id=test_name,
2113-
report=report,
2114-
properties=properties,
2115-
verbose=verbose,
2116-
app_config=app_config,
2117-
build_profile=build_profile)
2118-
2119-
except NotSupportedException:
2120-
pass
2121-
except ToolException:
2122-
result = False
2123-
if continue_on_build_fail:
2124-
continue
2125-
else:
2126-
break
2165+
results.append(p.apply_async(build_test_worker, args, kwargs))
21272166

2128-
# If a clean build was carried out last time, disable it for the next build.
2129-
# Otherwise the previously built test will be deleted.
2130-
if clean:
2131-
clean = False
2132-
2133-
# Normalize the path
2134-
if bin_file:
2135-
bin_file = norm_relative_path(bin_file, execution_directory)
2136-
2137-
test_build['tests'][test_name] = {
2138-
"binaries": [
2139-
{
2140-
"path": bin_file
2141-
}
2142-
]
2143-
}
2167+
p.close()
2168+
result = True
2169+
itr = 0
2170+
while len(results):
2171+
itr += 1
2172+
if itr > 360000:
2173+
p.terminate()
2174+
p.join()
2175+
raise ToolException("Compile did not finish in 10 minutes")
2176+
else:
2177+
sleep(0.01)
2178+
pending = 0
2179+
for r in results:
2180+
if r.ready() is True:
2181+
try:
2182+
worker_result = r.get()
2183+
results.remove(r)
2184+
2185+
# Take report from the kwargs and merge it into existing report
2186+
report_entry = worker_result['kwargs']['report'][target_name][toolchain_name]
2187+
for test_key in report_entry.keys():
2188+
report[target_name][toolchain_name][test_key] = report_entry[test_key]
2189+
2190+
# Set the overall result to a failure if a build failure occurred
2191+
if not worker_result['result'] and not isinstance(worker_result['reason'], NotSupportedException):
2192+
result = False
2193+
break
2194+
2195+
# Adding binary path to test build result
2196+
if worker_result['result'] and 'bin_file' in worker_result:
2197+
bin_file = norm_relative_path(worker_result['bin_file'], execution_directory)
2198+
2199+
test_build['tests'][worker_result['kwargs']['project_id']] = {
2200+
"binaries": [
2201+
{
2202+
"path": bin_file
2203+
}
2204+
]
2205+
}
2206+
2207+
test_key = worker_result['kwargs']['project_id'].upper()
2208+
print report[target_name][toolchain_name][test_key][0][0]['output'].rstrip()
2209+
print 'Image: %s\n' % bin_file
21442210

2145-
print 'Image: %s'% bin_file
2211+
except:
2212+
if p._taskqueue.queue:
2213+
p._taskqueue.queue.clear()
2214+
sleep(0.5)
2215+
p.terminate()
2216+
p.join()
2217+
raise
2218+
else:
2219+
pending += 1
2220+
if pending >= jobs_count:
2221+
break
2222+
2223+
# Break as soon as possible if there is a failure and we are not
2224+
# continuing on build failures
2225+
if not result and not continue_on_build_fail:
2226+
if p._taskqueue.queue:
2227+
p._taskqueue.queue.clear()
2228+
sleep(0.5)
2229+
p.terminate()
2230+
break
2231+
2232+
p.join()
21462233

21472234
test_builds = {}
21482235
test_builds["%s-%s" % (target_name, toolchain_name)] = test_build
2149-
21502236

21512237
return result, test_builds
21522238

0 commit comments

Comments
 (0)