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

Asynchronous API #41

Closed
gimenete opened this issue Dec 12, 2012 · 86 comments
Closed

Asynchronous API #41

gimenete opened this issue Dec 12, 2012 · 86 comments

Comments

@gimenete
Copy link
Contributor

I know that nunjucks is a direct port of jinja2, so probably due to this internally it is using some sync methods of nodejs to access files, etc. However I think that having an asynchronous API would bring better performance. For example inside FileSystemLoader there are calls to fs.existsSync, fs.readFileSync and fs.statSync. This blocks the whole javascript execution so it causes a scability problem.

I know that this could be a big change, but would be a big step forwards.

@devspacenine
Copy link

Bump

@jlongster
Copy link
Contributor

The thing is I don't think it would help very much. Once a template is loaded it is cached for the duration of the process. It is only reloaded if the file's modification times are newer than the cached version.

So those sync methods are never really used in production, only the very first request.

The only one that is used every request is statSync, so the chance of a scalability problem is pretty low. It's possible we need to make that async though, but that would require about the same amount of refactoring. Not a huge amount, but isn't a quick change.

@jbergstroem
Copy link
Contributor

In nodejs 0.9 there's a proper watchFs for all platforms, not just linux. That should make the stat on every access be replaceable with inotify/kqueue/etc.

@jlongster
Copy link
Contributor

@jbergstroem That's great. I suppose we could use that to set flags in the Environment object to reload templates.

That's why I love node. It's so freakin' easy to do async stuff.

@jbergstroem
Copy link
Contributor

We're doing similar stuff for vhosts in express (load entire site if changed) and eagerly await this too 👍

@boutell
Copy link

boutell commented Dec 30, 2012

In a production environment it is common to have a flag that assumes the app should not constantly stat() assets and templates just in case they have changed, because they are only changed at deployment time (when the app is restarted). That would be handy, and negative work almost (:

@jbergstroem
Copy link
Contributor

@boutell Indeed. To take that even further - you should cache your templates at a higher tier. Not saying it's one or the other; most preferably both. I recall both django and rails doing something similar with it's production/development flags. On the other hand, that also shows that its probably not up to the template engine to do rather than exposing an way of handling it.

@jlongster
Copy link
Contributor

@boutell I'm trying to avoid having to set a dev/prod flag if possible. I believe that using watchFs would mitigate the issue though, because it has zero performance impact because the filesystem events are coming from the kernel. Does that sound good?

@boutell
Copy link

boutell commented Jan 24, 2013

I think having the flag available makes a lot of sense, I wouldn't call it
'dev' or 'prod' though, I'd call it 'statEveryTime' and have it default to
true. Deciding what happens in what environment is then a decision for a
higher layer of the app.

Or, yeah, you could ask the OS to watch the files for you. I'm not sure
what the performance impact on the system itself is when everybody starts
doing that.

On Thu, Jan 24, 2013 at 1:53 PM, James Long notifications@github.comwrote:

@boutell https://github.com/boutell I'm trying to avoid having to set a
dev/prod flag if possible. I believe that using watchFs would mitigate the
issue though, because it has zero performance impact because the filesystem
events are coming from the kernel. Does that sound good?


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-12666815.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@dhendo
Copy link
Contributor

dhendo commented Feb 13, 2013

Another thought on this - I'm writing a Loader that pulls the template from a mongo db.
This operation can't (and shouldn't) be run synchronously, so will not work with the current loader system.

@jlongster
Copy link
Contributor

@dhendo That's a great use case. I will work on this. Autoescaping is my #1 priority, but this will come soon.

@boutell
Copy link

boutell commented Feb 16, 2013

Hmm, interesting. We just got used to the fact that Node's template
languages are synchronous and so you need to muster everything in advance
with middleware. Which works, and tends to keep the template code simple.
But David has a point about loading includes via something that can't be
synchronous.

On Fri, Feb 15, 2013 at 4:14 PM, James Long notifications@github.comwrote:

@dhendo https://github.com/dhendo That's a great use case. I will work
on this. Autoescaping is my #1https://github.com/jlongster/nunjucks/issues/1priority, but this will come soon.


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-13628355.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@devoidfury
Copy link
Contributor

Haven't seen anyone start on this yet, so I've begun working on it here - (https://github.com/devoidfury/nunjucks/tree/async_templates). There's still a long way to go, but it's a start.

An asynchronous API will break existing code, unless we use a flag and maintain both styles.

@jlongster
Copy link
Contributor

@boutell Our main focus should be loading templates asynchronously. So you'll still have to load all your data up front and pass it into the templates (I personally don't see this as a problem, otherwise your putting too much logic in your templates).

This change is mostly a backend change and most people won't notice if they are just using the express integration.

@devoidfury It will change the API, yes, but we can update the express integration to make it work seamlessly for people who aren't using the API directly, which is probably most people. Thanks for starting this, it looks good. This will also help the HttpLoader in the browser because right now it has to use sync ajax, which is yucky.

@devoidfury
Copy link
Contributor

@jlongster Already taken care of express integration and the HttpLoader in my branch. I'm working on extends, includes, and updating the tests at the moment.

@jlongster
Copy link
Contributor

@devoidfury I'm considering changing how precompiled templates are integrated, and the async API would be required with the new API I'm thinking of, so I'll make this a higher priority.

This would require all render calls to be async, right? So instead of this:

var tmpl = nunjucks.env.render('foo.html');

We now have:

nunjucks.env.render('foo.html', function(tmpl) {
  // ...
});

This is quite a big change, and we'll probably have to keep it behind a flag for a while to not break existing apps. Also, can you guys think of any major problems this causes with any integration with frameworks, like backbone, ermber, etc? All of the other templating libraries seem to be synchronous, so this worries me a lot. If other frameworks expect it to be sync, we have a problem.

@dhendo
Copy link
Contributor

dhendo commented Mar 1, 2013

Interesting... you could possibly have an optional callback, that is used if present. Otherwise return synchronously as before?

@jlongster
Copy link
Contributor

Now that I finished autoescaping and custom tags API, this is the next big feature to tackle. I'll spend some time on this next week. I agree that we can probably make sync and async version work at the same time, but that will require some more research.

@devoidfury
Copy link
Contributor

I'm really excited about this feature; made some good progress on my branch, but I probably wouldn't merge it at this point as it's written as a one-way breaking change (and unfinished).

The only async templating libraries I've found so far that aren't abandoned are QEJS, node-blue, and kiwi. Might look to them for inspiration. Kiwi seems to be express-compatible; we may look for other usages of it to see what kind of real-world challenges await an async implementation.

@jlongster
Copy link
Contributor

I'm somewhat worried about making something like this optional, because it might things too confusing -- writing filters, for example, shouldn't involving always handling an optional callback. I'm wondering though, if we can enable both styles at once, if we should deprecate the sync style and drop it later on.

I don't have a great grasp on how many people are using nunjucks, but I don't think it's a huge amount. At least, there isn't a big repo of nunjucks filters/extensions yet, so now would be a good time to enforce this if we are going to.

In fact, I'm planning on making the next release v0.2.0 (or maybe even higher) because it include autoescaping, a custom tag API, and possibly this async stuff. It would be a good time to introduce such a big change.

@boutell
Copy link

boutell commented Apr 10, 2013

We're relying on nunjucks for Apostrophe, so we would definitely appreciate not having a breaking change this big happen on a patchlevel version number. But as long as you choose the version number well it should work out.

@jbergstroem
Copy link
Contributor

@jlongster My take is to get it in sooner than later. Bumping to 0.2.0 sounds reasonable; the autoescaping will probably make a lot of people audit their stuff anyway. Will this also rely on the node 0.10 watchFs (read: a working cross-platform watchFs implementation)?

@jbuck
Copy link
Member

jbuck commented Jun 19, 2013

I have a concrete use case for an async filter API: https://github.com/brianloveswords/node-htmlsanitizer is a node module that hits a Bleach.py REST endpoint to sanitize HTML. I'd love to be able to add a | bleach filter to user input and just have it work. A man can always dream, right?

@jlongster
Copy link
Contributor

@jbuck yeah, that's a pretty good one. In the next week or two I plan on setting down and thinking hard about this. I have some ideas for other API improvement too, and I will post them to the mailing list. If you are interested in this discussion, please join the mailing list (https://groups.google.com/forum/?fromgroups#!forum/nunjucks). When we nail down specific actions we can pick this bug back up.

@faceleg
Copy link
Contributor

faceleg commented Jul 7, 2013

I for one would love asynchronicity. Especially around custom blocks :)

@boutell
Copy link

boutell commented Jul 7, 2013

We have a strong preference for being able to render "partials" (templates
that output HTML fragments) that can't see any variables in the scope of
the outer template they are being called from. This feature isn't native to
jinja2/nunjucks, but data hiding is a good principle whether you are
working in the view layer or the model layer.

So we usually wind up including a function called "partial" in the data
object given to our templates. And what "partial" does is render another
template, passing data to it, and return the result.

We also tend to write convenience functions that do more logic (although it
is truly view layer logic) than one really wants to write in pure nunjucks
and make those available to templates. And they often call partial() too.

So my concern is that it remain possible to use nunjucks in a synchronous
way, as long as we are choosing not to load templates asynchronously.

I also have to ask if all of these asynchronously loaded templates really
can't be cached into memory at startup or on modification or something
similar.

On Sun, Jul 7, 2013 at 6:24 AM, Michael Robinson
notifications@github.comwrote:

I for one would love asynchronicity. Especially around custom blocks :)


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-20568712
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@jlongster
Copy link
Contributor

I'm looking into this finally, and hope to have something implemented soon. I agree that it's useful to have both APIs, but I'm concerned at how much that will split filters/extensions. More research is needed.

@faceleg
Copy link
Contributor

faceleg commented Jul 22, 2013

Personally I'm interested in being able to use asynchronous data fetching within custom tag processing code, although I feel at the node community as a whole would love to have the async option for the flagship templating engine ;)

@jlongster
Copy link
Contributor

It seems like there a few things going on in this issue. Originally, the discussion was about using watchFs instead of stat'ing every hit to see if the templates have changed. I believe we can do that without the big async overhaul. We should probably just file another issue for that, and focus on making all rendering async.

I'm pickup the work back up, starting with what devoidfury has done, and will focus on making the rendering APIs async. I really don't want to maintain 2 different APIs and split the code into sync and async versions. I think the biggest pain point will be filters, since that's what most people have written. We can probably detect when a filter is async, so that we can allow both sync and async filters, which will solve that problem.

@boutell
Copy link

boutell commented Jul 22, 2013

In addition to filters, we also inject functions whose output will in some
cases include the content of other templates.

For example:

{{ aposArea(page.slug + ':sidebar', page.areas.sidebar) }}

It doesn't seem likely that it would still be possible to do this in an
async-only version of nunjucks.

The ability to inject a function that executes some fairly elaborate logic,
more than you'd want to write in jinja syntax, seems an important option
especially when there doesn't seem to be support for creating namespaces
for included files that come from different npm modules, etc.

On Mon, Jul 22, 2013 at 4:02 PM, James Long notifications@github.comwrote:

It seems like there a few things going on in this issue. Originally, the
discussion was about using watchFs instead of stat'ing every hit to see if
the templates have changed. I believe we can do that without the big async
overhaul. We should probably just file another issue for that, and focus on
making all rendering async.

I'm pickup the work back up, starting with what devoidfury has done, and
will focus on making the rendering APIs async. I really don't want to
maintain 2 different APIs and split the code into sync and async versions.
I think the biggest pain point will be filters, since that's what most
people have written. We can probably detect when a filter is async, so that
we can allow both sync and async filters, which will solve that problem.


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-21371373
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@jlongster
Copy link
Contributor

I've hit an edge case that won't be possible without converting all for loops to be async:

{% for i in x %}
  {% block foo %}...{% endblock %}
{% endfor %}

That's actually possibly right now, but only because everything is properly implemented without concerns of each other. This isn't possible when async because rendering blocks are async, and the for loop is going to stay sync. It's a really weird thing to do anyway, so I think we should drop support for it.

@boutell
Copy link

boutell commented Jul 24, 2013

What is the expected behavior there? The last iteration's value for the
block is the lucky winner?

On Wed, Jul 24, 2013 at 5:08 PM, James Long notifications@github.comwrote:

I've hit an edge case that won't be possible without converting all for
loops to be async:

{% for i in x %}
{% block foo %}...{% endblock %}
{% endfor %}

That's actually possibly right now, but only because everything is
properly implemented without concerns of each other. This isn't possible
when async because rendering blocks are async, and the for loop is going
to stay sync. It's a really weird thing to do anything, so I think we
should drop support for it.


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-21516261
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@jlongster
Copy link
Contributor

You could actually override the block in a sub-template and it would just be evaluated like normal for every iteration. I don't think anyone does this (nor should they), but it used to just work.

I actually think I've hit some serious problems with for loops, and I might have to make them async. Still looking into it.

@jlongster
Copy link
Contributor

Just an update. I've converted most things to be async. I'm having trouble with macros though, because when you call a macro, I don't know if it's a macro or normal function at compile time (so I don't know if I should generate async code or not). It'd be easy to track macro names, except you can do {% import "foo.html" as foo %} and then I'd have to scan for every foo.* call. It's possible, just annoying.

The much bigger issue I've hit is the performance degradation. On average there's a 25-30% perf hit across the board for being async. I think it's mainly because I ended up having to make loops async, so they aren't a normal C-style loop anymore. There's also a lot more function calls in general.

That's not acceptable to me. I think I'm going to change my approach and make async behavior "opt-in". The high-level API will always be async, but within nunjucks you'll have to be more explicit about when you are doing async stuff. This probably means a different for tag that iterates asynchronously. This also nicely leads into allowing different async behavior, such as a tag that is like for but runs in parallel as well as async. Being more explicit about stuff like this will make it clearer what's going on in the templates, as well as let me optimize things so that a mostly synchronous template will stay mostly synchronous and performant.

(another example: I think I'm going to force devs to tell the compiler which filters/extensions are async, so that it knows at compile-time what to do)

@boutell
Copy link

boutell commented Aug 2, 2013

I think this makes a lot of sense for something that runs as intensively on
every page render as a template engine.

I recently ported an old mandelbrot set plotter from Java to JavaScript to
see how good JavaScript performance has gotten. I was doing an async
callback once per scanline - so that was maybe 600 pixels worth of really
heavy math per callback - and I was disappointed at how slow it was.

I spoke with some optimization experts who told me to skip the async
callback and just plot the whole set in one loop. The performance jumped to
nearly C levels (although it's usually more than like 1/5th the speed of C
on code that isn't so optimizer-friendly).

When compared to threads and I/O is involved, async is indeed fast. But in
truly computational situations where the whole job can be done really
quickly in any case, async is a big penalty.

On Thu, Aug 1, 2013 at 7:33 PM, James Long notifications@github.com wrote:

Just an update. I've converted most things to be async. I'm having trouble
with macros though, because when you call a macro, I don't know if it's a
macro or normal function at compile time (so I don't know if I should
generate async code or not). It'd be easy to track macro names, except you
can do {% import "foo.html" as foo %} and then I'd have to scan for every
foo.* call. It's possible, just annoying.

The much bigger issue I've hit is the performance degradation. On average
there's a 25-30% perf hit across the board for being async. I think it's
mainly because I ended up having to make loops async, so they aren't a
normal C-style loop anymore. There's also a lot more function calls in
general.

That's not acceptable to me. I think I'm going to change my approach and
make async behavior "opt-in". The high-level API will always be async, but
within nunjucks you'll have to be more explicit about when you are doing
async stuff. This probably means a different for tag that iterates
asynchronously. This also nicely leads into allowing different async
behavior, such as a tag that is like for but runs in parallel as well as
async. Being more explicit about stuff like this will make it clearer
what's going on in the templates, as well as let me optimize things so that
a mostly synchronous template will stay mostly synchronous and performant.

(another example: I think I'm going to force devs to tell the compiler
which filters/extensions are async, so that it knows at compile-time what
to do)


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-21977427
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@mattbasta
Copy link
Contributor

In most cases, async patterns ruin the interpreter's ability to optimize basically anything that the code does because types can't be determined (or narrowed down) statically. In synchronous patterns, return types can be inferred (or aren't needed) and type checks can be eliminated in many places. That's not the case in async code, and most of the time code in nested callbacks will back on completely unoptimized bytecode (in spidermonkey, you'd fall back on the raw AST interpreter since it's faster to run against the AST than it is to keep profiling for IonMonkey).

Especially with string concatenation (the vast majority of what nunjucks does), the JS interpreter can do a ton of under-the-hood optimizations when the code is synchronous. I think in any circumstance where nunjucks isn't calling out to non-nunjucks (unjucked?) code, it will be faster to simply be synchronous and exposed as asynchronous.

The only time where async code would be conceivably faster than synchronous code is where user-provided globals, filters, and functions in the context take substantial amounts of time to execute (DB calls, heavy async computation, calls to other processes, etc.).

It'd be easy to track macro names, except you can do {% import "foo.html" as foo %} and then I'd have to scan for every foo.* call. It's possible, just annoying.

I'd be curious if, during precompilation, you could actually open that file (precompiled templates will always exist) and just look it up. If you do a basic first pass before you generate any of the output, you could get the list of exported macros and blocks.

Maybe the answer in the short term is to make async available only to precompiled templates? You could do a lot more at compile time to ease the burden on the runtime. Perhaps you could set some kind of __async property on those functions (a la keywords) to trigger them asynchronously. If you do multiple passes in precompilation, you could probably figure out whether making a blob of code async is beneficial and only asyncify the bits that need to be async. I don't know if that's a good plan, though.

@jlongster
Copy link
Contributor

I'll reply more tomorrow, but note that the only reason I'm interested in async is to allow more powerful interactions with async-only libraries, like db/http/fs calls, etc. I am definitely not doing it for performance, and I knew there would be a hit, I just didn't know how much.

@faceleg
Copy link
Contributor

faceleg commented Aug 2, 2013

I really like the suggestion to make it more obvious whether an action is going to be sync or async, and being required to specify whether a component is sync/async.

Explicit declarations all the way.

Michael Robinson
mike@pagesofinterest.net
http://pagesofinterest.net/

On 2/08/2013, at 11:33 AM, James Long notifications@github.com wrote:

Just an update. I've converted most things to be async. I'm having trouble with macros though, because when you call a macro, I don't know if it's a macro or normal function at compile time (so I don't know if I should generate async code or not). It'd be easy to track macro names, except you can do {% import "foo.html" as foo %} and then I'd have to scan for every foo.* call. It's possible, just annoying.

The much bigger issue I've hit is the performance degradation. On average there's a 25-30% perf hit across the board for being async. I think it's mainly because I ended up having to make loops async, so they aren't a normal C-style loop anymore. There's also a lot more function calls in general.

That's not acceptable to me. I think I'm going to change my approach and make async behavior "opt-in". The high-level API will always be async, but within nunjucks you'll have to be more explicit about when you are doing async stuff. This probably means a different for tag that iterates asynchronously. This also nicely leads into allowing different async behavior, such as a tag that is like for but runs in parallel as well as async. Being more explicit about stuff like this will make it clearer what's going on in the templates, as well as let me optimize things so that a mostly synchronous template will stay mostly synchronous and performant.

(another example: I think I'm going to force devs to tell the compiler which filters/extensions are async, so that it knows at compile-time what to do)


Reply to this email directly or view it on GitHub.

@faceleg
Copy link
Contributor

faceleg commented Aug 2, 2013

You're right - I'm rooting for async ONLY for my custom blocks - I want to pull content from the db. Everything else is non-blocking so async is unnecessary overhead there.

Michael Robinson
mike@pagesofinterest.net
http://pagesofinterest.net/

On 2/08/2013, at 12:05 PM, Tom Boutell notifications@github.com wrote:

I think this makes a lot of sense for something that runs as intensively on
every page render as a template engine.

I recently ported an old mandelbrot set plotter from Java to JavaScript to
see how good JavaScript performance has gotten. I was doing an async
callback once per scanline - so that was maybe 600 pixels worth of really
heavy math per callback - and I was disappointed at how slow it was.

I spoke with some optimization experts who told me to skip the async
callback and just plot the whole set in one loop. The performance jumped to
nearly C levels (although it's usually more than like 1/5th the speed of C
on code that isn't so optimizer-friendly).

When compared to threads and I/O is involved, async is indeed fast. But in
truly computational situations where the whole job can be done really
quickly in any case, async is a big penalty.

On Thu, Aug 1, 2013 at 7:33 PM, James Long notifications@github.com wrote:

Just an update. I've converted most things to be async. I'm having trouble
with macros though, because when you call a macro, I don't know if it's a
macro or normal function at compile time (so I don't know if I should
generate async code or not). It'd be easy to track macro names, except you
can do {% import "foo.html" as foo %} and then I'd have to scan for every
foo.* call. It's possible, just annoying.

The much bigger issue I've hit is the performance degradation. On average
there's a 25-30% perf hit across the board for being async. I think it's
mainly because I ended up having to make loops async, so they aren't a
normal C-style loop anymore. There's also a lot more function calls in
general.

That's not acceptable to me. I think I'm going to change my approach and
make async behavior "opt-in". The high-level API will always be async, but
within nunjucks you'll have to be more explicit about when you are doing
async stuff. This probably means a different for tag that iterates
asynchronously. This also nicely leads into allowing different async
behavior, such as a tag that is like for but runs in parallel as well as
async. Being more explicit about stuff like this will make it clearer
what's going on in the templates, as well as let me optimize things so that
a mostly synchronous template will stay mostly synchronous and performant.

(another example: I think I'm going to force devs to tell the compiler
which filters/extensions are async, so that it knows at compile-time what
to do)


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-21977427
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

Reply to this email directly or view it on GitHub.

@jlongster
Copy link
Contributor

I'd be curious if, during precompilation, you could actually open that file (precompiled templates will always exist) and just look it up. If you do a basic first pass before you generate any of the output, you could get the list of exported macros and blocks.

Maybe the answer in the short term is to make async available only to precompiled templates?

I like where you are going, but I think that's too limiting. I can see that being very confusing to a new developer to nunjucks, and it would be hard to explain why. However, I'm all for adding limitations to async code. I think only top-level filters/tags will be able to be async, because you can't do anything async underneath a synchronous form. This solves a lot of problems (macros won't be able to do async stuff, there will be async for/if variants, most code continues to be sync, etc)

@boutell
Copy link

boutell commented Aug 2, 2013

Can you include a file from inside a 'for' or 'if'? Would that affect this
plan?

On Fri, Aug 2, 2013 at 9:24 AM, James Long notifications@github.com wrote:

I'd be curious if, during precompilation, you could actually open that
file (precompiled templates will always exist) and just look it up. If you
do a basic first pass before you generate any of the output, you could get
the list of exported macros and blocks.

Maybe the answer in the short term is to make async available only to
precompiled templates?

I like where you are going, but I think that's too limiting. I can see
that being very confusing to a new developer to nunjucks, and it would be
hard to explain why. However, I'm all for adding limitations to async code.
I think only top-level filters/tags will be able to be async, because you
can't do anything async underneath a synchronous form. This solves a lot of
problems (macros won't be able to do async stuff, there will be async
for/if variants, most code continues to be sync, etc)


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-22005174
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@jlongster
Copy link
Contributor

Can you include a file from inside a 'for' or 'if'? Would that affect this plan?

That's a good point, but yes, you should be able to. Every include and other forms that fetches templates will generate async-style code, but if the template loaders are configured to be synchronous then it should run synchronously. Which leads me to another point: since we are doing async explicitly now, we should be able to make the template loaders async or sync, depending on how you configure it.

@boutell
Copy link

boutell commented Aug 3, 2013

Three cheers for being explicit - that makes my job a lot easier since
we're pretty far down the "achieve model/view separation by requiring
resources to be completely mustered before invoking a template" road.

On Fri, Aug 2, 2013 at 9:53 AM, James Long notifications@github.com wrote:

Can you include a file from inside a 'for' or 'if'? Would that affect this
plan?

That's a good point, but yes, you should be able to. Every include and
other forms that fetches templates will generate async-style code, but if
the template loaders are configured to be synchronous then it should run
synchronously. Which leads me to another point: since we are doing async
explicitly now, we should be able to make the template loaders async or
sync, depending on how you configure it.


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-22006872
.

Tom Boutell
P'unk Avenue
215 755 1330
punkave.com
window.punkave.com

@jlongster
Copy link
Contributor

@boutell Yeah, and most people really should do it that way. Async-ness was a featured requested enough that I think we'll see some neat applications of it, but it's bad practice to start loading everything in the template.

(side node: you referenced the problem that nunjucks can't include relative templates, and after thinking about it a bit more I'm open to the idea as long as it doesn't complicate precompiled templates too much. feel free to file an issue for it)

@jlongster
Copy link
Contributor

k guys. I think the async work is stabilizing.

As I said before, the default in nunjucks is still synchronous. However, the internals have been refactored a bit to allow you to introduce async behavior, but it comes with some caveats. The result is that the performance is basically the same, but you can still use async in most places.

Right now, the key part is filters. You can introduce async filters like this:

var fs = require('fs');
var env = new nunjucks.Environment();

env.addFilter('getContents', function(path, cb) {
    fs.readFile(path, cb);
}, true);

Note the last argument true. You need to pass this to addFilter to tell nunjucks that it's async; otherwise it will be called synchronously. Yes, this means at compile-time nunjucks needs to know this, so when precompiling templates you need to pass it a list of async filters (I haven't implemented that yet).

You can just use it normally in a template:

{{ "path/to/file/this/is/a/bad/example" | getContents }}

Now for the caveats: you can't use anything async inside the if and for statements. Those are still synchronous for performance reasons. I had to introduce two new tags: ifAsync and forAsync. Use if like normal:

{% ifAsync tmpl %}
  {{ tmpl | getContents }}
{% endif %}

This has a nice feature where it is obvious where async stuff is happening, too. Note that you still close it with normal endif and endfor. (is that confusing? should I use endifAsync?)

The last thing is template loaders. Bear with me here. Those can be async, too, but adding an async: true field on the loader. However, the FileSystemLoader is going to stay synchronously. This is because if you are using an async loader, you have to use the ifAsync and forAsync blocks whenever you use include inside of them. There's really no harm in keeping FileSystemLoader sync, since it's always cached and it just happens once.

HOWEVER, the important change is that FileSystemLoader now uses fs.watch to receive events when files change, and it invalidates the cache when it does (https://github.com/jlongster/nunjucks/blob/async/src/node-loaders.js#L26). You can turn off the watcher completely by passing false as the second argument, like new FileSystemLoader('views', false), so in production it never checks if templates have changed and there are 0 stat calls.

So, the very original minor issue of converting to fs.watch has been fixed. In addition, I completely re-hauled the internals of the compiler and implemented an AST transformer to generate async-friendly code.

tccb8onohn

@jlongster
Copy link
Contributor

Oh, forgot to mention that I've converted all the tests to use the async API and they are all passing. The render function will always look async: tmpl.render(ctx, function(err, res) {}, but it will execute synchronously if don't do anything async.

@jlongster
Copy link
Contributor

Now that I think about it, since I have the list of async filters at compile-time, it wouldn't be that hard to convert for and if into the async equivalents if async filters are used inside of them. Why don't we just do that.

@boutell
Copy link

boutell commented Aug 6, 2013

I think that is best since frontend devs would have a heck of a time
learning the ropes otherwise. It should be possible for someone who is not
a node dev to write the templates correctly.

Still wondering what happens if a macro uses a filter and contains if
statements. Or are they true macros, expanding to their source code with
the arguments substituted in a first pass, so that is a nonissue?
On Aug 5, 2013 7:04 PM, "James Long" notifications@github.com wrote:

Now that I think about it, since I have the list of async filters at
compile-time, it wouldn't be that hard to convert for and if into the
async equivalents if async filters are used inside of them. Why don't we
just do that.


Reply to this email directly or view it on GitHubhttps://github.com/jlongster/nunjucks/issues/41#issuecomment-22147394
.

@jlongster
Copy link
Contributor

Most people won't use async and won't care about any of this, I think, so the burden is placed on those who want to do this. However, it would be nice to get rid of ifAsync and forAsync and I think at this point it wouldn't be too hard to conditionally compile them.

Still wondering what happens if a macro uses a filter and contains if statements. Or are they true macros, expanding to their source code with the arguments substituted in a first pass, so that is a nonissue?

Macros don't allow async expressions in them, because macros look like function calls and I can't reliable detect whether or not to call it async or not (if you passed in normal functions, they should be called sync). I don't think this will be a big deal. You shouldn't be doing that much async stuff anyway.

@jlongster
Copy link
Contributor

If and For are automatically transformed to their async variants now if the compiler can detect it should do that. There are some cases where it can't detect it (like including other templates inside a for loop), so the user must manually use *Async, but this will be documented.

Lots of tests now: https://github.com/jlongster/nunjucks/blob/async/tests/compiler.js#L210

@jlongster
Copy link
Contributor

I'm pretty much done here. Precompiled templates and extensions can't use async yet, but that wouldn't be hard to add. I'll do that later.

Can someone volunteer to test the async branch with some complicated templates? I want to make sure there are no regressions. It'd be great if you want to write some async filters, too.

@mkoryak
Copy link

mkoryak commented Aug 8, 2013

A while ago I played around with http chunked responses to make some pages load seemingly "faster" without ajax. I used the usual method of sending back scripts that would pop rendered portlets into place on the page.
I tried to come up with a "framework" where you would pass a bunch of render functions into the nunjucks template and call them to render async portions. It was difficult to do async loading in loops and the whole thing was pretty unusable

I haven't been following this thread very closely, but I wonder what your thoughts are on using this with http 1.1 chunked?

@faceleg
Copy link
Contributor

faceleg commented Aug 8, 2013

I also have a question: am I now able to make my custom blocks async?

@jlongster
Copy link
Contributor

@mkoryak The internal infrastructure is there to do anything async, so you could kind of do what you are talking about. I carefully exposed the async control, and we can tweak how it works over time depending on use cases. If you are wanting to control the output buffer, you can't do that, but I would be open to discussing how we can make that work.

@faceleg You definitely will be able to, but not yet. I haven't exposed async control to the extensions but it will be easy since the internal code is ready for it.

@jlongster
Copy link
Contributor

@faceleg Ok, I implemented async extensions. Do you mind helping test it?

First, make sure your existing sync extensions still work. Then, test the new async work. Instead of returning CallExtension, you can now return CallExtensionAsync, and it will call your extension with a callback.

Here's an example:

function FooExtension() {
    this.tags = ['foo'];
    this._name = 'FooExtension';

    this.parse = function(parser, nodes) {
        var tok = parser.nextToken();
        var args = parser.parseSignature(null, true);
        parser.advanceAfterBlockEnd(tok.value);

        var body = parser.parseUntilBlocks('endfoo');
        parser.advanceAfterBlockEnd();

        return new nodes.CallExtensionAsync(this, 'run', args, [body]);
    };

    this.run = function(context, baz, body, cb) {
        cb(null, 'baz: ' + baz + ', body:' + body());
    };
}
{% foo "baz" %} some content here {% endfoo %}

One thing to note is the content functions, like body. You can also call them async style, which you must do it you do anything actually async inside of them:

    this.run = function(context, baz, body, cb) {
        body(function(err, res) {
            cb(null, 'baz: ' + baz + ', body:' + res);
        }
    };

@faceleg
Copy link
Contributor

faceleg commented Aug 10, 2013

I can't wait to start working with this, hopefully later tonight, definitely within the next few days. Will report back.

@jlongster
Copy link
Contributor

I've tweaked the loop constructs. We should go ahead and provide to async iteration primitives, one which iterates sequentially and one which executes all the items in parallel. I renamed forAsync to be asyncEach and introduced asyncAll. asyncEach provides an async way to iterate over an array/obj one step at a time, and asyncAll executes all the items in parallel.

Depending on the async functionality you are using inside the loop, you choose which one you want. I think most of the time you probably want asyncAll.

This will be clearly documented.

@faceleg
Copy link
Contributor

faceleg commented Aug 26, 2013

@jlongster works like a charm, thank you so much!

@jlongster
Copy link
Contributor

This has been released in 1.0, and the node loader now uses fs.watch.

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

No branches or pull requests