Skip to content

Commit

Permalink
Pg Client: improve Interval support (#1462)
Browse files Browse the repository at this point in the history
* 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 <tsegismont@gmail.com>

* 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 <tsegismont@gmail.com>

* 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 <tsegismont@gmail.com>

* 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 <tsegismont@gmail.com>
  • Loading branch information
tsegismont committed Sep 16, 2024
1 parent 91235f9 commit 3b29a6a
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 55 deletions.
40 changes: 40 additions & 0 deletions vertx-pg-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,44 @@
</plugins>
</build>

<profiles>
<profile>
<id>benchmarks</id>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble-benchmarks</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>org.openjdk.jmh.Main</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/test/assembly/benchmarks.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>

</project>
54 changes: 50 additions & 4 deletions vertx-pg-client/src/main/java/io/vertx/pgclient/data/Interval.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.vertx.pgclient.data;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;
import java.time.Duration;

/**
* Postgres Interval is date and time based
Expand Down Expand Up @@ -84,6 +83,35 @@ public static Interval of(int years) {
return new Interval(years);
}

/**
* Creates an instance from the given {@link Duration}.
* <p>
* 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.
*
* @param duration the value to convert
* @return a new instance of {@link Interval}
*/
public static Interval of(Duration duration) {
long totalSeconds = duration.getSeconds();

int years = (int) (totalSeconds / 31104000);
long remainder = totalSeconds % 31104000;

int months = (int) (remainder / 2592000);
remainder = totalSeconds % 2592000;

int days = (int) (remainder / 86400);
remainder = remainder % 86400;

int hours = (int) (remainder / 3600);
remainder = remainder % 3600;

int minutes = (int) (remainder / 60);
remainder = remainder % 60;

return new Interval(years, months, days, hours, minutes, (int) remainder, duration.getNano() / 1000);
}

public Interval years(int years) {
this.years = years;
return this;
Expand Down Expand Up @@ -203,7 +231,25 @@ public int hashCode() {

@Override
public String toString() {
return "Interval( " + years + " years " + months + " months " + days + " days " + hours + " hours " +
minutes + " minutes " + seconds + (microseconds == 0 ? "" : "." + Math.abs(microseconds)) + " seconds )";
return "Interval( "
+ years + " years "
+ months + " months "
+ days + " days "
+ hours + " hours "
+ minutes + " minutes "
+ seconds + " seconds "
+ microseconds + " microseconds )";
}

/**
* Convert this interval to an instance of {@link Duration}.
* <p>
* 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.
*
* @return an instance of {@link Duration} representing the same amount of time as this interval
*/
public Duration toDuration() {
return Duration.ofSeconds(((((years * 12L + months) * 30L + days) * 24L + hours) * 60 + minutes) * 60 + seconds)
.plusNanos(microseconds * 1000L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@

import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
Expand Down Expand Up @@ -1305,32 +1304,49 @@ private static Circle binaryDecodeCircle(int index, int len, ByteBuf buff) {
}

private static void binaryEncodeINTERVAL(Interval interval, ByteBuf buff) {
Duration duration = Duration
.ofHours(interval.getHours())
.plusMinutes(interval.getMinutes())
.plusSeconds(interval.getSeconds())
.plus(interval.getMicroseconds(), ChronoUnit.MICROS);
// days won't be changed
Period monthYear = Period.of(interval.getYears(), interval.getMonths(), interval.getDays()).normalized();
binaryEncodeINT8(NANOSECONDS.toMicros(duration.toNanos()), buff);
binaryEncodeINT4(monthYear.getDays(), buff);
binaryEncodeINT4((int) monthYear.toTotalMonths(), buff);
// We decompose the interval in 3 parts: months, seconds and micros
int monthsPart = Math.addExact(Math.multiplyExact(interval.getYears(), 12), interval.getMonths());
// A long is big enough to store the maximum/minimum value of the seconds part
long secondsPart = interval.getDays() * 24 * 3600L
+ interval.getHours() * 3600L
+ interval.getMinutes() * 60L
+ interval.getSeconds()
+ interval.getMicroseconds() / 1000000;
int microsPart = interval.getMicroseconds() % 1000000;

// The actual number of months is the sum of the months part and the number of months present in the seconds part
int months = Math.addExact(monthsPart, Math.toIntExact(secondsPart / 2592000));
// The actual number of days is computed from the remainder of the previous division
// It's necessarily smaller than or equal to 29
int days = (int) secondsPart % 2592000 / 86400;
// The actual number of micros is the sum of the micros part and the remainder of previous divisions
// The remainder of previous divisions is necessarily smaller than or equal to a day less a second
// The microseconds part is smaller than a second
// Therefore, their sum is necessarily smaller than a day
long micros = microsPart + secondsPart % 2592000 % 86400 * 1000000;

binaryEncodeINT8(micros, buff);
binaryEncodeINT4(days, buff);
binaryEncodeINT4(months, buff);
}

private static Interval binaryDecodeINTERVAL(int index, int len, ByteBuf buff) {
Duration duration = Duration.of(buff.getLong(index), ChronoUnit.MICROS);
final long hours = duration.toHours();
duration = duration.minusHours(hours);
final long minutes = duration.toMinutes();
duration = duration.minusMinutes(minutes);
final long seconds = NANOSECONDS.toSeconds(duration.toNanos());
duration = duration.minusSeconds(seconds);
final long microseconds = NANOSECONDS.toMicros(duration.toNanos());
int days = buff.getInt(index + 8);
int months = buff.getInt(index + 12);
Period monthYear = Period.of(0, months, days).normalized();
return new Interval(monthYear.getYears(), monthYear.getMonths(), monthYear.getDays(),
(int) hours, (int) minutes, (int) seconds, (int) microseconds);
long micros = buff.getLong(index);
long seconds = micros / 1000000;
micros -= seconds * 1000000;
long minutes = seconds / 60;
seconds -= minutes * 60;
long hours = minutes / 60;
minutes -= hours * 60;
long days = hours / 24;
hours -= days * 24;
days += buff.getInt(index + 8);
long months = days / 30;
days -= months * 30;
months += buff.getInt(index + 12);
long years = months / 12;
months -= years * 12;
return new Interval((int) years, (int) months, (int) days, (int) hours, (int) minutes, (int) seconds, (int) micros);
}

private static UUID binaryDecodeUUID(int index, int len, ByteBuf buff) {
Expand Down
33 changes: 33 additions & 0 deletions vertx-pg-client/src/test/assembly/benchmarks.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!--
~ Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
~
~ This program and the accompanying materials are made available under the
~ terms of the Eclipse Public License 2.0 which is available at
~ http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
~ which is available at https://www.apache.org/licenses/LICENSE-2.0.
~
~ SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-->

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1 http://maven.apache.org/xsd/assembly-1.1.1.xsd">
<id>benchmarks</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.build.testOutputDirectory}</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>true</unpack>
<scope>test</scope>
</dependencySet>
</dependencySets>
</assembly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/

package io.vertx.pgclient.benchmarks;

import io.vertx.pgclient.data.Interval;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.io.IOException;
import java.time.Duration;
import java.time.Period;
import java.util.concurrent.TimeUnit;

import static java.time.temporal.ChronoUnit.MICROS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

@Threads(1)
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 20, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgs = {"-Xms8g", "-Xmx8g", "-Xmn7g"})
public class IntervalBenchmarks {

private Interval interval;

@Setup
public void setup() throws IOException, InterruptedException {
interval = new Interval(-2, 3, 15, -13, 2, -57, -426994);
}

@Benchmark
public void encodeWithDurationAndPeriod(Blackhole blackhole) {
Duration duration = Duration
.ofHours(interval.getHours())
.plusMinutes(interval.getMinutes())
.plusSeconds(interval.getSeconds())
.plus(interval.getMicroseconds(), MICROS);
// days won't be changed
Period monthYear = Period.of(interval.getYears(), interval.getMonths(), interval.getDays()).normalized();

blackhole.consume(NANOSECONDS.toMicros(duration.toNanos()));
blackhole.consume(monthYear.getDays());
blackhole.consume((int) monthYear.toTotalMonths());
}

@Benchmark
public void encodeWithParts(Blackhole blackhole) {
// We decompose the interval in 3 parts: months, seconds and micros
int monthsPart = Math.addExact(Math.multiplyExact(interval.getYears(), 12), interval.getMonths());
// A long is big enough to store the maximum/minimum value of the seconds part
long secondsPart = interval.getDays() * 24 * 3600L
+ interval.getHours() * 3600L
+ interval.getMinutes() * 60L
+ interval.getSeconds()
+ interval.getMicroseconds() / 1000000;
int microsPart = interval.getMicroseconds() % 1000000;

// The actual number of months is the sum of the months part and the number of months present in the seconds part
int months = Math.addExact(monthsPart, Math.toIntExact(secondsPart / 2592000));
// The actual number of days is computed from the remainder of the previous division
// It's necessarily smaller than or equal to 29
int days = (int) secondsPart % 2592000 / 86400;
// The actual number of micros is the sum of the micros part and the remainder of previous divisions
// The remainder of previous divisions is necessarily smaller than or equal to a day less a second
// The microseconds part is smaller than a second
// Therefore, their sum is necessarily smaller than a day
long micros = microsPart + secondsPart % 2592000 % 86400 * 1000000;

blackhole.consume(micros);
blackhole.consume(days);
blackhole.consume(months);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.vertx.pgclient.data;

import io.vertx.pgclient.PgTestBase;
import io.vertx.core.Vertx;
import io.vertx.ext.unit.TestContext;
import io.vertx.pgclient.PgTestBase;
import io.vertx.sqlclient.ColumnChecker;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
Expand Down Expand Up @@ -34,7 +34,7 @@ public abstract class DataTypeTestBase extends PgTestBase {
protected static final OffsetTime dt = OffsetTime.parse("17:55:04.90512+03:00");
protected static final OffsetDateTime odt = OffsetDateTime.parse("2017-05-15T02:59:59.237666Z");
protected static final Interval[] intervals = new Interval[] {
Interval.of().years(10).months(3).days(332).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().years(11).months(2).days(2).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().minutes(20).seconds(20).microseconds(123456),
Interval.of().years(-2).months(-6)
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package io.vertx.pgclient.data;

import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.pgclient.PgConnection;
import io.vertx.sqlclient.ColumnChecker;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import org.junit.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.*;
import java.time.format.DateTimeFormatter;

public class DateTimeTypesExtendedCodecTest extends ExtendedQueryDataTypeCodecTestBase {
Expand Down Expand Up @@ -371,9 +367,9 @@ public void testEncodeTimestampTzAfterPgEpoch(TestContext ctx) {
@Test
public void testDecodeInterval(TestContext ctx) {
Interval interval = Interval.of()
.years(10)
.months(3)
.days(332)
.years(11)
.months(2)
.days(2)
.hours(20)
.minutes(20)
.seconds(20)
Expand Down Expand Up @@ -406,12 +402,12 @@ public void testEncodeInterval(TestContext ctx) {
PgConnection.connect(vertx, options).onComplete(ctx.asyncAssertSuccess(conn -> {
conn.prepare("UPDATE \"TemporalDataType\" SET \"Interval\" = $1 WHERE \"id\" = $2 RETURNING \"Interval\"").onComplete(
ctx.asyncAssertSuccess(p -> {
// 2000 years 1 months 403 days 59 hours 35 minutes 13.999998 seconds
// 2001 years 2 months 15 days 11 hours 35 minutes 13.999998 seconds
Interval expected = Interval.of()
.years(2000)
.months(1)
.days(403)
.hours(59)
.years(2001)
.months(2)
.days(15)
.hours(11)
.minutes(35)
.seconds(13)
.microseconds(999998);
Expand Down Expand Up @@ -578,7 +574,7 @@ public void testEncodeIntervalArray(TestContext ctx) {
conn.prepare("UPDATE \"ArrayDataType\" SET \"Interval\" = $1 WHERE \"id\" = $2 RETURNING \"Interval\"").onComplete(
ctx.asyncAssertSuccess(p -> {
Interval[] intervals = new Interval[]{
Interval.of().years(10).months(3).days(332).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().years(11).months(2).days(2).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().minutes(20).seconds(20).microseconds(123456),
Interval.of().years(-2).months(-6),
Interval.of()
Expand Down
Loading

0 comments on commit 3b29a6a

Please sign in to comment.