Что
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 КБ/с — немного, но это чистые потери, и они растут линейно и от нагрузки, и от числа обращений в коде потребителя.
Что предлагается
- Разбирать прямо из узла:
((JsonElement)(TransactionJson ?? Proposed)).Deserialize<TransactionResponse>(XrplJsonOptions.Default), с сохранением текущей ветки через строку для случая, когда там не JsonElement (объект, собранный руками, а не принятый с провода).
- Разобрать один раз и запомнить. Свойство публичное и в публичном контракте выглядит как поле, поэтому ленивое поле за ним — наименее удивляющее поведение. Нужно решить вопрос потокобезопасности: экземпляр
TransactionStream уходит в пользовательский колбэк, который вправе читать его из нескольких потоков.
- Заодно: при пустых обоих контейнерах
(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 в модели — вопрос к владельцу публичного контракта.
Что
TransactionStream.Transaction— expression-bodied свойство без кеширования:Два независимых дефекта в одной строке.
Промежуточная строка.
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 байт):
stream.Transaction, 1 обращениеstream.Transaction, 3 обращенияapi_version 2 (средний размер 3 537 байт, версия по умолчанию в SDK):
stream.Transaction, 1 обращениеstream.Transaction, 3 обращенияЧитается так:
14.82 = 3 × 4.94и11.89 = 3 × 3.96точно — прямое доказательство, что кеша нет вообще;В моём прогоне Blazor-клиента поток шёл ~18 транзакций в секунду при одном обращении к свойству на транзакцию. В абсолютных числах это ~89 КБ/с — немного, но это чистые потери, и они растут линейно и от нагрузки, и от числа обращений в коде потребителя.
Что предлагается
((JsonElement)(TransactionJson ?? Proposed)).Deserialize<TransactionResponse>(XrplJsonOptions.Default), с сохранением текущей ветки через строку для случая, когда там неJsonElement(объект, собранный руками, а не принятый с провода).TransactionStreamуходит в пользовательский колбэк, который вправе читать его из нескольких потоков.(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в модели — вопрос к владельцу публичного контракта.