Kotlin / kotlinx.html

Kotlin DSL for HTML
Apache License 2.0
1.63k stars 133 forks source link

Shouldn't DSL functions be inline? #67

Closed Miha-x64 closed 1 year ago

Miha-x64 commented 6 years ago

Is it necessary to have an anonymous class per DSL lambda?

image

cy6erGn0m commented 6 years ago

Originally it was planned to have them inline however it was inefficient in old kotlin-js compilers. Need to be investigated again

soywiz commented 6 years ago

+1 Not just because of anonymous classes/runtime performance, but making them inline would enable suspend calls inside the DSL which is not possible right now. So it is not prohibitive for the JS Backend now, it would be a really nice improvement :)

Miha-x64 commented 6 years ago

While writing DOM — may be. While streaming (on JVM) — no! First fetch data, next return HTML, so nothing will fail in DSL with mangled stack trace.

orangy commented 6 years ago

I agree with @Miha-x64 on data first. However, making it all inline might explode the size of bytecode generated. If it could be an option…

soywiz commented 6 years ago

I see your point on data first, and makes sense.

Now let me explain to you a use case based on my experience and with a example/rationale behind it. It doesn't mean it has to be a better approach, or that it do not have flaws, but just consider it.

Suppose I'm creating a CMS, or a plain website. That website has pieces that I can put in different places and conditionally: for example A/B testing or based on user preferences. Also consider that the amount of html generated for those blocks could be potentially big and that we want to cache database queries and that we want to prevent unnecessary database queries.

Now consider the following reusable piece of code:

suspend fun Tag.lastPostsInCategory(category: Category) {
    cache("last-posts-${category.name}") { // suspend call (inmemory, redis, memcached, ...)
        h1 { +"Last posts" }
        ul {
            for (post in category.lastPostsInCategory(category)) { // suspend call (uncached database access)
                li { 
                    a(PostRoute(post.name)) { +post.title }
                }
            }
        }
    }
}

Where cache block is just computed on cache-miss based on a key, and computes a plain string without having to generate objects per each tag for each requesst, and that could provide (somehow) a graceful fallback like generating "Data not available" just for that block instead of a full 500 Internal Server Error.

You can always compute what is required and what is not before html rendering. But you have to do that explicitly while here it is just natural. The data is computed just when required and lazily. You don't even have to have all that data in memory while doing the request, but potentially just one row at a time. It just requires one kind of cache. Instead of potentially having to cache that whole query and later that html too if you want to prevent that object creation.

Obviously this is just an idea. I have been coding a lot of years in PHP, and maybe this approach has too many flaws. But wanted to share anyway so you can consider it :)


In a plain MVC the controller usually grabs data and passes it to the view. But sometimes, you just compute more data than required, and that doesn't allow to compute it lazily. I worked for a big PHP company a lot of years ago and I have seen it. At least, they, computed more than required. Also since some people worked on views and other in controllers, that was pretty common.

Some years later I tried an approach where views were the ones that lazily requested what it needs. I did this https://github.com/soywiz/atpl.js and created a couple of sites with that approach. That's not MVC, but after trying it I found it pretty convenient. Also using plain files for html views allows to generate less objects per request. Since it treats a whole chunk of html as a single string. But you lose static typing, assisted autocompletion and typesafety. So I thought about this approach: simple, asynchronous, lazy, typesafe, cacheable to reduce object creation. And it is pretty appealing to me at least for my personal projects.

spand commented 6 years ago

Sorry if I am being dense but what is the bytecode size difference of inlining the code? Isnt it only these functions that need to be inlined?

I havent performed any tests but intuitively that doesnt seem like much. Especially when taking into account the overhead of all those class files.

spand commented 6 years ago

I performed a quick test with the test classes in module kotlinx-html-jvm. With noninline methods:

spand@DESKTOP MINGW64 ~/IdeaProjects/kotlinx.html/jvm/target/test-classes/kotlinx/html/tests (master)
$ du -sh .
860K    .

I added inline modifiers to consumerBuilderShared, htmlTagBuilderMethod and htmlTagEnumBuilderMethod:

spand@DESKTOP MINGW64 ~/IdeaProjects/kotlinx.html/jvm/target/test-classes/kotlinx/html/tests (master)
$ du -sh .
196K    .

Unless I have made a mistake or we need to count size some other way then it seems like a pretty nice win.

elizarov commented 6 years ago

I've recently stumbled upon the need for inline DSL functions, too, while trying to naively invoke a suspending function multipart.readPart() from inside of HTML builder block.

LouisCAD commented 6 years ago

@orangy

However, making it all inline might explode the size of bytecode generated. If it could be an option…

If the inline DLS functions delegate most of their implementation to non inline functions as to have minimum inline code apart from the passed lambda, the bytecode size might not explode that much, and could even shrink as inline generates less classes and methods.

spand commented 5 years ago

What are the outstanding issues to making a decision on this ?