From f0a1ad0133da9fc0fae995e4d1c88cee5df0e35b Mon Sep 17 00:00:00 2001 From: zackees Date: Sun, 9 Aug 2026 21:39:51 -0700 Subject: [PATCH 1/3] fix(esp32): use SysV size format to separate flash .rodata from DRAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1261 — ESP32-C6 RAM reported as 1.88MB / 320KB (602.5%). The Berkeley size format lumps .flash.rodata (flash-resident) into the data column alongside .dram0.data (RAM-resident). For ESP32-C6 this inflates the RAM figure by counting flash-rodata as if it were RAM. Switch to size -A (SysV format) which lists each section with its name and address. parse_esp32_size_output classifies by section prefix: - Flash: .flash.*, .rodata*, .iram0.*, .text - RAM: .dram0.*, .dram.* Falls back to None when no ESP32-prefixed sections are detected, so non-ESP32 targets sharing this path get the standard Berkeley parser. Co-Authored-By: Claude --- .../src/esp32/esp32_linker.rs | 178 +++++++++++++++++- 1 file changed, 176 insertions(+), 2 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs index 79cd67d2..e911bf5a 100644 --- a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs +++ b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs @@ -556,12 +556,11 @@ impl Linker for Esp32Linker { return Ok(size_info); } - let size_info = crate::linker::LinkerBase::report_size( + let size_info = esp32_report_size( &self.size_path, elf_path, self.max_flash, self.max_ram, - "size", ) .await?; self.save_size_cache(elf_path, &size_info); @@ -569,6 +568,132 @@ impl Linker for Esp32Linker { } } +/// Run `size -A` (SysV format) for ESP32 targets instead of the default +/// Berkeley format. SysV lists every section individually so flash-resident +/// sections (`.flash.*`, `.rodata`) stay out of the RAM total. +/// +/// The Berkeley format lumps `.flash.rodata` into the `data` column +/// alongside `.dram0.data`, inflating the RAM figure — for ESP32-C6 +/// this can report 602% RAM usage (FastLED/fbuild#1261). +async fn esp32_report_size( + size_path: &Path, + elf_path: &Path, + max_flash: Option, + max_ram: Option, +) -> fbuild_core::Result { + use fbuild_core::subprocess::run_command; + + let args = [ + size_path.to_string_lossy().to_string(), + "-A".to_string(), + elf_path.to_string_lossy().to_string(), + ]; + let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + let result = run_command( + &args_ref, + None, + None, + Some(std::time::Duration::from_secs(15)), + ) + .await?; + + if !result.success() { + return Err(fbuild_core::FbuildError::BuildFailed(format!( + "size -A failed: {}", + result.stderr + ))); + } + + parse_esp32_size_output(&result.stdout, max_flash, max_ram).ok_or_else(|| { + fbuild_core::FbuildError::BuildFailed(format!( + "failed to parse ESP32 size -A output:\n{}", + result.stdout + )) + }) +} + +/// Parse `size -A` (SysV format) output for ESP32 targets. +/// +/// SysV format lists every section individually: +/// ```text +/// section size addr +/// .flash.text 89012 0x42000020 +/// .flash.rodata 45678 0x42015c34 +/// .dram0.data 1234 0x3fc80000 +/// .dram0.bss 5678 0x3fc81234 +/// .iram0.text 789 0x40800000 +/// Total 142391 +/// ``` +/// +/// Classification: +/// - Flash: `.flash.*`, `.rodata*`, `.text*` (flash-mapped text) +/// - RAM: `.dram0.*`, `.data*`, `.bss*` +/// +/// Falls back to the standard Berkeley parser when no ESP32-prefixed +/// sections are detected (non-ESP32 targets sharing this code path). +fn parse_esp32_size_output( + output: &str, + max_flash: Option, + max_ram: Option, +) -> Option { + let mut flash: u64 = 0; + let mut ram_data: u64 = 0; + let mut ram_bss: u64 = 0; + let mut has_esp_sections = false; + + for line in output.lines() { + // Skip the header line and "Total" line + if line.starts_with("section") || line.starts_with("Total") { + continue; + } + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 2 { + continue; + } + let section = parts[0]; + let Ok(size) = parts[1].parse::() else { + continue; + }; + + if section.starts_with(".flash.") || section.starts_with(".rodata") { + flash += size; + has_esp_sections = true; + } else if section.starts_with(".dram0.") || section.starts_with(".dram.") { + // .dram0.data is initialized RAM, .dram0.bss is zeroed RAM + if section.ends_with(".bss") || section.contains(".bss") { + ram_bss += size; + } else { + ram_data += size; + } + has_esp_sections = true; + } else if section.starts_with(".iram0.") || section.starts_with(".iram.") { + // Instruction RAM — cached flash on most ESP32 variants + flash += size; + has_esp_sections = true; + } else if section == ".text" { + flash += size; + } else if section == ".data" { + ram_data += size; + } else if section == ".bss" { + ram_bss += size; + } + } + + if !has_esp_sections { + return None; + } + + Some(fbuild_core::SizeInfo { + text: flash, + data: ram_data, + bss: ram_bss, + total_flash: flash + ram_data, + total_ram: ram_data + ram_bss, + max_flash, + max_ram, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -917,4 +1042,53 @@ mod tests { let freq = f_flash_to_esptool_freq(Some("64000000L"), config.default_flash_freq()); assert_eq!(freq, "48m"); } + + // ── parse_esp32_size_output ───────────────────────────────────── + + #[test] + fn esp32_size_sysv_separates_flash_from_ram() { + // Simulates `size -A` output for an ESP32-C6 build. + // .flash.rodata (45KB in flash) must NOT inflate RAM. + let output = "\ +section size addr +.flash.text 89012 0x42000020 +.flash.rodata 45678 0x42015c34 +.dram0.data 1234 0x3fc80000 +.dram0.bss 5678 0x3fc81234 +.iram0.text 789 0x40800000 +Total 142391 +"; + let info = parse_esp32_size_output(output, Some(4_194_304), Some(327_680)).unwrap(); + assert_eq!(info.text, 89012 + 45678 + 789); // flash.text + flash.rodata + iram0.text + assert_eq!(info.data, 1234); + assert_eq!(info.bss, 5678); + assert_eq!(info.total_flash, 89012 + 45678 + 789 + 1234); + assert_eq!(info.total_ram, 1234 + 5678); // dram0.data + dram0.bss — no .flash.rodata contamination + assert!(info.ram_percent().unwrap() < 100.0); + } + + #[test] + fn esp32_size_returns_none_for_non_esp_output() { + // Standard Berkeley output without ESP32 section prefixes + // should return None, so the caller falls back to Berkeley parser. + let output = "\ + text data bss dec hex filename + 1234 56 78 1368 558 firmware.elf +"; + assert!(parse_esp32_size_output(output, None, None).is_none()); + } + + #[test] + fn esp32_size_ignores_total_and_header_lines() { + let output = "\ +section size addr +.dram0.data 1000 0x3fc80000 +.dram0.bss 2000 0x3fc81000 +.flash.text 40000 0x42000020 +Total 43000 +"; + let info = parse_esp32_size_output(output, None, None).unwrap(); + assert_eq!(info.total_flash, 40000 + 1000); + assert_eq!(info.total_ram, 1000 + 2000); + } } From b1ec6f3441cd7ccf2b26a8287ee885a788a0608f Mon Sep 17 00:00:00 2001 From: zackees Date: Sun, 9 Aug 2026 21:45:59 -0700 Subject: [PATCH 2/3] fix(esp32): extract size-report module to stay under LOC gate Move the SysV size parser and its tests to a new size_report.rs submodule so esp32_linker.rs stays under the 1000-line ceiling enforced by the LOC gate CI job. No behavioral change. Co-Authored-By: Claude --- .../src/esp32/esp32_linker.rs | 184 +----------------- crates/fbuild-build-esp/src/esp32/mod.rs | 1 + .../fbuild-build-esp/src/esp32/size_report.rs | 183 +++++++++++++++++ 3 files changed, 187 insertions(+), 181 deletions(-) create mode 100644 crates/fbuild-build-esp/src/esp32/size_report.rs diff --git a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs index e911bf5a..3416f63f 100644 --- a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs +++ b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs @@ -556,144 +556,14 @@ impl Linker for Esp32Linker { return Ok(size_info); } - let size_info = esp32_report_size( - &self.size_path, - elf_path, - self.max_flash, - self.max_ram, - ) - .await?; + let size_info = + super::size_report::esp32_report_size(&self.size_path, elf_path, self.max_flash, self.max_ram) + .await?; self.save_size_cache(elf_path, &size_info); Ok(size_info) } } -/// Run `size -A` (SysV format) for ESP32 targets instead of the default -/// Berkeley format. SysV lists every section individually so flash-resident -/// sections (`.flash.*`, `.rodata`) stay out of the RAM total. -/// -/// The Berkeley format lumps `.flash.rodata` into the `data` column -/// alongside `.dram0.data`, inflating the RAM figure — for ESP32-C6 -/// this can report 602% RAM usage (FastLED/fbuild#1261). -async fn esp32_report_size( - size_path: &Path, - elf_path: &Path, - max_flash: Option, - max_ram: Option, -) -> fbuild_core::Result { - use fbuild_core::subprocess::run_command; - - let args = [ - size_path.to_string_lossy().to_string(), - "-A".to_string(), - elf_path.to_string_lossy().to_string(), - ]; - let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); - let result = run_command( - &args_ref, - None, - None, - Some(std::time::Duration::from_secs(15)), - ) - .await?; - - if !result.success() { - return Err(fbuild_core::FbuildError::BuildFailed(format!( - "size -A failed: {}", - result.stderr - ))); - } - - parse_esp32_size_output(&result.stdout, max_flash, max_ram).ok_or_else(|| { - fbuild_core::FbuildError::BuildFailed(format!( - "failed to parse ESP32 size -A output:\n{}", - result.stdout - )) - }) -} - -/// Parse `size -A` (SysV format) output for ESP32 targets. -/// -/// SysV format lists every section individually: -/// ```text -/// section size addr -/// .flash.text 89012 0x42000020 -/// .flash.rodata 45678 0x42015c34 -/// .dram0.data 1234 0x3fc80000 -/// .dram0.bss 5678 0x3fc81234 -/// .iram0.text 789 0x40800000 -/// Total 142391 -/// ``` -/// -/// Classification: -/// - Flash: `.flash.*`, `.rodata*`, `.text*` (flash-mapped text) -/// - RAM: `.dram0.*`, `.data*`, `.bss*` -/// -/// Falls back to the standard Berkeley parser when no ESP32-prefixed -/// sections are detected (non-ESP32 targets sharing this code path). -fn parse_esp32_size_output( - output: &str, - max_flash: Option, - max_ram: Option, -) -> Option { - let mut flash: u64 = 0; - let mut ram_data: u64 = 0; - let mut ram_bss: u64 = 0; - let mut has_esp_sections = false; - - for line in output.lines() { - // Skip the header line and "Total" line - if line.starts_with("section") || line.starts_with("Total") { - continue; - } - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() < 2 { - continue; - } - let section = parts[0]; - let Ok(size) = parts[1].parse::() else { - continue; - }; - - if section.starts_with(".flash.") || section.starts_with(".rodata") { - flash += size; - has_esp_sections = true; - } else if section.starts_with(".dram0.") || section.starts_with(".dram.") { - // .dram0.data is initialized RAM, .dram0.bss is zeroed RAM - if section.ends_with(".bss") || section.contains(".bss") { - ram_bss += size; - } else { - ram_data += size; - } - has_esp_sections = true; - } else if section.starts_with(".iram0.") || section.starts_with(".iram.") { - // Instruction RAM — cached flash on most ESP32 variants - flash += size; - has_esp_sections = true; - } else if section == ".text" { - flash += size; - } else if section == ".data" { - ram_data += size; - } else if section == ".bss" { - ram_bss += size; - } - } - - if !has_esp_sections { - return None; - } - - Some(fbuild_core::SizeInfo { - text: flash, - data: ram_data, - bss: ram_bss, - total_flash: flash + ram_data, - total_ram: ram_data + ram_bss, - max_flash, - max_ram, - }) -} - #[cfg(test)] mod tests { use super::*; @@ -1043,52 +913,4 @@ mod tests { assert_eq!(freq, "48m"); } - // ── parse_esp32_size_output ───────────────────────────────────── - - #[test] - fn esp32_size_sysv_separates_flash_from_ram() { - // Simulates `size -A` output for an ESP32-C6 build. - // .flash.rodata (45KB in flash) must NOT inflate RAM. - let output = "\ -section size addr -.flash.text 89012 0x42000020 -.flash.rodata 45678 0x42015c34 -.dram0.data 1234 0x3fc80000 -.dram0.bss 5678 0x3fc81234 -.iram0.text 789 0x40800000 -Total 142391 -"; - let info = parse_esp32_size_output(output, Some(4_194_304), Some(327_680)).unwrap(); - assert_eq!(info.text, 89012 + 45678 + 789); // flash.text + flash.rodata + iram0.text - assert_eq!(info.data, 1234); - assert_eq!(info.bss, 5678); - assert_eq!(info.total_flash, 89012 + 45678 + 789 + 1234); - assert_eq!(info.total_ram, 1234 + 5678); // dram0.data + dram0.bss — no .flash.rodata contamination - assert!(info.ram_percent().unwrap() < 100.0); - } - - #[test] - fn esp32_size_returns_none_for_non_esp_output() { - // Standard Berkeley output without ESP32 section prefixes - // should return None, so the caller falls back to Berkeley parser. - let output = "\ - text data bss dec hex filename - 1234 56 78 1368 558 firmware.elf -"; - assert!(parse_esp32_size_output(output, None, None).is_none()); - } - - #[test] - fn esp32_size_ignores_total_and_header_lines() { - let output = "\ -section size addr -.dram0.data 1000 0x3fc80000 -.dram0.bss 2000 0x3fc81000 -.flash.text 40000 0x42000020 -Total 43000 -"; - let info = parse_esp32_size_output(output, None, None).unwrap(); - assert_eq!(info.total_flash, 40000 + 1000); - assert_eq!(info.total_ram, 1000 + 2000); - } } diff --git a/crates/fbuild-build-esp/src/esp32/mod.rs b/crates/fbuild-build-esp/src/esp32/mod.rs index cbcd228b..54bc06da 100644 --- a/crates/fbuild-build-esp/src/esp32/mod.rs +++ b/crates/fbuild-build-esp/src/esp32/mod.rs @@ -4,6 +4,7 @@ pub mod esp32_compiler; pub mod esp32_linker; pub mod mcu_config; pub mod orchestrator; +pub(crate) mod size_report; pub use esp32_compiler::Esp32Compiler; pub use esp32_linker::Esp32Linker; diff --git a/crates/fbuild-build-esp/src/esp32/size_report.rs b/crates/fbuild-build-esp/src/esp32/size_report.rs new file mode 100644 index 00000000..1f06bd4c --- /dev/null +++ b/crates/fbuild-build-esp/src/esp32/size_report.rs @@ -0,0 +1,183 @@ +//! ESP32 section-size reporting using SysV format (`size -A`). +//! +//! The default Berkeley `size` format lumps `.flash.rodata` (flash-resident) +//! into the `data` column alongside `.dram0.data` (RAM-resident), inflating +//! the RAM figure — for ESP32-C6 this can report 602% RAM usage +//! (FastLED/fbuild#1261). +//! +//! SysV format lists each section individually with its name and address, +//! so flash and RAM sections can be classified by prefix. + +use std::path::Path; + +use fbuild_core::subprocess::run_command; +use fbuild_core::SizeInfo; + +/// Run `size -A` and parse the SysV output for an ESP32 ELF. +/// +/// Falls back to `None` when no `.flash.*` / `.dram0.*` sections are +/// detected, so the caller can fall through to the standard Berkeley parser. +pub(crate) async fn esp32_report_size( + size_path: &Path, + elf_path: &Path, + max_flash: Option, + max_ram: Option, +) -> fbuild_core::Result { + let args = [ + size_path.to_string_lossy().to_string(), + "-A".to_string(), + elf_path.to_string_lossy().to_string(), + ]; + let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + let result = run_command( + &args_ref, + None, + None, + Some(std::time::Duration::from_secs(15)), + ) + .await?; + + if !result.success() { + return Err(fbuild_core::FbuildError::BuildFailed(format!( + "size -A failed: {}", + result.stderr + ))); + } + + parse_esp32_size_sysv(&result.stdout, max_flash, max_ram).ok_or_else(|| { + fbuild_core::FbuildError::BuildFailed(format!( + "failed to parse ESP32 size -A output:\n{}", + result.stdout + )) + }) +} + +/// Parse `size -A` (SysV format) output for ESP32 targets. +/// +/// ```text +/// section size addr +/// .flash.text 89012 0x42000020 +/// .flash.rodata 45678 0x42015c34 +/// .dram0.data 1234 0x3fc80000 +/// .dram0.bss 5678 0x3fc81234 +/// .iram0.text 789 0x40800000 +/// Total 142391 +/// ``` +/// +/// Classification: +/// - Flash: `.flash.*`, `.rodata*`, `.iram0.*`, `.text` +/// - RAM: `.dram0.*`, `.dram.*` +/// +/// Returns `None` when no ESP32-prefixed sections are detected. +pub(crate) fn parse_esp32_size_sysv( + output: &str, + max_flash: Option, + max_ram: Option, +) -> Option { + let mut flash: u64 = 0; + let mut ram_data: u64 = 0; + let mut ram_bss: u64 = 0; + let mut has_esp_sections = false; + + for line in output.lines() { + if line.starts_with("section") || line.starts_with("Total") { + continue; + } + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 2 { + continue; + } + let section = parts[0]; + let Ok(size) = parts[1].parse::() else { + continue; + }; + + if section.starts_with(".flash.") || section.starts_with(".rodata") { + flash += size; + has_esp_sections = true; + } else if section.starts_with(".dram0.") || section.starts_with(".dram.") { + if section.ends_with(".bss") || section.contains(".bss") { + ram_bss += size; + } else { + ram_data += size; + } + has_esp_sections = true; + } else if section.starts_with(".iram0.") || section.starts_with(".iram.") { + flash += size; + has_esp_sections = true; + } else if section == ".text" { + flash += size; + } else if section == ".data" { + ram_data += size; + } else if section == ".bss" { + ram_bss += size; + } + } + + if !has_esp_sections { + return None; + } + + Some(SizeInfo { + text: flash, + data: ram_data, + bss: ram_bss, + total_flash: flash + ram_data, + total_ram: ram_data + ram_bss, + max_flash, + max_ram, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sysv_separates_flash_from_ram() { + // Simulates `size -A` output for an ESP32-C6 build. + // .flash.rodata (45KB in flash) must NOT inflate RAM. + let output = "\ +section size addr +.flash.text 89012 0x42000020 +.flash.rodata 45678 0x42015c34 +.dram0.data 1234 0x3fc80000 +.dram0.bss 5678 0x3fc81234 +.iram0.text 789 0x40800000 +Total 142391 +"; + let info = parse_esp32_size_sysv(output, Some(4_194_304), Some(327_680)).unwrap(); + assert_eq!(info.text, 89012 + 45678 + 789); + assert_eq!(info.data, 1234); + assert_eq!(info.bss, 5678); + assert_eq!(info.total_flash, 89012 + 45678 + 789 + 1234); + // Only dram0 sections count as RAM — no .flash.rodata contamination + assert_eq!(info.total_ram, 1234 + 5678); + assert!(info.ram_percent().unwrap() < 100.0); + } + + #[test] + fn returns_none_for_non_esp_output() { + // Standard Berkeley output without ESP32 section prefixes + // should return None, so the caller falls back to Berkeley parser. + let output = "\ + text data bss dec hex filename + 1234 56 78 1368 558 firmware.elf +"; + assert!(parse_esp32_size_sysv(output, None, None).is_none()); + } + + #[test] + fn ignores_total_and_header_lines() { + let output = "\ +section size addr +.dram0.data 1000 0x3fc80000 +.dram0.bss 2000 0x3fc81000 +.flash.text 40000 0x42000020 +Total 43000 +"; + let info = parse_esp32_size_sysv(output, None, None).unwrap(); + assert_eq!(info.total_flash, 40000 + 1000); + assert_eq!(info.total_ram, 1000 + 2000); + } +} From 91afd8e841683ad961bfa31f0ed0e91a7792975d Mon Sep 17 00:00:00 2001 From: zackees Date: Sun, 9 Aug 2026 21:47:39 -0700 Subject: [PATCH 3/3] style(esp32): apply rustfmt to size_report submodule Co-Authored-By: Claude --- crates/fbuild-build-esp/src/esp32/esp32_linker.rs | 11 +++++++---- crates/fbuild-build-esp/src/esp32/size_report.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs index 3416f63f..1d30c746 100644 --- a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs +++ b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs @@ -556,9 +556,13 @@ impl Linker for Esp32Linker { return Ok(size_info); } - let size_info = - super::size_report::esp32_report_size(&self.size_path, elf_path, self.max_flash, self.max_ram) - .await?; + let size_info = super::size_report::esp32_report_size( + &self.size_path, + elf_path, + self.max_flash, + self.max_ram, + ) + .await?; self.save_size_cache(elf_path, &size_info); Ok(size_info) } @@ -912,5 +916,4 @@ mod tests { let freq = f_flash_to_esptool_freq(Some("64000000L"), config.default_flash_freq()); assert_eq!(freq, "48m"); } - } diff --git a/crates/fbuild-build-esp/src/esp32/size_report.rs b/crates/fbuild-build-esp/src/esp32/size_report.rs index 1f06bd4c..a3a6cc6f 100644 --- a/crates/fbuild-build-esp/src/esp32/size_report.rs +++ b/crates/fbuild-build-esp/src/esp32/size_report.rs @@ -10,8 +10,8 @@ use std::path::Path; -use fbuild_core::subprocess::run_command; use fbuild_core::SizeInfo; +use fbuild_core::subprocess::run_command; /// Run `size -A` and parse the SysV output for an ESP32 ELF. ///