Skip to content

Commit 83db6af

Browse files
authored
Reduce event counts (#256)
* Remove empty notification set events * Remove polling action from notifications * Only refresh metadata if dbId has changed * Add types to processAllMetadata fn * Cache and re-use metadataProcessingResult * Ensure metadataProcessing isn't triggered >2 times * Keep metadataProcessor cache checks synchronous * Remove unused rpc call logging * Purge old cache entries * Remove stale cache entries * Increase notification polling interval to 5 mins
1 parent f11ce86 commit 83db6af

File tree

9 files changed

+237
-102
lines changed

9 files changed

+237
-102
lines changed

apps/src/metabase/appState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export class MetabaseState extends DefaultAppState<MetabaseAppState> {
204204
}
205205
}))
206206
// Perf caching
207-
if (!isCancelled()) {
207+
if (!isCancelled() && dbId !== oldDbId) {
208208
console.log('Running perf caching')
209209
processAllMetadata()
210210
getDatabaseInfo(dbId)

web/src/app/rpc.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const setMinusxMode = (mode: string) =>
6767
export const toggleMinusXRoot = (
6868
className: ValidMinusxRootClass,
6969
value?: boolean
70-
) => sendMessage('toggleMinusXRoot', [className, value], { log_rpc: true })
70+
) => sendMessage('toggleMinusXRoot', [className, value], { log_rpc: false })
7171
export const captureVisibleTab = () =>
7272
sendMessage('captureVisibleTab', [], { log_rpc: true })
7373
export const getElementScreenCapture = (selector: QuerySelector) =>
@@ -107,11 +107,11 @@ export const fetchData = (
107107
})
108108
export const queryURL = () => sendMessage('queryURL', [])
109109
export const getMetabaseState = (path: Parameters<typeof get>[1]) =>
110-
sendMessage('getMetabaseState', [path], { log_rpc: true })
110+
sendMessage('getMetabaseState', [path], { log_rpc: false })
111111
export const dispatchMetabaseAction = (type: string, payload?: any) =>
112112
sendMessage('dispatchMetabaseAction', [type, payload], { log_rpc: true, timeout: 1000 })
113113
export const getSelectedTextOnEditor = () =>
114-
sendMessage('getSelectedTextOnEditor', [], { log_rpc: true })
114+
sendMessage('getSelectedTextOnEditor', [], { log_rpc: false })
115115
export const subscribeMetabaseState = (path: string) =>
116116
sendMessage('subscribeMetabaseState', [path], { log_rpc: true }) as unknown as Promise<number>
117117
export const getJupyterState = (mode?: string) =>
@@ -126,7 +126,7 @@ export const getPosthogAppContext = (path: Parameters<typeof get>[1]) =>
126126
export const setTextPosthog = (selector: QuerySelector, value: string = '') =>
127127
sendMessage('setTextPosthog', [selector, value], { log_rpc: true })
128128
export const attachMutationListener = (domQueryMap: DOMQueryMap) =>
129-
sendMessage('attachMutationListener', [domQueryMap], { log_rpc: true })
129+
sendMessage('attachMutationListener', [domQueryMap], { log_rpc: false })
130130
export const detachMutationListener = (id: number) =>
131131
sendMessage('detachMutationListener', [id], { log_rpc: true })
132132
export const forwardToTab = (tool: string, message: string) =>
@@ -166,11 +166,11 @@ export const gsheetSetUserToken = (token: string) =>
166166

167167
export const attachEventsListener = (
168168
selector: QuerySelector, events?: string[]
169-
) => sendMessage('attachEventsListener', [selector, events], { log_rpc: true })
169+
) => sendMessage('attachEventsListener', [selector, events], { log_rpc: false })
170170

171171
export const addNativeElements = (
172172
selector: QuerySelector, htmlElement: HTMLJSONNode, attachType: AttachType='lastChild'
173-
) => sendMessage('addNativeElements', [selector, htmlElement, attachType], { log_rpc: true })
173+
) => sendMessage('addNativeElements', [selector, htmlElement, attachType], { log_rpc: false })
174174

175175
export const startRecording = () => sendMessage('startRecording', [])
176176
export const stopRecording = () => sendMessage('stopRecording', [])

web/src/cache/cache.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,78 @@ export function memoize<T extends AsyncFunction>(
9494
};
9595

9696
return memoized as MemoizedFn<T>;
97-
}
97+
}
98+
99+
// Debug function to list all cache entries and clean up old ones
100+
async function clearStaleEntries() {
101+
const { openDB } = await import('idb');
102+
const db = await openDB('minusx-cache-db', 1);
103+
104+
// First, get all entries
105+
const tx = db.transaction('cache', 'readonly');
106+
const store = tx.objectStore('cache');
107+
const allEntries = await store.getAll();
108+
const allKeys = await store.getAllKeys();
109+
110+
const FOUR_WEEKS_MS = 28 * 24 * 60 * 60 * 1000; // 4 weeks in milliseconds
111+
const now = Date.now();
112+
113+
const entries = allEntries.map((entry, index) => {
114+
const key = allKeys[index];
115+
const hasCreatedAt = entry.createdAt && typeof entry.createdAt === 'number';
116+
117+
if (!hasCreatedAt) {
118+
return {
119+
key,
120+
createdAt: 'MISSING',
121+
ageInHours: 'N/A',
122+
dataSize: JSON.stringify(entry.data).length,
123+
isStale: true, // Remove entries without createdAt
124+
reason: 'Missing createdAt'
125+
};
126+
}
127+
128+
return {
129+
key,
130+
createdAt: new Date(entry.createdAt).toISOString(),
131+
ageInHours: ((now - entry.createdAt) / (1000 * 60 * 60)).toFixed(2),
132+
dataSize: JSON.stringify(entry.data).length,
133+
isStale: (now - entry.createdAt) > FOUR_WEEKS_MS,
134+
reason: (now - entry.createdAt) > FOUR_WEEKS_MS ? 'Older than 4 weeks' : 'Fresh'
135+
};
136+
});
137+
138+
// Find stale entries (older than 4 weeks OR missing createdAt)
139+
const staleEntries = entries.filter(entry => entry.isStale);
140+
141+
if (staleEntries.length > 0) {
142+
console.log(`Found ${staleEntries.length} stale entries (older than 4 weeks or missing createdAt), removing...`);
143+
console.log('Breakdown:', staleEntries.reduce((acc, entry) => {
144+
acc[entry.reason] = (acc[entry.reason] || 0) + 1;
145+
return acc;
146+
}, {} as Record<string, number>));
147+
148+
// Delete stale entries
149+
const deleteTx = db.transaction('cache', 'readwrite');
150+
const deleteStore = deleteTx.objectStore('cache');
151+
152+
for (const staleEntry of staleEntries) {
153+
await deleteStore.delete(staleEntry.key);
154+
}
155+
156+
await deleteTx.done;
157+
console.log(`Removed ${staleEntries.length} stale cache entries`);
158+
}
159+
160+
// Show remaining entries
161+
const remainingEntries = entries.filter(entry => !entry.isStale);
162+
console.table(remainingEntries);
163+
164+
return {
165+
total: entries.length,
166+
remaining: remainingEntries.length,
167+
removed: staleEntries.length,
168+
entries: remainingEntries
169+
};
170+
}
171+
clearStaleEntries()

web/src/helpers/documentSubscription.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ export const unsubscribe = async (id: number) => {
1919

2020
export const onSubscription = (payload: SubscriptionPayload) => {
2121
const { id, url } = payload
22-
captureEvent(GLOBAL_EVENTS.subscription, {
23-
subscription_id: id, url
24-
})
2522
if (!(id in listeners)) {
2623
return
2724
}

web/src/helpers/metadataProcessor.ts

Lines changed: 118 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import axios from 'axios';
99
import { configs } from '../constants';
1010
import { getOrigin } from './origin';
1111
import { get } from 'lodash';
12-
import { setMetadataHash } from '../state/settings/reducer';
12+
import { MetadataProcessingResult, setMetadataHash, setMetadataProcessingCache, clearMetadataProcessingCache } from '../state/settings/reducer';
1313
import { getState } from '../state/store';
1414
import { dispatch } from '../state/dispatch';
1515
import { getAllCards, getAllCardsLegacy, getDatabaseTablesAndModelsWithoutFields, getAllFields } from '../../../apps/src/metabase/helpers/metabaseAPIHelpers';
@@ -70,6 +70,9 @@ async function calculateMetadataHash(metadataType: string, metadataValue: any, v
7070
// Global map to track ongoing uploads by hash
7171
const ongoingUploads = new Map<string, Promise<string>>();
7272

73+
// Global map to track ongoing metadata processing by dbId
74+
const ongoingMetadataProcessing = new Map<number, Promise<MetadataProcessingResult>>();
75+
7376
/**
7477
* Generic function to upload any metadata type to the backend
7578
* @param metadataType The type of metadata (e.g., 'cards', 'dbSchema')
@@ -151,7 +154,7 @@ async function processMetadataWithCaching(
151154
return currentHash
152155
}
153156

154-
export async function processAllMetadata() {
157+
export async function processAllMetadata() : Promise<MetadataProcessingResult> {
155158
console.log('[minusx] Starting coordinated metadata processing with parallel API calls...')
156159

157160
// Step 1: Start all expensive API calls in parallel
@@ -162,82 +165,126 @@ export async function processAllMetadata() {
162165
throw new Error('No database selected for metadata processing')
163166
}
164167

165-
const [dbSchema, { cards, tables: referencedTables }, allFields] = await Promise.all([
166-
getDatabaseTablesAndModelsWithoutFields(),
167-
getAllCards(),
168-
fetchDatabaseFields({ db_id: selectedDbId })
169-
])
170-
171-
console.log('[minusx] All API calls completed. Processing data...')
172-
173-
// Step 2: Create sets for efficient lookup of existing tables
174-
const existingTableNames = new Set<string>()
168+
// Check cache for this database ID first (synchronous)
169+
const currentState = getState()
170+
const cacheEntry = currentState.settings.metadataProcessingCache[selectedDbId]
175171

176-
// Add tables from dbSchema
177-
if (dbSchema.tables) {
178-
dbSchema.tables.forEach((table: any) => {
179-
const tableName = table.name
180-
const schemaName = table.schema || dbSchema.default_schema
181-
const fullName = schemaName ? `${schemaName}.${tableName}` : tableName
182-
183-
existingTableNames.add(tableName)
184-
existingTableNames.add(fullName)
185-
})
172+
if (cacheEntry) {
173+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
174+
const isStale = Date.now() - cacheEntry.timestamp > SEVEN_DAYS_MS
175+
176+
if (!isStale) {
177+
console.log(`[minusx] Using cached metadata for database ${selectedDbId}`)
178+
return cacheEntry.result
179+
} else {
180+
console.log(`[minusx] Cached metadata for database ${selectedDbId} is stale, clearing cache`)
181+
// Clear stale cache entry using proper Redux action
182+
dispatch(clearMetadataProcessingCache(selectedDbId))
183+
}
186184
}
187185

188-
// Add models from dbSchema
189-
if (dbSchema.models) {
190-
dbSchema.models.forEach((model: any) => {
191-
existingTableNames.add(model.name)
192-
})
186+
// Check if processing is already in progress for this database ID
187+
if (ongoingMetadataProcessing.has(selectedDbId)) {
188+
console.log(`[minusx] Metadata processing already in progress for database ${selectedDbId}, returning existing promise`)
189+
return await ongoingMetadataProcessing.get(selectedDbId)!
193190
}
194191

195-
console.log('[minusx] Found existing tables/models:', existingTableNames.size)
196-
197-
// Step 3: Find intersection of referenced tables that actually exist
198-
const validReferencedTables = referencedTables.filter((table: any) => {
199-
const tableName = table.name
200-
const schemaName = table.schema
201-
const fullName = schemaName ? `${schemaName}.${tableName}` : tableName
202-
203-
return existingTableNames.has(tableName) || existingTableNames.has(fullName)
204-
})
205-
206-
console.log('[minusx] Valid referenced tables:', validReferencedTables.length, 'out of', referencedTables.length)
207-
208-
// Step 4: Filter fields in-memory using table names
209-
const validTableNames = new Set(validReferencedTables.map((table: any) => {
210-
const schemaName = table.schema
211-
return schemaName ? `${schemaName}.${table.name}` : table.name
212-
}))
213-
214-
console.log('[minusx] Filtering fields for', validTableNames.size, 'valid tables...')
215-
216-
const filteredFields = allFields.filter((field: any) => {
217-
const tableName = get(field, 'table_name')
218-
const tableSchema = get(field, 'schema')
219-
const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName
220-
221-
return validTableNames.has(tableName) || validTableNames.has(fullTableName)
222-
})
223-
224-
console.log('[minusx] Fields after filtering:', filteredFields.length, 'out of', allFields.length)
225-
226-
// Step 5: Process metadata for all three with filtered data
227-
console.log('[minusx] Processing metadata with filtered data...')
192+
// Create and store the processing promise
193+
const processingPromise = (async () => {
194+
try {
195+
196+
const [dbSchema, { cards, tables: referencedTables }, allFields] = await Promise.all([
197+
getDatabaseTablesAndModelsWithoutFields(),
198+
getAllCards(),
199+
fetchDatabaseFields({ db_id: selectedDbId })
200+
])
201+
202+
console.log('[minusx] All API calls completed. Processing data...')
203+
204+
// Step 2: Create sets for efficient lookup of existing tables
205+
const existingTableNames = new Set<string>()
206+
207+
// Add tables from dbSchema
208+
if (dbSchema.tables) {
209+
dbSchema.tables.forEach((table: any) => {
210+
const tableName = table.name
211+
const schemaName = table.schema || dbSchema.default_schema
212+
const fullName = schemaName ? `${schemaName}.${tableName}` : tableName
213+
214+
existingTableNames.add(tableName)
215+
existingTableNames.add(fullName)
216+
})
217+
}
218+
219+
// Add models from dbSchema
220+
if (dbSchema.models) {
221+
dbSchema.models.forEach((model: any) => {
222+
existingTableNames.add(model.name)
223+
})
224+
}
225+
226+
console.log('[minusx] Found existing tables/models:', existingTableNames.size)
227+
228+
// Step 3: Find intersection of referenced tables that actually exist
229+
const validReferencedTables = referencedTables.filter((table: any) => {
230+
const tableName = table.name
231+
const schemaName = table.schema
232+
const fullName = schemaName ? `${schemaName}.${tableName}` : tableName
233+
234+
return existingTableNames.has(tableName) || existingTableNames.has(fullName)
235+
})
236+
237+
console.log('[minusx] Valid referenced tables:', validReferencedTables.length, 'out of', referencedTables.length)
238+
239+
// Step 4: Filter fields in-memory using table names
240+
const validTableNames = new Set(validReferencedTables.map((table: any) => {
241+
const schemaName = table.schema
242+
return schemaName ? `${schemaName}.${table.name}` : table.name
243+
}))
244+
245+
console.log('[minusx] Filtering fields for', validTableNames.size, 'valid tables...')
246+
247+
const filteredFields = allFields.filter((field: any) => {
248+
const tableName = get(field, 'table_name')
249+
const tableSchema = get(field, 'schema')
250+
const fullTableName = tableSchema ? `${tableSchema}.${tableName}` : tableName
251+
252+
return validTableNames.has(tableName) || validTableNames.has(fullTableName)
253+
})
254+
255+
console.log('[minusx] Fields after filtering:', filteredFields.length, 'out of', allFields.length)
256+
257+
// Step 5: Process metadata for all three with filtered data
258+
console.log('[minusx] Processing metadata with filtered data...')
259+
260+
const [cardsHash, dbSchemaHash, fieldsHash] = await Promise.all([
261+
processMetadataWithCaching('cards', async () => cards),
262+
processMetadataWithCaching('dbSchema', async () => dbSchema),
263+
processMetadataWithCaching('fields', async () => filteredFields)
264+
])
265+
266+
console.log('[minusx] Coordinated metadata processing complete')
267+
268+
const result = {
269+
cardsHash,
270+
dbSchemaHash,
271+
fieldsHash
272+
}
228273

229-
const [cardsHash, dbSchemaHash, fieldsHash] = await Promise.all([
230-
processMetadataWithCaching('cards', async () => cards),
231-
processMetadataWithCaching('dbSchema', async () => dbSchema),
232-
processMetadataWithCaching('fields', async () => filteredFields)
233-
])
274+
// Cache the result for this database ID
275+
dispatch(setMetadataProcessingCache({ dbId: selectedDbId, result }))
276+
console.log(`[minusx] Cached metadata processing result for database ${selectedDbId}`)
277+
278+
return result
279+
} finally {
280+
// Clean up the ongoing processing tracking
281+
ongoingMetadataProcessing.delete(selectedDbId)
282+
}
283+
})()
234284

235-
console.log('[minusx] Coordinated metadata processing complete')
285+
// Store the promise in the map
286+
ongoingMetadataProcessing.set(selectedDbId, processingPromise)
236287

237-
return {
238-
cardsHash,
239-
dbSchemaHash,
240-
fieldsHash
241-
}
288+
return await processingPromise
242289
}
243290

0 commit comments

Comments
 (0)