diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 71e04bf84..a010dba4e 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -150,6 +150,31 @@ module.exports = { return true; }, + startProfiler_meta: true, + async startProfiler({ sesid }) { + const jslid = uuidv1(); + const session = this.opened.find(x => x.sesid == sesid); + if (!session) { + throw new Error('Invalid session'); + } + + console.log(`Starting profiler, sesid=${sesid}`); + session.loadingReader_jslid = jslid; + session.subprocess.send({ msgtype: 'startProfiler', jslid }); + + return { state: 'ok', jslid }; + }, + + stopProfiler_meta: true, + async stopProfiler({ sesid }) { + const session = this.opened.find(x => x.sesid == sesid); + if (!session) { + throw new Error('Invalid session'); + } + session.subprocess.send({ msgtype: 'stopProfiler' }); + return { state: 'ok' }; + }, + // cancel_meta: true, // async cancel({ sesid }) { // const session = this.opened.find((x) => x.sesid == sesid); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 4b7d140fe..4772e4404 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -16,6 +16,7 @@ let storedConnection; let afterConnectCallbacks = []; // let currentHandlers = []; let lastPing = null; +let currentProfiler = null; class TableWriter { constructor() { @@ -210,6 +211,31 @@ function waitConnected() { }); } +async function handleStartProfiler({ jslid }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + + if (!allowExecuteCustomScript(driver)) { + process.send({ msgtype: 'done' }); + return; + } + + const writer = new TableWriter(); + writer.initializeFromReader(jslid); + + currentProfiler = await driver.startProfiler(systemConnection, { + row: data => writer.rowFromReader(data), + }); + currentProfiler.writer = writer; +} + +async function handleStopProfiler({ jslid }) { + const driver = requireEngineDriver(storedConnection); + currentProfiler.writer.close(); + driver.stopProfiler(systemConnection, currentProfiler); + currentProfiler = null; +} + async function handleExecuteQuery({ sql }) { await waitConnected(); const driver = requireEngineDriver(storedConnection); @@ -280,6 +306,8 @@ const messageHandlers = { connect: handleConnect, executeQuery: handleExecuteQuery, executeReader: handleExecuteReader, + startProfiler: handleStartProfiler, + stopProfiler: handleStopProfiler, ping: handlePing, // cancel: handleCancel, }; diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index e2da64c8b..f3d5ce678 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -77,6 +77,7 @@ export interface EngineDriver { supportsDatabaseUrl?: boolean; supportsDatabaseDump?: boolean; supportsServerSummary?: boolean; + supportsDatabaseProfiler?: boolean; isElectronOnly?: boolean; supportedCreateDatabase?: boolean; showConnectionField?: (field: string, values: any) => boolean; @@ -130,6 +131,8 @@ export interface EngineDriver { callMethod(pool, method, args); serverSummary(pool): Promise; summaryCommand(pool, command, row): Promise; + startProfiler(pool, options): Promise; + stopProfiler(pool, profiler): Promise; analyserClass?: any; dumperClass?: any; diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index f660ad802..d1baed29a 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -254,6 +254,18 @@ }); }; + const handleDatabaseProfiler = () => { + openNewTab({ + title: 'Profiler', + icon: 'img profiler', + tabComponent: 'ProfilerTab', + props: { + conid: connection._id, + database: name, + }, + }); + }; + async function handleConfirmSql(sql) { saveScriptToDatabase({ conid: connection._id, database: name }, sql, false); } @@ -284,7 +296,8 @@ !connection.singleDatabase && { onClick: handleDropDatabase, text: 'Drop database' }, { divider: true }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: 'Show diagram' }, - isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' }, + driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlGenerator, text: 'SQL Generator' }, + driver?.supportsDatabaseProfiler && { onClick: handleDatabaseProfiler, text: 'Database profiler' }, isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' }, isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' }, isSqlOrDoc && diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 713eadf3a..4d34a7d26 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -49,6 +49,9 @@ 'icon close': 'mdi mdi-close', 'icon unsaved': 'mdi mdi-record', 'icon stop': 'mdi mdi-close-octagon', + 'icon play': 'mdi mdi-play', + 'icon play-stop': 'mdi mdi-stop', + 'icon pause': 'mdi mdi-pause', 'icon filter': 'mdi mdi-filter', 'icon filter-off': 'mdi mdi-filter-off', 'icon reload': 'mdi mdi-reload', @@ -176,6 +179,7 @@ 'img app-command': 'mdi mdi-flash color-icon-green', 'img app-query': 'mdi mdi-view-comfy color-icon-magenta', 'img connection': 'mdi mdi-connection color-icon-blue', + 'img profiler': 'mdi mdi-gauge color-icon-blue', 'img add': 'mdi mdi-plus-circle color-icon-green', 'img minus': 'mdi mdi-minus-circle color-icon-red', diff --git a/packages/web/src/tabs/ProfilerTab.svelte b/packages/web/src/tabs/ProfilerTab.svelte new file mode 100644 index 000000000..823b63c9a --- /dev/null +++ b/packages/web/src/tabs/ProfilerTab.svelte @@ -0,0 +1,115 @@ + + + + + + {#if jslid} + + {:else} + + {/if} + + + + + + + diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js index db9ab5037..54c202cd2 100644 --- a/packages/web/src/tabs/index.js +++ b/packages/web/src/tabs/index.js @@ -27,6 +27,7 @@ import * as ConnectionTab from './ConnectionTab.svelte'; import * as MapTab from './MapTab.svelte'; import * as PerspectiveTab from './PerspectiveTab.svelte'; import * as ServerSummaryTab from './ServerSummaryTab.svelte'; +import * as ProfilerTab from './ProfilerTab.svelte'; export default { TableDataTab, @@ -58,4 +59,5 @@ export default { MapTab, PerspectiveTab, ServerSummaryTab, + ProfilerTab, }; diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index 0621fab16..b28ccde5e 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -38,7 +38,7 @@ async function getScriptableDb(pool) { const db = pool.__getDatabase(); const collections = await db.listCollections().toArray(); for (const collection of collections) { - db[collection.name] = db.collection(collection.name); + _.set(db, collection.name, db.collection(collection.name)); } return db; } @@ -165,6 +165,49 @@ const driver = { options.done(); }, + async startProfiler(pool, options) { + const db = await getScriptableDb(pool); + const old = await db.command({ profile: -1 }); + await db.command({ profile: 2 }); + const cursor = await db.collection('system.profile').find({ + ns: /^((?!(admin\.\$cmd|\.system|\.tmp\.)).)*$/, + ts: { $gt: new Date() }, + 'command.profile': { $exists: false }, + 'command.collStats': { $exists: false }, + 'command.collstats': { $exists: false }, + 'command.createIndexes': { $exists: false }, + 'command.listIndexes': { $exists: false }, + // "command.cursor": {"$exists": false}, + 'command.create': { $exists: false }, + 'command.dbstats': { $exists: false }, + 'command.scale': { $exists: false }, + 'command.explain': { $exists: false }, + 'command.killCursors': { $exists: false }, + 'command.count': { $ne: 'system.profile' }, + op: /^((?!(getmore|killcursors)).)/i, + }); + + cursor.addCursorFlag('tailable', true); + cursor.addCursorFlag('awaitData', true); + + cursor + .forEach((row) => { + // console.log('ROW', row); + options.row(row); + }) + .catch((err) => { + console.error('Cursor stopped with error:', err.message); + }); + return { + cursor, + old, + }; + }, + async stopProfiler(pool, { cursor, old }) { + cursor.close(); + const db = await getScriptableDb(pool); + await db.command({ profile: old.was, slowms: old.slowms }); + }, async readQuery(pool, sql, structure) { try { const json = JSON.parse(sql); diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index ca50a0735..eb2fa8933 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -33,6 +33,7 @@ const driver = { defaultPort: 27017, supportsDatabaseUrl: true, supportsServerSummary: true, + supportsDatabaseProfiler: true, databaseUrlPlaceholder: 'e.g. mongodb://username:password@mongodb.mydomain.net/dbname', getQuerySplitterOptions: () => mongoSplitterOptions,