From 1807f4e26b8a9da4460437ce764d22c68d1539de Mon Sep 17 00:00:00 2001 From: Mattijs Kneppers Date: Thu, 4 Jul 2024 10:32:15 +0200 Subject: [PATCH] Add statistics for frozen devices Adds first statistic: total amount of object instances --- maxdiff/frozen_device_printer.py | 2 + maxdiff/get_frozen_stats.py | 125 ++++++++++++++++++ maxdiff/tests/test.py | 10 ++ .../tests/test_baselines/FrozenTest.amxd.txt | 7 + .../tests/test_baselines/StatsTest.amxd.txt | 13 ++ maxdiff/tests/test_files/MyAbstraction.maxpat | 25 +++- maxdiff/tests/test_files/StatsTest.amxd | Bin 0 -> 8667 bytes maxdiff/tests/test_rewrite_baselines.py | 1 + 8 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 maxdiff/get_frozen_stats.py create mode 100644 maxdiff/tests/test_baselines/StatsTest.amxd.txt create mode 100644 maxdiff/tests/test_files/StatsTest.amxd diff --git a/maxdiff/frozen_device_printer.py b/maxdiff/frozen_device_printer.py index 5ebd9c4..684b02a 100644 --- a/maxdiff/frozen_device_printer.py +++ b/maxdiff/frozen_device_printer.py @@ -1,4 +1,5 @@ from freezing_utils import * +from get_frozen_stats import get_frozen_stats def print_frozen_device(data: bytes) -> str: @@ -20,6 +21,7 @@ def print_frozen_device(data: bytes) -> str: device_entries = get_device_entries(data, footer_entries) for entry in device_entries: frozen_string += entry["description"] + "\n" + frozen_string += get_frozen_stats(device_entries) return frozen_string diff --git a/maxdiff/get_frozen_stats.py b/maxdiff/get_frozen_stats.py new file mode 100644 index 0000000..6d72cb8 --- /dev/null +++ b/maxdiff/get_frozen_stats.py @@ -0,0 +1,125 @@ +import json +from freezing_utils import footer_entry_with_data + + +def get_frozen_stats(entries: list[footer_entry_with_data]): + """Returns statistics for this device""" + + device_data = entries[0]["data"] # the first entry is always the device file + + abstractions = [item for item in entries if item["file_name"].endswith(".maxpat")] + + # cache names of known abstractions + file_names = [item["file_name"] for item in abstractions] + + patch = get_patcher_dict(device_data) + object_count = count_objects(patch, abstractions, file_names) + + summary = "\n" + summary += "Total - Counting every abstraction instance - Indicates loading time\n" + summary += f" Object instances: {object_count}\n" + summary += " Connections:\n" + summary += "Unique - Counting abstractions once - Indicates maintainability\n" + summary += " Object instances:\n" + summary += " Connections:\n" + return summary + + +def count_objects(patcher, entries: list[dict], file_names: list[str]): + """Recursively counts all object instances in this patcher, + inluding in every instance of its dependencies""" + boxes = patcher["boxes"] + count = 0 + for box_entry in boxes: + box = box_entry["box"] + count += 1 + + if "patcher" in box: + patch = box["patcher"] + if box.get("maxclass") == "bpatcher" and box.get("embed") == 1: + # get embedded bpatcher count + count += count_objects(patch, entries, file_names) + else: + # get subpatcher count + count += count_objects(patch, entries, file_names) + else: + abstraction_name = get_abstraction_name(box, file_names) + if abstraction_name is None: + continue + + abstraction = [ + item for item in entries if item["file_name"] == abstraction_name + ][0] + abstraction_data = abstraction["data"] + abstraction_patch = get_patcher_dict(abstraction_data) + abstraction_count = count_objects(abstraction_patch, entries, file_names) + + if "text" in box and box["text"].startswith("poly~"): + # get poly abstraction count + voice_count = int(box["text"].split(" ")[2]) + count += abstraction_count * voice_count + else: + # get abstraction count + count += abstraction_count + + return count + + +def get_abstraction_name(box, file_names: list[str]): + """ + 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 + """ + if "text" in box: + if box["text"].startswith("poly~"): + name = box["text"].split(" ")[1] + ".maxpat" + if name in file_names: + return name + else: + raise ValueError( + "poly~ pointing to file that is not known as a dependency: " + name + ) + else: + name = box["text"].split(" ")[0] + ".maxpat" + if name in file_names: + return name + + if box.get("maxclass") == "bpatcher" and box.get("embed") != 1: + if box.get("name") in file_names: + return box["name"] + else: + raise ValueError( + "Non-embedded bpatcher pointing to file that is not known as a dependency: " + + box["name"] + ) + + return None + + +def get_patcher_dict(patch_data): + """Returns the dict that is represents the given patcher data. + Throws errors if parsing fails""" + device_data_text = "" + + try: + if patch_data[len(patch_data) - 1] == 0: + device_data_text = patch_data[: len(patch_data) - 1].decode("utf-8") + else: + device_data_text = patch_data.decode("utf-8") + except Exception as e: + print(f"Error getting device patch data as text: {e}") + + if device_data_text == "": + print("Device has no data") + + patcher_dict = {} + try: + patcher_dict = json.loads(device_data_text) + except ValueError as e: + print(f"Error parsing device patch data as json: {e}") + + if not "patcher" in patcher_dict: + print("Device content does not seem to be a patcher") + + return patcher_dict["patcher"] diff --git a/maxdiff/tests/test.py b/maxdiff/tests/test.py index 99cd4ca..6b4b3fd 100644 --- a/maxdiff/tests/test.py +++ b/maxdiff/tests/test.py @@ -40,6 +40,16 @@ def test_parse_frozen_device(self): actual = parse(test_path) self.assertEqual(expected, actual) + def test_parse_frozen_device(self): + self.maxDiff = None + + expected_path, test_path = get_test_path_files("StatsTest.amxd") + + with open(expected_path, mode="r") as expected_file: + expected = expected_file.read() + actual = parse(test_path) + self.assertEqual(expected, actual) + def test_parse_maxpat(self): self.maxDiff = None diff --git a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt index 3536011..8cac611 100644 --- a/maxdiff/tests/test_baselines/FrozenTest.amxd.txt +++ b/maxdiff/tests/test_baselines/FrozenTest.amxd.txt @@ -10,3 +10,10 @@ 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 collContent.txt: 8 bytes, modified at 2024/05/24 13:59:36 UTC + +Total - Counting every abstraction instance - Indicates loading time + Object instances: 44 + Connections: +Unique - Counting abstractions once - Indicates maintainability + Object instances: + Connections: diff --git a/maxdiff/tests/test_baselines/StatsTest.amxd.txt b/maxdiff/tests/test_baselines/StatsTest.amxd.txt new file mode 100644 index 0000000..b8dfa6e --- /dev/null +++ b/maxdiff/tests/test_baselines/StatsTest.amxd.txt @@ -0,0 +1,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 + +Total - Counting every abstraction instance - Indicates loading time + Object instances: 27 + Connections: +Unique - Counting abstractions once - Indicates maintainability + Object instances: + Connections: diff --git a/maxdiff/tests/test_files/MyAbstraction.maxpat b/maxdiff/tests/test_files/MyAbstraction.maxpat index 283c322..9bb3c77 100644 --- a/maxdiff/tests/test_files/MyAbstraction.maxpat +++ b/maxdiff/tests/test_files/MyAbstraction.maxpat @@ -3,8 +3,8 @@ "fileversion" : 1, "appversion" : { "major" : 8, - "minor" : 5, - "revision" : 5, + "minor" : 6, + "revision" : 2, "architecture" : "x64", "modernui" : 1 } @@ -39,6 +39,18 @@ "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", @@ -67,6 +79,15 @@ "lines" : [ { "patchline" : { "destination" : [ "obj-2", 0 ], + "order" : 1, + "source" : [ "obj-1", 0 ] + } + + } +, { + "patchline" : { + "destination" : [ "obj-3", 0 ], + "order" : 0, "source" : [ "obj-1", 0 ] } diff --git a/maxdiff/tests/test_files/StatsTest.amxd b/maxdiff/tests/test_files/StatsTest.amxd new file mode 100644 index 0000000000000000000000000000000000000000..9b28f1124b17942943d7dc32cb59f4691a773e28 GIT binary patch literal 8667 zcmd^FJC7sB5oTaO;KG5NDR40E_-@4^_jQgehXQN|=WHPm&6y^*b@ReZb00G9{)BS{ z`Ws4=DN*jwfZ@PD!J+HX-P1js;c^8ThV2E899H$Ks;la&sySIU+n>#5GYNmCG7_IZ zf`5(4?!NuiY*s%0CWD_J!$0^-{_>}@xo9LXRV$KTCv*5c7h7GZd)0Qju8^3%LZWP% zfHtR*b5Y7KYnpk3gW$KW`1ck6Zq>bZ>nxch+w4vom6?63aCz}~bs_BRI#+GA*R(6F z_x=p&A}eIqRUl8-$c*P~>PMO*T385&-zLk|)%oI8k}fXs>mp6@Yk9ReUnF19GIgV> zemKxyCH+Tw^~Mj}?20;jr*d3(;iu}VX;r5xBaKxI^K!M7`@*cZb!9sJ6`e1|h4JVi zb88QO$U)gi6`!?Q7RhhxB4_D#tf`EvMR=-Wa`OtU4Lwsh+Txc?70qS{u$*zX3Dj&(QXg{kw zG%)xR)gBzVP=*$#TesADZycH383r@WJT-Lm+eB<+wG*%GHyb8SClPeDTlY?wvdb>f zh*#u{OZe=k81=}?G|48}2s+*SGyH)Esx^(gKEA?`~MF&^v$sr_wxXzHqp#cXRz(Y9e048Y004j)f z2p2@2h74Y502@eT=pgnH_#l37jyH@Q%Nx4GeAZ1ub0ZG!1QJ z_HNhK`-%WO5+}g)t4KMK@?u;k5&n(JgA^skch=1t0>&FgLZ z^R+al)nLG?f1;EMj}si&mkCXg)dd$&JR}rKJz@dkswv?$XTl^c<=LTB&H9XR@qYYdl%AHawUM&h7Y@thzq ztelvF3);MeYQ$GPeg-pLR^rJ~AaP}iU(u^Fa@%v51iJ1=olr=)uqNv5wo|s6e@&6} zHKs`3=?_D==y@ZNIZdIar|Q_Z1#ZCN`#JYoJy33<3vHh8N>VZcwoV(Z+3-Rfv!*Dp z;`eG8l-4UY1r}uXsaL?5(zqnMpxpP8 zBKK4kQ|x!J*@qW7#805J)LU0%q5pZEEy&!S)tRndrn2!QAhaT%0Gw_`-sxg1^5g?6 zddAhrYS?J530o$NCW;oZ<*-=}fZci@8EOdV&IiYV96>tm&HMnw!Gw@M>FhsZyCAHL zZs&3a7h9~fxq`wmS5@}3&Lp&9F$jg-93NjI#l>M3VpDD4$b=o@!>8aDP)T6z)zP!w zZa+1U@O1RK#`bP!eMq>rjrjcSAAawM8ah!r(-ev+BECO!mjewdIIBl5p}Td-jUU1d zYyj(WU#<&%uhv1s*!H^|8;&@_#s1f}Zp(F6mmrVc__)lAH*Jk=Ox)dOPa@cYZC7Ni zqP#A*y|mTko5kXCdGTgNs44)E^;Ty`#~?8DZ2?N~>0`B^FK8rs7!h(0nr?T{YqY&z z-sVoj)$(~Cbd}c+1TW0Pj-J1G&1tMXJ+20LEDO6D2@WlAfEs8$yW}zS3TV``LQo~K zMdL3~$R}`SJA_Br97l8tJ1fp|U~tUN;RS*&x}bWur;+7j?hGfrTCJ|H)0=CnUeG?Y ztc%`6FxkGxJ=_eth^6Ae#5r4@=<3$Q36sF97?5<86?@Ph6h{);t+>JhL}jVyK74oh z4D<8%XERV3C&T~!?>Sc7VO%ab%q|J2(`%pD95$%Lk>-@o;iF^)`(L2ZhZKpgZCUIk zy%U%e+vP`5S1N?*mnE-3HtE|1{*F(|FGCxnyFC2>;AcS_O!xzMy`FRctC8pWNu(nW zG;ssW>Tj%vM6QG%gztIL8TkJUf6sNR;Jd(71HONbN$nO=Xn6kR+kIKe_UVt12%lpD z1GHK9)oO|3X0uQJQE%Pz>|@L-nLe9s3%Rq;zbGN8#OJ?#^Zj>ruV!Sg_|=|G)?r?- W-$#G#_xt3Z6Z_5leqaCa=YIj!>J=~m literal 0 HcmV?d00001 diff --git a/maxdiff/tests/test_rewrite_baselines.py b/maxdiff/tests/test_rewrite_baselines.py index 390cac9..dbcc68f 100644 --- a/maxdiff/tests/test_rewrite_baselines.py +++ b/maxdiff/tests/test_rewrite_baselines.py @@ -14,6 +14,7 @@ def run(): rewrite_file("Test.amxd") rewrite_file("EncryptedTest.amxd") rewrite_file("FrozenTest.amxd") + rewrite_file("StatsTest.amxd") rewrite_file("Test.maxpat") rewrite_file("Test Project/Zipped.als") rewrite_file("Test Project/Test.als")