Skip to content

Commit 46df609

Browse files
Copilotfilipton
andauthored
chore: plan zero-heap static log buffer approach
Agent-Logs-Url: https://github.com/FKMTime/firmware/sessions/979ed59b-c59d-48c0-a2df-2d26d23bc225 Co-authored-by: filipton <37213766+filipton@users.noreply.github.com>
1 parent 64d5aa4 commit 46df609

3 files changed

Lines changed: 80 additions & 36 deletions

File tree

src/main.rs

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -381,44 +381,74 @@ async fn logger_task(global_state: GlobalState) {
381381
loop {
382382
Timer::after_millis(LOG_SEND_INTERVAL_MS).await;
383383

384-
let mut tmp_logs: Vec<String> = Vec::new();
385-
for _ in 0..consts::LOG_BATCH_SIZE {
386-
match utils::logger::LOGS_CHANNEL.try_receive() {
387-
Ok(msg) => tmp_logs.push(msg),
388-
Err(_) => break,
389-
}
384+
if ota_state() || sleep_state() {
385+
// Drain and discard so the channel does not fill up.
386+
while utils::logger::LOGS_CHANNEL.try_receive().is_ok() {}
387+
continue;
390388
}
391389

392-
if ota_state() || sleep_state() {
390+
// How many entries can we drain this tick?
391+
let available = utils::logger::LOGS_CHANNEL.len().min(consts::LOG_BATCH_SIZE);
392+
if available == 0 {
393+
continue;
394+
}
395+
396+
// Binary log packet layout:
397+
// [1] packet type = 0x01
398+
// [8] current_time (u64 LE)
399+
// [1] entry count (u8)
400+
// per entry:
401+
// [2] byte length (u16 LE)
402+
// [N] UTF-8 string
403+
//
404+
// Pre-compute worst-case payload size and trim the count until it fits
405+
// in available heap, keeping MIN_HEAP_REMEANING as safety margin.
406+
// This is a single, non-growing allocation — no serde_json, no copies.
407+
const HEADER: usize = 10; // 1 + 8 + 1
408+
const ENTRY_OVERHEAD: usize = 2; // u16 length prefix
409+
let mut count = available;
410+
loop {
411+
if count == 0 {
412+
break;
413+
}
414+
let max_payload =
415+
HEADER + count * (ENTRY_OVERHEAD + utils::logger::LOG_MSG_MAX_LEN);
416+
if esp_alloc::HEAP.free()
417+
> max_payload + utils::logger::MIN_HEAP_REMEANING
418+
{
419+
break;
420+
}
421+
count -= 1;
422+
}
423+
if count == 0 {
393424
continue;
394425
}
395426

396-
if !tmp_logs.is_empty() {
397-
tmp_logs.reverse();
398-
399-
// Drop oldest entries (end of vec after reverse) until the estimated
400-
// serialised JSON fits comfortably in available heap. Each pop()
401-
// immediately frees the String's heap allocation, raising HEAP.free().
402-
while tmp_logs.len() > 1 {
403-
// Estimate: each entry contributes its byte length + 3 (quotes +
404-
// comma), plus ~80 bytes of JSON wrapper. Multiply by 2 to cover
405-
// serde_json's internal growth doubling.
406-
let estimated: usize =
407-
tmp_logs.iter().map(|s| s.len() + 3).sum::<usize>() * 2 + 80;
408-
if esp_alloc::HEAP.free() > estimated + utils::logger::MIN_HEAP_REMEANING {
409-
break;
427+
let capacity = HEADER + count * (ENTRY_OVERHEAD + utils::logger::LOG_MSG_MAX_LEN);
428+
let mut payload: Vec<u8> = Vec::with_capacity(capacity);
429+
430+
let current_time = unsafe { crate::stackmat::CURRENT_TIME };
431+
payload.push(0x01_u8); // packet type: Logs
432+
payload.extend_from_slice(&current_time.to_le_bytes());
433+
payload.push(count as u8); // placeholder; corrected below
434+
435+
let mut actual_count: u8 = 0;
436+
for _ in 0..count {
437+
match utils::logger::LOGS_CHANNEL.try_receive() {
438+
Ok(msg) => {
439+
let b = msg.as_bytes();
440+
payload.extend_from_slice(&(b.len() as u16).to_le_bytes());
441+
payload.extend_from_slice(b);
442+
actual_count += 1;
443+
// `msg` (heapless::String, BSS-backed) is dropped here
410444
}
411-
tmp_logs.pop();
445+
Err(_) => break,
412446
}
447+
}
448+
payload[9] = actual_count; // correct the count byte
413449

414-
ws::send_packet(structs::TimerPacket {
415-
tag: None,
416-
data: structs::TimerPacketInner::Logs {
417-
current_time: Some(unsafe { crate::stackmat::CURRENT_TIME }),
418-
logs: tmp_logs,
419-
},
420-
})
421-
.await;
450+
if actual_count > 0 {
451+
ws::send_binary(payload).await;
422452
}
423453

424454
#[cfg(not(feature = "release_build"))]

src/utils/logger.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
use crate::state::{ota_state, sleep_state};
2-
use alloc::string::String;
2+
use core::fmt::Write;
33
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel};
44

5+
/// Maximum byte length of a single log message stored in the channel.
6+
/// Messages longer than this are silently truncated.
7+
pub const LOG_MSG_MAX_LEN: usize = 200;
8+
59
const MAX_LOGS_SIZE: usize = 100;
610
pub const MIN_HEAP_REMEANING: usize = 10240;
7-
pub static LOGS_CHANNEL: Channel<CriticalSectionRawMutex, String, MAX_LOGS_SIZE> = Channel::new();
11+
12+
/// Log messages are stored as fixed-size inline strings so that the channel
13+
/// lives entirely in BSS — no heap allocations, no fragmentation.
14+
pub static LOGS_CHANNEL: Channel<
15+
CriticalSectionRawMutex,
16+
heapless::String<LOG_MSG_MAX_LEN>,
17+
MAX_LOGS_SIZE,
18+
> = Channel::new();
819

920
#[cfg(feature = "release_build")]
1021
pub const FILTER_MAX: log::LevelFilter = log::LevelFilter::Info;
@@ -63,11 +74,10 @@ impl log::Log for FkmLogger {
6374
_ = LOGS_CHANNEL.try_receive();
6475
}
6576

66-
let msg = alloc::format!("{} - {}", record.level(), record.args());
67-
68-
// Do not send log msg to channel if heap space is too low!
69-
// maybe not performent but thats ok
7077
if esp_alloc::HEAP.free() > MIN_HEAP_REMEANING {
78+
let mut msg = heapless::String::<LOG_MSG_MAX_LEN>::new();
79+
// write! truncates silently once the string is full
80+
_ = write!(msg, "{} - {}", record.level(), record.args());
7181
_ = LOGS_CHANNEL.try_send(msg);
7282
} else {
7383
// clear logs channel if heap space is too low

src/ws.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,10 @@ pub async fn send_packet(packet: TimerPacket) {
605605
}
606606
}
607607

608+
pub async fn send_binary(data: alloc::vec::Vec<u8>) {
609+
FRAME_CHANNEL.send(WsFrameOwned::Binary(data)).await;
610+
}
611+
608612
#[allow(dead_code)]
609613
pub fn clear_frame_channel() {
610614
FRAME_CHANNEL.clear();

0 commit comments

Comments
 (0)