🏆 Performance tests with K6 (#303)
This commit is contained in:
@@ -16,6 +16,7 @@ lerna-debug.log*
|
|||||||
# Tests
|
# Tests
|
||||||
/coverage
|
/coverage
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
/profiles
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
/.idea
|
||||||
|
|||||||
Generated
+8896
-4483
File diff suppressed because it is too large
Load Diff
@@ -185,6 +185,7 @@
|
|||||||
"unoconv-promise": "^1.0.8",
|
"unoconv-promise": "^1.0.8",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"uuid-time": "^1.0.0",
|
"uuid-time": "^1.0.0",
|
||||||
|
"v8-profiler-next": "^1.10.0",
|
||||||
"yargs": "^16.2.0"
|
"yargs": "^16.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export class DocumentsController {
|
|||||||
private rootAdmins: string[] = config.has("drive.rootAdmins")
|
private rootAdmins: string[] = config.has("drive.rootAdmins")
|
||||||
? config.get("drive.rootAdmins")
|
? config.get("drive.rootAdmins")
|
||||||
: [];
|
: [];
|
||||||
|
public profilingEnabled: boolean = config.has("drive.profilingEnabled")
|
||||||
|
? config.get("drive.profilingEnabled")
|
||||||
|
: false;
|
||||||
/**
|
/**
|
||||||
* Creates a DriveFile item
|
* Creates a DriveFile item
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FastifyInstance, FastifyPluginCallback } from "fastify";
|
import { FastifyInstance, FastifyPluginCallback } from "fastify";
|
||||||
import { DocumentsController } from "./controllers";
|
import { DocumentsController } from "./controllers";
|
||||||
import { createDocumentSchema, createVersionSchema } from "./schemas";
|
import { createDocumentSchema, createVersionSchema } from "./schemas";
|
||||||
|
import profilerPlugin from "../../../utils/profiler";
|
||||||
|
|
||||||
const baseUrl = "/companies/:company_id";
|
const baseUrl = "/companies/:company_id";
|
||||||
const serviceUrl = `${baseUrl}/item`;
|
const serviceUrl = `${baseUrl}/item`;
|
||||||
@@ -8,6 +9,11 @@ const serviceUrl = `${baseUrl}/item`;
|
|||||||
const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) => {
|
const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next) => {
|
||||||
const documentsController = new DocumentsController();
|
const documentsController = new DocumentsController();
|
||||||
|
|
||||||
|
fastify.register(profilerPlugin, {
|
||||||
|
active: documentsController.profilingEnabled,
|
||||||
|
outputDir: "profiles",
|
||||||
|
});
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `${serviceUrl}`,
|
url: `${serviceUrl}`,
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import fp from "fastify-plugin";
|
||||||
|
import v8profiler from "v8-profiler-next";
|
||||||
|
|
||||||
|
export class Profiler {
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
outputDir: string;
|
||||||
|
constructor(options) {
|
||||||
|
const { title, active, outputDir } = options;
|
||||||
|
this.title = title;
|
||||||
|
this.active = active;
|
||||||
|
this.outputDir = outputDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.active) {
|
||||||
|
v8profiler.startProfiling(this.title, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
if (this.active) {
|
||||||
|
const profile = v8profiler.stopProfiling(this.title);
|
||||||
|
if (profile === undefined) {
|
||||||
|
console.log("profile is undefined: ", this.title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profile.export((error, result) => {
|
||||||
|
if (error) {
|
||||||
|
console.log("Profiling error: ", error);
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(`${process.cwd()}/${this.outputDir}/${this.title}.cpuprofile`, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function profilerPlugin(fastify, options) {
|
||||||
|
fastify.addHook("onRequest", async request => {
|
||||||
|
const profiler = new Profiler({
|
||||||
|
title: `${request.method}-${request.url}`,
|
||||||
|
active: options.active,
|
||||||
|
outputDir: options.outputDir || "profiles",
|
||||||
|
});
|
||||||
|
|
||||||
|
profiler.start();
|
||||||
|
|
||||||
|
// Attach profiler to request for access in routes or other hooks
|
||||||
|
request.profiler = profiler;
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.addHook("onResponse", async request => {
|
||||||
|
if (request.profiler) {
|
||||||
|
request.profiler.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fp(profilerPlugin);
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
import http from "k6/http";
|
||||||
|
import { check, sleep } from "k6";
|
||||||
|
import { Trend } from "k6/metrics";
|
||||||
|
|
||||||
|
// Custom metric to track document creation duration
|
||||||
|
let docBrowseDuration = new Trend("doc_browse_duration");
|
||||||
|
|
||||||
|
export let options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: "30s", target: 20 },
|
||||||
|
{ duration: "1m", target: 20 },
|
||||||
|
{ duration: "10s", target: 0 },
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
doc_browse_duration: ["p(95)<500"], // 95% of browse operatins should be faster than 500ms
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = `${__ENV.BACKEND}/internal/services/documents/v1/companies`;
|
||||||
|
|
||||||
|
function browseDocuments(JWT, companyID) {
|
||||||
|
const url = `${baseURL}/${companyID}/browse/root`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${JWT}`,
|
||||||
|
};
|
||||||
|
const body = {
|
||||||
|
company_id: companyID,
|
||||||
|
mime_type: "",
|
||||||
|
};
|
||||||
|
let creationStartTime = new Date().getTime();
|
||||||
|
const response = http.post(url, JSON.stringify(body), { headers });
|
||||||
|
let creationEndTime = new Date().getTime();
|
||||||
|
|
||||||
|
docBrowseDuration.add(creationEndTime - creationStartTime);
|
||||||
|
|
||||||
|
return JSON.parse(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const JWT = __ENV.JWT; // JWT set as an environment variable
|
||||||
|
const companyID = __ENV.COMPANY_ID; // Company ID set as an environment variable
|
||||||
|
|
||||||
|
const responseBody = browseDocuments(JWT, companyID);
|
||||||
|
check(responseBody, {
|
||||||
|
"response is successful": body => body.path[0].id == "root",
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import http from "k6/http";
|
||||||
|
import { check, sleep } from "k6";
|
||||||
|
import { Trend } from "k6/metrics";
|
||||||
|
|
||||||
|
// Custom metric to track document creation duration
|
||||||
|
let docCreationDuration = new Trend("doc_creation_duration");
|
||||||
|
|
||||||
|
export let options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: "30s", target: 20 },
|
||||||
|
{ duration: "1m", target: 20 },
|
||||||
|
{ duration: "10s", target: 0 },
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
doc_creation_duration: ["p(95)<500"], // 95% of document creations should be faster than 500ms
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseURL = `${__ENV.BACKEND}/internal/services/files/v1/companies`;
|
||||||
|
|
||||||
|
function createDocument(JWT, companyID, item, version) {
|
||||||
|
const url = `${baseURL}/${companyID}/item`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${JWT}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
let creationStartTime = new Date().getTime();
|
||||||
|
const response = http.post(url, JSON.stringify({ item, version }), { headers });
|
||||||
|
let creationEndTime = new Date().getTime();
|
||||||
|
|
||||||
|
docCreationDuration.add(creationEndTime - creationStartTime);
|
||||||
|
|
||||||
|
return JSON.parse(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const JWT = __ENV.JWT; // JWT set as an environment variable
|
||||||
|
const companyID = __ENV.COMPANY_ID; // Company ID set as an environment variable
|
||||||
|
|
||||||
|
// Example item and version, modify as needed
|
||||||
|
const scope = "shared";
|
||||||
|
const randomInt = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||||
|
const fileName = `${randomInt.toString(36)}`;
|
||||||
|
const item = {
|
||||||
|
name: fileName,
|
||||||
|
parent_id: "root",
|
||||||
|
company_id: companyID,
|
||||||
|
scope,
|
||||||
|
};
|
||||||
|
const version = {};
|
||||||
|
|
||||||
|
const responseBody = createDocument(JWT, companyID, item, version);
|
||||||
|
|
||||||
|
check(responseBody, {
|
||||||
|
"response is successful": body => body.resource !== undefined,
|
||||||
|
"company_id is present": body => body.resource.company_id !== undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import http from "k6/http";
|
||||||
|
import { check, sleep } from "k6";
|
||||||
|
import { FormData } from "https://jslib.k6.io/formdata/0.0.2/index.js";
|
||||||
|
import { Trend } from "k6/metrics";
|
||||||
|
|
||||||
|
// Custom metric to track upload duration
|
||||||
|
let uploadDuration = new Trend("upload_duration");
|
||||||
|
|
||||||
|
export let options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: "30s", target: 20 },
|
||||||
|
{ duration: "1m", target: 20 },
|
||||||
|
{ duration: "10s", target: 0 },
|
||||||
|
],
|
||||||
|
thresholds: {
|
||||||
|
upload_duration: ["p(95)<17000"], // 95% of uploads should be faster than 500ms
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{ path: "../assets/sample.pdf", type: "application/pdf" },
|
||||||
|
{ path: "../assets/sample.png", type: "image/png" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const baseURL = `${__ENV.BACKEND}/internal/services/files/v1/companies`;
|
||||||
|
|
||||||
|
function uploadFile(filePath, fileType, JWT, companyID) {
|
||||||
|
const url = `${baseURL}/${companyID}/files?thumbnail_sync=0`;
|
||||||
|
const formData = new FormData();
|
||||||
|
const randomInt = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||||
|
const fileName = `${randomInt.toString(36)}.${fileType.split("/")[1]}`; // Extract extension from MIME type
|
||||||
|
formData.append("file", http.file(open(filePath), fileName, fileType));
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${JWT}`,
|
||||||
|
"Content-Type": `multipart/form-data; boundary=${formData.boundary}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
let uploadStartTime = new Date().getTime();
|
||||||
|
const response = http.post(url, formData.body(), { headers });
|
||||||
|
let uploadEndTime = new Date().getTime();
|
||||||
|
|
||||||
|
uploadDuration.add(uploadEndTime - uploadStartTime);
|
||||||
|
|
||||||
|
return JSON.parse(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const JWT = __ENV.JWT; // Set JWT as an environment variable
|
||||||
|
const companyID = __ENV.COMPANY_ID; // Set Company ID as an environment variable
|
||||||
|
const file = files[Math.floor(Math.random() * files.length)];
|
||||||
|
|
||||||
|
const responseBody = uploadFile(file.path, file.type, JWT, companyID);
|
||||||
|
check(responseBody, {
|
||||||
|
"response is successful": body => body.resource !== undefined,
|
||||||
|
"company_id is present": body => body.resource.company_id !== undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
const { execSync } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const http = require("http"); // or 'https' if your URL is https
|
||||||
|
|
||||||
|
const COMPANY_ID = "COMPANY_ID";
|
||||||
|
const JWT = "JWT";
|
||||||
|
const BASE_URL = "https://tdrive.qa.lin-saas.com";
|
||||||
|
const shouldDownload = process.argv.includes("--download");
|
||||||
|
|
||||||
|
const directory = path.join(__dirname, "./");
|
||||||
|
const tests = fs.readdirSync(directory).filter(filePath => fs.statSync(filePath).isDirectory());
|
||||||
|
|
||||||
|
// Ensure the assets folder exists
|
||||||
|
const assetsDirectory = path.join(__dirname, "assets");
|
||||||
|
if (!fs.existsSync(assetsDirectory)) {
|
||||||
|
fs.mkdirSync(assetsDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array of item IDs with corresponding file formats
|
||||||
|
const items = [
|
||||||
|
{ id: "751416c3-e486-4e2e-8ad6-f6155e4c7c4b", format: "pdf" },
|
||||||
|
{ id: "another-item-id", format: "png" },
|
||||||
|
{ id: "yet-another-item-id", format: "jpg" },
|
||||||
|
// Add more items and formats as needed
|
||||||
|
];
|
||||||
|
|
||||||
|
// Function to download the file
|
||||||
|
const downloadFile = (url, dest) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(dest);
|
||||||
|
http
|
||||||
|
.get(url, response => {
|
||||||
|
response.pipe(file);
|
||||||
|
file.on("finish", () => {
|
||||||
|
file.close(resolve);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", err => {
|
||||||
|
fs.unlink(dest);
|
||||||
|
reject(err.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to run the tests
|
||||||
|
const runTests = () => {
|
||||||
|
tests.forEach(test => {
|
||||||
|
const testDirectory = path.join(directory, test);
|
||||||
|
const files = fs.readdirSync(testDirectory).filter(file => file.endsWith(".js"));
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
console.log(`Running ${test}/${file}...`);
|
||||||
|
execSync(
|
||||||
|
`k6 run -e BACKEND=${BASE_URL} -e COMPANY_ID=${COMPANY_ID} -e JWT=${JWT} ./${test}/${file}`,
|
||||||
|
{
|
||||||
|
stdio: "inherit",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to download all files
|
||||||
|
const downloadAllFiles = async () => {
|
||||||
|
for (const item of items) {
|
||||||
|
const fileUrl = `${BASE_URL}/internal/services/documents/v1/companies/${COMPANY_ID}/item/${item.id}/download`;
|
||||||
|
const localFilePath = path.join(assetsDirectory, `sample.${item.format}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(fileUrl, localFilePath);
|
||||||
|
console.log(`File sample.${item.format} downloaded successfully.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error downloading file sample.${item.format}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main logic
|
||||||
|
if (shouldDownload) {
|
||||||
|
downloadAllFiles().then(runTests);
|
||||||
|
} else {
|
||||||
|
runTests();
|
||||||
|
}
|
||||||
+1169
-786
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ services:
|
|||||||
- PUBSUB_TYPE=local
|
- PUBSUB_TYPE=local
|
||||||
- ./docker-data/documents/:/storage/
|
- ./docker-data/documents/:/storage/
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./backend/node/profiles:/usr/src/app/profiles
|
||||||
- ./backend/node/src:/usr/src/app/src
|
- ./backend/node/src:/usr/src/app/src
|
||||||
- ./docker-data/documents/:/storage/
|
- ./docker-data/documents/:/storage/
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
Reference in New Issue
Block a user