asmdiff 0.1.7__tar.gz → 0.2.0__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.2.0/2026-07-07-codegen-hotspots-handoff.md:Zone.Identifier +0 -0
- asmdiff-0.2.0/2026-07-07-table-layout-handoff.md +90 -0
- asmdiff-0.1.7/README.md → asmdiff-0.2.0/PKG-INFO +77 -6
- asmdiff-0.1.7/PKG-INFO → asmdiff-0.2.0/README.md +59 -24
- asmdiff-0.2.0/amy-hotspot-analysis.md +214 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/asmdiff.py +2 -2
- asmdiff-0.2.0/demo/esp32-matrix.gif +0 -0
- asmdiff-0.2.0/demo/quickstart.gif +0 -0
- asmdiff-0.2.0/docs/superpowers/plans/2026-07-08-asmdiff-demo-gifs.md +277 -0
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-design.md +147 -0
- asmdiff-0.2.0/docs/superpowers/specs/2026-07-08-asmdiff-demo-gifs-pipeline.mmd +16 -0
- asmdiff-0.2.0/gif-plan.md +248 -0
- asmdiff-0.2.0/how-could-i-create-velvety-hollerith.md:Zone.Identifier +3 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/pyproject.toml +2 -2
- {asmdiff-0.1.7 → asmdiff-0.2.0}/test_asmdiff.py +40 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/.claude/settings.local.json +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/.gitignore +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/2026-07-06-inspect-mode.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/2026-07-07-elf-mode-handoff.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/CLAUDE.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/FIELD-TEST-REPORT.md +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/LICENSE +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/Screenshot 2026-07-07 225145.jpg +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/asmdiff.example.toml +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/asmdiff_example.c +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/.gitignore +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.1-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.1.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.2-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.2.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.3-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.3.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.4-py3-none-any.whl +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/dist_1/asmdiff-0.1.4.tar.gz +0 -0
- {asmdiff-0.1.7 → asmdiff-0.2.0}/docs/superpowers/specs/2026-07-07-edit-config-design.md +0 -0
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# asmdiff summary-table legibility - checkpoint 2026-07-07
|
|
2
|
+
|
|
3
|
+
Handoff for a fresh session in THIS repo (`~/esp-idf/tools/asmdiff`).
|
|
4
|
+
Feature is COMPLETE and verified, NOTHING COMMITTED. Origin: ELF-mode
|
|
5
|
+
output (`asmdiff build/S3-Amysynth.elf --filter 'core_'`) was unreadable -
|
|
6
|
+
every row hard-wrapped in the terminal (screenshot in repo root,
|
|
7
|
+
`Screenshot 2026-07-07 225145.jpg`).
|
|
8
|
+
|
|
9
|
+
## Root cause
|
|
10
|
+
|
|
11
|
+
`render_table` pads every column to its widest cell. The `calls` column
|
|
12
|
+
sat in the middle; with real firmware call lists (~150 chars at the
|
|
13
|
+
8-callee cap) plus 50-char LTO symbol names, `loop spans` started past
|
|
14
|
+
column ~210 and every row wrapped: dangling `-` lines, split headers,
|
|
15
|
+
call lists colliding with the next function name.
|
|
16
|
+
|
|
17
|
+
## What changed (asmdiff.py)
|
|
18
|
+
|
|
19
|
+
1. **Column order**: all three builders now emit
|
|
20
|
+
`function / [role] / insns / loop spans / calls` - calls (the one
|
|
21
|
+
unbounded column) moved last, ragged right. Builders:
|
|
22
|
+
`summary_table`, `file_summary_table` (TOTAL row spans cell stays
|
|
23
|
+
`-`), `inspect_table`. Each gained a `max_width=None` passthrough
|
|
24
|
+
param.
|
|
25
|
+
2. **Width-aware trim**: `render_table(rows, max_width=None)` - when
|
|
26
|
+
set, fixed columns keep full width, the last column's over-budget
|
|
27
|
+
cells are trimmed by `_fit_calls`: whole callees dropped from the
|
|
28
|
+
end, closed with `Total Calls:N` (N = full callee count, preserved
|
|
29
|
+
from an existing summary). The FIRST callee is never dropped - a
|
|
30
|
+
single huge symbol overflows/wraps instead of vanishing
|
|
31
|
+
(`sequencer_core_set_melodic_envelope2` is the real-world case).
|
|
32
|
+
Cells that fit, `-`, and headers are untouched. `TOTAL_CALLS_RE`
|
|
33
|
+
guards summary parsing.
|
|
34
|
+
3. **`format_calls`** cap suffix changed from `+N more` to
|
|
35
|
+
`Total Calls:N` (user-requested format; one consistent suffix for
|
|
36
|
+
both the 8-callee cap and the width trim).
|
|
37
|
+
4. **`table_width()`**: `shutil.get_terminal_size().columns` when
|
|
38
|
+
`sys.stdout.isatty()`, else `None` - piped/redirected output skips
|
|
39
|
+
the trim so it stays deterministic and greppable. Passed at all six
|
|
40
|
+
print sites (both summary_table sites, both file_summary_table
|
|
41
|
+
sites, both inspect_table sites).
|
|
42
|
+
|
|
43
|
+
All new code sits together around `render_table` (~line 1053 region).
|
|
44
|
+
|
|
45
|
+
## Tests (test_asmdiff.py)
|
|
46
|
+
|
|
47
|
+
- New `TestTableMaxWidth` (9 tests): trim-to-fit, fitting cell left
|
|
48
|
+
alone, merges existing `Total Calls:N` (does not recount), single
|
|
49
|
+
callee never dropped, dash untouched, `max_width=None` unchanged,
|
|
50
|
+
builder passthrough, `table_width()` tty/piped (via `mock.patch` on
|
|
51
|
+
`asmdiff.sys.stdout.isatty` / `asmdiff.shutil.get_terminal_size`).
|
|
52
|
+
- Updated for new column order / suffix: `TestFormatCalls`
|
|
53
|
+
`test_long_list_capped`, `test_summary_table`,
|
|
54
|
+
`test_summary_table_loop_spans_column`,
|
|
55
|
+
`test_file_summary_totals_and_call_union`, both
|
|
56
|
+
`TestInspectRendering` inspect_table tests.
|
|
57
|
+
- Suite: 191 tests, sole failure the KNOWN env-dependent
|
|
58
|
+
`TestShortAliases.test_p_is_pair` (global ~/.config/asmdiff.toml with
|
|
59
|
+
target esp32s3 short-circuits it; fails at HEAD too).
|
|
60
|
+
|
|
61
|
+
## Docs
|
|
62
|
+
|
|
63
|
+
README: all 8 example tables reordered; the truncation paragraph (ELF
|
|
64
|
+
section, "Only labels the assembler types...") now documents
|
|
65
|
+
`Total Calls:N`, the tty-only width trim, and the piped-output
|
|
66
|
+
exemption; intro prose order updated. Module docstring summary line
|
|
67
|
+
reordered to match. No flags or config keys changed, so
|
|
68
|
+
`asmdiff.example.toml` / `EXAMPLE_CONFIG` are untouched (the
|
|
69
|
+
byte-match unit test still applies to any future config edit).
|
|
70
|
+
|
|
71
|
+
## Verification done
|
|
72
|
+
|
|
73
|
+
- Full suite as above.
|
|
74
|
+
- Real ELF (`~/esp-idf/S3-Amysynth/build/S3-Amysynth.elf`, config-derived
|
|
75
|
+
xtensa objdump), `--filter 'core_'`:
|
|
76
|
+
- piped: full capped lists, new column order, aligned.
|
|
77
|
+
- fake tty via `script -qec "stty cols 190 ..."`: all rows one line
|
|
78
|
+
except the designed first-callee-floor overflow (4 chars over).
|
|
79
|
+
- 110-col tty: fixed columns alone can exceed width when a loopy
|
|
80
|
+
function widens `loop spans`; calls floor at one callee - accepted
|
|
81
|
+
graceful degradation, only `calls` ever trims (scope decision).
|
|
82
|
+
|
|
83
|
+
## Next steps
|
|
84
|
+
|
|
85
|
+
1. Commit (user has not asked yet; suggested scope: asmdiff.py,
|
|
86
|
+
test_asmdiff.py, README.md in one commit - pure output-format
|
|
87
|
+
change, no behavior change to analysis).
|
|
88
|
+
2. Optional follow-up the user explicitly deferred: making the
|
|
89
|
+
`loop spans` cells themselves friendlier (e.g. "2 ZOL + 3 br"
|
|
90
|
+
summary) - scoped OUT of this change ("layout only").
|
|
@@ -1,7 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asmdiff
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
|
|
5
|
+
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
|
+
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
7
|
+
Author: Rasmus Tikkanen
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
15
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
1
19
|
# [asmdiff](https://pypi.org/project/asmdiff/)
|
|
2
|
-
## per-function assembly comparison
|
|
20
|
+
## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
|
|
21
|
+
|
|
22
|
+
> asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for
|
|
23
|
+
investigating compiler code generation rather than benchmarking runtime performance.
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
|
|
27
|
+
<summary>Quickstart Example</summary>
|
|
28
|
+
|
|
29
|
+
[](demo/quickstart.gif)
|
|
30
|
+
|
|
31
|
+
*Each function appears twice - `old_*` and `new_*`, the same routine
|
|
32
|
+
before and after a rewrite - compiled across a gcc/clang matrix and diffed
|
|
33
|
+
side by side. The summary's `insns` and `calls` columns are the payoff:
|
|
34
|
+
`old_rt`'s 9-instruction `exp2f` call collapses to `new_rt`'s 2-instruction
|
|
35
|
+
`ldexpf`. (Click to enlarge.)*
|
|
3
36
|
|
|
4
|
-
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
</details>
|
|
5
40
|
|
|
6
41
|
### Try it yourself:
|
|
7
42
|
|
|
@@ -255,9 +290,10 @@ everywhere else; two things are ELF-specific:
|
|
|
255
290
|
evidence is missing or the register is overwritten in between -
|
|
256
291
|
a genuine function-pointer dispatch still reads as indirect.
|
|
257
292
|
|
|
258
|
-
What this looks like on real firmware:
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
What this looks like on real firmware: this binary's sequencer path -
|
|
294
|
+
here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
|
|
295
|
+
with the divide sourced from `amy_sysclock()` in `api.c`) - surfaces
|
|
296
|
+
three findings in one row scan.
|
|
261
297
|
|
|
262
298
|
```
|
|
263
299
|
$ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
|
|
@@ -271,7 +307,12 @@ sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaph
|
|
|
271
307
|
A timer callback pays a software float divide *and* a 64-bit
|
|
272
308
|
`__udivdi3` inside its 41-instruction loop - the
|
|
273
309
|
[silent software divide](#example-catching-a-silent-software-divide)
|
|
274
|
-
pattern, caught in shipped firmware instead of a harness.
|
|
310
|
+
pattern, caught in shipped firmware instead of a harness. asmdiff was
|
|
311
|
+
built to hunt exactly this in AMY, and this callback was the payoff: the
|
|
312
|
+
pairing was confirmed as both a codegen cost and an upstream correctness
|
|
313
|
+
bug - `amy_sysclock()`'s `uint32_t` sample count wraps far sooner than
|
|
314
|
+
its comment claims and loses precision on long uptimes - and is fixed
|
|
315
|
+
locally with a PR pending to shorepine/amy.
|
|
275
316
|
`sequencer_recompute`'s `__extendsfdf2 -> __divdf3 -> __muldf3` chain
|
|
276
317
|
is the double-promotion smell (typically an unsuffixed `60.0`-style
|
|
277
318
|
literal) on a chip whose FPU is single-precision only. And the
|
|
@@ -535,6 +576,35 @@ Details that make it robust:
|
|
|
535
576
|
|
|
536
577
|
### Auto-discovering the database
|
|
537
578
|
|
|
579
|
+
<details>
|
|
580
|
+
|
|
581
|
+
<summary>Real Project Example</summary>
|
|
582
|
+
|
|
583
|
+
#### All Esp32 Xtensa toolchains in one call from the same compile_commands
|
|
584
|
+
|
|
585
|
+
Below, `-db` (short for `--compile-commands`) does this on a real ESP-IDF
|
|
586
|
+
synth firmware: one `synth_core` effects source, compiled across the
|
|
587
|
+
esp32 / esp32s2 / esp32s3 cross-toolchains in a single run, with each
|
|
588
|
+
chip's include/define flags auto-borrowed from `build/compile_commands.json`
|
|
589
|
+
so the component actually compiles. No `--target` and no path are given -
|
|
590
|
+
the config's `default` names the three chips and `-db` walks up to find the
|
|
591
|
+
database. Reading it left to right: the three `==` banners are the three
|
|
592
|
+
toolchains; each table is that chip's per-function `insns` / loop spans /
|
|
593
|
+
`calls`. The tell is in the `calls` column - the esp32-S2 (the one variant
|
|
594
|
+
with no hardware FPU) picks up softfloat conversion libcalls
|
|
595
|
+
`__floatsisf`, `__floatunsisf`, `__ltsf2` that its FPU-equipped siblings
|
|
596
|
+
never emit, so the same source is measurably heavier there:
|
|
597
|
+
|
|
598
|
+
[](demo/esp32-matrix.gif)
|
|
599
|
+
|
|
600
|
+
*One source, three real chip toolchains, build flags borrowed
|
|
601
|
+
automatically - the kind of comparison that otherwise needs three separate
|
|
602
|
+
build trees. (Click to enlarge.)*
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
</details>
|
|
607
|
+
|
|
538
608
|
When you run asmdiff from inside the project anyway, the path is
|
|
539
609
|
redundant. Two opt-ins skip it:
|
|
540
610
|
|
|
@@ -550,6 +620,7 @@ asmdiff biquad.c --compile-commands # same, for any matrix
|
|
|
550
620
|
asmdiff biquad.c --compile-commands path/to/compile_commands.json
|
|
551
621
|
```
|
|
552
622
|
|
|
623
|
+
|
|
553
624
|
Both walk up from the current directory, checking each level for
|
|
554
625
|
`compile_commands.json` and then `build/compile_commands.json` (where
|
|
555
626
|
CMake and idf.py leave it) — first hit wins. The walk stops at the
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: asmdiff
|
|
3
|
-
Version: 0.1.7
|
|
4
|
-
Summary: Compare per-function assembly between paired C implementations
|
|
5
|
-
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
|
-
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
7
|
-
Author: Rasmus Tikkanen
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,objdump,riscv,static-analysis,xtensa
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Topic :: Software Development :: Compilers
|
|
15
|
-
Classifier: Topic :: Software Development :: Debuggers
|
|
16
|
-
Requires-Python: >=3.8
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
1
|
# [asmdiff](https://pypi.org/project/asmdiff/)
|
|
20
|
-
## per-function assembly comparison
|
|
2
|
+
## per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF
|
|
3
|
+
|
|
4
|
+
> asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for
|
|
5
|
+
investigating compiler code generation rather than benchmarking runtime performance.
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
|
|
9
|
+
<summary>Quickstart Example</summary>
|
|
10
|
+
|
|
11
|
+
[](demo/quickstart.gif)
|
|
12
|
+
|
|
13
|
+
*Each function appears twice - `old_*` and `new_*`, the same routine
|
|
14
|
+
before and after a rewrite - compiled across a gcc/clang matrix and diffed
|
|
15
|
+
side by side. The summary's `insns` and `calls` columns are the payoff:
|
|
16
|
+
`old_rt`'s 9-instruction `exp2f` call collapses to `new_rt`'s 2-instruction
|
|
17
|
+
`ldexpf`. (Click to enlarge.)*
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
</details>
|
|
23
22
|
|
|
24
23
|
### Try it yourself:
|
|
25
24
|
|
|
@@ -273,9 +272,10 @@ everywhere else; two things are ELF-specific:
|
|
|
273
272
|
evidence is missing or the register is overwritten in between -
|
|
274
273
|
a genuine function-pointer dispatch still reads as indirect.
|
|
275
274
|
|
|
276
|
-
What this looks like on real firmware:
|
|
277
|
-
|
|
278
|
-
|
|
275
|
+
What this looks like on real firmware: this binary's sequencer path -
|
|
276
|
+
here in the vendored AMY synth engine (`components/amy/src/sequencer.c`,
|
|
277
|
+
with the divide sourced from `amy_sysclock()` in `api.c`) - surfaces
|
|
278
|
+
three findings in one row scan.
|
|
279
279
|
|
|
280
280
|
```
|
|
281
281
|
$ asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback)'
|
|
@@ -289,7 +289,12 @@ sequencer_process_tick$lto_priv$0 117 .L2e:96 .L48:85 .L8d:6 xQueueSemaph
|
|
|
289
289
|
A timer callback pays a software float divide *and* a 64-bit
|
|
290
290
|
`__udivdi3` inside its 41-instruction loop - the
|
|
291
291
|
[silent software divide](#example-catching-a-silent-software-divide)
|
|
292
|
-
pattern, caught in shipped firmware instead of a harness.
|
|
292
|
+
pattern, caught in shipped firmware instead of a harness. asmdiff was
|
|
293
|
+
built to hunt exactly this in AMY, and this callback was the payoff: the
|
|
294
|
+
pairing was confirmed as both a codegen cost and an upstream correctness
|
|
295
|
+
bug - `amy_sysclock()`'s `uint32_t` sample count wraps far sooner than
|
|
296
|
+
its comment claims and loses precision on long uptimes - and is fixed
|
|
297
|
+
locally with a PR pending to shorepine/amy.
|
|
293
298
|
`sequencer_recompute`'s `__extendsfdf2 -> __divdf3 -> __muldf3` chain
|
|
294
299
|
is the double-promotion smell (typically an unsuffixed `60.0`-style
|
|
295
300
|
literal) on a chip whose FPU is single-precision only. And the
|
|
@@ -553,6 +558,35 @@ Details that make it robust:
|
|
|
553
558
|
|
|
554
559
|
### Auto-discovering the database
|
|
555
560
|
|
|
561
|
+
<details>
|
|
562
|
+
|
|
563
|
+
<summary>Real Project Example</summary>
|
|
564
|
+
|
|
565
|
+
#### All Esp32 Xtensa toolchains in one call from the same compile_commands
|
|
566
|
+
|
|
567
|
+
Below, `-db` (short for `--compile-commands`) does this on a real ESP-IDF
|
|
568
|
+
synth firmware: one `synth_core` effects source, compiled across the
|
|
569
|
+
esp32 / esp32s2 / esp32s3 cross-toolchains in a single run, with each
|
|
570
|
+
chip's include/define flags auto-borrowed from `build/compile_commands.json`
|
|
571
|
+
so the component actually compiles. No `--target` and no path are given -
|
|
572
|
+
the config's `default` names the three chips and `-db` walks up to find the
|
|
573
|
+
database. Reading it left to right: the three `==` banners are the three
|
|
574
|
+
toolchains; each table is that chip's per-function `insns` / loop spans /
|
|
575
|
+
`calls`. The tell is in the `calls` column - the esp32-S2 (the one variant
|
|
576
|
+
with no hardware FPU) picks up softfloat conversion libcalls
|
|
577
|
+
`__floatsisf`, `__floatunsisf`, `__ltsf2` that its FPU-equipped siblings
|
|
578
|
+
never emit, so the same source is measurably heavier there:
|
|
579
|
+
|
|
580
|
+
[](demo/esp32-matrix.gif)
|
|
581
|
+
|
|
582
|
+
*One source, three real chip toolchains, build flags borrowed
|
|
583
|
+
automatically - the kind of comparison that otherwise needs three separate
|
|
584
|
+
build trees. (Click to enlarge.)*
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
</details>
|
|
589
|
+
|
|
556
590
|
When you run asmdiff from inside the project anyway, the path is
|
|
557
591
|
redundant. Two opt-ins skip it:
|
|
558
592
|
|
|
@@ -568,6 +602,7 @@ asmdiff biquad.c --compile-commands # same, for any matrix
|
|
|
568
602
|
asmdiff biquad.c --compile-commands path/to/compile_commands.json
|
|
569
603
|
```
|
|
570
604
|
|
|
605
|
+
|
|
571
606
|
Both walk up from the current directory, checking each level for
|
|
572
607
|
`compile_commands.json` and then `build/compile_commands.json` (where
|
|
573
608
|
CMake and idf.py leave it) — first hit wins. The walk stops at the
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# S3-Amysynth codegen hotspots - checkpoint 2026-07-07
|
|
2
|
+
|
|
3
|
+
Handoff for a fresh session in THIS repo (`~/esp-idf/S3-Amysynth`). Origin:
|
|
4
|
+
field-testing asmdiff's new ELF input mode (built in `~/esp-idf/tools/asmdiff`,
|
|
5
|
+
its own handoff: `2026-07-07-elf-mode-handoff.md` there) against
|
|
6
|
+
`build/S3-Amysynth.elf` surfaced likely codegen problems in NON-amy code
|
|
7
|
+
(synth_core). All evidence is disassembly-level; no source has been read and
|
|
8
|
+
no code changed. The amy-owned side is already tracked in
|
|
9
|
+
`docs/zol-audit-2026-07-07.md` and is out of scope here.
|
|
10
|
+
|
|
11
|
+
## Evidence base
|
|
12
|
+
|
|
13
|
+
- ELF: `build/S3-Amysynth.elf` built 2026-07-07 ~16:32, esp-15.2.0
|
|
14
|
+
xtensa-esp32s3-elf, -O2 + LTO. A rebuild invalidates the exact
|
|
15
|
+
`.L<off>` labels quoted below; function-level facts should survive.
|
|
16
|
+
- Tool: `asmdiff build/S3-Amysynth.elf FUNC...` or `--filter REGEX`,
|
|
17
|
+
run from this repo (objdump is auto-derived from the esp32s3 target
|
|
18
|
+
in `~/.config/asmdiff.toml`; `python3 ~/esp-idf/tools/asmdiff/asmdiff.py`
|
|
19
|
+
if the package isn't installed). Longcalls are resolved to real
|
|
20
|
+
callees; a bare `aN` in the calls column is genuine function-pointer
|
|
21
|
+
dispatch. LTO renames survivors (`$lto_priv$0`, `$constprop$0`,
|
|
22
|
+
`$isra$0`), so sweep with `--filter`, don't guess names.
|
|
23
|
+
|
|
24
|
+
## Findings, ordered by suspected payoff
|
|
25
|
+
|
|
26
|
+
1. **`sequencer_timer_callback$lto_priv$0`** - 77 insns, calls
|
|
27
|
+
`__divsf3` AND `__udivdi3`, and its single loop span is `.L21:41`,
|
|
28
|
+
i.e. most of the function repeats. A software float divide plus a
|
|
29
|
+
64-bit integer divide in a timer callback is the exact pattern the
|
|
30
|
+
asmdiff README's "silent software divide" example warns about.
|
|
31
|
+
Hypothesis: per-callback recomputation of a period/rate that only
|
|
32
|
+
changes on tempo edits. Fix shape: precompute at tempo-change time,
|
|
33
|
+
or narrow to 32-bit / reciprocal-multiply math.
|
|
34
|
+
|
|
35
|
+
2. **`sequencer_recompute`** - 46 insns, call chain `__extendsfdf2 ->
|
|
36
|
+
__divdf3 -> __muldf3 -> __fixunsdfsi` plus `__divsf3`, no loops.
|
|
37
|
+
That chain is the double-promotion smell: the S3 FPU is
|
|
38
|
+
single-precision only, so an unsuffixed literal (`60.0`, `1000.0`)
|
|
39
|
+
or a non-f math call promotes the whole expression to softdouble.
|
|
40
|
+
Fix is usually mechanical (f-suffix literals, float-only helpers).
|
|
41
|
+
Likely called only on tempo change - cheap to fix, but confirm call
|
|
42
|
+
rate before counting it as a win.
|
|
43
|
+
|
|
44
|
+
3. **`arp_rebuild$lto_priv$0`** - 362 insns, spans `.L29:319` /
|
|
45
|
+
`.L77:318` containing `__divsf3` and queue sends. Config-path code
|
|
46
|
+
(runs on patch/settings changes), so probably acceptable - look
|
|
47
|
+
only if arp edits audibly stutter.
|
|
48
|
+
|
|
49
|
+
4. **`sequencer_process_tick$lto_priv$0`** - 117 insns, loops
|
|
50
|
+
`.L2e:96` / `.L48:85` containing `xQueueSemaphoreTake` /
|
|
51
|
+
`xQueueGenericSend` and one indirect call (`a8`). Per-tick queue
|
|
52
|
+
traffic is an architecture fact, not a bug; noted as the place to
|
|
53
|
+
look during any future tick-latency work.
|
|
54
|
+
|
|
55
|
+
Checked and healthy (no action): `quantizer_snap_midi_note` (51 insns,
|
|
56
|
+
call-free, divide-by-12 folded to mulsh, inner 12-insn scan shipped as
|
|
57
|
+
a ZOL nested in a branch loop over octaves), `arp_core_refresh` /
|
|
58
|
+
`arp_collect_up$lto_priv$0` (call-free collect loops, one ZOL),
|
|
59
|
+
`render_clock_on_alarm$lto_priv$0` (13 insns, just the ISR notify),
|
|
60
|
+
the `arp_set_*` setters (all tiny). Fully inlined by LTO, confirming
|
|
61
|
+
the audit's picture: `arp_collect_down`, `arp_snap`, `arp_sort_asc`
|
|
62
|
+
(bodies live inside `arp_core_refresh`).
|
|
63
|
+
|
|
64
|
+
## Domain facts to reuse
|
|
65
|
+
|
|
66
|
+
- ZOL kill rules (verified on esp-15.2.0 -O2, now documented in the
|
|
67
|
+
asmdiff README's ELF section): one surviving call anywhere in the
|
|
68
|
+
loop body kills ZOL even behind a never-taken condition; early
|
|
69
|
+
`break` kills it; a spilled loop counter kills it; nested loops get
|
|
70
|
+
ZOL on the innermost only.
|
|
71
|
+
- asmdiff's branch-span counts include unconditional backward `j`
|
|
72
|
+
latches, so they read higher than the July audit's awk (which
|
|
73
|
+
counted conditional backedges only). ZOL counts are identical.
|
|
74
|
+
- objdump quirk seen once: a function-tail `retw.n` misframed with a
|
|
75
|
+
padding byte makes a few branches print as `sym+0xNN` instead of a
|
|
76
|
+
synthesized label (`quantizer_snap_midi_note+0x88`). Cosmetic;
|
|
77
|
+
spans and call counts are unaffected.
|
|
78
|
+
|
|
79
|
+
## Hypotheses
|
|
80
|
+
|
|
81
|
+
Stated inline per finding above; none tested against source yet. The
|
|
82
|
+
whole list is one build old - re-run the sweeps before acting:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
asmdiff build/S3-Amysynth.elf --filter 'sequencer_(process_tick|recompute|timer_callback|add_event)'
|
|
86
|
+
asmdiff build/S3-Amysynth.elf --filter '^arp_'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Next steps
|
|
90
|
+
|
|
91
|
+
1. Read the timer-callback and recompute source under
|
|
92
|
+
`components/synth_core/sequencer_core/`; identify the divides'
|
|
93
|
+
operands and the callback rate.
|
|
94
|
+
2. Apply the fix shapes from findings 1-2.
|
|
95
|
+
3. Iterate on the changed TU with compile-mode asmdiff (borrow real
|
|
96
|
+
flags: `asmdiff FILE.c FUNC --target esp32s3 -db
|
|
97
|
+
build/compile_commands.json`) until the libcalls are gone from the
|
|
98
|
+
changed functions.
|
|
99
|
+
4. `idf.py build`, re-run the ELF sweeps, confirm the calls column is
|
|
100
|
+
clean and the loops that matter still (or now) ship as ZOL.
|
|
101
|
+
5. Only then measure on target - instruction counts are not cycles;
|
|
102
|
+
the win to verify is tick jitter / CPU headroom.
|
|
103
|
+
|
|
104
|
+
## Hardware-verify pending
|
|
105
|
+
|
|
106
|
+
Timing claims (callback cost, tick jitter) need on-target measurement;
|
|
107
|
+
every codegen claim above is settled by the ELF alone.
|
|
108
|
+
|
|
109
|
+
## Source evaluation 2026-07-07 (second session)
|
|
110
|
+
|
|
111
|
+
Sweeps re-run against the current ELF; all four functions unchanged
|
|
112
|
+
(same insn counts and libcall lists). Hypotheses checked against source.
|
|
113
|
+
|
|
114
|
+
**Correction to the origin note:** findings 1 and 2 are NOT synth_core.
|
|
115
|
+
`sequencer_timer_callback` / `sequencer_check_and_fill` /
|
|
116
|
+
`sequencer_recompute` live in vendored AMY (`components/amy/src/sequencer.c`),
|
|
117
|
+
and the float divide inside them comes from `amy_sysclock()`
|
|
118
|
+
(`components/amy/src/api.c:214`). Any fix is an AMY edit -> classify per
|
|
119
|
+
the upstream bug policy. Only `arp_rebuild` (finding 3) is synth_core.
|
|
120
|
+
|
|
121
|
+
### Finding 1 - CONFIRMED, and upgraded to a correctness bug
|
|
122
|
+
|
|
123
|
+
The callback (2 kHz on the Core-0 esp_timer task) inlines
|
|
124
|
+
`sequencer_check_and_fill()`:
|
|
125
|
+
|
|
126
|
+
- `__udivdi3`: `amy_global.next_amy_tick_us / 1000L` in the while
|
|
127
|
+
condition (`sequencer.c:280`). GCC cannot strength-reduce a 64-bit
|
|
128
|
+
divide by constant on a 32-bit target; it is a real libcall per
|
|
129
|
+
loop-condition check.
|
|
130
|
+
- `__divsf3`: inlined `amy_sysclock()` =
|
|
131
|
+
`(uint32_t)((total_blocks * AMY_BLOCK_SIZE / (float)AMY_SAMPLE_RATE) * 1000)`
|
|
132
|
+
- called twice per callback (line 275 and the while condition), each
|
|
133
|
+
with u32->float and float->u32 conversion traffic around the divide.
|
|
134
|
+
|
|
135
|
+
Beyond cost, `amy_sysclock()` has two upstream correctness bugs:
|
|
136
|
+
|
|
137
|
+
1. **Wraps at 24.85 h, not the documented 49.7 days.** `total_blocks`
|
|
138
|
+
is `uint32_t` and `AMY_BLOCK_SIZE` is int, so `total_blocks * 256`
|
|
139
|
+
wraps in u32 at 2^32 samples = 89478 s. The "rollover is 49.7 days"
|
|
140
|
+
comment assumes ms-domain wrap; the samples-domain multiply wraps
|
|
141
|
+
~48x sooner.
|
|
142
|
+
2. **Float precision decay.** The u32 sample count passes through
|
|
143
|
+
float (24-bit mantissa). Past 2^24 samples (~6 min) the clock
|
|
144
|
+
quantizes; by ~12 h uptime the quantum is ~128 samples (~2.7 ms) -
|
|
145
|
+
against an 11.57 ms tick at 108 BPM the sequencer the whole synth
|
|
146
|
+
slaves to gets visibly lumpy on long uptimes.
|
|
147
|
+
|
|
148
|
+
Fix shape (both cost and correctness):
|
|
149
|
+
- `amy_sysclock()` -> integer math:
|
|
150
|
+
`(uint32_t)(((uint64_t)total_blocks * (AMY_BLOCK_SIZE * 1000u)) / AMY_SAMPLE_RATE)`
|
|
151
|
+
- exact, monotonic, true 49.7-day ms rollover. Generic form keeps one
|
|
152
|
+
`__udivdi3` per call; for this target the 48 kHz / 256-block case
|
|
153
|
+
reduces exactly to `total_blocks * 5 + total_blocks / 3` (u32
|
|
154
|
+
divide-by-constant -> hw `quou` / reciprocal-mul, call-free).
|
|
155
|
+
- `sequencer_check_and_fill()` -> compare in the us domain: compute
|
|
156
|
+
`now_us` once, `while (now_us >= amy_global.next_amy_tick_us)`.
|
|
157
|
+
Kills the per-iteration `__udivdi3` AND the second sysclock call.
|
|
158
|
+
Semantic delta: the loop no longer re-samples the clock mid-catch-up;
|
|
159
|
+
the existing 1 s skip-ahead guard already bounds that path.
|
|
160
|
+
|
|
161
|
+
CPU impact honest estimate: ~150-400 cycles/callback saved at 2 kHz =
|
|
162
|
+
roughly 0.1-0.35% of Core 0 - small. The real wins are (a) less
|
|
163
|
+
occupancy/jitter on the shared esp_timer dispatch task (same rationale
|
|
164
|
+
as moving button dispatch off it) and (b) the long-uptime clock
|
|
165
|
+
stability, which matters for a box left droning overnight.
|
|
166
|
+
|
|
167
|
+
Classification: universal upstream bugs (hit every 32-bit port incl.
|
|
168
|
+
Pico) -> PR candidates for shorepine/amy; do as LOCAL EDIT first per
|
|
169
|
+
established pattern, log in AMY-EDITS.md.
|
|
170
|
+
|
|
171
|
+
### Finding 2 - CONFIRMED, trivial
|
|
172
|
+
|
|
173
|
+
`sequencer_recompute()` (`sequencer.c:141`): `1000000.0` and `60.0`
|
|
174
|
+
double literals promote the whole tempo expression to softdouble;
|
|
175
|
+
`__divsf3` in its calls column is the inlined `amy_sysclock()` on the
|
|
176
|
+
next line. Called only on tempo change / init - CPU impact ~zero.
|
|
177
|
+
Fix: f-suffix the two literals. Bundle into the same upstream PR.
|
|
178
|
+
|
|
179
|
+
### Finding 3 - no action (hypothesis confirmed)
|
|
180
|
+
|
|
181
|
+
`arp_rebuild` divide is `s_arp.lfo.depth / 100.0f`
|
|
182
|
+
(`components/synth_core/arp_core.c:238`), config-path only, runs on
|
|
183
|
+
settings edits. Leave alone.
|
|
184
|
+
|
|
185
|
+
### Finding 4 - no action (architecture, as suspected)
|
|
186
|
+
|
|
187
|
+
`sequencer_process_tick` per-tick mutex + queue sends are the LOCAL
|
|
188
|
+
EDIT seq-lock design; the `%` modulo at `sequencer.c:157` compiles to
|
|
189
|
+
the S3 hardware divide (no libcall in the sweep). Nothing to fix;
|
|
190
|
+
remains the marker for future tick-latency work.
|
|
191
|
+
|
|
192
|
+
### Recommended order
|
|
193
|
+
|
|
194
|
+
1. LOCAL EDIT: integer `amy_sysclock()` + us-domain compare in
|
|
195
|
+
`sequencer_check_and_fill()` + f-suffixes in `sequencer_recompute()`.
|
|
196
|
+
2. Re-run the ELF sweep: callback calls column should be empty (or
|
|
197
|
+
`quou`-only); recompute column should drop all softdouble calls.
|
|
198
|
+
3. HW verify per policy (tick jitter / esp_timer task headroom), then
|
|
199
|
+
draft the upstream PR (correctness framing: rollover + precision;
|
|
200
|
+
the codegen win is a bonus).
|
|
201
|
+
|
|
202
|
+
### Status 2026-07-07 (same second session): steps 1-2 DONE
|
|
203
|
+
|
|
204
|
+
Edits applied to `components/amy/src/api.c` + `sequencer.c` (marked
|
|
205
|
+
LOCAL EDIT, logged in AMY-EDITS.md), `build_project` green, host-side
|
|
206
|
+
math check green (new == 128-bit reference, 0 mismatches; old version
|
|
207
|
+
demonstrated wrapping at 24.85 h and stepping 8 ms at 12 h uptime).
|
|
208
|
+
Post-fix sweep: callback 77 -> 53 insns, calls `__divsf3, __udivdi3`
|
|
209
|
+
-> one `__udivdi3` (inlined sysclock's u64 divide-by-constant);
|
|
210
|
+
`sequencer_recompute` and `amy_sysclock` fully inlined away (no
|
|
211
|
+
softdouble possible - source is float/integer only now). PR draft:
|
|
212
|
+
`docs/pr-draft-sysclock-integer-clock.md` + verification program
|
|
213
|
+
`docs/pr-draft-sysclock-check.c`. Remaining: HW soak (overnight uptime
|
|
214
|
+
+ tick jitter), then take the draft to the amy fork.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Per-function assembly inspection and comparison, across a compiler matrix or from a shipped ELF.
|
|
3
3
|
|
|
4
|
-
Compiles a
|
|
4
|
+
Compiles a C source file across a matrix of compilers, extracts each
|
|
5
5
|
variant function's assembly from the -S output, and prints side-by-side
|
|
6
6
|
listings plus a summary of instruction counts, loop spans (instructions
|
|
7
7
|
between a local label and its last backward branch), and outbound calls.
|
|
Binary file
|
|
Binary file
|