Skip to content

Commit 6a269f9

Browse files
authored
perf(admin): read dashboard ledger usage one bounded scan per entity (#9448)
* perf(admin): read dashboard spend and workflow runs index-only The dashboard's per-entity usage was one statement OR-ing every entity's period, which the planner can only answer with a BitmapOr and a heap fetch per matched ledger row, and its count(distinct execution_id) needed the heap anyway because no index carries execution_id. Each read is now a UNION ALL with one arm per entity, so spend is an index-only scan on the existing covering cost indexes, and workflow runs are a separate statement on a new partial index over workflow rows that carries execution_id. The four copies of the period predicate now come from buildUsageAnalyticsScope. What the dashboard shows is unchanged: a run is still a distinct execution_id among the entity's workflow rows in the period. The reads stay on the primary: the replica serves these scans from disk and cancels statements held past its replay delay. * perf(admin): count dashboard workflow runs on the existing ledger indexes Drops the workflow-runs index and its migration: an extra write on every workflow ledger insert is not worth paying for a staff-only page. Spend stays index-only per entity; the separate run-count statement fetches the matched workflow rows from the heap on the existing period and created_at indexes.
1 parent 20ec175 commit 6a269f9

4 files changed

Lines changed: 428 additions & 186 deletions

File tree

‎apps/sim/lib/admin/dashboard-organizations.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ describe('listDashboardOrganizations', () => {
232232
metadata: { invoiceAmountCents: 10_000, seats: 1 },
233233
},
234234
])
235-
queueTableRows(usageLog, [{ organizationId: 'org-1', cost: '2.5', workflowRuns: 3 }])
235+
queueTableRows(usageLog, [{ entityId: 'org-1', cost: '2.5' }])
236+
queueTableRows(usageLog, [{ entityId: 'org-1', workflowRuns: 3 }])
236237

237238
const result = await listDashboardOrganizations({ search: '', limit: 50, offset: 0 })
238239

@@ -273,7 +274,8 @@ describe('listDashboardOrganizations', () => {
273274
},
274275
},
275276
])
276-
queueTableRows(usageLog, [{ organizationId: 'org-1', cost: '2.5', workflowRuns: 3 }])
277+
queueTableRows(usageLog, [{ entityId: 'org-1', cost: '2.5' }])
278+
queueTableRows(usageLog, [{ entityId: 'org-1', workflowRuns: 3 }])
277279
queueTableRows(member, [{ organizationId: 'org-1', cost: '1.5' }])
278280

279281
const result = await listDashboardOrganizations({ search: '', limit: 50, offset: 0 })
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
2+
import { generateId } from '@sim/utils/id'
3+
import { drizzle } from 'drizzle-orm/postgres-js'
4+
import postgres from 'postgres'
5+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
6+
7+
const { select } = vi.hoisted(() => ({ select: vi.fn() }))
8+
const databaseUrl = readTestDatabaseUrl()
9+
vi.mock('@sim/db', () => ({ db: { select } }))
10+
11+
import {
12+
type DashboardLedgerWindow,
13+
readDashboardHistoricalActorUsage,
14+
readDashboardLedgerUsage,
15+
readDashboardLedgerUsageByUser,
16+
} from '@/lib/admin/dashboard-usage'
17+
import type { ResolvedUsagePeriod } from '@/lib/billing/core/reporting-period'
18+
19+
const stampedPeriod: ResolvedUsagePeriod = {
20+
start: new Date('2026-08-01T00:00:00.000Z'),
21+
end: new Date('2026-09-01T00:00:00.000Z'),
22+
source: 'stripe',
23+
anchorDate: null,
24+
interval: 'month',
25+
}
26+
const reportingPeriod: ResolvedUsagePeriod = {
27+
start: new Date('2026-08-15T00:00:00.000Z'),
28+
end: new Date('2026-09-15T00:00:00.000Z'),
29+
source: 'reporting',
30+
anchorDate: '2026-01-15',
31+
interval: 'month',
32+
}
33+
const organizationWindow = (id: string, period: ResolvedUsagePeriod): DashboardLedgerWindow => ({
34+
entity: { type: 'organization', id },
35+
period,
36+
})
37+
38+
const P = `'2026-08-01', '2026-09-01'`
39+
const PREVIOUS = `'2026-07-01', '2026-08-01'`
40+
41+
const schemaName = `dashboard_usage_${generateId().replaceAll('-', '')}`
42+
const connection = postgres(databaseUrl, {
43+
max: 1,
44+
prepare: false,
45+
connection: { search_path: schemaName },
46+
onnotice: () => undefined,
47+
})
48+
49+
beforeAll(async () => {
50+
await connection.unsafe(`CREATE SCHEMA "${schemaName}"`)
51+
await connection.unsafe(`
52+
CREATE TABLE usage_log (
53+
id text PRIMARY KEY DEFAULT gen_random_uuid()::text, user_id text NOT NULL,
54+
category text NOT NULL DEFAULT 'model', source text NOT NULL, cost numeric NOT NULL,
55+
billing_entity_type text, billing_entity_id text, billing_period_start timestamp,
56+
billing_period_end timestamp, execution_id text, created_at timestamp NOT NULL
57+
);
58+
CREATE TABLE member (id text PRIMARY KEY, organization_id text, user_id text);
59+
CREATE TABLE workspace (id text PRIMARY KEY, organization_id text);
60+
CREATE TABLE permissions (id text PRIMARY KEY, entity_type text, entity_id text, user_id text);
61+
62+
INSERT INTO member VALUES ('m1', 'org-a', 'u1');
63+
INSERT INTO workspace VALUES ('ws-a', 'org-a'), ('ws-other', 'org-other');
64+
INSERT INTO permissions VALUES ('p2', 'workspace', 'ws-a', 'u2'), ('p5', 'workspace', 'ws-other', 'u5');
65+
66+
INSERT INTO usage_log (user_id, category, source, cost, billing_entity_type, billing_entity_id,
67+
billing_period_start, billing_period_end, execution_id, created_at) VALUES
68+
-- org-a, stamped period: e1 writes one row per charged line and boundary — one run.
69+
('u1', 'model', 'workflow', 0.5, 'organization', 'org-a', ${P}, 'e1', '2026-08-02'),
70+
('u1', 'tool', 'workflow', 0.25, 'organization', 'org-a', ${P}, 'e1', '2026-08-02'),
71+
('u1', 'fixed', 'workflow', 0.01, 'organization', 'org-a', ${P}, 'e1', '2026-08-02'),
72+
-- resumed after the period ended, still stamped with the period it started in
73+
('u1', 'model', 'workflow', 0.1, 'organization', 'org-a', ${P}, 'e1', '2026-09-02'),
74+
-- a free run is still a run
75+
('u2', 'model_unbilled', 'workflow', 0, 'organization', 'org-a', ${P}, 'e2', '2026-08-03'),
76+
-- one run with rows from two actors
77+
('u1', 'model', 'workflow', 0.2, 'organization', 'org-a', ${P}, 'e4', '2026-08-04'),
78+
('u2', 'model', 'workflow', 0.3, 'organization', 'org-a', ${P}, 'e4', '2026-08-04'),
79+
-- spend that is not a workflow run
80+
('u1', 'model', 'copilot', 0.4, 'organization', 'org-a', ${P}, NULL, '2026-08-05'),
81+
('u1', 'model', 'workflow-test', 0.05, 'organization', 'org-a', ${P}, 'e9', '2026-08-05'),
82+
('u2', 'fixed', 'workflow', 0.07, 'organization', 'org-a', ${P}, NULL, '2026-08-05'),
83+
-- the previous stamped period, though created inside this one
84+
('u1', 'model', 'workflow', 8, 'organization', 'org-a', ${PREVIOUS}, 'e3', '2026-08-01'),
85+
-- billed to someone else
86+
('u1', 'model', 'workflow', 9, 'user', 'org-a', ${P}, 'e3b', '2026-08-06'),
87+
('u1', 'model', 'workflow', 9, NULL, NULL, NULL, NULL, 'e3c', '2026-08-06'),
88+
-- actors who are no longer members or collaborators of org-a
89+
('u3', 'model', 'workflow', 0.9, 'organization', 'org-a', ${P}, 'e10', '2026-08-07'),
90+
('u3', 'model', 'copilot', 0.1, 'organization', 'org-a', ${P}, NULL, '2026-08-07'),
91+
('u4', 'model', 'copilot', 0.2, 'organization', 'org-a', ${P}, NULL, '2026-08-07'),
92+
('u5', 'model', 'workflow', 0.3, 'organization', 'org-a', ${P}, 'e11', '2026-08-07'),
93+
94+
-- org-b, reporting period: matched on created_at, start inclusive and end exclusive,
95+
-- whatever the rows are stamped with.
96+
('u6', 'model', 'workflow', 1, 'organization', 'org-b', ${P}, 'e5', '2026-08-15 00:00:00'),
97+
('u6', 'model', 'workflow', 0.5, 'organization', 'org-b', ${PREVIOUS}, 'e7', '2026-08-20'),
98+
('u6', 'model', 'workflow', 2, 'organization', 'org-b', ${P}, 'e6', '2026-09-15 00:00:00'),
99+
('u6', 'model', 'workflow', 4, 'organization', 'org-b', ${P}, 'e6b', '2026-08-14 23:59:59.999'),
100+
-- e1 also billed a line to org-b: each entity counts the run
101+
('u6', 'model', 'workflow', 0.25, 'organization', 'org-b', ${P}, 'e1', '2026-08-16'),
102+
103+
-- a personal payer
104+
('p1', 'model', 'workflow', 0.3, 'user', 'p1', ${P}, 'e8', '2026-08-08'),
105+
('p1', 'tool', 'workflow', 0.3, 'user', 'p1', ${P}, 'e8', '2026-08-08');
106+
`)
107+
const database = drizzle(connection)
108+
select.mockImplementation((fields) => database.select(fields))
109+
})
110+
111+
afterAll(async () => {
112+
await connection.unsafe(`DROP SCHEMA "${schemaName}" CASCADE`)
113+
await connection.end()
114+
})
115+
116+
describe('dashboard ledger usage', () => {
117+
it('sums spend and counts distinct workflow runs per entity over each period', async () => {
118+
const usage = await readDashboardLedgerUsage([
119+
organizationWindow('org-a', stampedPeriod),
120+
organizationWindow('org-b', reportingPeriod),
121+
organizationWindow('org-c', stampedPeriod),
122+
{ entity: { type: 'user', id: 'p1' }, period: stampedPeriod },
123+
])
124+
125+
expect(Object.fromEntries(usage)).toEqual({
126+
'org-a': { cost: 3.38, workflowRuns: 5 },
127+
'org-b': { cost: 1.75, workflowRuns: 3 },
128+
p1: { cost: 0.6, workflowRuns: 1 },
129+
})
130+
})
131+
132+
it('reads an entity once however many times it is asked for', async () => {
133+
const usage = await readDashboardLedgerUsage([
134+
organizationWindow('org-a', stampedPeriod),
135+
organizationWindow('org-a', stampedPeriod),
136+
])
137+
expect(usage.get('org-a')).toEqual({ cost: 3.38, workflowRuns: 5 })
138+
})
139+
140+
it('splits spend and runs by acting user, counting a shared run for each actor', async () => {
141+
const usage = await readDashboardLedgerUsageByUser([
142+
organizationWindow('org-a', stampedPeriod),
143+
organizationWindow('org-b', reportingPeriod),
144+
])
145+
146+
expect(Object.fromEntries(usage.get('org-a') ?? [])).toEqual({
147+
u1: { cost: 1.51, workflowRuns: 2 },
148+
u2: { cost: 0.37, workflowRuns: 2 },
149+
u3: { cost: 1, workflowRuns: 1 },
150+
u4: { cost: 0.2, workflowRuns: 0 },
151+
u5: { cost: 0.3, workflowRuns: 1 },
152+
})
153+
expect(Object.fromEntries(usage.get('org-b') ?? [])).toEqual({
154+
u6: { cost: 1.75, workflowRuns: 3 },
155+
})
156+
})
157+
158+
it('narrows the per-user split to the requested users', async () => {
159+
const window = organizationWindow('org-a', stampedPeriod)
160+
161+
const usage = await readDashboardLedgerUsageByUser([window], ['u2', 'u4'])
162+
expect(Object.fromEntries(usage.get('org-a') ?? [])).toEqual({
163+
u2: { cost: 0.37, workflowRuns: 2 },
164+
u4: { cost: 0.2, workflowRuns: 0 },
165+
})
166+
expect((await readDashboardLedgerUsageByUser([window], [])).size).toBe(0)
167+
})
168+
169+
it('counts only actors who are neither members nor collaborators of the organization', async () => {
170+
expect(await readDashboardHistoricalActorUsage('org-a', stampedPeriod)).toEqual({
171+
usedDollars: 1.5,
172+
workflowRuns: 2,
173+
actorCount: 3,
174+
})
175+
})
176+
})

0 commit comments

Comments
 (0)