asmdiff 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asmdiff-0.1.6 → asmdiff-0.1.7}/PKG-INFO +36 -31
- {asmdiff-0.1.6 → asmdiff-0.1.7}/README.md +35 -30
- asmdiff-0.1.7/Screenshot 2026-07-07 225145.jpg +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/asmdiff.py +74 -29
- {asmdiff-0.1.6 → asmdiff-0.1.7}/pyproject.toml +1 -1
- {asmdiff-0.1.6 → asmdiff-0.1.7}/test_asmdiff.py +70 -8
- {asmdiff-0.1.6 → asmdiff-0.1.7}/.claude/settings.local.json +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/.gitignore +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/2026-07-06-inspect-mode.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/2026-07-07-elf-mode-handoff.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/CLAUDE.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/FIELD-TEST-REPORT.md +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/LICENSE +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/asmdiff.example.toml +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/asmdiff_example.c +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/.gitignore +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
- {asmdiff-0.1.6 → asmdiff-0.1.7}/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Compare per-function assembly between paired C implementations
|
|
5
5
|
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
6
|
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
does the compiler actually emit - before and after?** It compiles a small
|
|
31
31
|
harness file across a matrix of compilers, extracts each variant function's
|
|
32
32
|
assembly, and prints side-by-side listings plus a summary of instruction
|
|
33
|
-
counts,
|
|
33
|
+
counts, loop spans, and outbound calls.
|
|
34
34
|
|
|
35
35
|
Compilers and flags are configured per project through named targets in an
|
|
36
36
|
`asmdiff.toml` file, and any GNU-as ELF assembly is parsed.
|
|
@@ -94,9 +94,9 @@ endbr64 | endbr64
|
|
|
94
94
|
mulss .LC0(%rip), %xmm0 | movl $-5, %edi
|
|
95
95
|
ret | jmp ldexpf@PLT
|
|
96
96
|
|
|
97
|
-
function role insns
|
|
98
|
-
old_scale baseline 3 -
|
|
99
|
-
new_scale candidate 3 ldexpf
|
|
97
|
+
function role insns loop spans calls
|
|
98
|
+
old_scale baseline 3 - -
|
|
99
|
+
new_scale candidate 3 - ldexpf
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
Read the `calls` column first: `-` means the construct lowered to inline
|
|
@@ -157,9 +157,9 @@ call8 __udivdi3 |
|
|
|
157
157
|
mov.n a2, a10 |
|
|
158
158
|
retw.n |
|
|
159
159
|
|
|
160
|
-
function role insns
|
|
161
|
-
old_elapsed_ms baseline 10 __udivdi3
|
|
162
|
-
new_elapsed_ms candidate 6 -
|
|
160
|
+
function role insns loop spans calls
|
|
161
|
+
old_elapsed_ms baseline 10 - __udivdi3
|
|
162
|
+
new_elapsed_ms candidate 6 - -
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
The candidate is six inline instructions ending in a multiply-high by
|
|
@@ -216,8 +216,8 @@ apply_gain:
|
|
|
216
216
|
.L1:
|
|
217
217
|
retw.n
|
|
218
218
|
|
|
219
|
-
function insns
|
|
220
|
-
apply_gain 13
|
|
219
|
+
function insns loop spans calls
|
|
220
|
+
apply_gain 13 .L3_LEND:4 -
|
|
221
221
|
```
|
|
222
222
|
|
|
223
223
|
The span `.L3_LEND:4` is the four-instruction body of the Xtensa
|
|
@@ -246,11 +246,11 @@ $ asmdiff build/S3-Amysynth.elf --filter '^render_' # stats table only
|
|
|
246
246
|
```
|
|
247
247
|
|
|
248
248
|
```
|
|
249
|
-
function insns
|
|
250
|
-
render_lut_cub 98
|
|
251
|
-
render_lut 51
|
|
252
|
-
render_external_audio_in 32
|
|
253
|
-
render_partial 112 exp2f, __divsf3, ...
|
|
249
|
+
function insns loop spans calls
|
|
250
|
+
render_lut_cub 98 .L32:74 -
|
|
251
|
+
render_lut 51 .L7e_LEND:32 -
|
|
252
|
+
render_external_audio_in 32 .L52_LEND:10 -
|
|
253
|
+
render_partial 112 .L66:31 exp2f, __divsf3, ...
|
|
254
254
|
```
|
|
255
255
|
|
|
256
256
|
(Four of the eleven matched rows shown.) The columns are the same as
|
|
@@ -280,10 +280,10 @@ one row scan.
|
|
|
280
280
|
```
|
|
281
281
|
$ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
|
|
282
282
|
|
|
283
|
-
function insns
|
|
284
|
-
sequencer_timer_callback$lto_priv$0 77 __divsf3, __udivdi3
|
|
285
|
-
sequencer_recompute 46 __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
|
|
286
|
-
sequencer_process_tick$lto_priv$0 117
|
|
283
|
+
function insns loop spans calls
|
|
284
|
+
sequencer_timer_callback$lto_priv$0 77 .L21:41 __divsf3, __udivdi3
|
|
285
|
+
sequencer_recompute 46 - __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
|
|
286
|
+
sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8
|
|
287
287
|
```
|
|
288
288
|
|
|
289
289
|
A timer callback pays a software float divide *and* a 64-bit
|
|
@@ -625,14 +625,14 @@ $ asmdiff old/delay.c new/delay.c
|
|
|
625
625
|
|
|
626
626
|
-- old/delay.c --
|
|
627
627
|
|
|
628
|
-
function insns
|
|
629
|
-
stereo_reverb 437
|
|
628
|
+
function insns loop spans calls
|
|
629
|
+
stereo_reverb 437 .L108:327 -
|
|
630
630
|
...
|
|
631
|
-
TOTAL (13 functions) 956 malloc_caps, free, ...
|
|
631
|
+
TOTAL (13 functions) 956 - malloc_caps, free, ...
|
|
632
632
|
|
|
633
633
|
-- new/delay.c --
|
|
634
634
|
...
|
|
635
|
-
TOTAL (13 functions) 1028 malloc_caps, free, ...
|
|
635
|
+
TOTAL (13 functions) 1028 - malloc_caps, free, ...
|
|
636
636
|
```
|
|
637
637
|
|
|
638
638
|
The TOTAL row is a coarse sanity check — did this refactor move the file's
|
|
@@ -643,8 +643,13 @@ measurement, and per-function rows are where the real information is.
|
|
|
643
643
|
Only labels the assembler types as functions are listed — global data
|
|
644
644
|
(string constants, state structs, lookup tables) gets column-0 labels too
|
|
645
645
|
but is not code. A `calls` list longer than 8 symbols is truncated to
|
|
646
|
-
`...,
|
|
647
|
-
symbols and would otherwise make rows thousands of characters
|
|
646
|
+
`..., Total Calls:N`; real firmware dispatch functions call dozens of
|
|
647
|
+
distinct symbols and would otherwise make rows thousands of characters
|
|
648
|
+
wide. When stdout is a terminal, rows are additionally trimmed to the
|
|
649
|
+
terminal width: callees are dropped from the end of the `calls` column
|
|
650
|
+
(never the first one) behind the same `Total Calls:N` summary, so every
|
|
651
|
+
row stays on one line. Piped or redirected output skips the width trim
|
|
652
|
+
and keeps the full capped list, so it stays stable and greppable.
|
|
648
653
|
|
|
649
654
|
## Comparing the same function across two builds (`--across`)
|
|
650
655
|
|
|
@@ -705,9 +710,9 @@ movl %r14d, %r11d | sarl $11, %ebp
|
|
|
705
710
|
call SMULR6 | leal 1024(%rax), %edx
|
|
706
711
|
[... 60 rows omitted ...]
|
|
707
712
|
|
|
708
|
-
function role insns
|
|
709
|
-
dsps_biquad_f32_ansi [cc#1] baseline 59
|
|
710
|
-
dsps_biquad_f32_ansi [cc#2] candidate 89
|
|
713
|
+
function role insns loop spans calls
|
|
714
|
+
dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
|
|
715
|
+
dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
|
|
711
716
|
```
|
|
712
717
|
|
|
713
718
|
(The listing is abridged here; the tool prints all 92 rows. The columns
|
|
@@ -743,9 +748,9 @@ call ldexpf@PLT |
|
|
|
743
748
|
leave |
|
|
744
749
|
ret |
|
|
745
750
|
|
|
746
|
-
function role insns
|
|
747
|
-
new_rt [cc#1] baseline 13 ldexpf
|
|
748
|
-
new_rt [cc#2] candidate 2 ldexpf
|
|
751
|
+
function role insns loop spans calls
|
|
752
|
+
new_rt [cc#1] baseline 13 - ldexpf
|
|
753
|
+
new_rt [cc#2] candidate 2 - ldexpf
|
|
749
754
|
```
|
|
750
755
|
|
|
751
756
|
**Two files** — before/after versions of a source file (e.g. from a git
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
does the compiler actually emit - before and after?** It compiles a small
|
|
13
13
|
harness file across a matrix of compilers, extracts each variant function's
|
|
14
14
|
assembly, and prints side-by-side listings plus a summary of instruction
|
|
15
|
-
counts,
|
|
15
|
+
counts, loop spans, and outbound calls.
|
|
16
16
|
|
|
17
17
|
Compilers and flags are configured per project through named targets in an
|
|
18
18
|
`asmdiff.toml` file, and any GNU-as ELF assembly is parsed.
|
|
@@ -76,9 +76,9 @@ endbr64 | endbr64
|
|
|
76
76
|
mulss .LC0(%rip), %xmm0 | movl $-5, %edi
|
|
77
77
|
ret | jmp ldexpf@PLT
|
|
78
78
|
|
|
79
|
-
function role insns
|
|
80
|
-
old_scale baseline 3 -
|
|
81
|
-
new_scale candidate 3 ldexpf
|
|
79
|
+
function role insns loop spans calls
|
|
80
|
+
old_scale baseline 3 - -
|
|
81
|
+
new_scale candidate 3 - ldexpf
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
Read the `calls` column first: `-` means the construct lowered to inline
|
|
@@ -139,9 +139,9 @@ call8 __udivdi3 |
|
|
|
139
139
|
mov.n a2, a10 |
|
|
140
140
|
retw.n |
|
|
141
141
|
|
|
142
|
-
function role insns
|
|
143
|
-
old_elapsed_ms baseline 10 __udivdi3
|
|
144
|
-
new_elapsed_ms candidate 6 -
|
|
142
|
+
function role insns loop spans calls
|
|
143
|
+
old_elapsed_ms baseline 10 - __udivdi3
|
|
144
|
+
new_elapsed_ms candidate 6 - -
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
The candidate is six inline instructions ending in a multiply-high by
|
|
@@ -198,8 +198,8 @@ apply_gain:
|
|
|
198
198
|
.L1:
|
|
199
199
|
retw.n
|
|
200
200
|
|
|
201
|
-
function insns
|
|
202
|
-
apply_gain 13
|
|
201
|
+
function insns loop spans calls
|
|
202
|
+
apply_gain 13 .L3_LEND:4 -
|
|
203
203
|
```
|
|
204
204
|
|
|
205
205
|
The span `.L3_LEND:4` is the four-instruction body of the Xtensa
|
|
@@ -228,11 +228,11 @@ $ asmdiff build/S3-Amysynth.elf --filter '^render_' # stats table only
|
|
|
228
228
|
```
|
|
229
229
|
|
|
230
230
|
```
|
|
231
|
-
function insns
|
|
232
|
-
render_lut_cub 98
|
|
233
|
-
render_lut 51
|
|
234
|
-
render_external_audio_in 32
|
|
235
|
-
render_partial 112 exp2f, __divsf3, ...
|
|
231
|
+
function insns loop spans calls
|
|
232
|
+
render_lut_cub 98 .L32:74 -
|
|
233
|
+
render_lut 51 .L7e_LEND:32 -
|
|
234
|
+
render_external_audio_in 32 .L52_LEND:10 -
|
|
235
|
+
render_partial 112 .L66:31 exp2f, __divsf3, ...
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
(Four of the eleven matched rows shown.) The columns are the same as
|
|
@@ -262,10 +262,10 @@ one row scan.
|
|
|
262
262
|
```
|
|
263
263
|
$ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
|
|
264
264
|
|
|
265
|
-
function insns
|
|
266
|
-
sequencer_timer_callback$lto_priv$0 77 __divsf3, __udivdi3
|
|
267
|
-
sequencer_recompute 46 __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
|
|
268
|
-
sequencer_process_tick$lto_priv$0 117
|
|
265
|
+
function insns loop spans calls
|
|
266
|
+
sequencer_timer_callback$lto_priv$0 77 .L21:41 __divsf3, __udivdi3
|
|
267
|
+
sequencer_recompute 46 - __extendsfdf2, __divdf3, __muldf3, __fixunsdfsi, __divsf3
|
|
268
|
+
sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaphoreTake, xQueueGenericSend, add_delta_to_queue, a8
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
A timer callback pays a software float divide *and* a 64-bit
|
|
@@ -607,14 +607,14 @@ $ asmdiff old/delay.c new/delay.c
|
|
|
607
607
|
|
|
608
608
|
-- old/delay.c --
|
|
609
609
|
|
|
610
|
-
function insns
|
|
611
|
-
stereo_reverb 437
|
|
610
|
+
function insns loop spans calls
|
|
611
|
+
stereo_reverb 437 .L108:327 -
|
|
612
612
|
...
|
|
613
|
-
TOTAL (13 functions) 956 malloc_caps, free, ...
|
|
613
|
+
TOTAL (13 functions) 956 - malloc_caps, free, ...
|
|
614
614
|
|
|
615
615
|
-- new/delay.c --
|
|
616
616
|
...
|
|
617
|
-
TOTAL (13 functions) 1028 malloc_caps, free, ...
|
|
617
|
+
TOTAL (13 functions) 1028 - malloc_caps, free, ...
|
|
618
618
|
```
|
|
619
619
|
|
|
620
620
|
The TOTAL row is a coarse sanity check — did this refactor move the file's
|
|
@@ -625,8 +625,13 @@ measurement, and per-function rows are where the real information is.
|
|
|
625
625
|
Only labels the assembler types as functions are listed — global data
|
|
626
626
|
(string constants, state structs, lookup tables) gets column-0 labels too
|
|
627
627
|
but is not code. A `calls` list longer than 8 symbols is truncated to
|
|
628
|
-
`...,
|
|
629
|
-
symbols and would otherwise make rows thousands of characters
|
|
628
|
+
`..., Total Calls:N`; real firmware dispatch functions call dozens of
|
|
629
|
+
distinct symbols and would otherwise make rows thousands of characters
|
|
630
|
+
wide. When stdout is a terminal, rows are additionally trimmed to the
|
|
631
|
+
terminal width: callees are dropped from the end of the `calls` column
|
|
632
|
+
(never the first one) behind the same `Total Calls:N` summary, so every
|
|
633
|
+
row stays on one line. Piped or redirected output skips the width trim
|
|
634
|
+
and keeps the full capped list, so it stays stable and greppable.
|
|
630
635
|
|
|
631
636
|
## Comparing the same function across two builds (`--across`)
|
|
632
637
|
|
|
@@ -687,9 +692,9 @@ movl %r14d, %r11d | sarl $11, %ebp
|
|
|
687
692
|
call SMULR6 | leal 1024(%rax), %edx
|
|
688
693
|
[... 60 rows omitted ...]
|
|
689
694
|
|
|
690
|
-
function role insns
|
|
691
|
-
dsps_biquad_f32_ansi [cc#1] baseline 59
|
|
692
|
-
dsps_biquad_f32_ansi [cc#2] candidate 89
|
|
695
|
+
function role insns loop spans calls
|
|
696
|
+
dsps_biquad_f32_ansi [cc#1] baseline 59 .L27:32 SMULR6
|
|
697
|
+
dsps_biquad_f32_ansi [cc#2] candidate 89 .L26:54 -
|
|
693
698
|
```
|
|
694
699
|
|
|
695
700
|
(The listing is abridged here; the tool prints all 92 rows. The columns
|
|
@@ -725,9 +730,9 @@ call ldexpf@PLT |
|
|
|
725
730
|
leave |
|
|
726
731
|
ret |
|
|
727
732
|
|
|
728
|
-
function role insns
|
|
729
|
-
new_rt [cc#1] baseline 13 ldexpf
|
|
730
|
-
new_rt [cc#2] candidate 2 ldexpf
|
|
733
|
+
function role insns loop spans calls
|
|
734
|
+
new_rt [cc#1] baseline 13 - ldexpf
|
|
735
|
+
new_rt [cc#2] candidate 2 - ldexpf
|
|
731
736
|
```
|
|
732
737
|
|
|
733
738
|
**Two files** — before/after versions of a source file (e.g. from a git
|
|
Binary file
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
Compiles a harness C file across a matrix of compilers, extracts each
|
|
5
5
|
variant function's assembly from the -S output, and prints side-by-side
|
|
6
|
-
listings plus a summary of instruction counts,
|
|
7
|
-
|
|
6
|
+
listings plus a summary of instruction counts, loop spans (instructions
|
|
7
|
+
between a local label and its last backward branch), and outbound calls.
|
|
8
8
|
Automates fold-vs-libcall analysis when evaluating micro-optimisations
|
|
9
9
|
(e.g. "does this still compile to one instruction, or is it a libcall?").
|
|
10
10
|
|
|
@@ -1050,14 +1050,54 @@ def side_by_side(left, right, ltitle, rtitle, width=44):
|
|
|
1050
1050
|
return "\n".join(rows)
|
|
1051
1051
|
|
|
1052
1052
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1053
|
+
TOTAL_CALLS_RE = re.compile(r"Total Calls:\d+")
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def _fit_calls(cell, budget):
|
|
1057
|
+
"""Trim a comma-joined callee cell to at most budget columns by
|
|
1058
|
+
dropping whole callees from the end, closing with a Total Calls:N
|
|
1059
|
+
summary (N is always the full callee count, preserved from an
|
|
1060
|
+
existing summary when the cell was already capped). The first
|
|
1061
|
+
callee is never dropped; a cell that still overflows just wraps."""
|
|
1062
|
+
if len(cell) <= budget:
|
|
1063
|
+
return cell
|
|
1064
|
+
items = cell.split(", ")
|
|
1065
|
+
if TOTAL_CALLS_RE.fullmatch(items[-1]):
|
|
1066
|
+
items, summary = items[:-1], items[-1]
|
|
1067
|
+
else:
|
|
1068
|
+
summary = f"Total Calls:{len(items)}"
|
|
1069
|
+
if len(items) < 2:
|
|
1070
|
+
return cell
|
|
1071
|
+
for keep in range(len(items) - 1, 0, -1):
|
|
1072
|
+
cand = ", ".join(items[:keep]) + ", " + summary
|
|
1073
|
+
if len(cand) <= budget:
|
|
1074
|
+
return cand
|
|
1075
|
+
return items[0] + ", " + summary
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def render_table(rows, max_width=None):
|
|
1079
|
+
"""Column-aligned text for a list of equal-length string tuples.
|
|
1080
|
+
|
|
1081
|
+
max_width (terminal columns) keeps each row on one line by
|
|
1082
|
+
trimming the last column's cells (see _fit_calls); the other
|
|
1083
|
+
columns are never touched. None renders untrimmed."""
|
|
1084
|
+
if max_width is not None:
|
|
1085
|
+
fixed = [max(len(row[i]) for row in rows)
|
|
1086
|
+
for i in range(len(rows[0]) - 1)]
|
|
1087
|
+
budget = max_width - sum(fixed) - 2 * len(fixed)
|
|
1088
|
+
rows = [row[:-1] + (_fit_calls(row[-1], budget),) for row in rows]
|
|
1055
1089
|
widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))]
|
|
1056
1090
|
return "\n".join(
|
|
1057
1091
|
" ".join(cell.ljust(w) for cell, w in zip(row, widths)).rstrip()
|
|
1058
1092
|
for row in rows)
|
|
1059
1093
|
|
|
1060
1094
|
|
|
1095
|
+
def table_width():
|
|
1096
|
+
"""Width budget for summary tables: the terminal width when stdout
|
|
1097
|
+
is a tty, else None so piped output keeps every callee greppable."""
|
|
1098
|
+
return shutil.get_terminal_size().columns if sys.stdout.isatty() else None
|
|
1099
|
+
|
|
1100
|
+
|
|
1061
1101
|
def format_spans(spans):
|
|
1062
1102
|
return " ".join(f"{label}:{n}" for label, n in spans) or "-"
|
|
1063
1103
|
|
|
@@ -1068,27 +1108,31 @@ MAX_CALLS_SHOWN = 8
|
|
|
1068
1108
|
|
|
1069
1109
|
|
|
1070
1110
|
def format_calls(calls):
|
|
1071
|
-
"""Comma-joined callee list
|
|
1111
|
+
"""Comma-joined callee list; longer lists keep the first
|
|
1112
|
+
MAX_CALLS_SHOWN callees and close with a Total Calls:N summary."""
|
|
1072
1113
|
if not calls:
|
|
1073
1114
|
return "-"
|
|
1074
1115
|
if len(calls) > MAX_CALLS_SHOWN:
|
|
1075
1116
|
return (", ".join(calls[:MAX_CALLS_SHOWN])
|
|
1076
|
-
+ f",
|
|
1117
|
+
+ f", Total Calls:{len(calls)}")
|
|
1077
1118
|
return ", ".join(calls)
|
|
1078
1119
|
|
|
1079
1120
|
|
|
1080
|
-
def summary_table(pairs, funcs):
|
|
1081
|
-
"""Instruction counts,
|
|
1082
|
-
|
|
1121
|
+
def summary_table(pairs, funcs, max_width=None):
|
|
1122
|
+
"""Instruction counts, loop spans, and outbound calls per pair
|
|
1123
|
+
member. Calls come last: the one unbounded column stays ragged
|
|
1124
|
+
right so the counts and spans keep their alignment."""
|
|
1125
|
+
rows = [("function", "role", "insns", "loop spans", "calls")]
|
|
1083
1126
|
for old, new in pairs:
|
|
1084
1127
|
for name, role in ((old, "baseline"), (new, "candidate")):
|
|
1085
1128
|
insns, calls = analyze(funcs[name])
|
|
1086
|
-
rows.append((name, role, str(insns),
|
|
1087
|
-
format_spans(loop_spans(funcs[name]))
|
|
1088
|
-
|
|
1129
|
+
rows.append((name, role, str(insns),
|
|
1130
|
+
format_spans(loop_spans(funcs[name])),
|
|
1131
|
+
format_calls(calls)))
|
|
1132
|
+
return render_table(rows, max_width)
|
|
1089
1133
|
|
|
1090
1134
|
|
|
1091
|
-
def file_summary_table(funcs):
|
|
1135
|
+
def file_summary_table(funcs, max_width=None):
|
|
1092
1136
|
"""Per-function counts plus a whole-file total row.
|
|
1093
1137
|
|
|
1094
1138
|
The total sums instruction counts over every function parsed from
|
|
@@ -1096,7 +1140,7 @@ def file_summary_table(funcs):
|
|
|
1096
1140
|
check, not a code-size measurement (literal pools, data, and
|
|
1097
1141
|
alignment are not included).
|
|
1098
1142
|
"""
|
|
1099
|
-
rows = [("function", "insns", "
|
|
1143
|
+
rows = [("function", "insns", "loop spans", "calls")]
|
|
1100
1144
|
total_insns, all_calls = 0, []
|
|
1101
1145
|
for name, lines in funcs.items():
|
|
1102
1146
|
insns, calls = analyze(lines)
|
|
@@ -1104,11 +1148,11 @@ def file_summary_table(funcs):
|
|
|
1104
1148
|
for sym in calls:
|
|
1105
1149
|
if sym not in all_calls:
|
|
1106
1150
|
all_calls.append(sym)
|
|
1107
|
-
rows.append((name, str(insns),
|
|
1108
|
-
format_spans(loop_spans(lines))))
|
|
1151
|
+
rows.append((name, str(insns),
|
|
1152
|
+
format_spans(loop_spans(lines)), format_calls(calls)))
|
|
1109
1153
|
rows.append((f"TOTAL ({len(funcs)} functions)", str(total_insns),
|
|
1110
|
-
format_calls(all_calls)
|
|
1111
|
-
return render_table(rows)
|
|
1154
|
+
"-", format_calls(all_calls)))
|
|
1155
|
+
return render_table(rows, max_width)
|
|
1112
1156
|
|
|
1113
1157
|
|
|
1114
1158
|
def listing(name, lines):
|
|
@@ -1119,15 +1163,16 @@ def listing(name, lines):
|
|
|
1119
1163
|
return "\n".join([f"{name}:"] + body)
|
|
1120
1164
|
|
|
1121
1165
|
|
|
1122
|
-
def inspect_table(fn_names, funcs):
|
|
1166
|
+
def inspect_table(fn_names, funcs, max_width=None):
|
|
1123
1167
|
"""Stats rows for the inspected functions only - no pair roles and
|
|
1124
1168
|
no whole-file total, unlike summary_table/file_summary_table."""
|
|
1125
|
-
rows = [("function", "insns", "
|
|
1169
|
+
rows = [("function", "insns", "loop spans", "calls")]
|
|
1126
1170
|
for name in fn_names:
|
|
1127
1171
|
insns, calls = analyze(funcs[name])
|
|
1128
|
-
rows.append((name, str(insns),
|
|
1129
|
-
format_spans(loop_spans(funcs[name]))
|
|
1130
|
-
|
|
1172
|
+
rows.append((name, str(insns),
|
|
1173
|
+
format_spans(loop_spans(funcs[name])),
|
|
1174
|
+
format_calls(calls)))
|
|
1175
|
+
return render_table(rows, max_width)
|
|
1131
1176
|
|
|
1132
1177
|
|
|
1133
1178
|
def file_tags(a, b):
|
|
@@ -1158,7 +1203,7 @@ def report_across(fn_names, left_funcs, right_funcs, left_tag, right_tag):
|
|
|
1158
1203
|
for lt, rt in pairs:
|
|
1159
1204
|
print(side_by_side(decorated[lt], decorated[rt], lt, rt))
|
|
1160
1205
|
print()
|
|
1161
|
-
print(summary_table(pairs, decorated))
|
|
1206
|
+
print(summary_table(pairs, decorated, table_width()))
|
|
1162
1207
|
|
|
1163
1208
|
|
|
1164
1209
|
def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
@@ -1228,7 +1273,7 @@ def run_summary(sources, matrix, extra_flags, tmp):
|
|
|
1228
1273
|
if len(sections) > 1:
|
|
1229
1274
|
print(f"\n-- {tag} --")
|
|
1230
1275
|
print()
|
|
1231
|
-
print(file_summary_table(funcs) if funcs
|
|
1276
|
+
print(file_summary_table(funcs, table_width()) if funcs
|
|
1232
1277
|
else "(no functions found)")
|
|
1233
1278
|
if not ran_any:
|
|
1234
1279
|
sys.exit("error: no usable compiler in the matrix")
|
|
@@ -1256,7 +1301,7 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
|
|
|
1256
1301
|
'declare the pairs extern "C" or use --pair with the '
|
|
1257
1302
|
'mangled names', file=sys.stderr)
|
|
1258
1303
|
print(f"\n== {cc_cmd} ==\n")
|
|
1259
|
-
print(file_summary_table(funcs) if funcs
|
|
1304
|
+
print(file_summary_table(funcs, table_width()) if funcs
|
|
1260
1305
|
else "(no functions found)")
|
|
1261
1306
|
continue
|
|
1262
1307
|
missing = sorted({n for p in pairs for n in p if n not in funcs})
|
|
@@ -1268,7 +1313,7 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
|
|
|
1268
1313
|
for old, new in pairs:
|
|
1269
1314
|
print(side_by_side(funcs[old], funcs[new], old, new))
|
|
1270
1315
|
print()
|
|
1271
|
-
print(summary_table(pairs, funcs))
|
|
1316
|
+
print(summary_table(pairs, funcs, table_width()))
|
|
1272
1317
|
if not ran_any:
|
|
1273
1318
|
sys.exit("error: no usable compiler in the matrix")
|
|
1274
1319
|
return 0
|
|
@@ -1316,7 +1361,7 @@ def run_inspect(source, matrix, fn_names, layout, extra_flags, tmp):
|
|
|
1316
1361
|
print()
|
|
1317
1362
|
print(listing(name, funcs[name]))
|
|
1318
1363
|
print()
|
|
1319
|
-
print(inspect_table(fn_names, funcs))
|
|
1364
|
+
print(inspect_table(fn_names, funcs, table_width()))
|
|
1320
1365
|
return 0
|
|
1321
1366
|
|
|
1322
1367
|
|
|
@@ -1379,7 +1424,7 @@ def run_elf(elf, fn_names, filter_regex, objdump):
|
|
|
1379
1424
|
print()
|
|
1380
1425
|
print(listing(name, funcs[name]))
|
|
1381
1426
|
print()
|
|
1382
|
-
print(inspect_table(selected, funcs))
|
|
1427
|
+
print(inspect_table(selected, funcs, table_width()))
|
|
1383
1428
|
return 0
|
|
1384
1429
|
|
|
1385
1430
|
|
|
@@ -1189,11 +1189,72 @@ class TestFormatCalls(unittest.TestCase):
|
|
|
1189
1189
|
def test_long_list_capped(self):
|
|
1190
1190
|
calls = [f"fn{i}" for i in range(12)]
|
|
1191
1191
|
out = asmdiff.format_calls(calls)
|
|
1192
|
-
self.assertTrue(out.endswith("
|
|
1192
|
+
self.assertTrue(out.endswith("Total Calls:12"))
|
|
1193
1193
|
self.assertIn("fn7", out)
|
|
1194
1194
|
self.assertNotIn("fn8,", out)
|
|
1195
1195
|
|
|
1196
1196
|
|
|
1197
|
+
class TestTableMaxWidth(unittest.TestCase):
|
|
1198
|
+
"""render_table(max_width=) fits rows to the terminal by trimming
|
|
1199
|
+
the last column only, whole callees at a time, replacing the tail
|
|
1200
|
+
with (or preserving) a Total Calls:N summary."""
|
|
1201
|
+
|
|
1202
|
+
def test_no_max_width_keeps_long_cells(self):
|
|
1203
|
+
rows = [("function", "calls"),
|
|
1204
|
+
("f", "alpha, bravo, charlie, delta")]
|
|
1205
|
+
out = asmdiff.render_table(rows)
|
|
1206
|
+
self.assertIn("alpha, bravo, charlie, delta", out)
|
|
1207
|
+
|
|
1208
|
+
def test_trims_to_fit_with_total_summary(self):
|
|
1209
|
+
rows = [("function", "insns", "calls"),
|
|
1210
|
+
("f", "5", "alpha, bravo, charlie, delta")]
|
|
1211
|
+
out = asmdiff.render_table(rows, max_width=37)
|
|
1212
|
+
self.assertIn("alpha, Total Calls:4", out)
|
|
1213
|
+
self.assertNotIn("bravo", out)
|
|
1214
|
+
self.assertTrue(all(len(l) <= 37 for l in out.splitlines()))
|
|
1215
|
+
|
|
1216
|
+
def test_fitting_cell_left_alone(self):
|
|
1217
|
+
rows = [("function", "calls"), ("f", "alpha, bravo")]
|
|
1218
|
+
out = asmdiff.render_table(rows, max_width=80)
|
|
1219
|
+
self.assertIn("alpha, bravo", out)
|
|
1220
|
+
self.assertNotIn("Total Calls", out)
|
|
1221
|
+
|
|
1222
|
+
def test_merges_existing_total_summary(self):
|
|
1223
|
+
rows = [("fn", "calls"), ("f", "a, b, c, Total Calls:12")]
|
|
1224
|
+
out = asmdiff.render_table(rows, max_width=21)
|
|
1225
|
+
self.assertIn("a, Total Calls:12", out)
|
|
1226
|
+
self.assertNotIn("b", out.splitlines()[1])
|
|
1227
|
+
|
|
1228
|
+
def test_single_callee_never_dropped(self):
|
|
1229
|
+
rows = [("fn", "calls"), ("f", "very_long_single_symbol_name")]
|
|
1230
|
+
out = asmdiff.render_table(rows, max_width=10)
|
|
1231
|
+
self.assertIn("very_long_single_symbol_name", out)
|
|
1232
|
+
|
|
1233
|
+
def test_dash_cell_untouched(self):
|
|
1234
|
+
rows = [("function_with_a_very_long_header", "calls"), ("f", "-")]
|
|
1235
|
+
out = asmdiff.render_table(rows, max_width=10)
|
|
1236
|
+
self.assertRegex(out.splitlines()[1], r"f\s+-")
|
|
1237
|
+
|
|
1238
|
+
def test_inspect_table_passes_max_width(self):
|
|
1239
|
+
funcs = {"f": ["call\talpha", "call\tbravo",
|
|
1240
|
+
"call\tcharlie", "call\tdelta", "ret"]}
|
|
1241
|
+
out = asmdiff.inspect_table(["f"], funcs, max_width=49)
|
|
1242
|
+
self.assertIn("alpha, Total Calls:4", out)
|
|
1243
|
+
self.assertNotIn("bravo", out)
|
|
1244
|
+
|
|
1245
|
+
def test_table_width_is_terminal_width_on_tty(self):
|
|
1246
|
+
with mock.patch.object(asmdiff.sys.stdout, "isatty",
|
|
1247
|
+
return_value=True), \
|
|
1248
|
+
mock.patch.object(asmdiff.shutil, "get_terminal_size",
|
|
1249
|
+
return_value=os.terminal_size((100, 24))):
|
|
1250
|
+
self.assertEqual(asmdiff.table_width(), 100)
|
|
1251
|
+
|
|
1252
|
+
def test_table_width_is_none_when_piped(self):
|
|
1253
|
+
with mock.patch.object(asmdiff.sys.stdout, "isatty",
|
|
1254
|
+
return_value=False):
|
|
1255
|
+
self.assertIsNone(asmdiff.table_width())
|
|
1256
|
+
|
|
1257
|
+
|
|
1197
1258
|
class TestDiscoveredCompileCommands(unittest.TestCase):
|
|
1198
1259
|
"""compile_commands = true in a target / bare --compile-commands."""
|
|
1199
1260
|
|
|
@@ -1341,15 +1402,16 @@ class TestRendering(unittest.TestCase):
|
|
|
1341
1402
|
lines = out.splitlines()
|
|
1342
1403
|
self.assertIn("function", lines[0])
|
|
1343
1404
|
self.assertIn("loop spans", lines[0])
|
|
1405
|
+
self.assertTrue(lines[0].endswith("calls"))
|
|
1344
1406
|
self.assertRegex(lines[1], r"old_c\s+baseline\s+2\s+-\s+-")
|
|
1345
|
-
self.assertRegex(lines[2], r"new_c\s+candidate\s+1\s+ldexpf
|
|
1407
|
+
self.assertRegex(lines[2], r"new_c\s+candidate\s+1\s+-\s+ldexpf")
|
|
1346
1408
|
|
|
1347
1409
|
def test_summary_table_loop_spans_column(self):
|
|
1348
1410
|
funcs = {"a": [".L2:", "addl\t$1, %eax", "jne\t.L2"],
|
|
1349
1411
|
"b": ["ret"]}
|
|
1350
1412
|
out = asmdiff.summary_table([("a", "b")], funcs)
|
|
1351
1413
|
lines = out.splitlines()
|
|
1352
|
-
self.assertRegex(lines[1], r"a\s+baseline\s+2\s
|
|
1414
|
+
self.assertRegex(lines[1], r"a\s+baseline\s+2\s+\.L2:2\s+-")
|
|
1353
1415
|
self.assertRegex(lines[2], r"b\s+candidate\s+1\s+-\s+-")
|
|
1354
1416
|
|
|
1355
1417
|
def test_file_summary_totals_and_call_union(self):
|
|
@@ -1358,10 +1420,10 @@ class TestRendering(unittest.TestCase):
|
|
|
1358
1420
|
"call\tmalloc", "call\tfree", "ret"]}
|
|
1359
1421
|
out = asmdiff.file_summary_table(funcs)
|
|
1360
1422
|
lines = out.splitlines()
|
|
1361
|
-
self.assertRegex(lines[1], r"f\s+2\s+malloc
|
|
1362
|
-
self.assertRegex(lines[2], r"g\s+5\s+malloc, free
|
|
1423
|
+
self.assertRegex(lines[1], r"f\s+2\s+-\s+malloc")
|
|
1424
|
+
self.assertRegex(lines[2], r"g\s+5\s+\.L2:2\s+malloc, free")
|
|
1363
1425
|
self.assertRegex(lines[3],
|
|
1364
|
-
r"TOTAL \(2 functions\)\s+7\s+malloc, free
|
|
1426
|
+
r"TOTAL \(2 functions\)\s+7\s+-\s+malloc, free")
|
|
1365
1427
|
|
|
1366
1428
|
|
|
1367
1429
|
class TestInspectRendering(unittest.TestCase):
|
|
@@ -1379,13 +1441,13 @@ class TestInspectRendering(unittest.TestCase):
|
|
|
1379
1441
|
lines = out.splitlines()
|
|
1380
1442
|
self.assertIn("function", lines[0])
|
|
1381
1443
|
self.assertEqual(len(lines), 2) # header + the one function
|
|
1382
|
-
self.assertRegex(lines[1], r"new_const\s+2\s+ldexpf
|
|
1444
|
+
self.assertRegex(lines[1], r"new_const\s+2\s+-\s+ldexpf")
|
|
1383
1445
|
self.assertNotIn("TOTAL", out)
|
|
1384
1446
|
|
|
1385
1447
|
def test_inspect_table_loop_spans(self):
|
|
1386
1448
|
funcs = asmdiff.extract_functions(LOOP_ASM)
|
|
1387
1449
|
out = asmdiff.inspect_table(["looper"], funcs)
|
|
1388
|
-
self.assertRegex(out.splitlines()[1], r"looper\s+5\s
|
|
1450
|
+
self.assertRegex(out.splitlines()[1], r"looper\s+5\s+\.L2:3\s+-")
|
|
1389
1451
|
|
|
1390
1452
|
|
|
1391
1453
|
class TestRunInspect(unittest.TestCase):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|