Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: TreeItem trait #33

Closed
wants to merge 1 commit into from
Closed

feat: TreeItem trait #33

wants to merge 1 commit into from

Conversation

EdJoPaTo
Copy link
Owner

@EdJoPaTo EdJoPaTo commented May 6, 2024

Introduce a TreeItem trait which can render itself to the buffer directly when needed. Due to the decoupling of height and render the items in view can be determined without needing to generate the Text for each item upfront. Only the relevant items are rendered then.

This has a huge performance benefit and renders the cargo metadata JSON data in 1/5 of the time. It still takes ~70 ms on a Raspberry Pi 2 to parse the cargo metadata JSON to JsonTreeItems and render them, but it's better than ~320 ms ✨

The current TreeItem continues to exist as PrerenderedTreeItem but using more efficient TreeItem implementations (like the JsonTreeItem) will be better especially with many (hidden) items.


cargo bench output on Raspberry Pi 2:

$ cargo bench
…
    Finished `bench` profile [optimized + debuginfo] target(s) in 20m 38s
…
init/empty              time:   [445.38 ns 445.59 ns 445.83 ns]
                        change: [+0.9440% +1.0696% +1.1920%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 18 outliers among 100 measurements (18.00%)
  5 (5.00%) high mild
  13 (13.00%) high severe
init/example-items      time:   [20.460 µs 20.463 µs 20.465 µs]
                        change: [-67.323% -67.288% -67.262%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
  1 (1.00%) low severe
  1 (1.00%) low mild
  3 (3.00%) high mild
  5 (5.00%) high severe
Benchmarking init/metadata: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 5.0s, or reduce sample count to 90.
init/metadata           time:   [49.955 ms 50.056 ms 50.114 ms]
                        change: [-77.299% -77.247% -77.210%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 9 outliers among 100 measurements (9.00%)
  1 (1.00%) low severe
  4 (4.00%) high mild
  4 (4.00%) high severe

render/empty            time:   [231.75 µs 231.81 µs 231.87 µs]
                        change: [-5.8634% -5.1479% -4.3294%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 13 outliers among 100 measurements (13.00%)
  3 (3.00%) high mild
  10 (10.00%) high severe
render/example-items    time:   [328.60 µs 328.73 µs 328.87 µs]
                        change: [-7.7537% -6.4460% -5.4807%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild
  5 (5.00%) high severe
render/metadata         time:   [19.944 ms 19.957 ms 19.969 ms]
                        change: [-79.122% -79.080% -79.036%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  1 (1.00%) low severe
  4 (4.00%) high mild
  2 (2.00%) high severe

@EdJoPaTo
Copy link
Owner Author

EdJoPaTo commented May 6, 2024

When you are reading this, feel free to provide a review. I am curious about your thoughts. I am also interested in smaller things like hard to understand stuff. Better comments / function names will also improve the code here. Another aspect is the usage of this. Personally I will attempt migrating mqttui in the coming days to this to see pain points of this approach.

This change involves a lot of generics / lifetimes and having just written them I kinda know what I did so other pairs of eyes might be interesting here.

Mentioning some people that might have insights here. Maybe @Teufelchen1 or (can't mention @ratatui-org/maintainers, likely to prevent spamming) @joshka @orhun @kdheepak? Perhaps @erak or @inanna-malick who forked this last year and played around with a TreeItem trait?

Copy link

@joshka joshka left a comment

Choose a reason for hiding this comment

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

I like the change overall. Makes a lot of sense.

Regarding lifetimes, there's a few named 'a that could be 'content perhaps.

In the Tree type, you refer to the concept of a tree nodes implicitly, but there's no trait or struct backing the concept. This means there's two synonymous concepts (or very closely related concepts if a node isn't an item). A possible approach to avoid the rename could be to name the trait TreeNode instead of TreeItem (you might still rename that if it's not quite the right name).

Do you happen to know the actual size of the metadata tree btw?

A good idea for an example to really test out the trait is a WidgetNode (which can render any boxed WidgetRef instead of just Text etc.). Obviously height is a problem there too.

src/json.rs Outdated Show resolved Hide resolved
src/json.rs Outdated Show resolved Hide resolved
src/json.rs Outdated Show resolved Hide resolved
src/json.rs Outdated Show resolved Hide resolved
src/lib.rs Show resolved Hide resolved
Comment on lines -98 to +37
/// Get a mutable reference to a child by index.
/// Returns the render height of this item.
///
/// When you choose to change the `identifier` the [`TreeState`](crate::TreeState) might not work as expected afterwards.
/// This is the height later available at [`render`](Self::render).
/// It is used to check which items are visible when the full [`Tree`](crate::Tree) is being rendered.
#[must_use]
pub fn child_mut(&mut self, index: usize) -> Option<&mut Self> {
self.children.get_mut(index)
}
fn height(&self) -> usize;
Copy link

Choose a reason for hiding this comment

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

See note earlier about height - likely more applies to this line than the prerendered tree item

Comment on lines +6 to +7
fn inner<Item>(items: &[Item], error: &'static str) -> std::io::Result<()>
where
Copy link

Choose a reason for hiding this comment

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

Perhaps a custom error might be better than io::Result - looking at e.g. HashMap, it defines an OccupiedError for a similar use case (adding an item that already exists).

Copy link

Choose a reason for hiding this comment

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

Perhaps instead of this, I wonder if looking for an Ordered Hash Map crate to use (no direct experience with these, just an idea), then the calling code might be something like:

fn add_child(identifier: Identifier, value: Value) -> SomeError
  if map.contains(identifier) { ... }
  map.push(...)
}

src/json.rs Outdated Show resolved Hide resolved
src/json.rs Outdated
Copy link

Choose a reason for hiding this comment

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

It might be worth thinking about whether it could be useful for editing use cases to support mapping back to the json using Value::pointer / pointer_mut somehow.

Also, more generally it might be nice to consider how to make mutable treeviews work well.

@EdJoPaTo
Copy link
Owner Author

EdJoPaTo commented May 7, 2024

Do you happen to know the actual size of the metadata tree btw?

Currently: 706 kB minified, 954 kB with tabs as indentation.

This might change over time, but the main idea is to have a somewhat large JSON to benchmark against.

@joshka
Copy link

joshka commented May 14, 2024

As an aside (and as much as I hate it) it's really difficult to review larger PRs when you rebase them. I noticed this on the assert_eq PR too. Github doesn't have a good way to help show how they change over time (without manually url hacking, and even then, that's painful), meaning that you have to manually wade through things to work out what has changed.

I'd advise generally (except in really small change situations) just adding more commits and merging to bring in changes from main / other PRs. It sucks that this is the approach that seems to work best, but that's one of github's failures to be a good git citizen.

@EdJoPaTo
Copy link
Owner Author

As an aside (and as much as I hate it) it's really difficult to review larger PRs when you rebase them. I noticed this on the assert_eq PR too. Github doesn't have a good way to help show how they change over time (without manually url hacking, and even then, that's painful), meaning that you have to manually wade through things to work out what has changed.

I'd advise generally (except in really small change situations) just adding more commits and merging to bring in changes from main / other PRs. It sucks that this is the approach that seems to work best, but that's one of github's failures to be a good git citizen.

The compare button in the history helps, but yeah.

In these two branches / PR I regularly move improvements back on the main and reuse them in both. The idea here was quick iteration on improvements.

As merge commits also have only one merge conflict instead of multiple like on a rebase (per commit) that might also work well 🤔

@EdJoPaTo
Copy link
Owner Author

The TreeData trait suggested in #33 is way more flexible and useful, so I stop looking into this approach. (Some review hints are still worth looking into)

@EdJoPaTo EdJoPaTo closed this May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants