Skip to content

Conversation

@afishhh
Copy link
Contributor

@afishhh afishhh commented Oct 28, 2025

A large chunk of time spent each frame on large immutable TextEdits is cloning the text string repeatedly:

~60% time in `TextEdit::show` spent in `str::to_owned`

This PR attempts to switch surrounding code to Arc<str> and adds TextBuffer::clone_to_arc + impl TextBuffer for Arc<str> which allow skipping most of the string copying work for buffers that are themselves Arcs.

These changes (and modifying the app to pass Arc<str>) make the profile look like this:

~50% time in `TextEdit::show` spent in `str::to_owned`1

Clearly better and decreases the frame times in this test app by ~10ms.

However there is one elephant in the room (the remaining to_owned) that requires more invasive changes to handle: LayoutJob. This elephant is actually very significant: I am able to achieve acceptable performance (in my real-world app that I'm trying to speed up) with a 1MB XML string if I apply a hacky solution to avoid cloning the string on LayoutJob creation.

Here's what that looks like on the benchmark (#7649 + this + hacky LayoutJob fix):

`to_owned` absent in `TextEdit::show`12

This does require changing LayoutJob though and I'm not exactly sure what to do here which is why this is a draft. Should I just make LayoutJob itself immutable and add a builder type for it? If so what should the builder type's API look like? There's a few possibilities that come to mind but not sure I particularly like any of them.

  • I have followed the instructions in the PR template

Footnotes

  1. Ignore the sample count, these profiles were run for completely arbitrary amounts of time. 2

  2. Also suggests that the next optimization opportunity is in skipping undo handling for immutable buffers, along with PaintStats related stuff which is not visible here but constitutes a large percentage of runtime.

@github-actions
Copy link

github-actions bot commented Oct 28, 2025

Preview available at https://egui-pr-preview.github.io/pr/7673-textedit-less-cloning
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

View snapshot changes at kitdiff

@afishhh afishhh force-pushed the textedit-less-cloning branch from edde2b2 to 3da488e Compare October 28, 2025 21:41
@afishhh afishhh force-pushed the textedit-less-cloning branch from 3da488e to 71f34ea Compare October 28, 2025 21:47
@afishhh
Copy link
Contributor Author

afishhh commented Nov 3, 2025

Actually for my use case it seems sufficient to make the layout_job layout entry point accept an Arc<LayoutJob>. That's enough for displaying large amounts of immutable non-line-wrapped text. It's not perfect but it works and doesn't require changing how LayoutJob itself works.

@afishhh afishhh force-pushed the textedit-less-cloning branch from b033ae4 to 6e04a39 Compare November 3, 2025 18:56
@afishhh afishhh force-pushed the textedit-less-cloning branch from 6e04a39 to f5d9f00 Compare November 3, 2025 19:12
// * https://github.com/emilk/egui/issues/5163

job.wrap.max_width = job.wrap.max_width.round();
Arc::make_mut(&mut job).wrap.max_width = job.wrap.max_width.round();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is slightly unfortunate and could be avoided by threading a separate max_width throughout layout but that sounds like a lot of work while when working with a lot of text you probably don't necessarily need line wrapping anyway.

@afishhh afishhh marked this pull request as ready for review November 3, 2025 22:21
@emilk emilk added performance Lower CPU/GPU usage (optimize) egui labels Nov 25, 2025
Copy link
Owner

@emilk emilk left a comment

Choose a reason for hiding this comment

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

Overall looks good, but I would love it if you added some benchmarks to crates/egui/benches and showed us the before/after numbers of that. Otherwise this may regress again.

Comment on lines +519 to +520
// TODO(afishhh): Keep as `Arc<str>` instead of cloning to a `String`
let text = (*mask_if_password(password, text.clone_to_arc())).to_owned();
Copy link
Owner

Choose a reason for hiding this comment

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

I suggest we do this in this PR, or it feels a bit half-finished

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do agree but this goes straight into a LayoutJob and I'm not sure how exactly to handle Arcifying that.

Should I just split out a builder type and make LayoutJob itself immutable?

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

Labels

egui performance Lower CPU/GPU usage (optimize)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants