Skip to content

Conversation

@maksymar
Copy link
Contributor

@maksymar maksymar commented Dec 8, 2025

This PR introduces a lazy initialization for log_memory_store to avoid charging for dirty page_map.

The canister can be created without wasm module or be uninstalled (when all its memories are cleaned). In those states the canister should require no charge, therefore should use zero memory (especially in page_map's). Previously we were storing log_memory_limit implicitly in ring-buffer header in the first page of page_map, which dirtied the page and required memory usage charging even if canister had no wasm module or was uninstalled.

In this PR we store log_memory_limit outside of page_map keeping it clean and only initizlizing it when appending logs. In the follow up PR this value will be preserved in canister state bits protobuf.

@github-actions github-actions bot added the feat label Dec 8, 2025
@maksymar maksymar changed the title feat: set_log_memory_limit feat: lazy initialization for log_memory_store to avoid unnecessary log-memory charges Dec 8, 2025
@maksymar maksymar marked this pull request as ready for review December 8, 2025 15:22
@maksymar maksymar requested a review from a team as a code owner December 8, 2025 15:22
/// Clears the canister log records.
pub fn clear(&mut self) {
// Write an invalid ring buffer to the page map to avoid unnecessary log-memory charges.
self.page_map = RingBuffer::invalid(self.page_map.clone()).to_page_map();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.page_map = RingBuffer::invalid(self.page_map.clone()).to_page_map();
self.page_map = PageMap::new(allocator)

How about clearing it like that? That would reset the entire PageMap to zeroes. The way you do it the old log is still in the state/checkpoint, as you simply marked it as invalid. Replacing the PageMap will discard the old data. Your invalid marker would then simply be zero bytes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, PTAL

@maksymar maksymar enabled auto-merge December 9, 2025 07:59
let mut ring_buffer = self.load_ring_buffer();
f(&mut ring_buffer);
let mut ring_buffer = self.load_ring_buffer().unwrap_or(RingBuffer::new(
self.page_map.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this cloning the old page_map, instead of creating a new, empty one? I thought this is the case where the ring buffer does not exist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In append_delta_log when it's called the first time after creation we don't have access to the allocator, but we do have access to the empty page_map that was created in new_inner or in clear. So it's actually initializing this empty page map with the ring buffer for the first time.

/// Clears the canister log records.
pub fn clear(&mut self, fd_factory: Arc<dyn PageAllocatorFileDescriptor>) {
// This creates a new empty page map with invalid ring buffer header.
self.page_map = PageMap::new(fd_factory);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More an interface question on future PRs really, but what is the plan regarding the log_memory_limit when you uninstall code?

When the code uninstalls because we are out of cycles, presumably we want to clear the log, and also want to set the limit back down to DATA_CAPACITY_MIN so that the memory is freed again.

On the other hand, if a user uninstalls code manually, do they even want the log to be cleared? And if so, should the log limit change in that case? Is this maybe something the user can choose with a new field for uninstall_code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when canister is uninstalls then log is deleted. but log_memory_limit is more like a user setting (similar to wasm memory limit, etc) and I don't think this should be dropped to default. when the user installs the wasm, then it'll start working with the previous log memory limit value.

//
// Recreate a ring buffer with the new capacity and restore records.
let mut new = RingBuffer::new(
self.page_map.clone(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question, should this be an empty PageMap instead of a clone?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't store the allocator it was easier just to clone an existing page map. Especially considering that this would be the first candidate for future optimizations.

self.page_map.clone(),
MemorySize::new(new_log_memory_limit as u64),
);
new.append_log(old.all_records());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for a future PR that calls this function: This is potentially expensive and needs to be reflected correctly in cycles cost/instruction counters/heap deltas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, noted. that's the first candidate for optimization.

// Only resize on an existing ring buffer.
if let Some(old) = self.load_ring_buffer() {
// Only resize when the capacity actually changes.
if old.byte_capacity() != new_log_memory_limit {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do this expensive clone even if the limit increases? PageMaps can already grow today.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we do, because it's a ring buffer which is always almost full and 99% of a time is wrapped, so you can't just grow memory and continue saving records. you need to re-wrap the whole buffer at the new capacity. that's why I read all the records in memory, re-create ring buffer with a new capacity and write records back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants