From 3b08f936a0c45c13d411903f35cf44b0559fd32c Mon Sep 17 00:00:00 2001 From: mkp Date: Tue, 15 Oct 2024 16:27:30 +0200 Subject: [PATCH] Show how often abstractions are used in frozen devices TODO: show other dependencies too --- maxdiff/frozen_device_printer.py | 25 ++- maxdiff/get_frozen_stats.py | 160 +++++++++++++----- .../tests/test_baselines/FrozenTest.amxd.txt | 12 +- .../tests/test_baselines/StatsTest.amxd.txt | 13 +- maxdiff/tests/test_files/StatsTest.amxd | Bin 8762 -> 13762 bytes .../tests/test_files/UnusedAbstraction.maxpat | 105 ++++++++++++ 6 files changed, 254 insertions(+), 61 deletions(-) create mode 100644 maxdiff/tests/test_files/UnusedAbstraction.maxpat diff --git a/maxdiff/frozen_device_printer.py b/maxdiff/frozen_device_printer.py index 3aa7fbc..08eba01 100644 --- a/maxdiff/frozen_device_printer.py +++ b/maxdiff/frozen_device_printer.py @@ -1,5 +1,5 @@ from freezing_utils import * -from get_frozen_stats import get_frozen_stats +from get_frozen_stats import get_stats, get_used_abstractions def print_frozen_device(data: bytes) -> str: @@ -19,10 +19,27 @@ def print_frozen_device(data: bytes) -> str: footer_entries = parse_footer(footer_data[8:]) device_entries = get_device_entries(data, footer_entries) + used_abstractions = get_used_abstractions(device_entries) + + i = 0 for entry in device_entries: - if isinstance(entry["description"], str): - frozen_string += entry["description"] + "\n" - frozen_string += get_frozen_stats(device_entries) + description = entry["description"] + if isinstance(description, str): + file_name = str(entry["file_name"]) + if i == 0: + frozen_string += f"{description} <= Device \n" + else: + if file_name.endswith(".maxpat"): + if file_name in used_abstractions: + frozen_string += ( + f"{description}, {used_abstractions[file_name]} instances\n" + ) + else: + frozen_string += f"{description}, UNUSED\n" + else: + frozen_string += f"{description}\n" + i += 1 + frozen_string += get_stats(device_entries) return frozen_string diff --git a/maxdiff/get_frozen_stats.py b/maxdiff/get_frozen_stats.py index 8c601c8..6e4a18e 100644 --- a/maxdiff/get_frozen_stats.py +++ b/maxdiff/get_frozen_stats.py @@ -1,8 +1,18 @@ import json from freezing_utils import device_entry_with_data +from collections import Counter -def get_frozen_stats(entries: list[device_entry_with_data]): +class Processor: + def process_elements(self, patcher, voice_count: int, abstraction_name=""): + """Processes patchers.""" + + def get_results(self): + """Returns the current counts.""" + return None + + +def get_stats(entries: list[device_entry_with_data]): """Returns statistics for this device""" device = entries[0] # the first entry is always the device file @@ -11,14 +21,16 @@ def get_frozen_stats(entries: list[device_entry_with_data]): item for item in entries if str(item["file_name"]).endswith(".maxpat") ] - # cache names of known abstractions - abstraction_file_names = [str(item["file_name"]) for item in abstraction_entries] - device_patch = get_patcher_dict(device) - object_count_recursive, line_count_recursive = count( - device_patch, abstraction_entries, abstraction_file_names # do recurse into abstractions + count_processor = CountProcessor() + process_patch_recursive( + device_patch, + abstraction_entries, + count_processor, # do recurse into abstractions ) + object_count_recursive, line_count_recursive = count_processor.get_results() + summary = "\n" summary += "Total - Counting every abstraction instance - Indicates loading time\n" summary += f" Object instances: {object_count_recursive}\n" @@ -31,7 +43,10 @@ def get_frozen_stats(entries: list[device_entry_with_data]): continue entry_patch = get_patcher_dict(entry) - o, l = count(entry_patch, [], []) # don't recurse into abstractions + count_processor = CountProcessor() + # don't recurse into abstractions + process_patch_recursive(entry_patch, [], count_processor) + o, l = count_processor.get_results() object_count_once += o line_count_once += l @@ -42,65 +57,87 @@ def get_frozen_stats(entries: list[device_entry_with_data]): return summary -def count( - patcher, abstractions: list[dict], abstraction_file_names: list[str] -) -> tuple[int, int]: - """Recursively counts all object instances and connections in this patcher, - inluding in every instance of its dependencies that can be found among the - files that were passed in""" - boxes = patcher["boxes"] - lines = patcher["lines"] - object_count = len(boxes) - line_count = len(lines) +def get_used_abstractions(entries: list[device_entry_with_data]) -> dict[str, int]: + device = entries[0] # the first entry is always the device file - for box_entry in boxes: + abstraction_entries = [ + item for item in entries if str(item["file_name"]).endswith(".maxpat") + ] + + device_patch = get_patcher_dict(device) + abstractions_processor = AbstractionFileNamesProcessor() + process_patch_recursive(device_patch, abstraction_entries, abstractions_processor) + return abstractions_processor.get_results() + + +def process_patch_recursive( + patcher, + abstraction_entries: list[dict], + processor: Processor, + voice_count: int = 1, + abstraction_file_name: str = "", +): + """Recursively progress through subpatchers, invoking the processor for every patcher + Inluding every instance of the patch's dependencies that can be found among the + abstraction files that were passed in. + + Arguments + patcher: patcher to process + abstraction_entries: list of abstraction entries in frozen device + processor: instance of a Processor that is invoked for this patch + voice_count: the amount of voices when this patcher occurs in a poly~ in its parent patch + abstraction_file_name: the file name of the abstraction this patch is in, if it is in an abstraction + """ + processor.process_elements(patcher, voice_count, abstraction_file_name) + + for box_entry in patcher["boxes"]: box = box_entry["box"] - if "patcher" in box: + if "patcher" in box and ( + box.get("maxclass") != "bpatcher" or box.get("embed") == 1 + ): patch = box["patcher"] - if box.get("maxclass") != "bpatcher" or ( - box.get("maxclass") == "bpatcher" and box.get("embed") == 1 - ): - # get subpatcher or embedded bpatcher count - o, l = count(patch, abstractions, abstraction_file_names) - object_count += o - line_count += l - - if len(abstraction_file_names) == 0: - return (object_count, line_count) - - # recurse into abstractions - for box_entry in boxes: - box = box_entry["box"] + # get subpatcher or embedded bpatcher count + process_patch_recursive(patch, abstraction_entries, processor) - file_name = get_abstraction_name(box, abstraction_file_names) + # if no abstractions were passed in, we assume we don't want to recurse into abstarctions + if len(abstraction_entries) == 0: + continue + + # check for known abstraction + file_name = get_abstraction_name(box, abstraction_entries) if file_name is None: continue - abstraction = [item for item in abstractions if item["file_name"] == file_name][0] - abstraction_patch = get_patcher_dict(abstraction) - o, l = count(abstraction_patch, abstractions, abstraction_file_names) + abstraction = [ + item for item in abstraction_entries if item["file_name"] == file_name + ][0] + patch = get_patcher_dict(abstraction) + voice_count = 1 if "text" in box and box["text"].startswith("poly~"): # get poly abstraction count tokens = box["text"].split(" ") voice_count = int(tokens[2]) if len(tokens) > 2 else 1 - object_count += o * voice_count - line_count += l * voice_count - else: - # get abstraction count - object_count += o - line_count += l - return (object_count, line_count) + process_patch_recursive( + patch, + abstraction_entries, + processor, + voice_count, + file_name, + ) -def get_abstraction_name(box, abstraction_file_names: list[str]): +def get_abstraction_name(box, abstraction_entries: list[dict]): """ Checks if this box is an abstraction and if so, return the name of the abstraction file. - returns None if this is not an abstraction - throws error if an abstraction name was expected but it was not found in the list of known names """ + # cache names of known abstractions. TODO: find a way to do this more efficiently. + abstraction_file_names = [str(item["file_name"]) for item in abstraction_entries] + if "text" in box: if box["text"].startswith("poly~"): name = box["text"].split(" ")[1] + ".maxpat" @@ -167,3 +204,36 @@ def get_patcher_dict(entry: device_entry_with_data): except: print(f"Content of entry {name} does not seem to be a patcher") return {} + + +class CountProcessor(Processor): + def __init__(self): + self.object_count = 0 + self.line_count = 0 + + def process_elements(self, patcher, voice_count: int, abstraction_name=""): + """Counts objects and lines in the given patcher.""" + self.object_count += len(patcher.get("boxes", [])) * voice_count + self.line_count += len(patcher.get("lines", [])) * voice_count + + def get_results(self): + """Returns the current counts.""" + return self.object_count, self.line_count + + +# TODO: expend with dependencies that might be included in the frozen device, like pictures +class AbstractionFileNamesProcessor(Processor): + def __init__(self): + self.found_abstractions = {} + + def process_elements(self, patcher, voice_count: int, abstraction_name=""): + """If this patcher is an abstraction, i.e. when an abstraction_name is passed in, increment the entry in the dict""" + if abstraction_name != "": + if abstraction_name in self.found_abstractions: + self.found_abstractions[abstraction_name] += voice_count + else: + self.found_abstractions[abstraction_name] = voice_count + + def get_results(self): + """Returns a dict of used abstractions mapped to how oftern they are used.""" + return self.found_abstractions diff --git a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt index 1c88ab7..25a2398 100644 --- a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt +++ b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt @@ -2,10 +2,10 @@ MIDI Effect Device ------------------- Device is frozen ----- Contents ----- -Test.amxd: 23958 bytes, modified at 2024/10/15 14:31:39 UTC -ParamAbstraction.maxpat: 1570 bytes, modified at 2024/05/24 13:59:36 UTC -MyAbstraction.maxpat: 2015 bytes, modified at 2024/10/15 14:31:31 UTC -AbstractionWithParameter.maxpat: 1569 bytes, modified at 2024/05/24 13:59:36 UTC +Test.amxd: 23958 bytes, modified at 2024/10/15 14:31:39 UTC <= Device +ParamAbstraction.maxpat: 1570 bytes, modified at 2024/05/24 13:59:36 UTC, 2 instances +MyAbstraction.maxpat: 2015 bytes, modified at 2024/10/15 14:31:31 UTC, 2 instances +AbstractionWithParameter.maxpat: 1569 bytes, modified at 2024/05/24 13:59:36 UTC, 2 instances hz-icon.svg: 484 bytes, modified at 2024/05/24 13:59:36 UTC beat-icon.svg: 533 bytes, modified at 2024/05/24 13:59:36 UTC fpic.png: 7094 bytes, modified at 2024/05/24 13:59:36 UTC @@ -15,5 +15,5 @@ Total - Counting every abstraction instance - Indicates loading time Object instances: 49 Connections: 16 Unique - Counting abstractions once - Indicates maintainability - Object instances: 40 - Connections: 13 + Object instances: 44 + Connections: 14 diff --git a/maxdiff/tests/test_baselines/StatsTest.amxd.txt b/maxdiff/tests/test_baselines/StatsTest.amxd.txt index 3d277c2..ea52654 100644 --- a/maxdiff/tests/test_baselines/StatsTest.amxd.txt +++ b/maxdiff/tests/test_baselines/StatsTest.amxd.txt @@ -2,12 +2,13 @@ Audio Effect Device ------------------- Device is frozen ----- Contents ----- -StatsTest.amxd: 6475 bytes, modified at 2024/10/15 13:54:31 UTC -MyAbstraction.maxpat: 2015 bytes, modified at 2024/10/15 13:54:08 UTC +StatsTest.amxd: 9344 bytes, modified at 2024/10/15 13:55:42 UTC <= Device +MyAbstraction.maxpat: 2015 bytes, modified at 2024/10/15 13:54:08 UTC, 7 instances +UnusedAbstraction.maxpat: 2015 bytes, modified at 2024/10/15 13:54:59 UTC, UNUSED Total - Counting every abstraction instance - Indicates loading time - Object instances: 27 - Connections: 15 + Object instances: 33 + Connections: 17 Unique - Counting abstractions once - Indicates maintainability - Object instances: 12 - Connections: 5 + Object instances: 18 + Connections: 7 diff --git a/maxdiff/tests/test_files/StatsTest.amxd b/maxdiff/tests/test_files/StatsTest.amxd index 1ea475d0e8508fc5541975ce5b64249a8f2965a4..6027b538d2912dc906585b010f3b156c27eab054 100644 GIT binary patch delta 810 zcmZ8fzi-n(6i(7#aT17I8mCI5);XdAQb_GIPD2+OK^clvK)})nkYisG)7Y-;D+Hk; z5F-*QdK&`+0RGI5)tW7qq^}%`8EQ3>D9w_3!6w@k=0T{5CmxCr{Qb599@RA_!zqLS-3GKNN&6B zW5QrfSt%3oe!e=qW9xmR zr~5u5MuuzKn1Vt8G)8CPSK%!z@d0=yIFK}t&SLrlQ!)XaP+ZMnj=DH@yV zHD>U@isO@W@FleX_YS>~DjMHk;@G8ds#O6^o`=L(GAN90M$>AAQ`I>})!@=pK6o|O zNyfwV>Zu?zxjHUXIFqW>pqYJXhHrJ%+3~U2X!*p`4Pv=YnZ6PAkSXVgyR1uSpe@Vc zeS-!L|9x`Qa}B*WWEBPbmec8B0uRO|_?cNLGJb@{nByYNLZ3K*Ff>lx@M7#e{| zL(|C*_@p