diff --git a/package.json b/package.json index 8dd51ef649..2d43bf63d9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "e2e": "nocobase e2e", "ts": "nocobase test:server", "tc": "nocobase test:client", + "benchmark": "nocobase benchmark", "doc": "nocobase doc", "doc:cn": "nocobase doc --lang=zh-CN", "postinstall": "nocobase postinstall", diff --git a/packages/core/build/src/buildDeclaration.ts b/packages/core/build/src/buildDeclaration.ts index c9102be470..bfd92c740a 100644 --- a/packages/core/build/src/buildDeclaration.ts +++ b/packages/core/build/src/buildDeclaration.ts @@ -25,6 +25,7 @@ export const buildDeclaration = (cwd: string, targetDir: string) => { `!${path.join(srcPath, '**/demos{,/**}')}`, `!${path.join(srcPath, '**/__test__{,/**}')}`, `!${path.join(srcPath, '**/__tests__{,/**}')}`, + `!${path.join(srcPath, '**/__benchmarks__{,/**}')}`, `!${path.join(srcPath, '**/__e2e__{,/**}')}`, `!${path.join(srcPath, '**/*.mdx')}`, `!${path.join(srcPath, '**/*.md')}`, diff --git a/packages/core/build/src/buildPlugin.ts b/packages/core/build/src/buildPlugin.ts index 9b7f228072..ed8a766f95 100644 --- a/packages/core/build/src/buildPlugin.ts +++ b/packages/core/build/src/buildPlugin.ts @@ -15,6 +15,7 @@ import fs from 'fs-extra'; import path from 'path'; import { build as tsupBuild } from 'tsup'; +import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; import { EsbuildSupportExts, globExcludeFiles } from './constant'; import { PkgLog, UserConfig, getPackageJson } from './utils'; import { @@ -26,12 +27,15 @@ import { getSourcePackages, } from './utils/buildPluginUtils'; import { getDepPkgPath, getDepsConfig } from './utils/getDepsConfig'; -import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; const validExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs']; const serverGlobalFiles: string[] = ['src/**', '!src/client/**', ...globExcludeFiles]; const clientGlobalFiles: string[] = ['src/**', '!src/server/**', ...globExcludeFiles]; -const sourceGlobalFiles: string[] = ['src/**/*.{ts,js,tsx,jsx,mjs}', '!src/**/__tests__']; +const sourceGlobalFiles: string[] = [ + 'src/**/*.{ts,js,tsx,jsx,mjs}', + '!src/**/__tests__', + '!src/**/__benchmarks__', +]; const external = [ // nocobase diff --git a/packages/core/build/src/constant.ts b/packages/core/build/src/constant.ts index 6da14636a8..43dd612c74 100644 --- a/packages/core/build/src/constant.ts +++ b/packages/core/build/src/constant.ts @@ -12,6 +12,7 @@ import path from 'path'; export const globExcludeFiles = [ '!src/**/__tests__', + '!src/**/__benchmarks__', '!src/**/__test__', '!src/**/__e2e__', '!src/**/demos', diff --git a/packages/core/cli/src/commands/benchmark.js b/packages/core/cli/src/commands/benchmark.js new file mode 100644 index 0000000000..3de9b9da77 --- /dev/null +++ b/packages/core/cli/src/commands/benchmark.js @@ -0,0 +1,73 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +const glob = require('fast-glob'); +const { Command } = require('commander'); +const { run } = require('../util'); + +/** + * + * @param {Command} cli + */ +module.exports = (cli) => { + return ( + cli + .command('benchmark') + .description('Run benchmark tests') + // .option('--single-thread [singleThread]') + .option('-a, --all [all]', 'Run all benchmark files which ends with .benchmark.{js,ts}') + .arguments('[paths...]') + .allowUnknownOption() + .action(async (paths, opts) => { + process.env.NODE_ENV = 'test'; + process.env.LOGGER_LEVEL = 'error'; + + const cliArgs = ['--max_old_space_size=14096']; + + // if (process.argv.includes('-h') || process.argv.includes('--help')) { + // await run('node', cliArgs); + // return; + // } + + // if (!opts.singleThread) { + // process.argv.splice(process.argv.indexOf('--single-thread=false'), 1); + // } else { + // process.argv.push('--poolOptions.threads.singleThread=true'); + // } + + if (!paths.length) { + if (opts.all) { + paths.push('**/*.benchmark.ts'); + } else { + console.warn( + 'No benchmark files specified. Please provide at least 1 benchmark file or path to run. Or use --all to run all "*.benchmark.ts".', + ); + return; + } + } + + const files = []; + + for (const pattern of paths) { + for (const file of glob.sync(pattern)) { + files.push(file); + } + } + + if (!files.length) { + console.log('No benchmark files found'); + return; + } + + for (const file of files) { + await run('tsx', [...cliArgs, file]); + } + }) + ); +}; diff --git a/packages/core/cli/src/commands/index.js b/packages/core/cli/src/commands/index.js index 97a685f668..14dd20e2ef 100644 --- a/packages/core/cli/src/commands/index.js +++ b/packages/core/cli/src/commands/index.js @@ -29,6 +29,7 @@ module.exports = (cli) => { require('./pm2')(cli); require('./test')(cli); require('./test-coverage')(cli); + require('./benchmark')(cli); require('./umi')(cli); require('./update-deps')(cli); require('./upgrade')(cli); diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index 855acd317b..ef8f2272fc 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -389,7 +389,7 @@ exports.initEnv = function initEnv() { if ( !process.env.APP_ENV_PATH && process.argv[2] && - ['test', 'test:client', 'test:server'].includes(process.argv[2]) + ['test', 'test:client', 'test:server', 'benchmark'].includes(process.argv[2]) ) { if (fs.existsSync(resolve(process.cwd(), '.env.test'))) { process.env.APP_ENV_PATH = '.env.test'; diff --git a/packages/core/database/src/__tests__/collection.test.ts b/packages/core/database/src/__tests__/collection.test.ts index d94a87ba04..3eb0a9888e 100644 --- a/packages/core/database/src/__tests__/collection.test.ts +++ b/packages/core/database/src/__tests__/collection.test.ts @@ -9,8 +9,10 @@ import { Collection, Database, createMockDatabase } from '@nocobase/database'; import { IdentifierError } from '../errors/identifier-error'; +import { isPg } from '@nocobase/test'; + +const pgOnly = () => (isPg() ? it : it.skip); -const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? it : it.skip); describe('collection', () => { let db: Database; diff --git a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts index 6ef9c240c6..e7cfc57167 100644 --- a/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/belongs-to-many-repository.test.ts @@ -8,7 +8,9 @@ */ import { BelongsToManyRepository, Collection, createMockDatabase, Database } from '@nocobase/database'; -import { pgOnly } from '@nocobase/test'; +import { isPg } from '@nocobase/test'; + +const pgOnly = () => (isPg() ? describe : describe.skip); pgOnly()('belongs to many with targetCollection', () => { let db: Database; diff --git a/packages/core/database/src/__tests__/view/view-with-association.test.ts b/packages/core/database/src/__tests__/view/view-with-association.test.ts index 2f22781910..5fe1ac1549 100644 --- a/packages/core/database/src/__tests__/view/view-with-association.test.ts +++ b/packages/core/database/src/__tests__/view/view-with-association.test.ts @@ -8,8 +8,10 @@ */ import { createMockDatabase, Database, ViewFieldInference } from '@nocobase/database'; -import { pgOnly } from '@nocobase/test'; import { uid } from '@nocobase/utils'; +import { isPg } from '@nocobase/test'; + +const pgOnly = () => (isPg() ? describe : describe.skip); pgOnly()('view with association', () => { let db: Database; diff --git a/packages/core/devtools/package.json b/packages/core/devtools/package.json index 13f6d465ba..b51cb0cfcc 100644 --- a/packages/core/devtools/package.json +++ b/packages/core/devtools/package.json @@ -36,6 +36,7 @@ "react-dom": "^18.0.0", "rimraf": "^3.0.0", "serve": "^14.2.4", + "tinybench": "^4.0.1", "ts-loader": "^7.0.4", "ts-node": "9.1.1", "ts-node-dev": "1.1.8", diff --git a/packages/core/test/src/server/index.ts b/packages/core/test/src/server/index.ts index cbe223c54b..2fdfdc1d68 100644 --- a/packages/core/test/src/server/index.ts +++ b/packages/core/test/src/server/index.ts @@ -7,7 +7,6 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { describe } from 'vitest'; import ws from 'ws'; export { createMockDatabase, MockDatabase, mockDatabase } from '@nocobase/database'; @@ -16,7 +15,6 @@ export * from './memory-pub-sub-adapter'; export * from './mock-isolated-cluster'; export * from './mock-server'; -export const pgOnly: () => any = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip); export const isPg = () => process.env.DB_DIALECT == 'postgres'; export const isMysql = () => process.env.DB_DIALECT == 'mysql'; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/inherited-collection.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/inherited-collection.test.ts index 26f51e3084..b18da435e5 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/inherited-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/inherited-collection.test.ts @@ -7,9 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { MockServer, pgOnly } from '@nocobase/test'; +import { isPg, MockServer } from '@nocobase/test'; import { createApp } from '..'; +const pgOnly = () => (isPg() ? describe : describe.skip); + pgOnly()('Inherited Collection', () => { let app: MockServer; let agent; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inhertied-collection-with-schema.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection-with-schema.test.ts similarity index 97% rename from packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inhertied-collection-with-schema.test.ts rename to packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection-with-schema.test.ts index 9a850f77e2..d6f5fe5366 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inhertied-collection-with-schema.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection-with-schema.test.ts @@ -9,8 +9,10 @@ import Database, { Repository } from '@nocobase/database'; import Application from '@nocobase/server'; +import { isPg } from '@nocobase/test'; import { createApp } from '..'; -import { pgOnly } from '@nocobase/test'; + +const pgOnly = () => (isPg() ? describe : describe.skip); pgOnly()('Inherited Collection with schema options', () => { let db: Database; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection.test.ts index 56626b7f17..33c1d8adb3 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/inherits/inherited-collection.test.ts @@ -14,9 +14,11 @@ import Database, { Repository, } from '@nocobase/database'; import Application from '@nocobase/server'; -import { pgOnly } from '@nocobase/test'; +import { isPg } from '@nocobase/test'; import { createApp } from '..'; +const pgOnly = () => (isPg() ? describe : describe.skip); + pgOnly()('Inherited Collection', () => { let db: Database; let app: Application; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/migrations/set-collection-schema.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/migrations/set-collection-schema.test.ts index c6ea02f6bb..5dd07f58b9 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/migrations/set-collection-schema.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/migrations/set-collection-schema.test.ts @@ -8,10 +8,12 @@ */ import { Database, MigrationContext } from '@nocobase/database'; -import { MockServer, pgOnly } from '@nocobase/test'; +import { MockServer, isPg } from '@nocobase/test'; import Migrator from '../../migrations/20230918024546-set-collection-schema'; import { createApp } from '../index'; +const pgOnly = () => (isPg() ? describe : describe.skip); + pgOnly()('set collection schema', () => { let app: MockServer; let db: Database; diff --git a/packages/plugins/@nocobase/plugin-workflow/package.json b/packages/plugins/@nocobase/plugin-workflow/package.json index 2ecffb798e..19851d8d9a 100644 --- a/packages/plugins/@nocobase/plugin-workflow/package.json +++ b/packages/plugins/@nocobase/plugin-workflow/package.json @@ -28,7 +28,8 @@ "react-i18next": "^11.15.1", "react-js-cron": "^3.1.0", "react-router-dom": "^6.11.2", - "sequelize": "^6.26.0" + "sequelize": "^6.26.0", + "tinybench": "4.x" }, "peerDependencies": { "@nocobase/actions": "1.x", diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__benchmarks__/single-process.benchmark.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__benchmarks__/single-process.benchmark.ts new file mode 100644 index 0000000000..9d012f9317 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__benchmarks__/single-process.benchmark.ts @@ -0,0 +1,145 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Bench } from 'tinybench'; + +import { getApp } from '@nocobase/plugin-workflow-test'; + +import Plugin from '..'; + +async function run(app, fn, args) { + const db = app.db; + const WorkflowRepo = db.getCollection('workflows').repository; + + const workflow = await WorkflowRepo.create({ + values: { + enabled: true, + sync: true, + type: 'syncTrigger', + }, + }); + + await fn({ app, workflow }, args); +} + +async function loopEcho({ app, workflow }, target) { + const plugin = app.pm.get(Plugin) as Plugin; + + const loopNode = await workflow.createNode({ + type: 'loop', + config: { + target, + }, + }); + + await workflow.createNode({ + type: 'echo', + upstreamId: loopNode.id, + branchIndex: 0, + }); + + await plugin.trigger(workflow, {}); +} + +async function loopQuery({ app, workflow }, target) { + const plugin = app.pm.get(Plugin) as Plugin; + + const loopNode = await workflow.createNode({ + type: 'loop', + config: { + target, + }, + }); + + await workflow.createNode({ + type: 'query', + config: { + collection: 'posts', + params: { + filterByTk: Math.ceil(Math.random() * 1000), + }, + }, + upstreamId: loopNode.id, + branchIndex: 0, + }); + + await plugin.trigger(workflow, {}); +} + +async function loopCreate({ app, workflow }, target) { + const plugin = app.pm.get(Plugin) as Plugin; + + const loopNode = await workflow.createNode({ + type: 'loop', + config: { + target, + }, + }); + + await workflow.createNode({ + type: 'create', + config: { + collection: 'posts', + params: { + values: {}, + }, + }, + upstreamId: loopNode.id, + branchIndex: 0, + }); + + await plugin.trigger(workflow, {}); +} + +async function benchmark() { + const app = await getApp({ + plugins: ['workflow-loop'], + }); + + const PostModel = app.db.getCollection('posts').model; + await PostModel.bulkCreate(Array.from({ length: 1000 }, (_, i) => ({ title: `test-${i}` }))); + + const bench = new Bench() + .add('1 node', async () => { + await run(app, loopEcho, 0); + }) + .add('20 nodes: loop 10 echos', async () => { + await run(app, loopEcho, 10); + }) + .add('200 nodes: loop 100 echos', async () => { + await run(app, loopEcho, 100); + }) + .add('20 nodes: loop 10 queries', async () => { + await run(app, loopQuery, 10); + }) + .add('200 nodes: loop 100 queries', async () => { + await run(app, loopQuery, 100); + }) + .add('20 nodes: loop 10 creates', async () => { + await run(app, loopCreate, 10); + }) + .add('200 nodes: loop 100 creates', async () => { + await run(app, loopCreate, 100); + }); + await bench.run(); + + await app.cleanDb(); + await app.destroy(); + + console.table(bench.table()); +} + +benchmark() + .then(() => { + process.exit(0); + }) + .catch((err) => { + console.error(err); + process.exit(1); + });