Skip to content

Commit 10b5d87

Browse files
committed
fix: resolve performance problems by getting rid of huge reactive array
1 parent ce467f9 commit 10b5d87

1 file changed

Lines changed: 58 additions & 21 deletions

File tree

custom/VisionAction.vue

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@
6767
:click-to-close-outside="false"
6868
>
6969
<div class="bulk-vision-table flex flex-col items-center gap-3 md:gap-4 overflow-y-auto">
70-
<template v-if="globalState.records.length && popupMode === 'generation'" >
70+
<template v-if="recordsList.length && popupMode === 'generation'" >
7171
<VisionTable
7272
class="md:max-h-[75vh] max-w-[1560px] w-full h-full"
73-
:records="globalState.records"
73+
:records="recordsList"
7474
:meta="props.meta"
7575
:tableHeaders="tableHeaders"
7676
:customFieldNames="customFieldNames"
@@ -195,10 +195,9 @@ type RecordState = {
195195
listOfImageNotGenerated: Record<string, any>;
196196
};
197197
198-
const globalState = reactive<{ records: RecordState[]; recordsById: Record<string, RecordState> }>({
199-
records: [],
200-
recordsById: {},
201-
});
198+
const recordIds = ref<Array<string | number>>([]);
199+
const recordsById = new Map<string, RecordState>();
200+
const uncheckedRecordIds = new Set<string>();
202201
203202
defineExpose({
204203
click
@@ -218,11 +217,16 @@ const generationPrompts = ref<any>({});
218217
const isDataSaved = ref(false);
219218
const overwriteExistingValues = ref<boolean>(false);
220219
221-
const checkedCount = computed(() => globalState.records.filter(record => record.isChecked).length);
222-
const isProcessingAny = computed(() => globalState.records.some(record => record.status === 'processing'));
220+
const checkedCount = computed(() => recordIds.value.length - uncheckedRecordIds.size);
221+
const isProcessingAny = computed(() => Array.from(recordsById.values()).some(record => record.status === 'processing'));
223222
224223
const tableHeaders = computed(() => generateTableHeaders(props.meta.outputFields));
225224
const customFieldNames = computed(() => tableHeaders.value.slice((props.meta.isAttachFiles) ? 3 : 2).map(h => h.fieldName));
225+
const recordsVersion = ref(0);
226+
const recordsList = computed(() => {
227+
recordsVersion.value;
228+
return recordIds.value.map(id => getOrCreateRecord(id));
229+
});
226230
227231
const openDialog = async () => {
228232
window.addEventListener('beforeunload', beforeUnloadHandler);
@@ -281,9 +285,9 @@ async function runAiActions() {
281285
return;
282286
}
283287
const limit = pLimit(props.meta.concurrencyLimit || 10);
284-
const tasks = globalState.records
285-
.filter(record => record.isChecked)
286-
.map(record => limit(() => processOneRecord(String(record.id))));
288+
const tasks = recordIds.value
289+
.filter(id => !uncheckedRecordIds.has(String(id)))
290+
.map(id => limit(() => processOneRecord(String(id))));
287291
await Promise.all(tasks);
288292
}
289293
@@ -300,14 +304,30 @@ const closeDialog = () => {
300304
301305
async function initializeGlobalState() {
302306
const ids = await getListOfIds();
303-
const records = ids.map((recordId: any) => createEmptyRecord(recordId));
304-
globalState.records.splice(0, globalState.records.length, ...records);
305-
globalState.recordsById = Object.fromEntries(records.map(record => [String(record.id), record]));
307+
recordIds.value = ids;
308+
recordsById.clear();
309+
uncheckedRecordIds.clear();
306310
}
307311
308312
function resetGlobalState() {
309-
globalState.records.splice(0, globalState.records.length);
310-
globalState.recordsById = {};
313+
recordIds.value = [];
314+
recordsById.clear();
315+
uncheckedRecordIds.clear();
316+
}
317+
318+
function getOrCreateRecord(recordId: string | number): RecordState {
319+
const key = String(recordId);
320+
let record = recordsById.get(key);
321+
if (!record) {
322+
record = createEmptyRecord(recordId);
323+
record.isChecked = !uncheckedRecordIds.has(key);
324+
recordsById.set(key, record);
325+
}
326+
return record;
327+
}
328+
329+
function touchRecords() {
330+
recordsVersion.value += 1;
311331
}
312332
313333
function createImageFieldMap<T>(factory: () => T): Record<string, T> {
@@ -346,11 +366,12 @@ function createEmptyRecord(recordId: string | number): RecordState {
346366
}
347367
348368
async function processOneRecord(recordId: string) {
349-
const record = globalState.recordsById[recordId];
369+
const record = getOrCreateRecord(recordId);
350370
if (!record || !record.isChecked) {
351371
return;
352372
}
353373
record.status = 'processing';
374+
touchRecords();
354375
record.imageGenerationFailed = false;
355376
record.imageGenerationErrorMessage = '';
356377
record.imageToTextErrorMessages = {};
@@ -365,6 +386,7 @@ async function processOneRecord(recordId: string) {
365386
record.aiStatus.generatedImages = true;
366387
record.aiStatus.analyzedImages = true;
367388
record.aiStatus.analyzedNoImages = true;
389+
touchRecords();
368390
return;
369391
}
370392
record.label = oldDataResult._label || record.label;
@@ -387,6 +409,7 @@ async function processOneRecord(recordId: string) {
387409
const results = await Promise.allSettled(actions.map(actionType => runActionForRecord(record, actionType)));
388410
const hasError = results.some(result => result.status === 'rejected');
389411
record.status = hasError ? 'failed' : 'completed';
412+
touchRecords();
390413
}
391414
392415
async function checkRateLimits() {
@@ -535,6 +558,7 @@ function applyJobResult(record: RecordState, job: any, actionType: 'analyze' | '
535558
...(job?.result || {}),
536559
};
537560
}
561+
touchRecords();
538562
}
539563
540564
function applyJobFailure(record: RecordState, job: any, actionType: 'analyze' | 'analyze_no_images' | 'generate_images') {
@@ -555,6 +579,7 @@ function applyJobFailure(record: RecordState, job: any, actionType: 'analyze' |
555579
record.textToTextErrorMessages[props.meta.outputPlainFields[field]] = job?.error || 'Unknown error';
556580
}
557581
}
582+
touchRecords();
558583
}
559584
560585
async function waitForRefresh(actionType: 'analyze' | 'analyze_no_images' | 'generate_images') {
@@ -612,6 +637,7 @@ function initializeRecordData(record: RecordState, oldRecord: Record<string, any
612637
newOldData._label = oldRecord._label;
613638
record.data = newData;
614639
record.oldData = newOldData;
640+
touchRecords();
615641
}
616642
617643
function normalizeEnumValue(key: string, value: any) {
@@ -633,6 +659,7 @@ async function fetchImages(record: RecordState, oldRecord: Record<string, any>)
633659
},
634660
});
635661
record.images = res.images?.[0] || [];
662+
touchRecords();
636663
} catch (error) {
637664
console.error('Failed to get images:', error);
638665
isError.value = true;
@@ -675,7 +702,10 @@ function handleTableError(errorData) {
675702
errorMessage.value = errorData.errorMessage;
676703
}
677704
async function prepareDataForSave() {
678-
const checkedRecords = globalState.records.filter(record => record.isChecked);
705+
const checkedRecords = recordIds.value
706+
.filter(id => !uncheckedRecordIds.has(String(id)))
707+
.map(id => recordsById.get(String(id)))
708+
.filter((record): record is RecordState => Boolean(record));
679709
const checkedItems = checkedRecords.map(record => ({
680710
...record.data,
681711
[primaryKey]: record.id,
@@ -731,7 +761,8 @@ async function saveData() {
731761
const [reqData, checkedRecords] = await prepareDataForSave();
732762
if (!checkedRecords.some(record => record.imageGenerationFailed)) {
733763
const imagesToUpload = [];
734-
for (const [index, item] of reqData.entries()) {
764+
for (let index = 0; index < reqData.length; index++) {
765+
const item = reqData[index];
735766
const record = checkedRecords[index];
736767
for (const [key, value] of Object.entries(item)) {
737768
if (props.meta.outputImageFields?.includes(key)) {
@@ -880,13 +911,15 @@ function regenerateImages({ recordId }: { recordId: any }) {
880911
});
881912
return;
882913
}
883-
const record = globalState.recordsById[String(recordId)];
914+
const record = recordsById.get(String(recordId));
884915
if (!record) {
885916
return;
886917
}
887918
record.aiStatus.generatedImages = false;
919+
touchRecords();
888920
runActionForRecord(record, 'generate_images').catch(() => {
889921
record.aiStatus.generatedImages = true;
922+
touchRecords();
890923
});
891924
}
892925
@@ -1028,14 +1061,15 @@ async function regenerateCell(recordInfo: any) {
10281061
});
10291062
return;
10301063
}
1031-
const record = globalState.recordsById[String(recordInfo.recordId)];
1064+
const record = recordsById.get(String(recordInfo.recordId));
10321065
if (!record) {
10331066
return;
10341067
}
10351068
if (!record.regeneratingFieldsStatus[recordInfo.fieldName]) {
10361069
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
10371070
}
10381071
record.regeneratingFieldsStatus[recordInfo.fieldName] = true;
1072+
touchRecords();
10391073
const actionType = props.meta.outputFieldsForAnalizeFromImages?.includes(recordInfo.fieldName)
10401074
? 'analyze'
10411075
: props.meta.outputPlainFields?.includes(recordInfo.fieldName)
@@ -1044,6 +1078,7 @@ async function regenerateCell(recordInfo: any) {
10441078
if (!actionType) {
10451079
console.error(`Field ${recordInfo.fieldName} is not configured for analysis.`);
10461080
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
1081+
touchRecords();
10471082
return;
10481083
}
10491084
@@ -1114,6 +1149,7 @@ async function regenerateCell(recordInfo: any) {
11141149
record.aiStatus.analyzedNoImages = true;
11151150
}
11161151
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
1152+
touchRecords();
11171153
return;
11181154
} else if (res.job?.status === 'completed') {
11191155
record.data = {
@@ -1128,6 +1164,7 @@ async function regenerateCell(recordInfo: any) {
11281164
record.aiStatus.analyzedNoImages = true;
11291165
}
11301166
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
1167+
touchRecords();
11311168
}
11321169
}
11331170

0 commit comments

Comments
 (0)