IO-3051 canvas-handler optimization

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
This commit is contained in:
Allan Carr
2024-12-04 14:22:04 -08:00
parent 11b906103a
commit a885bdec74
3 changed files with 131 additions and 79 deletions

View File

@@ -0,0 +1,20 @@
const { isObject } = require("lodash");
const validateCanvasRequestMiddleware = (req, res, next) => {
const { w, h, values, keys, override } = req.body;
if (!values || !keys) {
return res.status(400).send("Missing required data");
}
if (override && !isObject(override)) {
return res.status(400).send("Override must be an object");
}
if (w && (!Number.isFinite(w) || w <= 0)) {
return res.status(400).send("Width must be a positive number");
}
if (h && (!Number.isFinite(h) || h <= 0)) {
return res.status(400).send("Height must be a positive number");
}
next();
};
module.exports = validateCanvasRequestMiddleware;

View File

@@ -5,89 +5,120 @@ const logger = require("../utils/logger");
const { backgroundColors, borderColors } = require("./canvas-colors");
const { isObject, defaultsDeep, isNumber } = require("lodash");
let isProcessing = false;
const requestQueue = [];
const processCanvasRequest = async (req, res) => {
try {
const { w, h, values, keys, override } = req.body;
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Set dimensions with defaults
const width = isNumber(w) ? w : 500;
const height = isNumber(h) ? h : 275;
const configuration = {
type: "doughnut",
data: {
labels: keys,
datasets: [
{
data: values,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1
}
]
},
options: {
animation: false,
devicePixelRatio: 4,
responsive: false,
maintainAspectRatio: true,
circumference: 180,
rotation: -90,
plugins: {
legend: {
labels: {
boxWidth: 20,
font: {
family: "'Montserrat'",
size: 10,
style: "normal",
weight: "normal"
}
},
position: "left"
}
}
}
};
// If we have a valid override object, merge it with the default configuration object.
// This allows for you to override the default configuration with a custom one.
const defaults = () => {
if (!override || !isObject(override)) {
return configuration;
}
return defaultsDeep(override, configuration);
};
// Generate chart
let canvas = createCanvas(width, height);
let ctx = canvas.getContext("2d");
let chart = new Chart(ctx, defaults());
const result = canvas.toDataURL();
chart.destroy();
canvas.width = 0;
canvas.height = 0;
ctx = null;
canvas = null;
chart = null;
res.status(200).send(result);
} catch (error) {
if (chart) chart.destroy();
if (canvas) {
canvas.width = 0;
canvas.height = 0;
}
ctx = null;
canvas = null;
chart = null;
logger.log("inbound-canvas-creation", "error", "jsr", null, { error: error.message, stack: error.stack });
res.status(500).send("Error generating canvas");
}
};
const processNextInQueue = async () => {
if (requestQueue.length === 0) {
isProcessing = false;
return;
}
const { req, res } = requestQueue.shift();
await processCanvasRequest(req, res);
processNextInQueue();
};
exports.canvastest = function (req, res) {
//console.log("Incoming test request.", req);
res.status(200).send("OK");
};
exports.canvas = function (req, res) {
const { w, h, values, keys, override } = req.body;
//console.log("Incoming Canvas Request:", w, h, values, keys, override);
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Gate required values
if (!values || !keys) {
res.status(400).send("Missing required data");
exports.canvas = async function (req, res) {
if (isProcessing) {
if (requestQueue.length >= 100) {
// Set a maximum queue size
return res.status(503).send("Server is busy. Please try again later.");
}
requestQueue.push({ req, res });
logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return;
}
// Override must be an object if it exists
if (override && !isObject(override)) {
res.status(400).send("Override must be an object");
return;
}
// Set the default Width and Height
let [width, height] = [500, 275];
// Allow for custom width and height
if (isNumber(w)) {
width = w;
}
if (isNumber(h)) {
height = h;
}
const configuration = {
type: "doughnut",
data: {
labels: keys,
datasets: [
{
data: values,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1
}
]
},
options: {
devicePixelRatio: 4,
responsive: false,
maintainAspectRatio: true,
circumference: 180,
rotation: -90,
plugins: {
legend: {
labels: {
boxWidth: 20,
font: {
family: "'Montserrat'",
size: 10,
style: "normal",
weight: "normal"
}
},
position: "left"
}
}
}
};
// If we have a valid override object, merge it with the default configuration object.
// This allows for you to override the default configuration with a custom one.
const defaults = () => {
if (!override || !isObject(override)) {
return configuration;
}
return defaultsDeep(override, configuration);
};
res.status(200).send(
(() => {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
new Chart(ctx, defaults());
return canvas.toDataURL();
})()
);
isProcessing = true;
await processCanvasRequest(req, res);
processNextInQueue();
};

View File

@@ -2,10 +2,11 @@ const express = require("express");
const router = express.Router();
const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const validateCanvasRequestMiddleware = require("../middleware/validateCanvasRequestMiddleware");
const { canvas } = require("../render/canvas-handler");
// Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas);
router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasRequestMiddleware], canvas);
module.exports = router;