From 33675188fd471c4463efc1b1bb808fdb7a9515df Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Tue, 7 Apr 2026 23:16:55 -0700 Subject: [PATCH 1/4] Speed up package minification --- pymini/pymini.py | 74 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/pymini/pymini.py b/pymini/pymini.py index a474314..e0726d0 100644 --- a/pymini/pymini.py +++ b/pymini/pymini.py @@ -1,6 +1,7 @@ import ast import copy import keyword +from collections import Counter from typing import Dict, List, Optional, Set from .utils import variable_name_generator @@ -299,6 +300,8 @@ def __init__( self.class_member_mappings = {} self.callable_argument_infos = {} self.class_method_argument_infos = {} + self._class_public_member_reference_cache = {} + self._module_attribute_reference_cache = {} self.modules = set(modules) # don't alias variables imported from these modules self.keep_global_variables = keep_global_variables self.rename_arguments = rename_arguments @@ -416,28 +419,61 @@ def _public_class_reference_count(self, node, old_name): count += 1 return count - def _public_member_reference_count(self, class_node, class_name, member_name): - count = 1 + def _class_public_member_references(self, class_node): + cache_key = id(class_node) + cached = self._class_public_member_reference_cache.get(cache_key) + if cached is not None: + return cached + + name_loads = Counter() + attribute_loads_by_base = {} for current in ast.walk(class_node): - if isinstance(current, ast.Name) and isinstance(current.ctx, ast.Load) and current.id == member_name: - count += 1 - elif ( - isinstance(current, ast.Attribute) - and current.attr == member_name - and isinstance(current.value, ast.Name) - and current.value.id in {"self", "cls", class_name} - ): - count += 1 + if isinstance(current, ast.Name) and isinstance(current.ctx, ast.Load): + name_loads[current.id] += 1 + elif isinstance(current, ast.Attribute) and isinstance(current.value, ast.Name): + base_name = current.value.id + base_counts = attribute_loads_by_base.get(base_name) + if base_counts is None: + base_counts = Counter() + attribute_loads_by_base[base_name] = base_counts + base_counts[current.attr] += 1 + + cached = { + "name_loads": name_loads, + "attribute_loads_by_base": attribute_loads_by_base, + } + self._class_public_member_reference_cache[cache_key] = cached + return cached + + def _module_attribute_references(self, module): + cache_key = id(module) + cached = self._module_attribute_reference_cache.get(cache_key) + if cached is not None: + return cached + + attribute_loads_by_base = {} + for current in ast.walk(module): + if not isinstance(current, ast.Attribute) or not isinstance(current.value, ast.Name): + continue + base_name = current.value.id + base_counts = attribute_loads_by_base.get(base_name) + if base_counts is None: + base_counts = Counter() + attribute_loads_by_base[base_name] = base_counts + base_counts[current.attr] += 1 + + self._module_attribute_reference_cache[cache_key] = attribute_loads_by_base + return attribute_loads_by_base + + def _public_member_reference_count(self, class_node, class_name, member_name): + references = self._class_public_member_references(class_node) + count = 1 + references["name_loads"].get(member_name, 0) + attribute_loads_by_base = references["attribute_loads_by_base"] + for base_name in {"self", "cls", class_name}: + count += attribute_loads_by_base.get(base_name, {}).get(member_name, 0) module = self._containing_module(class_node) if module is not None: - for current in ast.walk(module): - if ( - isinstance(current, ast.Attribute) - and current.attr == member_name - and isinstance(current.value, ast.Name) - and current.value.id == class_name - ): - count += 1 + count += self._module_attribute_references(module).get(class_name, {}).get(member_name, 0) return count def _public_global_reference_count(self, node, old_name): From a60536b014a49b90ab2c084de977f15bf52b9648 Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Tue, 7 Apr 2026 23:21:56 -0700 Subject: [PATCH 2/4] Document package speed baselines --- benchmarks/README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index e391878..81a13fb 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -95,6 +95,23 @@ Wheel-specific failures: ## Speed +### Package Mode vs `origin/main` + +One-shot package minification timings on the checked-in fixtures under +`.bench-repos`, using the same aggressive package settings as the compression +results (`--rename-modules --rename-global-variables --rename-arguments`): + +| Package | `origin/main` | this branch | speedup | +| --- | ---: | ---: | ---: | +| click | 9.290 s | 3.529 s | 2.63x | +| pytest | 27.858 s | 15.592 s | 1.79x | + +These are package-mode API timings measured with `.venv/bin/python` on the same +machine, comparing the current branch against a detached `origin/main` +worktree. + +### Tool Comparison + | Input | pymini | pyminifier | python-minifier | | --- | ---: | ---: | ---: | | pyminifier.py | 11.8 ms | 1.7 ms | 7.5 ms | @@ -110,7 +127,9 @@ Speed failures: The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The package rows are one-shot package minification timings from the same -environment used for the compression comparison. +environment used for the compression comparison. The `click` and `pytest` +baseline rows above are branch-vs-`origin/main` measurements on the checked-in +fixtures, rather than external-tool comparisons. # Reproduce From 25c9b2b0643f98692229138de6967be6d425f8df Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Tue, 7 Apr 2026 23:27:57 -0700 Subject: [PATCH 3/4] Fold speed baselines into table --- benchmarks/README.md | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index 81a13fb..9340eb6 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -95,31 +95,16 @@ Wheel-specific failures: ## Speed -### Package Mode vs `origin/main` - -One-shot package minification timings on the checked-in fixtures under -`.bench-repos`, using the same aggressive package settings as the compression -results (`--rename-modules --rename-global-variables --rename-arguments`): - -| Package | `origin/main` | this branch | speedup | -| --- | ---: | ---: | ---: | -| click | 9.290 s | 3.529 s | 2.63x | -| pytest | 27.858 s | 15.592 s | 1.79x | - -These are package-mode API timings measured with `.venv/bin/python` on the same -machine, comparing the current branch against a detached `origin/main` -worktree. - -### Tool Comparison - -| Input | pymini | pyminifier | python-minifier | -| --- | ---: | ---: | ---: | -| pyminifier.py | 11.8 ms | 1.7 ms | 7.5 ms | -| pyminify.py | 25.3 ms | 4.4 ms | 24.2 ms | -| TexSoup | 124.9 ms | 52.2 ms | 117.2 ms | -| timefhuman | 352.0 ms | 71.0 ms | 266.0 ms | -| pyminifier | 137.1 ms | 35.6 ms | 114.8 ms | -| rich | 3286.6 ms | failed | 1838.7 ms | +| Input | pymini | pymini (`origin/main`) | pyminifier | python-minifier | +| --- | ---: | ---: | ---: | ---: | +| pyminifier.py | 11.8 ms | - | 1.7 ms | 7.5 ms | +| pyminify.py | 25.3 ms | - | 4.4 ms | 24.2 ms | +| click | 3.529 s | 9.290 s | - | - | +| pytest | 15.592 s | 27.858 s | - | - | +| TexSoup | 124.9 ms | - | 52.2 ms | 117.2 ms | +| timefhuman | 352.0 ms | - | 71.0 ms | 266.0 ms | +| pyminifier | 137.1 ms | - | 35.6 ms | 114.8 ms | +| rich | 3286.6 ms | - | failed | 1838.7 ms | Speed failures: @@ -128,8 +113,9 @@ Speed failures: The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The package rows are one-shot package minification timings from the same environment used for the compression comparison. The `click` and `pytest` -baseline rows above are branch-vs-`origin/main` measurements on the checked-in -fixtures, rather than external-tool comparisons. +baseline column values are branch-vs-`origin/main` package-mode API timings on +the checked-in fixtures under `.bench-repos`, measured with `.venv/bin/python` +using `--rename-modules --rename-global-variables --rename-arguments`. # Reproduce From 9105854f3e200a23360f75230b27d39c68a87860 Mon Sep 17 00:00:00 2001 From: Alvin Wan Date: Tue, 7 Apr 2026 23:36:47 -0700 Subject: [PATCH 4/4] Update click and pytest speed baselines --- benchmarks/README.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index 9340eb6..ef603bd 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -95,27 +95,32 @@ Wheel-specific failures: ## Speed -| Input | pymini | pymini (`origin/main`) | pyminifier | python-minifier | -| --- | ---: | ---: | ---: | ---: | -| pyminifier.py | 11.8 ms | - | 1.7 ms | 7.5 ms | -| pyminify.py | 25.3 ms | - | 4.4 ms | 24.2 ms | -| click | 3.529 s | 9.290 s | - | - | -| pytest | 15.592 s | 27.858 s | - | - | -| TexSoup | 124.9 ms | - | 52.2 ms | 117.2 ms | -| timefhuman | 352.0 ms | - | 71.0 ms | 266.0 ms | -| pyminifier | 137.1 ms | - | 35.6 ms | 114.8 ms | -| rich | 3286.6 ms | - | failed | 1838.7 ms | +| Input | pymini | pyminifier | python-minifier | +| --- | ---: | ---: | ---: | +| pyminifier.py | 11.8 ms | 1.7 ms | 7.5 ms | +| pyminify.py | 25.3 ms | 4.4 ms | 24.2 ms | +| click | 3.529 s | failed | 914.4 ms | +| pytest | 15.592 s | failed | 4.567 s | +| TexSoup | 124.9 ms | 52.2 ms | 117.2 ms | +| timefhuman | 352.0 ms | 71.0 ms | 266.0 ms | +| pyminifier | 137.1 ms | 35.6 ms | 114.8 ms | +| rich | 3.287 s | failed | 1.839 s | Speed failures: +- click + pyminifier: minification fails on `click/__init__.py` with + `TypeError: 'NoneType' object is not subscriptable`. +- pytest + pyminifier: minification fails on `_pytest/_argcomplete.py` with + `TypeError: 'NoneType' object is not subscriptable`. - rich + pyminifier: the same minification failure prevents a timing result. The single-file rows come from [benchmark_speed.py](./benchmark_speed.py). The package rows are one-shot package minification timings from the same environment used for the compression comparison. The `click` and `pytest` -baseline column values are branch-vs-`origin/main` package-mode API timings on -the checked-in fixtures under `.bench-repos`, measured with `.venv/bin/python` -using `--rename-modules --rename-global-variables --rename-arguments`. +rows were measured on the checked-in fixtures under `.bench-repos`; `pymini` +used package mode with `--rename-modules --rename-global-variables +--rename-arguments`, while the baseline tools minified each file independently +in the preserved package tree. # Reproduce