SomMeri / less4j

Less language is an extension of css and less4j compiles it into regular css. Less adds several dynamic features into css: variables, expressions, nested rules, and so on. Less was designed to be compatible with css and any correct css file is also correct less file.
145 stars 47 forks source link

Add support for caching AST(s) #315

Closed a701440 closed 8 years ago

a701440 commented 8 years ago

When compiling a large number of modules that all use the same @import(s) each time each import is parsed with ANTLR again and again. Would like to have a way to plugin a Cache implementation to cache ASTs for shared imports and not parse those again.

SomMeri commented 8 years ago

May I ask how many modules do you compile? I am curious about how less4j is used in practice with that question.

a701440 commented 8 years ago

In some cases there up to 30-100 top level imports in a less file and those can be importing 3-5 modules of their own, can potentially be up to 5-10 levels of imports. The problem is that all 30 top level ones can import the same library module. I would be glad to profile custom builds and supply call stacks for problematic areas and anything else that can be helpful. Performance is very important to us. The number of overall modules we may compile could be 2000 - 10000.

a701440 commented 8 years ago

I think it would be sufficient to allow the caller to provide an implementation of some ICache interface via Compiler.Configuration that would be used instead of the "astCache" in the SingleImportSolver when provided.

SomMeri commented 8 years ago

That sounds good, I like it. SingleImportSolver could just replace astCache by whatever is in configuration. The best would be if the interface would leak as little from implementation as possible, so that cache format can be changed later on.

a701440 commented 8 years ago

I made a custom build with the "external" cache that is reused across multiple compile calls (using ConcurrentHashMap to test). I got approximately 2x speed-up. Our environment is probably not typical though - we compile thousands of modules with a large number of shared imports. One small issue is the LessSource is used as the key there. It means that all sub-classes of LessSource must have a good hashCode and equals implementation which is not the case currently - I had to fix that too. In my case I made the Cache interface use Object key and Object value and had to cast inside.

SomMeri commented 8 years ago

LessSource with bad hashCode was one of default ones or something you use? If it is something in less4j, I would fix it.

On another topic, thank you for profiling less4j. I want it to be fast and real world test case is always better then made up one.

a701440 commented 8 years ago

Actually this was my subclass of LessSource - it is not clear that a subclass must implement hashCode and equals - Without it cache does not work and @import(once) does not work as well.

SomMeri commented 8 years ago

Fixed by #320

sherrybomb commented 8 years ago

Is there an example of using an external cache?

twhoff commented 7 years ago

@sherrybomb - a little bit of a late reply, but good to have a reference at any point. The way we have implemented using ehcache is as follows:

In our LessEngine implementation, we implement the Cache interface like so:

        cache = new Cache() {
            @Override
            public Object getAst(LessSource key) {
                try {
                    String lessContent = ((LessContent) key).getContent();
                    return localCache.get(lessContent);
                } catch (Exception e) {
                }
                return null;
            }
            @Override
            public void setAst(LessSource key, Object value) {
                try {
                    String lessContent = ((LessContent) key).getContent();
                    localCache.put(lessContent, value);
                } catch (Exception e) {
                }
            }

localCache is our ehcache manager instance (persistent within our application).

This is then added to the LESS4J compiler as a configuration option:

// LESS4J Default compiler
LessCompiler compiler = new DefaultLessCompiler();

// LESS4J configuration object
Configuration options = new Configuration();
options.setCache(cache);

// Compiler call
CompilationResult result = compiler.compile(<LessSource object goes here>, options);

It seems inefficient to me to store the entire LESS file contents as a key, but this what the LESS4J compiler expects.

We have seen dramatic performance improvements regarding runtime LESS compilation, using this caching.

I should note however, that runtime LESS compilation is not a good idea. It consumes a lot of memory and can become very unstable.

I think a better approach would be to use this cache for a build-time or save-time implementation of the compiler - which would use a file watcher to compile individual files and share things like framework resources using the cache between each compilation run.