Editor’s note: Today we invited @Youma to share his experience of using server to render large screen animation in a data visualization large screen project. When it comes to server rendering, we usually think of Vue SSR or React isomorphism, but animation can also be rendered on the server. So let’s get down to the basics and see how it works


introduce

ThinkJS is an enterprise-level server development framework based on [email protected]. In addition to the basic HTTP service, this project also uses the scheduled task and Websocket function.

Sprite.js is a cross-platform 2D drawing object model library, which supports multi-terminal drawing and animation of Web, Node, desktop application and wechat applet. Sprite.js uses Node-canvas for server-side rendering, which means we can use Sprite.js in a Node environment and save drawn graphics as PNG, or animation as GIF. The following features are used in the project in this article:

  • Scene (Scene) : Sprite.js implements layer management by creating Scene Scene;
  • Layer: Each Layer is a wrapped Canvas 2D object;
  • Sprite: A renderable object with a box model. Sprite. Js supports four Sprite types by default: Sprite, Label, Path and Group. Sprite is the most basic Sprite.

🤔 ️ doubt

Why canvas server rendering?

The requirements of this project is implemented at the peak of millions per hour real-time data display, in order to achieve the best display effect, and can back in the historical situation, we decided to use the front isomorphism, the server-side code, front-end animation display real-time data, the service side rendering data attack path at the same time, the specific strategies are as follows:

  • As a Websocket client, the server receives data from the upstream of WebSocket, uses Sprite.js to draw images, takes snapshots through ThinkJS scheduled tasks, and uploads images to CDN to save URLS.
  • At the same time, the server also acts as the Websocket server to filter the upstream data and send it to the front end. The front end will draw the received data to canvas in real time through sprite.js.
  • When tracing the historical situation, the front-end needs to request the server to obtain a historical snapshot. The server merges the snapshots within the request time into one, uploads them to the CDN, and returns the URL to the front end, which draws the URL to the canvas.

👀 a pit climb before development

The biggest of these is 😂 dug by Node-Canvas, and I crashed the server at one point (luckily it was just a Docker container).

Installation node – canvas

Node-canvas is a Canvas implementation that uses the Node.js environment supported by Cairo. Open its developer listing page and you’ll see the familiar name TJ Holowaychuk. These problems encountered at present are also found in the process of changing the server for many times, I hope you pay attention to, so as not to be pit cry later.

Missing precompiled binaries

Node-canvas is only used on the Node server, so it is not added to the sprite.js dependency, which requires us to manually install NPM I canvas@next into the project. By default, the latest version will be installed. At installation time it will download the corresponding binaries in the precompiled project, depending on the system architecture. If you experience the error shown in Figure 1, there are two solutions:

  1. To compile and install Node-Canvas, the official documentation clearly states the dependencies required by different operating systems.
  2. Install the most recent version of the precompiled binaries, currently [email protected];

The lack of GLIBC_2. 14

You may run into this problem again after solving the last one

Error: /lib64/libc.so.6: version `GLIBC_2.14` not found
Copy the code

There is no library for glibc_2.14 on the server.

GLIBC is the GNU distributed LIBC library, or C runtime. GLIBC is the lowest level API in Linux, and almost any other runtime relies on GLIBC. In addition to encapsulating the system services provided by the Linux operating system, GLIBC itself provides implementations of many other necessary functional services.

After reading this paragraph, you should know what level of dependency loss you are dealing with. Do a search and see how many people have reinstalled their systems because of it.

Check whether the system kernel supports glibc_2.14

strings /lib64/libc.so.6 | grep GLIBC

If the glibc_2.14 keyword does not exist in the result, you can try to solve the problem in either of the following ways:

  1. Add the GLIBC source on your operating system and install the corresponding version of GLIBC.
  2. Select an operating system whose version is GLIBC >= 2.14, such as CentOS 7.

If you do not find the source of the server kernel, do not try to compile and install the library, the operations staff said that some older kernels do not support glibc_2.14. Then read the following sentence:

Since GLIBC includes almost all of the common UNIX standards, you can expect everything. Like other UNIX systems, it contains archive groups scattered in a tree directory structure that acts as a scaffolding for the entire operating system.

So it’s best to go straight to the supported operating system.

Missing font file

Once node-Canvas is installed, you can test it with this code. If the text on the output image appears as a rectangle like the one below, this indicates that the system you are using lacks font files. You happen to have the need to render text, and you need to solve this problem.

A typical PC will have a lot of font files, but a server environment without an interface may lack font files, so you need to install at least one font, as described in this article. When finished, run the following code to generate an image, and if the text is displayed correctly, the font file has been successfully installed.

// label.js
const {Scene,Label} = require('spritejs');
const fs = require('fs');
const writeFileAsync = think.promisify(fs.writeFile, fs);

(function () {
  // Create scene and layer
  const scene = new Scene('#paper', {resolution: [1200.1200]});
  const fglayer = scene.layer('fglayer');
  
  // Create a label and set the properties
  const text1 = new Label('Hello World ! ');
  text1.attr({
    anchor: [0.5.0.5].pos: [600.600].font: 'bold 48px Arial'.color: '#ffdc45'});// Add the label to the layer and save the canvas as a picture
  fglayer.append(text1);
  await fglayer.prepareRender();
  await writeFileAsync('spritejs.png', fglayer.canvas.toBuffer()); } ());Copy the code

🌵 Server rendering

The key operation of canvas rendering is image output and merging. Understanding and flexible application of these two processes can satisfy most scenes rendered by Canvas server.

Image output

In this project’s scenario, the server gets the new data and creates a Sprite and adds it to the layer. The Sprite should only do one thing: leave an image at the layer and then delete it (it would be too much memory if it wasn’t deleted). To do this, you need to override the clearContext method on layer so that the pattern drawn by Sprite remains.

// Override the clearContext method to ensure that the image remains after Sprite, label, path, etc
layer.clearContext = (a)= > {}

// Generate new Sprite elements from data
const sprite = drawSomething(data);
// Draw to layer
layer.append(sprite);
// Make sure Sprite is drawn on layer
await layer.prepareRender();
// Remove the Sprite element because the clearContext method was overridden and the image remains on the layer
sprite.remove();

// If you want to clear the layer
const {width, height} = layer.context.canvas;
layer.context.clearRect(0.0, width, height);
Copy the code

Sprite.js has a snapshot method on the scene. It takes a screenshot of the scene and returns the canvas. We can call toBuffer on this canvas to get binary image data. Then use the synchronous write method provided by the FS module in Node.js to generate a PNG image.

const canvas = await scene.snapshot()
await fs.writeFile('snap.png', canvas.toBuffer())
Copy the code

If you don’t want to take a snapshot of the whole scene and just want to take a quick picture of a specific layer, you can obtain the Canvas object through layer and then take a snapshot of the layer in the same way.

async function snapshot(layer) {
  await layer.prepareRender();
  return layer.canvas.toBuffer();
};
Copy the code

If the number of snapshot images is large, you need to upload the snapshot periodically to the CDN or an independent file server and save the URL of the snapshot in the database. This process uses the ThinkJS scheduled task, you can add the following configuration in SRC /config/crontab.js and write the corresponding processing method. If you want to ensure that a scheduled task is executed at a certain time, for example, when the task is executed at a multiple of 5 minutes, you can set a more fine-grained timer and determine in the processing method that the timer is not executed at a multiple of 5 minutes.

// src/config/crontab.js
module.exports = [
  {
    enable: true.interval: '1m'.// Execute this command every 1 minute
    handle: 'crontab/snapshot'}];// src/controller/crontab.js
module.exports = class extends think.Controller {
  async snapshotAction() {
    // Refuse to start non-scheduled tasks
    if (!this.isCli) return;
    const now = new Date(a);// If it is not an integer multiple of 5 minutes, the task is not executed
    if (now.Minutes() % 5) {
      return;
    }
    // Take a snapshot -> upload CDN -> save database logic
    // ...}}Copy the code

The image processing

Using Sprite.js, you can combine images on the server, combine images, add filters, etc. This solution simply combines multiple images of the same type into a single image. Sprite.js implements a common pre-loading function on both the front and back ends. Images can be pre-loaded and then used in sprite.js. The following code implements this process.

const spritejs = require('spritejs');
const fs = require('fs');
const writeFileAsync = think.promisify(fs.writeFile, fs);

(async function() {
  const {Scene, Sprite} = spritejs;
  const scene = new Scene('#paper', {resolution: [1200.1200]});
  // Preload images
  await scene.preload(
    'https://p3.ssl.qhimg.com/t01ccaee34d3f92a10c.png'.'https://p2.ssl.qhimg.com/t01eb096408038e7496.png'
  );
  // Whether to delegate DOM events. If this parameter is set to false, this Layer will not handle DOM events
  // Can improve performance
  const layer = scene.layer('fglayer', {
    handleEvent: false
  });
  const sprite = new Sprite();
  // Add multiple textures to the Sprite element
  // http://spritejs.org/#/zh-cn/doc/attribute?id=textures
  sprite.attr({
    textures: [{src: 'https://p3.ssl.qhimg.com/t01ccaee34d3f92a10c.png'
      },
      {
        src: 'https://p2.ssl.qhimg.com/t01eb096408038e7496.png'}}]);// Add to layer
  layer.append(sprite);
  const buffer = await snapshot(layer);
  await writeFileAsync('test.png', buffer); layer.remove(sprite); }) ();Copy the code

websocket

ThinkJS uses master/ Workers multi-process model. If the project does not use Websocket, the master receives the request and sends it to workers through Round Robin algorithm, which basically ensures load balancing. If websocket communication is required on the front and back ends of a project, you need to configure stickyCluster in ThinkJS: This ensures that requests from the same client will be processed by the same worker, thus successfully establishing websocket communication. This will sacrifice some performance. See The ThinkJS multi-process model in detail.

Since our project is a large screen data visualization project, there is not much traffic in general, so we only start a worker in this project and set stickyCluster to false to successfully establish websocket communication. Since there is only one worker doing the work, all requests must be handed to it.

Although the front-end can communicate directly with the WebSocket upstream service, why not do so (currently with the ThinkJS service to establish webSocket communication), mainly considering that the ThinkJS service can process data through a set of policies. Decide on server-side rendering and front-end real-time data presentation so that large front-end pages can focus only on drawing.

📓 summary

This is the first time to do ThinkJS and Sprite.js server-side rendering big screen project, it is a new attempt for us, but the technology is to solve the problem of how to achieve, what kind of display to achieve? And why? It is still a problem that needs to be considered in the process of visualization.