Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/x-scheduler-internals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -237,12 +239,145 @@ 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) {
expect(lookup[occurrence.key]).to.equal(occurrence.position.firstIndex);
}
});
});

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<EventBuilder['toOccurrence']>[],
maxSpan: number,
) {
const sortedOccurrences = sortEventOccurrences(occurrences);
const firstIndexLookup: Record<string, number> = {};
let maxIndex = 1;

for (let index = 0; index < sortedOccurrences.length; index += 1) {
const occurrence = sortedOccurrences[index];
const usedIndexes = new Set<number>();
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,
};
}
Loading
Loading