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

Thoughts on an html! macro that emits an impl Read value #37

Open
infogulch opened this issue Nov 7, 2022 · 3 comments
Open

Thoughts on an html! macro that emits an impl Read value #37

infogulch opened this issue Nov 7, 2022 · 3 comments

Comments

@infogulch
Copy link

infogulch commented Nov 7, 2022

Hi! First I want to say thank you for making this library! I appreciate the features and principles you aimed for, especially "non opinionated" parser that accepts any xml-like pattern, the minimal dependencies (especially avoiding wasm-bindgen), and a modular design that exposes the parsed node tree as a base to build any kind of macro on top of.

Because of these features I was able to easily integrate html-to-string-macro into a demo project based around htmx, a library that adds interactivity to html by requesting html fragments over http and inserting them into the DOM (an approach that offers more bang-for-buck than its simplistic appearance indicates). (See also htmx intro presentation if you like.)


I'd like to expand this demo project to be something more, but I'm concerned that that deeply recursive use of html-to-string-macro's html! would lead to O(n²) performance due to repeatedly calling format! on larger and larger fragments of the page as the page is gradually built up.

To solve this I'm thinking about writing a new implementation of the html! macro, maybe called html-to-reader-macro, that instead of emitting a String it emits a value that implements the Read trait. The idea being that you can build up a big graph of objects that impl Read and then calling read() on the root will traverse through the component tree and scan out all the bytes directly into the final destination buffer exactly once with no unnecessary intermediate copying.

What do you think? Useful? Feasible? Gotchas?

@stoically
Copy link
Owner

stoically commented Nov 7, 2022

Glad it's useful! syn-rsx came to be while I wanted to implement a liveview-ish web framework (https://github.com/dbohdan/liveviews, with Phoenix Live.View being the most prominent example), but I never actually started to work on it. Sounds like you want to go down a similar route with your project?

I'd like to expand this demo project to be something more, but I'm concerned that that deeply recursive use of html-to-string-macro's html! would lead to O(n²) performance due to repeatedly calling format! on larger and larger fragments of the page as the page is gradually built up.

Not sure if that's actually the case? It does end up with a potentially really large format! call in the end (fn html), but the recursion itself (fn walk_nodes) only keeps appending tiny bits of strings and values.

To solve this I'm thinking about writing a new implementation of the html! macro, maybe called html-to-reader-macro, that instead of emitting a String it emits a value that implements the Read trait. [..] What do you think? Useful? Feasible? Gotchas?

That sounds like an useful and more elegant way to solve it – and I can't think of any larger gotchas! If you come up with something, I'd be also interested in changing the html-to-string-macro implementation, since the format! approach was mostly a quick hack.

@infogulch
Copy link
Author

infogulch commented Nov 7, 2022

Live.View looks pretty interesting and broadly similar to what htmx does. From a glance the major difference may be the simplicity of the server side: htmx doesn't have any system for managing client state, it simply serves html pages or fragments (serving fragments is the 'novel' part) in response to plain http requests.

You're right, walk_nodes only appends strings and expressions in a single call to format!. Sure it could emit bytes directly into some destination buffer but this only avoids one copy, which could be nice but doesn't devolve to O(n²) performance problem alone. The problem comes when nesting calls to html! inside an expr node.

Here's a (contrived) example of what I mean. This is way more atomized than I would expect a normal app to be, but it still demonstrates the issue I'm pointing at:

fn root(count: u32) -> String {
    html! {
        <html>
            <head><title>"O(n²) 😔"</title></head>
            <body>
            { container(count) }
            </body>
        </html>
    }
}

fn container(count: u32) -> String {
    html! {
        <div>{ table(count) }</div>
    }
}

fn table(count: u32) -> String {
    html! {
        <table>{ rows(count) }</table>
    }
}

fn rows(count: u32) -> String {
    html! {
        { (0..count).map(row).collect::<String>() }
    }
}

fn row(n: u32) -> String {
    html! {
        <tr>
            { col1(n) }
            { col2(n) }
        </tr>
    }
}

fn col1(n: u32) -> String {
    html! { <td>"N: " { n }</td> }
}

fn col2(n: u32) -> String {
    html! { <td>"Double: " { n*2 }</td> }
}

Just to follow along explicitly, col1 and col2 both generate strings on demand with format!, then those strings are passed to row which concats them together into a tr, then rows takes all these rows and concats them together and then format!s them into a string again, then table, then container, then root. This is a lot of copying, and the copying grows multiplicatively with depth and the total size of lower levels.

htmx encourages some fragmentation (not to this degree of course) because you're meant to be able to make a request, say for row 33, and the server will respond with just that row as an html fragment.

@infogulch
Copy link
Author

infogulch commented Dec 8, 2022

On a scale of "make it work, make it right, make it fast", this issue is on the "make it fast" side, and may reasonably be prioritized below #48 which is closer to the "make it right" side imo.

Note, the linked commit, 9626096, includes a technique to serialize string literals directly into the format string (as opposed to providing them as separate args) which could improve rendering performance.

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

No branches or pull requests

2 participants