Skip to content

Commit 7d71038

Browse files
authored
Pg Client: improve Interval support (#1462)
* Refactor Pg Interval codec This a preliminary commit related to issue #1281 Encoding and decoding an Interval implied creating many objects (Duration and Period are immutable). Besides, the Duration.plusXXX and Period.normalized methods perform several computations. With the proposed change: - the codec doesn't create redundant objects - a decoded interval is always normalized (i.e. the number of months is strictly smaller than 12, the number of days is strictly smaller than 30,... etc). Signed-off-by: Thomas Segismont <[email protected]> * Added a JMH test for Interval encoding The test compares the previous and the proposed solutions. It confirms the assumptions about the proposed solution: - about twice as fast - removes pressure on GC # JMH version: 1.19 # VM version: JDK 1.8.0_422, VM 25.422-b05 # VM options: -Xms8g -Xmx8g -Xmn7g # Warmup: 20 iterations, 1 s each # Measurement: 10 iterations, 2 s each # Timeout: 10 min per iteration # Threads: 1 thread, will synchronize iterations # Benchmark mode: Throughput, ops/time Benchmark Mode Cnt Score Error Units IntervalBenchmarks.encodeWithDurationAndPeriod thrpt 30 46339.530 ± 389.115 ops/ms IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.alloc.rate thrpt 30 3393.324 ± 28.481 MB/sec IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.alloc.rate.norm thrpt 30 96.000 ± 0.001 B/op IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.churn.PS_Eden_Space thrpt 30 3436.887 ± 778.345 MB/sec IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.churn.PS_Eden_Space.norm thrpt 30 97.302 ± 22.205 B/op IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.churn.PS_Survivor_Space thrpt 30 0.005 ± 0.006 MB/sec IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.churn.PS_Survivor_Space.norm thrpt 30 ≈ 10⁻⁴ B/op IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.count thrpt 30 36.000 counts IntervalBenchmarks.encodeWithDurationAndPeriod:·gc.time thrpt 30 30.000 ms IntervalBenchmarks.encodeWithParts thrpt 30 82322.542 ± 109.306 ops/ms IntervalBenchmarks.encodeWithParts:·gc.alloc.rate thrpt 30 ≈ 10⁻⁴ MB/sec IntervalBenchmarks.encodeWithParts:·gc.alloc.rate.norm thrpt 30 ≈ 10⁻⁶ B/op IntervalBenchmarks.encodeWithParts:·gc.count thrpt 30 ≈ 0 counts Signed-off-by: Thomas Segismont <[email protected]> * Interval conversion from/to Duration The conversion algorithm assumes a year last 12 months and a month lasts 30 days, as Postgres does and ISO 8601 suggests. See https://github.com/postgres/postgres/blob/5bbdfa8a18dc56d3e64aa723a68e02e897cb5ec3/src/include/datatype/timestamp.h#L116 Signed-off-by: Thomas Segismont <[email protected]> * Update vertx-pg-client/src/main/java/io/vertx/pgclient/data/Interval.java * Update vertx-pg-client/src/main/java/io/vertx/pgclient/data/Interval.java --------- Signed-off-by: Thomas Segismont <[email protected]>
1 parent 0e69ec7 commit 7d71038

File tree

9 files changed

+332
-55
lines changed

9 files changed

+332
-55
lines changed

vertx-pg-client/pom.xml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,44 @@
201201
</plugins>
202202
</build>
203203

204+
<profiles>
205+
<profile>
206+
<id>benchmarks</id>
207+
<build>
208+
<plugins>
209+
<plugin>
210+
<artifactId>maven-assembly-plugin</artifactId>
211+
<executions>
212+
<execution>
213+
<id>assemble-benchmarks</id>
214+
<phase>package</phase>
215+
<goals>
216+
<goal>single</goal>
217+
</goals>
218+
<configuration>
219+
<archive>
220+
<manifest>
221+
<mainClass>org.openjdk.jmh.Main</mainClass>
222+
</manifest>
223+
</archive>
224+
<descriptors>
225+
<descriptor>src/test/assembly/benchmarks.xml</descriptor>
226+
</descriptors>
227+
</configuration>
228+
</execution>
229+
</executions>
230+
</plugin>
231+
</plugins>
232+
</build>
233+
<dependencies>
234+
<dependency>
235+
<groupId>org.openjdk.jmh</groupId>
236+
<artifactId>jmh-generator-annprocess</artifactId>
237+
<version>${jmh.version}</version>
238+
<scope>test</scope>
239+
</dependency>
240+
</dependencies>
241+
</profile>
242+
</profiles>
243+
204244
</project>

vertx-pg-client/src/main/java/io/vertx/pgclient/data/Interval.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.vertx.pgclient.data;
22

3-
import io.vertx.codegen.annotations.DataObject;
4-
import io.vertx.core.json.JsonObject;
3+
import java.time.Duration;
54

65
/**
76
* Postgres Interval is date and time based
@@ -84,6 +83,35 @@ public static Interval of(int years) {
8483
return new Interval(years);
8584
}
8685

86+
/**
87+
* Creates an instance from the given {@link Duration}.
88+
* <p>
89+
* The conversion algorithm assumes a year lasts 12 months and a month lasts 30 days, as <a href="https://github.com/postgres/postgres/blob/5bbdfa8a18dc56d3e64aa723a68e02e897cb5ec3/src/include/datatype/timestamp.h#L116">Postgres does</a> and ISO 8601 suggests.
90+
*
91+
* @param duration the value to convert
92+
* @return a new instance of {@link Interval}
93+
*/
94+
public static Interval of(Duration duration) {
95+
long totalSeconds = duration.getSeconds();
96+
97+
int years = (int) (totalSeconds / 31104000);
98+
long remainder = totalSeconds % 31104000;
99+
100+
int months = (int) (remainder / 2592000);
101+
remainder = totalSeconds % 2592000;
102+
103+
int days = (int) (remainder / 86400);
104+
remainder = remainder % 86400;
105+
106+
int hours = (int) (remainder / 3600);
107+
remainder = remainder % 3600;
108+
109+
int minutes = (int) (remainder / 60);
110+
remainder = remainder % 60;
111+
112+
return new Interval(years, months, days, hours, minutes, (int) remainder, duration.getNano() / 1000);
113+
}
114+
87115
public Interval years(int years) {
88116
this.years = years;
89117
return this;
@@ -203,7 +231,25 @@ public int hashCode() {
203231

204232
@Override
205233
public String toString() {
206-
return "Interval( " + years + " years " + months + " months " + days + " days " + hours + " hours " +
207-
minutes + " minutes " + seconds + (microseconds == 0 ? "" : "." + Math.abs(microseconds)) + " seconds )";
234+
return "Interval( "
235+
+ years + " years "
236+
+ months + " months "
237+
+ days + " days "
238+
+ hours + " hours "
239+
+ minutes + " minutes "
240+
+ seconds + " seconds "
241+
+ microseconds + " microseconds )";
242+
}
243+
244+
/**
245+
* Convert this interval to an instance of {@link Duration}.
246+
* <p>
247+
* The conversion algorithm assumes a year lasts 12 months and a month lasts 30 days, as <a href="https://github.com/postgres/postgres/blob/5bbdfa8a18dc56d3e64aa723a68e02e897cb5ec3/src/include/datatype/timestamp.h#L116">Postgres does</a> and ISO 8601 suggests.
248+
*
249+
* @return an instance of {@link Duration} representing the same amount of time as this interval
250+
*/
251+
public Duration toDuration() {
252+
return Duration.ofSeconds(((((years * 12L + months) * 30L + days) * 24L + hours) * 60 + minutes) * 60 + seconds)
253+
.plusNanos(microseconds * 1000L);
208254
}
209255
}

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252

5353
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
5454
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
55-
import static java.util.concurrent.TimeUnit.NANOSECONDS;
5655

5756
/**
5857
* @author <a href="mailto:[email protected]">Julien Viet</a>
@@ -1303,32 +1302,49 @@ private static Circle binaryDecodeCircle(int index, int len, ByteBuf buff) {
13031302
}
13041303

13051304
private static void binaryEncodeINTERVAL(Interval interval, ByteBuf buff) {
1306-
Duration duration = Duration
1307-
.ofHours(interval.getHours())
1308-
.plusMinutes(interval.getMinutes())
1309-
.plusSeconds(interval.getSeconds())
1310-
.plus(interval.getMicroseconds(), ChronoUnit.MICROS);
1311-
// days won't be changed
1312-
Period monthYear = Period.of(interval.getYears(), interval.getMonths(), interval.getDays()).normalized();
1313-
binaryEncodeINT8(NANOSECONDS.toMicros(duration.toNanos()), buff);
1314-
binaryEncodeINT4(monthYear.getDays(), buff);
1315-
binaryEncodeINT4((int) monthYear.toTotalMonths(), buff);
1305+
// We decompose the interval in 3 parts: months, seconds and micros
1306+
int monthsPart = Math.addExact(Math.multiplyExact(interval.getYears(), 12), interval.getMonths());
1307+
// A long is big enough to store the maximum/minimum value of the seconds part
1308+
long secondsPart = interval.getDays() * 24 * 3600L
1309+
+ interval.getHours() * 3600L
1310+
+ interval.getMinutes() * 60L
1311+
+ interval.getSeconds()
1312+
+ interval.getMicroseconds() / 1000000;
1313+
int microsPart = interval.getMicroseconds() % 1000000;
1314+
1315+
// The actual number of months is the sum of the months part and the number of months present in the seconds part
1316+
int months = Math.addExact(monthsPart, Math.toIntExact(secondsPart / 2592000));
1317+
// The actual number of days is computed from the remainder of the previous division
1318+
// It's necessarily smaller than or equal to 29
1319+
int days = (int) secondsPart % 2592000 / 86400;
1320+
// The actual number of micros is the sum of the micros part and the remainder of previous divisions
1321+
// The remainder of previous divisions is necessarily smaller than or equal to a day less a second
1322+
// The microseconds part is smaller than a second
1323+
// Therefore, their sum is necessarily smaller than a day
1324+
long micros = microsPart + secondsPart % 2592000 % 86400 * 1000000;
1325+
1326+
binaryEncodeINT8(micros, buff);
1327+
binaryEncodeINT4(days, buff);
1328+
binaryEncodeINT4(months, buff);
13161329
}
13171330

13181331
private static Interval binaryDecodeINTERVAL(int index, int len, ByteBuf buff) {
1319-
Duration duration = Duration.of(buff.getLong(index), ChronoUnit.MICROS);
1320-
final long hours = duration.toHours();
1321-
duration = duration.minusHours(hours);
1322-
final long minutes = duration.toMinutes();
1323-
duration = duration.minusMinutes(minutes);
1324-
final long seconds = NANOSECONDS.toSeconds(duration.toNanos());
1325-
duration = duration.minusSeconds(seconds);
1326-
final long microseconds = NANOSECONDS.toMicros(duration.toNanos());
1327-
int days = buff.getInt(index + 8);
1328-
int months = buff.getInt(index + 12);
1329-
Period monthYear = Period.of(0, months, days).normalized();
1330-
return new Interval(monthYear.getYears(), monthYear.getMonths(), monthYear.getDays(),
1331-
(int) hours, (int) minutes, (int) seconds, (int) microseconds);
1332+
long micros = buff.getLong(index);
1333+
long seconds = micros / 1000000;
1334+
micros -= seconds * 1000000;
1335+
long minutes = seconds / 60;
1336+
seconds -= minutes * 60;
1337+
long hours = minutes / 60;
1338+
minutes -= hours * 60;
1339+
long days = hours / 24;
1340+
hours -= days * 24;
1341+
days += buff.getInt(index + 8);
1342+
long months = days / 30;
1343+
days -= months * 30;
1344+
months += buff.getInt(index + 12);
1345+
long years = months / 12;
1346+
months -= years * 12;
1347+
return new Interval((int) years, (int) months, (int) days, (int) hours, (int) minutes, (int) seconds, (int) micros);
13321348
}
13331349

13341350
private static UUID binaryDecodeUUID(int index, int len, ByteBuf buff) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!--
2+
~ Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
3+
~
4+
~ This program and the accompanying materials are made available under the
5+
~ terms of the Eclipse Public License 2.0 which is available at
6+
~ http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
~ which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
~
9+
~ SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
-->
11+
12+
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1"
13+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14+
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1 http://maven.apache.org/xsd/assembly-1.1.1.xsd">
15+
<id>benchmarks</id>
16+
<formats>
17+
<format>jar</format>
18+
</formats>
19+
<includeBaseDirectory>false</includeBaseDirectory>
20+
<fileSets>
21+
<fileSet>
22+
<directory>${project.build.testOutputDirectory}</directory>
23+
<outputDirectory>/</outputDirectory>
24+
</fileSet>
25+
</fileSets>
26+
<dependencySets>
27+
<dependencySet>
28+
<outputDirectory>/</outputDirectory>
29+
<unpack>true</unpack>
30+
<scope>test</scope>
31+
</dependencySet>
32+
</dependencySets>
33+
</assembly>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.pgclient.benchmarks;
13+
14+
import io.vertx.pgclient.data.Interval;
15+
import org.openjdk.jmh.annotations.*;
16+
import org.openjdk.jmh.infra.Blackhole;
17+
18+
import java.io.IOException;
19+
import java.time.Duration;
20+
import java.time.Period;
21+
import java.util.concurrent.TimeUnit;
22+
23+
import static java.time.temporal.ChronoUnit.MICROS;
24+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
25+
26+
@Threads(1)
27+
@State(Scope.Thread)
28+
@BenchmarkMode(Mode.Throughput)
29+
@OutputTimeUnit(TimeUnit.MILLISECONDS)
30+
@Warmup(iterations = 20, time = 1, timeUnit = TimeUnit.SECONDS)
31+
@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS)
32+
@Fork(value = 3, jvmArgs = {"-Xms8g", "-Xmx8g", "-Xmn7g"})
33+
public class IntervalBenchmarks {
34+
35+
private Interval interval;
36+
37+
@Setup
38+
public void setup() throws IOException, InterruptedException {
39+
interval = new Interval(-2, 3, 15, -13, 2, -57, -426994);
40+
}
41+
42+
@Benchmark
43+
public void encodeWithDurationAndPeriod(Blackhole blackhole) {
44+
Duration duration = Duration
45+
.ofHours(interval.getHours())
46+
.plusMinutes(interval.getMinutes())
47+
.plusSeconds(interval.getSeconds())
48+
.plus(interval.getMicroseconds(), MICROS);
49+
// days won't be changed
50+
Period monthYear = Period.of(interval.getYears(), interval.getMonths(), interval.getDays()).normalized();
51+
52+
blackhole.consume(NANOSECONDS.toMicros(duration.toNanos()));
53+
blackhole.consume(monthYear.getDays());
54+
blackhole.consume((int) monthYear.toTotalMonths());
55+
}
56+
57+
@Benchmark
58+
public void encodeWithParts(Blackhole blackhole) {
59+
// We decompose the interval in 3 parts: months, seconds and micros
60+
int monthsPart = Math.addExact(Math.multiplyExact(interval.getYears(), 12), interval.getMonths());
61+
// A long is big enough to store the maximum/minimum value of the seconds part
62+
long secondsPart = interval.getDays() * 24 * 3600L
63+
+ interval.getHours() * 3600L
64+
+ interval.getMinutes() * 60L
65+
+ interval.getSeconds()
66+
+ interval.getMicroseconds() / 1000000;
67+
int microsPart = interval.getMicroseconds() % 1000000;
68+
69+
// The actual number of months is the sum of the months part and the number of months present in the seconds part
70+
int months = Math.addExact(monthsPart, Math.toIntExact(secondsPart / 2592000));
71+
// The actual number of days is computed from the remainder of the previous division
72+
// It's necessarily smaller than or equal to 29
73+
int days = (int) secondsPart % 2592000 / 86400;
74+
// The actual number of micros is the sum of the micros part and the remainder of previous divisions
75+
// The remainder of previous divisions is necessarily smaller than or equal to a day less a second
76+
// The microseconds part is smaller than a second
77+
// Therefore, their sum is necessarily smaller than a day
78+
long micros = microsPart + secondsPart % 2592000 % 86400 * 1000000;
79+
80+
blackhole.consume(micros);
81+
blackhole.consume(days);
82+
blackhole.consume(months);
83+
}
84+
}

vertx-pg-client/src/test/java/io/vertx/pgclient/data/DataTypeTestBase.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package io.vertx.pgclient.data;
22

3-
import io.vertx.pgclient.PgTestBase;
43
import io.vertx.core.Vertx;
54
import io.vertx.ext.unit.TestContext;
5+
import io.vertx.pgclient.PgTestBase;
66
import io.vertx.sqlclient.ColumnChecker;
77
import io.vertx.sqlclient.Row;
88
import io.vertx.sqlclient.Tuple;
@@ -34,7 +34,7 @@ public abstract class DataTypeTestBase extends PgTestBase {
3434
protected static final OffsetTime dt = OffsetTime.parse("17:55:04.90512+03:00");
3535
protected static final OffsetDateTime odt = OffsetDateTime.parse("2017-05-15T02:59:59.237666Z");
3636
protected static final Interval[] intervals = new Interval[] {
37-
Interval.of().years(10).months(3).days(332).hours(20).minutes(20).seconds(20).microseconds(999991),
37+
Interval.of().years(11).months(2).days(2).hours(20).minutes(20).seconds(20).microseconds(999991),
3838
Interval.of().minutes(20).seconds(20).microseconds(123456),
3939
Interval.of().years(-2).months(-6)
4040
};

vertx-pg-client/src/test/java/io/vertx/pgclient/data/DateTimeTypesExtendedCodecTest.java

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
package io.vertx.pgclient.data;
22

3+
import io.vertx.ext.unit.Async;
4+
import io.vertx.ext.unit.TestContext;
35
import io.vertx.pgclient.PgConnection;
46
import io.vertx.sqlclient.ColumnChecker;
57
import io.vertx.sqlclient.Row;
68
import io.vertx.sqlclient.Tuple;
7-
import io.vertx.ext.unit.Async;
8-
import io.vertx.ext.unit.TestContext;
99
import org.junit.Test;
1010

11-
import java.time.LocalDate;
12-
import java.time.LocalDateTime;
13-
import java.time.LocalTime;
14-
import java.time.OffsetDateTime;
15-
import java.time.OffsetTime;
11+
import java.time.*;
1612
import java.time.format.DateTimeFormatter;
1713

1814
public class DateTimeTypesExtendedCodecTest extends ExtendedQueryDataTypeCodecTestBase {
@@ -365,9 +361,9 @@ public void testEncodeTimestampTzAfterPgEpoch(TestContext ctx) {
365361
@Test
366362
public void testDecodeInterval(TestContext ctx) {
367363
Interval interval = Interval.of()
368-
.years(10)
369-
.months(3)
370-
.days(332)
364+
.years(11)
365+
.months(2)
366+
.days(2)
371367
.hours(20)
372368
.minutes(20)
373369
.seconds(20)
@@ -397,12 +393,12 @@ public void testEncodeInterval(TestContext ctx) {
397393
PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> {
398394
conn.prepare("UPDATE \"TemporalDataType\" SET \"Interval\" = $1 WHERE \"id\" = $2 RETURNING \"Interval\"",
399395
ctx.asyncAssertSuccess(p -> {
400-
// 2000 years 1 months 403 days 59 hours 35 minutes 13.999998 seconds
396+
// 2001 years 2 months 15 days 11 hours 35 minutes 13.999998 seconds
401397
Interval expected = Interval.of()
402-
.years(2000)
403-
.months(1)
404-
.days(403)
405-
.hours(59)
398+
.years(2001)
399+
.months(2)
400+
.days(15)
401+
.hours(11)
406402
.minutes(35)
407403
.seconds(13)
408404
.microseconds(999998);
@@ -568,7 +564,7 @@ public void testEncodeIntervalArray(TestContext ctx) {
568564
conn.prepare("UPDATE \"ArrayDataType\" SET \"Interval\" = $1 WHERE \"id\" = $2 RETURNING \"Interval\"",
569565
ctx.asyncAssertSuccess(p -> {
570566
Interval[] intervals = new Interval[]{
571-
Interval.of().years(10).months(3).days(332).hours(20).minutes(20).seconds(20).microseconds(999991),
567+
Interval.of().years(11).months(2).days(2).hours(20).minutes(20).seconds(20).microseconds(999991),
572568
Interval.of().minutes(20).seconds(20).microseconds(123456),
573569
Interval.of().years(-2).months(-6),
574570
Interval.of()

0 commit comments

Comments
 (0)