Skip to content

perf: TransactionStream.Transaction переразбирает транзакцию на каждое обращение, через промежуточную строку #95

Description

@Platonenkov

Что

TransactionStream.Transaction — expression-bodied свойство без кеширования:

[JsonIgnore]
public TransactionResponse Transaction => JsonSerializer.Deserialize<TransactionResponse>(
    (TransactionJson ?? Proposed).ToString(), XrplJsonOptions.Default);

Два независимых дефекта в одной строке.

Промежуточная строка. TransactionJson/Proposed объявлены как object, System.Text.Json кладёт туда самодостаточный JsonElement — то есть транзакция уже разобрана. .ToString() рендерит её обратно в UTF-16 строку, и сериализатор парсит эту строку заново. Это тот же антипаттерн, который убран из RequestManager.Resolve в #94.

Отсутствие кеширования. Свойство вычисляется на каждое обращение, поэтому потребитель, который прочитал tx.Transaction.TransactionType, а потом tx.Transaction.Hash, разобрал транзакцию дважды. Расход линеен по числу обращений, и ничто в сигнатуре свойства об этом не предупреждает.

Путь горячий: это событие OnTransaction, то есть каждая транзакция подписки transactions.

Замер

300 реальных сообщений стрима transactions с wss://s2.ripple.com, по 300 на каждую версию API, 5 прогонов, счётчик аллокаций потоковый. Тайминги на моей машине шумят, аллокации воспроизводятся точно.

api_version 1 (средний размер сообщения 3 631 байт):

КБ на сообщение мкс на сообщение
разбор самого стримового сообщения 11.50 ~250–300
stream.Transaction, 1 обращение 4.94 ~33–36
stream.Transaction, 3 обращения 14.82 ~70–107
то же из узла, без строки 3.53 ~23–36
разобрать один раз, прочитать трижды 3.53 ~30

api_version 2 (средний размер 3 537 байт, версия по умолчанию в SDK):

КБ на сообщение мкс на сообщение
разбор самого стримового сообщения 11.28 ~250
stream.Transaction, 1 обращение 3.96 ~23
stream.Transaction, 3 обращения 11.89 ~78
то же из узла, без строки 2.84 ~31
разобрать один раз, прочитать трижды 2.84 ~22

Читается так:

  • промежуточная строка стоит 1.41 КБ на обращение в v1 и 1.12 КБ в v2 — это 29 % и 28 % от стоимости обращения, и тратятся они ни на что;
  • 14.82 = 3 × 4.94 и 11.89 = 3 × 3.96 точно — прямое доказательство, что кеша нет вообще;
  • потребитель, трогающий свойство трижды за транзакцию, платит вчетверо против необходимого: 14.82 КБ там, где достаточно 3.53.

В моём прогоне Blazor-клиента поток шёл ~18 транзакций в секунду при одном обращении к свойству на транзакцию. В абсолютных числах это ~89 КБ/с — немного, но это чистые потери, и они растут линейно и от нагрузки, и от числа обращений в коде потребителя.

Что предлагается

  1. Разбирать прямо из узла: ((JsonElement)(TransactionJson ?? Proposed)).Deserialize<TransactionResponse>(XrplJsonOptions.Default), с сохранением текущей ветки через строку для случая, когда там не JsonElement (объект, собранный руками, а не принятый с провода).
  2. Разобрать один раз и запомнить. Свойство публичное и в публичном контракте выглядит как поле, поэтому ленивое поле за ним — наименее удивляющее поведение. Нужно решить вопрос потокобезопасности: экземпляр TransactionStream уходит в пользовательский колбэк, который вправе читать его из нескольких потоков.
  3. Заодно: при пустых обоих контейнерах (TransactionJson ?? Proposed).ToString() уронит NullReferenceException прямо из геттера свойства. Стоит вернуть null.

Стенд замера воспроизводимый: захват реальных сообщений с mainnet плюс прогон по ним, могу приложить к работе над задачей.

Рядом, но отдельно

При проверке всплыла асимметрия, которая к производительности не относится и в эту задачу не входит — фиксирую, чтобы не потерялась. Хеш транзакции доступен в обеих версиях API, но через разные свойства: в v1 тело лежит в transaction, а хеш внутри него, так что заполняется stream.Transaction.Hash, а stream.Hash пуст; в v2 тело переехало в tx_json, а хеш поднялся наверх, и всё наоборот. Проверено на mainnet в обе стороны. Демо-клиент Blazor-WebAssembly читает tx.Hash, при этом в appsettings.json просит "ApiVersion": 1, — поэтому в его консоли хеш всегда пустой, хотя по умолчанию SDK работает на v2, где это свойство рабочее. Заводить ли отдельную задачу на нормализацию Hash в модели — вопрос к владельцу публичного контракта.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions