From 5fd5a8af454a3a53474d2cda6cca466666f17796 Mon Sep 17 00:00:00 2001 From: Mattijs Kneppers Date: Sun, 8 Sep 2024 06:47:25 +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 | 8 +- .../tests/test_baselines/StatsTest.amxd.txt | 13 +- maxdiff/tests/test_files/StatsTest.amxd | Bin 8667 -> 13693 bytes .../tests/test_files/UnusedAbstraction.maxpat | 105 ++++++++++++ 6 files changed, 252 insertions(+), 59 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 4bd69d3..ba3c664 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: 21093 bytes, modified at 2024/05/27 07:52:44 UTC -ParamAbstraction.maxpat: 1570 bytes, modified at 2024/05/24 13:59:36 UTC -MyAbstraction.maxpat: 1625 bytes, modified at 2024/05/24 13:59:36 UTC -AbstractionWithParameter.maxpat: 1569 bytes, modified at 2024/05/24 13:59:36 UTC +Test.amxd: 21093 bytes, modified at 2024/05/27 07:52:44 UTC <= Device +ParamAbstraction.maxpat: 1570 bytes, modified at 2024/05/24 13:59:36 UTC, 2 instances +MyAbstraction.maxpat: 1625 bytes, modified at 2024/05/24 13:59:36 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 diff --git a/maxdiff/tests/test_baselines/StatsTest.amxd.txt b/maxdiff/tests/test_baselines/StatsTest.amxd.txt index 3eaf6b7..a73b770 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 ----- -SummaryTest.amxd: 6376 bytes, modified at 2024/06/27 07:39:44 UTC -MyAbstraction.maxpat: 2015 bytes, modified at 2024/06/24 07:11:15 UTC +StatsTest.amxd: 9275 bytes, modified at 2024/09/10 07:31:52 UTC <= Device +MyAbstraction.maxpat: 2015 bytes, modified at 2024/06/24 07:11:15 UTC, 7 instances +UnusedAbstraction.maxpat: 2015 bytes, modified at 2024/09/09 08:40:24 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 9b28f1124b17942943d7dc32cb59f4691a773e28..d3a0ad90ad68f81daf2ab9a370ccc4c31398811b 100644 GIT binary patch delta 933 zcmZuwO;6N7816zB_5*GCDhM0sHXA^*?6zfhSs_t~5EJk--~~cVx1D7R+m^Hgt{O~? zCu3qT@76bEDAb@l%>Nf}t^we?tT&q-&f(VI5rK)Ko-PyJ&Z?1v; zn&>{MJC=p~a3eI!4#MwPH{1+IU^U*aD^x*_9H*#sUZ8TCxiP9yT4|o7WFSKtn)rD5_F5id=xzzUM}8!#C`04;iy154)O;eML{zj~)eNil5&np{-Xu&>&usgO-lCX+pSfE7mg#(}%B zV6t03>^q1&k>A(%PYtwBg_aG|MgBig!MC@_*s-v5^-9^+EPvjA>_A4dY6io2cP3PV vVVKl3OefRqe)vTNHj@i2sYQ5`Oi2wg{2#JZ;0-)Xrd!fye6+~!v&O++!v^so delta 181 zcmeyHb=!G@jL2?928P@Uhhzo@1_2-j0)^`v4W2Pi*5&8k?7|ntC}3n_tY@I3U}yv; z4NWIM;FI2bf(DfKrB diff --git a/maxdiff/tests/test_files/UnusedAbstraction.maxpat b/maxdiff/tests/test_files/UnusedAbstraction.maxpat new file mode 100644 index 0000000..9bb3c77 --- /dev/null +++ b/maxdiff/tests/test_files/UnusedAbstraction.maxpat @@ -0,0 +1,105 @@ +{ + "patcher" : { + "fileversion" : 1, + "appversion" : { + "major" : 8, + "minor" : 6, + "revision" : 2, + "architecture" : "x64", + "modernui" : 1 + } +, + "classnamespace" : "box", + "rect" : [ 312.0, 228.0, 221.0, 176.0 ], + "bglocked" : 0, + "openinpresentation" : 0, + "default_fontsize" : 12.0, + "default_fontface" : 0, + "default_fontname" : "Arial", + "gridonopen" : 1, + "gridsize" : [ 15.0, 15.0 ], + "gridsnaponopen" : 1, + "objectsnaponopen" : 1, + "statusbarvisible" : 2, + "toolbarvisible" : 1, + "lefttoolbarpinned" : 0, + "toptoolbarpinned" : 0, + "righttoolbarpinned" : 0, + "bottomtoolbarpinned" : 0, + "toolbars_unpinned_last_save" : 0, + "tallnewobj" : 0, + "boxanimatetime" : 200, + "enablehscroll" : 1, + "enablevscroll" : 1, + "devicewidth" : 0.0, + "description" : "", + "digest" : "", + "tags" : "", + "style" : "", + "subpatcher_template" : "", + "assistshowspatchername" : 0, + "boxes" : [ { + "box" : { + "id" : "obj-3", + "maxclass" : "button", + "numinlets" : 1, + "numoutlets" : 1, + "outlettype" : [ "bang" ], + "parameter_enable" : 0, + "patching_rect" : [ 113.0, 60.0, 24.0, 24.0 ] + } + + } +, { + "box" : { + "comment" : "", + "id" : "obj-2", + "index" : 1, + "maxclass" : "outlet", + "numinlets" : 1, + "numoutlets" : 0, + "patching_rect" : [ 40.0, 60.0, 30.0, 30.0 ] + } + + } +, { + "box" : { + "comment" : "", + "id" : "obj-1", + "index" : 1, + "maxclass" : "inlet", + "numinlets" : 0, + "numoutlets" : 1, + "outlettype" : [ "" ], + "patching_rect" : [ 40.0, 22.0, 30.0, 30.0 ] + } + + } + ], + "lines" : [ { + "patchline" : { + "destination" : [ "obj-2", 0 ], + "order" : 1, + "source" : [ "obj-1", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-3", 0 ], + "order" : 0, + "source" : [ "obj-1", 0 ] + } + + } + ], + "saved_attribute_attributes" : { + "default_plcolor" : { + "expression" : "" + } + + } + + } + +}