Make the operation more efficient

This commit is contained in:
midzelis
2025-10-25 18:44:22 +00:00
parent 8e97c584cf
commit 9bcbf003e6
7 changed files with 220 additions and 51 deletions

View File

@@ -4,9 +4,9 @@ import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils'; import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
import { plainDateTimeCompare } from '$lib/utils/timeline-util'; import { plainDateTimeCompare } from '$lib/utils/timeline-util';
import { SvelteSet } from 'svelte/reactivity'; import { onCreateDayGroup } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
import type { MonthGroup } from './month-group.svelte'; import type { MonthGroup } from './month-group.svelte';
import type { AssetOperation, Direction, MoveAsset, TimelineAsset } from './types'; import type { AssetOperation, Direction, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
export class DayGroup { export class DayGroup {
@@ -31,6 +31,7 @@ export class DayGroup {
this.monthGroup = monthGroup; this.monthGroup = monthGroup;
this.day = day; this.day = day;
this.groupTitle = groupTitle; this.groupTitle = groupTitle;
onCreateDayGroup(this);
} }
get top() { get top() {
@@ -104,15 +105,18 @@ export class DayGroup {
runAssetOperation(ids: Set<string>, operation: AssetOperation) { runAssetOperation(ids: Set<string>, operation: AssetOperation) {
if (ids.size === 0) { if (ids.size === 0) {
return { return {
moveAssets: [] as MoveAsset[], moveAssets: [] as TimelineAsset[],
processedIds: new SvelteSet<string>(), // eslint-disable-next-line svelte/prefer-svelte-reactivity
processedIds: new Set<string>(),
unprocessedIds: ids, unprocessedIds: ids,
changedGeometry: false, changedGeometry: false,
}; };
} }
const unprocessedIds = new SvelteSet<string>(ids); // eslint-disable-next-line svelte/prefer-svelte-reactivity
const processedIds = new SvelteSet<string>(); const unprocessedIds = new Set<string>(ids);
const moveAssets: MoveAsset[] = []; // eslint-disable-next-line svelte/prefer-svelte-reactivity
const processedIds = new Set<string>();
const moveAssets: TimelineAsset[] = [];
let changedGeometry = false; let changedGeometry = false;
for (const assetId of unprocessedIds) { for (const assetId of unprocessedIds) {
const index = this.viewerAssets.findIndex((viewAsset) => viewAsset.id == assetId); const index = this.viewerAssets.findIndex((viewAsset) => viewAsset.id == assetId);
@@ -121,6 +125,7 @@ export class DayGroup {
} }
const asset = this.viewerAssets[index].asset!; const asset = this.viewerAssets[index].asset!;
// save old time, pre-mutating operation
const oldTime = { ...asset.localDateTime }; const oldTime = { ...asset.localDateTime };
const opResult = operation(asset); const opResult = operation(asset);
let remove = false; let remove = false;
@@ -128,10 +133,12 @@ export class DayGroup {
remove = (opResult as { remove: boolean }).remove ?? false; remove = (opResult as { remove: boolean }).remove ?? false;
} }
const newTime = asset.localDateTime; const newTime = asset.localDateTime;
if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) { if (
const { year, month, day } = newTime; !remove &&
(oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day)
) {
remove = true; remove = true;
moveAssets.push({ asset, date: { year, month, day } }); moveAssets.push(asset);
} }
unprocessedIds.delete(assetId); unprocessedIds.delete(assetId);
processedIds.add(assetId); processedIds.add(assetId);

View File

@@ -0,0 +1,16 @@
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
let testHooks: TestHooks | undefined = undefined;
export type TestHooks = {
onCreateMonthGroup(monthGroup: MonthGroup): unknown;
onCreateDayGroup(dayGroup: DayGroup): unknown;
};
export const setTestHooks = (hooks: TestHooks) => {
testHooks = hooks;
};
export const onCreateMonthGroup = (monthGroup: MonthGroup) => testHooks?.onCreateMonthGroup(monthGroup);
export const onCreateDayGroup = (dayGroup: DayGroup) => testHooks?.onCreateDayGroup(dayGroup);

View File

@@ -9,7 +9,7 @@ import {
fromTimelinePlainDateTime, fromTimelinePlainDateTime,
fromTimelinePlainYearMonth, fromTimelinePlainYearMonth,
getTimes, getTimes,
setDifference, setDifferenceInPlace,
type TimelineDateTime, type TimelineDateTime,
type TimelineYearMonth, type TimelineYearMonth,
} from '$lib/utils/timeline-util'; } from '$lib/utils/timeline-util';
@@ -17,11 +17,11 @@ import {
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { SvelteSet } from 'svelte/reactivity'; import { onCreateMonthGroup } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
import { DayGroup } from './day-group.svelte'; import { DayGroup } from './day-group.svelte';
import { GroupInsertionCache } from './group-insertion-cache.svelte'; import { GroupInsertionCache } from './group-insertion-cache.svelte';
import type { TimelineManager } from './timeline-manager.svelte'; import type { TimelineManager } from './timeline-manager.svelte';
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types'; import type { AssetDescriptor, AssetOperation, Direction, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
export class MonthGroup { export class MonthGroup {
@@ -76,6 +76,7 @@ export class MonthGroup {
if (loaded) { if (loaded) {
this.isLoaded = true; this.isLoaded = true;
} }
onCreateMonthGroup(this);
} }
set intersecting(newValue: boolean) { set intersecting(newValue: boolean) {
@@ -119,26 +120,29 @@ export class MonthGroup {
runAssetOperation(ids: Set<string>, operation: AssetOperation) { runAssetOperation(ids: Set<string>, operation: AssetOperation) {
if (ids.size === 0) { if (ids.size === 0) {
return { return {
moveAssets: [] as MoveAsset[], moveAssets: [] as TimelineAsset[],
processedIds: new SvelteSet<string>(), // eslint-disable-next-line svelte/prefer-svelte-reactivity
processedIds: new Set<string>(),
unprocessedIds: ids, unprocessedIds: ids,
changedGeometry: false, changedGeometry: false,
}; };
} }
const { dayGroups } = this; const { dayGroups } = this;
let combinedChangedGeometry = false; let combinedChangedGeometry = false;
let idsToProcess = new SvelteSet(ids); // eslint-disable-next-line svelte/prefer-svelte-reactivity
const idsProcessed = new SvelteSet<string>(); const idsToProcess = new Set(ids);
const combinedMoveAssets: MoveAsset[][] = []; // eslint-disable-next-line svelte/prefer-svelte-reactivity
const idsProcessed = new Set<string>();
const combinedMoveAssets: TimelineAsset[] = [];
let index = dayGroups.length; let index = dayGroups.length;
while (index--) { while (index--) {
if (idsToProcess.size > 0) { if (idsToProcess.size > 0) {
const group = dayGroups[index]; const group = dayGroups[index];
const { moveAssets, processedIds, changedGeometry } = group.runAssetOperation(ids, operation); const { moveAssets, processedIds, changedGeometry } = group.runAssetOperation(ids, operation);
if (moveAssets.length > 0) { if (moveAssets.length > 0) {
combinedMoveAssets.push(moveAssets); combinedMoveAssets.push(...moveAssets);
} }
idsToProcess = setDifference(idsToProcess, processedIds); setDifferenceInPlace(idsToProcess, processedIds);
for (const id of processedIds) { for (const id of processedIds) {
idsProcessed.add(id); idsProcessed.add(id);
} }
@@ -150,7 +154,7 @@ export class MonthGroup {
} }
} }
return { return {
moveAssets: combinedMoveAssets.flat(), moveAssets: combinedMoveAssets,
unprocessedIds: idsToProcess, unprocessedIds: idsToProcess,
processedIds: idsProcessed, processedIds: idsProcessed,
changedGeometry: combinedChangedGeometry, changedGeometry: combinedChangedGeometry,

View File

@@ -1,10 +1,14 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { sdkMock } from '$lib/__mocks__/sdk.mock';
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte'; import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
import { setTestHooks } from '$lib/managers/timeline-manager/internal/TestHooks.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { AbortError } from '$lib/utils'; import { AbortError } from '$lib/utils';
import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util'; import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory'; import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
import { tick } from 'svelte'; import { tick } from 'svelte';
import type { MockInstance } from 'vitest';
import { TimelineManager } from './timeline-manager.svelte'; import { TimelineManager } from './timeline-manager.svelte';
import type { TimelineAsset } from './types'; import type { TimelineAsset } from './types';
@@ -299,6 +303,122 @@ describe('TimelineManager', () => {
}); });
}); });
describe('ensure efficient timeline operations', () => {
let timelineManager: TimelineManager;
let month1day1asset1: TimelineAsset,
month1day2asset1: TimelineAsset,
month1day2asset2: TimelineAsset,
month1day3asset1: TimelineAsset,
month2day1asset1: TimelineAsset,
month2day2asset1: TimelineAsset,
month2day2asset2: TimelineAsset;
type DayMocks = {
layoutFn: MockInstance;
sortAssetsFn: MockInstance;
};
type MonthMocks = {
sortDayGroupsFn: MockInstance;
};
const dayGroups = new Map<DayGroup, DayMocks>();
const monthGroups = new Map<MonthGroup, MonthMocks>();
beforeEach(async () => {
timelineManager = new TimelineManager();
setTestHooks({
onCreateDayGroup: (dayGroup: DayGroup) => {
dayGroups.set(dayGroup, {
layoutFn: vi.spyOn(dayGroup, 'layout'),
sortAssetsFn: vi.spyOn(dayGroup, 'sortAssets'),
});
},
onCreateMonthGroup: (monthGroup: MonthGroup) => {
monthGroups.set(monthGroup, {
sortDayGroupsFn: vi.spyOn(monthGroup, 'sortDayGroups'),
});
},
});
sdkMock.getTimeBuckets.mockResolvedValue([]);
month1day1asset1 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);
month1day2asset1 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T12:00:00.000Z'),
}),
);
month1day2asset2 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-15T13:00:00.000Z'),
}),
);
month1day3asset1 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-16T12:00:00.000Z'),
}),
);
month2day1asset1 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-16T12:00:00.000Z'),
}),
);
month2day2asset1 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-18T12:00:00.000Z'),
}),
);
month2day2asset2 = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-02-18T13:00:00.000Z'),
}),
);
await timelineManager.updateViewport({ width: 1588, height: 1000 });
timelineManager.upsertAssets([
month1day1asset1,
month1day2asset1,
month1day2asset2,
month1day3asset1,
month2day1asset1,
month2day2asset1,
month2day2asset2,
]);
vitest.resetAllMocks();
});
it.skip('Not Ready Yet - optimizations not complete: moving asset between months only sorts/layout the affected months once', () => {
// move from 2024-01-15 to 2024-01-16
timelineManager.updateAssetOperation([month1day2asset1.id], (asset) => {
asset.localDateTime.day = asset.localDateTime.day + 1;
});
for (const [day, mocks] of dayGroups) {
if (day.day === 15 && day.monthGroup.yearMonth.month === 1) {
// source - should be layout once
expect.soft(mocks.layoutFn).toBeCalledTimes(1);
expect.soft(mocks.sortAssetsFn).toBeCalledTimes(1);
}
if (day.day === 16 && day.monthGroup.yearMonth.month === 1) {
// target - should be layout once
expect.soft(mocks.layoutFn).toBeCalledTimes(1);
expect.soft(mocks.sortAssetsFn).toBeCalledTimes(1);
}
// everything else - should not be layed-out
expect.soft(mocks.layoutFn).toBeCalledTimes(0);
expect.soft(mocks.sortAssetsFn).toBeCalledTimes(0);
}
for (const [_, mocks] of monthGroups) {
// if the day itself did not change, probably no need to sort it
// in the timeline manager, the day-group identity is immutable - you will never
// "move" a whole day to another day - only the assets inside will be moved from
// one to the other.
expect.soft(mocks.sortDayGroupsFn).toBeCalledTimes(0);
}
});
});
describe('upsertAssets', () => { describe('upsertAssets', () => {
let timelineManager: TimelineManager; let timelineManager: TimelineManager;

View File

@@ -15,7 +15,7 @@ import {
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte'; import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
import { CancellableTask } from '$lib/utils/cancellable-task'; import { CancellableTask } from '$lib/utils/cancellable-task';
import { import {
setDifference, setDifferenceInPlace,
toTimelineAsset, toTimelineAsset,
type TimelineDateTime, type TimelineDateTime,
type TimelineYearMonth, type TimelineYearMonth,
@@ -30,7 +30,6 @@ import type {
AssetDescriptor, AssetDescriptor,
AssetOperation, AssetOperation,
Direction, Direction,
MoveAsset,
ScrubberMonth, ScrubberMonth,
TimelineAsset, TimelineAsset,
TimelineManagerOptions, TimelineManagerOptions,
@@ -326,7 +325,7 @@ export class TimelineManager extends VirtualScrollManager {
upsertAssets(assets: TimelineAsset[]) { upsertAssets(assets: TimelineAsset[]) {
const notExcluded = assets.filter((asset) => !this.isExcluded(asset)); const notExcluded = assets.filter((asset) => !this.isExcluded(asset));
const notUpdated = this.#updateAssets(notExcluded); const notUpdated = this.#updateAssets(notExcluded);
this.addAssetsToSegments([...notUpdated]); this.addAssetsToSegments(notUpdated);
} }
async findMonthGroupForAsset(id: string) { async findMonthGroupForAsset(id: string) {
@@ -403,29 +402,40 @@ export class TimelineManager extends VirtualScrollManager {
return randomDay.viewerAssets[randomAssetIndex - accumulatedCount].asset; return randomDay.viewerAssets[randomAssetIndex - accumulatedCount].asset;
} }
/**
* Executes the given operation against every passed in asset id.
*
* @returns An object with the changed ids, unprocessed ids, and if this resulted
* in changes of the timeline geometry.
*/
updateAssetOperation(ids: string[], operation: AssetOperation) { updateAssetOperation(ids: string[], operation: AssetOperation) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity return this.#runAssetOperation(ids, operation);
return this.#runAssetOperation(new Set(ids), operation);
} }
#updateAssets(assets: TimelineAsset[]) { /**
* Looks up the specified asset from the TimelineAsset using its id, and then updates the
* existing object to match the rest of the TimelineAsset parameter.
* @returns list of assets that were updated (not found)
*/
#updateAssets(updatedAssets: TimelineAsset[]) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const lookup = new Map<string, TimelineAsset>(assets.map((asset) => [asset.id, asset])); const lookup = new Map<string, TimelineAsset>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity const ids = [];
const { unprocessedIds } = this.#runAssetOperation(new Set(lookup.keys()), (asset) => for (const asset of updatedAssets) {
updateObject(asset, lookup.get(asset.id)), ids.push(asset.id);
); lookup.set(asset.id, asset);
}
const { unprocessedIds } = this.#runAssetOperation(ids, (asset) => updateObject(asset, lookup.get(asset.id)));
const result: TimelineAsset[] = []; const result: TimelineAsset[] = [];
for (const id of unprocessedIds.values()) { for (const id of unprocessedIds) {
result.push(lookup.get(id)!); result.push(lookup.get(id)!);
} }
return result; return result;
} }
removeAssets(ids: string[]) { removeAssets(ids: string[]) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity this.#runAssetOperation(ids, () => ({ remove: true }));
const { unprocessedIds } = this.#runAssetOperation(new Set(ids), () => ({ remove: true }));
return [...unprocessedIds];
} }
protected createUpsertContext(): GroupInsertionCache { protected createUpsertContext(): GroupInsertionCache {
@@ -459,26 +469,26 @@ export class TimelineManager extends VirtualScrollManager {
this.updateIntersections(); this.updateIntersections();
} }
#runAssetOperation(ids: Set<string>, operation: AssetOperation) { #runAssetOperation(ids: string[], operation: AssetOperation) {
if (ids.size === 0) { if (ids.length === 0) {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
return { processedIds: new Set(), unprocessedIds: ids, changedGeometry: false }; return { processedIds: new Set<string>(), unprocessedIds: new Set<string>(), changedGeometry: false };
} }
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const changedMonthGroups = new Set<MonthGroup>(); const changedMonthGroups = new Set<MonthGroup>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
let idsToProcess = new Set(ids); const idsToProcess = new Set(ids);
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const idsProcessed = new Set<string>(); const idsProcessed = new Set<string>();
const combinedMoveAssets: MoveAsset[][] = []; const combinedMoveAssets: TimelineAsset[] = [];
for (const month of this.months) { for (const month of this.months) {
if (idsToProcess.size > 0) { if (idsToProcess.size > 0) {
const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation); const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation);
if (moveAssets.length > 0) { if (moveAssets.length > 0) {
combinedMoveAssets.push(moveAssets); combinedMoveAssets.push(...moveAssets);
} }
idsToProcess = setDifference(idsToProcess, processedIds); setDifferenceInPlace(idsToProcess, processedIds);
for (const id of processedIds) { for (const id of processedIds) {
idsProcessed.add(id); idsProcessed.add(id);
} }
@@ -488,7 +498,7 @@ export class TimelineManager extends VirtualScrollManager {
} }
} }
if (combinedMoveAssets.length > 0) { if (combinedMoveAssets.length > 0) {
this.addAssetsToSegments(combinedMoveAssets.flat().map((a) => a.asset)); this.addAssetsToSegments(combinedMoveAssets);
} }
const changedGeometry = changedMonthGroups.size > 0; const changedGeometry = changedMonthGroups.size > 0;
for (const month of changedMonthGroups) { for (const month of changedMonthGroups) {

View File

@@ -1,4 +1,4 @@
import type { TimelineDate, TimelineDateTime, TimelineYearMonth } from '$lib/utils/timeline-util'; import type { TimelineDateTime, TimelineYearMonth } from '$lib/utils/timeline-util';
import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk'; import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk';
export type ViewportTopMonth = TimelineYearMonth | undefined | 'lead-in' | 'lead-out'; export type ViewportTopMonth = TimelineYearMonth | undefined | 'lead-in' | 'lead-out';
@@ -37,9 +37,7 @@ export type TimelineAsset = {
longitude?: number | null; longitude?: number | null;
}; };
export type AssetOperation = (asset: TimelineAsset) => { remove: boolean } | unknown; export type AssetOperation = (asset: TimelineAsset) => unknown;
export type MoveAsset = { asset: TimelineAsset; date: TimelineDate };
export interface Viewport { export interface Viewport {
width: number; width: number;

View File

@@ -3,7 +3,6 @@ import { locale } from '$lib/stores/preferences.store';
import { getAssetRatio } from '$lib/utils/asset-utils'; import { getAssetRatio } from '$lib/utils/asset-utils';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
import { DateTime, type LocaleOptions } from 'luxon'; import { DateTime, type LocaleOptions } from 'luxon';
import { SvelteSet } from 'svelte/reactivity';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
// Move type definitions to the top // Move type definitions to the top
@@ -222,8 +221,13 @@ export const plainDateTimeCompare = (ascending: boolean, a: TimelineDateTime, b:
return aDateTime.millisecond - bDateTime.millisecond; return aDateTime.millisecond - bDateTime.millisecond;
}; };
export function setDifference<T>(setA: Set<T>, setB: Set<T>): SvelteSet<T> { export function setDifference<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const result = new SvelteSet<T>(); // Check if native Set.prototype.difference is available (ES2025)
const setWithDifference = setA as unknown as Set<T> & { difference?: (other: Set<T>) => Set<T> };
if (setWithDifference.difference && typeof setWithDifference.difference === 'function') {
return setWithDifference.difference(setB);
}
const result = new Set<T>();
for (const value of setA) { for (const value of setA) {
if (!setB.has(value)) { if (!setB.has(value)) {
result.add(value); result.add(value);
@@ -231,3 +235,13 @@ export function setDifference<T>(setA: Set<T>, setB: Set<T>): SvelteSet<T> {
} }
return result; return result;
} }
/**
* Removes all elements of setB from setA in-place (mutates setA).
*/
export function setDifferenceInPlace<T>(setA: Set<T>, setB: Set<T>): Set<T> {
for (const value of setB) {
setA.delete(value);
}
return setA;
}