From 0639e9e2c670e87d0415d2492fe236de121d3ba2 Mon Sep 17 00:00:00 2001 From: "[._.]/ Adam Eivy" Date: Wed, 9 Sep 2026 05:00:17 +0000 Subject: [PATCH] perf: fetch only generating 3D summaries for activity polls (#6690) --- server/services/activeProcessing.js | 6 +- server/services/activeProcessing.test.js | 9 ++- server/services/imageTo3d/db.js | 7 +++ server/services/imageTo3d/db.test.js | 62 +++++++++++++++++++ .../imageTo3d/models.genericDispatch.test.js | 1 + server/services/imageTo3d/models.js | 1 + server/services/imageTo3d/models.test.js | 1 + 7 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 server/services/imageTo3d/db.test.js diff --git a/server/services/activeProcessing.js b/server/services/activeProcessing.js index 889aa5c90c..42fe64c875 100644 --- a/server/services/activeProcessing.js +++ b/server/services/activeProcessing.js @@ -1,7 +1,7 @@ import { getCudaCapability, getCudaUtilization } from '../lib/cudaCapability.js'; import { listJobs, getRunningJob } from './mediaJobQueue/index.js'; import { sanitizeJob } from './mediaJobQueue/sanitizeJob.js'; -import { listModels } from './imageTo3d/models.js'; +import { listGeneratingModelSummaries } from './imageTo3d/models.js'; import { getLoadedModels } from './ollamaManager.js'; import * as cos from './cos.js'; @@ -11,7 +11,7 @@ export async function getActiveProcessing() { const [capability, jobs, models, loadedModels, taskData, agents] = await Promise.all([ getCudaCapability(), Promise.resolve(listJobs()).then((items) => items.filter((job) => LIVE_STATUSES.has(job.status))), - listModels().catch(() => []), + listGeneratingModelSummaries().catch(() => []), getLoadedModels().catch(() => []), cos.getAllTasks().catch(() => ({ user: {}, cos: {} })), // `null` = the read FAILED, distinct from `[]` = read fine, no agents. The @@ -51,7 +51,7 @@ export async function getActiveProcessing() { }, jobs: jobs.map(sanitizeJob), extras: { - imageTo3d: models.filter((model) => model.status === 'generating').map((model) => ({ id: model.id, name: model.name || model.id })), + imageTo3d: models.map((model) => ({ id: model.id, name: model.name || model.id })), ollama: loadedModels, }, agents: { diff --git a/server/services/activeProcessing.test.js b/server/services/activeProcessing.test.js index ea9f7a3584..1bd7c9d2b6 100644 --- a/server/services/activeProcessing.test.js +++ b/server/services/activeProcessing.test.js @@ -6,7 +6,7 @@ const deps = vi.hoisted(() => ({ vi.mock('../lib/cudaCapability.js', () => ({ getCudaCapability: deps.capability, getCudaUtilization: deps.utilization })); vi.mock('./mediaJobQueue/index.js', () => ({ listJobs: deps.jobs, getRunningJob: deps.running })); vi.mock('./mediaJobQueue/sanitizeJob.js', () => ({ sanitizeJob: (job) => ({ id: job.id, kind: job.kind, status: job.status, params: { musicStudio: job.params.musicStudio } }) })); -vi.mock('./imageTo3d/models.js', () => ({ listModels: deps.models })); +vi.mock('./imageTo3d/models.js', () => ({ listGeneratingModelSummaries: deps.models })); vi.mock('./ollamaManager.js', () => ({ getLoadedModels: deps.loaded })); vi.mock('./cos.js', () => ({ getAllTasks: deps.tasks, getStatus: deps.status, getAgents: deps.agents })); @@ -22,7 +22,7 @@ describe('active processing snapshot', () => { deps.utilization.mockResolvedValue({ status: 'available', gpus: [{ name: 'Example GPU', utilizationPercent: 44, memoryUsedMib: 1000, memoryTotalMib: 24000 }] }); deps.jobs.mockReturnValue([{ id: 'audio-1', kind: 'audio', status: 'running', params: { prompt: 'fake', musicStudio: { trackId: 'track-1' }, secretPath: '/private' } }]); deps.running.mockReturnValue({ kind: 'audio' }); - deps.models.mockResolvedValue([{ id: 'mesh-1', name: 'Fake mesh', status: 'generating' }]); + deps.models.mockResolvedValue([{ id: 'mesh-1', name: 'Fake mesh' }, { id: 'mesh-2', name: '' }]); deps.loaded.mockResolvedValue([{ id: 'model-1', name: 'Fake model' }]); deps.tasks.mockResolvedValue({ user: { tasks: [{ id: 'task-1', status: 'pending' }] }, cos: { tasks: [{ id: 'cos-task-1', status: 'completed' }] } }); deps.status.mockResolvedValue({ activeAgents: 99 }); @@ -34,7 +34,7 @@ describe('active processing snapshot', () => { expect(snapshot.jobs).toHaveLength(1); expect(snapshot.gpu).toMatchObject({ status: 'available', laneBusy: true, laneKind: 'audio' }); expect(snapshot.gpu.gpus[0]).toMatchObject({ utilizationPercent: 44, memoryUsedMib: 1000 }); - expect(snapshot.extras.imageTo3d).toEqual([{ id: 'mesh-1', name: 'Fake mesh' }]); + expect(snapshot.extras.imageTo3d).toEqual([{ id: 'mesh-1', name: 'Fake mesh' }, { id: 'mesh-2', name: 'mesh-2' }]); expect(snapshot.extras.ollama).toEqual([{ id: 'model-1', name: 'Fake model' }]); expect(snapshot.agents).toEqual({ active: 2, queued: 1 }); expect(deps.status).not.toHaveBeenCalled(); @@ -51,6 +51,9 @@ describe('active processing snapshot', () => { deps.agents.mockResolvedValue([]); const snapshot = await getActiveProcessing(); expect(snapshot.gpu).toMatchObject({ status: 'absent', laneBusy: false, laneKind: null, gpus: [] }); + expect(snapshot.extras.imageTo3d).toEqual([]); + deps.models.mockRejectedValueOnce(new Error('store unavailable')); + expect((await getActiveProcessing()).extras.imageTo3d).toEqual([]); expect(deps.utilization).not.toHaveBeenCalled(); }); }); diff --git a/server/services/imageTo3d/db.js b/server/services/imageTo3d/db.js index b05eb9102c..2ae338e382 100644 --- a/server/services/imageTo3d/db.js +++ b/server/services/imageTo3d/db.js @@ -46,6 +46,13 @@ export async function listModels({ includeDeleted = false } = {}) { return result.rows.map(rowToModel); } +export async function listGeneratingModelSummaries() { + const result = await query( + "SELECT id, name FROM image_to_3d_models WHERE deleted = FALSE AND status = 'generating' ORDER BY updated_at DESC", + ); + return result.rows; +} + export async function getModel(id, { includeDeleted = false } = {}) { const result = await query('SELECT data FROM image_to_3d_models WHERE id = $1', [id]); const model = rowToModel(result.rows[0]); diff --git a/server/services/imageTo3d/db.test.js b/server/services/imageTo3d/db.test.js new file mode 100644 index 0000000000..e06132f02c --- /dev/null +++ b/server/services/imageTo3d/db.test.js @@ -0,0 +1,62 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import * as connection from '../../lib/db.js'; +import { requireDbOrSkip } from '../../lib/dbTestGate.js'; +import { listGeneratingModelSummaries, listModels } from './db.js'; + +const health = await connection.checkHealth(); +const runDb = requireDbOrSkip('image-to-3D summaries', health.connected, health.error); + +describe.skipIf(!runDb)('image-to-3D activity projection', () => { + beforeAll(async () => { + await connection.ensureSchema(); + await connection.query('DELETE FROM image_to_3d_models'); + }); + + afterAll(async () => { + vi.restoreAllMocks(); + await connection.query('DELETE FROM image_to_3d_models'); + await connection.close(); + }); + + it('returns only live generating id/name rows in update order without hydrating gallery records', async () => { + expect(await listGeneratingModelSummaries()).toEqual([]); + + const completed = Array.from({ length: 1000 }, (_, i) => ({ + id: `completed-${i}`, name: 'Example completed mesh', status: 'completed', + updatedAt: '2026-01-01T00:00:00Z', deleted: false, + runs: [{ output: 'Example generation history' }], + })); + const others = [ + { id: 'draft', status: 'draft' }, + { id: 'failed', status: 'failed' }, + { id: 'deleted', status: 'generating', deleted: true }, + { id: 'older', status: 'generating', updatedAt: '2026-01-02T00:00:00Z', name: '' }, + { id: 'newer', status: 'generating', updatedAt: '2026-01-03T00:00:00Z' }, + ].map((record) => ({ + name: 'Example mesh', updatedAt: '2026-01-01T00:00:00Z', deleted: false, ...record, + })); + await connection.query( + `INSERT INTO image_to_3d_models (id, name, status, deleted, updated_at, data) + SELECT value->>'id', value->>'name', value->>'status', + (value->>'deleted')::boolean, (value->>'updatedAt')::timestamptz, value + FROM jsonb_array_elements($1::jsonb)`, + [JSON.stringify([...completed, ...others])], + ); + + const querySpy = vi.spyOn(connection, 'query'); + expect(await listGeneratingModelSummaries()).toEqual([ + { id: 'newer', name: 'Example mesh' }, + { id: 'older', name: '' }, + ]); + // A row-shape assertion alone would allow SELECT * followed by JS projection. + expect(querySpy).toHaveBeenCalledExactlyOnceWith( + "SELECT id, name FROM image_to_3d_models WHERE deleted = FALSE AND status = 'generating' ORDER BY updated_at DESC", + ); + querySpy.mockRestore(); + + const gallery = await listModels(); + expect(gallery).toHaveLength(1004); + expect(gallery.find((model) => model.id === 'completed-0')).toEqual(completed[0]); + expect(await listModels({ includeDeleted: true })).toHaveLength(1005); + }); +}); diff --git a/server/services/imageTo3d/models.genericDispatch.test.js b/server/services/imageTo3d/models.genericDispatch.test.js index 95a6743f52..673f324a81 100644 --- a/server/services/imageTo3d/models.genericDispatch.test.js +++ b/server/services/imageTo3d/models.genericDispatch.test.js @@ -43,6 +43,7 @@ vi.mock('../../lib/heavyJobClaim.js', () => ({ vi.mock('./db.js', () => ({ listModels: vi.fn(), + listGeneratingModelSummaries: vi.fn(), getModel: vi.fn(), createModel: vi.fn(), mutateModel: vi.fn(), diff --git a/server/services/imageTo3d/models.js b/server/services/imageTo3d/models.js index 81331f28a7..d105e67ac3 100644 --- a/server/services/imageTo3d/models.js +++ b/server/services/imageTo3d/models.js @@ -305,6 +305,7 @@ async function executeRender({ id, operationId, adapter, sourcePath, caps, optio } export const listModels = store.listModels; +export const listGeneratingModelSummaries = store.listGeneratingModelSummaries; export const getModel = store.getModel; /** diff --git a/server/services/imageTo3d/models.test.js b/server/services/imageTo3d/models.test.js index 1a0a319d78..46db2ae5e1 100644 --- a/server/services/imageTo3d/models.test.js +++ b/server/services/imageTo3d/models.test.js @@ -60,6 +60,7 @@ vi.mock('../localMemory.js', async (importOriginal) => ({ vi.mock('./db.js', () => ({ listModels: vi.fn(), + listGeneratingModelSummaries: vi.fn(), getModel: vi.fn(), createModel: vi.fn(), mutateModel: vi.fn(),