Repository: urlaubsverwaltung/urlaubsverwaltung (java)

Issue:
Erzeugen von ICal Dateien mit iCal4j 4.x deutlich langsamer
3.2.18 -> 23865.760 ± 1359.459  ops/s
4.0.2  -> 14623.176 ± 681.583  ops/s

Ical4j ist um etwa 38,73 % langsamer geworden, wenn die Geschwindigkeit von 23865.760 ops/s auf 14623.176 ops/s gesunken ist. ​​

Und dies nur bei einer Absence.

Mit Version 3.2.18

```bash
# JMH version: 1.37
# VM version: JDK 21.0.2, OpenJDK 64-Bit Server VM, 21.0.2+13-58
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 2000 ms each
# Measurement: 5 iterations, 2000 ms each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.synyx.urlaubsverwaltung.calendar.ICalServiceBenchmarkTest.benchmark

# Run progress: 0.00% complete, ETA 00:01:00
# Fork: 1 of 3
# Warmup Iteration   1: 5328.030 ops/s
# Warmup Iteration   2: 21287.367 ops/s
# Warmup Iteration   3: 24935.619 ops/s
# Warmup Iteration   4: 24939.304 ops/s
# Warmup Iteration   5: 23970.963 ops/s
Iteration   1: 24012.554 ops/s
Iteration   2: 24144.108 ops/s
Iteration   3: 24157.032 ops/s
Iteration   4: 24955.883 ops/s
Iteration   5: 24968.996 ops/s

# Run progress: 33.33% complete, ETA 00:00:42
# Fork: 2 of 3
# Warmup Iteration   1: 11224.323 ops/s
# Warmup Iteration   2: 23287.900 ops/s
# Warmup Iteration   3: 25372.291 ops/s
# Warmup Iteration   4: 24766.918 ops/s
# Warmup Iteration   5: 24953.585 ops/s
Iteration   1: 24920.953 ops/s
Iteration   2: 24181.162 ops/s
Iteration   3: 24745.129 ops/s
Iteration   4: 25362.229 ops/s
Iteration   5: 25211.406 ops/s

# Run progress: 66.67% complete, ETA 00:00:20
# Fork: 3 of 3
# Warmup Iteration   1: 8240.461 ops/s
# Warmup Iteration   2: 19431.596 ops/s
# Warmup Iteration   3: 21169.993 ops/s
# Warmup Iteration   4: 21780.823 ops/s
# Warmup Iteration   5: 22448.011 ops/s
Iteration   1: 22464.740 ops/s
Iteration   2: 22759.504 ops/s
Iteration   3: 21334.541 ops/s
Iteration   4: 22264.313 ops/s
Iteration   5: 22503.850 ops/s


Result "org.synyx.urlaubsverwaltung.calendar.ICalServiceBenchmarkTest.benchmark":
  23865.760 ±(99.9%) 1359.459 ops/s [Average]
  (min, avg, max) = (21334.541, 23865.760, 25362.229), stdev = 1271.639
  CI (99.9%): [22506.301, 25225.219] (assumes normal distribution)


# Run complete. Total time: 00:01:02

Benchmark                            Mode  Cnt      Score      Error  Units
ICalServiceBenchmarkTest.benchmark  thrpt   15  23865.760 ± 1359.459  ops/s
```

mit Version 4.0.2

```bash
# JMH version: 1.37
# VM version: JDK 21.0.2, OpenJDK 64-Bit Server VM, 21.0.2+13-58
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 2000 ms each
# Measurement: 5 iterations, 2000 ms each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.synyx.urlaubsverwaltung.calendar.ICalServiceBenchmarkTest.benchmark

# Run progress: 0.00% complete, ETA 00:01:00
# Fork: 1 of 3
# Warmup Iteration   1: 3143.838 ops/s
# Warmup Iteration   2: 8886.636 ops/s
# Warmup Iteration   3: 12333.217 ops/s
# Warmup Iteration   4: 12618.510 ops/s
# Warmup Iteration   5: 13711.294 ops/s
Iteration   1: 13773.108 ops/s
Iteration   2: 13699.118 ops/s
Iteration   3: 13998.683 ops/s
Iteration   4: 14049.559 ops/s
Iteration   5: 14072.945 ops/s

# Run progress: 33.33% complete, ETA 00:00:42
# Fork: 2 of 3
# Warmup Iteration   1: 5660.122 ops/s
# Warmup Iteration   2: 13510.723 ops/s
# Warmup Iteration   3: 14241.387 ops/s
# Warmup Iteration   4: 14692.912 ops/s
# Warmup Iteration   5: 14688.822 ops/s
Iteration   1: 14500.979 ops/s
Iteration   2: 14748.705 ops/s
Iteration   3: 14515.895 ops/s
Iteration   4: 14221.640 ops/s
Iteration   5: 14939.544 ops/s

# Run progress: 66.67% complete, ETA 00:00:21
# Fork: 3 of 3
# Warmup Iteration   1: 6394.670 ops/s
# Warmup Iteration   2: 14083.629 ops/s
# Warmup Iteration   3: 15313.593 ops/s
# Warmup Iteration   4: 15157.755 ops/s
# Warmup Iteration   5: 14864.908 ops/s
Iteration   1: 15480.583 ops/s
Iteration   2: 15366.696 ops/s
Iteration   3: 15385.496 ops/s
Iteration   4: 15370.634 ops/s
Iteration   5: 15224.051 ops/s


Result "org.synyx.urlaubsverwaltung.calendar.ICalServiceBenchmarkTest.benchmark":
  14623.176 ±(99.9%) 681.583 ops/s [Average]
  (min, avg, max) = (13699.118, 14623.176, 15480.583), stdev = 637.554
  CI (99.9%): [13941.592, 15304.759] (assumes normal distribution)


# Run complete. Total time: 00:01:03

Benchmark                            Mode  Cnt      Score     Error  Units
ICalServiceBenchmarkTest.benchmark  thrpt   15  14623.176 ± 681.583  ops/s
```

Geht man von mehreren `Absences` aus, in diesem Fall von 121, dann verlangsamt sich die Generierung um 42,29 % von 534.026 ops/s auf 308.193 ops/s


Pull request:
Downgrade ical4j to 3.2.18
closes #4806

Patch:
diff --git a/pom.xml b/pom.xml
index 5ce58c01a..eb1912dea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -179,7 +179,7 @@
     <dependency>
       <groupId>org.mnode.ical4j</groupId>
       <artifactId>ical4j</artifactId>
-      <version>4.0.2</version>
+      <version>3.2.18</version>
     </dependency>
 
     <!-- GOOGLE CALENDAR SYNC -->
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java b/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java
index d1daedb24..cef396e8c 100644
--- a/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java
+++ b/src/main/java/org/synyx/urlaubsverwaltung/calendar/ICalService.java
@@ -2,6 +2,8 @@ package org.synyx.urlaubsverwaltung.calendar;
 
 import net.fortuna.ical4j.data.CalendarOutputter;
 import net.fortuna.ical4j.model.Calendar;
+import net.fortuna.ical4j.model.Date;
+import net.fortuna.ical4j.model.DateTime;
 import net.fortuna.ical4j.model.ParameterList;
 import net.fortuna.ical4j.model.component.VEvent;
 import net.fortuna.ical4j.model.parameter.Cn;
@@ -15,6 +17,7 @@ import net.fortuna.ical4j.model.property.Uid;
 import net.fortuna.ical4j.model.property.XProperty;
 import net.fortuna.ical4j.validate.ValidationException;
 import org.apache.commons.codec.digest.DigestUtils;
+import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.io.ByteArrayResource;
 import org.springframework.stereotype.Service;
@@ -24,16 +27,20 @@ import org.synyx.urlaubsverwaltung.person.Person;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.URI;
-import java.time.LocalDate;
+import java.text.ParseException;
 import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Optional;
 
+import static java.lang.invoke.MethodHandles.lookup;
+import static java.util.Date.from;
 import static net.fortuna.ical4j.model.parameter.Role.REQ_PARTICIPANT;
+import static net.fortuna.ical4j.model.property.CalScale.GREGORIAN;
+import static net.fortuna.ical4j.model.property.Method.CANCEL;
 import static net.fortuna.ical4j.model.property.Transp.VALUE_TRANSPARENT;
-import static net.fortuna.ical4j.model.property.immutable.ImmutableCalScale.GREGORIAN;
-import static net.fortuna.ical4j.model.property.immutable.ImmutableMethod.CANCEL;
-import static net.fortuna.ical4j.model.property.immutable.ImmutableVersion.VERSION_2_0;
+import static net.fortuna.ical4j.model.property.Version.VERSION_2_0;
+import static org.slf4j.LoggerFactory.getLogger;
 import static org.synyx.urlaubsverwaltung.calendar.ICalType.CANCELLED;
 import static org.synyx.urlaubsverwaltung.calendar.ICalType.PUBLISHED;
 
@@ -41,8 +48,12 @@ import static org.synyx.urlaubsverwaltung.calendar.ICalType.PUBLISHED;
 @Service
 public class ICalService {
 
+    private static final Logger LOG = getLogger(lookup().lookupClass());
+
     private final CalendarProperties calendarProperties;
 
+    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
+
     @Autowired
     ICalService(CalendarProperties calendarProperties) {
         this.calendarProperties = calendarProperties;
@@ -60,8 +71,8 @@ public class ICalService {
 
     private Calendar generateCalendar(String title, List<Absence> absences, Person recipient) {
         final Calendar calendar = prepareCalendar(absences, PUBLISHED, recipient);
-        calendar.add(new XProperty("X-WR-CALNAME", title));
-        calendar.add(new RefreshInterval(new ParameterList(), calendarProperties.getRefreshInterval()));
+        calendar.getProperties().add(new XProperty("X-WR-CALNAME", title));
+        calendar.getProperties().add(new RefreshInterval(new ParameterList(), calendarProperties.getRefreshInterval()));
         return calendar;
     }
 
@@ -71,20 +82,20 @@ public class ICalService {
 
     private Calendar prepareCalendar(List<Absence> absences, ICalType method, Person recipient) {
         final Calendar calendar = new Calendar();
-        calendar.add(VERSION_2_0);
-        calendar.add(new ProdId("-//Urlaubsverwaltung//iCal4j 1.0//DE"));
-        calendar.add(GREGORIAN);
-        calendar.add(new XProperty("X-MICROSOFT-CALSCALE", GREGORIAN.getValue()));
+        calendar.getProperties().add(VERSION_2_0);
+        calendar.getProperties().add(new ProdId("-//Urlaubsverwaltung//iCal4j 1.0//DE"));
+        calendar.getProperties().add(GREGORIAN);
+        calendar.getProperties().add(new XProperty("X-MICROSOFT-CALSCALE", GREGORIAN.getValue()));
 
         if (method == CANCELLED) {
-            calendar.add(CANCEL);
+            calendar.getProperties().add(CANCEL);
         }
 
         absences.stream()
             .map(absence -> this.toVEvent(absence, method, absence.getPerson().equals(recipient)))
             .filter(Optional::isPresent)
             .map(Optional::get)
-            .forEach(calendar::add);
+            .forEach(event -> calendar.getComponents().add(event));
 
         return calendar;
     }
@@ -96,53 +107,57 @@ public class ICalService {
 
         final VEvent event;
         if (absence.isAllDay()) {
-            event = generateAllDayEvent(absence.getEventSubject(), startDateTime, endDateTime);
+            try {
+                final Date startDate = new Date(startDateTime.format(formatter));
+                if (isSameDay(startDateTime, endDateTime)) {
+                    event = new VEvent(startDate, absence.getEventSubject());
+                } else {
+                    final Date endDate = new Date(endDateTime.format(formatter));
+                    event = new VEvent(new Date(startDate), new Date(endDate), absence.getEventSubject());
+                }
+            } catch (ParseException e) {
+                LOG.warn("Could not generate all day ical event for absence {}", absence, e);
+                return Optional.empty();
+            }
+
+            event.getProperties().add(new XProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "TRUE"));
         } else {
-            event = new VEvent(startDateTime, endDateTime, absence.getEventSubject());
+            final DateTime start = new DateTime(from(startDateTime.toInstant()));
+            start.setUtc(true);
+            final DateTime end = new DateTime(from(endDateTime.toInstant()));
+            end.setUtc(true);
+
+            event = new VEvent(start, end, absence.getEventSubject());
         }
 
-        event.add(new Uid(generateUid(absence)));
+        event.getProperties().add(new Uid(generateUid(absence)));
         if (absence.getPerson().getEmail() != null) {
-            event.add(generateAttendee(absence));
+            event.getProperties().add(generateAttendee(absence));
         }
 
         if (absence.isHolidayReplacement() || !isOwn) {
-            event.add(new Transp(VALUE_TRANSPARENT));
+            event.getProperties().add(new Transp(VALUE_TRANSPARENT));
         }
 
         if (method == CANCELLED) {
-            event.add(new Sequence(1));
+            event.getProperties().add(new Sequence(1));
         }
 
-        event.add(new Organizer(URI.create("mailto:" + calendarProperties.getOrganizer())));
+        event.getProperties().add(new Organizer(URI.create("mailto:" + calendarProperties.getOrganizer())));
 
         return Optional.of(event);
     }
 
-    private VEvent generateAllDayEvent(String eventSubject, ZonedDateTime startDateTime, ZonedDateTime endDateTime) {
-        final LocalDate startDate = startDateTime.toLocalDate();
-        final LocalDate endDate = endDateTime.toLocalDate();
-
-        final VEvent event;
-        if (isSameDay(startDate, endDate)) {
-            event = new VEvent(startDate, eventSubject);
-        } else {
-            event = new VEvent(startDate, endDate, eventSubject);
-        }
-        event.add(new XProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "TRUE"));
-        return event;
-    }
-
     private Attendee generateAttendee(Absence absence) {
         final Attendee attendee = new Attendee(URI.create("mailto:" + absence.getPerson().getEmail()));
-        attendee.add(REQ_PARTICIPANT);
-        attendee.add(new Cn(absence.getPerson().getNiceName()));
+        attendee.getParameters().add(REQ_PARTICIPANT);
+        attendee.getParameters().add(new Cn(absence.getPerson().getNiceName()));
 
         return attendee;
     }
 
-    private boolean isSameDay(LocalDate startDate, LocalDate endDate) {
-        return startDate.isEqual(endDate.minusDays(1));
+    private boolean isSameDay(ZonedDateTime startDateTime, ZonedDateTime endDate) {
+        return startDateTime.toLocalDate().isEqual(endDate.toLocalDate().minusDays(1));
     }
 
     private String generateUid(Absence absence) {


Test patch:
diff --git a/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java b/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java
index e6ba4dd91..99d77d0fd 100644
--- a/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java
+++ b/src/test/java/org/synyx/urlaubsverwaltung/calendar/ICalServiceTest.java
@@ -107,8 +107,8 @@ class ICalServiceTest {
                 REFRESH-INTERVAL:P1D
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190426T080000
-                DTEND;TZID=Etc/UTC:20190426T120000
+                DTSTART:20190426T080000Z
+                DTEND:20190426T120000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:45BCD3F64AD76CEF9040DF93047C41B3
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -173,8 +173,8 @@ class ICalServiceTest {
                 REFRESH-INTERVAL:P1D
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -208,8 +208,8 @@ class ICalServiceTest {
                 REFRESH-INTERVAL:P1D
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -243,8 +243,8 @@ class ICalServiceTest {
                 METHOD:CANCEL
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org
@@ -277,8 +277,8 @@ class ICalServiceTest {
                 X-MICROSOFT-CALSCALE:GREGORIAN
                 BEGIN:VEVENT
                 DTSTAMP:<removedByConversionMethod>
-                DTSTART;TZID=Etc/UTC:20190526T120000
-                DTEND;TZID=Etc/UTC:20190526T160000
+                DTSTART:20190526T120000Z
+                DTEND:20190526T160000Z
                 SUMMARY:Marlene Muster abwesend
                 UID:BB2267885BD8DF263E88D3062853E8A7
                 ATTENDEE;ROLE=REQ-PARTICIPANT;CN=Marlene Muster:mailto:muster@example.org