mathjax / MathJax-node

MathJax for Node
Apache License 2.0
615 stars 97 forks source link

So slow on `require('mathjax-node')` #259

Closed Menci closed 8 years ago

Menci commented 8 years ago

It always costs 1s ~ 2s on require('mathjax-node') on my app initialization..

dpvc commented 8 years ago

You do not give enough detail to comment on your issue, and I am not able to reproduce your situation (of course, timing depends greatly on the speed of you processor and the other activity going on on your computer at the time).

The code

var start = new Date().getTime();
var mathjax = require('mathjax-node');
var end = new Date().getTime();
console.log(end-start);

reports times on the order of 480ms (less than half a second). Most of this time is due to mathjax-node loading jsdom, which is a fairly heavy dependency. A similar program that loads jsdom directly reports times around 375ms. So jsdom accounts for about 78% of mathjax-node's load time.

Menci commented 8 years ago

So could I replace jsdom with the native dom in electron?

dpvc commented 8 years ago

Probably, but if you are working in an environment with an actual DOM, then you probably should use plain MathJax rather than MathJax-node, which is intended for use as a server-side tool.

Menci commented 8 years ago

Oh, are there an API like mj.typeset in plain MathJax to render one string?

dpvc commented 8 years ago

The math has to be part of the page to be rendered in MathJax in the browser, but yes, there is an API to make calls for individual expressions to be typeset within the document. If you wanted to make something that accepted a string and typeset that, you would need to make a wrapper function that inserted the string into the page, typeset that, then removed the result and returned it. It is probably better just to put the math where you want it to be and have MathJax render it there.

Menci commented 8 years ago

I want to modify the mj-single.js to make it work in a DOM. I replaced jsdom with a iframe, and solved the "Array" problem. But now the StartupHook End won't be called and all typeset won't work.

Menci commented 8 years ago

I try to use plain MathJax, but the rendering speed is 3 times slower than MathJax-node, here is my code:

static render(str, display, cb, info) {
    let div = document.createElement('div');
    div.innerText = (display ? '$$' : '$') + str + (display ? '$$' : '$');
    div.style.display = 'none';
    document.body.appendChild(div);
    MathJax.Hub.Queue(['Typeset', MathJax.Hub, div]);
    MathJax.Hub.Queue(() => {
        let res = div.querySelector('svg').outerHTML;
        if (display) {
            res = '<div style="width: 100%; text-align: center">' + res + '</div>';
        }
        cb(res, info);
        document.body.removeChild(div);
    });
}

And

static render(str, display, cb, info) {
    mathjax.typeset({
        math: str,
        format: display ? "TeX" : "inline-TeX",
        svg: true,
        width: 0
    }, function (data) {
        var res = data.errors ? data.errors.toString() : data.svg;
        if (display) {
            res = '<div style="width: 100%; text-align: center">' + res + '</div>';
        }

        rendered.set((display ? 'd' : 'i') + str, res);
        cb(res, info);
    });
}

What's wrong?

dpvc commented 8 years ago

What's wrong?

It's hard to tell without seeing the full code (including the method you are using to determine your timing), as things like the configuration that you are using can affect the situation, and exactly what you are timing makes a difference. I suspect that the main issue is that your in-browser test is including all the MathJax loading and startup in you in-browser version, but not in the mathjax-node version. But I can't tell from what you have provided.

Note that MathJax loads components as they are needed; for example, it will load the output jax at the time the first expression is processed, so if you are timing only one expression in the browser, you may well be including all the time involved in loading the render and other components. For example, if I use your code in the browser for a single example equation, it took 750 ms, but subsequent renderings of the same equation took only 230ms (the initial equation was a factor of 3 longer). Note that mathjax-node processes a dummy equation during its start up in order to force the renderer and other components to load, so that time is probably not included in your timing of the mathjax-node version.

Another possible source of differences would be if you are loading your browser test page over the network rather than through a file:// URL. In this case, loading components will involve network access (slow), compared to local file access (fast) in mathjax-node.

Menci commented 8 years ago

I typeset many equations, MathJax-node use d 3s and MathJax used 12s.

I'll provide a demo with election to reproduce it.

On Saturday, 27 August 2016, Davide P. Cervone notifications@github.com wrote:

What's wrong?

It's hard to tell without seeing the full code (including the method you are using to determine your timing), as things like the configuration that you are using can affect the situation, and exactly what you are timing makes a difference. I suspect that the main issue is that your in-browser test is including all the MathJax loading and startup in you in-browser version, but not in the mathjax-node version. But I can't tell from what you have provided.

Note that MathJax loads components as they are needed; for example, it will load the output jax at the time the first expression is processed, so if you are timing only one expression in the browser, you may well be including all the time involved in loading the render and other components. For example, if I use your code in the browser for a single example equation, it took 750 ms, but subsequent renderings of the same equation took only 230ms (the initial equation was a factor of 3 longer). Note that mathjax-node processes a dummy equation during its start up in order to force the renderer and other components to load, so that time is probably not included in your timing of the mathjax-node version.

Another possible source of differences would be if you are loading your browser test page over the network rather than through a file:// URL. In this case, loading components will involve network access (slow), compared to local file access (fast) in mathjax-node.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/mathjax/MathJax-node/issues/259#issuecomment-242827346, or mute the thread https://github.com/notifications/unsubscribe-auth/AK0QgxoPrNUxWYnRrY25O5H4a6BDIBl8ks5qjzxigaJpZM4Jj4-S .

Menci commented 8 years ago

It's my demo.

git clone https://github.com/Menci/electron-playground
cd electron-playground
npm install
npm start

And click MathJax or MathJax-node to see the rendering and time.

dpvc commented 8 years ago

The problem is that you are using MathJax in the browser in the most inefficient way possible: you typeset each equation individually, and display each one immediately after it is typeset. That means that each equation causes (several) page reflows, and page reflows in the browser that are a source of considerable performance degradation, so it is best to minimize these as much as possible. MathJax is designed to handle multiple equations efficiently, but you are forcing it to handle them one at a time, inefficiently.

If you change your index.js file so that the MathJax button is handled as follows:

    document.getElementById('button-mathjax').addEventListener('click', () => {
        view.innerHTML = '';
        const startTime = new Date();
        for (let s of equations) {
            let div = document.createElement('div');
            view.appendChild(div);
            div.innerText = '$$' + s + '$$';
        }
        MathJax.Hub.Queue(["Typeset", MathJax.Hub, view]);
        MathJax.Hub.Queue(() => {
            document.getElementById('title').innerHTML = 'MathJax Demo - Time: ' + (((new Date()).getTime() - startTime.g\
etTime()) / 1000).toString() + 's';
        })
    });

so that all the equations are typeset at one pass (minimizing the number of reflows), then the MathJax version should outperform the mathjax-node version. For me, your original code took 6.754 seconds, while the modified version ran in .824 sec. On subsequent runs (pushing the button again) it took .6 sec (the difference is due to loading the various components on the first time through, which doesn't have to be done on the later ones). In contrast, mathjax-node took 1.184 sec for the first run and around .83 sec for subsequent runs.

The change from 6 seconds to less than 1 second is due almost entirely to the reduction in page reflows between the two approaches, not to the work that MathJax does itself, which is nearly the same in both cases. Because mathjax-node is running in jsdom rather than the browser, there are no reflows, so it is not less efficient to process each equation individually.

So if you have many equations to process, you should handle them all at once, not individually.

Menci commented 8 years ago

Can I got 'the typeset has done' callback after each equation's typeset in this way?

dpvc commented 8 years ago

You can set up a message hook for the "New Math" signal, which will fire once for each equation as it is processed. E.g.,

MathJax.Hub.Register.MessageHook("New Math", msg => {
  let div = MathJax.Hub.getJaxFor(msg[1]).SourceElement().parentNode;
  ... do what you need to do with div ...
});

But be aware that if you do actions that will cause a page reflow during this process, you can degrade the performance again.

Otherwise, wait for the final callback (where you set the timing output) and that will tell you that all the math is processed.

Menci commented 8 years ago

Thanks! My problem has solved.

Now my app has 1s faster on startup.

On Saturday, 27 August 2016, Davide P. Cervone notifications@github.com wrote:

You can set up a message hook for the "New Math" signal, which will fire once for each equation as it is processed. E.g.,

MathJax.Hub.Register.MessageHook("New Math", msg => { let div = MathJax.Hub.getJaxFor(msg[1]).SourceElement().parentNode; ... do what you need to do with div ... });

But be aware that if you do actions that will cause a page reflow during this process, you can degrade the performance again.

Otherwise, wait for the final callback (where you set the timing output) and that will tell you that all the math is processed.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/mathjax/MathJax-node/issues/259#issuecomment-242923120, or mute the thread https://github.com/notifications/unsubscribe-auth/AK0Qg8Fx3t6mQkEbu-WkNLgMVoqqB2tsks5qkFWagaJpZM4Jj4-S .

tawe141 commented 8 years ago

@dpvc Please excuse my naivete, but how come jsdom is used in mathjax-node? I thought mathjax-node was for server-side rendering into MathML or SVG, hence no need for DOM manipulations.

pkra commented 8 years ago

@tawe141 mathjax-node is basically a wrapper around the core MathJax libraray, running in jsdom instead of a browser DOM.

Menci commented 8 years ago

What about replacing jsdom with PhantomJS? Maybe it'll be faster to render.

pkra commented 8 years ago

What about replacing jsdom with PhantomJS? Maybe it'll be faster to render.

There are tools that render math content using MathJax and PhantomJS.

But the point of mathjax-node is that it runs in pure NodeJS and does not require a browser-engine as a dependency.