2
\$\begingroup\$

There is a function that draws triangles on the canvas area in each lines:

function DivotTemplate(divot, canvas) {
    if (divot === void 0) { divot = defaultConfig; }
    var config = __assign(__assign({}, defaultConfig), divot);
    var ctx = canvas.getContext('2d');
    if (!ctx)
        throw Error('Canvas context error');
    var width = canvas.width;
    var height = canvas.height;
    var step = config.step, radius = config.radius, strokeStyle = config.strokeStyle, triangleWidth = config.triangleWidth;
    var x = [1, 1, triangleWidth + 1];
    var y = [1, triangleWidth, triangleWidth / 2];
    var context = canvas.getContext('2d');
    if (!context)
        throw Error('Canvas context error');
    /* Рисуем три точки для первого треугольника */
    for (var c = 0; c < height; c++) {
        if (c % 2 === 0) {
            x[0] = triangleWidth + 1;
            x[1] = triangleWidth + 1;
            x[2] = 1;
        }
        else {
            var s = step / 2;
            x = [1 + s, 1 + s, triangleWidth + 1 + s];
        }
        for (var i = 0; i < width; i = i + step) {
            context.beginPath();
            context.arc(x[0], y[0], radius, 0, 2 * Math.PI, false);
            context.strokeStyle = strokeStyle;
            context.stroke();
            context.beginPath();
            context.arc(x[1], y[1], radius, 0, 2 * Math.PI, false);
            context.strokeStyle = strokeStyle;
            context.stroke();
            context.beginPath();
            context.arc(x[2], y[2], radius, 0, 2 * Math.PI, false);
            context.strokeStyle = strokeStyle;
            context.stroke();
            /* Соединяем две точки из трех */
            context.beginPath();
            context.moveTo(x[2], y[2]);
            context.lineTo(x[0], y[0]);
            context.strokeStyle = strokeStyle;
            context.stroke();
            context.beginPath();
            context.moveTo(x[2], y[2]);
            context.lineTo(x[1], y[1]);
            context.strokeStyle = strokeStyle;
            context.stroke();
            x[0] = x[0] + step;
            x[1] = x[1] + step;
            x[2] = x[2] + step;
        }
        x = [1, 1, triangleWidth + 1];
        y[0] = y[0] + step;
        y[1] = y[1] + step;
        y[2] = y[2] + step;
    }
    return canvas;
}

I measured the performance of this function and get bad indicators (pic. 1)

Timeline display for code

How it can be improved?

Why context.strokeStyle = strokeStyle; takes over 4 seconds?

Maybe pull out context.beginPath(); outside of loop? Or combine some operators? How to optimize canvas drawing?

\$\endgroup\$
5
\$\begingroup\$

2D canvas performance

You are running on a VERY slow system. Make sure you are not running any extension when measuring performance.

Avoid GPU state changes

The biggest performance penalty when using the 2D canvas API are state changes.

State changes are when you change rendering settings (state) such as the color. The state needs to be moved from the CPU to the GPU which means shutting down many systems so that the main Address and Data Bus can communicate with the GPU Address and Data Bus. Not only does this slow your code, but all processes are effected.

You can see the penalty on lines 44, 48, 52, 58, and 63, where you set context.strokeStyle = strokeStyle; (approx 400ms) This penalty is forced as your styles are all identical (The 2D API does not check if the style needs to be changed)

The most basic fix is to batch all render calls using the same style as one path operation. ctx.beginPath(); /* ... build path ... */; ctx.stroke();

Note that building a path does not require GPU communication until the render function is called. ctx.stroke or ctx.fill

Avoid exceptions.

Using try, catch, or throw in JS will mark the code (and any code called, or in the same scope) as Do not optimise. Meaning that the optimiser does not get to improve the performance.

Avoid arrays

Indexing into arrays is slower than using variables directly.

Creating an array is much slower than assigning variables directly. Up to a point, for 3 items its best to use variables, for 12 items then an array is better, but still avoid creating a new array, reuse the existing array.

General points

Avoid repeated calculations. Especially true if you have the code marked Do not optimise

Examples...

  • x = [1, 1, triangleWidth + 1]; at bottom of outer loop is not needed.

  • context.arc(x[2], y[2], radius, 0, 2 * Math.PI, false); Pre-calculate 2.PI. Use the defaults, eg the last argument false is the default and thus not needed.

  • You get the 2D context twice. Only needed once.

  • For ctx.arc if radius is less than 2px use ctx.rect(x - radius. y- radius, diameter, diameter) where diameter is calculated outside the loops as const diameter = radius * 2

    ctx.rect is much quicker than ctx.arc and at <2px radius they a visually very similar. Note when using ctx.rect reducing the radius by 0.8 will more closely approximate the visual density of the arc.

  • You are rendering 2 connected line segments using two moveTo calls. They are connected and as such only need one moveTo (see rewrite)

Rewrite

The following rewrite will be a huge improvement in performance. My estimate is at least twice as fast.

function DivotTemplate(divot = {}, canvas) {
    const TAU = 2 * Math.PI;
    const config = {
        ...defaultConfig,
        ... divot
    };
    const ctx = canvas.getContext('2d');
    const width = canvas.width, height = canvas.height;
    const step = config.step;
    const radius = config.radius;
    const strokeStyle = config.strokeStyle;
    const triangleWidth = config.triangleWidth;
    
    var x0 = 1, x1 = 1, x2 = triangleWidth + 1;
    var y0 = 1, y1 = triangleWidth, y2 = triangleWidth / 2;
    var c, s, i;
    
    /* One state change */
    ctx.strokeStyle = strokeStyle;
    
    /* Build one path */
    ctx.beginPath();

    for (c = 0; c < height; c++) {
        if (c % 2 === 0) {
            x0 = triangleWidth + 1;
            x1 = triangleWidth + 1;
            x2 = 1;
        } else {
            s = step / 2;
            x0 = 1 + s;
            x1 = 1 + s;
            x2 = triangleWidth + 1 + s;
        }
        for (i = 0; i < width; i += step) {
            ctx.moveTo(x0 + radius, y0);            
            ctx.arc(x0, y0, radius, 0, TAU);

            ctx.moveTo(x1 + radius, y1);
            ctx.arc(x1, y1, radius, 0, TAU);

            ctx.moveTo(x2 + radius, y2);
            ctx.arc(x2, y2, radius, 0, TAU);

            ctx.moveTo(x0, y0);
            ctx.lineTo(x1, y1);
            ctx.lineTo(x2, y2);

            x0 += step;
            x1 += step;
            x2 += step;
        }
        
        y0 + =step;
        y1 += step;
        y2 += step;
    }
    
    /* One render call */
    ctx.stroke();
    return canvas;
}

Multiple calls

If the function DivotTemplate is called many times (with the same settings) move setup, beginPath, and stroke out of the function and build one path for the many DivotTemplate calling stroke when all done.

Render pixels directly

It is unclear what the various values of divot are but if step is near 1 px then the whole function should write pixels via a buffer and avoid the 2D context path calls altogether.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.