Skip to content

Commit

Permalink
Show how often abstractions are used in frozen devices
Browse files Browse the repository at this point in the history
TODO: show other dependencies too
  • Loading branch information
Mattijs Kneppers committed Sep 10, 2024
1 parent c3467c0 commit 5fd5a8a
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 59 deletions.
25 changes: 21 additions & 4 deletions maxdiff/frozen_device_printer.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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


Expand Down
160 changes: 115 additions & 45 deletions maxdiff/get_frozen_stats.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand All @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions maxdiff/tests/test_baselines/FrozenTest.amxd.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions maxdiff/tests/test_baselines/StatsTest.amxd.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Binary file modified maxdiff/tests/test_files/StatsTest.amxd
Binary file not shown.
105 changes: 105 additions & 0 deletions maxdiff/tests/test_files/UnusedAbstraction.maxpat
Original file line number Diff line number Diff line change
@@ -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" : ""
}

}

}

}

0 comments on commit 5fd5a8a

Please sign in to comment.