From 967efd88f45a1c8a927ddf570b0ac01db95655ab Mon Sep 17 00:00:00 2001 From: delangle Date: Tue, 18 Aug 2026 11:39:26 +0200 Subject: [PATCH 1/2] [scheduler] Improve timeline event layout performance --- packages/x-scheduler-internals/package.json | 5 +- ...ntOccurrencesWithTimelinePosition.bench.ts | 33 ++ ...entOccurrencesWithTimelinePosition.test.ts | 137 +++++++- ...useEventOccurrencesWithTimelinePosition.ts | 315 +++++++++--------- .../x-scheduler-internals/tsconfig.build.json | 2 +- .../content/EventTimelinePremiumContent.tsx | 4 +- .../dependencyArrowGeometry.ts | 2 +- 7 files changed, 335 insertions(+), 163 deletions(-) create mode 100644 packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.bench.ts diff --git a/packages/x-scheduler-internals/package.json b/packages/x-scheduler-internals/package.json index 042dd0ae0e2a8..293cafd706384 100644 --- a/packages/x-scheduler-internals/package.json +++ b/packages/x-scheduler-internals/package.json @@ -57,8 +57,9 @@ ], "scripts": { "typescript": "tsgo -p tsconfig.json", - "build": "code-infra build --tsgo --copy \"src/**/*.css\" --copy \"src/**/*.css:esm\"", - "prebuild": "rimraf build tsconfig.build.tsbuildinfo" + "build": "code-infra build --tsgo --copy \"src/**/*.css\" --copy \"src/**/*.css:esm\" --ignore 'src/**/*.bench.ts'", + "prebuild": "rimraf build tsconfig.build.tsbuildinfo", + "bench:jsdom": "pnpm vitest bench -c ./vitest.config.jsdom.mts" }, "repository": { "type": "git", diff --git a/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.bench.ts b/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.bench.ts new file mode 100644 index 0000000000000..45c7d105bdc56 --- /dev/null +++ b/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.bench.ts @@ -0,0 +1,33 @@ +import { bench, describe } from 'vitest'; +import type { SchedulerEventOccurrence } from '../models'; +import { computeOccurrencesWithTimelinePosition } from './useEventOccurrencesWithTimelinePosition'; + +const denseOccurrences = createOccurrences(2_000, () => [0, 60]); +const rollingOccurrences = createOccurrences(10_000, (index) => [index, index + 100]); + +describe('timeline occurrence positioning', () => { + bench('2,000 fully overlapping occurrences', () => { + computeOccurrencesWithTimelinePosition(denseOccurrences, Infinity); + }); + + bench('10,000 rolling occurrences with 100 concurrent events', () => { + computeOccurrencesWithTimelinePosition(rollingOccurrences, 1); + }); +}); + +function createOccurrences( + count: number, + getRange: (index: number) => [start: number, end: number], +) { + return Array.from({ length: count }, (_, index) => { + const [start, end] = getRange(index); + return { + id: index, + key: String(index), + displayTimezone: { + start: { timestamp: start }, + end: { timestamp: end }, + }, + } as SchedulerEventOccurrence; + }); +} diff --git a/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts b/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts index c47b346f7fcd2..98b317f947026 100644 --- a/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts +++ b/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts @@ -2,7 +2,9 @@ import { adapter, EventBuilder } from 'test/utils/scheduler'; import { renderHook } from '@mui/internal-test-utils'; import type { SchedulerProcessedEvent } from '@mui/x-scheduler-internals/models'; import { getOccurrencesFromEvents } from '@mui/x-scheduler-internals/internals'; +import { sortEventOccurrences } from '@mui/x-scheduler-internals/sort-event-occurrences'; import { + computeOccurrencesWithTimelinePosition, computeOccurrencesFirstIndexLookup, useEventOccurrencesWithTimelinePosition, } from './useEventOccurrencesWithTimelinePosition'; @@ -237,7 +239,7 @@ describe('useDayListEventOccurrencesWithPosition', () => { visibleResources: {}, recurringEventsPlugin: null, }); - const lookup = computeOccurrencesFirstIndexLookup(adapter, occurrences); + const lookup = computeOccurrencesFirstIndexLookup(occurrences); expect(Object.keys(lookup)).to.have.length(result.occurrences.length); for (const occurrence of result.occurrences) { @@ -245,4 +247,137 @@ describe('useDayListEventOccurrencesWithPosition', () => { } }); }); + + it('should update the available span when a freed lane is reused', () => { + const occurrences = [ + EventBuilder.new().id('A').singleDay('2024-01-15T10:00:00Z', 30).toOccurrence(), + EventBuilder.new().id('B').singleDay('2024-01-15T10:01:00Z', 39).toOccurrence(), + EventBuilder.new().id('C').singleDay('2024-01-15T10:10:00Z', 120).toOccurrence(), + EventBuilder.new().id('D').singleDay('2024-01-15T10:50:00Z', 60).toOccurrence(), + EventBuilder.new().id('E').singleDay('2024-01-15T11:00:00Z', 30).toOccurrence(), + ]; + + const resultBeforeReuse = computeOccurrencesWithTimelinePosition( + occurrences.slice(0, -1), + Infinity, + ); + const result = computeOccurrencesWithTimelinePosition(occurrences, Infinity); + + expect(resultBeforeReuse.occurrences[3].position).to.deep.equal({ + firstIndex: 1, + lastIndex: 2, + }); + expect(result.maxIndex).to.equal(3); + expect(result.occurrences.map(({ id, position }) => ({ id, ...position }))).to.deep.equal([ + { id: 'A', firstIndex: 1, lastIndex: 1 }, + { id: 'B', firstIndex: 2, lastIndex: 2 }, + { id: 'C', firstIndex: 3, lastIndex: 3 }, + { id: 'D', firstIndex: 1, lastIndex: 1 }, + { id: 'E', firstIndex: 2, lastIndex: 2 }, + ]); + }); + + it('should match the quadratic layout for varied event distributions', () => { + let seed = 239; + const random = () => { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + }; + + for (let iteration = 0; iteration < 100; iteration += 1) { + const occurrenceCount = 1 + Math.floor(random() * 40); + const occurrences = Array.from({ length: occurrenceCount }, (_, index) => { + const startMinutes = Math.floor(random() * 12) * 30; + const durationMinutes = Math.floor(random() * 9) * 15; + const start = adapter.addMinutes(collectionStart, startMinutes).toISOString(); + return EventBuilder.new() + .id(`${iteration}-${index}`) + .singleDay(start, durationMinutes) + .toOccurrence(); + }); + const maxSpan = [1, 2, 4, Infinity][iteration % 4]; + + const result = computeOccurrencesWithTimelinePosition(occurrences, maxSpan); + const expected = computeLayoutQuadratically(occurrences, maxSpan); + + expect(result.occurrences.map(({ id, position }) => ({ id, ...position }))).to.deep.equal( + expected.occurrences, + ); + expect(result.maxIndex).to.equal(expected.maxIndex); + } + }); + + it('should position a dense set of occurrences', () => { + const occurrences = Array.from({ length: 1_000 }, (_, index) => + EventBuilder.new().id(index).singleDay('2024-01-15T10:00:00Z', 120).toOccurrence(), + ); + + const result = computeOccurrencesWithTimelinePosition(occurrences, Infinity); + + expect(result.maxIndex).to.equal(1_000); + expect(result.occurrences[0].position).to.deep.equal({ firstIndex: 1, lastIndex: 1 }); + expect(result.occurrences[999].position).to.deep.equal({ + firstIndex: 1_000, + lastIndex: 1_000, + }); + }); }); + +function computeLayoutQuadratically( + occurrences: ReturnType[], + maxSpan: number, +) { + const sortedOccurrences = sortEventOccurrences(occurrences); + const firstIndexLookup: Record = {}; + let maxIndex = 1; + + for (let index = 0; index < sortedOccurrences.length; index += 1) { + const occurrence = sortedOccurrences[index]; + const usedIndexes = new Set(); + for (let previousIndex = 0; previousIndex < index; previousIndex += 1) { + const previousOccurrence = sortedOccurrences[previousIndex]; + if ( + previousOccurrence.displayTimezone.end.timestamp > + occurrence.displayTimezone.start.timestamp + ) { + usedIndexes.add(firstIndexLookup[previousOccurrence.key]); + } + } + + let firstIndex = 1; + while (usedIndexes.has(firstIndex)) { + firstIndex += 1; + } + firstIndexLookup[occurrence.key] = firstIndex; + maxIndex = Math.max(maxIndex, firstIndex); + } + + return { + occurrences: sortedOccurrences.map((occurrence, index) => { + const firstIndex = firstIndexLookup[occurrence.key]; + const usedIndexes = new Set( + sortedOccurrences + .filter((otherOccurrence, otherIndex) => + otherIndex < index + ? otherOccurrence.displayTimezone.end.timestamp > + occurrence.displayTimezone.start.timestamp + : otherIndex > index && + otherOccurrence.displayTimezone.start.timestamp < + occurrence.displayTimezone.end.timestamp, + ) + .map((otherOccurrence) => firstIndexLookup[otherOccurrence.key]), + ); + let lastIndex = firstIndex; + while ( + !usedIndexes.has(lastIndex + 1) && + lastIndex < maxIndex && + lastIndex - firstIndex < maxSpan - 1 + ) { + lastIndex += 1; + } + + return { id: occurrence.id, firstIndex, lastIndex }; + }), + maxIndex, + }; +} diff --git a/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts b/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts index 107107e03d7cd..bc33885b96350 100644 --- a/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts +++ b/packages/x-scheduler-internals/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts @@ -1,8 +1,6 @@ import * as React from 'react'; import { sortEventOccurrences } from '../sort-event-occurrences'; import type { SchedulerEventOccurrence, SchedulerEventOccurrencePlaceholder } from '../models'; -import type { Adapter } from '../use-adapter'; -import { useAdapterContext } from '../use-adapter-context'; /** * Places event occurrences for a timeline UI. @@ -11,26 +9,32 @@ export function useEventOccurrencesWithTimelinePosition( parameters: useEventOccurrencesWithTimelinePosition.Parameters, ): useEventOccurrencesWithTimelinePosition.ReturnValue { const { occurrences, maxSpan } = parameters; - const adapter = useAdapterContext(); - return React.useMemo(() => { - const sortedOccurrences = sortEventOccurrences(occurrences); - const conflicts = buildOccurrenceConflicts(adapter, sortedOccurrences); - - const { firstIndexLookup, maxIndex } = buildFirstIndexLookup(conflicts); - - const lastIndexLookup = buildLastIndexLookup(conflicts, firstIndexLookup, maxIndex, maxSpan); - - const occurrencesWithPosition = sortedOccurrences.map((occurrence) => ({ - ...occurrence, - position: { - firstIndex: firstIndexLookup[occurrence.key], - lastIndex: lastIndexLookup[occurrence.key], - }, - })); + return React.useMemo( + () => computeOccurrencesWithTimelinePosition(occurrences, maxSpan), + [occurrences, maxSpan], + ); +} - return { occurrences: occurrencesWithPosition, maxIndex }; - }, [adapter, occurrences, maxSpan]); +export function computeOccurrencesWithTimelinePosition( + occurrences: readonly SchedulerEventOccurrence[], + maxSpan: number, +): useEventOccurrencesWithTimelinePosition.ReturnValue { + const sortedOccurrences = sortEventOccurrences(occurrences); + const { firstIndexLookup, lastIndexLookup, maxIndex } = buildOccurrenceIndexLookups( + sortedOccurrences, + maxSpan, + ); + + const occurrencesWithPosition = sortedOccurrences.map((occurrence) => ({ + ...occurrence, + position: { + firstIndex: firstIndexLookup[occurrence.key], + lastIndex: lastIndexLookup[occurrence.key], + }, + })); + + return { occurrences: occurrencesWithPosition, maxIndex }; } /** @@ -39,12 +43,10 @@ export function useEventOccurrencesWithTimelinePosition( * but callable outside React (e.g. inside a `useMemo`). */ export function computeOccurrencesMaxIndex( - adapter: Adapter, occurrences: readonly SchedulerEventOccurrence[], ): number { const sortedOccurrences = sortEventOccurrences(occurrences); - const conflicts = buildOccurrenceConflicts(adapter, sortedOccurrences); - return buildFirstIndexLookup(conflicts).maxIndex; + return buildOccurrenceIndexLookups(sortedOccurrences, 1).maxIndex; } /** @@ -53,12 +55,10 @@ export function computeOccurrencesMaxIndex( * to locate occurrences in rows that are not mounted. */ export function computeOccurrencesFirstIndexLookup( - adapter: Adapter, occurrences: readonly SchedulerEventOccurrence[], ): { [occurrenceKey: string]: number } { const sortedOccurrences = sortEventOccurrences(occurrences); - const conflicts = buildOccurrenceConflicts(adapter, sortedOccurrences); - return buildFirstIndexLookup(conflicts).firstIndexLookup; + return buildOccurrenceIndexLookups(sortedOccurrences, 1).firstIndexLookup; } export namespace useEventOccurrencesWithTimelinePosition { @@ -107,168 +107,171 @@ export namespace useEventOccurrencesWithTimelinePosition { } } -/** - * Looks for conflicts between occurrences and build a list of conflicts for each occurrence. - * The provided occurrences need to be sorted by starting date-time. - */ -function buildOccurrenceConflicts( - adapter: Adapter, - occurrences: SchedulerEventOccurrence[], -): OccurrenceConflicts[] { - const getEmptyBlock = (): OccurrenceBlock => ({ occurrences: [], longestDurationMs: 0 }); - - const occurrencesBlocks: OccurrenceBlock[] = []; - let currentBlock: OccurrenceBlock = getEmptyBlock(); - let lastEndTimestamp = 0; - - // Group occurrences in non-overlapping blocks to reduce the number of comparisons when looking for conflicts. - // Computes the properties needed for each occurrence. - for (const occurrence of occurrences) { - const startTimestamp = occurrence.displayTimezone.start.timestamp; - const endTimestamp = occurrence.displayTimezone.end.timestamp; - const occurrenceDurationMs = endTimestamp - startTimestamp; - - if (startTimestamp >= lastEndTimestamp) { - if (currentBlock.occurrences.length > 0) { - occurrencesBlocks.push(currentBlock); - } - currentBlock = getEmptyBlock(); - lastEndTimestamp = 0; - } - - currentBlock.occurrences.push({ - key: occurrence.key, - startTimestamp, - endTimestamp, - }); +class BinaryIndexedTree { + private readonly values: number[]; - if (occurrenceDurationMs > currentBlock.longestDurationMs) { - currentBlock.longestDurationMs = occurrenceDurationMs; - } + constructor(size: number) { + this.values = new Array(size + 1).fill(0); + } - if (endTimestamp > lastEndTimestamp) { - lastEndTimestamp = endTimestamp; + add(index: number, value: number) { + // eslint-disable-next-line no-bitwise + for (let i = index; i < this.values.length; i += i & -i) { + this.values[i] += value; } } - if (currentBlock.occurrences.length > 0) { - occurrencesBlocks.push(currentBlock); + sum(index: number) { + let result = 0; + // eslint-disable-next-line no-bitwise + for (let i = index; i > 0; i -= i & -i) { + result += this.values[i]; + } + return result; } - // For each block, looks for conflicts between occurrences to build the conflicts list. - const conflicts: OccurrenceConflicts[] = []; - for (const block of occurrencesBlocks) { - for (let i = 0; i < block.occurrences.length; i += 1) { - const occurrence = block.occurrences[i]; - const conflictsBefore = new Set(); - const conflictsAfter = new Set(); - - for (let j = i + 1; j < block.occurrences.length; j += 1) { - const occurrenceA = block.occurrences[j]; - if (occurrenceA.startTimestamp < occurrence.endTimestamp) { - conflictsAfter.add(occurrenceA.key); - } else { - // We know that all the next occurrences will start even later, so we can stop here. - break; - } - } + findByOrder(order: number) { + let index = 0; + let bit = 2 ** Math.floor(Math.log2(this.values.length - 1)); + let remaining = order; - for (let j = i - 1; j >= 0; j -= 1) { - const occurrenceB = block.occurrences[j]; - const diffBetweenOccurrencesStart = occurrence.startTimestamp - occurrenceB.startTimestamp; - if (diffBetweenOccurrencesStart > block.longestDurationMs) { - // We know that all the previous occurrences won't end after the start of the occurrence we are getting conflicts for, so we can stop here. - break; - } - - if (occurrenceB.endTimestamp > occurrence.startTimestamp) { - conflictsBefore.add(occurrenceB.key); - } + while (bit > 0) { + const nextIndex = index + bit; + if (nextIndex < this.values.length && this.values[nextIndex] < remaining) { + index = nextIndex; + remaining -= this.values[nextIndex]; } - - conflicts.push({ key: occurrence.key, before: conflictsBefore, after: conflictsAfter }); + // eslint-disable-next-line no-bitwise + bit >>= 1; } - } - return conflicts; + return index + 1; + } } -function buildFirstIndexLookup(conflicts: OccurrenceConflicts[]) { - let maxIndex: number = 1; +function buildOccurrenceIndexLookups(occurrences: SchedulerEventOccurrence[], maxSpan: number) { + const activeOccurrences: ActiveOccurrence[] = []; + const availableIndexes: number[] = []; + const activeOccurrenceByIndex: Array = []; + const activeIndexes = maxSpan >= 2 ? new BinaryIndexedTree(occurrences.length) : null; const firstIndexLookup: OccurrenceIndexLookup = {}; + const nextConflictingIndexLookup: OccurrenceIndexLookup = {}; + let nextIndex = 1; - for (const occurrence of conflicts) { - if (occurrence.before.size === 0) { - firstIndexLookup[occurrence.key] = 1; - } else { - const usedIndexes = new Set( - Array.from(occurrence.before).map( - (conflictingOccurrence) => firstIndexLookup[conflictingOccurrence], - ), - ); - let i = 1; - while (usedIndexes.has(i)) { - i += 1; + for (let occurrenceIndex = 0; occurrenceIndex < occurrences.length; occurrenceIndex += 1) { + const occurrence = occurrences[occurrenceIndex]; + const startTimestamp = occurrence.displayTimezone.start.timestamp; + + while (activeOccurrences[0]?.endTimestamp <= startTimestamp) { + const endedOccurrence = popHeap(activeOccurrences)!; + pushHeap(availableIndexes, endedOccurrence.index); + if (activeIndexes) { + activeOccurrenceByIndex[endedOccurrence.index] = undefined; + activeIndexes.add(endedOccurrence.index, -1); } - firstIndexLookup[occurrence.key] = i; - if (i > maxIndex) { - maxIndex = i; + } + + const index = popHeap(availableIndexes) ?? nextIndex; + if (index === nextIndex) { + nextIndex += 1; + } + + firstIndexLookup[occurrence.key] = index; + + if (activeIndexes) { + if (index > 1) { + // All lower indexes are active because this is the first available index. + const previousOccurrenceIndex = activeOccurrenceByIndex[index - 1]!; + const previousOccurrenceKey = occurrences[previousOccurrenceIndex].key; + nextConflictingIndexLookup[previousOccurrenceKey] = Math.min( + nextConflictingIndexLookup[previousOccurrenceKey] ?? Infinity, + index, + ); + } + + const activeIndexCount = activeIndexes.sum(index); + if (activeIndexCount < activeOccurrences.length) { + nextConflictingIndexLookup[occurrence.key] = activeIndexes.findByOrder( + activeIndexCount + 1, + ); } + + activeIndexes.add(index, 1); + activeOccurrenceByIndex[index] = occurrenceIndex; } - } - return { firstIndexLookup, maxIndex }; -} + pushHeap(activeOccurrences, { + endTimestamp: occurrence.displayTimezone.end.timestamp, + index, + }); + } -function buildLastIndexLookup( - conflicts: OccurrenceConflicts[], - firstIndexLookup: OccurrenceIndexLookup, - maxIndex: number, - maxSpan: number, -) { + const maxIndex = Math.max(1, nextIndex - 1); if (maxSpan < 2) { - return firstIndexLookup; + return { firstIndexLookup, lastIndexLookup: firstIndexLookup, maxIndex }; } const lastIndexLookup: OccurrenceIndexLookup = {}; - for (const occurrence of conflicts) { - const usedIndexes = new Set( - [...Array.from(occurrence.before), ...Array.from(occurrence.after)].map( - (conflictingOccurrence) => firstIndexLookup[conflictingOccurrence], - ), - ); + for (const occurrence of occurrences) { const firstIndex = firstIndexLookup[occurrence.key]; - let lastIndex = firstIndex; - while ( - !usedIndexes.has(lastIndex + 1) && - lastIndex < maxIndex && - lastIndex - firstIndex < maxSpan - 1 - ) { - lastIndex += 1; + const nextConflictingIndex = nextConflictingIndexLookup[occurrence.key] ?? maxIndex + 1; + lastIndexLookup[occurrence.key] = Math.min( + nextConflictingIndex - 1, + maxIndex, + firstIndex + maxSpan - 1, + ); + } + + return { firstIndexLookup, lastIndexLookup, maxIndex }; +} + +function pushHeap(heap: T[], value: T) { + heap.push(value); + let index = heap.length - 1; + while (index > 0) { + const parentIndex = Math.floor((index - 1) / 2); + if (getHeapValue(heap[parentIndex]) <= getHeapValue(value)) { + break; } - lastIndexLookup[occurrence.key] = lastIndex; + heap[index] = heap[parentIndex]; + index = parentIndex; + } + heap[index] = value; +} + +function popHeap(heap: T[]): T | undefined { + const first = heap[0]; + const last = heap.pop(); + if (heap.length === 0 || last === undefined) { + return first; } - return lastIndexLookup; + let index = 0; + while (index * 2 + 1 < heap.length) { + let childIndex = index * 2 + 1; + if ( + childIndex + 1 < heap.length && + getHeapValue(heap[childIndex + 1]) < getHeapValue(heap[childIndex]) + ) { + childIndex += 1; + } + if (getHeapValue(heap[childIndex]) >= getHeapValue(last)) { + break; + } + heap[index] = heap[childIndex]; + index = childIndex; + } + heap[index] = last; + return first; } -/** - * A block of occurrences that overlap in time. - * The occurrences of two distinct blocks never overlap in time, their conflicts can thus be computed independently. - */ -interface OccurrenceBlock { - occurrences: { - key: string; - startTimestamp: number; - endTimestamp: number; - }[]; - longestDurationMs: number; +function getHeapValue(value: number | ActiveOccurrence) { + return typeof value === 'number' ? value : value.endTimestamp; } -interface OccurrenceConflicts { - key: string; - before: Set; - after: Set; +interface ActiveOccurrence { + endTimestamp: number; + index: number; } type OccurrenceIndexLookup = { [occurrenceKey: string]: number }; diff --git a/packages/x-scheduler-internals/tsconfig.build.json b/packages/x-scheduler-internals/tsconfig.build.json index 8c04a3c95264b..25251f4ded32a 100644 --- a/packages/x-scheduler-internals/tsconfig.build.json +++ b/packages/x-scheduler-internals/tsconfig.build.json @@ -13,5 +13,5 @@ }, "references": [{ "path": "../x-internals/tsconfig.build.json" }], "include": ["src/**/*.ts*"], - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*", "src/**/tests/**/*"] + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*", "src/**/*.bench.ts*", "src/**/tests/**/*"] } diff --git a/packages/x-scheduler-premium/src/event-timeline-premium/content/EventTimelinePremiumContent.tsx b/packages/x-scheduler-premium/src/event-timeline-premium/content/EventTimelinePremiumContent.tsx index a6c20068123aa..ce5bed999dced 100644 --- a/packages/x-scheduler-premium/src/event-timeline-premium/content/EventTimelinePremiumContent.tsx +++ b/packages/x-scheduler-premium/src/event-timeline-premium/content/EventTimelinePremiumContent.tsx @@ -761,10 +761,10 @@ export const EventTimelinePremiumContent = React.forwardRef(function EventTimeli const laneCountByResource = React.useMemo(() => { const map = new Map(); for (const { resource, occurrences } of visibleResources) { - map.set(resource.id, computeOccurrencesMaxIndex(adapter, occurrences)); + map.set(resource.id, computeOccurrencesMaxIndex(occurrences)); } return map; - }, [visibleResources, adapter]); + }, [visibleResources]); const getRowHeight = React.useCallback( (row: { id: SchedulerResourceId }) => diff --git a/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-dependency-arrows/dependencyArrowGeometry.ts b/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-dependency-arrows/dependencyArrowGeometry.ts index 1923de21aa6e9..efa6c213c4875 100644 --- a/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-dependency-arrows/dependencyArrowGeometry.ts +++ b/packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-dependency-arrows/dependencyArrowGeometry.ts @@ -260,7 +260,7 @@ export function createDependencyAnchorResolver( const getLaneLookup = (rowIndex: number): { [occurrenceKey: string]: number } => { let laneLookup = laneLookupByRow.get(rowIndex); if (laneLookup == null) { - laneLookup = computeOccurrencesFirstIndexLookup(adapter, resources[rowIndex].occurrences); + laneLookup = computeOccurrencesFirstIndexLookup(resources[rowIndex].occurrences); laneLookupByRow.set(rowIndex, laneLookup); } return laneLookup; From 49d78bd045bdfa7080258814f8ae2b507b2d23ad Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 24 Aug 2026 08:33:06 +0200 Subject: [PATCH 2/2] [scheduler] Fix timeline layout benchmark types --- .../eventTimelinePremiumOccurrenceSelectors.bench.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/x-scheduler-internals-premium/src/event-timeline-premium-selectors/eventTimelinePremiumOccurrenceSelectors.bench.ts b/packages/x-scheduler-internals-premium/src/event-timeline-premium-selectors/eventTimelinePremiumOccurrenceSelectors.bench.ts index 7ca5403b0e3f3..5a06eff90b6cc 100644 --- a/packages/x-scheduler-internals-premium/src/event-timeline-premium-selectors/eventTimelinePremiumOccurrenceSelectors.bench.ts +++ b/packages/x-scheduler-internals-premium/src/event-timeline-premium-selectors/eventTimelinePremiumOccurrenceSelectors.bench.ts @@ -44,7 +44,7 @@ const groupedOccurrences = resources.map((resource, resourceIndex) => ({ })); function addLanePositionsToOccurrences(occurrences: SchedulerEventOccurrence[]) { - const firstIndexLookup = computeOccurrencesFirstIndexLookup(adapter, occurrences); + const firstIndexLookup = computeOccurrencesFirstIndexLookup(occurrences); return sortEventOccurrences(occurrences).map((occurrence) => { const firstIndex = firstIndexLookup[occurrence.key]; @@ -60,7 +60,7 @@ describe('event timeline resource layout', () => { // The virtualizer needs every resource's lane count. for (const { occurrences } of groupedOccurrences) { - result += computeOccurrencesMaxIndex(adapter, occurrences); + result += computeOccurrencesMaxIndex(occurrences); } // Mounted rows derive lanes once, while the list and event independently derive geometry. @@ -80,7 +80,7 @@ describe('event timeline resource layout', () => { // Dependency geometry only derives lanes for involved resources. for (const { occurrences } of groupedOccurrences.slice(0, 2)) { - result += Object.keys(computeOccurrencesFirstIndexLookup(adapter, occurrences)).length; + result += Object.keys(computeOccurrencesFirstIndexLookup(occurrences)).length; } benchmarkResult.value = result; @@ -91,7 +91,7 @@ describe('event timeline resource layout', () => { // The virtualizer needs every resource's lane count. for (const { occurrences } of groupedOccurrences) { - result += computeOccurrencesMaxIndex(adapter, occurrences); + result += computeOccurrencesMaxIndex(occurrences); } // Mounted rows derive lanes and geometry once, then share both with their children. @@ -107,7 +107,7 @@ describe('event timeline resource layout', () => { // Dependency geometry only derives lanes for involved resources. for (const { occurrences } of groupedOccurrences.slice(0, 2)) { - result += Object.keys(computeOccurrencesFirstIndexLookup(adapter, occurrences)).length; + result += Object.keys(computeOccurrencesFirstIndexLookup(occurrences)).length; } benchmarkResult.value = result;