Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of initial load and page rendering #71

Merged
merged 9 commits into from
Jul 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ class CachingMarketplaceClient(
return loadHistoricPluginData(plugin, "salesInfo", cachedSalesInfo, delegate::salesInfo)
}

override suspend fun licenseInfo(plugin: PluginId): SalesWithLicensesInfo {
return loadCached("licenseInfo") {
val sales = salesInfo(plugin) // cached sales, not using delegate.licenseInfo because it would fetch sales again
SalesWithLicensesInfo(sales, LicenseInfo.createFrom(sales))
}
}

override suspend fun compatibleProducts(plugin: PluginId): List<JetBrainsProductId> {
return loadCached("compatibleProducts.$plugin") {
delegate.compatibleProducts(plugin)
Expand Down Expand Up @@ -163,7 +170,7 @@ class CachingMarketplaceClient(
} catch (e: ClientRequestException) {
val status = e.response.status.value
when {
status < 500 -> CacheItem(now, null,e)
status < 500 -> CacheItem(now, null, e)
else -> throw e
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,28 @@ object KtorHttpClientFactory {
bearerAuthKey: String? = null,
apiProtocol: URLProtocol = URLProtocol.HTTPS,
apiPort: Int = apiProtocol.defaultPort,
logLevel: ClientLogLevel = ClientLogLevel.Normal
logLevel: ClientLogLevel = ClientLogLevel.Normal,
enableHttpCaching: Boolean = true,
): HttpClient {
val url = URLBuilder()
url.protocol = apiProtocol
url.host = apiHost
url.port = apiPort

return createHttpClientByUrl(url.buildString(), bearerAuthKey, logLevel)
return createHttpClientByUrl(url.buildString(), bearerAuthKey, logLevel, enableHttpCaching)
}

fun createHttpClientByUrl(
apiUrl: String,
bearerAuthKey: String? = null,
logLevel: ClientLogLevel = ClientLogLevel.Normal
bearerAuthKey: String?,
logLevel: ClientLogLevel,
enableHttpCaching: Boolean,
): HttpClient {
return HttpClient(Java) {
install(Logging) {
level = logLevel.ktorLogLevel
}
install(Resources)
install(HttpCache)
install(ContentNegotiation) {
json(Json {
prettyPrint = true
Expand All @@ -53,6 +54,10 @@ object KtorHttpClientFactory {
})
}

if (enableHttpCaching) {
install(HttpCache)
}

install(DefaultRequest) {
url(apiUrl)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ class KtorMarketplaceClient(
private val apiHost: String = "plugins.jetbrains.com",
private val apiPath: String = "api",
logLevel: ClientLogLevel = ClientLogLevel.None,
enableHttpCaching: Boolean = true,
) : MarketplaceClient {
private val httpClient = KtorHttpClientFactory.createHttpClient(apiHost, apiKey, logLevel = logLevel)
private val httpClient = KtorHttpClientFactory.createHttpClient(
apiHost,
apiKey,
logLevel = logLevel,
enableHttpCaching = enableHttpCaching
)

override fun assetUrl(path: String): String {
return "https://$apiHost/${path.removePrefix("/")}"
Expand All @@ -43,6 +49,11 @@ class KtorMarketplaceClient(
return salesInfo(plugin, Marketplace.Birthday.rangeTo(YearMonthDay.now()))
}

override suspend fun licenseInfo(plugin: PluginId): SalesWithLicensesInfo {
val sales = salesInfo(plugin)
return SalesWithLicensesInfo(sales, LicenseInfo.createFrom(sales))
}

override suspend fun salesInfo(plugin: PluginId, range: YearMonthDayRange): List<PluginSale> {
// fetch the sales info year-by-year, because the API only allows a year or less as range
return range.stepSequence(years = 1)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
/*
* Copyright (c) 2023-2024 Joachim Ansorg.
* Copyright (c) 2024 Joachim Ansorg.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package dev.ja.marketplace.data
package dev.ja.marketplace.client

import dev.ja.marketplace.client.*
import javax.money.MonetaryAmount

typealias LicenseId = dev.ja.marketplace.client.LicenseId

/**
* Purchase of a single plugin license, identified by a unique ID.
*/
Expand Down Expand Up @@ -54,11 +51,13 @@ data class LicenseInfo(
}

companion object {
fun create(sales: List<PluginSale>): List<LicenseInfo> {
val licenses = mutableListOf<LicenseInfo>()
fun createFrom(sales: List<PluginSale>): List<LicenseInfo> {
val expectedSize = sales.sumOf { sale -> sale.lineItems.sumOf { lineItem -> lineItem.licenseIds.size } }
val licenses = ArrayList<LicenseInfo>(expectedSize)

sales.forEach { sale ->
sale.lineItems.forEach { lineItem ->
SplitAmount.split(lineItem.amount, lineItem.amountUSD, lineItem.licenseIds) { amount, amountUSD, license ->
MonetaryAmountSplitter.split(lineItem.amount, lineItem.amountUSD, lineItem.licenseIds) { amount, amountUSD, license ->
licenses += LicenseInfo(
license,
lineItem.subscriptionDates,
Expand All @@ -71,7 +70,8 @@ data class LicenseInfo(
}
}
licenses.sort()

return licenses
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ interface MarketplaceClient : MarketplaceUrlSupport {
*/
suspend fun salesInfo(plugin: PluginId): List<PluginSale>

/**
* @return All plugin sales returned by [salesInfo] split into the purchased licenses.
*/
suspend fun licenseInfo(plugin:PluginId): SalesWithLicensesInfo

/**
* @param range Date range of sales, inclusive
* @return plugin sales during the given range
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package dev.ja.marketplace.data
package dev.ja.marketplace.client

import org.javamoney.moneta.FastMoney
import java.math.BigDecimal
Expand All @@ -13,7 +13,7 @@ import javax.money.MonetaryAmount
/**
* Split an amount into parts without errors by rounding.
*/
object SplitAmount {
internal object MonetaryAmountSplitter {
fun <T> split(
total: MonetaryAmount,
totalUSD: MonetaryAmount,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright (c) 2024 Joachim Ansorg.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package dev.ja.marketplace.client

data class SalesWithLicensesInfo(val sales: List<PluginSale>, val licenses: List<LicenseInfo>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright (c) 2023-2024 Joachim Ansorg.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package dev.ja.marketplace.client

interface WithDateRange {
val dateRange: YearMonthDayRange
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.*
import org.javamoney.moneta.FastMoney
import org.javamoney.moneta.Money
import java.math.BigDecimal
import javax.money.MonetaryAmount

Expand Down Expand Up @@ -142,14 +141,21 @@ object PluginSaleSerializer : KSerializer<PluginSale> {
else -> error("Unexpected index: $index")
}
}
require(ref != null && date != null && amountValue != null && amountValueUSD != null && period != null && customer != null)
require(ref != null && date != null && amountValue != null && currency != null && amountValueUSD != null && period != null && customer != null)
require(lineItems != null)

val amountCurrencyUnit = MarketplaceCurrencies.of(currency)
val amountUSD = FastMoney.of(amountValueUSD, MarketplaceCurrencies.USD)
val amount = when {
currency == "USD" -> amountUSD
else -> FastMoney.of(amountValue, amountCurrencyUnit)
}

PluginSale(
ref,
date,
FastMoney.of(amountValue, currency),
FastMoney.of(amountValueUSD, "USD"),
amount,
amountUSD,
period,
customer,
reseller,
Expand All @@ -158,8 +164,8 @@ object PluginSaleSerializer : KSerializer<PluginSale> {
it.type,
it.licenseIds,
it.subscriptionDates,
FastMoney.of(it.amount, currency),
FastMoney.of(it.amountUSD, "USD"),
FastMoney.of(it.amount, amountCurrencyUnit),
FastMoney.of(it.amountUSD, MarketplaceCurrencies.USD),
it.discountDescriptions
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import org.javamoney.moneta.convert.ecb.ECBCurrentRateProvider
import org.javamoney.moneta.convert.ecb.ECBHistoricRateProvider
import org.javamoney.moneta.spi.CompoundRateProvider
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import java.time.LocalDate
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.money.CurrencyUnit
import javax.money.Monetary
import javax.money.MonetaryAmount
Expand All @@ -24,44 +24,58 @@ import javax.money.convert.*
* Prefetched exchange rates.
*/
class ExchangeRates(targetCurrencyCode: String) {
val targetCurrency: CurrencyUnit = Monetary.getCurrency(targetCurrencyCode)
private val composedRateProvider = CompoundRateProvider(
listOf<ExchangeRateProvider>(
ECBHistoricRateProvider(),
ECBCurrentRateProvider()
)
)

private data class CacheKey(val date: YearMonthDay, val baseCurrency: CurrencyUnit)

private val cachedExchangeRates = ConcurrentHashMap<CacheKey, ExchangeRate>()

private val historicRateProvider: ExchangeRateProvider = ECBHistoricRateProvider()
private val currentRateProvider: ExchangeRateProvider = ECBCurrentRateProvider()
private val composedRateProvider = CompoundRateProvider(listOf(historicRateProvider, currentRateProvider))
val targetCurrency: CurrencyUnit = Monetary.getCurrency(targetCurrencyCode)

fun convert(date: YearMonthDay, amount: MonetaryAmount): MonetaryAmount {
if (amount.currency == targetCurrency) {
return amount
}

val now = YearMonthDay.now()
val fixedDate = if (date > now) now else date
val exchangeRate = when {
date >= now -> getExchangeRateUncached(now, amount)
else -> cachedExchangeRates.getOrPut(CacheKey(now, amount.currency)) {
getExchangeRateUncached(now, amount)
}
}

return applyConversion(exchangeRate, amount, targetCurrency)
}

private fun getExchangeRateUncached(
fixedDate: YearMonthDay,
amount: MonetaryAmount
): ExchangeRate? {
val query = ConversionQueryBuilder
.of()
.setTermCurrency(targetCurrency)
.set(Array<LocalDate>::class.java, createLookupDates(fixedDate))
.build()
val conversion = composedRateProvider.getCurrencyConversion(query)
return conversion.applyConversion(amount, targetCurrency)
return composedRateProvider.getCurrencyConversion(query).getExchangeRate(amount)
}

// fixed apply method to make it work with FastMoney
private fun CurrencyConversion.applyConversion(amount: MonetaryAmount, termCurrency: CurrencyUnit): MonetaryAmount {
if (termCurrency == amount.currency) {
return amount
private fun applyConversion(rate: ExchangeRate?, amount: MonetaryAmount, termCurrency: CurrencyUnit): MonetaryAmount {
val baseCurrency = amount.currency
if (rate == null || baseCurrency != rate.baseCurrency) {
throw CurrencyConversionException(baseCurrency, termCurrency, null)
}

val rate: ExchangeRate = getExchangeRate(amount)
if (Objects.isNull(rate) || amount.currency != rate.baseCurrency) {
throw CurrencyConversionException(
amount.currency,
termCurrency, null
)

}
val multiplied = rate.factor.numberValue(BigDecimal::class.java)
.multiply(amount.number.numberValue(BigDecimal::class.java))
.setScale(5, RoundingMode.HALF_UP)
return FastMoney.of(multiplied, rate.currency)
// return FastMoney.of(MoneyUtils.getBigDecimal(multiplied), rate.currency)//.with(MonetaryOperators.rounding(5))
// return amount.factory.setCurrency(rate.currency).setNumber(multiplied).create().with(MonetaryOperators.rounding(5))
}

// requested date with several fallback to make up to weekends
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ package dev.ja.marketplace.services
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

typealias CountryIsoCode = String

@Serializable
data class Country(
@SerialName("iso")
val isoCode: String,
val isoCode: CountryIsoCode,
@SerialName("printableName")
val printableName: String,
@SerialName("localName")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ package dev.ja.marketplace.services
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

typealias CurrencyIsoCode = String

@Serializable
data class Currency(
@SerialName("iso")
val isoCode: String,
val isoCode: CurrencyIsoCode,
@SerialName("symbol")
val symbol: String,
@SerialName("prefixSymbol")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
package dev.ja.marketplace.churn

import dev.ja.marketplace.client.YearMonthDay
import dev.ja.marketplace.data.LicenseInfo
import dev.ja.marketplace.client.LicenseInfo

class LicenseChurnProcessor(
previouslyActiveMarkerDate: YearMonthDay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ abstract class MarketplaceIntChurnProcessor<T>(
}

override fun addPreviousPeriodItem(value: T) {
previousPeriodItems += getId(value)
previousPeriodItems.add(getId(value))
}

override fun addActiveItem(value: T) {
activeItems += getId(value)
activeItems.add(getId(value))
}

override fun addActiveUnacceptedItem(value: T) {
activeUnacceptedItems += getId(value)
activeUnacceptedItems.add(getId(value))
}

override fun churnedItemsCount(): Int {
Expand Down
Loading